Webservice side projects

This commit is contained in:
2014-11-05 18:52:18 +01:00
parent aa21e8be3d
commit 073d4784df
17 changed files with 419 additions and 9 deletions

View File

@@ -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

View File

@@ -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(
'<div class="select2-container-multi">'
'<ul class="select2-choices" style="border:0;cursor:default;background:none;">%s</ul></div>' % (
''.join( ['<li class="select2-search-choice" style="padding:3px 5px;">'
'<div>'+item.name+'</div></li>' 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('<img src="%s">' % 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'
)

View File

@@ -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

View File

@@ -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'))

View File

@@ -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'))

View File

@@ -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)

View File

@@ -0,0 +1,20 @@
{% extends 'admin/base.html' %}
{% block brand %}
<span class="brand"><a href="#">{{ admin_view.admin.name }}</a></span>
{% endblock %}
{#
{% block access_control %}
{% if current_user.is_authenticated() %}
<div class="btn-group pull-right">
<a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
<i class="icon-user"></i> {{ current_user.login }} <span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li><a href="{{ url_for('admin.logout_view') }}">Log out</a></li>
</ul>
</div>
{% endif %}
{% endblock %}
#}

14
webservice/bam/manage.py Executable file
View File

@@ -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()

View File

@@ -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

View File

@@ -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()

View File

@@ -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"}

View File

@@ -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 ###

View File

@@ -1,3 +0,0 @@
#!/usr/bin/env python3
from application import app
app.run(debug=True)

View File

@@ -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