commit
e846b4d753
12
Makefile
12
Makefile
|
@ -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
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 »</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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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."""
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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&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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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,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)
|
|
@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -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,
|
||||
),
|
||||
]
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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')]),
|
||||
),
|
||||
]
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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')]),
|
||||
),
|
||||
]
|
|
@ -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
|
|
@ -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 %}
|
|
@ -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…</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 »</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 %}
|
|
@ -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 %}
|
|
@ -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 »</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 »</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block analytics %}
|
||||
{% if not debug %}
|
||||
window.ll('tagScreen', 'Team members view');
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -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))
|
|
@ -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'))
|
|
@ -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')
|
|
@ -1,4 +1,4 @@
|
|||
<a href="/signup" class="snipt-promo">
|
||||
<button class="btn btn-success btn-large pull-right">Sign up »</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>
|
||||
|
|
|
@ -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>
|
|
@ -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' %}'
|
||||
});
|
||||
|
|
|
@ -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 »</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block analytics %}
|
||||
{% if not debug %}
|
||||
window.ll('tagScreen', 'Team beta signup view');
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -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…</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 »</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…</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 »</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 »</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 %}
|
||||
|
|
|
@ -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 »</a>
|
||||
<a href="/{{ user.username }}/members/">Members »</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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
6
urls.py
6
urls.py
|
@ -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', {
|
||||
|
|
82
views.py
82
views.py
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue