Merge pull request #156 from nicksergeant/teams

Snipt for Teams
master
Nick Sergeant 2015-10-25 23:28:01 -04:00
commit e846b4d753
61 changed files with 1600 additions and 469 deletions

View File

@ -34,9 +34,9 @@ assets:
> media/css/snipt.css
@cat media/js/src/account.js > media/js/src/account.min.js
@cat media/js/src/snipts.js > media/js/src/snipts.min.js
@cat media/js/src/search.js > media/js/src/search.min.js
@cat media/js/src/jobs.js > media/js/src/jobs.min.js
@cat media/js/src/application.js > media/js/src/application.min.js
@cat media/js/src/team.js > media/js/src/team.min.js
@cat media/js/src/modules/site.js > media/js/src/modules/site.min.js
@cat media/js/src/modules/snipt.js > media/js/src/modules/snipt.min.js
@cat media/js/src/pro.js > media/js/src/pro.min.js
@ -146,13 +146,16 @@ vagrant:
@$(ssh-vagrant) '$(pm) rebuild_index --noinput;'
pulldb:
# @ssh nick@snipt.net -p 55555 'sudo su -c "pg_dump snipt|gzip > /tmp/snipt.dump" postgres'
# @scp -q -P 55555 nick@snipt.net:/tmp/snipt.dump snipt.dump.gz
# @dropdb snipt
@ssh nick@snipt.net -p 55555 'sudo su -c "pg_dump snipt|gzip > /tmp/snipt.dump" postgres'
@scp -q -P 55555 nick@snipt.net:/tmp/snipt.dump snipt.dump.gz
@dropdb snipt
@createdb snipt
@cat snipt.dump.gz | gunzip | psql snipt
@rm snipt.dump.gz
sass:
sass --sourcemap=none --watch -t compressed --scss media/css/style.scss:media/css/style.css
.PHONY: assets, \
db, \
deploy, \
@ -162,5 +165,6 @@ pulldb:
provision-vagrant, \
salt-server, \
salt-vagrant, \
sass, \
server, \
vagrant

View File

@ -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,9 +113,37 @@ class UserProfile(models.Model):
public=True).count() > 0 \
else False
@property
def is_a_team(self):
if get_object_or_None(Team, user=self.user, disabled=False):
return True
else:
return False
def teams(self):
teams_owned = Team.objects.filter(owner=self.user, disabled=False)
teams_in = Team.objects.filter(members=self.user, disabled=False)
return list(chain(teams_owned, teams_in))
@property
def has_teams(self):
if (len(self.teams()) > 0):
return True
else:
return False
def get_account_age(self):
delta = datetime.now().replace(tzinfo=None) - \
self.user.date_joined.replace(tzinfo=None)
return delta.days
@property
def has_pro(self):
if (self.is_pro or
self.has_teams or
self.is_a_team):
return True
else:
return False
User.profile = property(lambda u: UserProfile.objects.get_or_create(user=u)[0])

View File

@ -26,7 +26,7 @@
<li ng-class="{active: route.current.scope.section == 'Editor'}">
<a href="/account/editor/">Editor</a>
</li>
<li ng-show="user.is_pro && user.stripe_id && user.stripe_id != 'COMP'" ng-class="{active: route.current.scope.section == 'Billing'}">
<li ng-show="user.has_pro && user.stripe_id && user.stripe_id != 'COMP'" ng-class="{active: route.current.scope.section == 'Billing'}">
<a href="/account/billing/">Billing</a>
</li>
</ul>

View File

@ -4,6 +4,7 @@ import stripe
from annoying.decorators import ajax_request, render_to
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.mail import send_mail
from snipts.models import Snipt
@ -30,6 +31,15 @@ def cancel_subscription(request):
profile.stripe_id = None
profile.save()
send_mail('[Snipt] User cancelled Pro: {}'.format(request.user.username),
"""
User: https://snipt.net/{}
Email: {}
""".format(request.user.username, request.user.email),
'support@snipt.net',
['nick@snipt.net'],
fail_silently=False)
return {'deleted': True}

View File

@ -34,6 +34,7 @@ class BlogMiddleware:
get_object_or_404(User, username__iexact=blog_user)
if request.blog_user is None:
# TODO: This needs to check profile.has_pro() instead.
pro_users = User.objects.filter(userprofile__is_pro=True)
for pro_user in pro_users:

View File

@ -77,7 +77,7 @@
<script type="text/javascript" src="//pagead2.googlesyndication.com/pagead/show_ads.js"></script>
</div>
{% endif %}
{% if not blog_user.profile.is_pro %}
{% if not blog_user.profile.has_pro %}
<nav class="footer {% if sidebar %}with-sidebar{% endif %}">
<ul class="powered">
<li class="snipt"><a href="https://snipt.net/blogging/">Blog powered by Snipt</a></li>

View File

@ -32,9 +32,9 @@ cat media/css/bootstrap.min.css \
cat media/js/src/account.js|jsmin > media/js/src/account.min.js
cat media/js/src/snipts.js|jsmin > media/js/src/snipts.min.js
cat media/js/src/search.js|jsmin > media/js/src/search.min.js
cat media/js/src/jobs.js|jsmin > media/js/src/jobs.min.js
cat media/js/src/application.js|jsmin > media/js/src/application.min.js
cat media/js/src/team.js|jsmin > media/js/src/team.min.js
cat media/js/src/modules/site.js|jsmin > media/js/src/modules/site.min.js
cat media/js/src/modules/snipt.js|jsmin > media/js/src/modules/snipt.min.js
cat media/js/src/pro.js|jsmin > media/js/src/pro.min.js

File diff suppressed because one or more lines are too long

View File

