Commit 42a15dcb authored by Cédric de Saint Martin's avatar Cédric de Saint Martin

Merge branch 'slaprunner'

parents f8ab2eaa 1069aead
...@@ -46,6 +46,7 @@ setup(name=name, ...@@ -46,6 +46,7 @@ setup(name=name,
'lampconfigure': ["mysql-python"], #needed for MySQL Database access 'lampconfigure': ["mysql-python"], #needed for MySQL Database access
'zodbpack': ['ZODB3'], # needed to play with ZODB 'zodbpack': ['ZODB3'], # needed to play with ZODB
'agent': ['erp5.util'], 'agent': ['erp5.util'],
'flask_auth' : ["Flask-Auth"],
}, },
zip_safe=False, # proxy depends on Flask, which has issues with zip_safe=False, # proxy depends on Flask, which has issues with
# accessing templates # accessing templates
......
...@@ -5,7 +5,7 @@ import logging.handlers ...@@ -5,7 +5,7 @@ import logging.handlers
import os import os
import sys import sys
import subprocess import subprocess
import hashlib from datetime import timedelta
class Parser(OptionParser): class Parser(OptionParser):
""" """
...@@ -118,15 +118,10 @@ def serve(config): ...@@ -118,15 +118,10 @@ def serve(config):
workspace = workdir, workspace = workdir,
instance_profile='instance.cfg', instance_profile='instance.cfg',
software_profile='software.cfg', software_profile='software.cfg',
SECRET_KEY='123', SECRET_KEY="123456",
PERMANENT_SESSION_LIFETIME=timedelta(days=31),
) )
if not os.path.exists(workdir): if not os.path.exists(workdir):
os.mkdir(workdir) os.mkdir(workdir)
if not os.path.exists(os.path.join(config.runner_workdir, '.users')):
#set default user and password
salt = "runner81" #to be changed
pwd = hashlib.md5( salt + "insecure" ).hexdigest()
user = "root;"+pwd+";;Slaprunner Administrator"
open(os.path.join(config.runner_workdir, '.users'), 'w').write(user)
app.run(host=config.runner_host, port=int(config.runner_port), app.run(host=config.runner_host, port=int(config.runner_port),
debug=config.debug, threaded=True) debug=config.debug, threaded=True)
...@@ -43,7 +43,7 @@ textarea { ...@@ -43,7 +43,7 @@ textarea {
} }
body { body {
background: #2281C1;/*url("../images/1307251316-background-stripes.gif") repeat #9C9C9C;*/ background: #1E73BD;/*#1862C4 url("../images/1307251316-background-stripes.gif") repeat #9C9C9C;*/
font-family: 'Helvetica Neue',Tahoma,Helvetica,Arial,sans-serif; font-family: 'Helvetica Neue',Tahoma,Helvetica,Arial,sans-serif;
color: #000000; color: #000000;
font-size: 13px; font-size: 13px;
...@@ -72,6 +72,7 @@ overflow-y: scroll; ...@@ -72,6 +72,7 @@ overflow-y: scroll;
text-align: left; text-align: left;
padding-left: 20px; padding-left: 20px;
height: 30px; height: 30px;
position:relative;
} }
#header .run{ #header .run{
...@@ -617,6 +618,8 @@ a.lshare img{ ...@@ -617,6 +618,8 @@ a.lshare img{
border-radius: 0 0 4px 4px; border-radius: 0 0 4px 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
text-align: center; text-align: center;
position: relative;
z-index: 170;
} }
#error table{ #error table{
...@@ -706,8 +709,10 @@ input[type=radio]:hover { ...@@ -706,8 +709,10 @@ input[type=radio]:hover {
#login-page{width:429px; height:236px; margin:130px auto 0px; background:url(../images/loginBox.png) no-repeat; #login-page{width:429px; height:236px; margin:130px auto 0px; background:url(../images/loginBox.png) no-repeat;
padding:10px; font-size:14px; color:#03406A} padding:10px; font-size:14px; color:#03406A}
#login-page h2{color:#fff; font-size:26px; font-weight:normal; text-indent:50px;} #login-page h2{color:#fff; font-size:26px; font-weight:normal; text-indent:50px;}
.login-content{margin:10px; margin-top:40px; margin-bottom:0; height:90px;} .login-content{position:relative;margin:10px; margin-top:30px; margin-bottom:0;}
.login-button{width:140px; margin:0 auto;} .login-button{width:140px; margin:0 auto;}
.login-element{float:left; min-width:120px;} .login-element{float:left; min-width:120px;}
.login-label{padding:5px; font-size:16px;} .login-label{padding:5px; font-size:16px;}
.login-input{width:220px;} .login-input{width:220px;}
\ No newline at end of file .information{display:block; float:left; height:16px; margin-top:10px; margin-left:10px; font-weight:bold}
.account{margin-left:60px;}
\ No newline at end of file
...@@ -9,25 +9,17 @@ $(function () { ...@@ -9,25 +9,17 @@ $(function () {
$('a[rel=tooltip], a[rel=tooltip-min], .popup').mouseover(function () { $('a[rel=tooltip], a[rel=tooltip-min], .popup').mouseover(function () {
var height = $(this).height(); var height = $(this).height();
var top = $(this).offset().top + height; var top = $(this).offset().top + height;
var left = $(this).offset().left - ($(this).width() /2); var left = $(this).offset().left +($(this).width()/2)-30;
var content = "#tooltip-" + $(this).attr('id'); var content = "#tooltip-" + $(this).attr('id');
if (hideDelayTimer) clearTimeout(hideDelayTimer); if (hideDelayTimer) clearTimeout(hideDelayTimer);
if (beingShown || shown) { if (beingShown || shown) {
return; return;
} else { } else {
$('#jqtooltip').empty(); $('#jqtooltip').empty();
var contentValue = $(content).clone(true, true); var contentValue = $(content).clone(true, true);
/*$(contentValue).find("*").each(function(index, element) {
if(element.id){element.id = "jqt_" + element.id;}
if($(this).attr('for')){$(this).attr('for', 'jqt_' + $(this).attr('for'))}
//var events = $(content).get[0];
//for (var type in events)
// for (var handler in events[type])
// jQuery.event.add(this, type, events[type][handler], events[type][handler].data);
});*/
$(contentValue).appendTo('#jqtooltip'); $(contentValue).appendTo('#jqtooltip');
$('#jqtooltip ' + content).show(); $('#jqtooltip ' + content).show();
// reset position of info box // reset position of info box
beingShown = true; beingShown = true;
$('.popup').css({ $('.popup').css({
top: top, top: top,
...@@ -49,7 +41,7 @@ $(function () { ...@@ -49,7 +41,7 @@ $(function () {
$('.popup').animate({ $('.popup').animate({
top: '-=' + distance + 'px', top: '-=' + distance + 'px',
opacity: 0 opacity: 0
}, time, 'swing', function () { }, time, 'swing', function () {
$('.popup').css('display', 'none'); $('.popup').css('display', 'none');
shown = false; shown = false;
}); });
......
...@@ -2,10 +2,15 @@ $(document).ready( function() { ...@@ -2,10 +2,15 @@ $(document).ready( function() {
var send = false; var send = false;
$("#update").click(function(){ $("#update").click(function(){
var haspwd = false; var haspwd = false;
var hasAccount = !($("input#hasAccount").val() === "");
if($("input#username").val() === "" || !$("input#username").val().match(/^[\w\d\._-]+$/)){ if($("input#username").val() === "" || !$("input#username").val().match(/^[\w\d\._-]+$/)){
$("#error").Popup("Invalid user name. Please check it!", {type:'alert', duration:3000}); $("#error").Popup("Invalid user name. Please check it!", {type:'alert', duration:3000});
return false; return false;
} }
else if ($("input#username").val().length <6){
$("#error").Popup("Username must have at least 6 characters", {type:'alert', duration:3000});
return false;
}
if($("input#name").val() === ""){ if($("input#name").val() === ""){
$("#error").Popup("Please enter your name and surname!", {type:'alert', duration:3000}); $("#error").Popup("Please enter your name and surname!", {type:'alert', duration:3000});
return false; return false;
...@@ -14,6 +19,14 @@ $(document).ready( function() { ...@@ -14,6 +19,14 @@ $(document).ready( function() {
$("#error").Popup("Please enter a valid email adress!", {type:'alert', duration:3000}); $("#error").Popup("Please enter a valid email adress!", {type:'alert', duration:3000});
return false; return false;
} }
if(!hasAccount && !$("input#password").val().match(/^[\w\d\._-]+$/)){
$("#error").Popup("Please enter your new password!", {type:'alert', duration:3000});
return false;
}
if ($("input#password").val() !== "" && $("input#password").val().length <6){
$("#error").Popup("The password must have at least 6 characters", {type:'alert', duration:3000});
return false;
}
if($("input#password").val() !== ""){ if($("input#password").val() !== ""){
if($("input#password").val() === "" || !$("input#password").val().match(/^[\w\d\._-]+$/)){ if($("input#password").val() === "" || !$("input#password").val().match(/^[\w\d\._-]+$/)){
$("#error").Popup("Please enter your new password!", {type:'alert', duration:3000}); $("#error").Popup("Please enter your new password!", {type:'alert', duration:3000});
...@@ -25,16 +38,20 @@ $(document).ready( function() { ...@@ -25,16 +38,20 @@ $(document).ready( function() {
} }
haspwd = true; haspwd = true;
} }
if(!$("input#rcode").val().match(/^[\w\d]+$/)){
$("#error").Popup("Please enter your password recovery code.", {type:'alert', duration:3000});
return false;
}
if(send) return false; if(send) return false;
send = true; send = true;
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: $SCRIPT_ROOT + '/updateAccount', url: $SCRIPT_ROOT + ((hasAccount)? '/updateAccount':'/configAccount'),
data: {name: $("input#name").val(), username:$("input#username").val(), email:$("input#email").val(), data: {name: $("input#name").val(), username:$("input#username").val(), email:$("input#email").val(),
password:((haspwd) ? $("input#password").val():"")}, password:((haspwd) ? $("input#password").val():""), rcode:$("input#rcode").val()},
success: function(data){ success: function(data){
if(data.code ==1){ if(data.code ==1){
$("#error").Popup("Your account informations has been saved!", {type:'confirm', duration:3000}); location.href = $SCRIPT_ROOT+"/"
} }
else{ else{
$("#error").Popup(data.result, {type:'error', duration:5000}); $("#error").Popup(data.result, {type:'error', duration:5000});
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<h2 class='title'>Your personal informations</h2><br/> <h2 class='title'>Your personal informations</h2><br/>
<form> <form class="account">
<div class='form'> <div class='form'>
<label for="name">Your name: </label> <label for="name">Your name: </label>
<input type='text' name='name' id='name' value='{{name}}'/> <input type='text' name='name' id='name' value='{{name}}'/>
...@@ -24,12 +24,23 @@ ...@@ -24,12 +24,23 @@
<input type='password' name='cpassword' id='cpassword' value=''/> <input type='password' name='cpassword' id='cpassword' value=''/>
<div class='clear'></div> <div class='clear'></div>
<br/> <br/>
<label for="rcode">Password Recover code:</label>
<input type='password' name='rcode' id='rcode' value=''/>
<span class="information"><a href="#" id="information" rel="tooltip">help?</a></span>
<div class='clear'></div>
<br/>
<label></label> <label></label>
<input type="submit" name="update" id ="update" value="Update" class="button"/> <input type="submit" name="update" id ="update" value="Update Account" class="button"/>
<div class='clear'></div> <div class='clear'></div>
<br/><br/><br/> <br/><br/><br/>
</div> </div>
<div id="file_info" class="file_info">leave passwords blank to preserve your current password...</div> <input type="hidden" name="hasAccount" id="hasAccount" value="{{name}}"/>
<br/>
</form> </form>
{% if username %}<div id="file_info" class="file_info">leave passwords blank to preserve your current password...
</div><br/>{%endif%}
<div id="tooltip-information" style="display:none; float:left">
<p style="font-size:12px;">
Please find this information in your slaprunner<br/> instance parameters.
</p>
</div>
{% endblock %} {% endblock %}
...@@ -18,11 +18,11 @@ ...@@ -18,11 +18,11 @@
<div class="tabDetails"> <div class="tabDetails">
<div id="tab1" class="tabContents"> <div id="tab1" class="tabContents">
<div id="repository" style="margin-left:40px;"> <div id="repository" style="margin-left:40px;">
<label for='name'>Project name*: </label> <label for='name'>Project name: </label>
<input type="text" name="name" id="name" size='20' value="Enter the project name..." /> <input type="text" name="name" id="name" size='20' value="Enter the project name..." />
<label for='repo'>&nbsp;url*: &nbsp;&nbsp;</label> <label for='repo'>&nbsp;url: &nbsp;&nbsp;</label>
<input type="text" name="repo" id="repo" size='25' value="Enter the url of your repository..." /><br/> <input type="text" name="repo" id="repo" size='25' value="Enter the url of your repository..." /><br/>
<label for='user'>Your name: &nbsp;&nbsp;&nbsp;&nbsp;</label> <label for='user'>Your name: &nbsp;&nbsp;&nbsp;</label>
<input type="text" name="user" id="user" size='20' value="{{name}}" /> <input type="text" name="user" id="user" size='20' value="{{name}}" />
<label for='email'>Email: </label> <label for='email'>Email: </label>
<input type="text" name="email" id="email" size='25' value="{% if not email %}Enter your email adress...{% else %}{{email}}{%endif%}" /> <input type="text" name="email" id="email" size='25' value="{% if not email %}Enter your email adress...{% else %}{{email}}{%endif%}" />
......
...@@ -54,7 +54,7 @@ ...@@ -54,7 +54,7 @@
<div class="line"></div> <div class="line"></div>
<a href="{{ url_for('editCurrentProject') }}" style="float:left" title="Edit your current project"><img alt="" src="{{ url_for('static', filename='images/project.png') }}" /></a> <a href="{{ url_for('editCurrentProject') }}" style="float:left" title="Edit your current project"><img alt="" src="{{ url_for('static', filename='images/project.png') }}" /></a>
<div class="line"></div> <div class="line"></div>
<a href="{{ url_for('logout') }}" style="float:left" title="Close your session"><img alt="" src="{{ url_for('static', filename='images/logout.png') }}" /></a> <a href="{{ url_for('dologout') }}" style="float:left" title="Close your session"><img alt="" src="{{ url_for('static', filename='images/logout.png') }}" /></a>
<div class="line"></div> <div class="line"></div>
<h2 class="info">{% block title %}{% endblock %} - {{session.title}}</h2> <h2 class="info">{% block title %}{% endblock %} - {{session.title}}</h2>
<div class="run"><span id="running" style="display:none"><img alt="" src="{{ url_for('static', filename='images/ajax_roller.gif') }}" <div class="run"><span id="running" style="display:none"><img alt="" src="{{ url_for('static', filename='images/ajax_roller.gif') }}"
...@@ -97,7 +97,7 @@ ...@@ -97,7 +97,7 @@
</div> </div>
{% if request.path != '/login' %} {% if request.path != '/login' %}
<div id="footer"> <div id="footer">
SlapOs web runner &copy; Vifib SARL 2011 - All right reserved - Creative Commons Shared Alike Non Commercial SlapOS web runner &copy; Vifib SARL 2011 - All right reserved - Creative Commons Shared Alike Non Commercial
</div> </div>
{%endif%} {%endif%}
</div> </div>
......
...@@ -6,17 +6,26 @@ ...@@ -6,17 +6,26 @@
{% block body %} {% block body %}
<form method="POST" action=""> <form method="POST" action="">
<h2>Login to Slapos Web Runner</h2> <h2>Login to Slapos Web Runner</h2>
<div class="login-content"> <div class="login-content"><br/>
<div class="login-element login-label"><label for="clogin">Your login&nbsp; : </label></div> <div class="login-element login-label"><label for="clogin">Your login&nbsp; : </label></div>
<div class="login-element"><input type="text" class="login-input" name="clogin" id="clogin" value="Enter login..." /></div><br/><br/> <div class="login-element"><input type="text" class="login-input" name="clogin" id="clogin" value="Enter login..." /></div><br/><br/>
<div class="clear"></div> <div class="clear"></div>
<div class="login-element login-label"><label for="cpwd">Password : </label></div> <div class="login-element login-label"><label for="cpwd">Password : </label></div>
<div class="login-element"><input type="password" class="idleField login-input" name="cpwd" id="cpwd" value="******" /></div> <div class="login-element"><input type="password" class="idleField login-input" name="cpwd" id="cpwd" value="******" /></div>
<div class="clear"></div> <div class="clear"></div>
<!--<br/><a href="#" id="information" rel="tooltip">do you need help?</a><br/>-->
</div> </div>
<div style="text-align:center"> <div style="text-align:center; margin-top:7px;">
<input type="reset" class="button" value="reset" /> <input type="reset" class="button" value="reset" />
<input type="submit" class="button" id="login" value="login" /> <input type="submit" class="button" id="login" value="login" />
</div> </div>
</form> </form>
<!--
<div id="tooltip-information" style="display:none">
<p style="font-size:12px;">
If it is your first connexion, use default parameters:<br/>
login: <strong>root</strong>, password: <strong>insecure</strong> and set your
one<br/> parameters at <strong>home->Your Account</strong>.
</p>
</div>-->
{% endblock %} {% endblock %}
\ No newline at end of file
...@@ -40,46 +40,23 @@ def html_escape(text): ...@@ -40,46 +40,23 @@ def html_escape(text):
"""Produce entities within text.""" """Produce entities within text."""
return "".join(html_escape_table.get(c,c) for c in text) return "".join(html_escape_table.get(c,c) for c in text)
def checkLogin(config, login, pwd):
"""
User authentication method
Args:
config: Slaprunner configuration.
login: username of the user.
pwd: password associate to username.
Returns:
a list of user informations or False if authentication fail.
list=[username, password, email, complete_name]
"""
user = getSession(config)
salt = "runner81" #to be changed
current_pwd = hashlib.md5( salt + pwd ).hexdigest()
if current_pwd == user[1]:
return user
return False
def getSession(config): def getSession(config):
""" """
Get the session data of current user. Get the session data of current user.
Returns: Returns:
a list of user informations or False if fail to read data. a list of user informations or False if fail to read data.
""" """
user_path = os.path.join(config['runner_workdir'], '.users') user_path = os.path.join(config['etc_dir'], '.users')
user = "" user = ""
if os.path.exists(user_path): if os.path.exists(user_path):
user = open(user_path, 'r').read().split(';') f = open(user_path, 'r')
user = f.read().split(';')
f.close()
if type(user) == type(""): if type(user) == type(""):
#Error: try to restore data from backup return False
if os.path.exists(user_path+'.back'):
os.rename(user_path+'.back', user_path)
user = open(user_path, 'r').read().split(';')
else:
return False
return user return user
def saveSession(config, session, account): def saveSession(config, account):
""" """
Save account information for the current user Save account information for the current user
...@@ -91,20 +68,20 @@ def saveSession(config, session, account): ...@@ -91,20 +68,20 @@ def saveSession(config, session, account):
Returns: Returns:
True if all goes well or str (error message) if fail True if all goes well or str (error message) if fail
""" """
user = os.path.join(config['runner_workdir'], '.users') user = os.path.join(config['etc_dir'], '.users')
backup = False backup = False
try: try:
if account[1]: if os.path.exists(user):
salt = "runner81" #to be changed f = open(user, 'r')
account[1] = hashlib.md5(salt + account[1]).hexdigest() #backup previous data
else: data = f.read()
account[1] = session['account'][1] open(user+'.back', 'w').write(data)
#backup previous data f.close()
open(user+'.back', 'w').write(';'.join(session['account'])) backup = True
backup = True if not account[1]:
account[1] = data.split(';')[1]
#save new account data #save new account data
open(user, 'w').write((';'.join(account)).encode("utf-8")) open(user, 'w').write((';'.join(account)).encode("utf-8"))
session['account'] = account
return True return True
except Exception, e: except Exception, e:
try: try:
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from flask import Flask, request, redirect, url_for, \ from flask import Flask, request, redirect, url_for, \
render_template, flash, jsonify, session render_template, g, flash, jsonify, session
from utils import * from utils import *
import os import os
import shutil import shutil
import md5 import md5
from gittools import cloneRepo, gitStatus, switchBranch, addBranch, getDiff, \ from gittools import cloneRepo, gitStatus, switchBranch, addBranch, getDiff, \
gitPush, gitPull gitPush, gitPull
from flaskext.auth import Auth, AuthUser, login_required, logout
app = Flask(__name__) app = Flask(__name__)
auth = Auth(app, login_url_name='login')
auth.user_timeout = 0
def login_redirect(*args, **kwargs):
return redirect(url_for('login'))
#Access Control: Only static files and login pages are allowed to guest #Access Control: Only static files and login pages are allowed to guest
@app.before_request @app.before_request
def before_request(): def before_request():
if (not session.has_key('account') or not session['account']) \ if not request.path.startswith('/static'):
and request.path != '/login' \ account = getSession(app.config)
and request.path != '/doLogin' and not request.path.startswith('/static'): if account:
return redirect(url_for('login')) user = AuthUser(username=account[0])
if session.has_key('account') and session['account']: user.set_and_encrypt_password(account[1], "123400ZYX")
session['title'] = getProjectTitle(app.config) session['title'] = getProjectTitle(app.config)
session['account'] = getSession(app.config) g.users = {account[0]: user}
else:
session['title'] = "No account is defined"
if request.path != "/setAccount" and request.path != "/configAccount":
return redirect(url_for('setAccount'))
# general views # general views
@app.route('/') @login_required()
def home(): def home():
return render_template('index.html') return render_template('index.html')
...@@ -31,34 +41,43 @@ def home(): ...@@ -31,34 +41,43 @@ def home():
def login(): def login():
return render_template('login.html') return render_template('login.html')
@app.route("/myAccount") @app.route("/setAccount")
def setAccount():
account = getSession(app.config)
if not account:
return render_template('account.html')
return redirect(url_for('login'))
@login_required()
def myAccount(): def myAccount():
return render_template('account.html', username=session['account'][0], account = getSession(app.config)
email=session['account'][2], name=session['account'][3].decode('utf-8')) return render_template('account.html', username=account[0],
email=account[2], name=account[3].decode('utf-8'))
@app.route("/logout") @app.route("/dologout")
def logout(): def dologout():
session['account'] = None user_data = logout()
return redirect(url_for('login')) return redirect(url_for('login'))
@app.route('/configRepo') @login_required()
def configRepo(): def configRepo():
public_key = open(app.config['public_key'], 'r').read() public_key = open(app.config['public_key'], 'r').read()
account = getSession(app.config)
return render_template('cloneRepository.html', workDir='workspace', return render_template('cloneRepository.html', workDir='workspace',
public_key=public_key, name=session['account'][3].decode('utf-8'), public_key=public_key, name=account[3].decode('utf-8'),
email=session['account'][2]) email=account[2])
@app.route("/doLogin", methods=['POST']) @app.route("/doLogin", methods=['POST'])
def doLogin(): def doLogin():
check_user = checkLogin(app.config, request.form['clogin'], request.form['cpwd']) username = request.form['clogin']
if not check_user: if username in g.users:
return jsonify(code=0, result="Login or password is incorrect, please check it!") # Authenticate and log in!
else: if g.users[username].authenticate(request.form['cpwd']):
session['account'] = check_user return jsonify(code=1, result="")
return jsonify(code=1, result=check_user) return jsonify(code=0, result="Login or password is incorrect, please check it!")
# software views # software views
@app.route('/editSoftwareProfile') @login_required()
def editSoftwareProfile(): def editSoftwareProfile():
profile = getProfilePath(app.config['runner_workdir'], app.config['software_profile']) profile = getProfilePath(app.config['runner_workdir'], app.config['software_profile'])
if profile == "": if profile == "":
...@@ -66,7 +85,7 @@ def editSoftwareProfile(): ...@@ -66,7 +85,7 @@ def editSoftwareProfile():
return render_template('updateSoftwareProfile.html', workDir='workspace', return render_template('updateSoftwareProfile.html', workDir='workspace',
profile=profile, projectList=getProjectList(app.config['workspace'])) profile=profile, projectList=getProjectList(app.config['workspace']))
@app.route('/inspectSoftware', methods=['GET']) @login_required()
def inspectSoftware(): def inspectSoftware():
if not os.path.exists(app.config['software_root']): if not os.path.exists(app.config['software_root']):
result = "" result = ""
...@@ -76,7 +95,7 @@ def inspectSoftware(): ...@@ -76,7 +95,7 @@ def inspectSoftware():
softwares=loadSoftwareData(app.config['runner_workdir'])) softwares=loadSoftwareData(app.config['runner_workdir']))
#remove content of compiled software release #remove content of compiled software release
@app.route('/removeSoftware') @login_required()
def removeSoftware(): def removeSoftware():
file_config = os.path.join(app.config['runner_workdir'], ".softdata") file_config = os.path.join(app.config['runner_workdir'], ".softdata")
if isSoftwareRunning(app.config) or isInstanceRunning(app.config): if isSoftwareRunning(app.config) or isInstanceRunning(app.config):
...@@ -88,14 +107,14 @@ def removeSoftware(): ...@@ -88,14 +107,14 @@ def removeSoftware():
flash('Software removed') flash('Software removed')
return redirect(url_for('inspectSoftware')) return redirect(url_for('inspectSoftware'))
@app.route('/runSoftwareProfile', methods=['POST']) @login_required()
def runSoftwareProfile(): def runSoftwareProfile():
if runSoftwareWithLock(app.config): if runSoftwareWithLock(app.config):
return jsonify(result = True) return jsonify(result = True)
else: else:
return jsonify(result = False) return jsonify(result = False)
@app.route('/viewSoftwareLog', methods=['GET']) @login_required()
def viewSoftwareLog(): def viewSoftwareLog():
if os.path.exists(app.config['software_log']): if os.path.exists(app.config['software_log']):
result = tail(open(app.config['software_log'], 'r'), lines=1500) result = tail(open(app.config['software_log'], 'r'), lines=1500)
...@@ -105,7 +124,7 @@ def viewSoftwareLog(): ...@@ -105,7 +124,7 @@ def viewSoftwareLog():
result=result) result=result)
# instance views # instance views
@app.route('/editInstanceProfile') @login_required()
def editInstanceProfile(): def editInstanceProfile():
profile = getProfilePath(app.config['runner_workdir'], app.config['instance_profile']) profile = getProfilePath(app.config['runner_workdir'], app.config['instance_profile'])
if profile == "": if profile == "":
...@@ -114,7 +133,7 @@ def editInstanceProfile(): ...@@ -114,7 +133,7 @@ def editInstanceProfile():
profile=profile, projectList=getProjectList(app.config['workspace'])) profile=profile, projectList=getProjectList(app.config['workspace']))
# get status of all computer partitions and process state # get status of all computer partitions and process state
@app.route('/inspectInstance', methods=['GET']) @login_required()
def inspectInstance(): def inspectInstance():
file_content = '' file_content = ''
result = '' result = ''
...@@ -128,7 +147,7 @@ def inspectInstance(): ...@@ -128,7 +147,7 @@ def inspectInstance():
supervisore=result, partition_amount=app.config['partition_amount']) supervisore=result, partition_amount=app.config['partition_amount'])
#Reload instance process ans returns new value to ajax #Reload instance process ans returns new value to ajax
@app.route('/supervisordStatus', methods=['GET']) @login_required()
def supervisordStatus(): def supervisordStatus():
result = getSvcStatus(app.config) result = getSvcStatus(app.config)
if not (result): if not (result):
...@@ -143,7 +162,7 @@ def supervisordStatus(): ...@@ -143,7 +162,7 @@ def supervisordStatus():
html +="</tr>" html +="</tr>"
return jsonify(code=1, result=html) return jsonify(code=1, result=html)
@app.route('/removeInstance') @login_required()
def removeInstance(): def removeInstance():
if isInstanceRunning(app.config): if isInstanceRunning(app.config):
flash('Instantiation in progress, cannot remove') flash('Instantiation in progress, cannot remove')
...@@ -158,7 +177,7 @@ def removeInstance(): ...@@ -158,7 +177,7 @@ def removeInstance():
flash('Instance removed') flash('Instance removed')
return redirect(url_for('inspectInstance')) return redirect(url_for('inspectInstance'))
@app.route('/runInstanceProfile', methods=['POST']) @login_required()
def runInstanceProfile(): def runInstanceProfile():
if not os.path.exists(app.config['instance_root']): if not os.path.exists(app.config['instance_root']):
os.mkdir(app.config['instance_root']) os.mkdir(app.config['instance_root'])
...@@ -167,7 +186,7 @@ def runInstanceProfile(): ...@@ -167,7 +186,7 @@ def runInstanceProfile():
else: else:
return jsonify(result = False) return jsonify(result = False)
@app.route('/viewInstanceLog', methods=['GET']) @login_required()
def viewInstanceLog(): def viewInstanceLog():
if os.path.exists(app.config['instance_log']): if os.path.exists(app.config['instance_log']):
result = open(app.config['instance_log'], 'r').read() result = open(app.config['instance_log'], 'r').read()
...@@ -176,30 +195,22 @@ def viewInstanceLog(): ...@@ -176,30 +195,22 @@ def viewInstanceLog():
return render_template('viewLog.html', type='instance', return render_template('viewLog.html', type='instance',
result=result) result=result)
@app.route('/stopAllPartition', methods=['GET']) @login_required()
def stopAllPartition(): def stopAllPartition():
svcStopAll(app.config) svcStopAll(app.config)
return redirect(url_for('inspectInstance')) return redirect(url_for('inspectInstance'))
@app.route('/tailProcess/name/<process>', methods=['GET']) @login_required(login_redirect)
def tailProcess(process): def tailProcess(process):
return render_template('processTail.html', return render_template('processTail.html',
process_log=getSvcTailProcess(app.config, process), process=process) process_log=getSvcTailProcess(app.config, process), process=process)
@app.route('/startStopProccess/name/<process>/cmd/<action>', methods=['GET']) @login_required(login_redirect)
def startStopProccess(process, action): def startStopProccess(process, action):
svcStartStopProcess(app.config, process, action) svcStartStopProcess(app.config, process, action)
return redirect(url_for('inspectInstance')) return redirect(url_for('inspectInstance'))
@app.route('/showBuildoudAnnotate', methods=['GET']) @login_required()
def showBuildoudAnnotate():
if runBuildoutAnnotate(app.config):
flash('Started.')
else:
flash('Please run software before')
return redirect(url_for('viewBuildoudAnnotate'))
@app.route('/viewBuildoudAnnotate', methods=['GET'])
def viewBuildoudAnnotate(): def viewBuildoudAnnotate():
if os.path.exists(app.config['annotate_log']): if os.path.exists(app.config['annotate_log']):
result = open(app.config['annotate_log'], 'r').read() result = open(app.config['annotate_log'], 'r').read()
...@@ -208,35 +219,35 @@ def viewBuildoudAnnotate(): ...@@ -208,35 +219,35 @@ def viewBuildoudAnnotate():
return render_template('viewLog.html', type='Instance', return render_template('viewLog.html', type='Instance',
result=result, running=isInstanceRunning(app.config)) result=result, running=isInstanceRunning(app.config))
@app.route('/openProject/<method>', methods=['GET']) @login_required(login_redirect)
def openProject(method): def openProject(method):
return render_template('projectFolder.html', method=method, return render_template('projectFolder.html', method=method,
workDir='workspace') workDir='workspace')
@app.route('/cloneRepository', methods=['POST']) @login_required()
def cloneRepository(): def cloneRepository():
path = realpath(app.config, request.form['name'], False) path = realpath(app.config, request.form['name'], False)
data = {"repo":request.form['repo'], "user":request.form['user'], data = {"repo":request.form['repo'], "user":request.form['user'],
"email":request.form['email'], "path":path} "email":request.form['email'], "path":path}
return cloneRepo(data) return cloneRepo(data)
@app.route('/readFolder', methods=['POST']) @login_required()
def readFolder(): def readFolder():
return getFolderContent(app.config, request.form['dir']) return getFolderContent(app.config, request.form['dir'])
@app.route('/openFolder', methods=['POST']) @login_required()
def openFolder(): def openFolder():
return getFolder(app.config, request.form['dir']) return getFolder(app.config, request.form['dir'])
@app.route('/createSoftware', methods=['POST']) @login_required()
def createSoftware(): def createSoftware():
return newSoftware(request.form['folder'], app.config, session) return newSoftware(request.form['folder'], app.config, session)
@app.route("/checkFolder", methods=['POST']) @login_required()
def checkFolder(): def checkFolder():
return checkSoftwareFolder(request.form['path'], app.config) return checkSoftwareFolder(request.form['path'], app.config)
@app.route("/setCurrentProject", methods=['POST']) @login_required()
def setCurrentProject(): def setCurrentProject():
if configNewSR(app.config, request.form['path']): if configNewSR(app.config, request.form['path']):
session['title'] = getProjectTitle(app.config) session['title'] = getProjectTitle(app.config)
...@@ -244,12 +255,12 @@ def setCurrentProject(): ...@@ -244,12 +255,12 @@ def setCurrentProject():
else: else:
return jsonify(code=0, result=("Can not setup this Software Release")) return jsonify(code=0, result=("Can not setup this Software Release"))
@app.route("/manageProject", methods=['GET']) @login_required()
def manageProject(): def manageProject():
return render_template('manageProject.html', workDir='workspace', return render_template('manageProject.html', workDir='workspace',
project=getProjectList(app.config['workspace'])) project=getProjectList(app.config['workspace']))
@app.route("/getProjectStatus", methods=['POST']) @login_required()
def getProjectStatus(): def getProjectStatus():
path = realpath(app.config, request.form['project']) path = realpath(app.config, request.form['project'])
if path: if path:
...@@ -258,7 +269,7 @@ def getProjectStatus(): ...@@ -258,7 +269,7 @@ def getProjectStatus():
return jsonify(code=0, result="Can not read folder: Permission Denied") return jsonify(code=0, result="Can not read folder: Permission Denied")
#view for current software release files #view for current software release files
@app.route("/editCurrentProject") @login_required()
def editCurrentProject(): def editCurrentProject():
project = os.path.join(app.config['runner_workdir'], ".project") project = os.path.join(app.config['runner_workdir'], ".project")
if os.path.exists(project): if os.path.exists(project):
...@@ -268,7 +279,7 @@ def editCurrentProject(): ...@@ -268,7 +279,7 @@ def editCurrentProject():
return redirect(url_for('configRepo')) return redirect(url_for('configRepo'))
#create file or directory #create file or directory
@app.route("/createFile", methods=['POST']) @login_required()
def createFile(): def createFile():
path = realpath(app.config, request.form['file'], False) path = realpath(app.config, request.form['file'], False)
if not path: if not path:
...@@ -284,7 +295,7 @@ def createFile(): ...@@ -284,7 +295,7 @@ def createFile():
return jsonify(code=0, result=str(e)) return jsonify(code=0, result=str(e))
#remove file or directory #remove file or directory
@app.route("/removeFile", methods=['POST']) @login_required()
def removeFile(): def removeFile():
try: try:
if request.form['type'] == "folder": if request.form['type'] == "folder":
...@@ -295,7 +306,7 @@ def removeFile(): ...@@ -295,7 +306,7 @@ def removeFile():
except Exception, e: except Exception, e:
return jsonify(code=0, result=str(e)) return jsonify(code=0, result=str(e))
@app.route("/removeSoftwareDir", methods=['POST']) @login_required()
def removeSoftwareDir(): def removeSoftwareDir():
try: try:
data = removeSoftwareByName(app.config, request.form['name']) data = removeSoftwareByName(app.config, request.form['name'])
...@@ -304,7 +315,7 @@ def removeSoftwareDir(): ...@@ -304,7 +315,7 @@ def removeSoftwareDir():
return jsonify(code=0, result=str(e)) return jsonify(code=0, result=str(e))
#read file and return content to ajax #read file and return content to ajax
@app.route("/getFileContent", methods=['POST']) @login_required()
def getFileContent(): def getFileContent():
file_path = realpath(app.config, request.form['file']) file_path = realpath(app.config, request.form['file'])
if file_path: if file_path:
...@@ -316,7 +327,7 @@ def getFileContent(): ...@@ -316,7 +327,7 @@ def getFileContent():
else: else:
return jsonify(code=0, result="Error: No such file!") return jsonify(code=0, result="Error: No such file!")
@app.route("/saveFileContent", methods=['POST']) @login_required()
def saveFileContent(): def saveFileContent():
file_path = realpath(app.config, request.form['file']) file_path = realpath(app.config, request.form['file'])
if file_path: if file_path:
...@@ -325,7 +336,7 @@ def saveFileContent(): ...@@ -325,7 +336,7 @@ def saveFileContent():
else: else:
return jsonify(code=0, result="Error: No such file!") return jsonify(code=0, result="Error: No such file!")
@app.route("/changeBranch", methods=['POST']) @login_required()
def changeBranch(): def changeBranch():
path = realpath(app.config, request.form['project']) path = realpath(app.config, request.form['project'])
if path: if path:
...@@ -333,7 +344,7 @@ def changeBranch(): ...@@ -333,7 +344,7 @@ def changeBranch():
else: else:
return jsonify(code=0, result="Can not read folder: Permission Denied") return jsonify(code=0, result="Can not read folder: Permission Denied")
@app.route("/newBranch", methods=['POST']) @login_required()
def newBranch(): def newBranch():
path = realpath(app.config, request.form['project']) path = realpath(app.config, request.form['project'])
if path: if path:
...@@ -344,13 +355,13 @@ def newBranch(): ...@@ -344,13 +355,13 @@ def newBranch():
else: else:
return jsonify(code=0, result="Can not read folder: Permission Denied") return jsonify(code=0, result="Can not read folder: Permission Denied")
@app.route("/getProjectDiff/<project>", methods=['GET']) @login_required(login_redirect)
def getProjectDiff(project): def getProjectDiff(project):
path = os.path.join(app.config['workspace'], project) path = os.path.join(app.config['workspace'], project)
return render_template('projectDiff.html', project=project, return render_template('projectDiff.html', project=project,
diff=getDiff(path)) diff=getDiff(path))
@app.route("/pushProjectFiles", methods=['POST']) @login_required()
def pushProjectFiles(): def pushProjectFiles():
path = realpath(app.config, request.form['project']) path = realpath(app.config, request.form['project'])
if path: if path:
...@@ -358,7 +369,7 @@ def pushProjectFiles(): ...@@ -358,7 +369,7 @@ def pushProjectFiles():
else: else:
return jsonify(code=0, result="Can not read folder: Permission Denied") return jsonify(code=0, result="Can not read folder: Permission Denied")
@app.route("/pullProjectFiles", methods=['POST']) @login_required()
def pullProjectFiles(): def pullProjectFiles():
path = realpath(app.config, request.form['project']) path = realpath(app.config, request.form['project'])
if path: if path:
...@@ -366,7 +377,7 @@ def pullProjectFiles(): ...@@ -366,7 +377,7 @@ def pullProjectFiles():
else: else:
return jsonify(code=0, result="Can not read folder: Permission Denied") return jsonify(code=0, result="Can not read folder: Permission Denied")
@app.route("/checkFileType", methods=['POST']) @login_required()
def checkFileType(): def checkFileType():
path = realpath(app.config, request.form['path']) path = realpath(app.config, request.form['path'])
if not path: if not path:
...@@ -376,7 +387,7 @@ def checkFileType(): ...@@ -376,7 +387,7 @@ def checkFileType():
else: else:
return jsonify(code=0, result="Can not open a binary file, please select a text file!") return jsonify(code=0, result="Can not open a binary file, please select a text file!")
@app.route("/getmd5sum", methods=['POST']) @login_required()
def getmd5sum(): def getmd5sum():
realfile = realpath(app.config, request.form['file']) realfile = realpath(app.config, request.form['file'])
if not realfile: if not realfile:
...@@ -388,7 +399,7 @@ def getmd5sum(): ...@@ -388,7 +399,7 @@ def getmd5sum():
return jsonify(code=0, result="Can not get md5sum for this file!") return jsonify(code=0, result="Can not get md5sum for this file!")
#return informations about state of slapgrid process #return informations about state of slapgrid process
@app.route("/slapgridResult", methods=['POST']) @login_required()
def slapgridResult(): def slapgridResult():
software_state = isSoftwareRunning(app.config) software_state = isSoftwareRunning(app.config)
instance_state = isInstanceRunning(app.config) instance_state = isInstanceRunning(app.config)
...@@ -402,12 +413,12 @@ def slapgridResult(): ...@@ -402,12 +413,12 @@ def slapgridResult():
return jsonify(software=software_state, instance=instance_state, return jsonify(software=software_state, instance=instance_state,
result=(instance_state or software_state), content=log_result) result=(instance_state or software_state), content=log_result)
@app.route("/stopSlapgrid", methods=['POST']) @login_required()
def stopSlapgrid(): def stopSlapgrid():
result = killRunningSlapgrid(app.config, request.form['type']) result = killRunningSlapgrid(app.config, request.form['type'])
return jsonify(result=result) return jsonify(result=result)
@app.route("/getPath", methods=['POST']) @login_required()
def getPath(): def getPath():
files = request.form['file'].split('#') files = request.form['file'].split('#')
list = [] list = []
...@@ -425,7 +436,7 @@ def getPath(): ...@@ -425,7 +436,7 @@ def getPath():
return jsonify(code=1, result=realfile) return jsonify(code=1, result=realfile)
#update instance parameter into a local xml file #update instance parameter into a local xml file
@app.route("/saveParameterXml", methods=['POST']) @login_required()
def saveParameterXml(): def saveParameterXml():
project = os.path.join(app.config['runner_workdir'], ".project") project = os.path.join(app.config['runner_workdir'], ".project")
if not os.path.exists(project): if not os.path.exists(project):
...@@ -452,7 +463,7 @@ def saveParameterXml(): ...@@ -452,7 +463,7 @@ def saveParameterXml():
return jsonify(code=1, result="") return jsonify(code=1, result="")
#read instance parameters into the local xml file and return a dict #read instance parameters into the local xml file and return a dict
@app.route("/getParameterXml/<request>", methods=['GET']) @login_required()
def getParameterXml(request): def getParameterXml(request):
param_path = os.path.join(app.config['runner_workdir'], ".parameter.xml") param_path = os.path.join(app.config['runner_workdir'], ".parameter.xml")
if not os.path.exists(param_path): if not os.path.exists(param_path):
...@@ -469,7 +480,7 @@ def getParameterXml(request): ...@@ -469,7 +480,7 @@ def getParameterXml(request):
return jsonify(code=1, result=parameters) return jsonify(code=1, result=parameters)
#update user account data #update user account data
@app.route("/updateAccount", methods=['POST']) @login_required()
def updateAccount(): def updateAccount():
account = [] account = []
user = os.path.join(app.config['runner_workdir'], '.users') user = os.path.join(app.config['runner_workdir'], '.users')
...@@ -477,8 +488,82 @@ def updateAccount(): ...@@ -477,8 +488,82 @@ def updateAccount():
account.append(request.form['password'].strip()) account.append(request.form['password'].strip())
account.append(request.form['email'].strip()) account.append(request.form['email'].strip())
account.append(request.form['name'].strip()) account.append(request.form['name'].strip())
result = saveSession(app.config, session, account) code = request.form['rcode'].strip()
recovery_code = open(os.path.join(app.config['etc_dir'], ".rcode"), "r").read()
if code != recovery_code:
return jsonify(code=0, result="Your password recovery code is not valid!")
result = saveSession(app.config, account)
if type(result) == type(""): if type(result) == type(""):
return jsonify(code=0, result=result) return jsonify(code=0, result=result)
else: else:
return jsonify(code=1, result="") return jsonify(code=1, result="")
\ No newline at end of file
#update user account data
@app.route("/configAccount", methods=['POST'])
def configAccount():
last_account = getSession(app.config)
if not last_account:
account = []
user = os.path.join(app.config['runner_workdir'], '.users')
account.append(request.form['username'].strip())
account.append(request.form['password'].strip())
account.append(request.form['email'].strip())
account.append(request.form['name'].strip())
code = request.form['rcode'].strip()
recovery_code = open(os.path.join(app.config['etc_dir'], ".rcode"), "r").read()
if code != recovery_code:
return jsonify(code=0, result="Your password recovery code is not valid!")
result = saveSession(app.config, account)
if type(result) == type(""):
return jsonify(code=0, result=result)
else:
return jsonify(code=1, result="")
return jsonify(code=0, result="Unable to respond to your request, permission denied.")
#Setup List of URLs
app.add_url_rule('/', 'home', home)
app.add_url_rule('/editSoftwareProfile', 'editSoftwareProfile', editSoftwareProfile)
app.add_url_rule('/inspectSoftware', 'inspectSoftware', inspectSoftware)
app.add_url_rule('/removeSoftware', 'removeSoftware', removeSoftware)
app.add_url_rule('/runSoftwareProfile', 'runSoftwareProfile', runSoftwareProfile, methods=['POST'])
app.add_url_rule('/viewSoftwareLog', 'viewSoftwareLog', viewSoftwareLog, methods=['GET'])
app.add_url_rule('/editInstanceProfile', 'editInstanceProfile', editInstanceProfile)
app.add_url_rule('/inspectInstance', 'inspectInstance', inspectInstance, methods=['GET'])
app.add_url_rule('/supervisordStatus', 'supervisordStatus', supervisordStatus, methods=['GET'])
app.add_url_rule('/runInstanceProfile', 'runInstanceProfile', runInstanceProfile, methods=['POST'])
app.add_url_rule('/removeInstance', 'removeInstance', removeInstance)
app.add_url_rule('/viewInstanceLog', 'viewInstanceLog', viewInstanceLog, methods=['GET'])
app.add_url_rule('/stopAllPartition', 'stopAllPartition', stopAllPartition, methods=['GET'])
app.add_url_rule('/tailProcess/name/<process>', 'tailProcess', tailProcess, methods=['GET'])
app.add_url_rule('/startStopProccess/name/<process>/cmd/<action>', 'startStopProccess', startStopProccess, methods=['GET'])
app.add_url_rule('/viewBuildoudAnnotate', 'viewBuildoudAnnotate', viewBuildoudAnnotate, methods=['GET'])
app.add_url_rule("/getParameterXml/<request>", 'getParameterXml', getParameterXml, methods=['GET'])
app.add_url_rule("/stopSlapgrid", 'stopSlapgrid', stopSlapgrid, methods=['POST'])
app.add_url_rule("/slapgridResult", 'slapgridResult', slapgridResult, methods=['POST'])
app.add_url_rule("/getmd5sum", 'getmd5sum', getmd5sum, methods=['POST'])
app.add_url_rule("/checkFileType", 'checkFileType', checkFileType, methods=['POST'])
app.add_url_rule("/pullProjectFiles", 'pullProjectFiles', pullProjectFiles, methods=['POST'])
app.add_url_rule("/pushProjectFiles", 'pushProjectFiles', pushProjectFiles, methods=['POST'])
app.add_url_rule("/getProjectDiff/<project>", 'getProjectDiff', getProjectDiff, methods=['GET'])
app.add_url_rule("/newBranch", 'newBranch', newBranch, methods=['POST'])
app.add_url_rule("/changeBranch", 'changeBranch', changeBranch, methods=['POST'])
app.add_url_rule("/saveFileContent", 'saveFileContent', saveFileContent, methods=['POST'])
app.add_url_rule("/removeSoftwareDir", 'removeSoftwareDir', removeSoftwareDir, methods=['POST'])
app.add_url_rule("/getFileContent", 'getFileContent', getFileContent, methods=['POST'])
app.add_url_rule("/removeFile", 'removeFile', removeFile, methods=['POST'])
app.add_url_rule("/createFile", 'createFile', createFile, methods=['POST'])
app.add_url_rule("/editCurrentProject", 'editCurrentProject', editCurrentProject)
app.add_url_rule("/getProjectStatus", 'getProjectStatus', getProjectStatus, methods=['POST'])
app.add_url_rule('/openProject/<method>', 'openProject', openProject, methods=['GET'])
app.add_url_rule("/manageProject", 'manageProject', manageProject, methods=['GET'])
app.add_url_rule("/setCurrentProject", 'setCurrentProject', setCurrentProject, methods=['POST'])
app.add_url_rule("/checkFolder", 'checkFolder', checkFolder, methods=['POST'])
app.add_url_rule('/createSoftware', 'createSoftware', createSoftware, methods=['POST'])
app.add_url_rule('/cloneRepository', 'cloneRepository', cloneRepository, methods=['POST'])
app.add_url_rule('/openFolder', 'openFolder', openFolder, methods=['POST'])
app.add_url_rule('/readFolder', 'readFolder', readFolder, methods=['POST'])
app.add_url_rule('/configRepo', 'configRepo', configRepo)
app.add_url_rule("/saveParameterXml", 'saveParameterXml', saveParameterXml, methods=['POST'])
app.add_url_rule("/getPath", 'getPath', getPath, methods=['POST'])
app.add_url_rule("/myAccount", 'myAccount', myAccount)
app.add_url_rule("/updateAccount", 'updateAccount', updateAccount, methods=['POST'])
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment