registration and other stuff

This commit is contained in:
Matthew Scragg 2013-10-03 09:58:07 -05:00
parent 613d1c6ca3
commit cba28239a2
13 changed files with 326 additions and 50 deletions

View file

@ -4,11 +4,12 @@ import time
import redis import redis
import rethinkdb as rdb import rethinkdb as rdb
from flask import Flask, request, render_template, url_for, redirect from flask import Flask, request, render_template, url_for, redirect, flash, session
from flask.ext.bcrypt import Bcrypt from flask.ext.bcrypt import Bcrypt
from flask.ext.login import LoginManager from flask.ext.login import LoginManager, login_user, logout_user
from flask.ext.assets import Environment from flask.ext.assets import Environment
from recaptcha.client import captcha from recaptcha.client import captcha
from werkzeug.routing import BaseConverter
import config import config
from session import RedisSessionInterface from session import RedisSessionInterface
@ -16,12 +17,18 @@ from wiki import Wiki
from util import to_canonical, remove_ext from util import to_canonical, remove_ext
class RegexConverter(BaseConverter):
def __init__(self, url_map, *items):
super(RegexConverter, self).__init__(url_map)
self.regex = items[0]
app = Flask(__name__) app = Flask(__name__)
app.config.update(config.flask) app.config.update(config.flask)
app.debug = (config.ENV is not 'PROD') app.debug = (config.ENV is not 'PROD')
app.secret_key = config.secret_key app.secret_key = config.secret_key
app.static_path = os.sep + 'static' app.static_path = os.sep + 'static'
app.session_interface = RedisSessionInterface() app.session_interface = RedisSessionInterface()
app.url_map.converters['regex'] = RegexConverter
bcrypt = Bcrypt(app) bcrypt = Bcrypt(app)
@ -45,7 +52,13 @@ if not config.db['dbname'] in rdb.db_list().run(conn) and config.ENV is not 'PRO
repo_dir = config.repo['dir'] repo_dir = config.repo['dir']
from models import Site # This is down here because of dependencies above
from models import Site, User, CurrentUser
@login_manager.user_loader
def load_user(user_id):
return CurrentUser(user_id)
w = Wiki(repo_dir) w = Wiki(repo_dir)
@ -54,6 +67,14 @@ def redirect_url():
return request.args.get('next') or request.referrer or url_for('index') return request.args.get('next') or request.referrer or url_for('index')
def validate_captcha():
response = captcha.submit(
request.form['recaptcha_challenge_field'],
request.form['recaptcha_response_field'],
app.config['RECAPTCHA_PRIVATE_KEY'],
request.remote_addr)
return response.is_valid
@app.template_filter('datetime') @app.template_filter('datetime')
def _jinja2_filter_datetime(ts): def _jinja2_filter_datetime(ts):
return time.strftime('%b %d, %Y %I:%M %p', time.localtime(ts)) return time.strftime('%b %d, %Y %I:%M %p', time.localtime(ts))
@ -72,7 +93,18 @@ def page_error(e):
@app.route("/") @app.route("/")
def root(): def root():
return redirect('/home') return render('home')
#return redirect('/home')
@app.route("/account/")
def account():
return render_template('account/index.html')
@app.route("/logout/")
def logout():
logout_user()
del session['user']
return redirect(url_for('root'))
@app.route("/commit/<sha>/<name>") @app.route("/commit/<sha>/<name>")
def commit_sha(name, sha): def commit_sha(name, sha):
@ -85,18 +117,29 @@ def commit_sha(name, sha):
return redirect('/create/'+cname) return redirect('/create/'+cname)
@app.route("/compare/<name>/<regex('[^.]+'):fsha><regex('\.{2,3}'):dots><regex('.+'):lsha>")
def compare(name, fsha, dots, lsha):
diff = w.compare(name, fsha, lsha)
return render_template('page/compare.html', name=name, diff=diff)
@app.route("/register", methods=['GET', 'POST']) @app.route("/register", methods=['GET', 'POST'])
def register(): def register():
if request.method == 'POST': if request.method == 'POST':
response = captcha.submit( user = User()
request.form['recaptcha_challenge_field'], if user.get_by_email(request.form['email']):
request.form['recaptcha_response_field'], flash('Email is already taken')
app.config['RECAPTCHA_PRIVATE_KEY'], return redirect('/register')
request.remote_addr) if user.get_by_username(request.form['username']):
if not response.is_valid: flash('Username is already taken')
return redirect('/register?fail') return redirect('/register')
else:
return redirect("/") # Create user and login
u = User.create(email=request.form['email'].lower(),
username=request.form['username'],
password=bcrypt.generate_password_hash(request.form['password']))
login_user(u)
return redirect("/")
else: else:
return render_template('account/register.html') return render_template('account/register.html')
@ -104,7 +147,11 @@ def register():
@app.route("/login", methods=['GET', 'POST']) @app.route("/login", methods=['GET', 'POST'])
def login(): def login():
if request.method == 'POST': if request.method == 'POST':
pass if User.auth(request.form['email'], request.form['password']):
return redirect("/")
else:
flash("Email or Password invalid")
return redirect("/login")
else: else:
return render_template('account/login.html') return render_template('account/login.html')

