More work on Snipt for Teams.

master
Nick Sergeant 2015-09-30 16:33:05 -04:00
Parent 55ef0b8f78
révision f79fc97188
21 fichiers modifiés avec 1561 ajouts et 49 suppressions

Voir le fichier

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

Diff de fichier supprimé car une ou plusieurs lignes sont trop longues

Voir le fichier

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

Voir le fichier

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

Voir le fichier

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

Voir le fichier

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

Voir le fichier

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

Fichier binaire non affiché.

Après

Largeur:  |  Hauteur:  |  Taille: 1.5 KiB

Diff de fichier supprimé car une ou plusieurs lignes sont trop longues

Voir le fichier

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

Fichier diff supprimé car celui-ci est trop grand Voir la Diff

Voir le fichier

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

Voir le fichier

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

Voir le fichier

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

Voir le fichier

@ -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 &raquo;</button>
<button type="submit" class="btn btn-success">Create team &raquo;</button>
</div>
</fieldset>
</form>
{% endif %}
{% endblock %}
{% block analytics %}

Voir le fichier

@ -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
Voir le fichier

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

Voir le fichier

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

Voir le fichier

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

Voir le fichier

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

Voir le fichier

@ -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', {