Skip to content

Commit

Permalink
Notifications Improvements (amundsen-io#301)
Browse files Browse the repository at this point in the history
* Initial start to notifications API (amundsen-io#215)

* initial start to notifications API

* fixing some styling

* fixed lint errors

* update types

* added tests

* linter, moved notification types

* addressed comments regarding imports/enum naming

* fixed alphabetical order

* Notifs post email functionality (amundsen-io#222)

* initial start to notifications API

* fixing some styling

* fixed lint errors

* update types

* added tests

* linter, moved notification types

* added template support

* made changes to reflect private changes

* added helper function

* fixed lint issue

* addressed comments, added some type checking and cleaned up comments

* testing removing test

* fixed linter

* fixed lint

* fixed linting issues

* skip type checking

* fixed lint

* fixed typing on get request args

* removed typing for get request to fix lint issues

* fixed linter again

* re added test

* raise exception inside of getmailclient

* added exceptions

* addressed comments

* whitespace issue

* removed calls to get_query_param

* fixed syntax error

* Send notification when adding/removing owner from table (amundsen-io#237)

* basic e2e functionality for adding/removing

* send_notification refactor

* fix lint errors

* blank line lint error

* fixed syntax issue

* arg typing

* addressed comments, fixed code style

* Prevent Self-Notifications (amundsen-io#243)

* Prevent user from notifying themselves

* removed exception

* added owner check to send_notification

* Fixed return for no recipients (amundsen-io#244)

* fixed return for no recipients

* fixed linter issue

* Request notifications component (amundsen-io#238)

* init of request form

* basic request component

* getting basic functionality in

* clearing out css

* removed z-index fixes and add constants

* fixed string casting

* added redux-saga calls

* removed reset request notification

* fixed tests

* addressed comments, added basic test, added redux state management for opening/closing component

* added tests, just need to add render test

* cleaned up component tests:

* addressed html/css comments

* removed unecessary styling

* removed collapsed class

* cleaned up render method

* fixed test

* Open request component (amundsen-io#254)

* added button to open up request component

* removed tabledetail changes

* className styling

* fixed text-decoration

* added tests, changed naming for OpenRequest

* styling formatting

* Add, Request, and Remove Email Copy (amundsen-io#257)

* init for fixing email copy for request, add, and remove

* removed print statement

* fixed python unit test

* fixed linter issues

* addressed comments, fixed linter issues

* added notification unit test

* fixed test positional arg

* fix test

* Add notification action logging (amundsen-io#258)

* init of adding action logging

* changed location of action logging

* fixed linter errors

* fixed comment

* addressed comments

* remove request test call (amundsen-io#259)

* hide request if description already exists (amundsen-io#269)

* fixed open request button, request form styling (amundsen-io#267)

* Added request dropdown component (amundsen-io#262)

* init

* made fixes

* cleaned up code

* fixed color issues

* fixed import order

* fixed styling, changed ducks/sagas

* User dropdown (amundsen-io#263)

* init

* fixed sty;es

* fixed test issue

* fixed test

* added tests, addressed comments

* Request Metadata Component Tests (amundsen-io#270)

* added tests + readonly field to stop errors

* fixed tslint

* addressed comments, added header tests

* Request form navigation fix, dropdown fix (amundsen-io#272)

* Request form navigation fix, dropdown fix

* added test

* added unique id to dropdown

* Creates User Preferences page with no functionality (amundsen-io#266)

* init

* added event handlers

* removed test file

* added constants

* addressed comments

* fixed test, removed all links to page

* updated test

* fixed call to onclick

* removed preferences page

* Python cleanup + tests (amundsen-io#277)

* Python cleanup + tests

* More tests + revert some unecessary changes

* Bring dropdown UI closer to design (amundsen-io#278)

* Rename OpenRequestDescription for clarity + code cleanup + test additions (amundsen-io#279)

* Notifications ducks cleanup + tests (amundsen-io#280)

* Notifications ducks cleanup + tests

* Fix issues

* Fix template for edge case of empty form (amundsen-io#281)

* Temporary debugging code, will revert

* Temporary debugging code, will revert

* Implement notification form confirmation (amundsen-io#289)

* Preserve compatibility in base_mail_client (amundsen-io#290)

* Notifications Configs + Doc (amundsen-io#291)

* Add notification config

* Code cleanup

* More cleanup + add a test

* Add some doc for how to enable features

* Add config utils test + fix type error

* Relative URLs to child configuration docs (amundsen-io#294)

* Relative URLs to child configuration docs

Relative URLs to docs in the same folder should do. They work for any branch, local copies of the docs - and should work better if we ever (or whenever :-) we get to having e.g a Sphinx generated site.

* Update application_config.md

Relative doc link

* Update flask_config.md

Relative doc link

* Update flask_config.md

Relative doc link

* Remove temporary debugging code

* Improve behavior of notification sending for owner editing (amundsen-io#296)

* Initial Implementation: Notification only on success

* Cleanup + tests: Notification only on success

* Cleanup: Remove test code to trigger failure

* Cleanup: Lint fix

* Workaround for not notifying teams or alumni

* Cleanup: Remove import mistake

* Utilize NotificationType enums instead of hardcoded string

* Remove use of render_template

* More minor cleanups

* Address some feedback

* Cleanup

* More cleanup

* Updates for RequestMetadataForm

* Switch to generating a  for url + comment required for request column descriptions

* Update some tests + comment out ones that need update before merge

* Update some tests + comment out ones that need update before merge

* Code cleanup

* Update and rename notification_utils python tests

* Modify resource_url check + add docstrings for python tests

* Component cleanup

* Cleanup component tests

* Fix some typos
ttannis authored Oct 7, 2019
1 parent 53ca663 commit 0378acb
Showing 21 changed files with 500 additions and 223 deletions.
34 changes: 23 additions & 11 deletions frontend/amundsen_application/api/utils/notification_utils.py
Original file line number Diff line number Diff line change
@@ -48,26 +48,37 @@ def get_mail_client(): # type: ignore
return mail_client


def validate_options(*, options: Dict) -> None:
"""
Raises an Exception if the options do not contain resource_path or resource_name
"""
if options.get('resource_path') is None:
raise Exception('resource_path was not provided in the notification options')
if options.get('resource_name')is None:
raise Exception('resource_name was not provided in the notification options')


def get_notification_html(*, notification_type: str, options: Dict, sender: str) -> str:
"""
Returns the formatted html for the notification based on the notification_type
:return: A string representing the html markup to send in the notification
"""
resource_url = options.get('resource_url')
if resource_url is None:
raise Exception('resource_url was not provided in the notification options')
validate_options(options=options)

resource_name = options.get('resource_name')
if resource_name is None:
raise Exception('resource_name was not provided in the notification options')
url_base = app.config['FRONTEND_BASE']
resource_url = '{url_base}{resource_path}'.format(resource_path=options.get('resource_path'),
url_base=url_base)
joined_chars = resource_url[len(url_base) - 1:len(url_base) + 1]
if joined_chars.count('/') != 1:
raise Exception('Configured "FRONTEND_BASE" and "resource_path" do not form a valid url')

notification_strings = NOTIFICATION_STRINGS.get(notification_type)
if notification_strings is None:
raise Exception('Unsupported notification_type')

greeting = 'Hello,<br/>'
notification = notification_strings.get('notification', '').format(resource_url=resource_url,
resource_name=resource_name,
resource_name=options.get('resource_name'),
sender=sender)
comment = notification_strings.get('comment', '')
end_note = notification_strings.get('end_note', '')
@@ -105,11 +116,12 @@ def get_notification_subject(*, notification_type: str, options: Dict) -> str:
:param options: data necessary to render email template content
:return: The subject to be used with the notification
"""
resource_name = options.get('resource_name')
notification_subject_dict = {
'added': 'You are now an owner of {}'.format(options['resource_name']),
'removed': 'You have been removed as an owner of {}'.format(options['resource_name']),
'edited': 'Your dataset {}\'s metadata has been edited'.format(options['resource_name']),
'requested': 'Request for metadata on {}'.format(options['resource_name']),
'added': 'You are now an owner of {}'.format(resource_name),
'removed': 'You have been removed as an owner of {}'.format(resource_name),
'edited': 'Your dataset {}\'s metadata has been edited'.format(resource_name),
'requested': 'Request for metadata on {}'.format(resource_name),
}
return notification_subject_dict.get(notification_type, '')

Original file line number Diff line number Diff line change
@@ -11,15 +11,15 @@ import AppConfig from 'config/config';
import ColumnDescEditableText from 'components/TableDetail/ColumnDescEditableText';
import { GlobalState } from 'ducks/rootReducer';
import { logClick } from 'ducks/utilMethods';
import { ToggleRequestAction } from 'ducks/notification/types';
import { OpenRequestAction } from 'ducks/notification/types';
import { openRequestDescriptionDialog } from 'ducks/notification/reducer';
import { TableColumn } from 'interfaces';
import { RequestMetadataType, TableColumn } from 'interfaces';

// TODO: Use css-modules instead of 'import'
import './styles.scss';

interface DispatchFromProps {
openRequestDescriptionDialog: () => ToggleRequestAction;
openRequestDescriptionDialog: (requestMetadataType: RequestMetadataType, columnName: string) => OpenRequestAction;
}

interface OwnProps {
@@ -47,7 +47,7 @@ class DetailListItem extends React.Component<DetailListItemProps, DetailListItem
}

openRequest = () => {
this.props.openRequestDescriptionDialog();
this.props.openRequestDescriptionDialog(RequestMetadataType.COLUMN_DESCRIPTION, this.props.data.name);
}

onClick = (e) => {
@@ -156,7 +156,7 @@ class DetailListItem extends React.Component<DetailListItemProps, DetailListItem
</div>
{
notificationsEnabled() &&
<Dropdown id={`detail-list-item-dropdown:${this.props.index}`} pullRight={true}>
<Dropdown id={`detail-list-item-dropdown:${this.props.index}`} pullRight={true} className="column-dropdown">
<Dropdown.Toggle noCaret={true} className="dropdown-icon-more">
<img className="icon icon-more"/>
</Dropdown.Toggle>
Original file line number Diff line number Diff line change
@@ -96,26 +96,30 @@
}
}

.dropdown-icon-more {
border-style: none;
border-radius: 4px;
height: 22px;
width: 22px;
padding: 4px;
margin-right: 5px;
.icon {
background-color: $gray-light;
height: 14px;
-webkit-mask-size: 14px;
mask-size: 14px;
width: 14px;
margin: 0;
}
&:hover,
&:focus {
background-color: $gray-lightest;
.column-dropdown {
height: fit-content;

.dropdown-icon-more {
border-style: none;
border-radius: 4px;
height: 22px;
width: 22px;
padding: 4px;
margin-right: 5px;
.icon {
background-color: $gray-base;
background-color: $gray-light;
height: 14px;
-webkit-mask-size: 14px;
mask-size: 14px;
width: 14px;
margin: 0;
}
&:hover,
&:focus {
background-color: $gray-lightest;
.icon {
background-color: $gray-base;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -3,13 +3,15 @@ import './styles.scss';

import { GlobalState } from 'ducks/rootReducer';
import { connect } from 'react-redux';
import { ToggleRequestAction } from 'ducks/notification/types';
import { OpenRequestAction } from 'ducks/notification/types';
import { openRequestDescriptionDialog } from 'ducks/notification/reducer';
import { bindActionCreators } from 'redux';
import { REQUEST_DESCRIPTION } from './constants';

import { RequestMetadataType } from 'interfaces';

export interface DispatchFromProps {
openRequestDescriptionDialog: () => ToggleRequestAction;
openRequestDescriptionDialog: (requestMetadataType: RequestMetadataType) => OpenRequestAction;
}

export type RequestDescriptionTextProps = DispatchFromProps;
@@ -24,7 +26,7 @@ export class RequestDescriptionText extends React.Component<RequestDescriptionTe
}

openRequest = () => {
this.props.openRequestDescriptionDialog();
this.props.openRequestDescriptionDialog(RequestMetadataType.TABLE_DESCRIPTION);
}

render() {
Original file line number Diff line number Diff line change
@@ -5,12 +5,13 @@ import { shallow } from 'enzyme';
import { RequestDescriptionText, mapDispatchToProps, RequestDescriptionTextProps } from '../';
import globalState from 'fixtures/globalState';
import { REQUEST_DESCRIPTION } from '../constants';
import { RequestMetadataType } from 'interfaces';

describe('RequestDescriptionText', () => {
const setup = (propOverrides?: Partial<RequestDescriptionTextProps>) => {
const props: RequestDescriptionTextProps = {
openRequestDescriptionDialog: jest.fn(),
...propOverrides,
...propOverrides,
};
const wrapper = shallow<RequestDescriptionText>(<RequestDescriptionText {...props} />)
return {props, wrapper}
@@ -21,10 +22,10 @@ describe('RequestDescriptionText', () => {
const { props, wrapper } = setup();
const openRequestDescriptionDialogSpy = jest.spyOn(props, 'openRequestDescriptionDialog');
wrapper.instance().openRequest();
expect(openRequestDescriptionDialogSpy).toHaveBeenCalled();
expect(openRequestDescriptionDialogSpy).toHaveBeenCalledWith(RequestMetadataType.TABLE_DESCRIPTION);
});
});

describe('render', () => {
it('renders Request Description button with correct text', () => {
const { props, wrapper } = setup();
@@ -40,7 +41,7 @@ describe('RequestDescriptionText', () => {
dispatch = jest.fn(() => Promise.resolve());
result = mapDispatchToProps(dispatch);
});

it('sets openRequestDescriptionDialog on the props', () => {
expect(result.openRequestDescriptionDialog).toBeInstanceOf(Function);
});
Original file line number Diff line number Diff line change
@@ -11,3 +11,10 @@ export const SEND_BUTTON = 'Send Request';
export const SEND_INPROGRESS_MESSAGE = 'Your request is being sent...';
export const SEND_FAILURE_MESSAGE = 'Your request was not successfully sent, please try again';
export const SEND_SUCCESS_MESSAGE = 'Your request has been successfully sent';

export const RECIPIENT_LIST_DELIMETER = ', ';

export const COMMENT_PLACEHOLDER_DEFAULT = 'Please enter more information about your request';
export const COMMENT_PLACEHOLDER_COLUMN = 'Please enter which column(s) need an improved description';

export const COLUMN_REQUESTED_COMMENT_PREFIX = 'Description requested for column: ';
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import { bindActionCreators } from 'redux';

import './styles.scss';

import { NotificationType, SendNotificationOptions, SendingState } from 'interfaces';
import { NotificationType, RequestMetadataType, SendNotificationOptions, SendingState, TableMetadata } from 'interfaces';

import FlashMessage from 'components/common/FlashMessage'

@@ -17,31 +17,37 @@ import {
REQUEST_TYPE,
TABLE_DESCRIPTION,
COLUMN_DESCRIPTIONS,
COLUMN_REQUESTED_COMMENT_PREFIX,
COMMENT_PLACEHOLDER_COLUMN,
COMMENT_PLACEHOLDER_DEFAULT,
ADDITIONAL_DETAILS,
RECIPIENT_LIST_DELIMETER,
SEND_BUTTON,
SEND_FAILURE_MESSAGE,
SEND_INPROGRESS_MESSAGE,
SEND_SUCCESS_MESSAGE,
} from './constants'
import { ToggleRequestAction, SubmitNotificationRequest } from 'ducks/notification/types';
import { CloseRequestAction, OpenRequestAction, SubmitNotificationRequest } from 'ducks/notification/types';
import { closeRequestDescriptionDialog, submitNotification } from 'ducks/notification/reducer';

interface StateFromProps {
columnName?: string;
requestMetadataType?: RequestMetadataType;
userEmail: string;
displayName: string;
tableOwners: Array<string>;
tableMetadata: TableMetadata;
tableOwners: string[];
requestIsOpen: boolean;
sendState: SendingState;
}

export interface DispatchFromProps {
submitNotification: (
recipients: Array<string>,
recipients: string[],
sender: string,
notificationType: NotificationType,
options?: SendNotificationOptions
) => SubmitNotificationRequest;
closeRequestDescriptionDialog: () => ToggleRequestAction;
closeRequestDescriptionDialog: () => CloseRequestAction;
}

export type RequestMetadataProps = StateFromProps & DispatchFromProps;
@@ -91,34 +97,40 @@ export class RequestMetadataForm extends React.Component<RequestMetadataProps, R
const form = document.getElementById("RequestForm") as HTMLFormElement;
const formData = new FormData(form);
const recipientString = formData.get('recipients') as string
const recipients = recipientString.split(",")
const recipients = recipientString.split(RECIPIENT_LIST_DELIMETER.trim())
const sender = formData.get('sender') as string;
const descriptionRequested = formData.get('table-description') === "on";
const fieldsRequested = formData.get('column-description') === "on";
const comment = formData.get('comment') as string;
const { cluster, database, schema, table_name } = this.props.tableMetadata;
this.props.submitNotification(
recipients,
sender,
NotificationType.METADATA_REQUESTED,
{
comment,
resource_name: this.props.displayName,
resource_url: window.location.href,
resource_name: `${schema}.${table_name}`,
resource_path: `/table_detail/${cluster}/${database}/${schema}/${table_name}`,
description_requested: descriptionRequested,
fields_requested: fieldsRequested,
}
)
};

render() {
if (this.props.sendState !== SendingState.IDLE) {
const { columnName, requestIsOpen, requestMetadataType, sendState, tableMetadata, tableOwners, userEmail } = this.props;
const tableDescriptionNeeded = requestMetadataType === RequestMetadataType.TABLE_DESCRIPTION;
const colDescriptionNeeded = requestMetadataType === RequestMetadataType.COLUMN_DESCRIPTION;
const defaultComment = columnName ? `${COLUMN_REQUESTED_COMMENT_PREFIX}${columnName}`: '';

if (sendState !== SendingState.IDLE) {
return (
<div className="request-component">
{this.renderFlashMessage()}
</div>
);
}
if (!this.props.requestIsOpen) {
if (!requestIsOpen) {
return (null);
}
return (
@@ -130,20 +142,43 @@ export class RequestMetadataForm extends React.Component<RequestMetadataProps, R
<form onSubmit={ this.submitNotification } id="RequestForm">
<div id="sender-form-group" className="form-group">
<label>{FROM_LABEL}</label>
<input type="email" name="sender" className="form-control" required={true} value={this.props.userEmail} readOnly={true}/>
<input type="email" name="sender" className="form-control" required={true} value={userEmail} readOnly={true}/>
</div>
<div id="recipients-form-group" className="form-group">
<label>{TO_LABEL}</label>
<input type="email" name="recipients" className="form-control" required={true} multiple={true} defaultValue={this.props.tableOwners.join(",")}/>
<input type="text" name="recipients" className="form-control" required={true} multiple={true} defaultValue={tableOwners.join(RECIPIENT_LIST_DELIMETER)}/>
</div>
<div id="request-type-form-group" className="form-group">
<label>{REQUEST_TYPE}</label>
<label className="select-label"><input type="checkbox" name="table-description"/>{TABLE_DESCRIPTION}</label>
<label className="select-label"><input type="checkbox" name="column-description"/>{COLUMN_DESCRIPTIONS}</label>
<label className="select-label">
<input
type="checkbox"
name="table-description"
defaultChecked={tableDescriptionNeeded}
/>
{TABLE_DESCRIPTION}
</label>
<label className="select-label">
<input
type="checkbox"
name="column-description"
defaultChecked={colDescriptionNeeded}
/>
{COLUMN_DESCRIPTIONS}
</label>
</div>
<div id="additional-comments-form-group" className="form-group">
<label>{ADDITIONAL_DETAILS}</label>
<textarea className="form-control" name="comment" rows={ 8 } maxLength={ 2000 } />
<textarea
className="form-control"
name="comment"
placeholder={ colDescriptionNeeded ? COMMENT_PLACEHOLDER_COLUMN : COMMENT_PLACEHOLDER_DEFAULT }
required={ colDescriptionNeeded }
rows={ 8 }
maxLength={ 2000 }
>
{ defaultComment }
</textarea>
</div>
<button id="submit-request-button" className="btn btn-primary" type="submit">
{SEND_BUTTON}
@@ -156,16 +191,22 @@ export class RequestMetadataForm extends React.Component<RequestMetadataProps, R

export const mapStateToProps = (state: GlobalState) => {
const userEmail = state.user.loggedInUser.email;
const displayName = `${state.tableMetadata.tableData.schema}.${state.tableMetadata.tableData.table_name}`;
const { columnName, requestMetadataType, requestIsOpen, sendState } = state.notification;
const ownerObj = state.tableMetadata.tableOwners.owners;
const { requestIsOpen, sendState } = state.notification;
return {
const mappedProps = {
userEmail,
displayName,
requestIsOpen,
sendState,
tableMetadata: state.tableMetadata.tableData,
tableOwners: Object.keys(ownerObj),
};
if (columnName) {
mappedProps['columnName'] = columnName;
}
if (requestMetadataType) {
mappedProps['requestMetadataType'] = requestMetadataType;
}
return mappedProps;
};

export const mapDispatchToProps = (dispatch: any) => {
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
@import 'variables';

input[type="email"] {
color: $text-medium !important;
}

.request-component {
box-shadow: 0 0 24px -2px rgba(0, 0, 0, .2);
border-radius: 6px;
@@ -33,6 +29,10 @@ input[type="email"] {
font-weight: $font-weight-body-regular;
}

input,
textarea {
color: $text-medium;
}
input[type="checkbox"] {
margin-right: 8px;
}
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import { shallow } from 'enzyme';
import FlashMessage from 'components/common/FlashMessage';

import globalState from 'fixtures/globalState';
import { NotificationType, SendingState } from 'interfaces';
import { NotificationType, RequestMetadataType, SendingState } from 'interfaces';
import { RequestMetadataForm, mapDispatchToProps, mapStateToProps, RequestMetadataProps } from '../';
import {
TITLE_TEXT,
@@ -13,6 +13,9 @@ import {
REQUEST_TYPE,
TABLE_DESCRIPTION,
COLUMN_DESCRIPTIONS,
COLUMN_REQUESTED_COMMENT_PREFIX,
COMMENT_PLACEHOLDER_COLUMN,
COMMENT_PLACEHOLDER_DEFAULT,
ADDITIONAL_DETAILS,
SEND_BUTTON,
SEND_FAILURE_MESSAGE,
@@ -38,8 +41,8 @@ describe('RequestMetadataForm', () => {
const setup = (propOverrides?: Partial<RequestMetadataProps>) => {
const props: RequestMetadataProps = {
userEmail: 'test0@lyft.com',
displayName: '',
tableOwners: ['test1@lyft.com', 'test2@lyft.com'],
tableMetadata: globalState.tableMetadata.tableData,
submitNotification: jest.fn(),
requestIsOpen: true,
sendState: SendingState.IDLE,
@@ -111,15 +114,16 @@ describe('RequestMetadataForm', () => {
it('calls submitNotification', () => {
const { props, wrapper } = setup();
const submitNotificationSpy = jest.spyOn(props, 'submitNotification');
const { cluster, database, schema, table_name } = props.tableMetadata;
wrapper.instance().submitNotification({ preventDefault: jest.fn() });
expect(submitNotificationSpy).toHaveBeenCalledWith(
mockFormData['recipients'].split(','),
mockFormData['sender'],
NotificationType.METADATA_REQUESTED,
{
comment: mockFormData['comment'],
resource_name: props.displayName,
resource_url: window.location.href,
resource_name: `${schema}.${table_name}`,
resource_path: `/table_detail/${cluster}/${database}/${schema}/${table_name}`,
description_requested: true,
fields_requested: false,
}
@@ -133,68 +137,112 @@ describe('RequestMetadataForm', () => {
let element;

describe('when this.props.requestIsOpen', () => {
beforeAll(() => {
const setupResult = setup();
props = setupResult.props;
wrapper = setupResult.wrapper;
});
it('renders header title', () => {
element = wrapper.find('#request-metadata-title');
expect(element.find('h3').text()).toEqual(TITLE_TEXT);
});
it('renders close button', () => {
element = wrapper.find('#request-metadata-title');
expect(element.find('button').exists()).toEqual(true);
});
describe('no optional props', () => {
beforeAll(() => {
const setupResult = setup();
props = setupResult.props;
wrapper = setupResult.wrapper;
});
it('renders header title', () => {
element = wrapper.find('#request-metadata-title');
expect(element.find('h3').text()).toEqual(TITLE_TEXT);
});
it('renders close button', () => {
element = wrapper.find('#request-metadata-title');
expect(element.find('button').exists()).toEqual(true);
});

it('renders from input with current user', () => {
element = wrapper.find('#sender-form-group');
expect(element.find('input').props().value).toEqual('test0@lyft.com');
});
it('renders from input with current user', () => {
element = wrapper.find('#sender-form-group');
expect(element.find('input').props().value).toEqual('test0@lyft.com');
});

it('renders from label', () => {
element = wrapper.find('#sender-form-group');
expect(element.find('label').text()).toEqual(FROM_LABEL);
});
it('renders from input with current user', () => {
element = wrapper.find('#sender-form-group');
expect(element.find('input').props().value).toEqual('test0@lyft.com');
});
it('renders from label', () => {
element = wrapper.find('#sender-form-group');
expect(element.find('label').text()).toEqual(FROM_LABEL);
});
it('renders from input with current user', () => {
element = wrapper.find('#sender-form-group');
expect(element.find('input').props().value).toEqual('test0@lyft.com');
});

it('renders to label', () => {
element = wrapper.find('#recipients-form-group');
expect(element.find('label').text()).toEqual(TO_LABEL);
});
it('renders to input with correct recipients', () => {
element = wrapper.find('#recipients-form-group');
expect(element.find('input').props().defaultValue).toEqual('test1@lyft.com,test2@lyft.com');
});
it('renders to label', () => {
element = wrapper.find('#recipients-form-group');
expect(element.find('label').text()).toEqual(TO_LABEL);
});
it('renders to input with correct recipients', () => {
element = wrapper.find('#recipients-form-group');
expect(element.find('input').props().defaultValue).toEqual('test1@lyft.com, test2@lyft.com');
});

it('renders request type label', () => {
element = wrapper.find('#request-type-form-group');
expect(element.find('label').at(0).text()).toEqual(REQUEST_TYPE);
});
it('renders table description checkbox', () => {
element = wrapper.find('#request-type-form-group');
expect(element.find('label').at(1).text()).toEqual(TABLE_DESCRIPTION);
});
it('renders column descriptions checkbox', () => {
element = wrapper.find('#request-type-form-group');
expect(element.find('label').at(2).text()).toEqual(COLUMN_DESCRIPTIONS);
});
it('renders request type label', () => {
element = wrapper.find('#request-type-form-group');
expect(element.find('label').at(0).text()).toEqual(REQUEST_TYPE);
});
it('renders unchecked table description checkbox', () => {
element = wrapper.find('#request-type-form-group');
const label = element.find('label').at(1);
expect(label.text()).toEqual(TABLE_DESCRIPTION);
expect(label.find('input').props().defaultChecked).toBe(false);
});
it('renders unchecked column descriptions checkbox', () => {
element = wrapper.find('#request-type-form-group');
const label = element.find('label').at(2);
expect(label.text()).toEqual(COLUMN_DESCRIPTIONS);
expect(label.find('input').props().defaultChecked).toBe(false);
});

it('renders additional details label', () => {
element = wrapper.find('#additional-comments-form-group');
expect(element.find('label').text()).toEqual(ADDITIONAL_DETAILS);
it('renders additional details label', () => {
element = wrapper.find('#additional-comments-form-group');
expect(element.find('label').text()).toEqual(ADDITIONAL_DETAILS);
});
it('renders default textarea', () => {
element = wrapper.find('#additional-comments-form-group');
const textArea = element.find('textarea');
expect(textArea.text()).toEqual('');
expect(textArea.props().required).toBe(false);
expect(textArea.props().placeholder).toBe(COMMENT_PLACEHOLDER_DEFAULT);
});

it('renders submit button with correct text', () => {
element = wrapper.find('#submit-request-button');
expect(element.text()).toEqual(SEND_BUTTON);
});
});
it('renders empty textarea', () => {
element = wrapper.find('#additional-comments-form-group');
expect(element.find('textarea').text()).toEqual('');


describe('table description requested', () => {
beforeAll(() => {
const setupResult = setup({ requestMetadataType: RequestMetadataType.TABLE_DESCRIPTION });
props = setupResult.props;
wrapper = setupResult.wrapper;
});
it('renders checked table description checkbox', () => {
element = wrapper.find('#request-type-form-group');
const label = element.find('label').at(1);
expect(label.find('input').props().defaultChecked).toBe(true);
});
});

it('renders submit button with correct text', () => {
element = wrapper.find('#submit-request-button');
expect(element.text()).toEqual(SEND_BUTTON);
describe('column description requested', () => {
beforeAll(() => {
const setupResult = setup({ requestMetadataType: RequestMetadataType.COLUMN_DESCRIPTION, columnName: 'Test' });
props = setupResult.props;
wrapper = setupResult.wrapper;
});
it('renders checked column description checkbox', () => {
element = wrapper.find('#request-type-form-group');
const label = element.find('label').at(2);
expect(label.find('input').props().defaultChecked).toBe(true);
});

it('renders textarea for column request', () => {
element = wrapper.find('#additional-comments-form-group');
const textArea = element.find('textarea');
expect(textArea.text()).toEqual(`${COLUMN_REQUESTED_COMMENT_PREFIX}Test`);
expect(textArea.props().required).toBe(true);
expect(textArea.props().placeholder).toBe(COMMENT_PLACEHOLDER_COLUMN);
});
});
});

@@ -235,9 +283,6 @@ describe('RequestMetadataForm', () => {
it('sets userEmail on the props', () => {
expect(result.userEmail).toEqual(globalState.user.loggedInUser.email);
});
it('sets displayName on the props', () => {
expect(result.displayName).toEqual(globalState.tableMetadata.tableData.schema + '.' + globalState.tableMetadata.tableData.table_name);
});
it('sets ownerObj on the props', () => {
expect(result.tableOwners).toEqual(Object.keys(globalState.tableMetadata.tableOwners.owners));
});
@@ -247,6 +292,18 @@ describe('RequestMetadataForm', () => {
it('sets sendState on the props', () => {
expect(result.sendState).toEqual(globalState.notification.sendState);
});
it('sets columnName on the props if it exists in globalState', () => {
const newState = { ...globalState };
newState.notification.columnName = 'test_name';
result = mapStateToProps(newState);
expect(result.columnName).toEqual(newState.notification.columnName);
});
it('sets requestMetadataType on the props if it exists in globalState', () => {
const newState = { ...globalState };
newState.notification.requestMetadataType = RequestMetadataType.TABLE_DESCRIPTION;
result = mapStateToProps(newState);
expect(result.requestMetadataType).toEqual(newState.notification.requestMetadataType);
});
});

describe('mapDispatchToProps', () => {
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ describe('sendNotification', () => {
const testNotificationType = NotificationType.OWNER_ADDED;
const testOptions = {
resource_name: 'testResource',
resource_url: 'https://testResource.com',
resource_path: '/testResource',
description_requested: false,
fields_requested: false,
};
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import { NotificationType, SendNotificationOptions, SendingState } from 'interfaces'
import { NotificationType, RequestMetadataType, SendNotificationOptions, SendingState } from 'interfaces'

import {
SubmitNotification,
SubmitNotificationRequest,
SubmitNotificationResponse,
ToggleRequest,
ToggleRequestAction,
CloseRequestAction,
OpenRequestAction,
} from './types';

/* ACTIONS */
export function submitNotification(recipients: Array<string>, sender: string, notificationType: NotificationType, options?: SendNotificationOptions): SubmitNotificationRequest {
return {
payload: {
recipients,
sender,
notificationType,
options
recipients,
sender,
notificationType,
options
},
type: SubmitNotification.REQUEST,
};
@@ -31,20 +32,34 @@ export function submitNotificationSuccess(): SubmitNotificationResponse {
};
};

export function closeRequestDescriptionDialog(): ToggleRequestAction {
export function closeRequestDescriptionDialog(): CloseRequestAction {
return {
type: ToggleRequest.CLOSE,
type: ToggleRequest.CLOSE
};
};

export function openRequestDescriptionDialog(): ToggleRequestAction {
export function openRequestDescriptionDialog(requestMetadataType: RequestMetadataType, columnName?: string): OpenRequestAction {
if (columnName) {
return {
type: ToggleRequest.OPEN,
payload: {
columnName,
requestMetadataType
}
}
}
return {
type: ToggleRequest.OPEN,
payload: {
requestMetadataType
}
}
}

/* REDUCER */
export interface NotificationReducerState {
columnName?: string,
requestMetadataType?: RequestMetadataType,
requestIsOpen: boolean,
sendState: SendingState,
};
@@ -68,6 +83,7 @@ export default function reducer(state: NotificationReducerState = initialState,
}
case SubmitNotification.REQUEST:
return {
...state,
requestIsOpen: false,
sendState: SendingState.WAITING,
}
@@ -77,10 +93,16 @@ export default function reducer(state: NotificationReducerState = initialState,
sendState: SendingState.IDLE,
}
case ToggleRequest.OPEN:
return {
const newState = {
requestMetadataType: (<OpenRequestAction>action).payload.requestMetadataType,
requestIsOpen: true,
sendState: SendingState.IDLE,
}
const columnName = (<OpenRequestAction>action).payload.columnName;
if (columnName) {
newState['columnName'] = columnName;
}
return newState;
default:
return state;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { testSaga } from 'redux-saga-test-plan';

import { NotificationType, SendingState } from 'interfaces';
import { NotificationType, RequestMetadataType, SendingState } from 'interfaces';

import * as API from '../api/v0';
import reducer, {
@@ -23,7 +23,7 @@ const testSender = 'user2@test.com';
const testNotificationType = NotificationType.OWNER_ADDED;
const testOptions = {
resource_name: 'testResource',
resource_url: 'https://testResource.com',
resource_path: '/testResource',
description_requested: false,
fields_requested: false,
};
@@ -55,9 +55,23 @@ describe('notifications ducks', () => {
expect(action.type).toBe(ToggleRequest.CLOSE);
});

it('openRequestDescriptionDialog - returns the action to trigger the request description to opem', () => {
const action = openRequestDescriptionDialog();
expect(action.type).toBe(ToggleRequest.OPEN);
it('openRequestDescriptionDialog - returns the action to trigger the request description to open', () => {
const testType = RequestMetadataType.TABLE_DESCRIPTION;
const action = openRequestDescriptionDialog(testType);
const { payload, type } = action;
expect(type).toBe(ToggleRequest.OPEN);
expect(payload.requestMetadataType).toBe(testType);
expect(payload.columnName).toBe(undefined);
});

it('openRequestDescriptionDialog w/ columnName - returns the action to trigger the request description to open', () => {
const testType = RequestMetadataType.TABLE_DESCRIPTION;
const testName = 'columnName';
const action = openRequestDescriptionDialog(testType, testName);
const { payload, type } = action;
expect(type).toBe(ToggleRequest.OPEN);
expect(payload.requestMetadataType).toBe(testType);
expect(payload.columnName).toBe(testName);
});
});

@@ -73,8 +87,18 @@ describe('notifications ducks', () => {
expect(reducer(testState, { type: 'INVALID.ACTION' })).toEqual(testState);
});

it('should handle ToggleRequest.OPEN', () => {
expect(reducer(testState, openRequestDescriptionDialog())).toEqual({
it('should handle ToggleRequest.OPEN without columnName', () => {
expect(reducer(testState, openRequestDescriptionDialog(RequestMetadataType.TABLE_DESCRIPTION))).toEqual({
requestMetadataType: RequestMetadataType.TABLE_DESCRIPTION,
requestIsOpen: true,
sendState: SendingState.IDLE,
});
});

it('should handle ToggleRequest.OPEN with columnName', () => {
expect(reducer(testState, openRequestDescriptionDialog(RequestMetadataType.TABLE_DESCRIPTION, 'col'))).toEqual({
columnName: 'col',
requestMetadataType: RequestMetadataType.TABLE_DESCRIPTION,
requestIsOpen: true,
sendState: SendingState.IDLE,
});
@@ -97,6 +121,7 @@ describe('notifications ducks', () => {
it('should handle SubmitNotification.REQUEST', () => {
const action = submitNotification(testRecipients, testSender, testNotificationType, testOptions);
expect(reducer(testState, action)).toEqual({
...testState,
requestIsOpen: false,
sendState: SendingState.WAITING,
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NotificationType, SendNotificationOptions } from 'interfaces'
import { NotificationType, RequestMetadataType, SendNotificationOptions } from 'interfaces'

export enum SubmitNotification {
REQUEST = 'amundsen/notification/SUBMIT_NOTIFICATION_REQUEST',
@@ -9,7 +9,7 @@ export enum SubmitNotification {
export interface SubmitNotificationRequest {
type: SubmitNotification.REQUEST;
payload: {
recipients: Array<string>,
recipients: string[],
sender: string,
notificationType: NotificationType,
options?: SendNotificationOptions
@@ -24,6 +24,14 @@ export enum ToggleRequest {
CLOSE = 'close',
};

export interface ToggleRequestAction {
type: ToggleRequest.OPEN | ToggleRequest.CLOSE;
export interface OpenRequestAction {
type: ToggleRequest.OPEN,
payload: {
columnName?: string,
requestMetadataType: RequestMetadataType,
}
};

export interface CloseRequestAction {
type: ToggleRequest.CLOSE
};
Original file line number Diff line number Diff line change
@@ -41,12 +41,12 @@ export function getTableTagsFromResponseData(responseData: API.TableDataAPI): Ta
/**
* Creates post data for sending a notification to owners when they are added/removed
*/
export function createOwnerNotificationData(payload: UpdateOwnerPayload, resourceName: string) {
export function createOwnerNotificationData(payload: UpdateOwnerPayload, tableData: TableMetadata) {
return {
notificationType: payload.method === UpdateMethod.PUT ? NotificationType.OWNER_ADDED : NotificationType.OWNER_REMOVED,
options: {
resource_name: resourceName,
resource_url: window.location.href,
resource_name: `${tableData.schema}.${tableData.table_name}`,
resource_path: `/table_detail/${tableData.cluster}/${tableData.database}/${tableData.schema}/${tableData.table_name}`
},
recipients: [payload.id],
};
Original file line number Diff line number Diff line change
@@ -73,29 +73,34 @@ describe('helpers', () => {
});

describe('createOwnerNotificationData', () => {
let testData;
let testId;
let expectedName;
let expectedPath;
beforeAll(() => {
testData = globalState.tableMetadata.tableData;
testId = 'testId@test.com';
expectedName = `${testData.schema}.${testData.table_name}`;
expectedPath = `/table_detail/${testData.cluster}/${testData.database}/${testData.schema}/${testData.table_name}`;
});

it('creates correct request data for PUT', () => {
const testId = 'testId@test.com';
const testMethod = UpdateMethod.PUT;
const testName = 'schema.tableName';
expect(Helpers.createOwnerNotificationData({ method: testMethod, id: testId }, testName)).toMatchObject({
expect(Helpers.createOwnerNotificationData({ method: UpdateMethod.PUT, id: testId }, testData)).toMatchObject({
notificationType: NotificationType.OWNER_ADDED,
options: {
resource_name: testName,
resource_url: window.location.href,
resource_name: expectedName,
resource_path: expectedPath,
},
recipients: [testId],
});
});

it('creates correct request data for DELETE', () => {
const testId = 'testId@test.com';
const testMethod = UpdateMethod.DELETE;
const testName = 'schema.tableName';
expect(Helpers.createOwnerNotificationData({ method: testMethod, id: testId }, testName)).toMatchObject({
expect(Helpers.createOwnerNotificationData({ method: UpdateMethod.DELETE, id: testId }, testData)).toMatchObject({
notificationType: NotificationType.OWNER_REMOVED,
options: {
resource_name: testName,
resource_url: window.location.href,
resource_name: expectedName,
resource_path: expectedPath,
},
recipients: [testId],
});
Original file line number Diff line number Diff line change
@@ -89,13 +89,13 @@ export function getTableOwners(tableKey: string) {
}

/* TODO: Typing return type generates redux-saga related type error that need more dedicated debugging */
export function generateOwnerUpdateRequests(updateArray: UpdateOwnerPayload[], tableKey: string, resourceName: string) {
export function generateOwnerUpdateRequests(updateArray: UpdateOwnerPayload[], tableData: TableMetadata) {
const updateRequests = [];

/* Create the request for updating each owner*/
updateArray.forEach((item) => {
const updatePayload = createOwnerUpdatePayload(item, tableKey);
const notificationData = createOwnerNotificationData(item, resourceName);
const updatePayload = createOwnerUpdatePayload(item, tableData.key);
const notificationData = createOwnerNotificationData(item, tableData);

/* Chain requests to send notification on success to desired users */
const request =
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ export function* updateTableOwnerWorker(action: UpdateTableOwnerRequest): SagaIt
const state = yield select();
const tableData = state.tableMetadata.tableData;
try {
const requestList = API.generateOwnerUpdateRequests(payload.updateArray, tableData.key, `${tableData.schema}.${tableData.table_name}`);
const requestList = API.generateOwnerUpdateRequests(payload.updateArray, tableData);
yield all(requestList);
const newOwners = yield call(API.getTableOwners, tableData.key);
yield put(updateTableOwnerSuccess(newOwners));
Original file line number Diff line number Diff line change
@@ -131,7 +131,7 @@ describe('tableMetadata:owners ducks', () => {
sagaTest = (action) => {
return testSaga(updateTableOwnerWorker, action)
.next().select()
.next(globalState).all(API.generateOwnerUpdateRequests(updatePayload, globalState.tableMetadata.tableData.key, globalState.tableMetadata.tableData.table_name))
.next(globalState).all(API.generateOwnerUpdateRequests(updatePayload, globalState.tableMetadata.tableData))
.next().call(API.getTableOwners, globalState.tableMetadata.tableData.key)
.next(expectedOwners).put(updateTableOwnerSuccess(expectedOwners));
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { GlobalState } from 'ducks/rootReducer';
import { ResourceType, SendingState } from 'interfaces';
import { RequestMetadataType, ResourceType, SendingState } from 'interfaces';

const globalState: GlobalState = {
announcements: {
Original file line number Diff line number Diff line change
@@ -6,9 +6,14 @@ export enum NotificationType {
METADATA_REQUESTED = 'requested',
}

export enum RequestMetadataType {
COLUMN_DESCRIPTION = 'columnDescriptionRequested',
TABLE_DESCRIPTION = 'tableDescriptionRequested',
}

export interface SendNotificationOptions {
resource_name: string,
resource_url: string,
resource_path: string,
description_requested: boolean,
fields_requested: boolean,
comment?: string,
Original file line number Diff line number Diff line change
@@ -34,48 +34,104 @@ class NotificationUtilsTest(unittest.TestCase):
def setUp(self) -> None:
self.mock_table_key = 'db://cluster.schema/table'

def test_get_notification_html_no_resource_url(self) -> None:
test_notification_type = 'added'
def test_validate_resource_path_none(self) -> None:
"""
Test Exception is raised if resource_path is None
:return:
"""
test_notification_type = 'removed'
test_sender = 'test@test.com'
test_options = {'resource_name': 'testtable'}

self.assertRaises(Exception,
get_notification_html,
notification_type=test_notification_type,
options=test_options,
sender=test_sender)
with local_app.app_context():
self.assertRaises(Exception,
get_notification_html,
notification_type=test_notification_type,
options=test_options,
sender=test_sender)

def test_validate_resource_path_bad_syntax(self) -> None:
"""
Test Exception is raised if resource_path violates leading '/' syntax
:return:
"""
test_notification_type = 'removed'
test_sender = 'test@test.com'
test_options = {'resource_name': 'testtable', 'resource_path': 'testpath'}

with local_app.app_context():
self.assertRaises(Exception,
get_notification_html,
notification_type=test_notification_type,
options=test_options,
sender=test_sender)

def test_get_notification_html_bad_base_url(self) -> None:
"""
Test Exception is raised if configured FRONTEND_BASE violates no trailing '/' syntax
:return:
"""
test_notification_type = 'added'
test_sender = 'test@test.com'
test_options = {'resource_name': 'testtable', 'resource_path': '/testpath'}

with local_app.app_context():
temp = local_app.config['FRONTEND_BASE']
local_app.config['FRONTEND_BASE'] = 'garbagetest_rewrite_file_to_setup_teardown_each_case/'
self.assertRaises(Exception,
get_notification_html,
notification_type=test_notification_type,
options=test_options,
sender=test_sender)
local_app.config['FRONTEND_BASE'] = temp

def test_get_notification_html_no_resource_name(self) -> None:
"""
Test Exception is raised if resource_name is not provided
:return:
"""
test_notification_type = 'added'
test_sender = 'test@test.com'
test_options = {'resource_url': 'testUrl'}
test_options = {'resource_path': '/testpath'}

self.assertRaises(Exception,
get_notification_html,
notification_type=test_notification_type,
options=test_options,
sender=test_sender)
with local_app.app_context():
self.assertRaises(Exception,
get_notification_html,
notification_type=test_notification_type,
options=test_options,
sender=test_sender)

def test_get_notification_html_unsupported_type(self) -> None:
"""
Test Exception is raised if notification_type is not supported
:return:
"""
test_notification_type = 'invalid_type'
test_sender = 'test@test.com'
test_options = {'resource_name': 'testtable', 'resource_url': 'testUrl'}
test_options = {'resource_name': 'testtable', 'resource_path': '/testpath'}

self.assertRaises(Exception,
get_notification_html,
notification_type=test_notification_type,
options=test_options,
sender=test_sender)
with local_app.app_context():
self.assertRaises(Exception,
get_notification_html,
notification_type=test_notification_type,
options=test_options,
sender=test_sender)

def test_get_notification_html_added_success(self) -> None:
"""
Test successful generation of html for 'added' notification email
:return:
"""
test_notification_type = 'added'
test_sender = 'test@test.com'
test_options = {'resource_name': 'testtable', 'resource_url': 'testUrl'}
test_options = {'resource_name': 'testtable', 'resource_path': '/testpath'}

html = get_notification_html(notification_type=test_notification_type,
options=test_options,
sender=test_sender)
expectedHTML = ('Hello,<br/><br/>You have been added to the owners list of the <a href="testUrl">testtable</a>'
with local_app.app_context():
html = get_notification_html(notification_type=test_notification_type,
options=test_options,
sender=test_sender)
expectedHTML = ('Hello,<br/><br/>You have been added to the owners list of the '
'<a href="http://0.0.0.0:5000/testpath">testtable</a>'
' dataset by test@test.com.<br/><br/>What is expected of you?<br/>As an owner, you take an '
'important part in making sure that the datasets you own can be used as swiftly as possible '
'across the company.<br/>Make sure the metadata is correct and up to date.<br/><br/>If you '
@@ -85,85 +141,117 @@ def test_get_notification_html_added_success(self) -> None:
self.assertEqual(html, expectedHTML)

def test_get_notification_html_removed_success(self) -> None:
"""
Test successful generation of html for 'removed' notification email
:return:
"""
test_notification_type = 'removed'
test_sender = 'test@test.com'
test_options = {'resource_name': 'testtable', 'resource_url': 'testUrl'}
test_options = {'resource_name': 'testtable', 'resource_path': '/testpath'}

html = get_notification_html(notification_type=test_notification_type,
options=test_options,
sender=test_sender)
with local_app.app_context():
html = get_notification_html(notification_type=test_notification_type,
options=test_options,
sender=test_sender)
expectedHTML = ('Hello,<br/><br/>You have been removed from the owners list of the '
'<a href="testUrl">testtable</a> dataset by test@test.com.<br/><br/>If you think you '
'have been incorrectly removed as an owner, add yourself back to the owners list.<br/>'
'<br/>Thanks,<br/>Amundsen Team')
'<a href="http://0.0.0.0:5000/testpath">testtable</a> dataset by test@test.com.<br/><br/>If you'
' think you have been incorrectly removed as an owner, add yourself back to the owners list.'
'<br/><br/>Thanks,<br/>Amundsen Team')
self.assertEqual(html, expectedHTML)

def test_get_notification_html_requested_success_all_fields(self) -> None:
"""
Test successful generation of html for 'requested' notification email using
all required and optional fields
:return:
"""
test_notification_type = 'requested'
test_sender = 'test@test.com'
test_options = {
'resource_name': 'testtable',
'resource_url': 'testUrl',
'resource_path': '/testpath',
'description_requested': True,
'fields_requested': True,
'comment': 'Test Comment'
}

html = get_notification_html(notification_type=test_notification_type,
options=test_options,
sender=test_sender)
expectedHTML = ('Hello,<br/><br/>test@test.com is trying to use <a href="testUrl">testtable</a>, '
with local_app.app_context():
html = get_notification_html(notification_type=test_notification_type,
options=test_options,
sender=test_sender)
expectedHTML = ('Hello,<br/><br/>test@test.com is trying to use '
'<a href="http://0.0.0.0:5000/testpath">testtable</a>, '
'and requests improved table and column descriptions.<br/><br/>test@test.com has included the '
'following information with their request:<br/>Test Comment<br/><br/>Please visit the provided '
'link and improve descriptions on that resource.<br/><br/>Thanks,<br/>Amundsen Team')
self.assertEqual(html, expectedHTML)

def test_get_notification_html_requested_success_table_only(self) -> None:
"""
Test successful generation of html for 'requested' notification email using
all required fields and 'description_requested' optional field
:return:
"""
test_notification_type = 'requested'
test_sender = 'test@test.com'
test_options = {
'resource_name': 'testtable',
'resource_url': 'testUrl',
'resource_path': '/testpath',
'description_requested': True,
}

html = get_notification_html(notification_type=test_notification_type,
options=test_options,
sender=test_sender)
expectedHTML = ('Hello,<br/><br/>test@test.com is trying to use <a href="testUrl">testtable</a>, and requests '
with local_app.app_context():
html = get_notification_html(notification_type=test_notification_type,
options=test_options,
sender=test_sender)
expectedHTML = ('Hello,<br/><br/>test@test.com is trying to use '
'<a href="http://0.0.0.0:5000/testpath">testtable</a>, and requests '
'an improved table description.<br/><br/>Please visit the provided link and improve '
'descriptions on that resource.<br/><br/>Thanks,<br/>Amundsen Team')
self.assertEqual(html, expectedHTML)

def test_get_notification_html_requested_success_columns_only(self) -> None:
"""
Test successful generation of html for 'requested' notification email using
all required fields and 'fields_requested' optional field
:return:
"""
test_notification_type = 'requested'
test_sender = 'test@test.com'
test_options = {
'resource_name': 'testtable',
'resource_url': 'testUrl',
'resource_path': '/testpath',
'fields_requested': True,
}

html = get_notification_html(notification_type=test_notification_type,
options=test_options,
sender=test_sender)
expectedHTML = ('Hello,<br/><br/>test@test.com is trying to use <a href="testUrl">testtable</a>, and requests '
with local_app.app_context():
html = get_notification_html(notification_type=test_notification_type,
options=test_options,
sender=test_sender)
expectedHTML = ('Hello,<br/><br/>test@test.com is trying to use '
'<a href="http://0.0.0.0:5000/testpath">testtable</a>, and requests '
'improved column descriptions.<br/><br/>Please visit the provided link and improve '
'descriptions on that resource.<br/><br/>Thanks,<br/>Amundsen Team')
self.assertEqual(html, expectedHTML)

def test_get_notification_html_requested_success_no_optional_options(self) -> None:
"""
Test successful generation of html for 'requested' notification email using
all required fields and no optional fields
:return:
"""
test_notification_type = 'requested'
test_sender = 'test@test.com'
test_options = {
'resource_name': 'testtable',
'resource_url': 'testUrl',
'resource_path': '/testpath',
}

html = get_notification_html(notification_type=test_notification_type,
options=test_options,
sender=test_sender)
expectedHTML = ('Hello,<br/><br/>test@test.com is trying to use <a href="testUrl">testtable</a>, and requests '
with local_app.app_context():
html = get_notification_html(notification_type=test_notification_type,
options=test_options,
sender=test_sender)
expectedHTML = ('Hello,<br/><br/>test@test.com is trying to use '
'<a href="http://0.0.0.0:5000/testpath">testtable</a>, and requests '
'more information about that resource.<br/><br/>Please visit the provided link and improve '
'descriptions on that resource.<br/><br/>Thanks,<br/>Amundsen Team')
self.assertEqual(html, expectedHTML)

0 comments on commit 0378acb

Please sign in to comment.