@@ -1,7 +1,10 @@ | |||
from annoying.functions import get_object_or_None | |||
from datetime import datetime | |||
from django.contrib.auth.models import User | |||
from django.db import models | |||
from itertools import chain | |||
from snipts.models import Snipt | |||
from teams.models import Team | |||
class UserProfile(models.Model): | |||
@@ -110,6 +113,14 @@ class UserProfile(models.Model): | |||
public=True).count() > 0 \ | |||
else False | |||
def has_team(self): | |||
return True if get_object_or_None(Team, user=self.user) else False | |||
def teams(self): | |||
teams_owned = Team.objects.filter(owner=self.user) | |||
teams_in = Team.objects.filter(members=self.user) | |||
return list(chain(teams_owned, teams_in)) | |||
def get_account_age(self): | |||
delta = datetime.now().replace(tzinfo=None) - \ | |||
self.user.date_joined.replace(tzinfo=None) | |||
@@ -209,6 +209,57 @@ header.main { | |||
float: right; | |||
margin-right: 13px; | |||
} | |||
&.teams-nav { | |||
position: relative; | |||
ul { | |||
background: transparent url('../img/aside-nav-open-bottom-bg.gif') top left repeat; | |||
display: none; | |||
left: -5px; | |||
padding: 10px 0; | |||
position: absolute; | |||
top: 48px; | |||
width: 189px; | |||
@include multi-border-radius(0, 0, 10px, 10px); | |||
li { | |||
float: none; | |||
list-style-type: none; | |||
a { | |||
border: none; | |||
color: #B0D7DD; | |||
display: block; | |||
float: none; | |||
font: bold 12px $Helvetica; | |||
margin: 0; | |||
overflow: hidden; | |||
padding: 7px 20px 7px 20px; | |||
text-overflow: ellipsis; | |||
white-space: nowrap; | |||
&:hover { | |||
background: rgba(#103A42, .5); | |||
text-decoration: none; | |||
} | |||
i { | |||
margin-right: 9px; | |||
opacity: .3; | |||
} | |||
} | |||
} | |||
} | |||
&.open { | |||
> a { | |||
background: #406064; | |||
border-radius: 5px; | |||
} | |||
> ul { | |||
display: block; | |||
} | |||
} | |||
} | |||
} | |||
} | |||
} | |||
@@ -42,6 +42,7 @@ | |||
this.$html_body = this.$body.add(this.$html); | |||
this.$aside_main = $('aside.main', this.$body); | |||
this.$aside_nav = $('aside.nav', this.$body); | |||
this.$teams_nav = $('li.teams-nav', this.$body); | |||
this.$aside_nav_ul = $('ul', this.$aside_nav); | |||
this.$search_form = $('form.search', this.$body); | |||
this.$search_query = $('input#search-query', this.$body); | |||
@@ -69,6 +70,7 @@ | |||
window.from_modal = false; | |||
} | |||
that.$aside_nav.removeClass('open'); | |||
that.$teams_nav.removeClass('open'); | |||
}); | |||
this.$aside_nav_ul.click(function(e) { | |||
@@ -190,7 +192,8 @@ | |||
}, | |||
events: { | |||
'showKeyboardShortcuts': 'showKeyboardShortcuts', | |||
'click a.mini-profile': 'toggleMiniProfile' | |||
'click a.mini-profile': 'toggleMiniProfile', | |||
'click a.teams-nav': 'toggleTeamsNav' | |||
}, | |||
keyboardShortcuts: function() { | |||
@@ -237,6 +240,10 @@ | |||
this.$aside_nav.toggleClass('open'); | |||
return false; | |||
}, | |||
toggleTeamsNav: function(e) { | |||
this.$teams_nav.toggleClass('open'); | |||
return false; | |||
}, | |||
inFieldLabels: function () { | |||
$('div.infield label', this.$body).inFieldLabels({ | |||
fadeDuration: 200 | |||
@@ -5,6 +5,7 @@ Django==1.8.3 | |||
django-annoying==0.8.3 | |||
django-bcrypt==0.9.2 | |||
django-debug-toolbar==1.3.2 | |||
django-extensions==1.5.7 | |||
django-haystack==2.4.0 | |||
django-markdown-deux==1.0.5 | |||
django-pagination==1.0.7 | |||
@@ -97,6 +97,7 @@ INSTALLED_APPS = ( | |||
'django.contrib.sessions', | |||
'django.contrib.sites', | |||
'django.contrib.staticfiles', | |||
'django_extensions', | |||
'gunicorn', | |||
'haystack', | |||
'markdown_deux', | |||
@@ -0,0 +1,38 @@ | |||
/** | |||
* @fileOverview CSS for jquery-autocomplete, the jQuery Autocompleter | |||
* @author <a href="mailto:dylan@dyve.net">Dylan Verheul</a> | |||
* @license MIT | GPL | Apache 2.0, see LICENSE.txt | |||
* @see https://github.com/dyve/jquery-autocomplete | |||
*/ | |||
.acResults { | |||
padding: 0px; | |||
border: 1px solid WindowFrame; | |||
background-color: Window; | |||
overflow: hidden; | |||
} | |||
.acResults ul { | |||
margin: 0px; | |||
padding: 0px; | |||
list-style-position: outside; | |||
list-style: none; | |||
} | |||
.acResults ul li { | |||
margin: 0px; | |||
padding: 2px 5px; | |||
cursor: pointer; | |||
display: block; | |||
font: menu; | |||
font-size: 12px; | |||
overflow: hidden; | |||
} | |||
.acLoading { | |||
background : url('../img/indicator.gif') right center no-repeat; | |||
} | |||
.acSelect { | |||
background-color: Highlight; | |||
color: HighlightText; | |||
} |
@@ -0,0 +1,119 @@ | |||
/** | |||
* Ajax Queue Plugin | |||
* | |||
* Homepage: http://jquery.com/plugins/project/ajaxqueue | |||
* Documentation: http://docs.jquery.com/AjaxQueue | |||
*/ | |||
/** | |||
<script> | |||
$(function(){ | |||
jQuery.ajaxQueue({ | |||
url: "test.php", | |||
success: function(html){ jQuery("ul").append(html); } | |||
}); | |||
jQuery.ajaxQueue({ | |||
url: "test.php", | |||
success: function(html){ jQuery("ul").append(html); } | |||
}); | |||
jQuery.ajaxSync({ | |||
url: "test.php", | |||
success: function(html){ jQuery("ul").append("<b>"+html+"</b>"); } | |||
}); | |||
jQuery.ajaxSync({ | |||
url: "test.php", | |||
success: function(html){ jQuery("ul").append("<b>"+html+"</b>"); } | |||
}); | |||
}); | |||
</script> | |||
<ul style="position: absolute; top: 5px; right: 5px;"></ul> | |||
*/ | |||
/* | |||
* Queued Ajax requests. | |||
* A new Ajax request won't be started until the previous queued | |||
* request has finished. | |||
*/ | |||
/* | |||
* Synced Ajax requests. | |||
* The Ajax request will happen as soon as you call this method, but | |||
* the callbacks (success/error/complete) won't fire until all previous | |||
* synced requests have been completed. | |||
*/ | |||
(function(jQuery) { | |||
var ajax = jQuery.ajax; | |||
var pendingRequests = {}; | |||
var synced = []; | |||
var syncedData = []; | |||
jQuery.ajax = function(settings) { | |||
// create settings for compatibility with ajaxSetup | |||
settings = jQuery.extend(settings, jQuery.extend({}, jQuery.ajaxSettings, settings)); | |||
var port = settings.port; | |||
switch(settings.mode) { | |||
case "abort": | |||
if ( pendingRequests[port] ) { | |||
pendingRequests[port].abort(); | |||
} | |||
return pendingRequests[port] = ajax.apply(this, arguments); | |||
case "queue": | |||
var _old = settings.complete; | |||
settings.complete = function(){ | |||
if ( _old ) | |||
_old.apply( this, arguments ); | |||
jQuery([ajax]).dequeue("ajax" + port );; | |||
}; | |||
jQuery([ ajax ]).queue("ajax" + port, function(){ | |||
ajax( settings ); | |||
}); | |||
return; | |||
case "sync": | |||
var pos = synced.length; | |||
synced[ pos ] = { | |||
error: settings.error, | |||
success: settings.success, | |||
complete: settings.complete, | |||
done: false | |||
}; | |||
syncedData[ pos ] = { | |||
error: [], | |||
success: [], | |||
complete: [] | |||
}; | |||
settings.error = function(){ syncedData[ pos ].error = arguments; }; | |||
settings.success = function(){ syncedData[ pos ].success = arguments; }; | |||
settings.complete = function(){ | |||
syncedData[ pos ].complete = arguments; | |||
synced[ pos ].done = true; | |||
if ( pos == 0 || !synced[ pos-1 ] ) | |||
for ( var i = pos; i < synced.length && synced[i].done; i++ ) { | |||
if ( synced[i].error ) synced[i].error.apply( jQuery, syncedData[i].error ); | |||
if ( synced[i].success ) synced[i].success.apply( jQuery, syncedData[i].success ); | |||
if ( synced[i].complete ) synced[i].complete.apply( jQuery, syncedData[i].complete ); | |||
synced[i] = null; | |||
syncedData[i] = null; | |||
} | |||
}; | |||
} | |||
return ajax.apply(this, arguments); | |||
}; | |||
})((typeof window.jQuery == 'undefined' && typeof window.django != 'undefined') | |||
? django.jQuery | |||
: jQuery | |||
); |
@@ -0,0 +1,10 @@ | |||
/* Copyright (c) 2006 Brandon Aaron (http://brandonaaron.net) | |||
* Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) | |||
* and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. | |||
* | |||
* $LastChangedDate: 2007-07-22 01:45:56 +0200 (Son, 22 Jul 2007) $ | |||
* $Rev: 2447 $ | |||
* | |||
* Version 2.1.1 | |||
*/ | |||
(function($){$.fn.bgIframe=$.fn.bgiframe=function(s){if($.browser.msie&&/6.0/.test(navigator.userAgent)){s=$.extend({top:'auto',left:'auto',width:'auto',height:'auto',opacity:true,src:'javascript:false;'},s||{});var prop=function(n){return n&&n.constructor==Number?n+'px':n;},html='<iframe class="bgiframe"frameborder="0"tabindex="-1"src="'+s.src+'"'+'style="display:block;position:absolute;z-index:-1;'+(s.opacity!==false?'filter:Alpha(Opacity=\'0\');':'')+'top:'+(s.top=='auto'?'expression(((parseInt(this.parentNode.currentStyle.borderTopWidth)||0)*-1)+\'px\')':prop(s.top))+';'+'left:'+(s.left=='auto'?'expression(((parseInt(this.parentNode.currentStyle.borderLeftWidth)||0)*-1)+\'px\')':prop(s.left))+';'+'width:'+(s.width=='auto'?'expression(this.parentNode.offsetWidth+\'px\')':prop(s.width))+';'+'height:'+(s.height=='auto'?'expression(this.parentNode.offsetHeight+\'px\')':prop(s.height))+';'+'"/>';return this.each(function(){if($('> iframe.bgiframe',this).length==0)this.insertBefore(document.createElement(html),this.firstChild);});}return this;};})((typeof window.jQuery=='undefined' && typeof window.django!='undefined')? django.jQuery : jQuery); |
@@ -0,0 +1,20 @@ | |||
# -*- coding: utf-8 -*- | |||
from __future__ import unicode_literals | |||
from django.db import models, migrations | |||
from django.conf import settings | |||
class Migration(migrations.Migration): | |||
dependencies = [ | |||
('teams', '0003_auto_20150818_0057'), | |||
] | |||
operations = [ | |||
migrations.AlterField( | |||
model_name='team', | |||
name='members', | |||
field=models.ManyToManyField(related_name='member', to=settings.AUTH_USER_MODEL, blank=True), | |||
), | |||
] |
@@ -6,10 +6,10 @@ from snipts.utils import slugify_uniquely | |||
class Team(models.Model): | |||
user = models.OneToOneField(User, blank=True, null=True) | |||
owner = models.ForeignKey(User, related_name='owner') | |||
name = models.CharField(max_length=255) | |||
name = models.CharField(max_length=30) | |||
email = models.EmailField(max_length=255) | |||
slug = models.SlugField(max_length=255, blank=True) | |||
members = models.ManyToManyField(User, related_name='member') | |||
members = models.ManyToManyField(User, related_name='member', blank=True) | |||
created = models.DateTimeField(auto_now_add=True, editable=False) | |||
modified = models.DateTimeField(auto_now=True, editable=False) | |||
@@ -18,22 +18,34 @@ | |||
<li>Members can create public or private snipts on a team.</li> | |||
<li>Public team posts are public to the world, as they are now.</li> | |||
<li>Private team posts are editable by all team members.</li> | |||
<li>All team members are automatically granted personal <span class="pro">Pro</span> accounts.</li> | |||
<li>Plans starting at $49/month.</li> | |||
<li>Plans from $49/month, all with a 7-day free trial.</li> | |||
</ul> | |||
</div> | |||
<div class="control-group"> | |||
<label class="control-label" for="name">Team name:</label> | |||
<label class="control-label" for="name">Team username:</label> | |||
<div class="controls"> | |||
<input required type="text" class="input-xlarge" name="name" id="name" /> | |||
<input maxlength="30" required type="text" class="input-xlarge" name="name" id="name" /> | |||
<p class="sub" style="margin-top: 3px; color: #999999;"> | |||
Maximum of 30 characters. | |||
</p> | |||
</div> | |||
</div> | |||
<div class="control-group"> | |||
<label class="control-label" for="name">Team email:</label> | |||
<div class="controls"> | |||
<input required type="text" class="input-xlarge" name="email" id="email" /> | |||
<p class="sub" style="margin-top: 3px; color: #999999;"> | |||
For billing and your team's Gravatar. Will remain private. | |||
</p> | |||
</div> | |||
</div> | |||
<div class="control-group"> | |||
<div class="form-actions"> | |||
{% csrf_token %} | |||
<button type="submit" class="btn btn-success">Request more info »</button> | |||
<button type="submit" class="btn btn-success">Create team »</button> | |||
</div> | |||
</fieldset> | |||
</form> | |||
{% endif %} | |||
{% endblock %} | |||
{% block analytics %} | |||
@@ -0,0 +1,20 @@ | |||
{% extends "base.html" %} | |||
{% block page-title %}Team Members{% endblock %} | |||
{% block body-class %}{{ block.super }}{% endblock %} | |||
{% block breadcrumb %} | |||
<li><a href="/{{ team.user.username }}/">{{ team.user.username }}</a></li> | |||
<span class="prompt">/</span> <li><a href="/{{ team.user.username }}/members/">Members</a></li> | |||
{% endblock %} | |||
{% block content %} | |||
{% endblock %} | |||
{% block analytics %} | |||
{% if not debug %} | |||
window.ll('tagScreen', 'Team members view'); | |||
{% endif %} | |||
{% endblock %} |
@@ -0,0 +1,11 @@ | |||
from django.conf.urls import * | |||
from teams import views | |||
urlpatterns = \ | |||
patterns('', | |||
url(r'^for-teams/$', views.for_teams), | |||
url(r'^for-teams/complete/$', views.for_teams_complete), | |||
url(r'^(?P<username>[^/]+)/members/$', | |||
views.team_members, | |||
name='team-members')) |
@@ -1,6 +1,9 @@ | |||
import uuid | |||
from annoying.decorators import render_to | |||
from django.contrib.auth.models import User | |||
from django.http import HttpResponseBadRequest | |||
from django.shortcuts import get_object_or_404 | |||
from teams.models import Team | |||
@@ -13,22 +16,32 @@ def for_teams(request): | |||
return {} | |||
@render_to('teams/team-members.html') | |||
def team_members(request, username): | |||
team = get_object_or_404(Team, slug=username) | |||
return { | |||
'team': team | |||
} | |||
@render_to('teams/for-teams-complete.html') | |||
def for_teams_complete(request): | |||
if request.method == 'POST' and request.user.is_authenticated(): | |||
team = Team(name=request.POST['name'], | |||
email='nick@snipt.net', | |||
email=request.POST['email'], | |||
owner=request.user) | |||
team.save() | |||
user = User.objects.create_user(team.slug, team.email, 'password') | |||
user = User.objects.create_user(team.slug, | |||
team.email, | |||
str(uuid.uuid4())) | |||
team.user = user | |||
team.save() | |||
return { | |||
'team': team | |||
} | |||
else: | |||
return HttpResponseBadRequest() |
@@ -106,8 +106,24 @@ | |||
</li> | |||
{% block add-snipt %}{% endblock %} | |||
{% endif %} | |||
<li> | |||
<a href="/for-teams/" {% if '/for-teams/' in request.path %} class="active"{% endif %}>Teams</a> | |||
<li class="teams-nav"> | |||
<a href="#" class="teams-nav {% if '/for-teams/' in request.path %}active{% endif %}">Teams</a> | |||
<ul> | |||
{% for team in request.user.profile.teams %} | |||
<li> | |||
<a href="/{{ team.user.username }}/"> | |||
<i class="icon-user icon-white"></i> | |||
{{ team.user.username }} | |||
</a> | |||
</li> | |||
{% endfor %} | |||
<li> | |||
<a href="/for-teams/"> | |||
<i class="icon-plus icon-white"></i> | |||
Create new team | |||
</a> | |||
</li> | |||
</ul> | |||
</li> | |||
</ul> | |||
</nav> | |||
@@ -1,33 +1,58 @@ | |||
{% load snipt_tags %} | |||
<div class="profile group"> | |||
<a href="/{{ user.username }}/"> | |||
<img src="https://secure.gravatar.com/avatar/{{ user.email|md5 }}?s=300" alt="{{ user.username }}" title="{{ user.username }}" /> | |||
</a> | |||
<div class="meta"> | |||
<div class="username" title="{{ user.username }}"> | |||
<a href=""> | |||
{{ user.username }} | |||
</a> | |||
</div> | |||
{% if user.username == 'nick' %} | |||
<div class="member-since">Snipt Founder in {{ user.date_joined|date:"Y" }}</div> | |||
{% else %} | |||
<div class="member-since">Member since {{ user.date_joined|date:"Y" }}</div> | |||
{% endif %} | |||
{% if user.profile.get_blog_posts %} | |||
<div class="urls"> | |||
Snipt Blog: | |||
<a href="{{ user.profile.get_user_profile_url }}"> | |||
{{ user.profile.get_user_profile_url }} | |||
</a> | |||
</div> | |||
{% endif %} | |||
</div> | |||
{% if user.profile.is_pro %} | |||
<a class="pro" href="/pro/">Pro</a> | |||
{% endif %} | |||
{% if user.profile.gittip_username %} | |||
<a class="gittip" href="https://www.gittip.com/{{ user.profile.gittip_username }}/">Tip</a> | |||
{% endif %} | |||
</div> | |||
{% if user.profile.has_team %} | |||
<div class="profile group"> | |||
<a href="/{{ user.username }}/"> | |||
<img src="https://secure.gravatar.com/avatar/{{ user.email|md5 }}?s=300" alt="{{ user.username }}" title="{{ user.username }}" /> | |||
</a> | |||
<div class="meta"> | |||
<div class="username" title="{{ user.username }}"> | |||
<a href=""> | |||
{{ user.username }} | |||
</a> | |||
</div> | |||
<div class="member-since">Team since {{ user.date_joined|date:"Y" }}</div> | |||
{% if user.profile.get_blog_posts %} | |||
<div class="urls"> | |||
Snipt Blog: | |||
<a href="{{ user.profile.get_user_profile_url }}"> | |||
{{ user.profile.get_user_profile_url }} | |||
</a> | |||
</div> | |||
{% endif %} | |||
<div class="member-since">{{ user.team.members.all|length }} member{{ user.team.members.all|pluralize }}</div> | |||
</div> | |||
</div> | |||
{% else %} | |||
<div class="profile group"> | |||
<a href="/{{ user.username }}/"> | |||
<img src="https://secure.gravatar.com/avatar/{{ user.email|md5 }}?s=300" alt="{{ user.username }}" title="{{ user.username }}" /> | |||
</a> | |||
<div class="meta"> | |||
<div class="username" title="{{ user.username }}"> | |||
<a href=""> | |||
{{ user.username }} | |||
</a> | |||
</div> | |||
{% if user.username == 'nick' %} | |||
<div class="member-since">Snipt Founder in {{ user.date_joined|date:"Y" }}</div> | |||
{% else %} | |||
<div class="member-since">Member since {{ user.date_joined|date:"Y" }}</div> | |||
{% endif %} | |||
{% if user.profile.get_blog_posts %} | |||
<div class="urls"> | |||
Snipt Blog: | |||
<a href="{{ user.profile.get_user_profile_url }}"> | |||
{{ user.profile.get_user_profile_url }} | |||
</a> | |||
</div> | |||
{% endif %} | |||
</div> | |||
{% if user.profile.is_pro %} | |||
<a class="pro" href="/pro/">Pro</a> | |||
{% endif %} | |||
{% if user.profile.gittip_username %} | |||
<a class="gittip" href="https://www.gittip.com/{{ user.profile.gittip_username }}/">Tip</a> | |||
{% endif %} | |||
</div> | |||
{% endif %} |
@@ -10,7 +10,6 @@ from snipts.api import (PublicSniptResource, | |||
PrivateUserResource, PublicTagResource) | |||
from snipts.views import search | |||
from tastypie.api import Api | |||
from teams.views import for_teams, for_teams_complete | |||
from utils.views import SniptRegistrationView | |||
from views import (homepage, lexers, login_redirect, pro, sitemap, tags, | |||
pro_complete, user_api_key) | |||
@@ -47,9 +46,6 @@ urlpatterns = \ | |||
url(r'^pro/$', pro), | |||
url(r'^pro/complete/$', pro_complete), | |||
url(r'^for-teams/$', for_teams), | |||
url(r'^for-teams/complete/$', for_teams_complete), | |||
url(r'^account/', include('accounts.urls')), | |||
url(r'^api/public/lexer/$', lexers), | |||
@@ -65,6 +61,7 @@ urlpatterns = \ | |||
name='registration_register'), | |||
url(r'', include('registration.backends.default.urls')), | |||
url(r'^', include('teams.urls')), | |||
url(r'^', include('snipts.urls')), | |||
url(r'^(?P<path>favicon\.ico)$', 'django.views.static.serve', { | |||