View file

@ -1,6 +1,18 @@
import rethinkdb as rdb
from flask import session
from flask.ext.login import login_user
from rethinkORM import RethinkModel from rethinkORM import RethinkModel
from reimagine import conn, bcrypt
from reimagine import conn
def to_dict(cur, first=False):
ret = []
for row in cur:
ret.append(row)
if ret and first:
return ret[0]
else:
return ret
class BaseModel(RethinkModel): class BaseModel(RethinkModel):
@ -14,14 +26,63 @@ class BaseModel(RethinkModel):
def create(cls, **kwargs): def create(cls, **kwargs):
return super(BaseModel, cls).create(**kwargs) return super(BaseModel, cls).create(**kwargs)
def get_all(self, arg, index):
return rdb.table(self.table).get_all(arg, index=index).run(self._conn)
def get_one(self, arg, index):
return rdb.table(self.table).get_all(arg, index=index).limit(1).run(self._conn)
class Site(BaseModel): class Site(BaseModel):
table = 'sites' table = 'sites'
class CurrentUser():
id = None
def __init__(self, id):
self.id = id
def get_id(self):
return self.id
def is_active(self):
return True
def is_anonymous(self):
return False if self.id else True
def is_authenticated(self):
return True if self.id else False
class User(BaseModel): class User(BaseModel):
table = 'users' table = 'users'
def get_by_email(self, email):
return to_dict(self.get_one(email, 'email'), True)
def get_by_username(self, username):
return to_dict(self.get_one(username, 'username'), True)
def login(self, login, password): def login(self, login, password):
pass pass
@classmethod
def get(cls, id):
print id
return cls(id=id)
@classmethod
def auth(cls, username, password):
u = User()
data = u.get_by_email(username)
if not data:
return False
if bcrypt.check_password_hash(data['password'], password):
login_user(CurrentUser(data['id']))
session['user'] = data
return True
else:
return False

View file

@ -1,5 +1,15 @@
.navbar { .navbar {
margin-bottom: 25px;
}
#main-body {
background-color: #fff;
padding: 20px;
margin: 0 -20px;
-webkit-border-radius: 0 0 6px 6px;
border-radius: 0 0 6px 6px;
-webkit-box-shadow: 0 1px 2px rgba(0,0,0,.15);
box-shadow: 0 1px 2px rgba(0,0,0,.15);
} }
.checkbox-cell { .checkbox-cell {
@ -26,7 +36,7 @@
left: 50%; left: 50%;
bottom: 10px; bottom: 10px;
right: 10px; right: 10px;
top: 60px; top: 50px;
overflow: auto; overflow: auto;
background: rgba(255,255,255,0.9); background: rgba(255,255,255,0.9);
border: 1px solid #EEE; border: 1px solid #EEE;
@ -58,7 +68,7 @@
#editor { #editor {
position: absolute; position: absolute;
margin-right: 5px; margin-right: 5px;
top: 60px; top: 50px;
left: 10px; left: 10px;
bottom: 10px; bottom: 10px;
right: 50%; right: 50%;
@ -85,4 +95,27 @@
-ms-box-flex: 1; -ms-box-flex: 1;
-o-box-flex: 1; -o-box-flex: 1;
box-flex: 1; box-flex: 1;
}
.user-avatar a img {
width: 32px;
height: 32px;
-webkit-box-shadow: 0 1px 3px #1e1e1e;
-moz-box-shadow: 0 1px 3px #1e1e1e;
box-shadow: 0 1px 3px #1e1e1e;
-webkit-border-radius: 2px;
-moz-border-radius: 2px;
-ms-border-radius: 2px;
-o-border-radius: 2px;
border-radius: 2px;
}
.navbar-nav .user-avatar a {
line-height: 30px;
}
.navbar-nav>li.user-avatar a {
padding-top: 9px;
padding-bottom: 9px;
} }

