Compare commits
54 commits
master
...
feature/gr
Author | SHA1 | Date | |
---|---|---|---|
cab417594c | |||
2abb0f7c49 | |||
036f35e1c0 | |||
![]() |
cec2d56338 | ||
0fdc66cd09 | |||
0dd428a43d | |||
35cb9ab6b2 | |||
1fa791829f | |||
ac891f6fcb | |||
5331780a83 | |||
386c618981 | |||
ebd371b43f | |||
dab1b69fb7 | |||
9c44419146 | |||
f306488b01 | |||
934ea0a905 | |||
d0386b475a | |||
e2aa534e76 | |||
![]() |
3c57ff9910 | ||
8cd4940215 | |||
![]() |
0dbc4c21e0 | ||
![]() |
9e1a68aac5 | ||
dac7766ee7 | |||
![]() |
0ab9f790fe | ||
![]() |
4bb36f6129 | ||
6d9f67d3bf | |||
![]() |
b456362a47 | ||
d3a949833d | |||
![]() |
8b988be06e | ||
6969af178c | |||
c389dc5581 | |||
c0bac128d7 | |||
e34a4aa15d | |||
72e2ae9290 | |||
cc84293480 | |||
0314a0f71e | |||
6bb4000d30 | |||
57ea318fcf | |||
8e2decc27e | |||
56587aa345 | |||
e044dbf19a | |||
2184fab678 | |||
068403f81f | |||
2da6b7395c | |||
529c932e33 | |||
fdb900a8a3 | |||
d2fc4ca84b | |||
5435ed5789 | |||
99f37ede03 | |||
127ae209b1 | |||
1fb1bec70e | |||
41718d128a | |||
2d81fb0143 | |||
f4218775ae |
14 changed files with 7370 additions and 51 deletions
|
@ -43,6 +43,8 @@ class GitHub(object):
|
|||
Returns an iterator over a resource, eg 'repos/divio/django-cms/watchers' that automatically handles
|
||||
pagination.
|
||||
"""
|
||||
if params is None:
|
||||
params = {}
|
||||
data, response = self._get(path, params)
|
||||
for thing in data:
|
||||
yield thing
|
||||
|
|
15
githubnetwork/middleware.py
Normal file
15
githubnetwork/middleware.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from django.utils.functional import SimpleLazyObject
|
||||
from githubnetwork.models import GHUser
|
||||
|
||||
|
||||
def get_github_user(request):
|
||||
if not request.user.is_authenticated():
|
||||
return None
|
||||
return GHUser.objects.get(user=request.user)
|
||||
|
||||
|
||||
class GithubUserMiddleware(object):
|
||||
def process_request(self, request):
|
||||
request.gh_user = SimpleLazyObject(lambda: get_github_user(request))
|
||||
|
|
@ -1,19 +1,13 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from collections import defaultdict
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpResponse, Http404, HttpResponseForbidden
|
||||
from django.shortcuts import render_to_response, redirect
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
from django.shortcuts import render_to_response
|
||||
from django.template.context import RequestContext
|
||||
from django.utils import simplejson
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.generic.base import TemplateView, TemplateResponseMixin
|
||||
from django.views.generic.detail import DetailView
|
||||
from django.views.generic.edit import CreateView, ModelFormMixin, FormView
|
||||
from django.views.generic.list import ListView
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from ghapi import api
|
||||
from models import GHUser, Repo
|
||||
from models import GHUser
|
||||
|
||||
|
||||
class Graph(object):
|
||||
|
@ -21,10 +15,10 @@ class Graph(object):
|
|||
self.nodes = set()
|
||||
self.edges = {}
|
||||
self.distances = {}
|
||||
|
||||
|
||||
def add_node(self, value):
|
||||
self.nodes.add(value)
|
||||
|
||||
|
||||
def add_edge(self, from_node, to_node, distance):
|
||||
self._add_edge(from_node, to_node, distance)
|
||||
self._add_edge(to_node, from_node, distance)
|
||||
|
@ -41,48 +35,77 @@ class NetworkView(DetailView):
|
|||
return self.user
|
||||
|
||||
def get_user_network(self):
|
||||
user = self.get_user()
|
||||
graph = []
|
||||
user = self.get_user()
|
||||
graph = []
|
||||
|
||||
def get_repo_network(self):
|
||||
user = self.get_user()
|
||||
graph = []
|
||||
for repo in api.get_iter('user/%s/repos' % user):
|
||||
# TODO: (Lynn) this is messy - clean up
|
||||
links = {}
|
||||
repo_info = api.get('repos/%s/%s' % (user, repo))
|
||||
parent = repo_info['parent']['owner']['login']
|
||||
child = repo_info['owner']['login']
|
||||
watchers = repo_info['parent']['watchers']
|
||||
network = repo_info['network_count']
|
||||
date_updated_parent = repo_info['parent']['updated_at']
|
||||
date_updated_child = repo_info['updated_at']
|
||||
links['source'] = parent
|
||||
links['target'] = child
|
||||
links['weight'] = {'watchers': watchers, 'network' : network,
|
||||
'date_updated_parent' : date_updated_parent,
|
||||
'date_updated_child' : date_updated_child }
|
||||
graph.append(links)
|
||||
return graph
|
||||
user = self.get_user()
|
||||
graph = []
|
||||
for repo in api.get_iter('user/%s/repos' % user):
|
||||
# TODO: (Lynn) this is messy - clean up
|
||||
links = {}
|
||||
repo_info = api.get('repos/%s/%s' % (user, repo))
|
||||
parent = repo_info['parent']['owner']['login']
|
||||
child = repo_info['owner']['login']
|
||||
watchers = repo_info['parent']['watchers']
|
||||
network = repo_info['network_count']
|
||||
date_updated_parent = repo_info['parent']['updated_at']
|
||||
date_updated_child = repo_info['updated_at']
|
||||
links['source'] = parent
|
||||
links['target'] = child
|
||||
links['weight'] = {'watchers': watchers, 'network': network,
|
||||
'date_updated_parent': date_updated_parent,
|
||||
'date_updated_child': date_updated_child}
|
||||
graph.append(links)
|
||||
return graph
|
||||
|
||||
def get_queryset(self):
|
||||
self.get_user()
|
||||
if repos:
|
||||
# do repo-y things
|
||||
self.get_repo_network()
|
||||
else:
|
||||
# do user-y things
|
||||
self.get_user_network()
|
||||
self.get_user()
|
||||
if repos:
|
||||
# do repo-y things
|
||||
self.get_repo_network()
|
||||
else:
|
||||
# do user-y things
|
||||
self.get_user_network()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
# TODO: (Lynn) figure out what's needed for context data
|
||||
context_object_name = self.get_context_object_name(queryset)
|
||||
|
||||
def get(self):
|
||||
self.object_list = self.get_queryset()
|
||||
self.object_list = self.get_queryset()
|
||||
context = self.get_context_data(object_list=self.object_list)
|
||||
return self.render_to_response(context)
|
||||
|
||||
@method_decorator(login_required)
|
||||
def dispatch(self, *args. **kwargs):
|
||||
return super(ProtectedView, self).dispatch(*args, **kwargs)
|
||||
def dispatch(self, *args, ** kwargs):
|
||||
return super(ProtectedView, self).dispatch(*args, **kwargs)
|
||||
|
||||
|
||||
def _sorted_repos(request):
|
||||
'''Get a list of repos for the currently authorized user, sort it, and
|
||||
return it.'''
|
||||
repos = [r for r in request.github.get_iter('users/%s/repos' %
|
||||
request.user.username)]
|
||||
repos.sort(key=lambda x: x['name'])
|
||||
return repos
|
||||
|
||||
|
||||
@login_required
|
||||
def me(request):
|
||||
context = RequestContext(request)
|
||||
context['followers'] = simplejson.dumps(
|
||||
[{'name': unicode(follower), 'avatar': follower.avatar_url}
|
||||
for follower in GHUser.objects.filter(following=request.gh_user)])
|
||||
context['repos'] = _sorted_repos(request)
|
||||
return render_to_response('me.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def get_user_followers(request):
|
||||
name = request.GET.get('user', None)
|
||||
if not name:
|
||||
raise HttpResponseBadRequest()
|
||||
names = simplejson.dumps([user['login']
|
||||
for user in request.github.get_iter('users/%s/followers' % name)])
|
||||
return HttpResponse(names, content_type='application/json')
|
||||
|
|
|
@ -43,6 +43,7 @@ MIDDLEWARE_CLASSES = [
|
|||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'ghapi.middleware.GithubAPIMiddleware',
|
||||
'githubnetwork.middleware.GithubUserMiddleware',
|
||||
]
|
||||
|
||||
TEMPLATE_CONTEXT_PROCESSORS = [
|
||||
|
@ -85,7 +86,7 @@ AUTHENTICATION_BACKENDS = [
|
|||
]
|
||||
|
||||
LOGIN_URL = '/login/'
|
||||
LOGIN_REDIRECT_URL = '/'
|
||||
LOGIN_REDIRECT_URL = '/me/'
|
||||
LOGIN_ERROR_URL = '/login/failed/'
|
||||
|
||||
|
||||
|
|
7034
static/d3/d3.v2.js
vendored
Normal file
7034
static/d3/d3.v2.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
4
static/d3/d3.v2.min.js
vendored
Normal file
4
static/d3/d3.v2.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
24
templates/graph_base.html
Normal file
24
templates/graph_base.html
Normal file
|
@ -0,0 +1,24 @@
|
|||
{# vim: set ft=htmldjango #}
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block head_css %}
|
||||
<style text="text/css">
|
||||
h1.placeholder {
|
||||
margin-top: 80px;
|
||||
text-align: center;
|
||||
color: #ccc;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block container %}
|
||||
{% include "navbar.html" %}
|
||||
{#<div class="container-fluid">#}
|
||||
{% block graph %}<h1 class="placeholder">PUT A GRAPH HERE</h1>{% endblock %}
|
||||
{#</div>#}
|
||||
{% endblock container %}
|
||||
|
||||
{% block body_js %}
|
||||
<script src="{{ STATIC_URL }}js/jquery.js"></script>
|
||||
<script src="{{ STATIC_URL }}bootstrap/js/bootstrap-dropdown.js"></script>
|
||||
{% endblock %}
|
2
templates/graph_followers.html
Normal file
2
templates/graph_followers.html
Normal file
|
@ -0,0 +1,2 @@
|
|||
{# vim: set ft=htmldjango #}
|
||||
{% extends "graph_base.html" %}
|
6
templates/graph_repo.html
Normal file
6
templates/graph_repo.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
{# vim: set ft=htmldjango #}
|
||||
{% extends "graph_base.html" %}
|
||||
|
||||
{% block graph %}
|
||||
<h1 class="placeholder">PUT A GRAPH OF<br/>{{ username }}'s<br/>{{ repo }} REPO<br/>HERE</h1>
|
||||
{% endblock %}
|
164
templates/me.html
Normal file
164
templates/me.html
Normal file
|
@ -0,0 +1,164 @@
|
|||
{% extends "graph_base.html" %}
|
||||
|
||||
{% block graph %}
|
||||
<h1>People following you</h1>
|
||||
<div id='chart'> </div>
|
||||
<style>
|
||||
#chart {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
circle.node {
|
||||
stroke: #fff;
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
|
||||
line.link {
|
||||
stroke: #999;
|
||||
stroke-opacity: .6;
|
||||
}
|
||||
</style>
|
||||
<script src="{{ STATIC_URL }}d3/d3.v2.js"></script>
|
||||
<script src="{{ STATIC_URL }}/js/jquery.js"></script>
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
|
||||
var followers = {{ followers|safe }};
|
||||
var map = {};
|
||||
|
||||
var w = $('#chart').width();
|
||||
h = $('#chart').height();
|
||||
var color = d3.scale.category20();
|
||||
|
||||
var nodeSize = 64;
|
||||
var borderSize = 3;
|
||||
var nodeCenter = nodeSize / 2;
|
||||
|
||||
/* Use a "flexible force-directed graph layout". */
|
||||
var force = d3.layout.force()
|
||||
.gravity(.05)
|
||||
.distance(250)
|
||||
.charge(-500)
|
||||
.size([w, h]);
|
||||
|
||||
var nodes = force.nodes(),
|
||||
links = force.links();
|
||||
|
||||
var vis = d3.select("#chart").append("svg:svg")
|
||||
.attr("width", w)
|
||||
.attr("height", h);
|
||||
|
||||
force.on("tick", function() {
|
||||
vis.selectAll("g.node")
|
||||
.attr("transform", function(d) {
|
||||
return "translate(" + d.x + "," + d.y + ")";
|
||||
});
|
||||
|
||||
vis.selectAll("line.link")
|
||||
.attr("x1", function(d) { return d.source.x; })
|
||||
.attr("y1", function(d) { return d.source.y; })
|
||||
.attr("x2", function(d) { return d.target.x; })
|
||||
.attr("y2", function(d) { return d.target.y; });
|
||||
});
|
||||
|
||||
function restart() {
|
||||
var link = vis.selectAll("line.link")
|
||||
.data(links, function(d) { return d.source.id + "-" + d.target.id; });
|
||||
|
||||
link.enter().insert("svg:line", "g.node")
|
||||
.attr("class", "link");
|
||||
|
||||
link.exit().remove();
|
||||
|
||||
var node = vis.selectAll("g.node")
|
||||
.data(nodes, function(d) { return d.id;});
|
||||
|
||||
var nodeEnter;
|
||||
nodeEnter = node.enter().append("svg:g")
|
||||
.attr('class', 'node')
|
||||
.call(force.drag);
|
||||
|
||||
// Node clip path
|
||||
nodeEnter.append("svg:clipPath")
|
||||
.attr("id", function(d) {return "clip-" + d.name;})
|
||||
.append("svg:circle").attr("r", nodeCenter + "px");
|
||||
|
||||
// Border
|
||||
nodeEnter.append("circle")
|
||||
.attr("r", (nodeCenter + borderSize) + "px")
|
||||
.attr("fill", "#2c2c2c");
|
||||
|
||||
// Node
|
||||
nodeEnter.append("svg:image")
|
||||
.attr("class", "circle")
|
||||
.attr("clip-path", function(d) {return "url(#clip-" + d.name + ")";})
|
||||
.attr("clip-rule", "nonzero")
|
||||
.attr("xlink:href", function(d) {return d.avatar;})
|
||||
.attr("x", -nodeCenter + "px")
|
||||
.attr("y", -nodeCenter + "px")
|
||||
.attr("width", nodeSize + "px")
|
||||
.attr("height", nodeSize + "px");
|
||||
|
||||
// Node title/tooltip
|
||||
nodeEnter.append("title")
|
||||
.text(function(d) { return d.name });
|
||||
node.exit().remove();
|
||||
|
||||
force.start();
|
||||
}
|
||||
|
||||
// Add three nodes and three links.
|
||||
function init() {
|
||||
var center = {"name": "{{ request.gh_user.gh_login }}", "avatar": "{{ request.gh_user.avatar_url }}"};
|
||||
nodes.push(center);
|
||||
for (var i = 0; i<followers.length;i++){
|
||||
nodes.push(followers[i]);
|
||||
links.push({source:center, target:followers[i]});
|
||||
map[followers[i].name] = followers[i];
|
||||
}
|
||||
restart();
|
||||
}
|
||||
|
||||
function addLink(link) {
|
||||
links.push(link);
|
||||
restart();
|
||||
}
|
||||
|
||||
restart();
|
||||
init();
|
||||
function loadFollowerFollowers(follower){
|
||||
$.getJSON('{% url get_user_followers %}?user=' + follower.name, function(data){
|
||||
for (var i = 0; i<data.length; i++){
|
||||
var target = map[data[i]];
|
||||
if (target){
|
||||
addLink({source:follower, target:target})
|
||||
}
|
||||
}
|
||||
});
|
||||
// addLink({source: followers[15], target: followers[5]});
|
||||
}
|
||||
for (var i = 0; i<followers.length; i++){
|
||||
loadFollowerFollowers(followers[i]);
|
||||
}
|
||||
|
||||
/* Install an event handler for window resizes */
|
||||
$(window).resize(function() {
|
||||
var chart = $('#chart');
|
||||
var svg = $('#chart svg');
|
||||
var w = chart.width();
|
||||
var h = chart.height();
|
||||
|
||||
force.size([w, h]);
|
||||
svg.width(w);
|
||||
svg.height(h);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
29
templates/navbar.html
Normal file
29
templates/navbar.html
Normal file
|
@ -0,0 +1,29 @@
|
|||
{# vim: set ft=htmldjango #}
|
||||
<div class="navbar navbar-fixed-top">
|
||||
<div class="navbar-inner">
|
||||
<div class="container-fluid">
|
||||
<a class="brand" href="">Cool name</a>
|
||||
<ul class="nav">
|
||||
<li><a href="{% url me %}">Me</a></li>
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">My Repos</a>
|
||||
<ul class="dropdown-menu">
|
||||
{% for r in repos %}
|
||||
<li><a href="#">{{ r.name }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="nav pull-right">
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
||||
{{ request.user.username }}
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="#">Logout</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
6
urls.py
6
urls.py
|
@ -11,7 +11,13 @@ admin.autodiscover()
|
|||
urlpatterns = patterns('',
|
||||
url(r'^%s(?P<path>.*)$' % re.escape(settings.STATIC_URL.lstrip('/')), 'django.contrib.staticfiles.views.serve', {'insecure': True}),
|
||||
url(r'^admin/', include(admin.site.urls)),
|
||||
url(r'^followers/', views.graph_followers,
|
||||
name='graph_followers'),
|
||||
url(r'^login/$', views.login, name='login'),
|
||||
url(r'^me/$', 'githubnetwork.views.me', name='me'),
|
||||
url(r'^~followers/$', 'githubnetwork.views.get_user_followers', name='get_user_followers'),
|
||||
url(r'^repo/(?P<user>\w+)/(?P<repo>\w+)/', views.graph_repo,
|
||||
name='graph_repo'),
|
||||
url(r'^$', views.index, name='index'),
|
||||
url(r'', include('social_auth.urls')),
|
||||
)
|
||||
|
|
23
views.py
23
views.py
|
@ -1,4 +1,5 @@
|
|||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import redirect, render_to_response
|
||||
from django.template import RequestContext
|
||||
|
||||
|
@ -7,12 +8,12 @@ def index(request):
|
|||
'''Index page. Everyone starts here. If the user is logged in (that is, they
|
||||
have a session id) return the follower_graph view. Otherwise, render the
|
||||
index page.'''
|
||||
if request.session.get('sessionid', False):
|
||||
return follower_graph(request)
|
||||
if request.user.is_authenticated():
|
||||
return redirect('graph_followers')
|
||||
# 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', RequestContext(request))
|
||||
return render_to_response('index.html', RequestContext(request))
|
||||
|
||||
|
||||
def login(request):
|
||||
|
@ -21,13 +22,21 @@ def login(request):
|
|||
# Make sure the user can accept cookies.
|
||||
if request.session.test_cookie_worked():
|
||||
request.session.delete_test_cookie()
|
||||
return redirect('/login/github/')
|
||||
return redirect('socialauth_begin', backend='github')
|
||||
else:
|
||||
# During development, I've landed here a lot, despite having cookies
|
||||
# enabled. So, set the test cookie so that trying to login from here
|
||||
# actually works.
|
||||
request.session.set_test_cookie()
|
||||
# Render an error -- fix your damn cookies!
|
||||
return render_to_response('login.html',
|
||||
return render_to_response('index.html',
|
||||
{ 'error': "Fix your damn cookies!" })
|
||||
|
||||
|
||||
@login_required
|
||||
def follower_graph(request):
|
||||
return 'Hello!'
|
||||
def graph_repo(request, user=None, repo=None):
|
||||
return render_to_response('graph_repo.html', {
|
||||
'graph_user': user,
|
||||
'graph_repo': repo,
|
||||
'repos': _sorted_repos(request)
|
||||
}, RequestContext(request))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue