├── .gitignore ├── .idea └── vcs.xml ├── Dockerfile ├── README.md ├── app ├── add_user.py ├── add_user_default.sh ├── config.json ├── db.py ├── forms.py ├── input.py ├── main.py ├── markup_page.py ├── requirements.txt ├── review.py ├── static │ ├── blank-img.jpg │ ├── css │ │ ├── bootstrap.min.css │ │ └── main.css │ └── js │ │ ├── bootstrap.min.js │ │ ├── jquery.min.js │ │ └── main.js ├── templates │ ├── bus.html │ ├── container.html │ ├── door.html │ ├── header.html │ ├── heads.html │ ├── input.html │ ├── login.html │ ├── nav_bar.html │ ├── onerect.html │ ├── register.html │ ├── review_choose_user.html │ ├── tiles.html │ ├── trainsegmentation.html │ └── uploadfiles.html └── uwsgi.ini └── confd_nginx.conf /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.pyc 3 | *.pyo 4 | app/__pycache__ 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM tiangolo/uwsgi-nginx-flask:python3.8 2 | 3 | COPY ./confd_nginx.conf /etc/nginx/conf.d/nginx.conf 4 | COPY ./app /app 5 | 6 | RUN apt update && apt install -y ca-certificates && update-ca-certificates --fresh 7 | RUN pip install -r /app/requirements.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a markup server, which may be connected to an ML core. 2 | 3 | 4 | You can run it by a docker with nginx as a web-server. 5 | To start you will need a MONGODB and an S3 bucket, where all input and result annotation are stored. 6 | 7 | Pay attention to YOUR_SITE in confd_nginx.conf before building a docker image - this should correspond to your site name. 8 | 9 | To build and run: 10 | 11 | ``` 12 | sudo docker build -t markup . 13 | 14 | sudo docker create --name markup_server -p 27017:27017 -p 443:443 \ 15 | -e .... 16 | markup:latest 17 | sudo docker cp ssl_and_bundle.crt markup_server:/app/ssl.crt 18 | sudo docker cp key.pem markup_server:/app/ssl.pem 19 | sudo docker start markup_server 20 | ``` 21 | 22 | Environmental variables: 23 | - SECRET_KEY - random key for FLASK 24 | - MONGO_DB_ADDRESS - like mongodb:// .... , that contain everything to connect 25 | - MONGO_COLLECTION - just a name of the collection for this service 26 | - AWS_KEY 27 | - AWS_KEY_ID 28 | 29 | You can exclude the SSL certificate by changing protocol in nginx config (change 443 port to 80 and remove lines about ssl certificates). 30 | 31 | 32 | To add a new user, run add_user.sh role token and a token. Then, you can register a new user passing him the token and the following link: your_site.com/register/role/token. 33 | 34 | Or, being an admin, you can add user through the address line: /add_pre_user/role/token (token is a random key-digit sequence) 35 | 36 | Possible roles for users: 37 | - admin 38 | - operator 39 | 40 | All html templates are placed in the folder templates with corresponding names, described in config.json 41 | 42 |

Input data

43 | 44 | There are two ways to upload new data to markup. 45 | 1) Just choose files in "upload" on the site your_site.com/markup/ 46 | 2) Or your can upload files through POST requests. ('/upload/'). Using API you also can upload results from an ML machine throug '/upload_result/' attaching 2 files: image + json. Then, you'll see pre-marked up images in b>your_site.com/markup/ 47 | 48 |

Debug

