PaiWeb
or
HUB.coupons.com
(Tech talk 5/8/15)
What is it?
- Web Application Platform
- Designed to be used by internal (Support) & external (Client & Partners) users
- Vision:
- single touch point for clients and partners
- consolidation of 13 tools used in offer provisioning and IO/account management
What it will be used for and when?
V1: manage IOs, Invoices, Support tickets
V2/V3: Offer creation
V4: Reporting
V5+: Convert all client tools to the HUB
Which Data Sources are used?
Platfom APIs
SalesForce APIs
Local DB
How does it look? (Demo)

Design mocks

Design

TEch stack

TECH stack Client side

Code Samples HTML
Angular attributes
<div class="btn-group list-filter">
<a class="btn btn-default btn-sm dropdown-toggle"
data-toggle="dropdown" href="#">
{{ selectedFilterName }}
<span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li ng-repeat="f in filters">
<a href="#" ng-click="selectFilter(f.id)">
{{ f.name }}
</a>
</li>
</ul>
</div>
KENDO ATTRIBUTES
using Angular-Kendo adapter
<div class="k-grid k-widget" kendo-grid id="k-grid" k-data-source="invoices" k-sortable="true" k-resizable="true" k-pageable="pagination" k-columns="columns" data-role="grid" k-on-data-binding="gridInit(kendoEvent)"></div>
code samples JS
Angularjs controller
angular.module('paiApp').controller('SearchPageCtrl',
['$scope','Ios','Invoices','Tickets', 'Msgs',
function($scope, Ios, Invoices, Tickets, Msgs) {
//-- error handler
if(PAI_CONTEXT.HAS_VIEW_ERR == 1) {
Msgs.addDataLoadErr({'detail': PAI_CONTEXT.ERR_DESC}, 'Search Results');
}
//==== list/grid support ====
$scope.data = PAI_CONTEXT.SEARCH_RESULTS ||
{"Insertion_Order__c":[],"Invoice_CI__c":[],"Case":[]};
$scope.io_columns = Ios.columns();
$scope.invoice_columns = Invoices.columns();
$scope.ticket_columns = Tickets.columns();
$scope.ios = new kendo.data.DataSource({
data: $scope.data.Insertion_Order__c,
schema : { model: { fields: {
CreatedDate : { type: "date" },
Commit_Amount__c : { type: "number" }
} } },
pageSize: 5
});
$scope.invoices = new kendo.data.DataSource({
data: $scope.data.Invoice_CI__c,
schema : { model: { fields: {
Due_Date__c : { type: "date" },
Amount_Due__c : { type: "number" }
} } },
pageSize: 5
});
$scope.tickets = new kendo.data.DataSource({
data: $scope.data.Case,
schema : { model: { fields: { CreatedDate__c : { type: "date" } } } },
pageSize: 5
});
}]); //SearchPageCtrl
ANgularjs service
paiApp.factory('RecentlyViewedServ',
['Restangular', function(Restangular) {
var mockData = [
{date: "2014/01/13", title: "SF-27857", link: "/account/SF-27857"},
{date: "2014/01/17", title: "Account: Kraft Foods", link: "/account/SF-27857"},
{date: "2014/01/27", title: "SF-27857", link: "/account/SF-27857"},
{date: "2014/01/10", title: "Account: Kraft Foods", link: "/account/SF-27857"},
{date: "2014/01/09", title: "Account: Kraft Foods", link: "/account/SF-27857"}
];
//-- public API
return {
getMockRecentItems: function() {
return mockData;
},
getRecentItems: function(item_count) {
return Restangular.one('api/sections/recent_items',item_count).get();
},
postRecentItem: function(item) {
var obj = Restangular.one('api/sections/recent_items');
obj.url = item.url;
obj.title = item.title;
return obj.post();
}
};
}]);
angularjs filter
/**
* angular filter to display default value if the property is empty(falsy)
* usage:
* {{ value | default: 'defaultValue' }}
*/
angular.module('paiApp').filter('default', function() {
return function(input, defaultVal) {
return (input) ? input : defaultVal;
}
});
Paiweb utils
addKendoTopPaging: function(event, pagination) {
var grid = event.sender, //grid from kendo event
g = $("#k-grid"), //grid from jquery selector
pager = $('#div .k-pager-wrap'), //regular bottom pager
pagerTop = $("#pager_top"), //additional top pager
nextElem = g.find(".k-grouping-header"); // if grouping enabled
if (nextElem.length == 0)
nextElem = g.find(".k-grid-header"); // if grouping disabled
if (pagerTop.length == 0) {
var newPager = $('<div id="pager_top" class=k-pager-wrap pagerTop" />')
.insertBefore(nextElem);
g.topPager = new kendo.ui.Pager(newPager, $.extend({},
pagination, { dataSource: grid.dataSource }));
g.topPager.refresh(); // DataSource change not fired, call this manually
}
},
server side Front end

Base template
{% load sekizai_tags %}
<!doctype html>
{% block adds %}{% endblock adds %}
<head>
{% render_block "meta" %}
<title>
{% block title %}Hub.coupons{% endblock title %}
</title>
{% render_block "css" %}
</head>
<body>
{% block body %}
{% block content %}
Base Html template
{% endblock content %}
{% endblock body %}
{% render_block "js" %}
</body>
</html>
ADDTOBLOCK
{% addtoblock "css" %}<link href="{{STATIC_URL}}vendor/css/bootstrap-3.0.3.min.css" rel="stylesheet" type="text/css"> <link href="{{STATIC_URL}}app/css/paiw-site.css" rel="stylesheet" type="text/css"> <link href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-glyphicons.css" rel="stylesheet">{% endaddtoblock %}{% addtoblock "js" %} <script type="text/javascript"> PAI_CONTEXT.OBJ_ID = "{{ id }}"; </script> <script type="text/javascript" src="{{ STATIC_URL }}app/js/io/ioServ.js"></script> <script type="text/javascript" src="{{ STATIC_URL }}app/js/io/ioCtrl.js"></script>{% endaddtoblock %}
common page TEmplates
{% block body %}
<div ng-app="paiApp" class="pai-application waiting-for-angular">
<div id="initializing-panel"></div>
<div id="content-outer" class="page-container" ng-controller="AuthCtrl">
{% block app_header %}
<div class="header container-fluid">
{% include "component/header.html" %}
{% main_nav view_id %}
</div>
{% endblock app_header %}
<div id="content" style="">
{% block content %}{% endblock %}
</div>
<div class="clear"></div>
{% block footer %}
{% include "component/footer.html" %}
{% endblock footer %}
</div>
{% block bottom %}{# add custom markup needed in а page like modal dialog sections #}
{% endblock %}
</div>
{% endblock %}
Specific page part
{% block right_pane %}
<div ng-controller="IoCtrl">
<section>
<p class="pane-header">
<span> {% trans "Insertion Order" %}: </span>
<span> {% verbatim %}{{ io.Name }}{% endverbatim %}</span>
</p>
<p>
<a class="back-link" href="#" ng-click="backToList()">< {% trans "Back to List: Insertion Orders" %}</a>
</p>
{% include "component/create_ticket_modal.html" %}
</section>
<section>
<h4 class="section-header">
{% trans "IO Information" %}
</h4>
...
SASS example
@import "compass" /* -------- Variables */ $text-color: #333 $quiet-color: #ccc
$mid-color: #555
$loud-color: #111 $heading-color: #111
/* ---- collapsible containers --- */
.panel-heading
.panel-title
font-size: 18px
color: $mid-color
font-weight: bold
&:hover, &:visited:hover
color: $mid-color
text-decoration: underline
&:hover
.accordion-toggle:after
/* symbol for "opening" panels */
font-family: 'Glyphicons Halflings' /* for enabling glyphicon */
content: "\e114" /* adjust as needed, taken from bootstrap.css */
float: right /* adjust as needed */
color: grey /* adjust as needed */
.accordion-toggle.collapsed:after
/* symbol for "collapsed" panels */
content: "\e080" /* adjust as needed, taken from bootstrap.css */
Server side architecture

SINGLE SIGN ON (SSO)

Server side control flow

Testing technologies
Django Unit Test Framework
Mock library for python
Coverage library for Python
JS unit testing Karma for Angular
E2E testing Protractor + Selenium
Grunt task runner
code coverage
Report in PyCharm

code coverage (cont.)
Report in a browser

Code coverage (cont.)
Coverage details

Pulling all together: Request flow

Q & A
Copy of PaiWeb
By pbosin
Copy of PaiWeb
- 987