View file

@ -0,0 +1,20 @@
{% extends 'layout.html' %}
{% block body %}
<h2>Account</h2>
<form method="POST" role="form" class="form-horizontal">
<div class="form-group">
<label for="email" class="col-md-2 control-label">Email</label>
<div class="col-md-10">
<input id="email" type="text" class="form-control" value="{{ session['user']['email'] }}" />
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" class="btn btn-primary" value="Save">
</div>
</div>
</form>
{% endblock %}

View file

@ -5,8 +5,8 @@
<form role="form" method="post"> <form role="form" method="post">
<div class="form-group"> <div class="form-group">
<label for="username">Username or Email</label> <label for="email">Email Address</label>
<input type="text" class="form-control" id="username" name="username" /> <input type="text" class="form-control" id="email" name="email" />
</div> </div>
<div class="form-group"> <div class="form-group">

View file

@ -27,4 +27,6 @@
<input type="submit" class="btn btn-primary" value="Submit" /> <input type="submit" class="btn btn-primary" value="Submit" />
</form> </form>
<a href="/login" class="pull-right">Already registered? Login here.</a>
{% endblock %} {% endblock %}

View file

@ -0,0 +1,6 @@
{% extends 'layout.html' %}
{% block body %}
<h1>Page Not Found</h1>
{% endblock %}

View file

@ -6,7 +6,7 @@
<meta name="description" content=""> <meta name="description" content="">
<meta name="author" content=""> <meta name="author" content="">
<title>ReImagine</title> <title>Realms</title>
<link href="/static/css/cerulean.bootstrap.min.css" rel="stylesheet"> <link href="/static/css/cerulean.bootstrap.min.css" rel="stylesheet">
<link href="/static/css/font-awesome.min.css" rel="stylesheet"> <link href="/static/css/font-awesome.min.css" rel="stylesheet">
@ -27,7 +27,7 @@
<body> <body>
<!-- Fixed navbar --> <!-- Fixed navbar -->
<div class="navbar navbar-default navbar-fixed-top"> <div class="navbar navbar-inverse navbar-fixed-top">
<div class="container"> <div class="container">
<div class="navbar-header"> <div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
@ -35,24 +35,54 @@
<span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
</button> </button>
<a class="navbar-brand" href="/">ReImagine</a> <a class="navbar-brand" href="/">Realms</a>
</div> </div>
<div class="navbar-collapse collapse"> <div class="navbar-collapse collapse navbar-inverse-collapse">
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
</ul> </ul>
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
<li><a href="/create/"><i class="icon-plus"></i> Create</a></li> <li><a href="/create/"><i class="icon-plus"></i> Create</a></li>
{% if name %} {% if session.get('user') %}
<li><a href="/edit/{{- name -}}"><i class="icon-edit"></i> Edit</a></li> <li class="dropdown user-avatar">
<li><a href="/history/{{- name -}}"><i class="icon-time"></i> History</a></li> <a href="#" class="dropdown-toggle" data-toggle="dropdown">
<span>
<img src="http://static.ffxiah.com/images/avatars/mini/5a85e45e0976c8a96c955e83a9743b47.jpg" class="menu-avatar">
<span>{{ session['user'].get('username') }} <i class="icon-caret-down"></i></span>
</span>
</a>
<ul class="dropdown-menu">
<li><a href="/account">Account</a></li>
<li><a href="/logout">Logout</a></li>
</ul>
</li>
{% else %}
<li><a href="/login"><i class="icon-user"></i> Login</a></li>
<li><a href="/register"><i class="icon-pencil"></i> Register</a></li>
{% endif %} {% endif %}
<li><a href="/login"><i class="icon-user"></i> Login</a></li>
</ul> </ul>
</div><!--/.nav-collapse --> </div><!--/.nav-collapse -->
</div> </div>
</div> </div>
<div class="container">{% block body %}{% endblock %}</div>
<div class="container">
<div id="main-body">
{% with messages = get_flashed_messages(with_categories=True) %}
{% if messages %}
{% for category, message in messages %}
{% if category == 'message' %}
{% set category = "info" %}
{% endif %}
<div class='alert alert-{{ category }}'>
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block body %}{% endblock %}
</div>
</div>
<script src="/static/js/jquery-1.10.2.min.js"></script> <script src="/static/js/jquery-1.10.2.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script> <script src="/static/js/bootstrap.min.js"></script>