49 | 50 | Also, to debug, you can run python3 main.py from app folder. 51 | 52 | You'll also need two more env variables DEBUG_ADMIN_EMAIL and DEBUG_ADMIN_PASSWORD to create a new admin user without running add_user.sh script. 53 | -------------------------------------------------------------------------------- /app/add_user.py: -------------------------------------------------------------------------------- 1 | import db 2 | import sys 3 | 4 | if len(sys.argv)==3: 5 | print('role: ',sys.argv[1]) 6 | print('token: ',sys.argv[2]) 7 | db.add_pre_user(sys.argv[1],sys.argv[2]) 8 | print('pre user was added') 9 | -------------------------------------------------------------------------------- /app/add_user_default.sh: -------------------------------------------------------------------------------- 1 | export MONGO_DB_ADDRESS="MONGO_DB_ADDRESS" 2 | 3 | python3 add_user.py $1 $2 4 | -------------------------------------------------------------------------------- /app/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "applications": ["heads","trainsegmentation","container","door"], 3 | 4 | "trainsegmentation": {"width": 448,"height": 336,"types":["platform number","coupler","cont_p1","cont_p2","cont_p3"],"bucket":"trainsegmentation.markup","input":"processed1","output":"processed2"}, 5 | "heads": {"width": 1520,"height": 1520,"types":["adult","child","hidden"],"bucket":"heads.markup","input":"images","output":"processed","image_ext":"jpg"}, 6 | "door": {"width":1520,"height":1520,"types":["opened","closed"],"bucket":"door.markup","input":"images","output":"processed","image_ext":"jpg"}, 7 | "container": {"width": 960,"height": 540,"types":["cont_p1","cont_p2","cont_p3"],"bucket":"container.markup","input":"images","output":"processed","image_ext":"png"} 8 | 9 | } 10 | -------------------------------------------------------------------------------- /app/db.py: -------------------------------------------------------------------------------- 1 | import pymongo 2 | import sys 3 | import os 4 | import time 5 | from passlib.hash import sha256_crypt 6 | from datetime import datetime 7 | import boto3 8 | import json 9 | import random 10 | 11 | ##Create a MongoDB client, open a connection to Amazon DocumentDB as a replica set and specify the read preference as secondary preferred 12 | line=os.environ['MONGO_DB_ADDRESS'] 13 | collection=os.environ['MONGO_COLLECTION'] 14 | 15 | client = pymongo.MongoClient(line) 16 | 17 | def get_random_file(application,folder,user_id=None,date=None,exts=None): 18 | filter={'folder':folder} 19 | if not user_id is None: 20 | filter['user_id']=user_id 21 | if not date is None: 22 | filter['date']=date 23 | if not exts is None: 24 | dicts=[] 25 | for j in range(len(exts)): 26 | dicts.append({'filename':{'$regex':'.*'+exts[j]+'.*'}}) 27 | filter['$or']=dicts 28 | 29 | count = client[application].files.find(filter).count() 30 | if count==0: 31 | return None 32 | index= random.randint(0,count-1) 33 | return client[application].files.find(filter)[index] 34 | 35 | def get_files_count(application,folder,user_id=None,date=None,exts=None): 36 | filter={'folder':folder} 37 | if not user_id is None: 38 | filter['user_id']=user_id 39 | if not date is None: 40 | filter['date']=date 41 | if not exts is None: 42 | dicts=[] 43 | for j in range(len(exts)): 44 | dicts.append({'filename':{'$regex':'.*'+exts[j]+'.*'}}) 45 | filter['$or']=dicts 46 | count = client[application].files.find(filter).count() 47 | return count 48 | 49 | 50 | def get_files(application,folder,user_id=None,ext=None,date=None): 51 | filter={'folder':folder} 52 | if not user_id is None: 53 | filter["user_id"]=user_id 54 | if not date is None: 55 | filter['date']=date 56 | 57 | cursor=client[application].files.find(filter) 58 | 59 | res=[] 60 | for document in cursor: 61 | if not ext is None: 62 | 63 | if ext in document['filename']: 64 | res.append(document) 65 | else: 66 | res.append(document) 67 | 68 | 69 | return res 70 | 71 | 72 | def delete_file(application,folder,file): 73 | client[application].files.delete_one({'folder':folder,'filename':file}) 74 | 75 | def put_file(application,folder,file,user_id=None): 76 | date=datetime.today().strftime("%d-%m-%Y") 77 | if not user_id is None: 78 | client[application].files.insert_one({'folder':folder,'filename':file,'user_id':user_id,'date':date}) 79 | else: 80 | client[application].files.insert_one({'folder':folder,'filename':file,'date':date}) 81 | 82 | def del_pre_user(role,token): 83 | client[collection].pre_users.delete_one({'role':role,'token':token}) 84 | 85 | def add_pre_user(role,token): 86 | if not client[collection].pre_users.find_one({'role':role,'token':token}): 87 | client[collection].pre_users.insert_one({'role':role,'token':token}) 88 | 89 | def check_pre_user(role,token): 90 | if client[collection].pre_users.find_one({'role':role,'token':token}): 91 | return True 92 | else: 93 | return False 94 | 95 | def add_user(name,email,password,role): 96 | hash=sha256_crypt.hash(password) 97 | if not client[collection].users.find_one({'name':name}): 98 | client[collection].users.insert_one({'name':name,'email':email,'hash':hash,'role':role}) 99 | return 100 | 101 | def is_user_by_name(name): 102 | if client[collection].users.find_one({'name':name}): 103 | return True 104 | return False 105 | 106 | def is_user_by_email(email): 107 | if client[collection].users.find_one({'email':email}): 108 | return True 109 | return False 110 | 111 | def check_password(email,password): 112 | user=client[collection].users.find_one({'email':email}) 113 | if user is None: 114 | return False 115 | return sha256_crypt.verify(password,user['hash']) 116 | 117 | def get_all_users(): 118 | users=client[collection].users.find({}) 119 | names=[] 120 | ids=[] 121 | for user in users: 122 | names.append(user['name']) 123 | ids.append(user['email']) 124 | return names,ids 125 | def get_user(email): 126 | return client[collection].users.find_one({'email':email}) -------------------------------------------------------------------------------- /app/forms.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField, PasswordField, BooleanField, SubmitField, Form 3 | from wtforms.validators import ValidationError, DataRequired, Email, EqualTo 4 | import db 5 | 6 | class RegistrationForm(FlaskForm): 7 | username = StringField('Username', validators=[DataRequired()]) 8 | email = StringField('Email', validators=[DataRequired(), Email()]) 9 | password = PasswordField('Password', validators=[DataRequired()]) 10 | password2 = PasswordField( 11 | 'Repeat Password', validators=[DataRequired(), EqualTo('password')]) 12 | submit = SubmitField('Register') 13 | def __init__(self,role_): 14 | self.role=role_ 15 | FlaskForm.__init__(self) 16 | 17 | def validate_username(self, username): 18 | if db.is_user_by_name(username.data): 19 | ValidationError('Please use a different username.') 20 | return True 21 | 22 | def validate_email(self, email): 23 | if db.is_user_by_email(email.data): 24 | ValidationError('Please use a different email.') 25 | return True 26 | -------------------------------------------------------------------------------- /app/input.py: -------------------------------------------------------------------------------- 1 | from flask import render_template 2 | from flask import send_from_directory, send_file,url_for,redirect 3 | from flask import request 4 | import json 5 | import logging 6 | from flask_login import login_user, logout_user, current_user, login_required,LoginManager,UserMixin 7 | from flask import Blueprint,session 8 | import db 9 | import os 10 | import boto3 11 | import botocore 12 | import datetime 13 | import calendar 14 | 15 | 16 | s3 = boto3.resource('s3',aws_access_key_id=os.environ['AWS_KEY_ID'], 17 | aws_secret_access_key=os.environ['AWS_KEY']) 18 | s3_client = boto3.client('s3',aws_access_key_id=os.environ['AWS_KEY_ID'], 19 | aws_secret_access_key=os.environ['AWS_KEY']) 20 | 21 | 22 | 23 | input_api = Blueprint('input_api', __name__) 24 | f=open(os.path.join(os.path.dirname(os.path.realpath(__file__)),'config.json'),'r+') 25 | config=json.load(f) 26 | f.close() 27 | 28 | @input_api.route('/input/del_day///', methods=['GET','POST']) 29 | @login_required 30 | def del_day(application,folder,day): 31 | logging.info('del_day '+application+' '+folder+' '+day) 32 | user=db.get_user(current_user.id) 33 | if user['role']!='admin': 34 | return 'You should be an admin' 35 | 36 | bucket_name = config[application]['bucket'] 37 | 38 | print(application,folder,day) 39 | files=db.get_files(application,folder,date=day) 40 | for i in range(len(files)): 41 | try: 42 | s3.Object(bucket_name,folder+'/'+files[i]['filename']).delete() 43 | db.delete_file(application,folder,files[i]['filename']) 44 | 45 | except botocore.exceptions.ClientError as e: 46 | logging.error(bucket_name+' '+folder+' '+ files[i]['filename']+' were not deleted') 47 | 48 | 49 | return redirect('/input/'+application) 50 | 51 | 52 | @input_api.route('/input/', methods=['GET','POST']) 53 | @login_required 54 | def input_get(application): 55 | logging.info('Visited input') 56 | user=db.get_user(current_user.id) 57 | 58 | if user['role']!='admin': 59 | return 'You should be an admin' 60 | 61 | if request.method == 'POST': 62 | session['cur_month']=request.form['month'] 63 | 64 | if session.get('cur_month') is not None: 65 | cur_month=session.get('cur_month') 66 | 67 | else: 68 | cur_month=datetime.datetime.today().strftime("%Y-%m") 69 | 70 | year=int(cur_month.split('-')[0]) 71 | month=int(cur_month.split('-')[1]) 72 | 73 | num_days = calendar.monthrange(year, month)[1] 74 | 75 | days = [datetime.date(year, month, day) for day in range(1, num_days+1)] 76 | 77 | day_by_day=[] 78 | for i in range(len(days)): 79 | day={} 80 | day['date']=days[i].strftime("%d-%m-%Y") 81 | files=db.get_files(application,config[application]['input'],date=day['date']) 82 | if len(files)>0: 83 | day['count']=len(files) 84 | day_by_day.append(day) 85 | 86 | return render_template('input.html',application=application,cur_month=cur_month,day_by_day=day_by_day,input=config[application]['input'],log_in_or_out='out') 87 | 88 | -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | from flask import Flask,render_template 2 | from flask import send_from_directory, send_file,url_for,redirect 3 | from flask import request 4 | import json 5 | import os 6 | import shutil 7 | import random 8 | import logging 9 | import time 10 | import boto3 11 | import botocore 12 | import db 13 | from forms import RegistrationForm 14 | from review import review_api 15 | from input import input_api 16 | from markup_page import markuppage_api 17 | 18 | from flask_login import login_user, logout_user, current_user, login_required,LoginManager,UserMixin 19 | from flask import session 20 | 21 | logging.basicConfig(filename='markup.log',level=logging.INFO) 22 | 23 | s3 = boto3.resource('s3',aws_access_key_id=os.environ['AWS_KEY_ID'], 24 | aws_secret_access_key=os.environ['AWS_KEY']) 25 | s3_client = boto3.client('s3',aws_access_key_id=os.environ['AWS_KEY_ID'], 26 | aws_secret_access_key=os.environ['AWS_KEY']) 27 | 28 | 29 | app = Flask(__name__) 30 | app.config["SECRET_KEY"] = os.environ['SECRET_KEY'] 31 | app.register_blueprint(review_api) 32 | app.register_blueprint(input_api) 33 | app.register_blueprint(markuppage_api) 34 | 35 | 36 | 37 | f=open(os.path.join(os.path.dirname(os.path.realpath(__file__)),'config.json'),'r+') 38 | config=json.load(f) 39 | f.close() 40 | 41 | login_manager = LoginManager() 42 | login_manager.init_app(app) 43 | 44 | class User(UserMixin): 45 | pass 46 | 47 | @login_manager.user_loader 48 | def user_loader(email): 49 | 50 | user_dict=db.get_user(email) 51 | if user_dict is None: 52 | return 53 | 54 | user = User() 55 | user.id = user_dict['email'] 56 | user.name=user_dict['email'] 57 | return user 58 | 59 | 60 | @login_manager.request_loader 61 | def request_loader(request): 62 | email = request.form.get('email') 63 | 64 | user_dict=db.get_user(email) 65 | 66 | if user_dict is None: 67 | return 68 | 69 | user = User() 70 | user.id = user_dict['email'] 71 | user.name=user_dict['email'] 72 | 73 | user.is_authenticated = db.check_password(email,request.form['password']) 74 | 75 | return user 76 | 77 | @app.route('/') 78 | @login_required 79 | def index(): 80 | logging.info('Visited root') 81 | return 'Logged in as: ' + current_user.id 82 | 83 | @app.route('/login', methods=['GET', 'POST']) 84 | def login(): 85 | if request.method == 'GET': 86 | return render_template('login.html', title='Login') 87 | email = request.form['email'] 88 | if db.check_password(email,request.form['password']): 89 | user_dict=db.get_user(email) 90 | user = User() 91 | user.id = user_dict['email'] 92 | user.name= user_dict['email'] 93 | login_user(user) 94 | if 'url' in session: 95 | return redirect(session['url']) 96 | else: 97 | return 'you have logged in the makrup service' 98 | return 'Bad login' 99 | 100 | @app.route('/logout') 101 | def logout(): 102 | logout_user() 103 | return redirect(url_for('login')) 104 | @login_manager.unauthorized_handler 105 | def unauthorized_handler(): 106 | return redirect('/login') 107 | 108 | @app.route('/add_pre_user//', methods=['GET']) 109 | @login_required 110 | def add_pre_user(role,token): 111 | print('add pre user',role,token) 112 | user=db.get_user(current_user.id) 113 | if user['role']!='admin': 114 | return 'You should be an admin' 115 | db.add_pre_user(role,token) 116 | return "pre user was added" 117 | 118 | 119 | @app.route('/register//', methods=['GET']) 120 | def register_get(role,token): 121 | print('register ',role,token) 122 | if db.check_pre_user(role,token): 123 | form = RegistrationForm(role) 124 | return render_template('register.html', title='Register', form=form) 125 | return "no such user" 126 | 127 | @app.route('/register//', methods=['POST']) 128 | def register_post(role,token): 129 | form = RegistrationForm(role) 130 | if form.validate_on_submit(): 131 | db.add_user(form.username.data,form.email.data,form.password.data,form.role) 132 | db.del_pre_user(role,token) 133 | return redirect(url_for('login')) 134 | return render_template('register.html', title='Register', form=form) 135 | 136 | @app.route('/upload/',methods=['GET']) 137 | @login_required 138 | def upload_get(application): 139 | logging.info('Visited get /upload/'+application) 140 | if application in config['applications']: 141 | return render_template('uploadfiles.html',application=application,log_in_or_out='out') 142 | else: 143 | return "no such application" 144 | 145 | @app.route('/upload/',methods=['POST']) 146 | def upload_post(application): 147 | logging.info('Visited post /upload/'+application) 148 | 149 | if application in config['applications']: 150 | 151 | bucket_name = config[application]['bucket'] 152 | files = request.files.getlist("files") 153 | for file in files: 154 | 155 | if '.jpg' in file.filename or '.png' in file.filename: 156 | 157 | db.put_file(application,config[application]['input'],file.filename) 158 | s3.Bucket(bucket_name).put_object(Key=config[application]['input']+'/'+file.filename, Body=file.read()) 159 | 160 | return redirect('/upload/'+application) 161 | else: 162 | return "no such application" 163 | 164 | @app.route('/upload_result/',methods=['POST']) 165 | def upload_result_post(application): 166 | logging.info('Visited post /upload_result/'+application) 167 | 168 | if application in config['applications']: 169 | bucket_name = config[application]['bucket'] 170 | files = request.files.getlist("files") 171 | if len(files)==2 and ('.jpg' in files[0].filename or '.png' in files[0].filename) and ('.json' in files[1].filename): 172 | db.put_file(application,config[application]['input'],files[0].filename) 173 | s3.Bucket(bucket_name).put_object(Key=config[application]['input']+'/'+files[0].filename, Body=files[0].read()) 174 | db.put_file(application,config[application]['input'],files[1].filename) 175 | s3.Bucket(bucket_name).put_object(Key=config[application]['input']+'/'+files[1].filename, Body=files[1].read()) 176 | return "ok" 177 | else: 178 | return "no such application" 179 | 180 | 181 | 182 | 183 | 184 | @app.route('//get_left_images') 185 | def get_left_images(application): 186 | if application in config['applications']: 187 | folder=config[application]['input'] 188 | return str(db.get_files_count(application,folder,exts=['.png','.jpg'])) 189 | else: 190 | return None 191 | 192 | @app.route('//get_config/') 193 | def get_config(application,field): 194 | if application in config['applications']: 195 | return json.dumps(config[application][field]) 196 | return None 197 | 198 | @app.route('//get_file_names/') 199 | def get_file_names(application,folder): 200 | 201 | if application in config['applications']: 202 | files=db.get_files(application,folder) 203 | res=[] 204 | for f in files: 205 | res.append(f['filename']) 206 | return json.dumps(res) 207 | else: 208 | return None 209 | @app.route('//get_file//') 210 | def get_file(application,folder,filename): 211 | 212 | if application in config['applications']: 213 | logging.info('Returning file with application = {}, folder = {}, filename = {}'.format(application,folder, filename)) 214 | bucket_name = config[application]['bucket'] 215 | file_key = folder+"/" + filename 216 | try: 217 | logging.info('before get') 218 | response = s3_client.get_object(Bucket=bucket_name, Key=file_key) 219 | logging.info('after get') 220 | except botocore.exceptions.ClientError as e: 221 | db.delete_file(application,config[application]['input'],filename) 222 | return None 223 | 224 | data = response['Body'] 225 | logging.info('before send') 226 | return send_file(data, attachment_filename=filename) 227 | else: 228 | return None 229 | 230 | 231 | 232 | @app.route('//get_random_pic_name') 233 | @login_required 234 | def get_random_pic_name(application): 235 | max_n=1024 236 | 237 | if application in config['applications']: 238 | 239 | folder=config[application]['input'] 240 | 241 | 242 | doc=db.get_random_file(application,folder,exts=['.jpg','.png']) 243 | if not doc is None: 244 | return '/'+application+'/get_file/'+folder+'/'+doc['filename'] 245 | return 'None' 246 | else: 247 | return 'None' 248 | 249 | @app.route('//get_json//') 250 | @login_required 251 | def get_json(application,folder,image_filename): 252 | 253 | if application in config['applications']: 254 | start=time.time() 255 | bucket_name = config[application]['bucket'] 256 | logging.info('get json, app = {}, bucket_name = {}, filename={}'.format(application, bucket_name,image_filename)) 257 | file_key=folder+'/'+image_filename.split('.')[0]+'.json' 258 | try: 259 | 260 | response=s3_client.get_object(Bucket=bucket_name, Key=file_key) 261 | str = response['Body'].read().decode('utf-8') 262 | return str 263 | except botocore.exceptions.ClientError as e: 264 | return '{}' 265 | return '{}' 266 | else: 267 | return '{}' 268 | 269 | 270 | 271 | @app.route('/save/',methods=['POST']) 272 | @login_required 273 | def savePost(application): 274 | if application in config['applications']: 275 | bucket_name = config[application]['bucket'] 276 | logging.info('savePost, application = {}, bucket_name = {}'.format(application, bucket_name)) 277 | data = request.data 278 | 279 | dict=json.loads(data.decode()) 280 | fn=dict['imageName'] 281 | 282 | 283 | #recived markup 284 | 285 | image_path = fn.split('/')[3]+'/'+fn.split('/')[4] 286 | #print(image_path) 287 | 288 | if image_path.endswith('.jpg'): 289 | mark_path = image_path.replace(config[application]['input'], config[application]['output']).replace('.jpg','.json') 290 | else: 291 | if image_path.endswith('.png'): 292 | mark_path = image_path.replace(config[application]['input'], config[application]['output']).replace('.png', '.json') 293 | else: 294 | return "wrong file format" 295 | 296 | logging.info("Saving markup to " + mark_path) 297 | s3.Bucket(bucket_name).put_object(Key=mark_path, Body=data) 298 | db.put_file(application,config[application]['output'],os.path.basename(mark_path),user_id=current_user.id) 299 | 300 | #move image 301 | old_location = image_path 302 | new_location = image_path.replace(config[application]['input'], config[application]['output']) 303 | logging.info("Moving image from {} to {}".format(old_location, new_location)) 304 | s3.Object(bucket_name, new_location).copy_from(CopySource=bucket_name+'/' + old_location) 305 | db.put_file(application,config[application]['output'],os.path.basename(new_location)) 306 | 307 | s3.Object(bucket_name, old_location).delete() 308 | db.delete_file(application,config[application]['input'],os.path.basename(old_location)) 309 | #print('to del mark:',mark_path) 310 | db.delete_file(application,config[application]['input'],os.path.basename(mark_path)) 311 | s3.Object(bucket_name,mark_path.replace(config[application]['output'],config[application]['input'])).delete() 312 | 313 | return "ok" 314 | else: 315 | return "no such application" 316 | 317 | if __name__=='__main__': 318 | 319 | db.add_user(os.environ['DEBUG_ADMIN_EMAIL'],os.environ['DEBUG_ADMIN_EMAIL'],os.environ['DEBUG_ADMIN_PASSWORD'],'admin') 320 | app.run(host='0.0.0.0',port=5000) 321 | -------------------------------------------------------------------------------- /app/markup_page.py: -------------------------------------------------------------------------------- 1 | from flask import render_template 2 | from flask import send_from_directory, send_file,url_for,redirect 3 | from flask import request 4 | import json 5 | import logging 6 | import os 7 | from flask_login import login_user, logout_user, current_user, login_required,LoginManager,UserMixin 8 | from flask import Blueprint,session 9 | 10 | markuppage_api = Blueprint('markuppage_api', __name__) 11 | 12 | f=open(os.path.join(os.path.dirname(os.path.realpath(__file__)),'config.json'),'r+') 13 | config=json.load(f) 14 | f.close() 15 | 16 | @markuppage_api.route('/markup/') 17 | @login_required 18 | def markup(application): 19 | session['url'] = '/markup/'+application 20 | 21 | logging.info('Visited /markup/'+application) 22 | if application in config['applications']: 23 | width=config[application]['width'] 24 | height=config[application]['height'] 25 | return render_template(application+'.html',width=width,height=height,application=application,review=False,log_in_or_out="out") 26 | else: 27 | return "no such application" 28 | 29 | @markuppage_api.route('/markup//set_resize', methods=['POST']) 30 | @login_required 31 | def set_resize(application): 32 | value=request.args.get('resize') 33 | if not value is None: 34 | session[application+':resize']=value 35 | 36 | else: 37 | session[application+':resize']='True' 38 | return "ok" 39 | @markuppage_api.route('/markup//get_resize', methods=['GET']) 40 | def get_resize(application): 41 | value=session.get(application+':resize') 42 | if not value is None: 43 | return value 44 | else: 45 | return 'True' 46 | -------------------------------------------------------------------------------- /app/requirements.txt: -------------------------------------------------------------------------------- 1 | flask==1.0.3 2 | werkzeug==0.16.1 3 | email-validator 4 | flask_wtf==0.14.2 5 | flask_login==0.4.1 6 | requests==2.18.4 7 | boto3==1.9.159 8 | pymongo[tls,srv]==3.8.0 9 | passlib==1.7.1 10 | Flask-Bootstrap==3.3.7.1 11 | -------------------------------------------------------------------------------- /app/review.py: -------------------------------------------------------------------------------- 1 | from flask import render_template 2 | from flask import send_from_directory, send_file,url_for,redirect 3 | from flask import request 4 | import json 5 | import logging 6 | from flask_login import login_user, logout_user, current_user, login_required,LoginManager,UserMixin 7 | from flask import Blueprint,session 8 | import db 9 | import os 10 | import boto3 11 | import botocore 12 | 13 | 14 | s3 = boto3.resource('s3',aws_access_key_id=os.environ['AWS_KEY_ID'], 15 | aws_secret_access_key=os.environ['AWS_KEY']) 16 | s3_client = boto3.client('s3',aws_access_key_id=os.environ['AWS_KEY_ID'], 17 | aws_secret_access_key=os.environ['AWS_KEY']) 18 | 19 | 20 | review_api = Blueprint('review_api', __name__) 21 | 22 | f=open(os.path.join(os.path.dirname(os.path.realpath(__file__)),'config.json'),'r+') 23 | config=json.load(f) 24 | f.close() 25 | 26 | 27 | @review_api.route('/review/', methods=['GET','POST']) 28 | @login_required 29 | def review_get(application): 30 | logging.info('Visited review') 31 | user=db.get_user(current_user.id) 32 | if user['role']!='admin': 33 | return 'You should be an admin' 34 | 35 | names,ids=db.get_all_users() 36 | ids.insert(0,'all users') 37 | 38 | if request.method == 'POST': 39 | session['user_to_review']=request.form['select_user'] 40 | 41 | files=[] 42 | if session.get('user_to_review') is not None: 43 | cur_user=session['user_to_review'] 44 | if not cur_user == 'all users': 45 | files_=db.get_files(application,config[application]['output'],cur_user) 46 | else: 47 | files_=db.get_files(application,config[application]['output'],ext='.json') 48 | 49 | for i in range(len(files_)): 50 | files.append(files_[i]['filename']) 51 | 52 | else: 53 | cur_user='user to review was not chosen' 54 | 55 | 56 | return render_template('review_choose_user.html',users=ids,application=application,cur_user=cur_user,files=files,output=config[application]['output'],log_in_or_out='out') 57 | 58 | @review_api.route('/review/del_sample///', methods=['GET','POST']) 59 | @login_required 60 | def del_sample(application,folder,filename): 61 | logging.info('Visited del sample '+application+' '+folder+' '+filename) 62 | user=db.get_user(current_user.id) 63 | 64 | if user['role']!='admin': 65 | return 'You should be an admin' 66 | 67 | bucket_name = config[application]['bucket'] 68 | fn_1=filename+'.json' 69 | fn_2=filename+'.'+config[application]["image_ext"] 70 | 71 | 72 | db.delete_file(application, folder, fn_1) 73 | db.delete_file(application, folder, fn_2) 74 | 75 | 76 | try: 77 | s3.Object(bucket_name,folder+'/'+fn_1).delete() 78 | except botocore.exceptions.ClientError as e: 79 | logging.error('delete error: '+bucket_name+' '+ folder+'/'+fn_1) 80 | try: 81 | s3.Object(bucket_name,folder+'/'+fn_2).delete() 82 | except botocore.exceptions.ClientError as e: 83 | logging.error('delete error: ' + bucket_name + ' ' + folder + '/' + fn_2) 84 | 85 | return redirect('/review/'+application) 86 | 87 | @review_api.route('/review/del_all_samples_for_user//', methods=['GET','POST']) 88 | @login_required 89 | def del_all_samples(application,folder): 90 | logging.info('Visited del all sample for user '+application+' '+folder) 91 | user=db.get_user(current_user.id) 92 | 93 | if user['role']!='admin': 94 | return 'You should be an admin' 95 | cur_user=session['user_to_review'] 96 | 97 | 98 | bucket_name = config[application]['bucket'] 99 | 100 | files=db.get_files(application,folder,cur_user) 101 | for i in range(len(files)): 102 | filename=files[i]['filename'] 103 | fn_1=filename 104 | fn_2=filename.replace('.json','.'+config[application]["image_ext"]) 105 | #print(fn_1,fn_2) 106 | 107 | try: 108 | s3.Object(bucket_name,folder+'/'+fn_1).delete() 109 | db.delete_file(application,folder,fn_1) 110 | s3.Object(bucket_name,folder+'/'+fn_2).delete() 111 | db.delete_file(application,folder,fn_2) 112 | 113 | except botocore.exceptions.ClientError as e: 114 | logging.error(bucket_name+' '+folder+' '+ fn_1+' '+fn_2+' were not deleted') 115 | 116 | return redirect('/review/'+application) 117 | 118 | 119 | 120 | @review_api.route('/review///', methods=['GET','POST']) 121 | @login_required 122 | def review_file(application,folder,filename): 123 | logging.info('Visited review') 124 | user=db.get_user(current_user.id) 125 | width=config[application]['width'] 126 | height=config[application]['height'] 127 | return render_template(application+'.html',width=width,height=height,application=application,review=True, 128 | filename=filename,folder=folder,image_ext=config[application]['image_ext']) 129 | 130 | 131 | 132 | @review_api.route('/files///', methods=['GET']) 133 | @login_required 134 | def files(application,folder,filename): 135 | if application in config['applications']: 136 | logging.info('Returning image with application = {}, folder={}, filename = {}'.format(application,folder, filename)) 137 | bucket_name = config[application]['bucket'] 138 | file_key = folder+"/" + filename 139 | logging.info('Use bucket_name = {}, and file_key = {}'.format(bucket_name, file_key)) 140 | try: 141 | response = s3_client.get_object(Bucket=bucket_name, Key=file_key) 142 | except botocore.exceptions.ClientError as e: 143 | db.delete_file(application,folder,filename) 144 | return None 145 | data = response['Body'] 146 | return send_file(data, attachment_filename=filename) 147 | return None 148 | 149 | -------------------------------------------------------------------------------- /app/static/blank-img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VasilyMorzhakov/markup_server/c0903fb26e865c5f6dae01986aafb54f83bdf20d/app/static/blank-img.jpg -------------------------------------------------------------------------------- /app/static/css/main.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Raleway'); 2 | 3 | body{ 4 | padding: 0; 5 | margin: 0; 6 | background:#f4f4f4; 7 | overflow-x: auto; 8 | overflow-y: auto; 9 | } 10 | html,h1,h2,h3,h4,h5,h6,a{ 11 | font-family: "Raleway"; 12 | } 13 | .navbar{ 14 | background:#7300F9; 15 | } 16 | .nav-link , .navbar-brand{ 17 | color: #f4f4f4; 18 | cursor: pointer; 19 | } 20 | .nav-link{ 21 | margin-right: 1em !important; 22 | } 23 | .nav-link:hover{ 24 | color: #73f9f9; 25 | } 26 | .navbar-collapse{ 27 | justify-content: flex-end; 28 | } 29 | .description{ 30 | margin-top: 66px; 31 | margin-left: auto; 32 | margin-right:auto; 33 | width: 600px; 34 | padding: 2em; 35 | 36 | } 37 | 38 | .description h4{ 39 | color:#7300F9 ; 40 | } 41 | .description h2{ 42 | color:#7300F9 ; 43 | } 44 | 45 | .description p{ 46 | color:#666; 47 | font-size: 14px; 48 | width: 50%; 49 | line-height: 1.5; 50 | } 51 | .description button{ 52 | border:1px solid #7300F9; 53 | background:#7300F9; 54 | color:#fff; 55 | } 56 | .files input { 57 | outline: 2px dashed #92b0b3; 58 | outline-offset: -10px; 59 | -webkit-transition: outline-offset .15s ease-in-out, background-color .15s linear; 60 | transition: outline-offset .15s ease-in-out, background-color .15s linear; 61 | padding: 120px 0px 85px 35%; 62 | text-align: center !important; 63 | margin: 0; 64 | width: 100% !important; 65 | } 66 | .files input:focus{ outline: 2px dashed #92b0b3; outline-offset: -10px; 67 | -webkit-transition: outline-offset .15s ease-in-out, background-color .15s linear; 68 | transition: outline-offset .15s ease-in-out, background-color .15s linear; border:1px solid #92b0b3; 69 | } 70 | .files{ position:relative} 71 | .files:after { pointer-events: none; 72 | position: absolute; 73 | top: 60px; 74 | left: 0; 75 | width: 50px; 76 | right: 0; 77 | height: 56px; 78 | content: ""; 79 | background-image: url(https://image.flaticon.com/icons/png/128/109/109612.png); 80 | display: block; 81 | margin: 0 auto; 82 | background-size: 100%; 83 | background-repeat: no-repeat; 84 | } 85 | .color input{ background-color:#f1f1f1;} 86 | .files:before { 87 | position: absolute; 88 | bottom: 10px; 89 | left: 0; pointer-events: none; 90 | width: 100%; 91 | right: 0; 92 | height: 57px; 93 | content: " or drag and drop it here. "; 94 | display: block; 95 | margin: 0 auto; 96 | color: #2ea591; 97 | font-weight: 600; 98 | text-transform: capitalize; 99 | text-align: center; 100 | } 101 | .input-table, .review-table { 102 | text-align: center; 103 | border-color: grey; 104 | } 105 | .review-table { 106 | width: 100%; 107 | } 108 | .review-table tbody { 109 | height: 220px; 110 | overflow-y: auto; 111 | width: 100%; 112 | } 113 | .review-table thead, .review-table tbody, .review-table tr, .review-table td, .review-table th { 114 | display: block; 115 | } 116 | .review-table tbody td { 117 | float: left; 118 | } 119 | .review-table thead tr th { 120 | float: left; 121 | background-color: #66ccff; 122 | } 123 | .review-table > thead >tr { 124 | padding-right: 17px; 125 | } 126 | -------------------------------------------------------------------------------- /app/static/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v4.0.0-beta.2 (https://getbootstrap.com) 3 | * Copyright 2011-2017 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | var bootstrap=function(t,e,n){"use strict";function i(t,e){for(var n=0;n0?n:null}catch(t){return null}},reflow:function(t){return t.offsetHeight},triggerTransitionEnd:function(t){e(t).trigger(r.end)},supportsTransitionEnd:function(){return Boolean(r)},isElement:function(t){return(t[0]||t).nodeType},typeCheckConfig:function(e,n,i){for(var s in i)if(Object.prototype.hasOwnProperty.call(i,s)){var r=i[s],o=n[s],l=o&&a.isElement(o)?"element":t(o);if(!new RegExp(r).test(l))throw new Error(e.toUpperCase()+': Option "'+s+'" provided type "'+l+'" but expected type "'+r+'".')}}};return r=i(),e.fn.emulateTransitionEnd=s,a.supportsTransitionEnd()&&(e.event.special[a.TRANSITION_END]=n()),a}(),r=function(t,e,n){return e&&i(t.prototype,e),n&&i(t,n),t},o=function(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e},a=function(){var t="alert",n=e.fn[t],i={CLOSE:"close.bs.alert",CLOSED:"closed.bs.alert",CLICK_DATA_API:"click.bs.alert.data-api"},o={ALERT:"alert",FADE:"fade",SHOW:"show"},a=function(){function t(t){this._element=t}var n=t.prototype;return n.close=function(t){t=t||this._element;var e=this._getRootElement(t);this._triggerCloseEvent(e).isDefaultPrevented()||this._removeElement(e)},n.dispose=function(){e.removeData(this._element,"bs.alert"),this._element=null},n._getRootElement=function(t){var n=s.getSelectorFromElement(t),i=!1;return n&&(i=e(n)[0]),i||(i=e(t).closest("."+o.ALERT)[0]),i},n._triggerCloseEvent=function(t){var n=e.Event(i.CLOSE);return e(t).trigger(n),n},n._removeElement=function(t){var n=this;e(t).removeClass(o.SHOW),s.supportsTransitionEnd()&&e(t).hasClass(o.FADE)?e(t).one(s.TRANSITION_END,function(e){return n._destroyElement(t,e)}).emulateTransitionEnd(150):this._destroyElement(t)},n._destroyElement=function(t){e(t).detach().trigger(i.CLOSED).remove()},t._jQueryInterface=function(n){return this.each(function(){var i=e(this),s=i.data("bs.alert");s||(s=new t(this),i.data("bs.alert",s)),"close"===n&&s[n](this)})},t._handleDismiss=function(t){return function(e){e&&e.preventDefault(),t.close(this)}},r(t,null,[{key:"VERSION",get:function(){return"4.0.0-beta.2"}}]),t}();return e(document).on(i.CLICK_DATA_API,{DISMISS:'[data-dismiss="alert"]'}.DISMISS,a._handleDismiss(new a)),e.fn[t]=a._jQueryInterface,e.fn[t].Constructor=a,e.fn[t].noConflict=function(){return e.fn[t]=n,a._jQueryInterface},a}(),l=function(){var t="button",n=e.fn[t],i={ACTIVE:"active",BUTTON:"btn",FOCUS:"focus"},s={DATA_TOGGLE_CARROT:'[data-toggle^="button"]',DATA_TOGGLE:'[data-toggle="buttons"]',INPUT:"input",ACTIVE:".active",BUTTON:".btn"},o={CLICK_DATA_API:"click.bs.button.data-api",FOCUS_BLUR_DATA_API:"focus.bs.button.data-api blur.bs.button.data-api"},a=function(){function t(t){this._element=t}var n=t.prototype;return n.toggle=function(){var t=!0,n=!0,r=e(this._element).closest(s.DATA_TOGGLE)[0];if(r){var o=e(this._element).find(s.INPUT)[0];if(o){if("radio"===o.type)if(o.checked&&e(this._element).hasClass(i.ACTIVE))t=!1;else{var a=e(r).find(s.ACTIVE)[0];a&&e(a).removeClass(i.ACTIVE)}if(t){if(o.hasAttribute("disabled")||r.hasAttribute("disabled")||o.classList.contains("disabled")||r.classList.contains("disabled"))return;o.checked=!e(this._element).hasClass(i.ACTIVE),e(o).trigger("change")}o.focus(),n=!1}}n&&this._element.setAttribute("aria-pressed",!e(this._element).hasClass(i.ACTIVE)),t&&e(this._element).toggleClass(i.ACTIVE)},n.dispose=function(){e.removeData(this._element,"bs.button"),this._element=null},t._jQueryInterface=function(n){return this.each(function(){var i=e(this).data("bs.button");i||(i=new t(this),e(this).data("bs.button",i)),"toggle"===n&&i[n]()})},r(t,null,[{key:"VERSION",get:function(){return"4.0.0-beta.2"}}]),t}();return e(document).on(o.CLICK_DATA_API,s.DATA_TOGGLE_CARROT,function(t){t.preventDefault();var n=t.target;e(n).hasClass(i.BUTTON)||(n=e(n).closest(s.BUTTON)),a._jQueryInterface.call(e(n),"toggle")}).on(o.FOCUS_BLUR_DATA_API,s.DATA_TOGGLE_CARROT,function(t){var n=e(t.target).closest(s.BUTTON)[0];e(n).toggleClass(i.FOCUS,/^focus(in)?$/.test(t.type))}),e.fn[t]=a._jQueryInterface,e.fn[t].Constructor=a,e.fn[t].noConflict=function(){return e.fn[t]=n,a._jQueryInterface},a}(),h=function(){var t="carousel",n="bs.carousel",i="."+n,o=e.fn[t],a={interval:5e3,keyboard:!0,slide:!1,pause:"hover",wrap:!0},l={interval:"(number|boolean)",keyboard:"boolean",slide:"(boolean|string)",pause:"(string|boolean)",wrap:"boolean"},h={NEXT:"next",PREV:"prev",LEFT:"left",RIGHT:"right"},c={SLIDE:"slide"+i,SLID:"slid"+i,KEYDOWN:"keydown"+i,MOUSEENTER:"mouseenter"+i,MOUSELEAVE:"mouseleave"+i,TOUCHEND:"touchend"+i,LOAD_DATA_API:"load.bs.carousel.data-api",CLICK_DATA_API:"click.bs.carousel.data-api"},u={CAROUSEL:"carousel",ACTIVE:"active",SLIDE:"slide",RIGHT:"carousel-item-right",LEFT:"carousel-item-left",NEXT:"carousel-item-next",PREV:"carousel-item-prev",ITEM:"carousel-item"},d={ACTIVE:".active",ACTIVE_ITEM:".active.carousel-item",ITEM:".carousel-item",NEXT_PREV:".carousel-item-next, .carousel-item-prev",INDICATORS:".carousel-indicators",DATA_SLIDE:"[data-slide], [data-slide-to]",DATA_RIDE:'[data-ride="carousel"]'},f=function(){function o(t,n){this._items=null,this._interval=null,this._activeElement=null,this._isPaused=!1,this._isSliding=!1,this.touchTimeout=null,this._config=this._getConfig(n),this._element=e(t)[0],this._indicatorsElement=e(this._element).find(d.INDICATORS)[0],this._addEventListeners()}var f=o.prototype;return f.next=function(){this._isSliding||this._slide(h.NEXT)},f.nextWhenVisible=function(){!document.hidden&&e(this._element).is(":visible")&&"hidden"!==e(this._element).css("visibility")&&this.next()},f.prev=function(){this._isSliding||this._slide(h.PREV)},f.pause=function(t){t||(this._isPaused=!0),e(this._element).find(d.NEXT_PREV)[0]&&s.supportsTransitionEnd()&&(s.triggerTransitionEnd(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null},f.cycle=function(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config.interval&&!this._isPaused&&(this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))},f.to=function(t){var n=this;this._activeElement=e(this._element).find(d.ACTIVE_ITEM)[0];var i=this._getItemIndex(this._activeElement);if(!(t>this._items.length-1||t<0))if(this._isSliding)e(this._element).one(c.SLID,function(){return n.to(t)});else{if(i===t)return this.pause(),void this.cycle();var s=t>i?h.NEXT:h.PREV;this._slide(s,this._items[t])}},f.dispose=function(){e(this._element).off(i),e.removeData(this._element,n),this._items=null,this._config=null,this._element=null,this._interval=null,this._isPaused=null,this._isSliding=null,this._activeElement=null,this._indicatorsElement=null},f._getConfig=function(n){return n=e.extend({},a,n),s.typeCheckConfig(t,n,l),n},f._addEventListeners=function(){var t=this;this._config.keyboard&&e(this._element).on(c.KEYDOWN,function(e){return t._keydown(e)}),"hover"===this._config.pause&&(e(this._element).on(c.MOUSEENTER,function(e){return t.pause(e)}).on(c.MOUSELEAVE,function(e){return t.cycle(e)}),"ontouchstart"in document.documentElement&&e(this._element).on(c.TOUCHEND,function(){t.pause(),t.touchTimeout&&clearTimeout(t.touchTimeout),t.touchTimeout=setTimeout(function(e){return t.cycle(e)},500+t._config.interval)}))},f._keydown=function(t){if(!/input|textarea/i.test(t.target.tagName))switch(t.which){case 37:t.preventDefault(),this.prev();break;case 39:t.preventDefault(),this.next();break;default:return}},f._getItemIndex=function(t){return this._items=e.makeArray(e(t).parent().find(d.ITEM)),this._items.indexOf(t)},f._getItemByDirection=function(t,e){var n=t===h.NEXT,i=t===h.PREV,s=this._getItemIndex(e),r=this._items.length-1;if((i&&0===s||n&&s===r)&&!this._config.wrap)return e;var o=(s+(t===h.PREV?-1:1))%this._items.length;return-1===o?this._items[this._items.length-1]:this._items[o]},f._triggerSlideEvent=function(t,n){var i=this._getItemIndex(t),s=this._getItemIndex(e(this._element).find(d.ACTIVE_ITEM)[0]),r=e.Event(c.SLIDE,{relatedTarget:t,direction:n,from:s,to:i});return e(this._element).trigger(r),r},f._setActiveIndicatorElement=function(t){if(this._indicatorsElement){e(this._indicatorsElement).find(d.ACTIVE).removeClass(u.ACTIVE);var n=this._indicatorsElement.children[this._getItemIndex(t)];n&&e(n).addClass(u.ACTIVE)}},f._slide=function(t,n){var i,r,o,a=this,l=e(this._element).find(d.ACTIVE_ITEM)[0],f=this._getItemIndex(l),_=n||l&&this._getItemByDirection(t,l),g=this._getItemIndex(_),m=Boolean(this._interval);if(t===h.NEXT?(i=u.LEFT,r=u.NEXT,o=h.LEFT):(i=u.RIGHT,r=u.PREV,o=h.RIGHT),_&&e(_).hasClass(u.ACTIVE))this._isSliding=!1;else if(!this._triggerSlideEvent(_,o).isDefaultPrevented()&&l&&_){this._isSliding=!0,m&&this.pause(),this._setActiveIndicatorElement(_);var p=e.Event(c.SLID,{relatedTarget:_,direction:o,from:f,to:g});s.supportsTransitionEnd()&&e(this._element).hasClass(u.SLIDE)?(e(_).addClass(r),s.reflow(_),e(l).addClass(i),e(_).addClass(i),e(l).one(s.TRANSITION_END,function(){e(_).removeClass(i+" "+r).addClass(u.ACTIVE),e(l).removeClass(u.ACTIVE+" "+r+" "+i),a._isSliding=!1,setTimeout(function(){return e(a._element).trigger(p)},0)}).emulateTransitionEnd(600)):(e(l).removeClass(u.ACTIVE),e(_).addClass(u.ACTIVE),this._isSliding=!1,e(this._element).trigger(p)),m&&this.cycle()}},o._jQueryInterface=function(t){return this.each(function(){var i=e(this).data(n),s=e.extend({},a,e(this).data());"object"==typeof t&&e.extend(s,t);var r="string"==typeof t?t:s.slide;if(i||(i=new o(this,s),e(this).data(n,i)),"number"==typeof t)i.to(t);else if("string"==typeof r){if("undefined"==typeof i[r])throw new Error('No method named "'+r+'"');i[r]()}else s.interval&&(i.pause(),i.cycle())})},o._dataApiClickHandler=function(t){var i=s.getSelectorFromElement(this);if(i){var r=e(i)[0];if(r&&e(r).hasClass(u.CAROUSEL)){var a=e.extend({},e(r).data(),e(this).data()),l=this.getAttribute("data-slide-to");l&&(a.interval=!1),o._jQueryInterface.call(e(r),a),l&&e(r).data(n).to(l),t.preventDefault()}}},r(o,null,[{key:"VERSION",get:function(){return"4.0.0-beta.2"}},{key:"Default",get:function(){return a}}]),o}();return e(document).on(c.CLICK_DATA_API,d.DATA_SLIDE,f._dataApiClickHandler),e(window).on(c.LOAD_DATA_API,function(){e(d.DATA_RIDE).each(function(){var t=e(this);f._jQueryInterface.call(t,t.data())})}),e.fn[t]=f._jQueryInterface,e.fn[t].Constructor=f,e.fn[t].noConflict=function(){return e.fn[t]=o,f._jQueryInterface},f}(),c=function(){var t="collapse",n="bs.collapse",i=e.fn[t],o={toggle:!0,parent:""},a={toggle:"boolean",parent:"(string|element)"},l={SHOW:"show.bs.collapse",SHOWN:"shown.bs.collapse",HIDE:"hide.bs.collapse",HIDDEN:"hidden.bs.collapse",CLICK_DATA_API:"click.bs.collapse.data-api"},h={SHOW:"show",COLLAPSE:"collapse",COLLAPSING:"collapsing",COLLAPSED:"collapsed"},c={WIDTH:"width",HEIGHT:"height"},u={ACTIVES:".show, .collapsing",DATA_TOGGLE:'[data-toggle="collapse"]'},d=function(){function i(t,n){this._isTransitioning=!1,this._element=t,this._config=this._getConfig(n),this._triggerArray=e.makeArray(e('[data-toggle="collapse"][href="#'+t.id+'"],[data-toggle="collapse"][data-target="#'+t.id+'"]'));for(var i=e(u.DATA_TOGGLE),r=0;r0&&this._triggerArray.push(o)}this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}var d=i.prototype;return d.toggle=function(){e(this._element).hasClass(h.SHOW)?this.hide():this.show()},d.show=function(){var t=this;if(!this._isTransitioning&&!e(this._element).hasClass(h.SHOW)){var r,o;if(this._parent&&((r=e.makeArray(e(this._parent).children().children(u.ACTIVES))).length||(r=null)),!(r&&(o=e(r).data(n))&&o._isTransitioning)){var a=e.Event(l.SHOW);if(e(this._element).trigger(a),!a.isDefaultPrevented()){r&&(i._jQueryInterface.call(e(r),"hide"),o||e(r).data(n,null));var c=this._getDimension();e(this._element).removeClass(h.COLLAPSE).addClass(h.COLLAPSING),this._element.style[c]=0,this._triggerArray.length&&e(this._triggerArray).removeClass(h.COLLAPSED).attr("aria-expanded",!0),this.setTransitioning(!0);var d=function(){e(t._element).removeClass(h.COLLAPSING).addClass(h.COLLAPSE).addClass(h.SHOW),t._element.style[c]="",t.setTransitioning(!1),e(t._element).trigger(l.SHOWN)};if(s.supportsTransitionEnd()){var f="scroll"+(c[0].toUpperCase()+c.slice(1));e(this._element).one(s.TRANSITION_END,d).emulateTransitionEnd(600),this._element.style[c]=this._element[f]+"px"}else d()}}}},d.hide=function(){var t=this;if(!this._isTransitioning&&e(this._element).hasClass(h.SHOW)){var n=e.Event(l.HIDE);if(e(this._element).trigger(n),!n.isDefaultPrevented()){var i=this._getDimension();if(this._element.style[i]=this._element.getBoundingClientRect()[i]+"px",s.reflow(this._element),e(this._element).addClass(h.COLLAPSING).removeClass(h.COLLAPSE).removeClass(h.SHOW),this._triggerArray.length)for(var r=0;r0},g._getPopperConfig=function(){var t=this,n={};"function"==typeof this._config.offset?n.fn=function(n){return n.offsets=e.extend({},n.offsets,t._config.offset(n.offsets)||{}),n}:n.offset=this._config.offset;var i={placement:this._getPlacement(),modifiers:{offset:n,flip:{enabled:this._config.flip}}};return this._inNavbar&&(i.modifiers.applyStyle={enabled:!this._inNavbar}),i},a._jQueryInterface=function(t){return this.each(function(){var n=e(this).data(i),s="object"==typeof t?t:null;if(n||(n=new a(this,s),e(this).data(i,n)),"string"==typeof t){if("undefined"==typeof n[t])throw new Error('No method named "'+t+'"');n[t]()}})},a._clearMenus=function(t){if(!t||3!==t.which&&("keyup"!==t.type||9===t.which))for(var n=e.makeArray(e(u.DATA_TOGGLE)),s=0;s0&&r--,40===t.which&&rdocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},u._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},u._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip"},u={SHOW:"show",OUT:"out"},d={HIDE:"hide"+i,HIDDEN:"hidden"+i,SHOW:"show"+i,SHOWN:"shown"+i,INSERTED:"inserted"+i,CLICK:"click"+i,FOCUSIN:"focusin"+i,FOCUSOUT:"focusout"+i,MOUSEENTER:"mouseenter"+i,MOUSELEAVE:"mouseleave"+i},f={FADE:"fade",SHOW:"show"},_={TOOLTIP:".tooltip",TOOLTIP_INNER:".tooltip-inner",ARROW:".arrow"},g={HOVER:"hover",FOCUS:"focus",CLICK:"click",MANUAL:"manual"},m=function(){function o(t,e){this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var m=o.prototype;return m.enable=function(){this._isEnabled=!0},m.disable=function(){this._isEnabled=!1},m.toggleEnabled=function(){this._isEnabled=!this._isEnabled},m.toggle=function(t){if(this._isEnabled)if(t){var n=this.constructor.DATA_KEY,i=e(t.currentTarget).data(n);i||(i=new this.constructor(t.currentTarget,this._getDelegateConfig()),e(t.currentTarget).data(n,i)),i._activeTrigger.click=!i._activeTrigger.click,i._isWithActiveTrigger()?i._enter(null,i):i._leave(null,i)}else{if(e(this.getTipElement()).hasClass(f.SHOW))return void this._leave(null,this);this._enter(null,this)}},m.dispose=function(){clearTimeout(this._timeout),e.removeData(this.element,this.constructor.DATA_KEY),e(this.element).off(this.constructor.EVENT_KEY),e(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&e(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,null!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},m.show=function(){var t=this;if("none"===e(this.element).css("display"))throw new Error("Please use show on visible elements");var i=e.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){e(this.element).trigger(i);var r=e.contains(this.element.ownerDocument.documentElement,this.element);if(i.isDefaultPrevented()||!r)return;var a=this.getTipElement(),l=s.getUID(this.constructor.NAME);a.setAttribute("id",l),this.element.setAttribute("aria-describedby",l),this.setContent(),this.config.animation&&e(a).addClass(f.FADE);var h="function"==typeof this.config.placement?this.config.placement.call(this,a,this.element):this.config.placement,c=this._getAttachment(h);this.addAttachmentClass(c);var d=!1===this.config.container?document.body:e(this.config.container);e(a).data(this.constructor.DATA_KEY,this),e.contains(this.element.ownerDocument.documentElement,this.tip)||e(a).appendTo(d),e(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new n(this.element,a,{placement:c,modifiers:{offset:{offset:this.config.offset},flip:{behavior:this.config.fallbackPlacement},arrow:{element:_.ARROW}},onCreate:function(e){e.originalPlacement!==e.placement&&t._handlePopperPlacementChange(e)},onUpdate:function(e){t._handlePopperPlacementChange(e)}}),e(a).addClass(f.SHOW),"ontouchstart"in document.documentElement&&e("body").children().on("mouseover",null,e.noop);var g=function(){t.config.animation&&t._fixTransition();var n=t._hoverState;t._hoverState=null,e(t.element).trigger(t.constructor.Event.SHOWN),n===u.OUT&&t._leave(null,t)};s.supportsTransitionEnd()&&e(this.tip).hasClass(f.FADE)?e(this.tip).one(s.TRANSITION_END,g).emulateTransitionEnd(o._TRANSITION_DURATION):g()}},m.hide=function(t){var n=this,i=this.getTipElement(),r=e.Event(this.constructor.Event.HIDE),o=function(){n._hoverState!==u.SHOW&&i.parentNode&&i.parentNode.removeChild(i),n._cleanTipClass(),n.element.removeAttribute("aria-describedby"),e(n.element).trigger(n.constructor.Event.HIDDEN),null!==n._popper&&n._popper.destroy(),t&&t()};e(this.element).trigger(r),r.isDefaultPrevented()||(e(i).removeClass(f.SHOW),"ontouchstart"in document.documentElement&&e("body").children().off("mouseover",null,e.noop),this._activeTrigger[g.CLICK]=!1,this._activeTrigger[g.FOCUS]=!1,this._activeTrigger[g.HOVER]=!1,s.supportsTransitionEnd()&&e(this.tip).hasClass(f.FADE)?e(i).one(s.TRANSITION_END,o).emulateTransitionEnd(150):o(),this._hoverState="")},m.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},m.isWithContent=function(){return Boolean(this.getTitle())},m.addAttachmentClass=function(t){e(this.getTipElement()).addClass("bs-tooltip-"+t)},m.getTipElement=function(){return this.tip=this.tip||e(this.config.template)[0],this.tip},m.setContent=function(){var t=e(this.getTipElement());this.setElementContent(t.find(_.TOOLTIP_INNER),this.getTitle()),t.removeClass(f.FADE+" "+f.SHOW)},m.setElementContent=function(t,n){var i=this.config.html;"object"==typeof n&&(n.nodeType||n.jquery)?i?e(n).parent().is(t)||t.empty().append(n):t.text(e(n).text()):t[i?"html":"text"](n)},m.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},m._getAttachment=function(t){return h[t.toUpperCase()]},m._setListeners=function(){var t=this;this.config.trigger.split(" ").forEach(function(n){if("click"===n)e(t.element).on(t.constructor.Event.CLICK,t.config.selector,function(e){return t.toggle(e)});else if(n!==g.MANUAL){var i=n===g.HOVER?t.constructor.Event.MOUSEENTER:t.constructor.Event.FOCUSIN,s=n===g.HOVER?t.constructor.Event.MOUSELEAVE:t.constructor.Event.FOCUSOUT;e(t.element).on(i,t.config.selector,function(e){return t._enter(e)}).on(s,t.config.selector,function(e){return t._leave(e)})}e(t.element).closest(".modal").on("hide.bs.modal",function(){return t.hide()})}),this.config.selector?this.config=e.extend({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},m._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},m._enter=function(t,n){var i=this.constructor.DATA_KEY;(n=n||e(t.currentTarget).data(i))||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),e(t.currentTarget).data(i,n)),t&&(n._activeTrigger["focusin"===t.type?g.FOCUS:g.HOVER]=!0),e(n.getTipElement()).hasClass(f.SHOW)||n._hoverState===u.SHOW?n._hoverState=u.SHOW:(clearTimeout(n._timeout),n._hoverState=u.SHOW,n.config.delay&&n.config.delay.show?n._timeout=setTimeout(function(){n._hoverState===u.SHOW&&n.show()},n.config.delay.show):n.show())},m._leave=function(t,n){var i=this.constructor.DATA_KEY;(n=n||e(t.currentTarget).data(i))||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),e(t.currentTarget).data(i,n)),t&&(n._activeTrigger["focusout"===t.type?g.FOCUS:g.HOVER]=!1),n._isWithActiveTrigger()||(clearTimeout(n._timeout),n._hoverState=u.OUT,n.config.delay&&n.config.delay.hide?n._timeout=setTimeout(function(){n._hoverState===u.OUT&&n.hide()},n.config.delay.hide):n.hide())},m._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},m._getConfig=function(n){return"number"==typeof(n=e.extend({},this.constructor.Default,e(this.element).data(),n)).delay&&(n.delay={show:n.delay,hide:n.delay}),"number"==typeof n.title&&(n.title=n.title.toString()),"number"==typeof n.content&&(n.content=n.content.toString()),s.typeCheckConfig(t,n,this.constructor.DefaultType),n},m._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},m._cleanTipClass=function(){var t=e(this.getTipElement()),n=t.attr("class").match(a);null!==n&&n.length>0&&t.removeClass(n.join(""))},m._handlePopperPlacementChange=function(t){this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},m._fixTransition=function(){var t=this.getTipElement(),n=this.config.animation;null===t.getAttribute("x-placement")&&(e(t).removeClass(f.FADE),this.config.animation=!1,this.hide(),this.show(),this.config.animation=n)},o._jQueryInterface=function(t){return this.each(function(){var n=e(this).data("bs.tooltip"),i="object"==typeof t&&t;if((n||!/dispose|hide/.test(t))&&(n||(n=new o(this,i),e(this).data("bs.tooltip",n)),"string"==typeof t)){if("undefined"==typeof n[t])throw new Error('No method named "'+t+'"');n[t]()}})},r(o,null,[{key:"VERSION",get:function(){return"4.0.0-beta.2"}},{key:"Default",get:function(){return c}},{key:"NAME",get:function(){return t}},{key:"DATA_KEY",get:function(){return"bs.tooltip"}},{key:"Event",get:function(){return d}},{key:"EVENT_KEY",get:function(){return i}},{key:"DefaultType",get:function(){return l}}]),o}();return e.fn[t]=m._jQueryInterface,e.fn[t].Constructor=m,e.fn[t].noConflict=function(){return e.fn[t]=o,m._jQueryInterface},m}(),_=function(){var t="popover",n=".bs.popover",i=e.fn[t],s=new RegExp("(^|\\s)bs-popover\\S+","g"),a=e.extend({},f.Default,{placement:"right",trigger:"click",content:"",template:''}),l=e.extend({},f.DefaultType,{content:"(string|element|function)"}),h={FADE:"fade",SHOW:"show"},c={TITLE:".popover-header",CONTENT:".popover-body"},u={HIDE:"hide"+n,HIDDEN:"hidden"+n,SHOW:"show"+n,SHOWN:"shown"+n,INSERTED:"inserted"+n,CLICK:"click"+n,FOCUSIN:"focusin"+n,FOCUSOUT:"focusout"+n,MOUSEENTER:"mouseenter"+n,MOUSELEAVE:"mouseleave"+n},d=function(i){function d(){return i.apply(this,arguments)||this}o(d,i);var f=d.prototype;return f.isWithContent=function(){return this.getTitle()||this._getContent()},f.addAttachmentClass=function(t){e(this.getTipElement()).addClass("bs-popover-"+t)},f.getTipElement=function(){return this.tip=this.tip||e(this.config.template)[0],this.tip},f.setContent=function(){var t=e(this.getTipElement());this.setElementContent(t.find(c.TITLE),this.getTitle()),this.setElementContent(t.find(c.CONTENT),this._getContent()),t.removeClass(h.FADE+" "+h.SHOW)},f._getContent=function(){return this.element.getAttribute("data-content")||("function"==typeof this.config.content?this.config.content.call(this.element):this.config.content)},f._cleanTipClass=function(){var t=e(this.getTipElement()),n=t.attr("class").match(s);null!==n&&n.length>0&&t.removeClass(n.join(""))},d._jQueryInterface=function(t){return this.each(function(){var n=e(this).data("bs.popover"),i="object"==typeof t?t:null;if((n||!/destroy|hide/.test(t))&&(n||(n=new d(this,i),e(this).data("bs.popover",n)),"string"==typeof t)){if("undefined"==typeof n[t])throw new Error('No method named "'+t+'"');n[t]()}})},r(d,null,[{key:"VERSION",get:function(){return"4.0.0-beta.2"}},{key:"Default",get:function(){return a}},{key:"NAME",get:function(){return t}},{key:"DATA_KEY",get:function(){return"bs.popover"}},{key:"Event",get:function(){return u}},{key:"EVENT_KEY",get:function(){return n}},{key:"DefaultType",get:function(){return l}}]),d}(f);return e.fn[t]=d._jQueryInterface,e.fn[t].Constructor=d,e.fn[t].noConflict=function(){return e.fn[t]=i,d._jQueryInterface},d}(),g=function(){var t="scrollspy",n=e.fn[t],i={offset:10,method:"auto",target:""},o={offset:"number",method:"string",target:"(string|element)"},a={ACTIVATE:"activate.bs.scrollspy",SCROLL:"scroll.bs.scrollspy",LOAD_DATA_API:"load.bs.scrollspy.data-api"},l={DROPDOWN_ITEM:"dropdown-item",DROPDOWN_MENU:"dropdown-menu",ACTIVE:"active"},h={DATA_SPY:'[data-spy="scroll"]',ACTIVE:".active",NAV_LIST_GROUP:".nav, .list-group",NAV_LINKS:".nav-link",NAV_ITEMS:".nav-item",LIST_ITEMS:".list-group-item",DROPDOWN:".dropdown",DROPDOWN_ITEMS:".dropdown-item",DROPDOWN_TOGGLE:".dropdown-toggle"},c={OFFSET:"offset",POSITION:"position"},u=function(){function n(t,n){var i=this;this._element=t,this._scrollElement="BODY"===t.tagName?window:t,this._config=this._getConfig(n),this._selector=this._config.target+" "+h.NAV_LINKS+","+this._config.target+" "+h.LIST_ITEMS+","+this._config.target+" "+h.DROPDOWN_ITEMS,this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,e(this._scrollElement).on(a.SCROLL,function(t){return i._process(t)}),this.refresh(),this._process()}var u=n.prototype;return u.refresh=function(){var t=this,n=this._scrollElement!==this._scrollElement.window?c.POSITION:c.OFFSET,i="auto"===this._config.method?n:this._config.method,r=i===c.POSITION?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),e.makeArray(e(this._selector)).map(function(t){var n,o=s.getSelectorFromElement(t);if(o&&(n=e(o)[0]),n){var a=n.getBoundingClientRect();if(a.width||a.height)return[e(n)[i]().top+r,o]}return null}).filter(function(t){return t}).sort(function(t,e){return t[0]-e[0]}).forEach(function(e){t._offsets.push(e[0]),t._targets.push(e[1])})},u.dispose=function(){e.removeData(this._element,"bs.scrollspy"),e(this._scrollElement).off(".bs.scrollspy"),this._element=null,this._scrollElement=null,this._config=null,this._selector=null,this._offsets=null,this._targets=null,this._activeTarget=null,this._scrollHeight=null},u._getConfig=function(n){if("string"!=typeof(n=e.extend({},i,n)).target){var r=e(n.target).attr("id");r||(r=s.getUID(t),e(n.target).attr("id",r)),n.target="#"+r}return s.typeCheckConfig(t,n,o),n},u._getScrollTop=function(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop},u._getScrollHeight=function(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)},u._getOffsetHeight=function(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height},u._process=function(){var t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),n=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=n){var i=this._targets[this._targets.length-1];this._activeTarget!==i&&this._activate(i)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(var s=this._offsets.length;s--;)this._activeTarget!==this._targets[s]&&t>=this._offsets[s]&&("undefined"==typeof this._offsets[s+1]||t li > .active",DATA_TOGGLE:'[data-toggle="tab"], [data-toggle="pill"], [data-toggle="list"]',DROPDOWN_TOGGLE:".dropdown-toggle",DROPDOWN_ACTIVE_CHILD:"> .dropdown-menu .active"},a=function(){function t(t){this._element=t}var a=t.prototype;return a.show=function(){var t=this;if(!(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&e(this._element).hasClass(i.ACTIVE)||e(this._element).hasClass(i.DISABLED))){var r,a,l=e(this._element).closest(o.NAV_LIST_GROUP)[0],h=s.getSelectorFromElement(this._element);if(l){var c="UL"===l.nodeName?o.ACTIVE_UL:o.ACTIVE;a=e.makeArray(e(l).find(c)),a=a[a.length-1]}var u=e.Event(n.HIDE,{relatedTarget:this._element}),d=e.Event(n.SHOW,{relatedTarget:a});if(a&&e(a).trigger(u),e(this._element).trigger(d),!d.isDefaultPrevented()&&!u.isDefaultPrevented()){h&&(r=e(h)[0]),this._activate(this._element,l);var f=function(){var i=e.Event(n.HIDDEN,{relatedTarget:t._element}),s=e.Event(n.SHOWN,{relatedTarget:a});e(a).trigger(i),e(t._element).trigger(s)};r?this._activate(r,r.parentNode,f):f()}}},a.dispose=function(){e.removeData(this._element,"bs.tab"),this._element=null},a._activate=function(t,n,r){var a,l=this,h=(a="UL"===n.nodeName?e(n).find(o.ACTIVE_UL):e(n).children(o.ACTIVE))[0],c=r&&s.supportsTransitionEnd()&&h&&e(h).hasClass(i.FADE),u=function(){return l._transitionComplete(t,h,c,r)};h&&c?e(h).one(s.TRANSITION_END,u).emulateTransitionEnd(150):u(),h&&e(h).removeClass(i.SHOW)},a._transitionComplete=function(t,n,r,a){if(n){e(n).removeClass(i.ACTIVE);var l=e(n.parentNode).find(o.DROPDOWN_ACTIVE_CHILD)[0];l&&e(l).removeClass(i.ACTIVE),"tab"===n.getAttribute("role")&&n.setAttribute("aria-selected",!1)}if(e(t).addClass(i.ACTIVE),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),r?(s.reflow(t),e(t).addClass(i.SHOW)):e(t).removeClass(i.FADE),t.parentNode&&e(t.parentNode).hasClass(i.DROPDOWN_MENU)){var h=e(t).closest(o.DROPDOWN)[0];h&&e(h).find(o.DROPDOWN_TOGGLE).addClass(i.ACTIVE),t.setAttribute("aria-expanded",!0)}a&&a()},t._jQueryInterface=function(n){return this.each(function(){var i=e(this),s=i.data("bs.tab");if(s||(s=new t(this),i.data("bs.tab",s)),"string"==typeof n){if("undefined"==typeof s[n])throw new Error('No method named "'+n+'"');s[n]()}})},r(t,null,[{key:"VERSION",get:function(){return"4.0.0-beta.2"}}]),t}();return e(document).on(n.CLICK_DATA_API,o.DATA_TOGGLE,function(t){t.preventDefault(),a._jQueryInterface.call(e(this),"show")}),e.fn.tab=a._jQueryInterface,e.fn.tab.Constructor=a,e.fn.tab.noConflict=function(){return e.fn.tab=t,a._jQueryInterface},a}();return function(){if("undefined"==typeof e)throw new Error("Bootstrap's JavaScript requires jQuery. jQuery must be included before Bootstrap's JavaScript.");var t=e.fn.jquery.split(" ")[0].split(".");if(t[0]<2&&t[1]<9||1===t[0]&&9===t[1]&&t[2]<1||t[0]>=4)throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}(),t.Util=s,t.Alert=a,t.Button=l,t.Carousel=h,t.Collapse=c,t.Dropdown=u,t.Modal=d,t.Popover=_,t.Scrollspy=g,t.Tab=m,t.Tooltip=f,t}({},$,Popper); 7 | //# sourceMappingURL=bootstrap.min.js.map -------------------------------------------------------------------------------- /app/static/js/main.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | 3 | }) -------------------------------------------------------------------------------- /app/templates/bus.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 | 10 |

Legend:

11 |

"s" - skip, "space" - save and next

12 |

images left: 0

13 | 14 | 15 | 16 | 17 | 236 | 237 | 238 | 239 | 240 | -------------------------------------------------------------------------------- /app/templates/container.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Markup heads 5 | {% include "header.html" %} 6 | 7 | 8 | 9 | {% include "nav_bar.html" %} 10 | 11 |

12 | {% if review %}

Review page

{%else%}

Markup page

{%endif%} 13 | 14 |
15 |
16 | 17 | 18 |
19 |
20 |
21 |
Legend:
22 |
23 |
24 | 25 |
26 |
27 |
28 | 29 | Images left:    30 | 31 | 32 |
33 |
34 | 433 |
434 | 435 | 436 | -------------------------------------------------------------------------------- /app/templates/door.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Markup door 5 | {% include "header.html" %} 6 | 7 | 8 | 9 | {% include "nav_bar.html" %} 10 | 11 | 12 |
13 | 14 | {% if review %}

Review page

{%else%}

Markup page

{%endif%} 15 | 16 |
17 |
18 | 19 | 20 |
21 |
22 |
23 |
Legend:
24 |
25 |
26 | 27 |
28 |
29 |
30 | 31 | Images left:    32 | 33 | 34 |
35 |
36 | 37 | 443 |
444 | 445 | 446 | -------------------------------------------------------------------------------- /app/templates/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/templates/heads.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Markup heads 5 | {% include "header.html" %} 6 | 7 | 8 | 9 | {% include "nav_bar.html" %} 10 | 11 | 12 |
13 | 14 | {% if review %}

