Skip to content

Commit

Permalink
Merge pull request #1343 from Kathenae/kathenae-task_notification_plugin
Browse files Browse the repository at this point in the history
Task notification plugin
  • Loading branch information
pierotofy committed May 19, 2023
2 parents d8825e2 + 4417829 commit bdf5b33
Show file tree
Hide file tree
Showing 13 changed files with 412 additions and 2 deletions.
5 changes: 5 additions & 0 deletions app/models/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,11 @@ def callback(progress):
else:
# FAILED, CANCELED
self.save()

if self.status == status_codes.FAILED:
from app.plugins import signals as plugin_signals
plugin_signals.task_failed.send_robust(sender=self.__class__, task_id=self.id)

else:
# Still waiting...
self.save()
Expand Down
4 changes: 2 additions & 2 deletions app/plugins/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ def get_plugin_by_name(name, only_active=True, refresh_cache_if_none=False):
else:
return res

def get_current_plugin():
def get_current_plugin(only_active=False):
"""
When called from a python module inside a plugin's directory,
it returns the plugin that this python module belongs to
Expand All @@ -289,7 +289,7 @@ def get_current_plugin():
parts = relp.split(os.sep)
if len(parts) > 0:
plugin_name = parts[0]
return get_plugin_by_name(plugin_name, only_active=False)
return get_plugin_by_name(plugin_name, only_active=only_active)

return None

Expand Down
1 change: 1 addition & 0 deletions app/plugins/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
task_completed = django.dispatch.Signal(providing_args=["task_id"])
task_removing = django.dispatch.Signal(providing_args=["task_id"])
task_removed = django.dispatch.Signal(providing_args=["task_id"])
task_failed = django.dispatch.Signal(providing_args=["task_id"])

processing_node_removed = django.dispatch.Signal(providing_args=["processing_node_id"])
1 change: 1 addition & 0 deletions coreplugins/tasknotification/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.conf
2 changes: 2 additions & 0 deletions coreplugins/tasknotification/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .plugin import *
from . import signals
40 changes: 40 additions & 0 deletions coreplugins/tasknotification/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import os
import configparser

script_dir = os.path.dirname(os.path.abspath(__file__))

def load():
config = configparser.ConfigParser()
config.read(f"{script_dir}/.conf")
smtp_configuration = {
'smtp_server': config.get('SETTINGS', 'smtp_server', fallback=""),
'smtp_port': config.getint('SETTINGS', 'smtp_port', fallback=587),
'smtp_username': config.get('SETTINGS', 'smtp_username', fallback=""),
'smtp_password': config.get('SETTINGS', 'smtp_password', fallback=""),
'smtp_use_tls': config.getboolean('SETTINGS', 'smtp_use_tls', fallback=False),
'smtp_from_address': config.get('SETTINGS', 'smtp_from_address', fallback=""),
'smtp_to_address': config.get('SETTINGS', 'smtp_to_address', fallback=""),
'notification_app_name': config.get('SETTINGS', 'notification_app_name', fallback=""),
'notify_task_completed': config.getboolean('SETTINGS', 'notify_task_completed', fallback=False),
'notify_task_failed': config.getboolean('SETTINGS', 'notify_task_failed', fallback=False),
'notify_task_removed': config.getboolean('SETTINGS', 'notify_task_removed', fallback=False)
}
return smtp_configuration

def save(data : dict):
config = configparser.ConfigParser()
config['SETTINGS'] = {
'smtp_server': str(data.get('smtp_server')),
'smtp_port': str(data.get('smtp_port')),
'smtp_username': str(data.get('smtp_username')),
'smtp_password': str(data.get('smtp_password')),
'smtp_use_tls': str(data.get('smtp_use_tls')),
'smtp_from_address': str(data.get('smtp_from_address')),
'smtp_to_address': str(data.get('smtp_to_address')),
'notification_app_name': str(data.get('notification_app_name')),
'notify_task_completed': str(data.get('notify_task_completed')),
'notify_task_failed': str(data.get('notify_task_failed')),
'notify_task_removed': str(data.get('notify_task_removed'))
}
with open(f"{script_dir}/.conf", 'w') as configFile:
config.write(configFile)
Empty file.
29 changes: 29 additions & 0 deletions coreplugins/tasknotification/email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from django.core.mail import send_mail
from django.core.mail.backends.smtp import EmailBackend
from . import config


def send(subject : str, message : str, smtp_config : dict = None):

if not smtp_config:
smtp_config = config.load()

email_backend = EmailBackend(
smtp_config.get('smtp_server'),
smtp_config.get('smtp_port'),
smtp_config.get('smtp_username'),
smtp_config.get('smtp_password'),
smtp_config.get('smtp_use_tls'),
timeout=10
)

result = send_mail(
subject,
message,
smtp_config.get('smtp_from_address'),
[smtp_config.get('smtp_to_address')],
connection=email_backend,
auth_user = smtp_config.get('smtp_username'),
auth_password = smtp_config.get('smtp_password'),
fail_silently = False
)
17 changes: 17 additions & 0 deletions coreplugins/tasknotification/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "Task Notification",
"webodmMinVersion": "0.6.2",
"description": "Get notified when a task has finished processing, has been removed or has failed",
"version": "0.1.0",
"author": "Ronald W. Machado",
"email": "[email protected]",
"repository": "https://github.com/OpenDroneMap/WebODM",
"tags": [
"notification",
"email",
"smtp"
],
"homepage": "https://github.com/OpenDroneMap/WebODM",
"experimental": false,
"deprecated": false
}
114 changes: 114 additions & 0 deletions coreplugins/tasknotification/plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
from app.plugins import PluginBase, Menu, MountPoint
from app.models import Setting
from django.utils.translation import gettext as _
from django.shortcuts import render
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django import forms
from smtplib import SMTPAuthenticationError, SMTPConnectError, SMTPDataError
from . import email
from . import config

class ConfigurationForm(forms.Form):
notification_app_name = forms.CharField(
label='App name',
max_length=100,
required=True,
)
smtp_to_address = forms.EmailField(
label='Send Notification to Address',
max_length=100,
required=True
)
smtp_from_address = forms.EmailField(
label='From Address',
max_length=100,
required=True
)
smtp_server = forms.CharField(
label='SMTP Server',
max_length=100,
required=True
)
smtp_port = forms.IntegerField(
label='Port',
required=True
)
smtp_username = forms.CharField(
label='Username',
max_length=100,
required=True
)
smtp_password = forms.CharField(
label='Password',
max_length=100,
required=True
)
smtp_use_tls = forms.BooleanField(
label='Use Transport Layer Security (TLS)',
required=False,
)

notify_task_completed = forms.BooleanField(
label='Notify Task Completed',
required=False,
)
notify_task_failed = forms.BooleanField(
label='Notify Task Failed',
required=False,
)
notify_task_removed = forms.BooleanField(
label='Notify Task Removed',
required=False,
)

def test_settings(self, request):
try:
settings = Setting.objects.first()
email.send(f'{self.cleaned_data["notification_app_name"]} - Testing Notification', 'Hi, just testing if notification is working', self.cleaned_data)
messages.success(request, f"Email sent successfully, check your inbox at {self.cleaned_data.get('smtp_to_address')}")
except SMTPAuthenticationError as e:
messages.error(request, 'Invalid SMTP username or password')
except SMTPConnectError as e:
messages.error(request, 'Could not connect to the SMTP server')
except SMTPDataError as e:
messages.error(request, 'Error sending email. Please try again later')
except Exception as e:
messages.error(request, f'An error occured: {e}')

def save_settings(self):
config.save(self.cleaned_data)

class Plugin(PluginBase):
def main_menu(self):
return [Menu(_("Task Notification"), self.public_url(""), "fa fa-envelope fa-fw")]

def include_css_files(self):
return ['style.css']

def app_mount_points(self):

@login_required
def index(request):
if request.method == "POST":

form = ConfigurationForm(request.POST)
test_configuration = request.POST.get("test_configuration")
if form.is_valid() and test_configuration:
form.test_settings(request)
elif form.is_valid() and not test_configuration:
form.save_settings()
messages.success(request, "Notification settings applied successfully!")
else:
config_data = config.load()

# notification_app_name initial value should be whatever is defined in the settings
settings = Setting.objects.first()
config_data['notification_app_name'] = config_data['notification_app_name'] or settings.app_name
form = ConfigurationForm(initial=config_data)

return render(request, self.template_path('index.html'), {'form' : form, 'title' : 'Task Notification'})

return [
MountPoint('$', index),
]
11 changes: 11 additions & 0 deletions coreplugins/tasknotification/public/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.errorlist {
color: red;
list-style: none;
margin: 0;
padding: 0;
}

.errorlist li {
margin: 0;
padding: 0;
}
98 changes: 98 additions & 0 deletions coreplugins/tasknotification/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import logging
from django.dispatch import receiver
from django.core.mail import send_mail
from app.plugins.signals import task_completed, task_failed, task_removed
from app.plugins.functions import get_current_plugin
from . import email as notification
from . import config
from app.models import Task, Setting

logger = logging.getLogger('app.logger')

@receiver(task_completed)
def handle_task_completed(sender, task_id, **kwargs):
if get_current_plugin(only_active=True) is None:
return

logger.info("TaskNotification: Task Completed")

config_data = config.load()
if config_data.get("notify_task_completed") == True:
task = Task.objects.get(id=task_id)
setting = Setting.objects.first()
notification_app_name = config_data['notification_app_name'] or settings.app_name

console_output = reverse_output(task.console_output)
notification.send(
f"{notification_app_name} - {task.project.name} Task Completed",
f"{task.project.name}\n{task.name} Completed\nProcessing time:{hours_minutes_secs(task.processing_time)}\n\nConsole Output:{console_output}",
config_data
)

@receiver(task_removed)
def handle_task_removed(sender, task_id, **kwargs):
if get_current_plugin(only_active=True) is None:
return

logger.info("TaskNotification: Task Removed")

config_data = config.load()
if config_data.get("notify_task_removed") == True:
task = Task.objects.get(id=task_id)
setting = Setting.objects.first()
notification_app_name = config_data['notification_app_name'] or settings.app_name
console_output = reverse_output(task.console_output)
notification.send(
f"{notification_app_name} - {task.project.name} Task removed",
f"{task.project.name}\n{task.name} was removed\nProcessing time:{hours_minutes_secs(task.processing_time)}\n\nConsole Output:{console_output}",
config_data
)

@receiver(task_failed)
def handle_task_failed(sender, task_id, **kwargs):
if get_current_plugin(only_active=True) is None:
return

logger.info("TaskNotification: Task Failed")

config_data = config.load()
if config_data.get("notify_task_failed") == True:
task = Task.objects.get(id=task_id)
setting = Setting.objects.first()
notification_app_name = config_data['notification_app_name'] or settings.app_name
console_output = reverse_output(task.console_output)
notification.send(
f"{notification_app_name} - {task.project.name} Task Failed",
f"{task.project.name}\n{task.name} Failed with error: {task.last_error}\nProcessing time:{hours_minutes_secs(task.processing_time)}\n\nConsole Output:{console_output}",
config_data
)

def hours_minutes_secs(milliseconds):
if milliseconds == 0 or milliseconds == -1:
return "-- : -- : --"

ch = 60 * 60 * 1000
cm = 60 * 1000
h = milliseconds // ch
m = (milliseconds - h * ch) // cm
s = round((milliseconds - h * ch - m * cm) / 1000)
pad = lambda n: '0' + str(n) if n < 10 else str(n)

if s == 60:
m += 1
s = 0
if m == 60:
h += 1
m = 0

return ':'.join([pad(h), pad(m), pad(s)])

def reverse_output(output_string):
# Split the output string into lines, then reverse the order
lines = output_string.split('\n')
lines.reverse()

# Join the reversed lines back into a single string with newlines
reversed_string = '\n'.join(lines)

return reversed_string
Loading

0 comments on commit bdf5b33

Please sign in to comment.