From 09f7d7ac68f521359c928f92e0532951d7e148fc Mon Sep 17 00:00:00 2001
From: Stephan Porada <sporada@uni-bielefeld.de>
Date: Tue, 17 Sep 2019 14:36:15 +0200
Subject: [PATCH] Add delete functions for jobs and users.

---
 app/auth/views.py                           | 34 +++++++++++++++---
 app/main/views.py                           | 38 +++++++++++++--------
 app/models.py                               | 17 +++++----
 app/templates/admin/admin_user_page.html.j2 |  3 +-
 4 files changed, 67 insertions(+), 25 deletions(-)

diff --git a/app/auth/views.py b/app/auth/views.py
index 7396e225..dd327cfb 100644
--- a/app/auth/views.py
+++ b/app/auth/views.py
@@ -1,10 +1,14 @@
-from flask import flash, redirect, render_template, request, url_for
+from flask import (flash, redirect, render_template, request, url_for,
+                   current_app)
 from flask_login import current_user, login_required, login_user, logout_user
 from . import auth
 from .. import db
-from .forms import ChangePasswordForm, LoginForm, PasswordResetForm, PasswordResetRequestForm, RegistrationForm, EditProfileForm
+from .forms import (ChangePasswordForm, LoginForm, PasswordResetForm,
+                    PasswordResetRequestForm, RegistrationForm, EditProfileForm)
 from ..email import send_email
 from ..models import User, Job
+import logging
+import threading
 
 
 @auth.route('/login', methods=['GET', 'POST'])
@@ -165,8 +169,30 @@ def settings():
 @auth.route('/settings/delete_self', methods=['GET', 'POST'])
 @login_required
 def delete_self():
-    user = current_user
-    db.session.delete(user)
+    logger = logging.getLogger(__name__)
+
+    def background_delete(app, current_user_id):
+        with app.app_context():
+            logger.warning('Called by delete_thread.')
+            logger.warning('User id is: {}.'.format(current_user_id))
+            jobs = Job.query.join(User).filter_by(id=current_user_id).all()
+            logger.warning('Jobs to delete are: {}'.format(jobs))
+            for job in jobs:
+                job.flag_for_stop()
+                logger.warning('Job status: {}'.format(job.status))
+                deleted = False
+                while deleted is False:
+                    db.session.refresh(job)
+                    if job.status == 'deleted':
+                        logger.warning('Job status is deleted.')
+                        job.delete_job()
+                        deleted = True
+
+    delete_thread = threading.Thread(target=background_delete,
+                                     args=(current_app._get_current_object(),
+                                           current_user.id))
+    delete_thread.start()
+    db.session.delete(current_user)
     db.session.commit()
     flash('Your account has been deleted!')
     return redirect(url_for('main.index'))
diff --git a/app/main/views.py b/app/main/views.py
index 5e81bbd8..51ba514e 100644
--- a/app/main/views.py
+++ b/app/main/views.py
@@ -7,7 +7,7 @@ from .. import db
 from ..models import Corpus, Job
 import os
 import logging
-import time
+import threading
 
 
 @main.route('/')
@@ -145,17 +145,27 @@ def job_download(job_id):
 @login_required
 def delete_job(job_id):
     logger = logging.getLogger(__name__)
-    logger.warning(job_id)
-    job = Job.query.filter_by(id=job_id).first()
-    logger.warning('Job status: {}'.format(job.status))
-    job.flag_for_stop()
-    logger.warning('Job status: {}'.format(job.status))
-    deleted = False
-    while deleted is False:
-        db.session.refresh(job)
-        if job.status == 'deleted':
-            logger.warning('Job status is deleted.')
-            time.sleep(5)  # Wait 5 seconds before deleteing job and job files
-            job.delete_job()  # See delete_job() method for further explanation
-            deleted = True
+
+    def background_delete(job_id, app):
+        with app.app_context():
+            logger.warning('Called by delete_thread.')
+            logger.warning('Job id is: {}.'.format(job_id))
+            job = Job.query.filter_by(id=job_id).first()
+            logger.warning('Job object is: {}'.format(job))
+            logger.warning('Job status: {}'.format(job.status))
+            job.flag_for_stop()
+            logger.warning('Job status: {}'.format(job.status))
+            deleted = False
+            while deleted is False:
+                db.session.refresh(job)
+                if job.status == 'deleted':
+                    logger.warning('Job status is deleted.')
+                    job.delete_job()
+                    deleted = True
+
+    delete_thread = threading.Thread(target=background_delete,
+                                     args=(job_id,
+                                           current_app._get_current_object()))
+    delete_thread.start()
+    flash('Job has been deleted!')
     return redirect(url_for('main.dashboard'))
diff --git a/app/models.py b/app/models.py
index 494d0b03..3d4d4f0b 100644
--- a/app/models.py
+++ b/app/models.py
@@ -300,18 +300,23 @@ class Job(db.Model):
     def delete_job(self):
         """
         Delete job with given job id from database. Also delete associated job
-        files. Wait 5 seconds after service has been flaged for stopping and
-        deleted. This method can only be used if the containers have been
-        totally stopped. Contianers are still running for a few seconds after
-        the associated service has been removed.
+        files. Contianers are still running for a few seconds after
+        the associated service has been removed. This is the reason for the
+        while loop. The loop checks if the file path to all the job files still
+        exists and removes it again and again till the container did shutdown
+        for good.
         See: https://docs.docker.com/engine/swarm/swarm-tutorial/delete-service/
         """
         logger = logging.getLogger(__name__)
         delete_path = os.path.join('/mnt/opaque/', str(self.user_id), 'jobs',
                                    str(self.id))
         logger.warning('Delete path is: {}'.format(delete_path))
-        if os.path.exists(delete_path):
-            shutil.rmtree(delete_path)
+        while os.path.exists(delete_path):
+            try:
+                shutil.rmtree(delete_path, ignore_errors=True)
+                logger.warning('Path does still exist.')
+            except OSError:
+                pass
         db.session.delete(self)
         db.session.commit()
 
diff --git a/app/templates/admin/admin_user_page.html.j2 b/app/templates/admin/admin_user_page.html.j2
index ab4772f1..8a2204f7 100644
--- a/app/templates/admin/admin_user_page.html.j2
+++ b/app/templates/admin/admin_user_page.html.j2
@@ -76,7 +76,8 @@
       <div id="modal-confirm-delete" class="modal">
         <div class="modal-content">
           <h4>Confirm deletion</h4>
-            <p>Do you really want to delete the current selected user ({{selected_user.username}})?</p>
+            <p>Do you really want to delete the current selected user ({{selected_user.username}})?
+            All associated jobs and job files will be permanently deleted.</p>
         </div>
         <div class="modal-footer">
           <a href="{{url_for('admin.admin_delete_user', user_id=selected_user.id)}}" class="modal-close waves-effect waves-green btn red"><i class="material-icons left">delete</i>Delete User</a></a>
-- 
GitLab