Review page

{%else%}

Markup page

{%endif%} 15 | 16 |
17 |
18 | 19 | 20 |
21 |
22 |
23 |
Legend:
24 |
25 |
26 | 27 |
28 |
29 |
30 | 31 | Images left:    32 | 33 | 34 |
35 |
36 | 37 | 446 |
447 | 448 | 449 | -------------------------------------------------------------------------------- /app/templates/input.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Input samples 6 | {% include "header.html" %} 7 | 8 | 9 | {% include "nav_bar.html" %} 10 | 11 | 12 |
13 |
14 |
15 |
16 |
17 | 18 | 19 |
20 | 21 |
22 |
23 |
24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | {% if day_by_day | length > 0 %} 34 | {% for day in day_by_day %} 35 | 36 | 37 | 38 | 39 | 40 | 41 | {% endfor %} 42 | {% else %} 43 | 44 | {% endif %} 45 | 46 |
NoDate SamplesDelete
{{loop.index}}{{day['date']}}{{day['count']}}
There is no data to show
47 |
48 |
49 |
50 | -------------------------------------------------------------------------------- /app/templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Login 5 | {% include "header.html" %} 6 | 7 | 8 |
9 |
10 |
11 |
12 |
13 |

Login

14 |
15 | 16 | 17 | 18 | 19 |
20 | 21 |
22 | 23 | 24 | 25 | 26 |
27 |
28 | 31 | Forgot Password? 32 |
33 |
34 | 35 |
36 |
37 |
38 |
39 |
40 |
41 | 42 | -------------------------------------------------------------------------------- /app/templates/nav_bar.html: -------------------------------------------------------------------------------- 1 | 2 | 29 | -------------------------------------------------------------------------------- /app/templates/onerect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | One rectangle 6 | 7 | 8 | 9 | 10 |

