diff --git a/.gitignore b/.gitignore
index 20fa823..15ff2f2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
*.pyc
*.sublime-project
*.sublime-workspace
+*.db
webservice/venv/
docs/venv/
diff --git a/webservice/bam/application/__init__.py b/webservice/bam/application/__init__.py
index 2e3f400..4b57a4c 100644
--- a/webservice/bam/application/__init__.py
+++ b/webservice/bam/application/__init__.py
@@ -27,7 +27,6 @@ if path not in sys.path:
del os, sys, path
# --------
-
import os
import json
import svn.local
@@ -37,12 +36,19 @@ from flask import Flask, jsonify, abort, request, make_response, url_for, Respon
from flask.views import MethodView
from flask.ext.restful import Api, Resource, reqparse, fields, marshal
from flask.ext.httpauth import HTTPBasicAuth
+from flask.ext.sqlalchemy import SQLAlchemy
app = Flask(__name__)
api = Api(app)
auth = HTTPBasicAuth()
import config
app.config.from_object(config.Development)
+db = SQLAlchemy(app)
+
+from application.modules.admin import backend
+from application.modules.admin import settings
+from application.modules.projects import admin
+from application.modules.projects.model import Project
@auth.get_password
@@ -73,11 +79,13 @@ class DirectoryAPI(Resource):
def get(self, project_name):
+ project = Project.query.filter_by(name=project_name).first()
+
path = request.args['path']
if not path:
path = ''
- absolute_path_root = app.config['STORAGE_PATH']
+ absolute_path_root = project.repository_path
parent_path = ''
if path != '':
@@ -134,8 +142,10 @@ class FileAPI(Resource):
filepath = request.args['filepath']
command = request.args['command']
+ project = Project.query.filter_by(name=project_name).first()
+
if command == 'info':
- r = svn.local.LocalClient(app.config['STORAGE_PATH'])
+ r = svn.local.LocalClient(project.repository_path)
log = r.log_default(None, None, 5, filepath)
log = [l for l in log]
@@ -145,7 +155,7 @@ class FileAPI(Resource):
log=log)
elif command == 'checkout':
- filepath = os.path.join(app.config['STORAGE_PATH'], filepath)
+ filepath = os.path.join(project.repository_path, filepath)
if not os.path.exists(filepath):
return jsonify(message="Path not found %r" % filepath)
@@ -192,6 +202,7 @@ class FileAPI(Resource):
return jsonify(message="Command unknown")
def put(self, project_name):
+ project = Project.query.filter_by(name=project_name).first()
command = request.args['command']
arguments = ''
if 'arguments' in request.args:
@@ -199,12 +210,12 @@ class FileAPI(Resource):
file = request.files['file']
if file and self.allowed_file(file.filename):
- local_client = svn.local.LocalClient(app.config['STORAGE_PATH'])
+ local_client = svn.local.LocalClient(project.repository_path)
# TODO, add the merge operation to a queue. Later on, the request could stop here
# and all the next steps could be done in another loop, or triggered again via
# another request
filename = werkzeug.secure_filename(file.filename)
- tmp_filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
+ tmp_filepath = os.path.join(project.upload_path, filename)
file.save(tmp_filepath)
# TODO, once all files are uploaded, unpack and run the tasklist (copy, add, remove
diff --git a/webservice/bam/application/modules/__init__.py b/webservice/bam/application/modules/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/webservice/bam/application/modules/admin/__init__.py b/webservice/bam/application/modules/admin/__init__.py
new file mode 100644
index 0000000..14bc62e
--- /dev/null
+++ b/webservice/bam/application/modules/admin/__init__.py
@@ -0,0 +1,115 @@
+from application import app, db
+#from application import thumb
+
+from flask import render_template, redirect, url_for
+from flask.ext.sqlalchemy import SQLAlchemy
+from flask.ext import admin, login
+from flask.ext.admin import Admin, expose
+from flask.ext.admin import form
+from flask.ext.admin.contrib import sqla
+from flask.ext.admin.contrib.sqla import ModelView
+from flask.ext.admin.base import BaseView
+from flask.ext.security import current_user
+
+from werkzeug import secure_filename
+from jinja2 import Markup
+from wtforms import fields, validators, widgets
+from wtforms.fields import SelectField, TextField
+import os, hashlib, time
+import os.path as op
+
+
+def _list_items(view, context, model, name):
+ """Utilities to upload and present images
+ """
+ if not model.name:
+ return ''
+ return Markup(
+ '
' % (
+ ''.join( [''
+ ''+item.name+'
' for item in getattr(model,name)] )))
+
+
+def _list_thumbnail(view, context, model, name):
+ if not getattr(model,name): #model.name only does not work because name is a string
+ return ''
+ return ''
+ # return Markup('
' % url_for('static',
+ # filename=thumb.thumbnail(getattr(model,name), '50x50', crop='fit')))
+
+# Create directory for file fields to use
+file_path = op.join(op.dirname(__file__), '../../static/files',)
+try:
+ os.mkdir(file_path)
+except OSError:
+ pass
+
+
+def prefix_name(obj, file_data):
+ # Collect name and extension
+ parts = op.splitext(file_data.filename)
+ # Get current time (for unique hash)
+ timestamp = str(round(time.time()))
+ # Has filename only (not extension)
+ file_name = secure_filename(timestamp + '%s' % parts[0])
+ # Put them together
+ full_name = hashlib.md5(file_name).hexdigest() + parts[1]
+ return full_name
+
+
+def image_upload_field(label):
+ return form.ImageUploadField(label,
+ base_path=file_path,
+ thumbnail_size=(100, 100, True),
+ namegen=prefix_name,
+ endpoint='filemanager.static')
+
+
+# Define wtforms widget and field
+class CKTextAreaWidget(widgets.TextArea):
+ def __call__(self, field, **kwargs):
+ kwargs.setdefault('class_', 'ckeditor')
+ return super(CKTextAreaWidget, self).__call__(field, **kwargs)
+
+
+class CKTextAreaField(fields.TextAreaField):
+ widget = CKTextAreaWidget()
+
+
+# Create customized views with access restriction
+class CustomModelView(ModelView):
+ def is_accessible(self):
+ return True
+ #return login.current_user.has_role('admin')
+
+class CustomBaseView(BaseView):
+ def is_accessible(self):
+ return True
+ #return login.current_user.has_role('admin')
+
+
+# Create customized index view class that handles login & registration
+class CustomAdminIndexView(admin.AdminIndexView):
+ def is_accessible(self):
+ return True
+ #return login.current_user.has_role('admin')
+
+ @expose('/')
+ def index(self):
+ return super(CustomAdminIndexView, self).index()
+
+ @expose('/logout/')
+ def logout_view(self):
+ login.logout_user()
+ return redirect(url_for('homepage'))
+
+
+# Create admin
+backend = Admin(
+ app,
+ 'BAM',
+ index_view=CustomAdminIndexView(),
+ base_template='admin/layout_admin.html'
+)
+
diff --git a/webservice/bam/application/modules/admin/model.py b/webservice/bam/application/modules/admin/model.py
new file mode 100644
index 0000000..83b24b7
--- /dev/null
+++ b/webservice/bam/application/modules/admin/model.py
@@ -0,0 +1,11 @@
+from application import db
+
+class Setting(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ name = db.Column(db.String(256), unique=True, nullable=False)
+ description = db.Column(db.Text)
+ value = db.Column(db.String(100), nullable=False)
+ data_type = db.Column(db.String(128), nullable=False)
+
+ def __unicode__(self):
+ return self.name
diff --git a/webservice/bam/application/modules/admin/settings.py b/webservice/bam/application/modules/admin/settings.py
new file mode 100644
index 0000000..b26c597
--- /dev/null
+++ b/webservice/bam/application/modules/admin/settings.py
@@ -0,0 +1,9 @@
+from application import app
+from application import db
+
+from application.modules.admin.model import Setting
+from application.modules.admin import *
+
+
+# Add views
+backend.add_view(CustomModelView(Setting, db.session, name='Settings', url='settings'))
diff --git a/webservice/bam/application/modules/projects/__init__.py b/webservice/bam/application/modules/projects/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/webservice/bam/application/modules/projects/admin.py b/webservice/bam/application/modules/projects/admin.py
new file mode 100644
index 0000000..689b463
--- /dev/null
+++ b/webservice/bam/application/modules/projects/admin.py
@@ -0,0 +1,17 @@
+from application import app
+from application import db
+
+from application.modules.projects.model import Project
+
+from application.modules.admin import *
+from application.modules.admin import _list_thumbnail
+
+
+class ProjectView(CustomModelView):
+ column_searchable_list = ('name',)
+ column_list = ('name', 'picture', 'creation_date')
+ #column_formatters = { 'picture': _list_thumbnail }
+ #form_extra_fields = {'picture': image_upload_field('Header')}
+
+# Add views
+backend.add_view(ProjectView(Project, db.session, name='Projects', url='projects'))
diff --git a/webservice/bam/application/modules/projects/model.py b/webservice/bam/application/modules/projects/model.py
new file mode 100644
index 0000000..f55501e
--- /dev/null
+++ b/webservice/bam/application/modules/projects/model.py
@@ -0,0 +1,14 @@
+import datetime
+from application import db
+
+class Project(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ name = db.Column(db.String(255), nullable=False)
+ repository_path = db.Column(db.Text, nullable=False)
+ upload_path = db.Column(db.Text, nullable=False)
+ picture = db.Column(db.String(80))
+ creation_date = db.Column(db.DateTime(), default=datetime.datetime.now)
+ status = db.Column(db.String(80)) #pending #active #inactive
+
+ def __str__(self):
+ return str(self.name)
diff --git a/webservice/bam/application/templates/admin/layout_admin.html b/webservice/bam/application/templates/admin/layout_admin.html
new file mode 100644
index 0000000..acb5d39
--- /dev/null
+++ b/webservice/bam/application/templates/admin/layout_admin.html
@@ -0,0 +1,20 @@
+{% extends 'admin/base.html' %}
+
+{% block brand %}
+ {{ admin_view.admin.name }}
+{% endblock %}
+
+{#
+{% block access_control %}
+{% if current_user.is_authenticated() %}
+
+{% endif %}
+{% endblock %}
+#}
diff --git a/webservice/bam/manage.py b/webservice/bam/manage.py
new file mode 100755
index 0000000..6c40b31
--- /dev/null
+++ b/webservice/bam/manage.py
@@ -0,0 +1,14 @@
+#!/usr/bin/env python3
+from application import app, db
+from flask.ext.script import Manager
+from flask.ext.migrate import Migrate, MigrateCommand
+
+migrate = Migrate(app, db)
+manager = Manager(app)
+manager.add_command('db', MigrateCommand)
+
+@manager.command
+def create_all_tables():
+ db.create_all()
+
+manager.run()
diff --git a/webservice/bam/migrations/alembic.ini b/webservice/bam/migrations/alembic.ini
new file mode 100644
index 0000000..f8ed480
--- /dev/null
+++ b/webservice/bam/migrations/alembic.ini
@@ -0,0 +1,45 @@
+# A generic, single database configuration.
+
+[alembic]
+# template used to generate migration files
+# file_template = %%(rev)s_%%(slug)s
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
diff --git a/webservice/bam/migrations/env.py b/webservice/bam/migrations/env.py
new file mode 100644
index 0000000..70961ce
--- /dev/null
+++ b/webservice/bam/migrations/env.py
@@ -0,0 +1,73 @@
+from __future__ import with_statement
+from alembic import context
+from sqlalchemy import engine_from_config, pool
+from logging.config import fileConfig
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+fileConfig(config.config_file_name)
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+# from myapp import mymodel
+# target_metadata = mymodel.Base.metadata
+from flask import current_app
+config.set_main_option('sqlalchemy.url', current_app.config.get('SQLALCHEMY_DATABASE_URI'))
+target_metadata = current_app.extensions['migrate'].db.metadata
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+def run_migrations_offline():
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ url = config.get_main_option("sqlalchemy.url")
+ context.configure(url=url)
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+def run_migrations_online():
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+ engine = engine_from_config(
+ config.get_section(config.config_ini_section),
+ prefix='sqlalchemy.',
+ poolclass=pool.NullPool)
+
+ connection = engine.connect()
+ context.configure(
+ connection=connection,
+ target_metadata=target_metadata
+ )
+
+ try:
+ with context.begin_transaction():
+ context.run_migrations()
+ finally:
+ connection.close()
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
+
diff --git a/webservice/bam/migrations/script.py.mako b/webservice/bam/migrations/script.py.mako
new file mode 100755
index 0000000..9570201
--- /dev/null
+++ b/webservice/bam/migrations/script.py.mako
@@ -0,0 +1,22 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision}
+Create Date: ${create_date}
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+def upgrade():
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+ ${downgrades if downgrades else "pass"}
diff --git a/webservice/bam/migrations/versions/4918c57ece7_initial_tables.py b/webservice/bam/migrations/versions/4918c57ece7_initial_tables.py
new file mode 100644
index 0000000..55b902b
--- /dev/null
+++ b/webservice/bam/migrations/versions/4918c57ece7_initial_tables.py
@@ -0,0 +1,45 @@
+"""initial_tables
+
+Revision ID: 4918c57ece7
+Revises: None
+Create Date: 2014-11-05 18:26:17.841382
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '4918c57ece7'
+down_revision = None
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+ ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('setting',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('name', sa.String(length=256), nullable=False),
+ sa.Column('description', sa.Text(), nullable=True),
+ sa.Column('value', sa.String(length=100), nullable=False),
+ sa.Column('data_type', sa.String(length=128), nullable=False),
+ sa.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('name')
+ )
+ op.create_table('project',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('name', sa.String(length=255), nullable=False),
+ sa.Column('repository_path', sa.Text(), nullable=False),
+ sa.Column('upload_path', sa.Text(), nullable=False),
+ sa.Column('picture', sa.String(length=80), nullable=True),
+ sa.Column('creation_date', sa.DateTime(), nullable=True),
+ sa.Column('status', sa.String(length=80), nullable=True),
+ sa.PrimaryKeyConstraint('id')
+ )
+ ### end Alembic commands ###
+
+
+def downgrade():
+ ### commands auto generated by Alembic - please adjust! ###
+ op.drop_table('project')
+ op.drop_table('setting')
+ ### end Alembic commands ###
diff --git a/webservice/bam/runserver.py b/webservice/bam/runserver.py
deleted file mode 100755
index d5398ce..0000000
--- a/webservice/bam/runserver.py
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/usr/bin/env python3
-from application import app
-app.run(debug=True)
diff --git a/webservice/requirements.txt b/webservice/requirements.txt
index c2d541e..41d262f 100644
--- a/webservice/requirements.txt
+++ b/webservice/requirements.txt
@@ -1,13 +1,29 @@
Flask==0.10.1
+Flask-Admin==1.0.8
Flask-HTTPAuth==2.3.0
+Flask-Login==0.2.11
+Flask-Mail==0.9.1
+Flask-Migrate==1.2.0
+Flask-Principal==0.4.0
Flask-RESTful==0.2.12
+Flask-SQLAlchemy==2.0
+Flask-Script==2.0.5
+Flask-Security==1.7.4
+Flask-WTF==0.10.2
Jinja2==2.7.3
+Mako==1.0.0
MarkupSafe==0.23
+Pillow==2.6.1
+SQLAlchemy==0.9.8
+WTForms==2.0.1
Werkzeug==0.9.6
+alembic==0.6.7
aniso8601==0.83
+blinker==1.3
gnureadline==6.3.3
ipython==2.3.0
itsdangerous==0.24
+passlib==1.6.2
python-dateutil==2.2
pytz==2014.7
six==1.7.2