Skip to content

Commit

Permalink
release v0.7.0
Browse files Browse the repository at this point in the history
  • Loading branch information
kikkomep committed Feb 10, 2022
2 parents 2f49426 + a7cfb8c commit a5ba842
Show file tree
Hide file tree
Showing 11 changed files with 416 additions and 53 deletions.
4 changes: 2 additions & 2 deletions k8s/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.6.0
version: 0.7.0

# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
appVersion: 0.6.0
appVersion: 0.7.0

# Chart dependencies
dependencies:
Expand Down
32 changes: 32 additions & 0 deletions lifemonitor/api/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,22 @@ def suites_post(wf_uuid, wf_version, body):
return {'wf_uuid': str(suite.uuid)}, 201


@authorized
def suites_put(suite_uuid, body):
try:
suite = _get_suite_or_problem(suite_uuid)
if isinstance(suite, Response):
return suite
suite.name = body.get('name', suite.name)
suite.save()
clear_cache()
logger.debug("Suite %r updated", suite_uuid)
return connexion.NoContent, 204
except Exception as e:
return lm_exceptions.report_problem(500, "Internal Error", extra_info={"exception": str(e)},
detail=messages.unable_to_delete_suite.format(suite_uuid))


@authorized
def suites_delete(suite_uuid):
try:
Expand Down Expand Up @@ -650,6 +666,22 @@ def instances_get_by_id(instance_uuid):
else serializers.TestInstanceSchema().dump(response)


@authorized
def instances_put(instance_uuid, body):
try:
instance = _get_instances_or_problem(instance_uuid)
if isinstance(instance, Response):
return instance
instance.name = body.get('name', instance.name)
instance.save()
clear_cache()
logger.debug("Instance %r updated", instance_uuid)
return connexion.NoContent, 204
except Exception as e:
return lm_exceptions.report_problem(500, "Internal Error", extra_info={"exception": str(e)},
detail=messages.unable_to_delete_suite.format(instance_uuid))


@authorized
def instances_delete_by_id(instance_uuid):
try:
Expand Down
6 changes: 5 additions & 1 deletion lifemonitor/api/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
# 'testsuites' package
from .testsuites import TestSuite, TestInstance, ManagedTestInstance, BuildStatus, TestBuild

# notifications
from .notifications import WorkflowStatusNotification

# 'testing_services'
from .services import TestingService, \
GithubTestingService, GithubTestBuild, \
Expand Down Expand Up @@ -71,8 +74,9 @@
"WorkflowRegistry",
"WorkflowRegistryClient",
"WorkflowStatus",
"WorkflowStatusNotification",
"WorkflowVersion",
"RegistryWorkflow"
"RegistryWorkflow",
]

# set module level logger
Expand Down
86 changes: 86 additions & 0 deletions lifemonitor/api/models/notifications.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Copyright (c) 2020-2021 CRS4
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.


import logging
from typing import List

from flask import render_template
from flask_mail import Message
from lifemonitor.auth.models import (EventType, Notification, User,
UserNotification)
from lifemonitor.db import db
from lifemonitor.utils import Base64Encoder
from sqlalchemy.exc import InternalError

from . import TestInstance

# set logger
logger = logging.getLogger(__name__)


class WorkflowStatusNotification(Notification):

__mapper_args__ = {
'polymorphic_identity': 'workflow_status_notification'
}

@property
def get_icon_path(self) -> str:
return 'lifemonitor/static/img/icons/' \
+ ('times-circle-solid.svg'
if self.event == EventType.BUILD_FAILED else 'check-circle-solid.svg')

def to_mail_message(self, recipients: List[User]) -> Message:
from lifemonitor.mail import mail
build_data = self.data['build']
try:
i = TestInstance.find_by_uuid(build_data['instance']['uuid'])
if i is not None:
wv = i.test_suite.workflow_version
b = i.get_test_build(build_data['build_id'])
suite = i.test_suite
suite.url_param = Base64Encoder.encode_object({
'workflow': str(wv.workflow.uuid),
'suite': str(suite.uuid)
})
instance_status = "is failing" \
if self.event == EventType.BUILD_FAILED else "has recovered"
msg = Message(
f'Workflow "{wv.name} ({wv.version})": test instance {i.name} {instance_status}',
bcc=recipients,
reply_to=self.reply_to
)
msg.html = render_template("mail/instance_status_notification.j2",
webapp_url=mail.webapp_url,
workflow_version=wv, build=b,
test_instance=i,
suite=suite,
json_data=build_data,
logo=self.base64Logo, icon=self.encodeFile(self.get_icon_path))
return msg
except InternalError as e:
logger.debug(e)
db.session.rollback()

@classmethod
def find_by_user(cls, user: User) -> List[Notification]:
return cls.query.join(UserNotification, UserNotification.notification_id == cls.id)\
.filter(UserNotification.user_id == user.id).all()
57 changes: 57 additions & 0 deletions lifemonitor/auth/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ class EventType(Enum):
ALL = 0
BUILD_FAILED = 1
BUILD_RECOVERED = 2
UNCONFIGURED_EMAIL = 3

@classmethod
def all(cls):
Expand Down Expand Up @@ -480,17 +481,30 @@ class Notification(db.Model, ModelMixin):
name = db.Column("name", db.String, nullable=True, index=True)
_event = db.Column("event", db.Integer, nullable=False)
_data = db.Column("data", JSON, nullable=True)
_type = db.Column("type", db.String, nullable=False)