Legend: "c" - clear, "s" - skip, "space" - save and next

11 | 12 | 13 | 14 | 15 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /app/templates/register.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Register 6 | {% include "header.html" %} 7 | 8 | 9 | 10 |
11 |
12 |
13 |

Register

14 |
15 | {{ form.hidden_tag() }} 16 |
17 |
18 | 19 | 20 | 21 | {{ form.username(size=32, class='form-control', placeholder='Username') }} 22 |
23 | {% for error in form.username.errors %} 24 | [{{ error }}] 25 | {% endfor %} 26 |
27 |
28 |
29 | 30 | 31 | 32 | {{ form.email(size=32, class='form-control', placeholder='Email') }} 33 |
34 | {% for error in form.email.errors %} 35 | [{{ error }}] 36 | {% endfor %} 37 |
38 |
39 |
40 | 41 | 42 | 43 | {{ form.password(size=32, class='form-control', placeholder='Password') }} 44 |
45 | {% for error in form.password.errors %} 46 | [{{ error }}] 47 | {% endfor %} 48 |
49 |
50 |
51 | 52 | 53 | 54 | {{ form.password2(size=32, class='form-control', placeholder='Confirm Password') }} 55 |
56 | {% for error in form.password2.errors %} 57 | [{{ error }}] 58 | {% endfor %} 59 |
60 |
61 | 62 |
63 |
64 |
65 |
66 |
67 | -------------------------------------------------------------------------------- /app/templates/review_choose_user.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Select a user 5 | {% include "header.html" %} 6 | 7 | 8 | {% include "nav_bar.html" %} 9 | 10 |
11 |
12 |
13 |