View file

@ -0,0 +1,16 @@
{% extends 'layout.html' %}
{% block body %}
<h2>History for <strong>{{ name }}</strong></h2>
<p>
<a class="btn btn-default btn-sm" href="/history/{{ name }}">Back to History</a>
<a href="" class="btn btn-default btn-sm">Revert Changes</a>
</p>
{{ diff|safe }}
<p></p>
<p>
<a class="btn btn-default btn-sm" href="/history/{{ name }}">Back to History</a>
</p>
{% endblock %}

View file

@ -10,16 +10,16 @@
<div id="app-wrap" class="container-fluid"> <div id="app-wrap" class="container-fluid">
<div id="app-controls" class="row"> <div id="app-controls" class="row">
<div class="col-xs-3"> <div class="col-xs-3">
<input id="page-name" type="text" class="form-control" name="name" placeholder="Name" value="{{- name -}}" /> <input id="page-name" type="text" class="form-control input-sm" name="name" placeholder="Name" value="{{- name -}}" />
</div> </div>
<div class="col-xs-3"> <div class="col-xs-3">
<input id="page-message" type="text" class="form-control" name="page-message" placeholder="Comment" value="" /> <input id="page-message" type="text" class="form-control input-sm" name="page-message" placeholder="Comment" value="" />
</div> </div>
<div class="col-xs-6"> <div class="col-xs-6">
<div class="pull-right"> <div class="pull-right">
<a href="#" id="drop6" role="button" class="dropdown-toggle btn btn-default" data-toggle="dropdown">Theme <b class="caret"></b></a> <a href="#" id="drop6" role="button" class="dropdown-toggle btn btn-default btn-sm" data-toggle="dropdown">Theme <b class="caret"></b></a>
<ul id="theme-list" class="dropdown-menu" role="menu" aria-labelledby="drop6"> <ul id="theme-list" class="dropdown-menu" role="menu" aria-labelledby="drop6">
<li><a tabindex="-1" href="#" data-value="ace/theme/chrome" class="">Chrome</a></li> <li><a tabindex="-1" href="#" data-value="ace/theme/chrome" class="">Chrome</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/clouds" class="">Clouds</a></li> <li><a tabindex="-1" href="#" data-value="ace/theme/clouds" class="">Clouds</a></li>
@ -47,7 +47,7 @@
<li><a tabindex="-1" href="#" data-value="ace/theme/twilight" class="">Twilight</a></li> <li><a tabindex="-1" href="#" data-value="ace/theme/twilight" class="">Twilight</a></li>
<li><a tabindex="-1" href="#" data-value="ace/theme/vibrant_ink" class="">Vibrant Ink</a></li> <li><a tabindex="-1" href="#" data-value="ace/theme/vibrant_ink" class="">Vibrant Ink</a></li>
</ul> </ul>
<a id="save-native" class="btn btn-primary"><i class="icon-save"></i> Save</a> <a id="save-native" class="btn btn-primary btn-sm"><i class="icon-save"></i> Save</a>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,16 +1,58 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% block body %} {% block body %}
<h2>History</h2> <h2>History for <strong>{{ name }}</strong></h2>
<table class="table table-bordered"> <p>
<a class="btn btn-default btn-sm compare-revisions">Compare Revisions</a>
</p>
<table class="table table-bordered revision-tbl">
<thead>
<tr>
<th></th>
<th>Name</th>
<th>Revision Message</th>
<th>Date</th>
</tr>
</thead>
{% for h in history %} {% for h in history %}
<tr> <tr>
<td class="checkbox-cell text-center"><input type="checkbox" /></td> <td class="checkbox-cell text-center">
<input type="checkbox" name="versions[]" value="{{ h.sha }}" />
</td>
<td>{{ h.author }}</td> <td>{{ h.author }}</td>
<td><a href="/commit/{{ h.sha }}/{{ name }}" class='label label-primary'>{{ h.sha|truncate(7, True, end="") }}</a> {{ h.message }} </td> <td><a href="/commit/{{ h.sha }}/{{ name }}" class='label label-primary'>{{ h.sha|truncate(7, True, end="") }}</a> {{ h.message }} </td>
<td>{{ h.time|datetime }}</td> <td>{{ h.time|datetime }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
<p>
<a class="btn btn-default btn-sm compare-revisions">Compare Revisions</a>
</p>
{% endblock %}
{% block js %}
<script>
$(function(){
$('.revision-tbl :checkbox').change(function () {
var $cs=$(this).closest('.revision-tbl').find(':checkbox:checked');
if ($cs.length > 2) {
this.checked=false;
}
});
$(".compare-revisions").click(function(){
var $cs = $('.revision-tbl').find(':checkbox:checked');
console.log($cs.length);
if ($cs.length != 2) return;
var revs = [];
$.each($cs, function(i, v){
revs.push(v.value);
});
revs = revs.join("...");
location.href = "/compare/{{ name }}/" + revs;
});
});
</script>
{% endblock %} {% endblock %}

View file

@ -1,15 +1,20 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% block body %} {% block body %}
<div id="page-content" style="display:none"> <div class="controls pull-right">
{{ page.data|safe }} <a class="btn btn-default btn-sm" href="/edit/{{ name }}">Edit</a>
</div> <a class="btn btn-default btn-sm" href="/history/{{ name }}">History</a>
</div>
<div id="page-content" style="display:none">
{{ page.data|safe }}
</div>
{% endblock %} {% endblock %}
{% block js %} {% block js %}
<script> <script>
$(function(){ $(function(){
$("#page-content").html(converter({{ page.data|tojson|safe }})).show(); $("#page-content").html(converter({{ page.data|tojson|safe }})).show();
}); });
</script> </script>
{% endblock %} {% endblock %}

