More work on Snipt for Teams.

master
Nick Sergeant 2015-09-30 16:33:05 -04:00
parent 55ef0b8f78
commit f79fc97188
21 changed files with 1561 additions and 49 deletions

View File

@ -1,7 +1,10 @@
from annoying.functions import get_object_or_None
from datetime import datetime from datetime import datetime
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import models from django.db import models
from itertools import chain
from snipts.models import Snipt from snipts.models import Snipt
from teams.models import Team
class UserProfile(models.Model): class UserProfile(models.Model):
@ -110,6 +113,14 @@ class UserProfile(models.Model):
public=True).count() > 0 \ public=True).count() > 0 \
else False 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): def get_account_age(self):
delta = datetime.now().replace(tzinfo=None) - \ delta = datetime.now().replace(tzinfo=None) - \
self.user.date_joined.replace(tzinfo=None) self.user.date_joined.replace(tzinfo=None)

File diff suppressed because one or more lines are too long

View File

@ -209,6 +209,57 @@ header.main {
float: right; float: right;
margin-right: 13px; 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;
}
}
}
} }
} }
} }

View File

@ -42,6 +42,7 @@
this.$html_body = this.$body.add(this.$html); this.$html_body = this.$body.add(this.$html);
this.$aside_main = $('aside.main', this.$body); this.$aside_main = $('aside.main', this.$body);
this.$aside_nav = $('aside.nav', 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.$aside_nav_ul = $('ul', this.$aside_nav);
this.$search_form = $('form.search', this.$body); this.$search_form = $('form.search', this.$body);
this.$search_query = $('input#search-query', this.$body); this.$search_query = $('input#search-query', this.$body);
@ -69,6 +70,7 @@
window.from_modal = false; window.from_modal = false;
} }
that.$aside_nav.removeClass('open'); that.$aside_nav.removeClass('open');
that.$teams_nav.removeClass('open');
}); });
this.$aside_nav_ul.click(function(e) { this.$aside_nav_ul.click(function(e) {
@ -190,7 +192,8 @@
}, },
events: { events: {
'showKeyboardShortcuts': 'showKeyboardShortcuts', 'showKeyboardShortcuts': 'showKeyboardShortcuts',
'click a.mini-profile': 'toggleMiniProfile' 'click a.mini-profile': 'toggleMiniProfile',
'click a.teams-nav': 'toggleTeamsNav'
}, },
keyboardShortcuts: function() { keyboardShortcuts: function() {
@ -237,6 +240,10 @@
this.$aside_nav.toggleClass('open'); this.$aside_nav.toggleClass('open');
return false; return false;
}, },
toggleTeamsNav: function(e) {
this.$teams_nav.toggleClass('open');
return false;
},
inFieldLabels: function () { inFieldLabels: function () {
$('div.infield label', this.$body).inFieldLabels({ $('div.infield label', this.$body).inFieldLabels({
fadeDuration: 200 fadeDuration: 200

View File

@ -5,6 +5,7 @@ Django==1.8.3
django-annoying==0.8.3 django-annoying==0.8.3
django-bcrypt==0.9.2 django-bcrypt==0.9.2
django-debug-toolbar==1.3.2 django-debug-toolbar==1.3.2
django-extensions==1.5.7
django-haystack==2.4.0 django-haystack==2.4.0
django-markdown-deux==1.0.5 django-markdown-deux==1.0.5
django-pagination==1.0.7 django-pagination==1.0.7

View File

@ -97,6 +97,7 @@ INSTALLED_APPS = (
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.sites', 'django.contrib.sites',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django_extensions',
'gunicorn', 'gunicorn',
'haystack', 'haystack',
'markdown_deux', 'markdown_deux',

View File

@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because one or more lines are too long

View File

@ -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
);

File diff suppressed because it is too large Load Diff

View File

@ -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);

View File

@ -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),
),
]

View File

@ -6,10 +6,10 @@ from snipts.utils import slugify_uniquely
class Team(models.Model): class Team(models.Model):
user = models.OneToOneField(User, blank=True, null=True) user = models.OneToOneField(User, blank=True, null=True)
owner = models.ForeignKey(User, related_name='owner') 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) email = models.EmailField(max_length=255)
slug = models.SlugField(max_length=255, blank=True) 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) created = models.DateTimeField(auto_now_add=True, editable=False)
modified = models.DateTimeField(auto_now=True, editable=False) modified = models.DateTimeField(auto_now=True, editable=False)

View File

@ -18,22 +18,34 @@
<li>Members can create public or private snipts on a team.</li> <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>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>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 from $49/month, all with a 7-day free trial.</li>
<li>Plans starting at $49/month.</li>
</ul> </ul>
</div> </div>
<div class="control-group"> <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"> <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>
<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"> <div class="form-actions">
{% csrf_token %} {% csrf_token %}
<button type="submit" class="btn btn-success">Request more info &raquo;</button> <button type="submit" class="btn btn-success">Create team &raquo;</button>
</div> </div>
</fieldset> </fieldset>
</form> </form>
{% endif %}
{% endblock %} {% endblock %}
{% block analytics %} {% block analytics %}

View File

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

11
teams/urls.py Normal file
View File

@ -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'))

View File

@ -1,6 +1,9 @@
import uuid
from annoying.decorators import render_to from annoying.decorators import render_to
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.http import HttpResponseBadRequest from django.http import HttpResponseBadRequest
from django.shortcuts import get_object_or_404
from teams.models import Team from teams.models import Team
@ -13,22 +16,32 @@ def for_teams(request):
return {} 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') @render_to('teams/for-teams-complete.html')
def for_teams_complete(request): def for_teams_complete(request):
if request.method == 'POST' and request.user.is_authenticated(): if request.method == 'POST' and request.user.is_authenticated():
team = Team(name=request.POST['name'], team = Team(name=request.POST['name'],
email='nick@snipt.net', email=request.POST['email'],
owner=request.user) owner=request.user)
team.save() 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.user = user
team.save() team.save()
return { return {
'team': team
} }
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()

View File

@ -106,8 +106,24 @@
</li> </li>
{% block add-snipt %}{% endblock %} {% block add-snipt %}{% endblock %}
{% endif %} {% endif %}
<li> <li class="teams-nav">
<a href="/for-teams/" {% if '/for-teams/' in request.path %} class="active"{% endif %}>Teams</a> <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> </li>
</ul> </ul>
</nav> </nav>

View File

@ -1,33 +1,58 @@
{% load snipt_tags %} {% load snipt_tags %}
<div class="profile group"> {% if user.profile.has_team %}
<a href="/{{ user.username }}/"> <div class="profile group">
<img src="https://secure.gravatar.com/avatar/{{ user.email|md5 }}?s=300" alt="{{ user.username }}" title="{{ user.username }}" /> <a href="/{{ user.username }}/">
</a> <img src="https://secure.gravatar.com/avatar/{{ user.email|md5 }}?s=300" alt="{{ user.username }}" title="{{ user.username }}" />
<div class="meta"> </a>
<div class="username" title="{{ user.username }}"> <div class="meta">
<a href=""> <div class="username" title="{{ user.username }}">
{{ user.username }} <a href="">
</a> {{ user.username }}
</div> </a>
{% if user.username == 'nick' %} </div>
<div class="member-since">Snipt Founder in {{ user.date_joined|date:"Y" }}</div> <div class="member-since">Team since {{ user.date_joined|date:"Y" }}</div>
{% else %} {% if user.profile.get_blog_posts %}
<div class="member-since">Member since {{ user.date_joined|date:"Y" }}</div> <div class="urls">
{% endif %} Snipt Blog:
{% if user.profile.get_blog_posts %} <a href="{{ user.profile.get_user_profile_url }}">
<div class="urls"> {{ user.profile.get_user_profile_url }}
Snipt Blog: </a>
<a href="{{ user.profile.get_user_profile_url }}"> </div>
{{ user.profile.get_user_profile_url }} {% endif %}
</a> <div class="member-since">{{ user.team.members.all|length }} member{{ user.team.members.all|pluralize }}</div>
</div> </div>
{% endif %} </div>
</div> {% else %}
{% if user.profile.is_pro %} <div class="profile group">
<a class="pro" href="/pro/">Pro</a> <a href="/{{ user.username }}/">
{% endif %} <img src="https://secure.gravatar.com/avatar/{{ user.email|md5 }}?s=300" alt="{{ user.username }}" title="{{ user.username }}" />
{% if user.profile.gittip_username %} </a>
<a class="gittip" href="https://www.gittip.com/{{ user.profile.gittip_username }}/">Tip</a> <div class="meta">
{% endif %} <div class="username" title="{{ user.username }}">
</div> <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 %}

View File

@ -10,7 +10,6 @@ from snipts.api import (PublicSniptResource,
PrivateUserResource, PublicTagResource) PrivateUserResource, PublicTagResource)
from snipts.views import search from snipts.views import search
from tastypie.api import Api from tastypie.api import Api
from teams.views import for_teams, for_teams_complete
from utils.views import SniptRegistrationView from utils.views import SniptRegistrationView
from views import (homepage, lexers, login_redirect, pro, sitemap, tags, from views import (homepage, lexers, login_redirect, pro, sitemap, tags,
pro_complete, user_api_key) pro_complete, user_api_key)
@ -47,9 +46,6 @@ urlpatterns = \
url(r'^pro/$', pro), url(r'^pro/$', pro),
url(r'^pro/complete/$', pro_complete), 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'^account/', include('accounts.urls')),
url(r'^api/public/lexer/$', lexers), url(r'^api/public/lexer/$', lexers),
@ -65,6 +61,7 @@ urlpatterns = \
name='registration_register'), name='registration_register'),
url(r'', include('registration.backends.default.urls')), url(r'', include('registration.backends.default.urls')),
url(r'^', include('teams.urls')),
url(r'^', include('snipts.urls')), url(r'^', include('snipts.urls')),
url(r'^(?P<path>favicon\.ico)$', 'django.views.static.serve', { url(r'^(?P<path>favicon\.ico)$', 'django.views.static.serve', {