Skip to content

Commit 6a4c4fd

Browse files
authored
Realtime support with Sockets-io (#102)
* Added socket to development envirment * Realtime image locking * Image locking fixes (#94) * Realtime annotation collaberation * Removed autosave * Production socket server * Fixed annotation session changing * Username duplication errror * Socket connection toastr * Navbar backend status * Fixed mobile navbar text alignment * Tasks (#103) * Created task model * Tasks webpage * Webpage updates * Create scan task * Realtime task progress updates and scan example task * Formatting * Created scanning task (#101) * Added tasks webpage to navbar * Delete tasks * Created task javascript model * Datasets javascript model * Import task * Task completion flag * Fixed coco annotation importer * Show only log warnings/errors * Login disabled sockets * Removed exporting of empty segmentations * Fixed production envirment * Added secret key to compose file * Connection lost warning, & formatting * Warning and error updates for tasks * Created admin model * Created undo model * Added dataset name to navbar (#88)
1 parent 83ae08b commit 6a4c4fd

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+4331
-3014
lines changed

app/__init__.py

Lines changed: 16 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,28 @@
1+
import eventlet
2+
eventlet.monkey_patch(thread=False)
3+
14
from flask import Flask
2-
from werkzeug.contrib.fixers import ProxyFix
35
from flask_cors import CORS
4-
from watchdog.observers import Observer
6+
from werkzeug.contrib.fixers import ProxyFix
57

6-
from .image_folder import ImageFolderHandler
7-
from .api import blueprint as api
8-
from .config import Config
98
from .models import *
10-
from .authentication import login_manager
9+
from .config import Config
10+
from .sockets import socketio
11+
from .watcher import run_watcher
12+
from .api import blueprint as api
1113
from .util import query_util, color_util
14+
from .authentication import login_manager
1215

1316
import threading
1417
import requests
1518
import time
1619
import os
1720

1821

19-
def run_watcher():
20-
observer = Observer()
21-
observer.schedule(ImageFolderHandler(), Config.DATASET_DIRECTORY, recursive=True)
22-
observer.start()
23-
24-
try:
25-
while True:
26-
time.sleep(1)
27-
except KeyboardInterrupt:
28-
observer.stop()
29-
30-
observer.join()
31-
32-
3322
def create_app():
3423

35-
if os.environ.get("APP_WORKER_ID", "1") == "1" and not Config.TESTING:
36-
print("Creating file watcher on PID: {}".format(os.getpid()), flush=True)
37-
watcher_thread = threading.Thread(target=run_watcher)
38-
watcher_thread.start()
24+
if Config.FILE_WATCHER:
25+
run_watcher()
3926

4027
flask = Flask(__name__,
4128
static_url_path='',
@@ -50,6 +37,11 @@ def create_app():
5037

5138
db.init_app(flask)
5239
login_manager.init_app(flask)
40+
socketio.init_app(flask)
41+
42+
# Remove all poeple who were annotating when
43+
# the server shutdown
44+
ImageModel.objects.update(annotating=[])
5345

5446
return flask
5547

@@ -60,9 +52,6 @@ def create_app():
6052
if Config.INITIALIZE_FROM_FILE:
6153
create_from_json(Config.INITIALIZE_FROM_FILE)
6254

63-
if Config.LOAD_IMAGES_ON_START:
64-
ImageModel.load_images(Config.DATASET_DIRECTORY)
65-
6655

6756
@app.route('/', defaults={'path': ''})
6857
@app.route('/<path:path>')
@@ -72,5 +61,3 @@ def index(path):
7261
return requests.get('http://frontend:8080/{}'.format(path)).text
7362

7463
return app.send_static_file('index.html')
75-
76-

app/api/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from .images import api as ns_images
99
from .users import api as ns_users
1010
from .admin import api as ns_admin
11+
from .tasks import api as ns_tasks
1112
from .undo import api as ns_undo
1213
from .info import api as ns_info
1314

@@ -33,6 +34,7 @@
3334
api.add_namespace(ns_categories)
3435
api.add_namespace(ns_annotator)
3536
api.add_namespace(ns_datasets)
37+
api.add_namespace(ns_tasks)
3638
api.add_namespace(ns_undo)
3739
api.add_namespace(ns_admin)
3840

app/api/datasets.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ def post(self, dataset_id):
170170

171171

172172
@api.route('/data')
173-
class Dataset(Resource):
173+
class DatasetData(Resource):
174174
@api.expect(page_data)
175175
@login_required
176176
def get(self):
@@ -234,7 +234,7 @@ def get(self, dataset_id):
234234
return {'message': 'Directory does not exist.'}, 400
235235

236236
images = ImageModel.objects(dataset_id=dataset_id, path__startswith=directory, deleted=False) \
237-
.order_by('file_name').only('id', 'file_name')
237+
.order_by('file_name').only('id', 'file_name', 'annotating')
238238

239239
pagination = Pagination(images.count(), limit, page)
240240
images = query_util.fix_ids(images[pagination.start:pagination.end])
@@ -263,12 +263,11 @@ def get(self, dataset_id):
263263

264264

265265
@api.route('/<int:dataset_id>/coco')
266-
class ImageCoco(Resource):
266+
class DatasetCoco(Resource):
267267

268268
@login_required
269269
def get(self, dataset_id):
270270
""" Returns coco of images and annotations in the dataset """
271-
272271
dataset = current_user.datasets.filter(id=dataset_id).first()
273272

274273
if dataset is None:
@@ -287,16 +286,11 @@ def post(self, dataset_id):
287286
if dataset is None:
288287
return {'message': 'Invalid dataset ID'}, 400
289288

290-
import_id = CocoImporter.import_coco(
291-
coco, dataset_id, current_user.username)
292-
293-
return {
294-
"import_id": import_id
295-
}
289+
return dataset.import_coco(json.load(coco))
296290

297291

298292
@api.route('/coco/<int:import_id>')
299-
class ImageCocoId(Resource):
293+
class DatasetCocoId(Resource):
300294

301295
@login_required
302296
def get(self, import_id):
@@ -311,3 +305,17 @@ def get(self, import_id):
311305
"progress": coco_import.progress,
312306
"errors": coco_import.errors
313307
}
308+
309+
310+
@api.route('/<int:dataset_id>/scan')
311+
class DatasetScan(Resource):
312+
313+
@login_required
314+
def get(self, dataset_id):
315+
316+
dataset = DatasetModel.objects(id=dataset_id).first()
317+
318+
if not dataset:
319+
return {'message': 'Invalid dataset ID'}, 400
320+
321+
return dataset.scan()

app/api/tasks.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from flask_restplus import Namespace, Resource, reqparse
2+
from flask_login import login_required, current_user
3+
4+
from ..util import query_util
5+
from ..config import Config
6+
from ..models import TaskModel
7+
8+
9+
api = Namespace('tasks', description='Task related operations')
10+
11+
12+
@api.route('/')
13+
class Task(Resource):
14+
@login_required
15+
def get(self):
16+
""" Returns all tasks """
17+
query = TaskModel.objects.only(
18+
'group', 'id', 'name', 'completed', 'progress',
19+
'priority', 'creator', 'desciption', 'errors',
20+
'warnings'
21+
).all()
22+
return query_util.fix_ids(query)
23+
24+
25+
@api.route('/<int:task_id>')
26+
class TaskId(Resource):
27+
@login_required
28+
def delete(self, task_id):
29+
""" Deletes task """
30+
task = TaskModel.objects(id=task_id).first()
31+
32+
if task is None:
33+
return {"message": "Invalid task id"}, 400
34+
35+
if not task.completed:
36+
return {"message": "Task is not completed"}, 400
37+
38+
task.delete()
39+
return {"success": True}
40+
41+
42+
@api.route('/<int:task_id>/logs')
43+
class TaskId(Resource):
44+
@login_required
45+
def get(self, task_id):
46+
""" Deletes task """
47+
task = TaskModel.objects(id=task_id).first()
48+
if task is None:
49+
return {"message": "Invalid task id"}, 400
50+
51+
return {'logs': task.logs}

app/api/users.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
set_password.add_argument('password', required=True, location='json')
2323
set_password.add_argument('new_password', required=True, location='json')
2424

25+
from flask_socketio import SocketIO, disconnect
2526

2627
@api.route('/')
2728
class User(Resource):

app/config.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,21 @@ class Config:
88
NAME = "COCO Annotator"
99
VERSION = get_tag()
1010

11+
# File Watcher
12+
FILE_WATCHER = os.getenv("FILE_WATCHER", False)
13+
IGNORE_DIRECTORIES = ["_thumbnail", "_settings"]
14+
1115
# Flask instance
1216
SWAGGER_UI_JSONEDITOR = True
1317
MAX_CONTENT_LENGTH = 1 * 1024 * 1024 * 1024 # 1GB
1418
MONGODB_HOST = os.getenv("MONGODB_HOST", "mongodb://database/flask")
15-
SECRET_KEY = os.getenv('SECRET_KEY', '<--- YOUR_SECRET_FORM_KEY --->')
19+
SECRET_KEY = os.getenv("SECRET_KEY", "<--- DEFAULT_SECRET_KEY --->")
1620

1721
TESTING = os.getenv("TESTING", False)
1822

1923
# Dataset Options
2024
DATASET_DIRECTORY = os.getenv("DATASET_DIRECTORY", "/datasets/")
2125
INITIALIZE_FROM_FILE = os.getenv("INITIALIZE_FROM_FILE")
22-
LOAD_IMAGES_ON_START = os.getenv("LOAD_IMAGES_ON_START", False)
2326

2427
# Coco Importer Options
2528
COCO_IMPORTER_VERBOSE = os.getenv("COCO_IMPORTER_VERBOSE", False)
@@ -30,5 +33,5 @@ class Config:
3033
os.getenv("COCO_IMPORTER_ANNOTATION_BATCH_SIZE", 1000))
3134

3235
# User Options
33-
LOGIN_DISABLED = os.getenv('LOGIN_DISABLED', False)
36+
LOGIN_DISABLED = os.getenv("LOGIN_DISABLED", False)
3437
ALLOW_REGISTRATION = True

app/gunicorn_config.py

Lines changed: 0 additions & 53 deletions
This file was deleted.

0 commit comments

Comments
 (0)