View file

@ -1,11 +1,11 @@
import os import os
import ghdiff
from gittle import Gittle from gittle import Gittle
from util import to_canonical from util import to_canonical
class MyGittle(Gittle): class MyGittle(Gittle):
def file_history(self, path): def file_history(self, path):
"""Returns all commits where given file was modified """Returns all commits where given file was modified
""" """
@ -57,7 +57,7 @@ class Wiki():
self.path = path self.path = path
def write_page(self, name, content, message=None, create=False): def write_page(self, name, content, message=None, create=False, username=None, email=None):
#content = clean_html(content) #content = clean_html(content)
filename = self.cname_to_filename(to_canonical(name)) filename = self.cname_to_filename(to_canonical(name))
f = open(self.path + "/" + filename, 'w') f = open(self.path + "/" + filename, 'w')
@ -67,8 +67,16 @@ class Wiki():
if create: if create:
self.repo.add(filename) self.repo.add(filename)
return self.repo.commit(name=self.default_committer_name, if not message:
email=self.default_committer_email, message = "Updated %s" % name
if not username:
username = self.default_committer_name
email = "%s@domain.com" % username
return self.repo.commit(name=username,
email=email,
message=message, message=message,
files=[filename]) files=[filename])
@ -88,6 +96,12 @@ class Wiki():
# HEAD doesn't exist yet # HEAD doesn't exist yet
return None return None
def compare(self, name, new_sha, old_sha):
old = self.get_page(name, sha=old_sha)
new = self.get_page(name, sha=new_sha)
return ghdiff.diff(old['data'], new['data'])
def get_history(self, name): def get_history(self, name):
return self.repo.file_history(self.cname_to_filename(name)) return self.repo.file_history(self.cname_to_filename(name))