diff --git a/.gitignore b/.gitignore index abc3bb4d0b13f29e3e2af5a20e30543ed3cfdec3..4641bcecd3639bd95fd758cc120a25825b7cf1c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .vscode/ -logs \ No newline at end of file +logs +__pycache__/ diff --git a/atlas_controller/.idea/atlas_controller.iml b/atlas_controller/.idea/atlas_controller.iml index beb492c84684dc1965df41e22394a0604e7e6289..7d0c25c84f9cb103fdae42733e9fca547cb33c93 100644 --- a/atlas_controller/.idea/atlas_controller.iml +++ b/atlas_controller/.idea/atlas_controller.iml @@ -2,8 +2,17 @@ <module type="PYTHON_MODULE" version="4"> <component name="NewModuleRootManager"> <content url="file://$MODULE_DIR$" /> - <orderEntry type="inheritedJdk" /> + <orderEntry type="jdk" jdkName="Python 3.7 (atlas_ui)" jdkType="Python SDK" /> <orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="module" module-name="atlas_server" /> </component> + <component name="PackageRequirementsSettings"> + <option name="requirementsPath" value="" /> + </component> + <component name="PyDocumentationSettings"> + <option name="myDocStringFormat" value="Google" /> + </component> + <component name="TestRunnerService"> + <option name="PROJECT_TEST_RUNNER" value="Unittests" /> + </component> </module> \ No newline at end of file diff --git a/atlas_controller/atlas_controller b/atlas_controller/atlas_controller new file mode 120000 index 0000000000000000000000000000000000000000..5590cf639e308fa42d65af25bdbcfc0a8f63557c --- /dev/null +++ b/atlas_controller/atlas_controller @@ -0,0 +1 @@ +/p/home/jusers/schiffer1/jureca/project_jinm16/atlasui/atlas_controller/ \ No newline at end of file diff --git a/atlas_controller/atlas_controller/__init__.py b/atlas_controller/atlas_controller/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/atlas_controller/atlas_controller/app.py b/atlas_controller/atlas_controller/app.py deleted file mode 100644 index c9162fd099831b4fbcdd6645ae7f77e10e6f5b98..0000000000000000000000000000000000000000 --- a/atlas_controller/atlas_controller/app.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python3 -# Server controlling all actions on JURECA -from flask import Flask, request -from flask_restful import Resource, Api - -app = Flask(__name__) -api = Api(app, prefix="/api") - - -@app.route("/") -def welcome(): - return """<h1>Welcome to ATLaS UI</h1> -API available <a href="/api">here</a> - """ - - -def get_json_data(): - assert request.is_json - data = request.get_json() - return data - - -class SetupTraining(Resource): - def post(self): - data = get_json_data() - print(data) - return "Thank you from JURECA!" - - def get(self): - return "This is a test page!" - - -api.add_resource(SetupTraining, "/setup_training") - -if __name__ == "__main__": - app.run(debug=True, port=8000) diff --git a/atlas_controller/atlas_controller/main.py b/atlas_controller/atlas_controller/main.py deleted file mode 100644 index 712251f6e384d4714703536998669632bb198e3d..0000000000000000000000000000000000000000 --- a/atlas_controller/atlas_controller/main.py +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env python3 -import os -from time import sleep -import glob as glob -import json -from atlas.experiments import get_logger, init_logging, require_directory, setup_file_logger -from atlas.util.iterable import group_by - -WAIT_INTERVAL = 2 -EVENT_DIRECTORY = "events" -DELETE_EVENTS = True -WORK_DIR = "work" - - -class Event(object): - def __init__(self, event_file): - self.event_file = event_file - - with open(self.event_file, "r") as f: - self.event_content = json.load(f) - self.event_type = self.event_content["event_type"] - self.keep_event = self.event_content.get("keep_event", False) - self.id = self.event_content.get("id", None) - - -def make_id_dir(id): - return require_directory(os.path.join(WORK_DIR, id)) - - -def search_for_events(directory): - event_files = glob.glob(os.path.join(directory, "atlas_*.json")) - return event_files - - -def stop(event, log): - log.info("Received STOP event") - return True - - -def setup_training(event, log): - if event.id is None: - log.info(f"Event contains no id, cannot create annotations! (File: {event.event_file})") - return False - annotations = event.event_content.get("annotations", None) - if annotations is None: - log.info(f"Event contains no annotations, cannot create annotations! (File: {event.event_file})") - return False - - # Create a directory to write the annotation files to - id_dir = make_id_dir(event.id) - annotation_dir = require_directory(os.path.join(id_dir, "annotations")) - # Group annotations by section - annotations_by_image = group_by(annotations, lambda item: item["image"]) - - for image, annotations_for_image in annotations_by_image.items(): - content_to_write = { - "Regions": [{"name": annotation["name"], "path": annotation["path"]} for annotation in annotations_for_image], - } - fname = os.path.join(annotation_dir, f"test_{image}.json") - log.info(f"Writing annotations to {fname}") - with open(fname, "w") as f: - json.dump(content_to_write, f) - - -def unknown_event(event, log): - log.error(f"No handler for event type {event.event_type}") - event.keep_event = True - return False - - -event_handlers = { - "stop": stop, - "setup_training": setup_training, -} - - -def main(): - init_logging() - log = get_logger() - - log.info(f"Wait interval: {WAIT_INTERVAL}") - log.info(f"Event directory: {EVENT_DIRECTORY}") - - # Create a directory to write logs to - log_folder = require_directory(".") - setup_file_logger(root=log_folder, prefix="altas_controller") - log.info("Started ATLaS controller") - - # This signal specifies when the program will stop - stop_signal = False - - while not stop_signal: - if not os.path.exists(EVENT_DIRECTORY): - log.warning(f"Event directory \"{EVENT_DIRECTORY}\" does not exist!") - sleep(WAIT_INTERVAL) - continue - - # log.info(f"Searching directory \"{EVENT_DIRECTORY}\" for events") - event_files = search_for_events(EVENT_DIRECTORY) - if event_files: - log.info(f"Found {len(event_files)} event(s)") - - for event_file in event_files: - event = Event(event_file) - handler = event_handlers.get(event.event_type, unknown_event) - stop_signal = handler(event, log) - - if not event.keep_event and DELETE_EVENTS: - log.info(f"Deleting event file \"{event_file}\"") - if os.path.exists(event_file): - os.remove(event_file) - - if not stop_signal: - sleep(WAIT_INTERVAL) - - log.info("ATLaS controller stopped") - - -if __name__ == "__main__": - main() diff --git a/atlas_controller/start_remote_server.sh b/atlas_controller/start_remote_server.sh index 8fb0f56ba2a7ac6a1c7ac30cc2860556bc06bf69..1f59dba7a094c8053a4344357a6be3db4df54fb0 100755 --- a/atlas_controller/start_remote_server.sh +++ b/atlas_controller/start_remote_server.sh @@ -3,4 +3,5 @@ USER=schiffer1 HOST=jureca06.fz-juelich.de -ssh ${USER}@${HOST} -t "bash -cl 'cd /p/home/jusers/schiffer1/jureca/project_jinm16/atlasui/; source load_modules.sh; source venv/bin/activate; python app.py'" \ No newline at end of file +COMMAND='cd /p/home/jusers/schiffer1/jureca/project_jinm16/atlasui/; source load_modules.sh; source venv/bin/activate; export PYTHONPATH=`pwd`:$PYTHONPATH; python atlas_controller/app.py' +ssh ${USER}@${HOST} -t "bash -cl '${COMMAND}'" \ No newline at end of file diff --git a/atlas_server/.idea/atlas_server.iml b/atlas_server/.idea/atlas_server.iml index 2942a30ab8aea01b3393f1d1e098342c0bf997c4..1972284c026c854b3f8a86d88eb8e1cb02192ff6 100644 --- a/atlas_server/.idea/atlas_server.iml +++ b/atlas_server/.idea/atlas_server.iml @@ -2,8 +2,17 @@ <module type="PYTHON_MODULE" version="4"> <component name="NewModuleRootManager"> <content url="file://$MODULE_DIR$" /> - <orderEntry type="inheritedJdk" /> + <orderEntry type="jdk" jdkName="Python 3.7 (atlas_ui)" jdkType="Python SDK" /> <orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="module" module-name="atlas_controller" /> </component> + <component name="PackageRequirementsSettings"> + <option name="requirementsPath" value="" /> + </component> + <component name="PyDocumentationSettings"> + <option name="myDocStringFormat" value="Google" /> + </component> + <component name="TestRunnerService"> + <option name="PROJECT_TEST_RUNNER" value="Unittests" /> + </component> </module> \ No newline at end of file diff --git a/atlas_server/atlas_server/app.py b/atlas_server/atlas_server/app.py index 7a46790dfdb7ce1e0a6cede0f9dcb292ca839aa9..44b5ed2d09723167acc3d25e9abd464875c6f0a9 100644 --- a/atlas_server/atlas_server/app.py +++ b/atlas_server/atlas_server/app.py @@ -1,170 +1,210 @@ #!/usr/bin/env python3 -import requests -from bson.objectid import ObjectId -from flask import Flask, request +from flask import Flask from flask_cors import CORS -from flask_restful import Resource, Api -from pymongo import MongoClient +from flask_restplus import Api, Resource, fields, abort -# DATABASE_HOST = "medpc018.ime.kfa-juelich.de" -DATABASE_HOST = "localhost" -DATABASE_PORT = 27017 -DATABASE_NAME = "atlas_ui_db" -# UPLOAD_ROOT = "/home/cschiffer/code/atlasui_prototype/atlas_controller/events" - -ATLAS_CONTROLLER_URL = "http://localhost:8000/api" -ATLAS_CONTROLLER_SETUP_TRAINING_ENDPOINT = f"{ATLAS_CONTROLLER_URL}/setup_training" +from atlas_server import models +from atlas_server.db import Database, ProjectNotFoundError, TaskNotFoundError, ForbiddenOperationError app = Flask(__name__) +# Setup flask with CORS, since we do requests to atlas controller CORS(app) -api = Api(app) - - -class MissingIdError(Exception): - pass - - -class InvalidIdError(Exception): - pass - - -def create_client(): - return MongoClient(DATABASE_HOST, DATABASE_PORT) - - -def get_projects_table(client): - database = client.get_database(DATABASE_NAME) - projects = database.projects - return projects - - -def check_id(id_to_check): - with create_client() as client: - projects = get_projects_table(client) - if projects.find_one({"_id": ObjectId(id_to_check)}): - return True - else: - return False - - -def generate_id(): - with create_client() as client: - projects = get_projects_table(client) - inserted_element = projects.insert_one({}) - new_id = str(inserted_element.inserted_id) - return new_id - - -def validate_id_from_data(data): - # Check if the given id exists and is valid - if "id" not in data: - raise MissingIdError("Id is missing") - - id_to_check = data["id"] - if not check_id(id_to_check): - raise InvalidIdError(f"Id {id_to_check} is invalid. Please request a valid id.") - - return id_to_check - - -def find_project_for_id(id): - with create_client() as client: - projects = get_projects_table(client) - project = projects.find_one({"_id": ObjectId(id)}) - return project - - -def get_data_for_id(id): - with create_client() as client: - projects = get_projects_table(client) - return projects.find_one({"_id": ObjectId(id)}) - - -def add_data_for_id(id, data): - with create_client() as client: - projects = get_projects_table(client) - projects.update_one({"_id": ObjectId(id)}, data) - - -def add_annotation(id, annotation_name, annotation_path, image): - print(f"Adding annotation: {annotation_name}") - add_data_for_id(id, {"$push": { - "annotations": { - "name": annotation_name, - "path": annotation_path, - "image": image, - } - }}) - - -def get_json_data(): - assert request.is_json - data = request.get_json() - return data - - -# def upload_data(fname, data): -# fname = os.path.join(UPLOAD_ROOT, fname) -# # Write data -# with open(fname, "w") as f: -# json.dump(data, f, indent=4) - -class RequestId(Resource): - def post(self): - # TODO Authentication - # Generate entry for the database - new_id = generate_id() - print(f"Generated new ID: {new_id}") - - return {"id": new_id} - - -api.add_resource(RequestId, "/request_id") - - -class SubmitAnnotations(Resource): +api = Api(app, + prefix="/api", + version="1.0", + title="ATLaS UI API", + description="API serving as backend for the ATLaS UI") + +# ----------------------------------- +# Namespace definitions +# ----------------------------------- + +project_namespace = api.namespace("projects", description="Operations to deal with projects") + +# ----------------------------------- +# Model definitions +# ----------------------------------- + +task_model = api.model("TaskModel", + { + "task_id": fields.Integer(description="ID of the task."), + "name": fields.String(description="The name of the task."), + "annotations": fields.List(fields.String(description="Annotations to add to the task.")), + }) + +project_model = api.model("ProjectModel", + { + "project_id": fields.Integer(description="ID of the project."), + "name": fields.String(description="The name of the project."), + "brain": fields.Integer(description="Number of the brain the projects refers to."), + "owner": fields.String(description="Owner of the project."), + "tasks": fields.List(fields.Nested(task_model), description="List of tasks of the project.") + }) + +_project_schema = models.ProjectSchema() +_task_schema = models.TaskSchema() + + +# ----------------------------------- +# Actual API definitions +# ----------------------------------- + +@project_namespace.route("/") +class ProjectList(Resource): + @project_namespace.doc("Get all projects") + def get(self): + # TODO This function should be hidden behind an authentication mechanism + with Database() as db: + projects = db.projects.find({}) + if not projects: + abort(404, f"No projects found") + return _project_schema.dump(projects, many=True) + + @project_namespace.doc("Create a new project") + @project_namespace.expect(project_model) def post(self): - data = get_json_data() - id = validate_id_from_data(data) - for annotation in data["annotations"]: - add_annotation(id=id, - annotation_name=annotation["annotation_name"], - annotation_path=annotation["annotation_path"], - image=annotation["image"]) + # Deserialize project + project = _project_schema.load(api.payload) + with Database() as db: + project_id = db.insert_project(project) -api.add_resource(SubmitAnnotations, "/submit_annotations") + # Return of newly inserted project + return {"project_id": project_id, "status": "success"} -class SetupTraining(Resource): - def post(self): - # Data is received as JSON - json_data = get_json_data() - # Validate ID - id = validate_id_from_data(json_data) - - # Get all fields we need - data_for_id = get_data_for_id(id) - annotations = json_data.get("annotations", None) - if annotations is None: - raise RuntimeError(f"Missing field: \"annotations\"") - - brain = json_data.get("brain", None) - if brain is None: - raise RuntimeError(f"Missing field: \"brain\"") - - # Create a file and upload it to the atlas controller - event_content = { - "id": id, - "brain": brain, - "annotations": annotations, - } - # Send to server - response = requests.post(ATLAS_CONTROLLER_SETUP_TRAINING_ENDPOINT, json=event_content) - return response.json() - - -api.add_resource(SetupTraining, "/setup_training") +@project_namespace.route("/<int:project_id>") +@project_namespace.response(404, "Project not found") +class Project(Resource): + @project_namespace.doc("Get project with the specified id") + def get(self, project_id): + with Database() as db: + project = db.get_project_by_id(project_id=project_id) + if not project: + abort(404, f"No project found for id {project_id}") + else: + return _project_schema.dump(project) + + @project_namespace.doc("Delete project with the specified id") + @project_namespace.response(204, "Project deleted") + def delete(self, project_id): + with Database() as db: + db.delete_project(project_id=project_id) + return {"status": "success"} + + @project_namespace.doc("Update project with the specified id") + @project_namespace.expect(project_model) + def put(self, project_id): + data = api.payload + if not data: + abort(400, "No fields to update provided") + + for field in ("created", "modified", "project_id", "tasks"): + data.pop(field, None) + + try: + with Database() as db: + db.update_project_values(project_id=project_id, key_value_dict=data) + except ProjectNotFoundError: + abort(404, f"No project found for id {project_id}") + except ForbiddenOperationError as ex: + abort(403, ex.message) + + return {"status": "success"} + + +@project_namespace.route("/users/<username>") +@project_namespace.response(404, "Project for user not found") +class UserProjectList(Resource): + @project_namespace.doc("Get projects of the specified user") + def get(self, username): + with Database() as db: + projects = db.get_projects_for_user(username=username) + if not projects: + abort(404, f"No projects found for user {username}") + return _project_schema.dump(projects, many=True) + + +@project_namespace.route("/<int:project_id>/tasks") +@project_namespace.response(404, "Project not found") +class TaskList(Resource): + @project_namespace.doc("Get tasks of the specified project") + def get(self, project_id): + with Database() as db: + project = db.get_project_by_id(project_id=project_id) + if not project: + abort(404, f"No project found for id {project_id}") + else: + return _task_schema.dump(project.tasks, many=True) + + @project_namespace.doc("Create a new task for the specified project") + @project_namespace.expect(task_model) + def post(self, project_id): + # Create a task + task = _task_schema.load(api.payload) + + # Add to project + try: + with Database() as db: + task_id = db.add_task_to_project(project_id=project_id, task=task) + except ProjectNotFoundError: + abort(404, f"No project found for id {project_id}") + return + + # Return ID of newly inserted task + return {"task_id": task_id, "status": "success"} + + +@project_namespace.route("/<int:project_id>/tasks/<int:task_id>") +@project_namespace.response(404, "Task not found") +class Task(Resource): + @project_namespace.doc("Get task with the specified task id and project id") + def get(self, project_id, task_id): + try: + with Database() as db: + task = db.get_task_by_id(project_id=project_id, + task_id=task_id) + except TaskNotFoundError: + abort(404, f"No task found for project id {project_id} and task id {task_id}") + return + + return _task_schema.dump(task) + + @project_namespace.doc("Delete project with the specified id") + @project_namespace.response(204, "Task deleted") + def delete(self, project_id, task_id): + # Delete the task by + try: + with Database() as db: + db.delete_task(project_id=project_id, task_id=task_id) + except TaskNotFoundError: + abort(404, f"No task found for id {task_id}") + return + + @project_namespace.doc("Update task with the specified task id and project id") + @project_namespace.expect(task_model) + def put(self, project_id, task_id): + data = api.payload + if not data: + abort(400, "No fields to update provided") + # Remove fields which may not be updated + for field in ("created", "modified", "task_id"): + data.pop(field, None) + + try: + with Database() as db: + db.update_task_values(project_id=project_id, task_id=task_id, key_value_dict=data) + except TaskNotFoundError: + abort(404, f"No task found for project id {project_id} and task id {task_id}") + except ForbiddenOperationError as ex: + abort(403, ex.message) + + return {"status": "success"} + + +# ----------------------------------- +# Main entry point +# ----------------------------------- if __name__ == "__main__": app.run(debug=True) diff --git a/atlas_server/atlas_server/config.py b/atlas_server/atlas_server/config.py new file mode 100644 index 0000000000000000000000000000000000000000..92456199cebe2df6d855f9d95f87f0adb06c5a55 --- /dev/null +++ b/atlas_server/atlas_server/config.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 +""" +Configuration for ATLaS server +""" +DATABASE_HOST = "localhost" +DATABASE_PORT = 27017 +DATABASE_NAME = "atlas_ui_db" +ATLAS_CONTROLLER_URL = "http://localhost:8000/api" diff --git a/atlas_server/atlas_server/db.py b/atlas_server/atlas_server/db.py new file mode 100644 index 0000000000000000000000000000000000000000..49739a0573c6547a2c337c921d03416afe3242be --- /dev/null +++ b/atlas_server/atlas_server/db.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python3 +""" +Access to Mondo database +""" +from time import time +from pymongo import MongoClient, DESCENDING +from atlas_server import models +from atlas_server.config import DATABASE_HOST, DATABASE_PORT, DATABASE_NAME + +_project_schema = models.ProjectSchema() +_task_schema = models.TaskSchema() + + +def _project_to_mongodb_item(project): + document = _project_schema.dump(project) + return document + + +class ProjectNotFoundError(Exception): + def __init__(self, project_id): + message = f"Project not found: {project_id}" + self.project_id = project_id + super(ProjectNotFoundError, self).__init__(message) + + +class TaskNotFoundError(Exception): + def __init__(self, project_id, task_id): + message = f"Task not found: {project_id} - {task_id}" + self.project_id = project_id + self.task_id = task_id + super(TaskNotFoundError, self).__init__(message) + + +class ForbiddenOperationError(Exception): + def __init__(self, message): + self.message = message + super(ForbiddenOperationError, self).__init__(message) + + +class Database(object): + """ + Connection object to encapsulate the MongoDB database + """ + + def __init__(self): + self.client = None + + def open(self): + if self.client: + raise RuntimeError("Cannot open connection: Connection already open.") + self.client = MongoClient(DATABASE_HOST, DATABASE_PORT) + return self + + def close(self): + if self.client is None: + raise RuntimeError("Cannot close connection: Connection already closed.") + self.client.close() + self.client = None + + def __enter__(self): + return self.open() + + def __exit__(self, *args): + self.close() + + @property + def database(self): + return self.client.get_database(DATABASE_NAME) + + @property + def projects(self): + return self.database.projects + + def _update_project_fields(self, project_id, key_value_dict, operation="set"): + """ + Update a single field + + Args: + project_id (int): Id of the project. + key_value_dict (dict): Dictionary of keys and values to update. + """ + forbidden_fields = ("modified", "project_id", "created") + operations = { + "set": "$set", + "add": "$push", + "delete": "$pull", + } + + if any(field in key_value_dict for field in forbidden_fields): + message = f"Following fields may not be updated: {', '.join(field for field in key_value_dict if field in forbidden_fields)}" + raise ForbiddenOperationError(message) + + mongo_operation = operations[operation] + update_res = self.projects.update_one({"project_id": project_id}, + { + mongo_operation: key_value_dict + }) + # Raise error if no projects were affected + # This means the project id does not exist + if operation == "delete": + affected_count = update_res.deleted_count + else: + affected_count = update_res.matched_count + if affected_count == 0: + raise ProjectNotFoundError(project_id=project_id) + + # Update modified date + if operation != "delete": + self._update_project_modified_date(project_id=project_id) + + def _update_project_modified_date(self, project_id): + """ + Update the modified date of a project. + """ + # Update modified data of project + modified_date = time() + self.projects.update_one({"project_id": project_id}, + { + "$set": + { + "modified": str(modified_date), + }, + }) + return modified_date + + def insert_project(self, project): + # Find current maximum project id + max_existing = self.projects.find_one(sort=[("project_id", DESCENDING)]) + if max_existing: + new_id = max_existing["project_id"] + 1 + else: + new_id = 1 + # Insert new id into project + project.project_id = new_id + # Insert created and modified date + project.created = project.modified = time() + # If the project has tasks, give them ids and modified/create dates + if project.tasks: + for i, task in enumerate(project.tasks, 1): + task.created = task.modified = project.created + task.task_id = i + # Serialize the project, while taking care that the id is correctly used by MongoDB + document = _project_to_mongodb_item(project=project) + # Insert into database + self.projects.insert_one(document) + + return new_id + + def update_project_values(self, project_id, key_value_dict): + self._update_project_fields(project_id=project_id, + key_value_dict=key_value_dict, + operation="set") + + def get_project_by_id(self, project_id): + """ + Get a project with a given id. + + Args: + project_id (int): Id of the project. + + Returns: + atlas_server.models.Project + """ + project = self.projects.find_one({"project_id": project_id}) + return _project_schema.load(project, unknown=True) + + def get_projects_for_user(self, username): + projects = list(self.projects.find({"owner": username})) + # noinspection PyTypeChecker + return _project_schema.load(projects, many=True, unknown=True) + + def delete_project(self, project_id): + deleted_res = self.projects.delete_one({"project_id": project_id}) + if deleted_res.deleted_count == 0: + raise ProjectNotFoundError(project_id=project_id) + + def _update_task_fields(self, project_id, task_id, key_value_dict, operation="set"): + """ + Update a single field + + Args: + project_id (int): Id of the project. + key_value_dict (dict): Dictionary of keys and values to update. + """ + forbidden_fields = ("modified", "task_id", "created") + operations = { + "set": "$set", + "add": "$push", + "delete": "$pull" + } + + if any(field in key_value_dict for field in forbidden_fields): + message = f"Following fields may not be updated: {', '.join(field for field in key_value_dict if field in forbidden_fields)}" + raise ForbiddenOperationError(message) + + mongo_operation = operations[operation] + # Modify the update dict to match the update syntax. We prefix "tasks.$." before each key + update_dict = {f"tasks.$.{key}": value for key, value in key_value_dict.items()} + update_res = self.projects.update_one({"project_id": project_id, "tasks.task_id": task_id, }, {mongo_operation: update_dict}) + # Raise error if no projects were affected + # This means the project id does not exist + if operation == "delete": + affected_count = update_res.deleted_count + else: + affected_count = update_res.matched_count + if affected_count == 0: + raise TaskNotFoundError(project_id=project_id, task_id=task_id) + + # Update modified date (if we did not delete the task) + if operation != "delete": + self._update_task_modified_date(project_id=project_id, task_id=task_id) + + def _update_task_modified_date(self, project_id, task_id): + """ + Update the modified date of a task. + """ + # Update modified data of project + modified_date = time() + self.projects.update_one({"project_id": project_id, "tasks.task_id": task_id, }, + { + "$set": + { + "task.$.modified": str(modified_date), + }, + }) + return modified_date + + # noinspection PyUnresolvedReferences + def add_task_to_project(self, project_id, task): + # Get the project and find out how many tasks it already has + project = self.get_project_by_id(project_id=project_id) + if project is None: + raise ProjectNotFoundError(project_id) + if not project.tasks: + new_task_id = 1 + else: + new_task_id = max(task.task_id for task in project.tasks) + 1 + task.task_id = new_task_id + # Add created and modified info to task + task.created = time() + task.modified = time() + # Serialize task + document = _task_schema.dump(task) + # Insert task into the projects task list + self._update_project_fields(project_id=project_id, + key_value_dict={ + "tasks": document + }, + operation="add") + # Return id of new task + return new_task_id + + def get_task_by_id(self, project_id, task_id): + res = self.projects.find_one( + {"project_id": project_id, + "tasks.task_id": task_id + }, + # Projection, only get the specific task + { + "tasks.$": 1, + } + ) + if not res: + raise TaskNotFoundError(project_id=project_id, task_id=task_id) + return _task_schema.load(res["tasks"][0]) + + def update_task_values(self, project_id, task_id, key_value_dict): + self._update_task_fields(project_id=project_id, + task_id=task_id, + key_value_dict=key_value_dict, + operation="set") + + def delete_task(self, project_id, task_id): + # delete_res = self.projects. + self._update_task_fields(project_id=project_id, + task_id=task_id, + key_value_dict={}, + operation="delete") diff --git a/atlas_server/atlas_server/models.py b/atlas_server/atlas_server/models.py new file mode 100644 index 0000000000000000000000000000000000000000..8526a51a41f921b572f5e2fac070ebff9274de40 --- /dev/null +++ b/atlas_server/atlas_server/models.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +""" +Model classes for ATLaS UI +""" +from marshmallow import Schema, fields, post_load + + +class TaskSchema(Schema): + task_id = fields.Integer() + name = fields.String() + created = fields.Integer() + modified = fields.Integer() + annotations = fields.List(fields.String()) + + @post_load + def create_task(self, data, **kwargs): + return Task(**data) + + +class ProjectSchema(Schema): + project_id = fields.Integer() + name = fields.String() + brain = fields.Integer() + owner = fields.String() + tasks = fields.List(fields.Nested(TaskSchema)) + created = fields.Integer() + modified = fields.Integer() + + @post_load + def create_project(self, data, **kwargs): + return Project(**data) + + +class Project(object): + def __init__(self, + project_id=None, + name=None, + brain=None, + owner=None, + tasks=None, + created=None, + modified=None): + self.project_id = project_id + self.name = name + self.brain = brain + self.owner = owner + self.tasks = tasks + + if self.name is None: + self.name = "Untitled project" + + if self.tasks is None: + self.tasks = [] + + self.created = created + self.modified = modified + + def __repr__(self): + return f"Project(project_id={self.project_id}, name={self.name}, owner={self.owner})" + + +class Task(object): + def __init__(self, + task_id=None, + name=None, + created=None, + modified=None, + annotations=None): + self.task_id = task_id + self.name = name + self.annotations = annotations + + if self.name is None: + self.name = "Untitled task" + + if self.annotations is None: + self.annotations = [] + + self.created = created + self.modified = modified + + def __repr__(self): + return f"Task(task_id={self.task_id}, name={self.name})" diff --git a/atlas_server/atlas_server/old_app.py b/atlas_server/atlas_server/old_app.py new file mode 100644 index 0000000000000000000000000000000000000000..7a46790dfdb7ce1e0a6cede0f9dcb292ca839aa9 --- /dev/null +++ b/atlas_server/atlas_server/old_app.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +import requests +from bson.objectid import ObjectId +from flask import Flask, request +from flask_cors import CORS +from flask_restful import Resource, Api +from pymongo import MongoClient + +# DATABASE_HOST = "medpc018.ime.kfa-juelich.de" +DATABASE_HOST = "localhost" +DATABASE_PORT = 27017 +DATABASE_NAME = "atlas_ui_db" +# UPLOAD_ROOT = "/home/cschiffer/code/atlasui_prototype/atlas_controller/events" + +ATLAS_CONTROLLER_URL = "http://localhost:8000/api" +ATLAS_CONTROLLER_SETUP_TRAINING_ENDPOINT = f"{ATLAS_CONTROLLER_URL}/setup_training" + +app = Flask(__name__) +CORS(app) +api = Api(app) + + +class MissingIdError(Exception): + pass + + +class InvalidIdError(Exception): + pass + + +def create_client(): + return MongoClient(DATABASE_HOST, DATABASE_PORT) + + +def get_projects_table(client): + database = client.get_database(DATABASE_NAME) + projects = database.projects + return projects + + +def check_id(id_to_check): + with create_client() as client: + projects = get_projects_table(client) + if projects.find_one({"_id": ObjectId(id_to_check)}): + return True + else: + return False + + +def generate_id(): + with create_client() as client: + projects = get_projects_table(client) + inserted_element = projects.insert_one({}) + new_id = str(inserted_element.inserted_id) + return new_id + + +def validate_id_from_data(data): + # Check if the given id exists and is valid + if "id" not in data: + raise MissingIdError("Id is missing") + + id_to_check = data["id"] + if not check_id(id_to_check): + raise InvalidIdError(f"Id {id_to_check} is invalid. Please request a valid id.") + + return id_to_check + + +def find_project_for_id(id): + with create_client() as client: + projects = get_projects_table(client) + project = projects.find_one({"_id": ObjectId(id)}) + return project + + +def get_data_for_id(id): + with create_client() as client: + projects = get_projects_table(client) + return projects.find_one({"_id": ObjectId(id)}) + + +def add_data_for_id(id, data): + with create_client() as client: + projects = get_projects_table(client) + projects.update_one({"_id": ObjectId(id)}, data) + + +def add_annotation(id, annotation_name, annotation_path, image): + print(f"Adding annotation: {annotation_name}") + add_data_for_id(id, {"$push": { + "annotations": { + "name": annotation_name, + "path": annotation_path, + "image": image, + } + }}) + + +def get_json_data(): + assert request.is_json + data = request.get_json() + return data + + +# def upload_data(fname, data): +# fname = os.path.join(UPLOAD_ROOT, fname) +# # Write data +# with open(fname, "w") as f: +# json.dump(data, f, indent=4) + +class RequestId(Resource): + def post(self): + # TODO Authentication + # Generate entry for the database + new_id = generate_id() + print(f"Generated new ID: {new_id}") + + return {"id": new_id} + + +api.add_resource(RequestId, "/request_id") + + +class SubmitAnnotations(Resource): + def post(self): + data = get_json_data() + id = validate_id_from_data(data) + for annotation in data["annotations"]: + add_annotation(id=id, + annotation_name=annotation["annotation_name"], + annotation_path=annotation["annotation_path"], + image=annotation["image"]) + + +api.add_resource(SubmitAnnotations, "/submit_annotations") + + +class SetupTraining(Resource): + def post(self): + # Data is received as JSON + json_data = get_json_data() + # Validate ID + id = validate_id_from_data(json_data) + + # Get all fields we need + data_for_id = get_data_for_id(id) + annotations = json_data.get("annotations", None) + if annotations is None: + raise RuntimeError(f"Missing field: \"annotations\"") + + brain = json_data.get("brain", None) + if brain is None: + raise RuntimeError(f"Missing field: \"brain\"") + + # Create a file and upload it to the atlas controller + event_content = { + "id": id, + "brain": brain, + "annotations": annotations, + } + # Send to server + response = requests.post(ATLAS_CONTROLLER_SETUP_TRAINING_ENDPOINT, json=event_content) + return response.json() + + +api.add_resource(SetupTraining, "/setup_training") + +if __name__ == "__main__": + app.run(debug=True) diff --git a/atlas_server/atlas_server/wsgi_app.py b/atlas_server/atlas_server/wsgi_app.py index 64c52ba3491640ae43f9837a6cb28031a69e2232..15caddf10fcd176c16edb7779304452fc0690ac1 100644 --- a/atlas_server/atlas_server/wsgi_app.py +++ b/atlas_server/atlas_server/wsgi_app.py @@ -1,3 +1,5 @@ #!/usr/bin/env python3 import sys -sys.path.insert(0, "/home/cschiffer/code/atlasui_prototype/atlas_server") +sys.path.insert(0, "/home/cschiffer/code/atlasui/atlas_server") +# noinspection PyUnresolvedReferences +from atlas_server.app import app as application \ No newline at end of file diff --git a/config/httpd/httpd.conf b/config/httpd/httpd.conf index 5e9370514f69c65b35e41e4e5f68e64e0bcd4007..3bbf2aa10f6b87951d27e984f8de5dd25847c2f1 100644 --- a/config/httpd/httpd.conf +++ b/config/httpd/httpd.conf @@ -545,7 +545,7 @@ SSLRandomSeed connect builtin AddHandler php7-script php -WSGIScriptAlias /atlas_server /srv/http/atlas_server/wsgi_app.py +WSGIScriptAlias /atlas_server /srv/http/atlas_server/atlas_server/wsgi_app.py WSGIPythonPath /srv/http/atlas_server/:/home/cschiffer/miniconda3/envs/atlas_ui/lib/python3.7/site-packages WSGIScriptReloading On diff --git a/microdraw/atlas_ui.js b/microdraw/atlas_ui.js index 455ff5118c07972375de661dd90ba23b8e58ce6a..3e156dd113b5f2fcb175a9d52ae2c36455119d2e 100644 --- a/microdraw/atlas_ui.js +++ b/microdraw/atlas_ui.js @@ -56,9 +56,6 @@ function removeRegionFromList(region, list) { } var app = new Vue({ - components: { - ContextMenu, - }, el: '#atlas_app', data: { brain: 20, diff --git a/microdraw/lib/mylogin/login.js b/microdraw/lib/mylogin/login.js index 08562ca84f3c0e611ef676fde3019f9de894e864..888d918a5d7dbae21d538b870c0ef9b4c7472949 100755 --- a/microdraw/lib/mylogin/login.js +++ b/microdraw/lib/mylogin/login.js @@ -71,6 +71,7 @@ var MyLoginWidget = { }, sendLogin: function() { var me=this; + console.log($("#password").val()) $.get(root+"login.php",{"action":"login","username":$("#username").val(),"password":$("#password").val()},function(data){ try { var msg=JSON.parse(data);