Select a user to review:

14 |
15 |
16 | 21 |
22 | 23 |
24 |
25 |
26 |

Current user to review: {{cur_user}}

27 |

Marked files: {{files|length}}

28 |

Delete them all for the user

29 | 30 | {% if files | length > 0 %} 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | {% for fn in files %} 41 | 42 | 43 | 44 | 45 | 46 | 47 | {% endfor %} 48 | 49 | {% else %} 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | {% endif %} 62 |
NoFile NameShowDelete
{{loop.index}}{{fn}}
NoFile NameViewDelete
There is no data to show
63 |
64 |
65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /app/templates/tiles.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 | 10 |

Legend:

11 |

"s" - skip, "space" - save and next

12 |

images left: 0

13 | 14 | 15 | 16 | 17 | 178 | 179 | 180 | 181 | -------------------------------------------------------------------------------- /app/templates/trainsegmentation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 | 10 |

Legend:

11 |

"s" - skip, "space" - save and next

12 |

images left: 0

13 | 14 | 15 | 16 | 17 | 178 | 179 | 180 | 181 | -------------------------------------------------------------------------------- /app/templates/uploadfiles.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% include "header.html" %} 6 | 7 | 8 | {% include "nav_bar.html" %} 9 | 10 |

11 |
12 |
13 |
14 |
Upload Files
15 |
16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 |
24 |
25 |
26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /app/uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | module = main 3 | logto = /app/app.log 4 | callable = app 5 | lazy-apps = true 6 | buffer-size = 65535 7 | processes = 1 8 | cheaper=0 9 | wsgi-disable-file-wrapper = true -------------------------------------------------------------------------------- /confd_nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443 ssl; 3 | server_name YOUR_ADDRESS.com www.YOUR_ADDRESS.com; 4 | ssl_certificate /app/ssl.crt; 5 | ssl_certificate_key /app/ssl.pem; 6 | location / { 7 | try_files $uri @app; 8 | proxy_max_temp_file_size 0; 9 | } 10 | location @app { 11 | include uwsgi_params; 12 | uwsgi_pass unix:///tmp/uwsgi.sock; 13 | } 14 | location /static { 15 | alias /app/static; 16 | } 17 | } 18 | --------------------------------------------------------------------------------