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

RestAPI hooks : allow post hook for submit, edit, delete, validation status change #4812

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
29 changes: 27 additions & 2 deletions jsapp/js/components/RESTServices/RESTServiceLogs.es6
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import mixins from '../../mixins';
import {dataInterface} from '../../dataInterface';
import {formatTime, notify} from 'utils';
import {
HOOK_LOG_EVENT,
HOOK_LOG_STATUSES,
MODAL_TYPES
} from '../../constants';
Expand All @@ -26,6 +27,7 @@ export default class RESTServiceLogs extends React.Component {
hookUid: props.hookUid,
isLoadingHook: true,
isLoadingLogs: true,
event: HOOK_LOG_EVENT.SUBMIT,
logs: [],
nextPageUrl: null
};
Expand All @@ -43,7 +45,8 @@ export default class RESTServiceLogs extends React.Component {
this.setState({
isLoadingHook: false,
hookName: data.name,
isHookActive: data.active
isHookActive: data.active,
event: data.event,
});
})
.fail(() => {
Expand All @@ -62,7 +65,8 @@ export default class RESTServiceLogs extends React.Component {
isLoadingLogs: false,
logs: data.results,
nextPageUrl: data.next,
totalLogsCount: data.count
totalLogsCount: data.count,
event: data.event,
});
},
onFail: () => {
Expand Down Expand Up @@ -261,6 +265,7 @@ export default class RESTServiceLogs extends React.Component {
<bem.FormView__cell m={['box']}>
<bem.ServiceRow m='header'>
<bem.ServiceRow__column m='submission'>{t('Submission')}</bem.ServiceRow__column>
<bem.ServiceRow__column m='event'>{t('Event')}</bem.ServiceRow__column>
<bem.ServiceRow__column m='status'>
{t('Status')}
{ this.hasAnyFailedLogs() &&
Expand Down Expand Up @@ -302,12 +307,32 @@ export default class RESTServiceLogs extends React.Component {
statusLabel = t('Failed');
}

let eventLabel = '';
switch(log.event) {
case HOOK_LOG_EVENT.SUBMIT:
eventLabel = t('Submission')
break;
case HOOK_LOG_EVENT.EDIT:
eventLabel = t('Edit')
break;
case HOOK_LOG_EVENT.DELETE:
eventLabel = t('Deletion')
break;
case HOOK_LOG_EVENT.VALIDATION:
eventLabel = t('Validation status')
break;
}

return (
<bem.ServiceRow {...rowProps}>
<bem.ServiceRow__column m='submission'>
{log.submission_id}
</bem.ServiceRow__column>

<bem.ServiceRow__column m='submission'>
{eventLabel}
</bem.ServiceRow__column>

<bem.ServiceRow__column
m={['status', statusMod]}
>
Expand Down
62 changes: 61 additions & 1 deletion jsapp/js/components/RESTServices/RESTServicesForm.es6
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import TextBox from 'js/components/common/textBox';
import {KEY_CODES} from 'js/constants';
import envStore from 'js/envStore';
import {notify} from 'js/utils';
import "./RESTServicesForm.scss"
import pageState from 'js/pageState.store';
import Button from 'js/components/common/button';

Expand Down Expand Up @@ -56,6 +57,12 @@ export default class RESTServicesForm extends React.Component {
EXPORT_TYPES.xml
],
isActive: true,
onEvent : {
onSubmit: true,
onEdit: false,
onDelete: false,
onValidation: false
},
emailNotification: true,
authLevel: null,
authOptions: [
Expand Down Expand Up @@ -83,6 +90,7 @@ export default class RESTServicesForm extends React.Component {
name: data.name,
endpoint: data.endpoint,
isActive: data.active,
onEvent: data.on_event,
emailNotification: data.email_notification,
subsetFields: data.subset_fields || [],
type: data.export_type,
Expand Down Expand Up @@ -173,6 +181,20 @@ export default class RESTServicesForm extends React.Component {
this.setState({emailNotification: isChecked});
}

handleMethodOnSubmitChange(isChecked) {
this.setState({ onEvent: {...this.state.onEvent, onSubmit: isChecked }})
}
handleMethodOnEditChange(isChecked) {
this.setState({ onEvent: {...this.state.onEvent, onEdit: isChecked }})
}
handleMethodOnDeleteChange(isChecked) {
this.setState({ onEvent: {...this.state.onEvent, onDelete: isChecked }})
console.log(this.state.onEvent)
}
handleMethodOnValidationChange(isChecked) {
this.setState({ onEvent: {...this.state.onEvent, onValidation: isChecked }})
}

handleTypeRadioChange(value, name) {this.setState({[name]: value});}

handleCustomHeaderChange(evt) {
Expand Down Expand Up @@ -211,6 +233,7 @@ export default class RESTServicesForm extends React.Component {
endpoint: this.state.endpoint,
active: this.state.isActive,
subset_fields: this.state.subsetFields,
on_event : this.state.onEvent,
email_notification: this.state.emailNotification,
export_type: this.state.type,
auth_level: authLevel,
Expand Down Expand Up @@ -453,7 +476,44 @@ export default class RESTServicesForm extends React.Component {
label={t('Enabled')}
/>
</bem.FormModal__item>

<bem.FormModal__item>
<div className='res-service-editor__sub-row'>
<Checkbox
name='onSubmit'
onChange={this.handleMethodOnSubmitChange.bind(this)}
checked={this.state.onEvent.onSubmit}
label={t('On submit')}
disabled={!this.state.isActive}
/>
</div>
<div className='res-service-editor__sub-row'>
<Checkbox
name='onEdit'
onChange={this.handleMethodOnEditChange.bind(this)}
checked={this.state.onEvent.onEdit}
label={t('On edit')}
disabled={!this.state.isActive}
/>
</div>
<div className='res-service-editor__sub-row'>
<Checkbox
name='onDelete'
onChange={this.handleMethodOnDeleteChange.bind(this)}
checked={this.state.onEvent.onDelete}
label={t('On delete')}
disabled={!this.state.isActive}
/>
</div>
<div className='res-service-editor__sub-row'>
<Checkbox
name='onValidationChange'
onChange={this.handleMethodOnValidationChange.bind(this)}
checked={this.state.onEvent.onValidation}
label={t('On validation change')}
disabled={!this.state.isActive}
/>
</div>
</bem.FormModal__item>
<bem.FormModal__item>
<Checkbox
name='emailNotification'
Expand Down
6 changes: 6 additions & 0 deletions jsapp/js/components/RESTServices/RESTServicesForm.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@use "scss/mixins";
@use "scss/libs/_mdl";

.res-service-editor__sub-row {
@include mixins.form-subrow;
}
7 changes: 7 additions & 0 deletions jsapp/js/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ export const HOOK_LOG_STATUSES = {
FAILED: 0,
};

export const HOOK_LOG_EVENT = {
SUBMIT: 'on_submit',
EDIT: 'on_edit',
DELETE: 'on_delete',
VALIDATION: 'on_validation_status_change',
}

export const KEY_CODES = Object.freeze({
TAB: 9,
ENTER: 13,
Expand Down
3 changes: 2 additions & 1 deletion jsapp/scss/components/_kobo.service-row.scss
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ $s-service-row-column-spacing: 10px;
}

&.service-row__column--submission,
&.service-row__column--status {
&.service-row__column--status,
&.service-row__column--event {
flex: 1;
padding: 0 $s-service-row-column-spacing;
white-space: nowrap;
Expand Down
18 changes: 18 additions & 0 deletions jsapp/scss/mixins.scss
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,21 @@
outline: sizes.$x3 solid colors.$kobo-mid-blue !important;
border-color: colors.$kobo-blue !important;
}

@mixin form-subrow() {
padding-left: 30px;
padding-block: 2px;
position: relative;

&::before {
content: '';
position: absolute;
top: 0;
left: 10px;
width: 15px;
height: 15px;
border-left: 1px solid colors.$kobo-gray-92;
border-bottom: 1px solid colors.$kobo-gray-92;
border-radius: 2px;
}
}
10 changes: 10 additions & 0 deletions kobo/apps/hook/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ class HookLogStatus(Enum):
PENDING = HOOK_LOG_PENDING
SUCCESS = HOOK_LOG_SUCCESS

HOOK_EVENT_SUBMIT = 'on_submit'
HOOK_EVENT_EDIT = 'on_edit'
HOOK_EVENT_DELETE = 'on_delete'
HOOK_EVENT_VALIDATION = 'on_validation_status_change'

class HookEvent(Enum):
ON_SUBMIT = HOOK_EVENT_SUBMIT,
ON_EDIT = HOOK_EVENT_EDIT,
ON_DELETE = HOOK_EVENT_DELETE,
ON_VALIDATION_STATUS_CHANGE = HOOK_EVENT_VALIDATION,

KOBO_INTERNAL_ERROR_STATUS_CODE = None

Expand Down
30 changes: 30 additions & 0 deletions kobo/apps/hook/migrations/0008_event_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 3.2.15 on 2024-01-17 12:38

from django.db import migrations, models


ON_EVENT = {
"onEdit": False,
"onDelete": False,
"onSubmit": True,
"onValidation": False
}

class Migration(migrations.Migration):

dependencies = [
('hook', '0007_do_nothing'),
]

operations = [
migrations.AddField(
model_name='hook',
name='on_event',
field=models.JSONField(default=ON_EVENT),
),
migrations.AddField(
model_name='hooklog',
name='event',
field=models.CharField(max_length=50, default='on_submit'),
),
]
7 changes: 7 additions & 0 deletions kobo/apps/hook/models/hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ class Hook(AbstractTimeStampedModel):
# Export types
XML = "xml"
JSON = "json"
ON_EVENT = {
"onEdit": False,
"onDelete": False,
"onSubmit": True,
"onValidation": False
}

# Authentication levels
NO_AUTH = "no_auth"
Expand All @@ -37,6 +43,7 @@ class Hook(AbstractTimeStampedModel):
name = models.CharField(max_length=255, blank=False)
endpoint = models.CharField(max_length=500, blank=False)
active = models.BooleanField(default=True)
on_event = models.JSONField(default=ON_EVENT)
export_type = models.CharField(choices=EXPORT_TYPE_CHOICES, default=JSON, max_length=10)
auth_level = models.CharField(choices=AUTHENTICATION_LEVEL_CHOICES, default=NO_AUTH, max_length=10)
settings = models.JSONField(default=dict)
Expand Down
6 changes: 4 additions & 2 deletions kobo/apps/hook/models/hook_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
HookLogStatus,
HOOK_LOG_PENDING,
HOOK_LOG_FAILED,
KOBO_INTERNAL_ERROR_STATUS_CODE
KOBO_INTERNAL_ERROR_STATUS_CODE,
HOOK_EVENT_SUBMIT
)


Expand All @@ -26,6 +27,7 @@ class HookLog(AbstractTimeStampedModel):
choices=[[e.value, e.name.title()] for e in HookLogStatus],
default=HookLogStatus.PENDING.value
) # Could use status_code, but will speed-up queries
event = models.TextField(default=HOOK_EVENT_SUBMIT)
status_code = models.IntegerField(default=KOBO_INTERNAL_ERROR_STATUS_CODE, null=True, blank=True)
message = models.TextField(default="")

Expand Down Expand Up @@ -95,7 +97,7 @@ def retry(self):
"""
try:
ServiceDefinition = self.hook.get_service_definition()
service_definition = ServiceDefinition(self.hook, self.submission_id)
service_definition = ServiceDefinition(self.hook, self.submission_id, uid=self.uid)
service_definition.send()
self.refresh_from_db()
except Exception as e:
Expand Down
26 changes: 16 additions & 10 deletions kobo/apps/hook/models/service_definition_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
from .hook import Hook
from .hook_log import HookLog
from ..constants import (
HOOK_EVENT_SUBMIT,
HOOK_EVENT_EDIT,
HOOK_EVENT_DELETE,
HOOK_EVENT_VALIDATION,
HOOK_LOG_SUCCESS,
HOOK_LOG_FAILED,
KOBO_INTERNAL_ERROR_STATUS_CODE,
Expand All @@ -20,10 +24,12 @@

class ServiceDefinitionInterface(metaclass=ABCMeta):

def __init__(self, hook, submission_id):
def __init__(self, hook, submission_id, uid=None, event = HOOK_EVENT_SUBMIT):
self._hook = hook
self._submission_id = submission_id
self._data = self._get_data()
self._event = event
self._uid_hook_log = uid # If uid == None, we considere it's a new log to create, else, it will be retrieve using this
self._data = self._get_data() # if data is changed between retries, get data will be reset and get the new data

def _get_data(self):
"""
Expand Down Expand Up @@ -169,15 +175,15 @@ def save_log(self, status_code: int, message: str, success: bool = False):
"""
fields = {
'hook': self._hook,
'submission_id': self._submission_id
'submission_id': self._submission_id,
'event': self._event
}
try:
# Try to load the log with a multiple field FK because
# we don't know the log `uid` in this context, but we do know
# its `hook` FK and its `submission_id`
log = HookLog.objects.get(**fields)
except HookLog.DoesNotExist:

# Retrieving the Hooklog if it already created within a retry(), or create it if not
if self._uid_hook_log is None:
log = HookLog(**fields)
else:
log = HookLog.objects.get(uid=self._uid_hook_log)

if success:
log.status = HOOK_LOG_SUCCESS
Expand All @@ -193,11 +199,11 @@ def save_log(self, status_code: int, message: str, success: bool = False):
json.loads(message)
except ValueError:
message = re.sub(r"<[^>]*>", " ", message).strip()

log.message = message

try:
log.save()
self._uid_hook_log = log.uid
except Exception as e:
logging.error(
f'ServiceDefinitionInterface.save_log - {str(e)}',
Expand Down
Loading