Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Task notification plugin #1343

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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