users: List[UserNotification] = db.relationship("UserNotification",
back_populates="notification", cascade="all, delete-orphan")

__mapper_args__ = {
'polymorphic_on': _type,
'polymorphic_identity': 'generic'
}

def __init__(self, event: EventType, name: str, data: object, users: List[User]) -> None:
self.name = name
self._event = event.value
self._data = data
for u in users:
self.add_user(u)

def __repr__(self) -> str:
return f"{self.__class__.__name__} ({self.id})"

@property
def reply_to(self) -> str:
return "noreply-lifemonitor@crs4.it"

@property
def event(self) -> EventType:
return EventType(self._event)
Expand All @@ -506,6 +520,25 @@ def add_user(self, user: User):
def remove_user(self, user: User):
self.users.remove(user)

def to_mail_message(self, recipients: List[User]) -> str:
return None

@property
def base64Logo(self) -> str:
try:
return lm_utils.Base64Encoder.encode_file('lifemonitor/static/img/logo/lm/LifeMonitorLogo.png')
except Exception as e:
logger.debug(e)
return None

@staticmethod
def encodeFile(file_path: str) -> str:
try:
return lm_utils.Base64Encoder.encode_file(file_path)
except Exception as e:
logger.debug(e)
return None

@classmethod
def find_by_name(cls, name: str) -> List[Notification]:
return cls.query.filter(cls.name == name).all()
Expand All @@ -520,6 +553,30 @@ def not_emailed(cls) -> List[Notification]:
return cls.query.join(UserNotification, UserNotification.notification_id == cls.id)\
.filter(UserNotification.emailed == null()).all()

@classmethod
def older_than(cls, date: datetime) -> List[Notification]:
return cls.query.filter(Notification.created < date).all()

@classmethod
def find_by_user(cls, user: User) -> List[Notification]:
return cls.query.join(UserNotification, UserNotification.notification_id == cls.id)\
.filter(UserNotification.user_id == user.id).all()


class UnconfiguredEmailNotification(Notification):

__mapper_args__ = {
'polymorphic_identity': 'unconfigured_email'
}

def __init__(self, name: str, data: object = None, users: List[User] = None) -> None:
super().__init__(EventType.UNCONFIGURED_EMAIL, name, data, users)

@classmethod
def find_by_user(cls, user: User) -> List[Notification]:
return cls.query.join(UserNotification, UserNotification.notification_id == cls.id)\
.filter(UserNotification.user_id == user.id).all()


class UserNotification(db.Model):

Expand Down
51 changes: 12 additions & 39 deletions lifemonitor/mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,10 @@

from flask import Flask, render_template
from flask_mail import Mail, Message
from sqlalchemy.exc import InternalError

from lifemonitor.api.models import TestInstance
from lifemonitor.auth.models import EventType, Notification, User
from lifemonitor.db import db
from lifemonitor.utils import Base64Encoder, get_external_server_url, boolean_value
from lifemonitor.auth.models import Notification, User
from lifemonitor.utils import (Base64Encoder, boolean_value,
get_external_server_url)

# set logger
logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -70,46 +68,21 @@ def send_email_validation_message(user: User):
conn.send(msg)


def send_notification(n: Notification, recipients: List[str]) -> Optional[datetime]:
def send_notification(n: Notification, recipients: List[str] = None) -> Optional[datetime]:
if mail.disabled:
logger.info("Mail notifications are disabled")
else:
with mail.connect() as conn:
logger.debug("Mail recipients for notification '%r': %r", n.id, recipients)
if not recipients:
recipients = [
u.user.email for u in n.users
if u.emailed is None and u.user.email_notifications_enabled and u.user.email
]
if len(recipients) > 0:
build_data = n.data['build']
try:
i = TestInstance.find_by_uuid(build_data['instance']['uuid'])
if i is not None:
wv = i.test_suite.workflow_version
b = i.get_test_build(build_data['build_id'])
suite = i.test_suite
logo = Base64Encoder.encode_file('lifemonitor/static/img/logo/lm/LifeMonitorLogo.png')
icon_path = 'lifemonitor/static/img/icons/' \
+ ('times-circle-solid.svg'
if n.event == EventType.BUILD_FAILED else 'check-circle-solid.svg')
icon = Base64Encoder.encode_file(icon_path)
suite.url_param = Base64Encoder.encode_object({
'workflow': str(wv.workflow.uuid),
'suite': str(suite.uuid)
})
instance_status = "is failing" \
if n.event == EventType.BUILD_FAILED else "has recovered"
msg = Message(
f'Workflow "{wv.name} ({wv.version})": test instance {i.name} {instance_status}',
bcc=recipients,
reply_to="noreply-lifemonitor@crs4.it"
)
msg.html = render_template("mail/instance_status_notification.j2",
webapp_url=mail.webapp_url,
workflow_version=wv, build=b,
test_instance=i,
suite=suite,
json_data=build_data,
logo=logo, icon=icon)
conn.send(msg)
return datetime.utcnow()
except InternalError as e:
conn.send(n.to_mail_message(recipients))
return datetime.utcnow()
except Exception as e:
logger.debug(e)
db.session.rollback()
return None
2 changes: 1 addition & 1 deletion lifemonitor/static/src/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "lifemonitor",
"description": "Workflow Testing Service",
"version": "0.6.0",
"version": "0.7.0",
"license": "MIT",
"author": "CRS4",
"main": "../dist/js/lifemonitor.min.js",
Expand Down
Loading

0 comments on commit a5ba842

Please sign in to comment.