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() %} +
    + + {{ current_user.login }} + + +
    +{% 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