@ -209,6 +209,61 @@ header.main {
float: right;
margin-right: 13px;
}
&.teams-nav, &.add-snipt {
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;
}
}
span.as {
color: #7C8D8E;
margin-right: 2px;
}
}
}
}
}
@ -890,11 +945,11 @@ article.snipt {
font: normal 12px/16px $Consolas;
margin: 0;
min-height: 220px;
min-width: 589px;
overflow-x: auto;
padding: 4px 0 4px 0;
white-space: pre;
word-wrap: normal;
width: 589px;
@include border-radius(0);
&::-webkit-scrollbar {
@ -1615,6 +1670,34 @@ div.profile {
text-decoration: none;
}
}
&.team-settings {
margin-top: -20px;
span.title {
color: #949494;
display: block;
font-size: 11px;
margin-bottom: 10px;
text-transform: uppercase;
}
a {
background: #dbdbdb;
box-sizing: border-box;
display: block;
margin-bottom: 5px;
padding: 5px 8px;
text-decoration: none;
width: 100%;
@include border-radius(3px);
&:hover {
background: #c8c8c8;
}
&:last-child {
margin-bottom: 0;
}
}
}
}
div.empty-snipts {
background: rgba(128, 128, 128, .08);
@ -1938,9 +2021,10 @@ body.pro {
div.payment-loading {
background: rgba(#F2F2F2, .6);
display: none;
height: 287px;
height: 410px;
left: 0;
position: absolute;
top: 261px;
top: 254px;
width: 100%;
span {
@ -1955,6 +2039,10 @@ body.pro {
width: 120px;
@include border-radius;
}
&.-teams {
height: 581px;
top: 449px;
}
}
div.stripe {
color: #C2C2C2;
@ -1979,6 +2067,19 @@ body.pro {
}
}
}
div.login-first {
background: #FFF;
border: 3px solid #3299B7;
color: #666;
display: block;
font-size: 18px;
font-weight: bold;
margin: 0 auto 10px auto;
padding: 20px;
text-align: center;
width: 400px;
@include border-radius;
}
}
body.search {
div.empty-snipts {
@ -2988,3 +3089,73 @@ a.snipt-promo {
background: #55a955;
}
}
div.team-controller {
div.add-member {
input {
margin-bottom: 0;
width: 50%;
}
ul {
margin: 12px 0 0 0;
}
}
li.user {
box-sizing: border-box;
list-style-type: none;
margin: 0;
padding: 10px;
width: 42%;
@include border-radius(3px);
span {
font-size: 16px;
}
a.btn {
float: right;
}
img {
margin-right: 10px;
}
&:last-of-type {
margin-bottom: 0;
}
&:hover {
background: #ebebeb;
}
}
ul.member-list {
margin: 0;
}
}
div.payment-form {
margin: 0 auto;
width: 86%;
}
div.with-teams-search {
display: inline-block;
width: 87%;
input.search-query {
width: 69% !important;
}
select {
display: inline-block;
width: 25%;
}
}
div.team-search {
form {
margin: 0;
padding: 5px;
}
input {
box-sizing: border-box;
margin: 0;
padding: 15px 10px;
width: 100%;
}
}

View File

@ -174,7 +174,7 @@ if (typeof angular !== 'undefined') {
AccountStorage.getAccount().then(function(response) {
$scope.user = response.data;
if ($scope.user.is_pro && $scope.user.stripe_id && $scope.user.stripe_id !== 'COMP') {
if ($scope.user.has_pro && $scope.user.stripe_id && $scope.user.stripe_id !== 'COMP') {
AccountStorage.getStripeAccount().then(function(response) {
$scope.user.stripeAccount = response.data;
});

View File

@ -17,6 +17,20 @@ var snipt = {
jQuery(function($) {
var SiteView = snipt.module('site').SiteView;
window.site = new SiteView();
var $pres = $('td.code pre');
$pres.each(function(i) {
var pre = $pres.eq(i);
pre.width(pre.parents('section.code').width() - 30);
});
$('form#cancel-team-subscription').submit(function() {
if (confirm('Are you sure you want to cancel your subscription?\n\nYou will no longer be able to create new Snipts under this team. This action is effective immediately and we unfortunately cannot issue any refunds.')) {
return true;
} else {
return false;
}
});
});
// Angular app init.

View File

@ -15,13 +15,13 @@
</div>
</div>
<div ng-show="!cancelled">
<div ng-show="!user.is_pro">
<div ng-show="!user.has_pro">
<p class="alert alert-info">
You're not a Pro yet, so we have nothing to show you here.<br />
<br /><a class="btn btn-success" href="/pro/">Signup for Pro &raquo;</a>
</p>
</div>
<div ng-show="user.is_pro">
<div ng-show="user.has_pro">
<div class="def" data-title="Plan" ng-show="user.stripeAccount.status != 'inactive'">
{[{ user.stripeAccount.name || 'Loading...' }]}
</div>

View File

@ -17,9 +17,9 @@
<div class="control-group" ng-class="{error: errors.blog_domain}">
<label class="control-label" for="id_blog_domain">Blog domain:</label>
<div class="controls">
<input ng-disabled="!user.is_pro" id="id_blog_domain" type="text" ng-model="user.blog_domain" maxlength="250">
<span ng-show="user.is_pro" class="help-block">Like 'snipt.nicksergeant.com' or 'nicksergeant.com' (without quotes). Set your CNAME to `snipt.net` or A-record to `96.126.110.160`. You can use multiple domains here: separate each domain with a space. The first domain will be your primary domain. All other domains will redirect to your primary domain.</span>
<span ng-show="!user.is_pro" class="help-block"><a href="https://snipt.net/pro/">Go Pro</a> to enable a custom domain for your <a href="https://snipt.net/blogging/">Snipt blog</a>.</span>
<input ng-disabled="!user.has_pro" id="id_blog_domain" type="text" ng-model="user.blog_domain" maxlength="250">
<span ng-show="user.has_pro" class="help-block">Like 'snipt.nicksergeant.com' or 'nicksergeant.com' (without quotes). Set your CNAME to `snipt.net` or A-record to `96.126.110.160`. You can use multiple domains here: separate each domain with a space. The first domain will be your primary domain. All other domains will redirect to your primary domain.</span>
<span ng-show="!user.has_pro" class="help-block"><a href="https://snipt.net/pro/">Go Pro</a> to enable a custom domain for your <a href="https://snipt.net/blogging/">Snipt blog</a>.</span>
</div>
</div>
</div>

View File

@ -42,6 +42,8 @@
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.$add_snipt = $('li.add-snipt', 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 +71,8 @@
window.from_modal = false;
}
that.$aside_nav.removeClass('open');
that.$teams_nav.removeClass('open');
that.$add_snipt.removeClass('open');
});
this.$aside_nav_ul.click(function(e) {
@ -96,7 +100,6 @@
var $form = $('form#pro-signup');
var $submit = $('button[type="submit"]', $form);
var $name = $('input#name');
var $cardNumber = $('input#number');
var $expMonth = $('select#exp-month');
var $expYear = $('select#exp-year');
@ -135,7 +138,6 @@
$('.payment-loading').show();
Stripe.createToken({
name: $name.val(),
number: $cardNumber.val(),
cvc: $cvc.val(),
exp_month: $expMonth.val(),
@ -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

View File

@ -134,10 +134,10 @@
$('div.alert-not-pro').hide();
if ($checkbox.is(':checked')) {
$label.removeClass('is-private').addClass('is-public');
if (!window.user_is_pro) $('div.alert-not-pro').hide();
if (!window.user_has_pro) $('div.alert-not-pro').hide();
} else {
$label.addClass('is-private').removeClass('is-public');
if (!window.user_is_pro) $('div.alert-not-pro').show();
if (!window.user_has_pro) $('div.alert-not-pro').show();
}
return false;
}).change();
@ -221,15 +221,23 @@
if (window.editor_theme != 'default') {
$selectTheme.val(window.editor_theme);
$selectTheme.trigger('liszt:updated');
$selectTheme.trigger('chosen:updated');
$selectTheme.trigger('change');
}
if (window.default_editor != 'codemirror') {
$selectEditor.val(window.default_editor);
$selectEditor.trigger('liszt:updated');
$selectEditor.trigger('chosen:updated');
$selectEditor.trigger('change');
}
// Init user
if (window.teams.length) {
var $selectUser = $('select#id_user', window.site.$main_edit);
$selectUser.chosen();
$selectUser.val(window.intended_user);
$selectUser.trigger('chosen:updated');
}
// Full-screen mode.
this.setupCodeMirrorFullScreen();
@ -474,11 +482,19 @@
code = window.editor.getValue();
}
var intendedUser;
if (window.teams.length) {
intendedUser = $('select[name="user"]').val();
} else {
intendedUser = window.intended_user;
}
that.model.save({
'title': $('input#snipt_title').val(),
'tags': $('label.tags textarea').val(),
'tags_list': $('label.tags textarea').val(),
'lexer': $('select[name="lexer"]').val(),
'intended_user': intendedUser,
'lexer_name': $('select[name="lexer"] option:selected').text(),
'code': code,
'description': $('textarea[name="description"]').val(),
@ -489,9 +505,20 @@
success: function(model, response) {
$('button.save, button.save-and-close, button.delete, button.cancel',
window.site.$main_edit).removeAttr('disabled');
that.model.set('new_from_js', false);
var $pres = $('td.code pre');
$pres.each(function(i) {
var pre = $pres.eq(i);
pre.width(pre.parents('section.code').width() - 30);
});
},
error: function(model, response) {
alert(JSON.stringify(response.responseJSON.snipt));
if (response.responseJSON) {
alert(JSON.stringify(response.responseJSON.snipt));
} else {
alert(JSON.stringify(response.statusText));
}
}
});
},
@ -590,8 +617,22 @@
}
$('span.cmd-ctrl').text(cmd);
$('button#add-snipt').click(function() {
that.addNewSnipt();
var $buttonAddSnipt = $('button#add-snipt');
$buttonAddSnipt.click(function(e) {
if (window.teams.length) {
e.stopPropagation();
$buttonAddSnipt.parent().toggleClass('open');
} else {
that.addNewSnipt();
}
});
var $addSniptTeams = $('ul.add-snipt-teams a');
$addSniptTeams.click(function(e) {
e.stopPropagation();
window.intended_user = $(e.target).attr('data-intended-user') ||
$(e.target).parent().attr('data-intended-user');
that.addNewSnipt();
});
},
@ -603,7 +644,7 @@
var $public = $('div.public', $el);
var $blog_post = $('div.blog-post', $el);
var $publish_date = $('div.publish-date', $el);
var $user = $('li.author a', $el);
var $user = $('li.author > a', $el);
var is_public = $public.text() === 'True' ? true : false;
var is_blog_post = $blog_post.text() === 'True' ? true : false;
@ -619,7 +660,7 @@
};
}
var is_pro = $user.siblings('span.pro').length ? true : false;
var has_pro = $user.siblings('span.pro').length ? true : false;
var data = {
code: $('textarea.raw', $el).text(),
@ -647,7 +688,7 @@
absolute_url: $user.attr('href'),
username: $user.text(),
profile: {
is_pro: is_pro
has_pro: has_pro
}
}
};
@ -685,7 +726,7 @@
user: {
username: '',
profile: {
is_pro: window.user_is_pro
has_pro: window.user_has_pro
}
}
};
@ -831,22 +872,6 @@
$document.bind('keydown', 'esc', function() {
that.escapeUI();
});
$document.bind('keydown', 'g', function() {
if (!window.ui_halted) {
if (window.$selected) {
window.$selected.trigger('deselect');
}
window.scrollTo(0, 0);
}
});
$document.bind('keydown', 'Shift+g', function() {
if (!window.ui_halted) {
if (window.$selected) {
window.$selected.trigger('deselect');
}
window.scrollTo(0, document.body.scrollHeight);
}
});
$document.bind('keydown', 'n', function() {
if (!window.ui_halted) {
var $anc = $('li.next a');

View File

@ -1,57 +0,0 @@
(function() {
if (typeof angular !== 'undefined') {
var root = this;
var $ = root.jQuery;
var controllers = {};
var app = root.app;
// Services.
app.factory('SearchService', function() {
return {
mineOnly: false,
query: ''
};
});
// Controllers.
controllers.HeaderSearchController = function($scope, SearchService) {
$scope.search = SearchService;
};
controllers.SearchController = function($scope, SearchService) {
$scope.search = SearchService;
$scope.$watch('search.query', function(query) {
if (query.indexOf('--mine') !== -1) {
$scope.search.mineOnly = true;
} else {
$scope.search.mineOnly = false;
}
});
$scope.toggleMineOnly = function() {
if ($scope.search.mineOnly) {
// Make sure '--mine' exists somewhere in the query.
if ($scope.search.query.indexOf('--mine') === -1) {
$scope.search.query = $scope.search.query.trim() + ' --mine';
}
}
else {
$scope.search.query = $scope.search.query.replace('--mine', '').trim();
}
};
};
// Assign the controllers.
app.controller(controllers);
}
}).call(this);

45
media/js/src/team.js Normal file
View File

@ -0,0 +1,45 @@
(function() { 'use strict';
if (typeof angular !== 'undefined') {
var root = this;
var $ = root.jQuery;
var controllers = {};
var app = root.app;
// Services.
app.factory('TeamStorage', function($http, $q) {
return {
searchUsers: function(query) {
var promise = $http({
method: 'GET',
url: '/api/public/user/?format=json&limit=100&username__contains=' + query
});
return promise;
}
};
});
// Controllers.
controllers.TeamController = function($scope, $timeout, TeamStorage) {
$scope.users = [];
$scope.search = '';
$scope.$watch('search', function(val) {
$timeout.cancel($scope.timeout);
if (!val) return $scope.users = [];
$scope.timeout = $timeout(function() {
TeamStorage.searchUsers(val).then(function(response) {
$scope.users = response.data.objects;
});
}, 350);
});
};
// Assign the controllers.
app.controller(controllers);
}
}).call(this);

View File

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

View File

@ -32,7 +32,7 @@ ALLOWED_HOSTS = ['*']
AUTH_PROFILE_MODULE = 'accounts.UserProfile'
AUTHENTICATION_BACKENDS = ('utils.backends.EmailOrUsernameModelBackend',)
BASE_PATH = os.path.dirname(__file__)
CSRF_COOKIE_DOMAIN = '.snipt.net'
CSRF_COOKIE_DOMAIN = '.snipt.net' if 'USE_SSL' in os.environ else False
CSRF_COOKIE_SECURE = True if 'USE_SSL' in os.environ else False
DEBUG = True if 'DEBUG' in os.environ else False
DEFAULT_FROM_EMAIL = 'support@snipt.net'
@ -97,6 +97,7 @@ INSTALLED_APPS = (
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.staticfiles',
'django_extensions',
'gunicorn',
'haystack',
'markdown_deux',
@ -108,6 +109,7 @@ INSTALLED_APPS = (
'storages',
'taggit',
'tastypie',
'teams',
'typogrify',
'user-admin',
'utils',
@ -125,6 +127,7 @@ LOGGING = {
}
MIDDLEWARE_CLASSES = (
'django.middleware.csrf.CsrfViewMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',

View File

@ -56,25 +56,30 @@ class PrivateSniptAuthorization(Authorization):
return object_list.filter(user=bundle.request.user)
def read_detail(self, object_list, bundle):
return bundle.obj.user == bundle.request.user
return bundle.obj.is_authorized_user(bundle.request.user)
def create_list(self, object_list, bundle):
raise Unauthorized()
def create_detail(self, object_list, bundle):
return bundle.obj.user == bundle.request.user
user = bundle.obj.user
if user == bundle.request.user:
return True
if user.profile.is_a_team:
return user.team.user_is_member(bundle.request.user)
return False
def update_list(self, object_list, bundle):
raise Unauthorized()
def update_detail(self, object_list, bundle):
return bundle.obj.user == bundle.request.user
return bundle.obj.is_authorized_user(bundle.request.user)
def delete_list(self, object_list, bundle):
raise Unauthorized()
def delete_detail(self, object_list, bundle):
return bundle.obj.user == bundle.request.user
return bundle.obj.is_authorized_user(bundle.request.user)
class PrivateUserProfileAuthorization(Authorization):
@ -145,7 +150,7 @@ class SniptValidation(Validation):
def is_valid(self, bundle, request=None):
errors = {}
if request.user.profile.is_pro is False:
if request.user.profile.has_pro is False:
if ('public' not in bundle.data or bundle.data['public'] is False):
errors['not-pro'] = ("You'll need to go Pro "
"(https://snipt.net/pro/) "
@ -176,7 +181,7 @@ class PublicUserResource(ModelResource):
fields = ['id', 'username']
include_absolute_url = True
allowed_methods = ['get']
filtering = {'username': 'exact'}
filtering = {'username': ['contains', 'exact']}
max_limit = 200
cache = SimpleCache()
@ -302,7 +307,7 @@ class PrivateUserProfileResource(ModelResource):
bundle.data['username'] = bundle.obj.user.username
bundle.data['user_id'] = bundle.obj.user.id
bundle.data['api_key'] = bundle.obj.user.api_key.key
bundle.data['is_pro'] = bundle.obj.user.profile.is_pro
bundle.data['has_pro'] = bundle.obj.user.profile.has_pro
return bundle
@ -327,7 +332,7 @@ class PrivateUserResource(ModelResource):
bundle.data['email_md5'] = hashlib \
.md5(bundle.obj.email.lower()) \
.hexdigest()
bundle.data['is_pro'] = bundle.obj.profile.is_pro
bundle.data['has_pro'] = bundle.obj.profile.has_pro
bundle.data['stats'] = {
'public_snipts': Snipt.objects.filter(user=bundle.obj.id,
public=True).count(),
@ -359,7 +364,12 @@ class PrivateUserResource(ModelResource):
class PrivateSniptResource(ModelResource):
user = fields.ForeignKey(PrivateUserResource, 'user', full=True)
last_user_saved = fields.ForeignKey(PrivateUserResource,
'last_user_saved',
full=False)
tags_list = ListField()
tags = fields.ToManyField(PublicTagResource, 'tags', related_name='tag',
full=True)
class Meta:
queryset = Snipt.objects.all().order_by('-created')
@ -408,20 +418,31 @@ class PrivateSniptResource(ModelResource):
return bundle
def obj_create(self, bundle, **kwargs):
bundle.data['last_user_saved'] = bundle.request.user
bundle.data['tags_list'] = bundle.data.get('tags')
bundle.data['tags'] = ''
bundle.data['tags'] = []
bundle.data['user'] = \
User.objects.get(username=bundle.data['intended_user'])
if 'blog_post' in bundle.data:
bundle = self._clean_publish_date(bundle)
return super(PrivateSniptResource, self) \
.obj_create(bundle,
user=bundle.request.user, **kwargs)
.obj_create(bundle, **kwargs)
def obj_update(self, bundle, **kwargs):
bundle.data['user'] = bundle.request.user
instance = Snipt.objects.get(pk=bundle.data['id'])
if (instance.user.profile.is_a_team):
user = instance.user
else:
user = bundle.request.user
bundle.data['created'] = None
bundle.data['last_user_saved'] = bundle.request.user
bundle.data['modified'] = None
bundle.data['user'] = user
if type(bundle.data['tags']) in (str, unicode):
bundle.data['tags_list'] = bundle.data['tags']
@ -433,8 +454,7 @@ class PrivateSniptResource(ModelResource):
bundle = self._clean_publish_date(bundle)
return super(PrivateSniptResource, self) \
.obj_update(bundle,
user=bundle.request.user, **kwargs)
.obj_update(bundle, **kwargs)
def _clean_publish_date(self, bundle):
if bundle.data['blog_post'] and 'publish_date' not in bundle.data:

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('snipts', '0002_sniptlogentry'),
]
operations = [
migrations.AddField(
model_name='snipt',
name='last_user_saved',
field=models.ForeignKey(related_name='last_user_saved', blank=True, to=settings.AUTH_USER_MODEL, null=True),
),
]

View File

@ -16,12 +16,17 @@ from pygments.util import ClassNotFound
from snipts.utils import slugify_uniquely
from taggit.managers import TaggableManager
from taggit.utils import edit_string_for_tags
from teams.models import Team
class Snipt(models.Model):
"""An individual Snipt."""
user = models.ForeignKey(User, blank=True, null=True)
last_user_saved = models.ForeignKey(User,
blank=True,
null=True,
related_name='last_user_saved')
title = models.CharField(max_length=255, blank=True, null=True,
default='Untitled')
@ -174,7 +179,7 @@ class Snipt(models.Model):
diff = self._unidiff_output(self.original_code or '', self.code)
if (diff != ''):
log_entry = SniptLogEntry(user=self.user,
log_entry = SniptLogEntry(user=self.last_user_saved,
snipt=self,
code=self.code,
diff=diff)
@ -298,6 +303,14 @@ class Snipt(models.Model):
else:
return get_lexer_by_name(self.lexer).name
def is_authorized_user(self, user):
if self.user == user:
return True
if self.user.profile.is_a_team:
team = Team.objects.get(user=self.user, disabled=False)
return team.user_is_member(user)
return False
class SniptLogEntry(models.Model):
"""An individual log entry for a Snipt changeset."""

View File

@ -1,12 +1,7 @@
{% extends "base.html" %}
{% block add-snipt %}
<li class="add-snipt">
<button class="btn btn-info btn-large" id="add-snipt">
Add {% if request.user.username == 'blog' %}Post{% else %}Snipt{% endif %}
<i class="icon-search icon-plus icon-white"></i>
</button>
</li>
{% include 'add-snipt.html' %}
{% endblock %}
{% block page-title %}/ {% if snipt.title %}{{ snipt.title }}{% else %}Untitled{% endif %} / {{ user.username }} - {{ block.super }}{% endblock %}
@ -50,7 +45,7 @@
<section class="snipts" id="snipts">
{% if not request.user.is_authenticated %}
{% include 'ad-leaderboard-pro.html' %}
{% elif not request.user.profile.teams_beta_seen %}
{% elif not request.user.profile.teams_beta_seen and not request.user.team and not request.user.profile.has_teams %}
{% include 'ad-leaderboard-teams.html' %}
{% endif %}
{% with 'true' as detail %}

View File

@ -1,12 +1,7 @@
{% extends "snipts/list.html" %}
{% block add-snipt %}
<li class="add-snipt">
<button class="btn btn-info btn-large" id="add-snipt">
Add {% if request.user.username == 'blog' %}Post{% else %}Snipt{% endif %}
<i class="icon-search icon-plus icon-white"></i>
</button>
</li>
{% include 'add-snipt.html' %}
{% endblock %}
{% block breadcrumb %}

View File

@ -1,12 +1,7 @@
{% extends "snipts/list.html" %}
{% block add-snipt %}
<li class="add-snipt">
<button class="btn btn-info btn-large" id="add-snipt">
Add {% if request.user.username == 'blog' %}Post{% else %}Snipt{% endif %}
<i class="icon-search icon-plus icon-white"></i>
</button>
</li>
{% include 'add-snipt.html' %}
{% endblock %}
{% block breadcrumb %}

View File

@ -12,13 +12,18 @@
{% endblock %}
{% block content %}
{% if 'team-cancelled' in request.GET %}
<div class="alert alert-success" style="margin: 30px;">
Your team plan has been succesfully cancelled.
</div>
{% endif %}
<section class="snipts" id="snipts" ng-controller="SniptListController"
{% if request.user.profile.list_view == 'C' %}
ng-cloak ng-show="$root.account.id"
{% endif %}>
{% if not request.user.is_authenticated %}
{% include 'ad-leaderboard-pro.html' %}
{% elif not request.user.profile.teams_beta_seen %}
{% elif not request.user.profile.teams_beta_seen and not request.user.team and not request.user.profile.has_teams %}
{% include 'ad-leaderboard-teams.html' %}
{% endif %}
{% autopaginate snipts 10 %}

View File

@ -37,6 +37,23 @@
</div>
<aside>
<div class="in">
<% if (snipt.new_from_js) { %>
{% endverbatim %}
{% if request.user.profile.has_teams %}
<div class="user">
<label class="user">
<span>User / team</span>
<select name="user" id="id_user">
<option value="{{ request.user.username }}">{{ request.user.username }}</option>
{% for team in request.user.profile.teams %}
<option value="{{ team.slug }}">{{ team.name }}</option>
{% endfor %}
</select>
</label>
</div>
{% endif %}
{% verbatim %}
<% } %>
<div class="type-lexer">
<label class="lexer type-lexer">
<span>Type</span>

View File

@ -35,11 +35,9 @@
</div>
<aside>
<ul class="options">
<% if (snipt.user.username === window.user) { %>
<li>
<a class="edit" href="#">Edit</a>
</li>
<% } %>
<li>
<a class="edit" href="#">Edit</a>
</li>
<li>
<a class="embed" href="#">Embed</a>
</li>
@ -47,16 +45,18 @@
<a class="copy" href="#">Copy</a>
</li>
</ul>
<section class="meta tags">
<h2><%= snipt.tags.length %> tag<% if ((snipt.tags.length > 1) || (snipt.tags.length === 0)) { print('s'); } %></h2>
<ul>
<% for (var i=0; i < snipt.tags.length; i++) { %>
<li <% if (i > 2 && !window.detail) { %>class="hidden"<% } %>>
<a href="<%= snipt.tags[i].absolute_url %>"><%= snipt.tags[i].name %></a>
</li>
<% } %>
</ul>
</section>
<% if (typeof(snipt.tags) === 'object') { %>
<section class="meta tags">
<h2><%= snipt.tags.length %> tag<% if ((snipt.tags.length > 1) || (snipt.tags.length === 0)) { print('s'); } %></h2>
<ul>
<% for (var i=0; i < snipt.tags.length; i++) { %>
<li <% if (i > 2 && !window.detail) { %>class="hidden"<% } %>>
<a href="<%= snipt.tags[i].absolute_url %>"><%= snipt.tags[i].name %></a>
</li>
<% } %>
</ul>
</section>
<% } %>
<section class="meta stats">
<ul>
<li><%= snipt.views %> views</li>
@ -72,16 +72,15 @@
<a href="<%= snipt.user.absolute_url %>">
<%= snipt.user.username %>
</a>
<% if (window.user_is_pro) { %>
<% if (window.teams.indexOf(snipt.user.username) !== -1) { %>
<span class="pro"><a href="/for-teams/">Team</a></span>
<% } else if (window.user_has_pro) { %>
<span class="pro"><a href="/pro/">Pro</a></span>
<% } %>
</li>
<% if (!snipt.new_from_js) { %>
<li class="created" title="<%= snipt.created %>"><%= snipt.created_formatted %></li>
<% } %>
<% if (snipt.public && !window.detail) { %>
<li class="comments"><a href="<%= snipt.absolute_url %>#disqus_thread" data-disqus-identifier="<%= snipt.id %>"></a></li>
<% } %>
<li class="raw">
<a href="<%= snipt.raw_url %>">Raw</a> /
<a href="<%= snipt.raw_url %>?nice">Raw Nice</a>

View File

@ -15,7 +15,7 @@
{% if snipt.line_count > 8 and not detail and 'snipt-expand' not in snipt.tags_list %}
expandable
{% endif %}
{% if snipt.user == request.user %}
{% if snipt|is_authorized_user:request.user %}
editable
{% endif %}
{% if is_favorited %}
@ -113,7 +113,7 @@
{% block aside %}
<aside ng-show="!account || account.list_view == 'N'">
<ul class="options">
{% if snipt.user == request.user %}
{% if snipt|is_authorized_user:request.user %}
{% if snipt.line_count <= 300 or detail %}
<li>
<a class="edit" href="#">Edit</a>
@ -132,7 +132,7 @@
<a class="copy" href="#">Copy</a>
</li>
{% endif %}
{% if snipt.user != request.user and request.user.is_authenticated %}
{% if snipt.user != request.user and request.user.is_authenticated and not snipt.user.team %}
<li>
{% if is_favorited %}
<a class="favorite favorited" href="#">Favorited</a>
@ -178,7 +178,9 @@
<li class="author">
<span class="avatar" style="background-image: url('https://secure.gravatar.com/avatar/{{ snipt.user.email|md5 }}?s=15&amp;d=https://snipt.s3.amazonaws.com/img/author-icon.png');"></span>
<a href="{{ snipt.user.get_absolute_url }}">{{ snipt.user.username }}</a>
{% if snipt.user.profile.is_pro %}
{% if snipt.user.profile.is_a_team %}
<span class="pro"><a href="/for-teams/">Team</a></span>
{% elif snipt.user.profile.has_pro %}
<span class="pro"><a href="/pro/">Pro</a></span>
{% endif %}
{% if snipt.user.profile.gittip_username %}
@ -234,7 +236,7 @@
<div class="hide public">{{ snipt.public }}</div>
<div class="hide blog-post">{{ snipt.blog_post }}</div>
<div class="hide publish-date">{{ snipt.publish_date|date:"M d, Y \a\t h:i A" }}</div>
{% if snipt.user == request.user %}
{% if snipt|is_authorized_user:request.user %}
<div class="hide resource-uri">/api/private/snipt/{{ snipt.pk }}/</div>
{% else %}
<div class="hide resource-uri">/api/public/snipt/{{ snipt.pk }}/</div>

View File

@ -61,3 +61,8 @@ def generate_line_numbers(context, line_numbers):
@register.filter
def md5(string):
return hashlib.md5(string.lower()).hexdigest()
@register.filter
def is_authorized_user(snipt, user):
return snipt.is_authorized_user(user)

View File

@ -12,7 +12,10 @@ def slugify_uniquely(value, model, slugfield="slug"):
while True:
if suffix:
potential = "-".join([base, str(suffix)])
if value:
potential = "-".join([base, str(suffix)])
else:
potential = str(suffix)
if not model.objects.filter(**{slugfield: potential}).count():
return potential
suffix = str(uuid.uuid4()).split('-')[0]

View File

@ -14,6 +14,7 @@ from haystack.query import EmptySearchQuerySet, SearchQuerySet
from pygments.lexers import get_lexer_by_name
from snipts.models import Favorite, Snipt
from taggit.models import Tag
from teams.models import Team
RESULTS_PER_PAGE = getattr(settings, 'HAYSTACK_SEARCH_RESULTS_PER_PAGE', 20)
@ -196,7 +197,10 @@ def list_user(request, username_or_custom_slug, tag_slug=None):
snipts = Snipt.objects
if user == request.user or \
(request.GET.get('api_key') == user.api_key.key):
(request.GET.get('api_key') == user.api_key.key) or \
(user.profile.is_a_team and
user.team.user_is_member(request.user)):
public = False
favorites = Favorite.objects.filter(user=user).values('snipt')
@ -283,17 +287,33 @@ def search(request, template='search/search.html', load_all=True,
query = ''
results = EmptySearchQuerySet()
# We have a query.
if request.GET.get('q'):
if request.user.is_authenticated() and '--mine' in \
request.GET.get('q'):
searchqueryset = SearchQuerySet() \
.filter(Q(public=True) | Q(author=request.user)) \
.order_by('-pub_date')
if request.user.is_authenticated() and \
'mine-only' in request.GET:
searchqueryset = SearchQuerySet().filter(author=request.user) \
.order_by('-pub_date')
else:
searchqueryset = SearchQuerySet() \
.filter(Q(public=True) | Q(author=request.user)) \
.order_by('-pub_date')
elif request.user.is_authenticated() and \
('author' in request.GET and
request.GET.get('author')):
author = request.GET.get('author')
if author == request.user.username:
searchqueryset = SearchQuerySet().filter(author=request.user) \
.order_by('-pub_date')
else:
team = get_object_or_None(Team, slug=author)
if team and team.user_is_member(request.user):
searchqueryset = SearchQuerySet().filter(author=team) \
.order_by('-pub_date')
form = ModelSearchForm(request.GET,
searchqueryset=searchqueryset,

0
teams/__init__.py Normal file
View File

9
teams/admin.py Normal file
View File

@ -0,0 +1,9 @@
from django.contrib import admin
from teams.models import Team
class TeamAdmin(admin.ModelAdmin):
list_display = ('name', 'owner', 'created', 'modified')
ordering = ('-created',)
admin.site.register(Team, TeamAdmin)

View File

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Team',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=255)),
('slug', models.SlugField(max_length=255, blank=True)),
('created', models.DateTimeField(auto_now_add=True)),
('modified', models.DateTimeField(auto_now=True)),
('members', models.ManyToManyField(related_name='member', to=settings.AUTH_USER_MODEL)),
('owner', models.ForeignKey(related_name='owner', to=settings.AUTH_USER_MODEL)),
('user', models.OneToOneField(to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('teams', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='team',
name='email',
field=models.EmailField(default='nick@snipt.net', max_length=255),
preserve_default=False,
),
]

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', '0002_team_email'),
]
operations = [
migrations.AlterField(
model_name='team',
name='user',
field=models.OneToOneField(null=True, blank=True, to=settings.AUTH_USER_MODEL),
),
]

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

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('teams', '0004_auto_20150930_1526'),
]
operations = [
migrations.AddField(
model_name='team',
name='stripe_id',
field=models.CharField(max_length=100, null=True, blank=True),
),
migrations.AlterField(
model_name='team',
name='name',
field=models.CharField(max_length=30),
),
]

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('teams', '0005_auto_20150930_2124'),
]
operations = [
migrations.AddField(
model_name='team',
name='plan',
field=models.CharField(default=b'snipt-teams-25-monthly', max_length=100, choices=[(b'snipt-teams-25-monthly', b'25 users, monthly'), (b'snipt-teams-100-monthly', b'100 users, monthly'), (b'snipt-teams-250-monthly', b'250 users, monthly'), (b'snipt-teams-unlimited-monthly', b'Unlimited users, monthly'), (b'snipt-teams-25-yearly', b'25 users, yearly'), (b'snipt-teams-100-yearly', b'100 users, yearly'), (b'snipt-teams-250-yearly', b'250 users, yearly'), (b'snipt-teams-unlimited-yearly', b'Unlimited users, yearly')]),
),
]

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('teams', '0006_team_plan'),
]
operations = [
migrations.AddField(
model_name='team',
name='disabled',
field=models.BooleanField(default=False),
),
]

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('teams', '0007_team_disabled'),
]
operations = [
migrations.AlterField(
model_name='team',
name='plan',
field=models.CharField(default=b'snipt-teams-25-monthly', max_length=100, null=True, blank=True, choices=[(b'snipt-teams-25-monthly', b'25 users, monthly'), (b'snipt-teams-100-monthly', b'100 users, monthly'), (b'snipt-teams-250-monthly', b'250 users, monthly'), (b'snipt-teams-unlimited-monthly', b'Unlimited users, monthly'), (b'snipt-teams-25-yearly', b'25 users, yearly'), (b'snipt-teams-100-yearly', b'100 users, yearly'), (b'snipt-teams-250-yearly', b'250 users, yearly'), (b'snipt-teams-unlimited-yearly', b'Unlimited users, yearly')]),
),
]

View File

72
teams/models.py Normal file
View File

@ -0,0 +1,72 @@
from django.contrib.auth.models import User
from django.db import models
from snipts.utils import slugify_uniquely
class Team(models.Model):
PLANS = (
('snipt-teams-25-monthly', '25 users, monthly'),
('snipt-teams-100-monthly', '100 users, monthly'),
('snipt-teams-250-monthly', '250 users, monthly'),
('snipt-teams-unlimited-monthly', 'Unlimited users, monthly'),
('snipt-teams-25-yearly', '25 users, yearly'),
('snipt-teams-100-yearly', '100 users, yearly'),
('snipt-teams-250-yearly', '250 users, yearly'),
('snipt-teams-unlimited-yearly', 'Unlimited users, yearly'),
)
email = models.EmailField(max_length=255)
members = models.ManyToManyField(User, related_name='member', blank=True)
name = models.CharField(max_length=30)
owner = models.ForeignKey(User, related_name='owner')
slug = models.SlugField(max_length=255, blank=True)
stripe_id = models.CharField(max_length=100, null=True, blank=True)
user = models.OneToOneField(User, blank=True, null=True)
plan = models.CharField(max_length=100, default='snipt-teams-25-monthly',
choices=PLANS, blank=True, null=True)
disabled = models.BooleanField(default=False)
created = models.DateTimeField(auto_now_add=True, editable=False)
modified = models.DateTimeField(auto_now=True, editable=False)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify_uniquely(self.name, User, 'username')
return super(Team, self).save(*args, **kwargs)
def __unicode__(self):
return self.name
@property
def member_count(self):
return self.members.all().count() + 1
@property
def member_limit(self):
if self.disabled:
return 0
plan_map = {
'snipt-teams-25-monthly': 25,
'snipt-teams-100-monthly': 100,
'snipt-teams-250-monthly': 250,
'snipt-teams-unlimited-monthly': float('inf'),
'snipt-teams-25-yearly': 25,
'snipt-teams-100-yearly': 100,
'snipt-teams-250-yearly': 250,
'snipt-teams-unlimited-yearly': float('inf')
}
if plan_map[self.plan] == float('inf'):
return 'Unlimited'
else:
return plan_map[self.plan]
def user_is_member(self, user):
if self.disabled:
return False
if self.owner == user or user in self.members.all():
return True
return False

View File

@ -13,12 +13,19 @@
<form class="form-horizontal static-box" id="pro-signup" method="post" action="/pro/complete/">
<fieldset>
<div class="info">
You rock.
<p class="sub" style="padding: 0 120px;">
We'll email you to get more information about your team and to help you get your first team created.
If you ever need anything at all, <a href="mailto:support@snipt.net">email support</a>, or use the chat in the bottom right.
</p>
Team created successfully.
</div>
<ul>
<li>
<a href="/{{ team.user.username }}">View your team profile</a>
</li>
<li>
<a href="/{{ team.user.username }}/members">Add some members</a>
</li>
<li>
<a href="/{{ team.user.username }}/billing">View billing info</a>
</li>
</ul>
</fieldset>
</form>
{% endblock %}

View File

@ -0,0 +1,159 @@
{% extends "base.html" %}
{% block page-title %}Snipt for Teams{% endblock %}
{% block body-class %}{{ block.super }} static signup pro pro-signup{% endblock %}
{% block extra-scripts %}
<script type="text/javascript" src="https://js.stripe.com/v1/"></script>
{% endblock %}
{% block js %}
{{ block.super }}
{% if debug %}
Stripe.setPublishableKey('pk_test_cgknmaWRMQeJt2adEdvH3T9l');
{% else %}
Stripe.setPublishableKey('pk_live_gUO2nCl7dhx6j0posz6gnbhA');
{% endif %}
{% endblock %}
{% block breadcrumb %}
<li><a href="/for-teams/">Snipt for Teams</a></li>
{% endblock %}
{% block content %}
{% if request.GET.declined %}
<div class="alert alert-error" style="margin: 30px;">
<strong>{{ request.GET.declined }}</strong> You have not been charged. Please try again.
</div>
{% endif %}
<form class="form-horizontal static-box" id="pro-signup" method="post" action="/for-teams/complete/">
<fieldset>
<div class="info">
Snipt for Teams
<ul class="features">
<li>Team profile at snipt.net/{team-name}.</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>Private team posts are editable by all team members.</li>
<li>All team members are automatically granted personal <a href="/pro/"><span class="pro">Pro</span></a> accounts.</li>
<li>Plans from $49/month, all with a 14-day free trial.</li>
</ul>
</div>
{% if not request.user.is_authenticated %}
<div class="login-first">
<span>
To create a team, <a href="/signup/?next=/for-teams/">sign up</a> or <a href="/login/?next=/for-teams/">log in</a>.
</span>
</div>
{% else %}
<div class="payment-form">
<div class="payment-loading -teams"><span>Please wait&hellip;</span></div>
<div class="payment-errors alert alert-error"></div>
<div class="control-group">
<label class="control-label" for="team-name">Team name:</label>
<div class="controls">
<input maxlength="30" required type="text" class="input-xlarge" name="team-name" id="team-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="email">Team email:</label>
<div class="controls">
<input required type="email" 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">
<label class="control-label" for="plan">Payment plan:</label>
<div class="controls">
<select name="plan" type="text" class="input-xlarge" id="plan">
<option value="snipt-teams-25-monthly">25 users - $49/month</option>
<option value="snipt-teams-100-monthly">100 users - $149/month</option>
<option value="snipt-teams-250-monthly">250 users - $299/month</option>
<option value="snipt-teams-unlimited-monthly">Unlimited users - $499/month</option>
<option value="snipt-teams-25-yearly">25 users - $588/year</option>
<option value="snipt-teams-100-yearly">100 users - $1,788/year</option>
<option value="snipt-teams-250-yearly">250 users - $3,588/year</option>
<option value="snipt-teams-unlimited-yearly">Unlimited users - $5,988/year</option>
</select>
<p class="sub" style="margin-top: 3px; color: #999999;">
Free 14-day trial (your card won't be charged until then, and you can cancel at any time).
</p>
</div>
</div>
<div class="control-group">
<label class="control-label" for="number">Card number:</label>
<div class="controls cards">
<input type="text" class="input-xlarge" id="number" />
<img src="{{ STATIC_URL }}img/card-visa.png" alt="Visa" />
<img src="{{ STATIC_URL }}img/card-mastercard.png" alt="MasterCard" />
<img src="{{ STATIC_URL }}img/card-discover.png" alt="Discover" />
<img src="{{ STATIC_URL }}img/card-american-express.png" alt="American Express" />
</div>
</div>
<div class="control-group">
<label class="control-label" for="exp-month">Expiration date:</label>
<div class="controls">
<select id="exp-month" class="span2 exp-month">
<option value="">----</option>
<option value="01">01 - January</option>
<option value="02">02 - February</option>
<option value="03">03 - March</option>
<option value="04">04 - April</option>
<option value="05">05 - May</option>
<option value="06">06 - June</option>
<option value="07">07 - July</option>
<option value="08">08 - August</option>
<option value="09">09 - September</option>
<option value="10">10 - October</option>
<option value="11">11 - November</option>
<option value="12">12 - December</option>
</select>
<select id="exp-year" class="span2">
<option value="">----</option>
<option value="2015">2015</option>
<option value="2016">2016</option>
<option value="2017">2017</option>
<option value="2018">2018</option>
<option value="2019">2019</option>
<option value="2020">2020</option>
<option value="2021">2021</option>
<option value="2022">2022</option>
<option value="2023">2023</option>
<option value="2024">2024</option>
<option value="2025">2025</option>
</select>
</div>
</div>
<div class="control-group">
<label class="control-label" for="cvc">Security code (CVC):</label>
<div class="controls">
<input type="text" class="input-min span1" id="cvc">
</div>
</div>
</div>
<div class="form-actions">
{% csrf_token %}
<button type="submit" class="btn btn-success">Create team &raquo;</button>
<div class="security">
<a href="https://stripe.com/help/security">Secure</a> by default. Every Snipt page is secure.
</div>
<div class="stripe" style="margin-left: 125px;">
Your credit card is stored securely with <a href="https://stripe.com">Stripe</a> and we use <a href="https://stripe.com/docs/stripe.js">Stripe.js</a> for maximum security.
</div>
</div>
{% endif %}
</fieldset>
</form>
{% endblock %}
{% block analytics %}
{% if not debug %}
window.ll('tagScreen', 'Team beta signup view');
{% endif %}
{% endblock %}

View File

@ -0,0 +1,64 @@
{% extends "base.html" %}
{% load team_tags %}
{% block page-title %}Team Billing{% endblock %}
{% block body-class %}account {{ 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 }}/billing/">Billing</a></li>
{% endblock %}
{% block content %}
<section class="snipts" id="snipts"></section>
<section class="profile group">
<aside>
<ul class="nav nav-list ng-cloak" ng-cloak>
<li class="nav-header">Team: {{ team.name }}</li>
<li>
<a href="/{{ team.slug }}/">Profile</a>
</li>
<li>
<a href="/{{ team.slug }}/members/">Members</a>
</li>
<li class="active">
<a href="/{{ team.slug }}/billing/">Billing</a>
</li>
</ul>
</aside>
<section class="content">
<div class="def" data-title="Plan">
{{ name }}
</div>
<div class="def" data-title="Price">
${{ amount|currency_convert }}.00 USD / {{ interval }}
</div>
<div class="def" data-title="Card">
xxxx-xxxx-xxxx-{{ last4 }}
</div>
<div class="def" data-title="Status">
{{ status }}
</div>
<div class="def" data-title="Team since">
{{ team.created|date:'M d, Y' }}
</div>
<div class="def" data-title="Next bill date" ng-show="user.stripeAccount.status != 'inactive'">
{{ nextBill|to_date|date:'M d, Y' }}
</div>
<form class="alert alert-info group" style="margin: 15px; padding-right: 8px;" id="cancel-team-subscription" action="/{{ team.slug }}/billing/cancel/" method="post">
{% csrf_token %}
<button type="submit" class="btn btn-danger pull-right">
Cancel subscription
</button>
</form>
</section>
</section>
{% endblock %}
{% block analytics %}
{% if not debug %}
window.ll('tagScreen', 'Team billing view');
{% endif %}
{% endblock %}

View File

@ -0,0 +1,83 @@
{% extends "base.html" %}
{% load snipt_tags %}
{% block page-title %}Team Members{% endblock %}
{% block body-class %}account {{ 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 %}
<div ng-controller="TeamController" class="team-controller">
<section class="snipts" id="snipts"></section>
<section class="profile group">
<aside>
<ul class="nav nav-list ng-cloak" ng-cloak>
<li class="nav-header">Team: {{ team.name }}</li>
<li>
<a href="/{{ team.slug }}/">Profile</a>
</li>
<li class="active">
<a href="/{{ team.slug }}/members/">Members</a>
</li>
{% if team.owner == request.user %}
<li>
<a href="/{{ team.slug }}/billing/">Billing</a>
</li>
{% endif %}
</ul>
</aside>
<section class="content">
{% if 'limit-reached' in request.GET %}
<p class="alert alert-error group">
You have no seats available to add this member.
To upgrade your plan, contact <a href="mailto:support@snipt.net">support@snipt.net</a>.
</p>
{% endif %}
<div class="def" data-title="Owner">
{{ team.owner }}
</div>
{% if team.owner == request.user %}
<div class="def add-member" data-title="Add member">
<input
ng-model="search"
placeholder="Search users..."
type="search"
value=""
/>
<ul ng-cloak ng-if="users.length">
<li ng-repeat="user in users" class="user">
<img src="https://secure.gravatar.com/avatar/{[{ user.email_md5 }]}?s=26" />
<a href="/{[{ user.username }]}/"><span>{[{ user.username }]}</span></a>
<a class="btn btn-small" href="/{{ team.slug }}/members/add/{[{ user.username }]}/">Add &raquo;</a>
</li>
</ul>
</div>
{% endif %}
<div class="def" data-title="Members ({{ team.members.all|length }} of {{ team.member_limit }})">
<ul class="member-list">
{% for member in team.members.all %}
<li class="user">
<img src="https://secure.gravatar.com/avatar/{{ member.email|md5 }}?s=26" />
<a href="/{{ member.username }}/"><span>{{ member.username }}</span></a>
{% if team.owner == request.user %}
<a class="btn btn-small" href="/{{ team.slug }}/members/remove/{{ member.username }}/">Remove &raquo;</a>
{% endif %}
</li>
{% endfor %}
</ul>
</div>
</section>
</section>
</div>
{% endblock %}
{% block analytics %}
{% if not debug %}
window.ll('tagScreen', 'Team members view');
{% endif %}
{% endblock %}

View File

View File

@ -0,0 +1,20 @@
import datetime
from django import template
register = template.Library()
@register.filter
def user_is_member(team, user):
return team.user_is_member(user)
@register.filter
def currency_convert(amount):
return amount / 100
@register.filter
def to_date(timestamp):
return datetime.datetime.fromtimestamp(float(timestamp))

23
teams/urls.py Normal file
View File

@ -0,0 +1,23 @@
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/remove/(?P<member>[^/]+)/$',
views.remove_team_member,
name='remove-team-member'),
url(r'^(?P<username>[^/]+)/members/add/(?P<member>[^/]+)/$',
views.add_team_member,
name='add-team-member'),
url(r'^(?P<username>[^/]+)/members/$',
views.team_members,
name='team-members'),
url(r'^(?P<username>[^/]+)/billing/$',
views.team_billing,
name='team-billing'),
url(r'^(?P<username>[^/]+)/billing/cancel/$',
views.cancel_team_subscription,
name='team-cancel-subscription'))

178
teams/views.py Normal file
View File

@ -0,0 +1,178 @@
import os
import stripe
import uuid
from annoying.decorators import render_to
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.core.mail import send_mail
from django.http import Http404, HttpResponseRedirect, HttpResponseBadRequest
from django.shortcuts import get_object_or_404
from teams.models import Team
@render_to('teams/for-teams.html')
def for_teams(request):
if request.user.is_authenticated():
profile = request.user.profile
profile.teams_beta_seen = True
profile.save()
return {}
@login_required
@render_to('teams/for-teams-complete.html')
def for_teams_complete(request):
if request.method == 'POST' and request.user.is_authenticated():
token = request.POST['token']
stripe.api_key = os.environ.get('STRIPE_SECRET_KEY',
settings.STRIPE_SECRET_KEY)
plan = request.POST['plan']
try:
customer = stripe.Customer.create(card=token,
plan=plan,
email=request.user.email)
except stripe.CardError, e:
error_message = e.json_body['error']['message']
return HttpResponseRedirect('/for-teams/?declined=%s' %
error_message or
'Your card was declined.')
team = Team(name=request.POST['team-name'],
email=request.POST['email'],
plan=plan,
owner=request.user)
team.stripe_id = customer.id
team.save()
user = User.objects.create_user(team.slug,
team.email,
str(uuid.uuid4()))
team.user = user
team.save()
send_mail('[Snipt] New team signup: {}'.format(team.name),
"""
Team: https://snipt.net/{}
Email: {}
Plan: {}
""".format(team.slug, team.email, team.plan),
'support@snipt.net',
['nick@snipt.net'],
fail_silently=False)
return {
'team': team
}
else:
return HttpResponseBadRequest()
@login_required
@render_to('teams/team-billing.html')
def team_billing(request, username):
team = get_object_or_404(Team, slug=username, disabled=False)
if team.owner != request.user:
raise Http404
stripe.api_key = os.environ.get('STRIPE_SECRET_KEY',
settings.STRIPE_SECRET_KEY)
customer = stripe.Customer.retrieve(team.stripe_id)
data = {
'last4': customer.active_card.last4,
'created': customer.created,
'email': customer.email,
'team': team
}
if customer.subscription:
data['amount'] = customer.subscription.plan.amount
data['interval'] = customer.subscription.plan.interval
data['name'] = customer.subscription.plan.name
data['status'] = customer.subscription.status
data['nextBill'] = customer.subscription.current_period_end
else:
data['status'] = 'inactive'
return data
return {
'team': team
}
@login_required
@render_to('teams/team-members.html')
def team_members(request, username):
team = get_object_or_404(Team, slug=username, disabled=False)
if not team.user_is_member(request.user):
raise Http404
return {
'team': team
}
@login_required
def add_team_member(request, username, member):
team = get_object_or_404(Team, slug=username, disabled=False)
user = get_object_or_404(User, username=member)
if (team.owner != request.user):
raise Http404
if ((team.members.all().count() + 1) > team.member_limit):
return HttpResponseRedirect('/' + team.slug +
'/members/?limit-reached')
else:
team.members.add(user)
return HttpResponseRedirect('/' + team.slug + '/members/')
@login_required
def remove_team_member(request, username, member):
team = get_object_or_404(Team, slug=username, disabled=False)
user = get_object_or_404(User, username=member)
if (team.owner != request.user):
raise Http404
team.members.remove(user)
return HttpResponseRedirect('/' + team.slug + '/members/')
@login_required
def cancel_team_subscription(request, username):
if request.method != 'POST':
raise Http404
team = get_object_or_404(Team, slug=username, disabled=False)
if team.owner != request.user:
raise Http404
stripe.api_key = os.environ.get('STRIPE_SECRET_KEY',
settings.STRIPE_SECRET_KEY)
customer = stripe.Customer.retrieve(team.stripe_id)
customer.delete()
team.disabled = True
team.stripe_id = None
team.plan = None
team.save()
send_mail('[Snipt] Team cancelled plan: {}'.format(team.name),
"""
Team: https://snipt.net/{}
Email: {}
""".format(team.slug, team.email),
'support@snipt.net',
['nick@snipt.net'],
fail_silently=False)
return HttpResponseRedirect('/' + team.slug + '/?team-cancelled=true')

View File

@ -1,4 +1,4 @@
<a href="/signup" class="snipt-promo">
<button class="btn btn-success btn-large pull-right">Sign up &raquo;</button>
Sign up for Snipt!<br /><span style="font-size: 16px;">Post public snipts for free. Private snipts just $5/mo.</span>
Sign up for Snipt!<br /><span style="font-size: 16px;">Post public snipts for free.</span>
</a>

26
templates/add-snipt.html Normal file
View File

@ -0,0 +1,26 @@
<li class="add-snipt">
<button class="btn btn-info btn-large" id="add-snipt">
Add {% if request.user.username == 'blog' %}Post{% else %}Snipt{% endif %}
<i class="icon-search icon-plus icon-white"></i>
</button>
{% if request.user.profile.has_teams %}
<ul class="add-snipt-teams">
<li>
<a href data-intended-user="{{ request.user.username }}">
<i class="icon-user icon-white"></i>
<span class="as">as</span>
<span>{{ request.user.username }}</span>
</a>
</li>
{% for team in request.user.profile.teams|dictsort:'name' %}
<li>
<a href data-intended-user="{{ team.user.username }}">
<i class="icon-user icon-white"></i>
<span class="as">under</span>
<span>{{ team.name }}</span>
</a>
</li>
{% endfor %}
</ul>
{% endif %}
</li>

View File

@ -68,14 +68,14 @@
<script src="https://www.google.com/recaptcha/api.js"></script>
</head>
<body class="{% block body-class %}{% endblock %} {% if request.user.profile.is_pro %}is-pro{% endif %}" ng-controller="AppController">
<body class="{% block body-class %}{% endblock %} {% if request.user.profile.has_pro %}is-pro{% endif %}" ng-controller="AppController">
{% block header %}
<header class="main">
<div class="inner">
<div class="shadey"></div>
<h1 class="main-logo"><a href="{% if request.user.is_authenticated %}/{{ request.user.username }}/{% else %}/{% endif %}">snip<span>t</span></a></h1>
<form class="search" action="/search/" method="get" ng-controller="HeaderSearchController">
<form class="search" action="/search/" method="get">
<fieldset>
<div class="fields">
<input ng-model="search.query" type="text" class="search-query" name="q"
@ -106,9 +106,33 @@
</li>
{% block add-snipt %}{% endblock %}
{% endif %}
<li>
<a href="/for-teams/" {% if '/for-teams/' in request.path %} class="active"{% endif %}>Teams</a>
</li>
{% if not request.user.is_authenticated %}
<li>
<a href="/for-teams/" class="{% if '/for-teams/' in request.path %}active{% endif %}">Teams</a>
</li>
{% else %}
<li class="teams-nav">
<a href="#" class="teams-nav {% if '/for-teams/' in request.path %}active{% endif %}">Teams</a>
<ul>
{% if request.user.profile.has_teams %}
{% for team in request.user.profile.teams|dictsort:'name' %}
<li>
<a href="/{{ team.user.username }}/">
<i class="icon-user icon-white"></i>
{{ team.name }}
</a>
</li>
{% endfor %}
{% endif %}
<li>
<a href="/for-teams/">
<i class="icon-plus icon-white"></i>
Create new team
</a>
</li>
</ul>
</li>
{% endif %}
</ul>
</nav>
{% if request.user.is_authenticated %}
@ -119,7 +143,7 @@
<span class="username">{{ request.user.username }}</span>
<i class="icon-cog icon-white"></i>
<span class="type">
{% if request.user.profile.is_pro %}
{% if request.user.profile.has_pro %}
<span class="is-pro">Pro</span>
{% else %}
Basic member
@ -146,7 +170,7 @@
Account
</a>
</li>
{% if not request.user.profile.is_pro %}
{% if not request.user.profile.has_pro %}
<li>
<a href="/pro/">
<i class="icon-star-empty icon-white"></i>
@ -198,7 +222,7 @@
{% block aside %}
<aside class="main">
{% block ad %}
{% if not request.user.profile.is_pro %}
{% if not request.user.profile.has_pro %}
{% include 'ad-sidebar.html' %}
{% endif %}
{% endblock %}
@ -212,7 +236,7 @@
<li class="twitter">
<a href="https://twitter.com/#!/snipt"><span>@snipt</span></a>
</li>
{% if not request.user.profile.is_pro %}
{% if not request.user.profile.has_pro %}
<li class="pro">
<a href="/pro/"><span>Go Pro</span></a>
</li>
@ -340,14 +364,6 @@
<td>p</td>
<td>Previous page</td>
</tr>
<tr>
<td>g</td>
<td>Scroll to top of page</td>
</tr>
<tr>
<td>G</td>
<td>Scroll to bottom of page</td>
</tr>
</tbody>
</table>
</div>
@ -364,6 +380,12 @@
window.user_ip = '{{ request.META.REMOTE_ADDR }}';
window.user_profile_id = {% firstof request.user.profile.id 'null' %};
window.user_email = '{{ request.user.email }}';
window.teams = [
{% for team in request.user.profile.teams %}
'{{ team.slug }}',
{% endfor %}
]
window.intended_user = '{{ request.user.username }}';
{% if public %}
window.pub = {{ public|lower }};
@ -373,10 +395,10 @@
window.api_key = '{{ request.user.api_key.key }}';
{% endblock %}
{% if request.user.profile.is_pro %}
window.user_is_pro = true;
{% if request.user.profile.has_pro %}
window.user_has_pro = true;
{% else %}
window.user_is_pro = false;
window.user_has_pro = false;
{% endif %}
window.default_editor = '{{ request.user.profile.get_default_editor_display|lower }}';
window.editor_theme = '{{ request.user.profile.editor_theme }}';
@ -398,11 +420,11 @@
<script type="text/javascript" src="{{ STATIC_URL }}js/libs/codemirror.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}js/libs/highlight.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}js/src/application.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}js/src/team.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}js/src/modules/site.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}js/src/modules/snipt.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}js/src/account.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}js/src/snipts.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}js/src/search.js"></script>
{% else %}
<script type="text/javascript" src="{{ STATIC_URL }}js/snipt-all.min.js?74"></script>
{% endif %}
@ -433,7 +455,7 @@
custom_data: {
'snipts count': {% snipts_count_for_user %},
'profile link': 'https://snipt.net/{{ request.user.username }}/',
'is pro': window.user_is_pro,
'is pro': window.user_has_pro,
'blog domain': '{{ request.user.profile.blog_domain }}',
'pro date': {% firstof request.user.profile.pro_date|date:"U" 'null' %},
'username': '{{ request.user.username }}'
@ -470,7 +492,7 @@
userId: {{ request.user.id }},
createdAt: '{{ request.user.date_joined }}',
sniptsCount: {% snipts_count_for_user %},
isPro: window.user_is_pro,
isPro: window.user_has_pro,
blogDomain: '{{ request.user.profile.blog_domain }}',
proDate: '{% firstof request.user.profile.pro_date 'null' %}'
});

View File

@ -1,76 +0,0 @@
{% extends "base.html" %}
{% block page-title %}Request beta access to Snipt for Teams{% endblock %}
{% block body-class %}{{ block.super }} static signup pro pro-signup{% endblock %}
{% block breadcrumb %}
<li><a href="/for-teams/">Snipt for Teams</a></li>
{% endblock %}
{% block content %}
{% if request.user.profile.teams_beta_applied %}
<div class="alert alert-success" style="margin: 30px;">
You've successfully applied for the beta program - we'll email you as soon as we're ready!
</div>
{% else %}
<form class="form-horizontal static-box" id="teams-signup" method="post" action="/for-teams/complete/">
<fieldset>
<div class="info">
Snipt for Teams
<ul class="features">
<li>Team profile at snipt.net/{team-name}.</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>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>
</ul>
</div>
{% if not request.user.is_authenticated %}
<div class="control-group">
<label class="control-label" for="name">Your name:</label>
<div class="controls">
<input required type="text" class="input-xlarge" name="username" id="username" />
</div>
</div>
<div class="control-group">
<label class="control-label" for="name">Your email:</label>
<div class="controls">
<input required type="email" class="input-xlarge" name="email" id="email" />
</div>
</div>
{% endif %}
<div class="control-group">
<label class="control-label" for="name">Team name:</label>
<div class="controls">
<input required type="text" class="input-xlarge" name="name" id="name" />
</div>
</div>
<div class="control-group">
<label class="control-label" for="members">How many users?</label>
<div class="controls">
<input required type="number" class="input-medium" name="members" id="members" />
</div>
</div>
<div class="control-group">
<label style="float: none; width: auto; text-align: left; padding-left: 180px; margin-bottom: 10px;" class="control-label" for="members">Tell us a bit about your organization and how<br />you'd like to use Snipt for your team:</label>
<div class="controls">
<textarea required name="info" name="info" id="info" class="input-xlarge" style="height: 100px;"></textarea>
</div>
</div>
<div style="margin-left: 180px;" class="g-recaptcha" data-sitekey="6LerYA0TAAAAAFJaMf7JMnlQR2wzqd_3dMRvLd-4"></div>
<div class="form-actions">
{% csrf_token %}
<button type="submit" class="btn btn-success">Request more info &raquo;</button>
</div>
</fieldset>
</form>
{% endif %}
{% endblock %}
{% block analytics %}
{% if not debug %}
window.ll('tagScreen', 'Team beta signup view');
{% endif %}
{% endblock %}

View File

@ -40,86 +40,101 @@
Group discounts available. Email <a href="mailto:support@snipt.net">support@snipt.net</a> for details.
</p>
</div>
<div class="payment-loading"><span>Please wait&hellip;</span></div>
<div class="payment-errors alert alert-error"></div>
<div class="control-group">
<label class="control-label" for="name">Payment plan:</label>
<div class="controls">
<select name="plan" type="text" class="input-medium" id="plan">
<option value="snipt-pro-monthly">$5/month</option>
<option value="snipt-pro-yearly">$60/year</option>
</select>
{% if not request.user.is_authenticated %}
<div class="login-first">
<span>
To go <span class="pro">Pro</span>, <a href="/signup/?next=/pro/">sign up</a> or <a href="/login/?next=/pro/">log in</a>.
</span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="name">Name on card:</label>
<div class="controls">
<input type="text" class="input-xlarge" id="name" />
{% elif request.user.profile.has_pro %}
<div class="login-first">
<span>
{% if request.user.profile.is_pro %}
You're already a <span class="pro">Pro</span>.
<a style="display: block; margin-top: 15px;" href="/account/billing/">View details &raquo;</a>
{% else %}
<p style="line-height: 26px;">You're a member of a team, and all team members automatically have <span class="pro">Pro</span> access.</p>
{% endif %}
</span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="number">Card number:</label>
<div class="controls cards">
<input type="text" class="input-xlarge" id="number" />
<img src="{{ STATIC_URL }}img/card-visa.png" alt="Visa" />
<img src="{{ STATIC_URL }}img/card-mastercard.png" alt="MasterCard" />
<img src="{{ STATIC_URL }}img/card-discover.png" alt="Discover" />
<img src="{{ STATIC_URL }}img/card-american-express.png" alt="American Express" />
{% else %}
<div class="payment-form">
<div class="payment-loading"><span>Please wait&hellip;</span></div>
<div class="payment-errors alert alert-error"></div>
<div class="control-group">
<label class="control-label" for="name">Payment plan:</label>
<div class="controls">
<select name="plan" type="text" class="input-medium" id="plan">
<option value="snipt-pro-monthly">$5/month</option>
<option value="snipt-pro-yearly">$60/year</option>
</select>
</div>
</div>
<div class="control-group">
<label class="control-label" for="number">Card number:</label>
<div class="controls cards">
<input type="text" class="input-xlarge" id="number" />
<img src="{{ STATIC_URL }}img/card-visa.png" alt="Visa" />
<img src="{{ STATIC_URL }}img/card-mastercard.png" alt="MasterCard" />
<img src="{{ STATIC_URL }}img/card-discover.png" alt="Discover" />
<img src="{{ STATIC_URL }}img/card-american-express.png" alt="American Express" />
</div>
</div>
<div class="control-group">
<label class="control-label" for="exp-month">Expiration date:</label>
<div class="controls">
<select id="exp-month" class="span2 exp-month">
<option value="">----</option>
<option value="01">01 - January</option>
<option value="02">02 - February</option>
<option value="03">03 - March</option>
<option value="04">04 - April</option>
<option value="05">05 - May</option>
<option value="06">06 - June</option>
<option value="07">07 - July</option>
<option value="08">08 - August</option>
<option value="09">09 - September</option>
<option value="10">10 - October</option>
<option value="11">11 - November</option>
<option value="12">12 - December</option>
</select>
<select id="exp-year" class="span2">
<option value="">----</option>
<option value="2015">2015</option>
<option value="2016">2016</option>
<option value="2017">2017</option>
<option value="2018">2018</option>
<option value="2019">2019</option>
<option value="2020">2020</option>
<option value="2021">2021</option>
<option value="2022">2022</option>
<option value="2023">2023</option>
<option value="2024">2024</option>
<option value="2025">2025</option>
</select>
</div>
</div>
<div class="control-group">
<label class="control-label" for="cvc">Security code (CVC):</label>
<div class="controls">
<input type="text" class="input-min span1" id="cvc">
</div>
</div>
</div>
</div>
<div class="control-group">
<label class="control-label" for="exp-month">Expiration date:</label>
<div class="controls">
<select id="exp-month" class="span2 exp-month">
<option value="">----</option>
<option value="01">01 - January</option>
<option value="02">02 - February</option>
<option value="03">03 - March</option>
<option value="04">04 - April</option>
<option value="05">05 - May</option>
<option value="06">06 - June</option>
<option value="07">07 - July</option>
<option value="08">08 - August</option>
<option value="09">09 - September</option>
<option value="10">10 - October</option>
<option value="11">11 - November</option>
<option value="12">12 - December</option>
</select>
<select id="exp-year" class="span2">
<option value="">----</option>
<option value="2014">2014</option>
<option value="2015">2015</option>
<option value="2016">2016</option>
<option value="2017">2017</option>
<option value="2018">2018</option>
<option value="2019">2019</option>
<option value="2020">2020</option>
<option value="2021">2021</option>
<option value="2022">2022</option>
<option value="2022">2023</option>
<option value="2022">2024</option>
</select>
<div class="form-actions">
{% csrf_token %}
<button type="submit" class="btn btn-success">Subscribe &raquo;</button>
<div class="security">
<a href="https://stripe.com/help/security">Secure</a> by default. Every Snipt page is secure.
</div>
<div class="stripe">
Your credit card is stored securely with <a href="https://stripe.com">Stripe</a> and we use <a href="https://stripe.com/docs/stripe.js">Stripe.js</a> for maximum security.
</div>
</div>
</div>
<div class="control-group">
<label class="control-label" for="cvc">Security code (CVC):</label>
<div class="controls">
<input type="text" class="input-min span1" id="cvc">
<div class="form-actions" style="color: #A2A2A2;">
Prefer to pay with PayPal? Email <a href="mailto:support@snipt.net">support@snipt.net</a>.
</div>
</div>
<div class="form-actions">
{% csrf_token %}
<button type="submit" class="btn btn-success">Subscribe &raquo;</button>
<div class="security">
<a href="https://stripe.com/help/security">Secure</a> by default. Every Snipt page is secure.
</div>
<div class="stripe">
Your credit card is stored securely with <a href="https://stripe.com">Stripe</a> and we use <a href="https://stripe.com/docs/stripe.js">Stripe.js</a> for maximum security.
</div>
</div>
<div class="form-actions" style="color: #A2A2A2;">
Prefer to pay with PayPal? Email <a href="mailto:support@snipt.net">support@snipt.net</a>.
</div>
{% endif %}
</fieldset>
</form>
{% endblock %}

View File

@ -1,33 +1,81 @@
{% load snipt_tags %}
{% load snipt_tags team_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 }}
{% if user.profile.is_a_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">
<a href="/{{ user.username }}/members/">
{{ user.team.member_count }} member{{ user.team.member_count|pluralize }}
</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>
</div>
</div>
{% if user.team|user_is_member:request.user %}
<div class="profile team-settings group">
<div class="team-search">
<form action="/search/" method="get">
<input type="text" class="text" value="" name="q" placeholder="Search team snipts...">
<input type="hidden" class="text" value="{{ user.username }}" name="author">
</form>
</div>
</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 %}
{% if user.team.owner == request.user %}
<div class="profile team-settings group">
<div class="meta">
<span class="title">Team settings</span>
<a href="/{{ user.username }}/billing/">Billing &raquo;</a>
<a href="/{{ user.username }}/members/">Members &raquo;</a>
</div>
</div>
{% endif %}
{% 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.has_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

@ -22,15 +22,47 @@
{% block content %}
<section class="snipts" id="snipts"></section>
<div class="static-box {% if page.object_list|length > 0 %}has-snipts{% endif %}">
<form method="get" class="form-search" action="." ng-controller="SearchController">
<input ng-model="search.query" type="text" class="search-query" name="q"
ng-init="search.query='{{ query|escapejs }}'"
placeholder="Search snipts" id="id_q"
value="{{ query }}" />
<label class="checkbox inline mine-only" ng-click="toggleMineOnly()">
<input {% if '--mine' in query %}checked="checked"{% endif %} ng-model="search.mineOnly" type="checkbox" id="inlineCheckbox1" value="option1"> Mine only
</label>
<button type="submit" class="btn">Search</button>
<form method="get" class="form-search" action=".">
{% if not request.user.profile.has_teams %}
<input type="text" class="search-query" name="q"
placeholder="Search snipts" id="id_q"
value="{{ query }}" />
{% if request.user.is_authenticated %}
<label class="checkbox inline mine-only">
<input {% if 'mine-only' in request.GET %}checked{% endif %} type="checkbox" name="mine-only"> Mine only
</label>
{% endif %}
{% else %}
<div class="with-teams-search">
<input type="text" class="search-query" name="q"
placeholder="Search snipts" id="id_q"
value="{{ query }}"
/>
<select name="author">
<option
{% if not request.GET.author or request.GET.author == '' %}selected{% endif %}
value=""
>
Mine and all public
</option>
<option
{% if request.GET.author == request.user.username %}selected{% endif %}
value="{{ request.user.username }}"
>
Mine only
</option>
{% for team in request.user.profile.teams %}
<option
{% if request.GET.author == team.slug %}selected{% endif %}
value="{{ team.slug }}"
>
{{ team.name }}
</option>
{% endfor %}
</select>
</div>
{% endif %}
<button type="submit" class="btn">Search</button>
</form>
</div>
{% if query %}

View File

@ -12,7 +12,7 @@ from snipts.views import search
from tastypie.api import Api
from utils.views import SniptRegistrationView
from views import (homepage, lexers, login_redirect, pro, sitemap, tags,
pro_complete, user_api_key, for_teams, for_teams_complete)
pro_complete, user_api_key)
public_api = Api(api_name='public')
public_api.register(PublicSniptResource())
@ -46,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),
@ -64,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', {

View File

@ -2,7 +2,6 @@ import datetime
import hashlib
import os
import stripe
import requests
from accounts.models import UserProfile
from annoying.decorators import ajax_request, render_to
@ -20,74 +19,6 @@ from snipts.utils import get_lexers_list
from taggit.models import Tag
@render_to('for-teams.html')
def for_teams(request):
if request.user.is_authenticated():
profile = request.user.profile
profile.teams_beta_seen = True
profile.save()
return {}
@render_to('for-teams-complete.html')
def for_teams_complete(request):
if request.method == 'POST':
if 'g-recaptcha-response' not in request.POST:
return HttpResponseBadRequest()
payload = {
'secret': settings.RECAPTCHA_SECRET,
'response': request.POST['g-recaptcha-response'],
'remoteip': request.META.get('REMOTE_ADDR')
}
r = requests.post('https://www.google.com/recaptcha/api/siteverify',
data=payload)
if not r.json()['success']:
return HttpResponseBadRequest()
if request.user.is_authenticated():
name = request.POST['name']
members = request.POST['members']
info = request.POST['info']
send_mail('[Snipt] New Snipt for Teams beta request.', """
User: %s (%s)
Team name: %s
Team members: %s
Info:
%s
""" % (request.user.username, request.user.email, name, members,
info), 'support@snipt.net',
['nick@nicksergeant.com'], fail_silently=False)
profile = request.user.profile
profile.teams_beta_applied = True
profile.save()
else:
username = request.POST['username']
email = request.POST['email']
name = request.POST['name']
members = request.POST['members']
info = request.POST['info']
send_mail('[Snipt] New Snipt for Teams beta request.', """
User: %s (%s) (not authenticated)
Team name: %s
Team members: %s
Info:
%s
""" % (username, email, name, members, info), 'support@snipt.net',
['nick@nicksergeant.com'], fail_silently=False)
return {}
else:
return HttpResponseBadRequest()
@render_to('homepage.html')
def homepage(request):
@ -152,11 +83,8 @@ def login_redirect(request):
return HttpResponseRedirect('/')
@login_required
@render_to('pro.html')
def pro(request):
if request.user.profile.is_pro:
return HttpResponseRedirect('/' + request.user.username + '/')
return {}
@ -190,6 +118,16 @@ def pro_complete(request):
profile.stripe_id = customer.id
profile.save()
send_mail('[Snipt] New Pro signup: {}'.format(request.user.username),
"""
User: https://snipt.net/{}
Email: {}
Plan: {}
""".format(request.user.username, request.user.email, plan),
'support@snipt.net',
['nick@snipt.net'],
fail_silently=False)
return {}
else: