diff --git a/config/local_settings_test.py b/config/local_settings_test.py
index 1521a02..eae1812 100644
--- a/config/local_settings_test.py
+++ b/config/local_settings_test.py
@@ -1,3 +1,5 @@
+from os import path
+
GITHUB_CONFIG = {
'repositories': [
('pretenders', 'deploystream'),
@@ -19,3 +21,7 @@
GIT_CONFIG = {
}
+
+CSRF_ENABLED = False
+TEST_DB_LOCATION = path.join(path.abspath(path.dirname(__file__)), 'test.db')
+SQLALCHEMY_DATABASE_URI = ('sqlite:///' + TEST_DB_LOCATION)
diff --git a/config/settings.py b/config/settings.py
index 33843ed..9b521e2 100644
--- a/config/settings.py
+++ b/config/settings.py
@@ -1,12 +1,28 @@
from os import path, environ
import traceback
+_basedir = path.abspath(path.dirname(__file__))
-APP_PACKAGE = path.basename(path.dirname(__file__))
+APP_PACKAGE = path.basename(_basedir)
+
+# Default settings, overridden by the python file pointed to by CONFIG variable
+SQLALCHEMY_DATABASE_URI = 'sqlite:///' + path.join(_basedir, 'app.db')
+DATABASE_CONNECT_OPTIONS = {}
+
+# THREADS_PER_PAGE = 8
+
+CSRF_ENABLED = True
+CSRF_SESSION_KEY = "somethingimpossibletoguess"
+
+# RECAPTCHA_USE_SSL = False
+# RECAPTCHA_PUBLIC_KEY = 'blahblahblahblahblahblahblahblahblah'
+# RECAPTCHA_PRIVATE_KEY = 'blahblahblahblahblahblahprivate'
+# RECAPTCHA_OPTIONS = {'theme': 'white'}
+
+GITHUB_CONFIG = GIT_CONFIG = SPRINTLY_CONFIG = JIRA_CONFIG = None
# The following is the programmatic equivalent of
# from deploystream.local_settings_ import *
-GITHUB_CONFIG = GIT_CONFIG = SPRINTLY_CONFIG = JIRA_CONFIG = None
try:
CONFIG = environ.get('CONFIG', 'sample')
diff --git a/deploystream/__init__.py b/deploystream/__init__.py
index 263a040..cafa438 100644
--- a/deploystream/__init__.py
+++ b/deploystream/__init__.py
@@ -3,6 +3,7 @@
from os import environ
from os.path import join, dirname
from flask import Flask
+from flask.ext.sqlalchemy import SQLAlchemy
APP_DIR = dirname(__file__)
CONFIG_DIR = join(dirname(APP_DIR), 'config')
@@ -51,6 +52,7 @@
# something that is actually a secret
app.secret_key = 'mysecret'
+db = SQLAlchemy(app)
# Initialise the providers.
from providers import init_providers
classes = init_providers(app.config['PROVIDERS'])
@@ -62,3 +64,6 @@
# Import any views we want to register here at the bottom of the file:
import deploystream.views # NOQA
import deploystream.apps.feature.views # NOQA
+
+from deploystream.apps.users.views import mod as usersModule
+app.register_blueprint(usersModule)
diff --git a/deploystream/apps/oauth/views.py b/deploystream/apps/oauth/views.py
index 5a31280..b265864 100644
--- a/deploystream/apps/oauth/views.py
+++ b/deploystream/apps/oauth/views.py
@@ -1,8 +1,11 @@
from flask import session, redirect, flash, request, url_for
from flask_oauth import OAuth
-from deploystream import app
+from deploystream import app, db
from deploystream.apps.oauth import get_token, set_token
+from deploystream.apps.users.models import User, OAuth as UserOAuth
+from deploystream.apps.users.lib import (load_user_to_session,
+ get_user_id_from_session)
from deploystream.providers.interfaces import class_implements, IOAuthProvider
@@ -62,18 +65,79 @@ def oauth_authorized(resp):
set_token(session, oauth_name, resp['access_token'])
- if request.args.get('islogin'):
- user = OAUTH_OBJECTS[oauth_name].get('/user')
- username = user.data['login']
- session['username'] = username
+ # If registering, add a User and UserOAuth and load to session.
+ # If Logging in, then just loading to session
+ # If linking, then just adding a userOauth.
+
+ current_user_id = get_user_id_from_session(session)
+ remote_user = OAUTH_OBJECTS[oauth_name].get('/user')
+ remote_user_id = remote_user.data['id']
+
+ user = get_or_create_user_oauth(current_user_id, remote_user_id,
+ oauth_name, remote_user.data['login'])
+
+ load_user_to_session(session, user)
return redirect(next_url)
-@app.route('/login')
-def login():
+def get_or_create_user_oauth(user_id, service_user_id, service_name,
+ service_username):
+ """
+ Get or create OAuth information and Users with the given information.
+
+ Handles a number of scenarios:
+ - No user id is known. (ie user is logged out)
+ - OAuth object exists: return the associated user.
+ - OAuth object doesn't exist: create it, along with a user and link
+ them
+ - User id is known (ie user is logged in)
+ - Create and link the OAuth data to the account.
+
+ :param user_id:
+ The id of the currently logged in user. Or ``None`` if logged out.
+ :param service_user_id:
+ The id of the user according to the external service.
+ :param service_name:
+ The name used internally to reference the external service.
+ :param service_username:
+ The username that the service knows this user by. This will be used to
+ create the account in deploystream if logging in for the first time.
+ """
+ if not user_id:
+ # We're either logging in or registering
+ oauth_obj = UserOAuth.query.filter_by(service_user_id=service_user_id,
+ service=service_name).first()
+ if not oauth_obj:
+ # Create a user and an OAuth linked to it.
+ current_user = User(username=service_username)
+ oauth = UserOAuth(service_user_id=service_user_id,
+ service=service_name,
+ service_username=service_username)
+ oauth.user = current_user
+ db.session.add(current_user)
+ db.session.add(oauth)
+ db.session.commit()
+ user_id = current_user.id
+ else:
+ user_id = oauth_obj.user.id
+
+ else:
+ # We're linking the account
+ oauth = UserOAuth(service_user_id=service_user_id,
+ service=service_name,
+ service_username=service_username,
+ user_id=user_id)
+ db.session.add(oauth)
+ db.session.commit()
+
+ return user_id
+
+
+@app.route('/oauth/')
+def link_up(oauth_name):
"Handler for calls to login via github."
- return start_token_processing('github', islogin=True)
+ return start_token_processing(oauth_name, islogin=True)
def start_token_processing(oauth_name, islogin=None):
@@ -83,4 +147,5 @@ def start_token_processing(oauth_name, islogin=None):
oauth_name=oauth_name,
islogin=islogin,
_external=True)
+
return OAUTH_OBJECTS[oauth_name].authorize(callback=url)
diff --git a/deploystream/apps/users/__init__.py b/deploystream/apps/users/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/deploystream/apps/users/constants.py b/deploystream/apps/users/constants.py
new file mode 100644
index 0000000..3635f24
--- /dev/null
+++ b/deploystream/apps/users/constants.py
@@ -0,0 +1,22 @@
+# User role
+ADMIN = 0
+STAFF = 1
+USER = 2
+ROLE = {
+ ADMIN: 'admin',
+ STAFF: 'staff',
+ USER: 'user',
+}
+
+# user status
+INACTIVE = 0
+NEW = 1
+ACTIVE = 2
+STATUS = {
+ INACTIVE: 'inactive',
+ NEW: 'new',
+ ACTIVE: 'active',
+}
+
+#OAUTH keys supported:
+OAUTHS = ['github']
diff --git a/deploystream/apps/users/decorators.py b/deploystream/apps/users/decorators.py
new file mode 100644
index 0000000..aa0e4e1
--- /dev/null
+++ b/deploystream/apps/users/decorators.py
@@ -0,0 +1,13 @@
+from functools import wraps
+
+from flask import g, flash, redirect, url_for, request
+
+
+def requires_login(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ if g.user is None:
+ flash(u'You need to be signed in for this page.')
+ return redirect(url_for('users.login', next=request.path))
+ return f(*args, **kwargs)
+ return decorated_function
diff --git a/deploystream/apps/users/forms.py b/deploystream/apps/users/forms.py
new file mode 100644
index 0000000..d00c389
--- /dev/null
+++ b/deploystream/apps/users/forms.py
@@ -0,0 +1,35 @@
+from flask.ext.wtf import (
+ Form, TextField, PasswordField, BooleanField, RecaptchaField
+)
+from flask.ext.wtf import Required, Email, EqualTo
+
+from .models import User
+
+
+class LoginForm(Form):
+ username = TextField('Username', [Required()])
+ password = PasswordField('Password', [Required()])
+
+
+class RegisterForm(Form):
+ username = TextField('Username', [Required()])
+ email = TextField('Email address', [Email()])
+ password = PasswordField('Password', [Required()])
+ confirm = PasswordField('Repeat Password', [
+ Required(),
+ EqualTo('password', message='Passwords must match')
+ ])
+ accept_tos = BooleanField('I accept the TOS', [Required()])
+ #recaptcha = RecaptchaField()
+
+ def validate(self):
+ rv = Form.validate(self)
+ if not rv:
+ return False
+
+ uname_clash = User.query.filter_by(username=self.username.data).first()
+ if uname_clash:
+ self.username.errors.append("This username is already in use.")
+ return False
+
+ return True
diff --git a/deploystream/apps/users/lib.py b/deploystream/apps/users/lib.py
new file mode 100644
index 0000000..a909eff
--- /dev/null
+++ b/deploystream/apps/users/lib.py
@@ -0,0 +1,10 @@
+def load_user_to_session(session, user):
+ session['user_id'] = user.id
+ session['user_name'] = user.username
+
+
+def get_user_id_from_session(session):
+ try:
+ return session['user_id']
+ except KeyError:
+ return None
diff --git a/deploystream/apps/users/models.py b/deploystream/apps/users/models.py
new file mode 100644
index 0000000..bea995d
--- /dev/null
+++ b/deploystream/apps/users/models.py
@@ -0,0 +1,48 @@
+from datetime import datetime
+
+from deploystream import db
+from deploystream.apps.users import constants as USERS
+
+
+class User(db.Model):
+
+ __tablename__ = 'users_user'
+ id = db.Column(db.Integer, primary_key=True)
+ username = db.Column(db.String(50), unique=True)
+ email = db.Column(db.String(120))
+ password = db.Column(db.String(20))
+ role = db.Column(db.SmallInteger, default=USERS.USER)
+ status = db.Column(db.SmallInteger, default=USERS.NEW)
+ created = db.Column(db.DateTime, default=datetime.now)
+
+ oauth_keys = db.relationship('OAuth', backref='user')
+
+ def __init__(self, username=None, email=None, password=None):
+ self.username = username
+ self.email = email
+ self.password = password
+
+ def getStatus(self):
+ return USERS.STATUS[self.status]
+
+ def getRole(self):
+ return USERS.ROLE[self.role]
+
+ def __repr__(self):
+ return '' % (self.username)
+
+
+class OAuth(db.Model):
+
+ __tablename__ = 'users_oauth'
+ id = db.Column(db.Integer, primary_key=True)
+ user_id = db.Column(db.Integer, db.ForeignKey('users_user.id'))
+ service = db.Column(db.String(20))
+ service_user_id = db.Column(db.String(120))
+ service_username = db.Column(db.String(50))
+
+ # A user on an external service should only exist once in deploystream.
+ db.UniqueConstraint('service', 'service_user_id', name='service_user')
+
+ # Each user in deploystream should only have each service oauth'd once.
+ db.UniqueConstraint('user_id', 'service', name='user_service')
diff --git a/deploystream/apps/users/template_helpers.py b/deploystream/apps/users/template_helpers.py
new file mode 100644
index 0000000..a7183af
--- /dev/null
+++ b/deploystream/apps/users/template_helpers.py
@@ -0,0 +1,27 @@
+from . import constants
+from .models import OAuth
+from deploystream import app
+
+
+@app.template_filter('all_oauths')
+def all_oauths(user):
+ """Return all available oauths for the User.
+
+ Returns all oauth information including those that the user hasn't
+ connected to yet.
+ """
+ all_oauth = []
+ for oauth_service in constants.OAUTHS:
+ user_oauth = OAuth.query.filter_by(user_id=user.id,
+ service=oauth_service).first()
+ if user_oauth:
+ all_oauth.append((oauth_service, user_oauth.service_username))
+ else:
+ all_oauth.append((oauth_service, None))
+
+ return all_oauth
+
+
+@app.template_filter('humanize_time')
+def humanize_time(datetime):
+ return datetime.strftime("%B %d, %Y")
diff --git a/deploystream/apps/users/views.py b/deploystream/apps/users/views.py
new file mode 100644
index 0000000..ed39573
--- /dev/null
+++ b/deploystream/apps/users/views.py
@@ -0,0 +1,76 @@
+from flask import (Blueprint, request, render_template, flash, g, session,
+ redirect, url_for)
+from werkzeug import check_password_hash, generate_password_hash
+
+from deploystream import db
+from .forms import RegisterForm, LoginForm
+from .models import User
+from .lib import load_user_to_session
+from .decorators import requires_login
+from . import template_helpers
+
+mod = Blueprint('users', __name__, url_prefix='/users')
+
+
+@mod.route('/me/')
+@requires_login
+def home():
+ return render_template("users/profile.html", user=g.user)
+
+
+@mod.before_request
+def before_request():
+ """
+ pull user's profile from the database before every request are treated
+ """
+ g.user = None
+ if 'user_id' in session:
+ g.user = User.query.get(session['user_id'])
+
+
+@mod.route('/login/', methods=['GET', 'POST'])
+def login():
+ """
+ Login form
+ """
+ form = LoginForm(request.form)
+ # make sure data are valid, but doesn't validate password is right
+ if form.validate_on_submit():
+ user = User.query.filter_by(username=form.username.data).first()
+ # we use werzeug to validate user's password
+ if user and check_password_hash(user.password, form.password.data):
+ load_user_to_session(session, user)
+
+ flash('Welcome %s' % user.username)
+ return redirect(url_for('users.home'))
+ flash('Wrong email or password', 'error-message')
+ if request.method == 'POST':
+ suffix = ".html"
+ else:
+ suffix = "_ajax.html"
+ return render_template("users/login" + suffix, form=form)
+
+
+@mod.route('/register/', methods=['GET', 'POST'])
+def register():
+ """
+ Registration Form
+ """
+ form = RegisterForm(request.form)
+ if form.validate_on_submit():
+ # create a user instance not yet stored in the database
+ user = User(form.username.data, form.email.data,
+ generate_password_hash(form.password.data))
+ # Insert the record in our database and commit it
+ db.session.add(user)
+ db.session.commit()
+
+ # Log the user in, as he now has an id
+ load_user_to_session(session, user)
+
+ # flash will display a message to the user
+ flash('Thanks for registering')
+ # redirect user to the 'home' method of the user module.
+ return redirect(url_for('users.home'))
+
+ return render_template("users/register.html", form=form)
diff --git a/deploystream/static/favicon.ico b/deploystream/static/favicon.ico
new file mode 100644
index 0000000..6848441
Binary files /dev/null and b/deploystream/static/favicon.ico differ
diff --git a/deploystream/static/font/FontAwesome.otf b/deploystream/static/font/FontAwesome.otf
new file mode 100644
index 0000000..64049bf
Binary files /dev/null and b/deploystream/static/font/FontAwesome.otf differ
diff --git a/deploystream/static/font/fontawesome-webfont.eot b/deploystream/static/font/fontawesome-webfont.eot
new file mode 100755
index 0000000..7d81019
Binary files /dev/null and b/deploystream/static/font/fontawesome-webfont.eot differ
diff --git a/deploystream/static/font/fontawesome-webfont.svg b/deploystream/static/font/fontawesome-webfont.svg
new file mode 100755
index 0000000..ba0afe5
--- /dev/null
+++ b/deploystream/static/font/fontawesome-webfont.svg
@@ -0,0 +1,284 @@
+
+
+
\ No newline at end of file
diff --git a/deploystream/static/font/fontawesome-webfont.ttf b/deploystream/static/font/fontawesome-webfont.ttf
new file mode 100755
index 0000000..d461724
Binary files /dev/null and b/deploystream/static/font/fontawesome-webfont.ttf differ
diff --git a/deploystream/static/font/fontawesome-webfont.woff b/deploystream/static/font/fontawesome-webfont.woff
new file mode 100755
index 0000000..3c89ae0
Binary files /dev/null and b/deploystream/static/font/fontawesome-webfont.woff differ
diff --git a/deploystream/static/less/bootstrap-modules.less b/deploystream/static/less/bootstrap-modules.less
index 798c5e0..8afb5bf 100644
--- a/deploystream/static/less/bootstrap-modules.less
+++ b/deploystream/static/less/bootstrap-modules.less
@@ -13,7 +13,7 @@
// Base CSS
@import "bootstrap/type.less";
@import "bootstrap/code.less";
-// @import "bootstrap/forms.less";
+@import "bootstrap/forms.less";
@import "bootstrap/tables.less";
// Components: common
diff --git a/deploystream/static/partials/home.html b/deploystream/static/partials/home.html
index 4248b82..0dcf07d 100644
--- a/deploystream/static/partials/home.html
+++ b/deploystream/static/partials/home.html
@@ -10,7 +10,7 @@ Welcome to Ployst!
from various tools.
-
+
Sign in via Github
diff --git a/deploystream/templates/base-angular.html b/deploystream/templates/base-angular.html
new file mode 100644
index 0000000..14535f0
--- /dev/null
+++ b/deploystream/templates/base-angular.html
@@ -0,0 +1,10 @@
+{% extends "base.html" %}
+
+{% block extrahead %}
+
+{% endblock %}
diff --git a/deploystream/templates/base.html b/deploystream/templates/base.html
index e36bb90..bb39707 100644
--- a/deploystream/templates/base.html
+++ b/deploystream/templates/base.html
@@ -6,18 +6,16 @@
DeployStream {% block extratitle %}{% endblock %}
-
-
+
+
+
+
+
{% block extrahead %}
{% endblock %}
{% endblock %}
@@ -43,10 +41,22 @@
-
-
-
-
+
{% block scripts %}{% endblock %}