diff --git a/data/database.py b/data/database.py index a4adabc7b..80e47bf2e 100644 --- a/data/database.py +++ b/data/database.py @@ -645,6 +645,9 @@ class LogEntry(BaseModel): indexes = ( # create an index on repository and date (('repository', 'datetime'), False), + + # create an index on repository, date and kind + (('repository', 'datetime', 'kind'), False), ) diff --git a/data/migrations/migration.sh b/data/migrations/migration.sh index c13ca5b84..9d20c5a6a 100755 --- a/data/migrations/migration.sh +++ b/data/migrations/migration.sh @@ -10,8 +10,8 @@ up_mysql() { docker run --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=password -d mysql # Sleep for 10s to get MySQL get started. - echo 'Sleeping for 10...' - sleep 10 + echo 'Sleeping for 20...' + sleep 20 # Add the database to mysql. docker run --rm --link mysql:mysql mysql sh -c 'echo "create database genschema" | mysql -h"$MYSQL_PORT_3306_TCP_ADDR" -P"$MYSQL_PORT_3306_TCP_PORT" -uroot -ppassword' diff --git a/data/migrations/versions/5232a5610a0a_add_logentry_repo_datetime_kind_index.py b/data/migrations/versions/5232a5610a0a_add_logentry_repo_datetime_kind_index.py new file mode 100644 index 000000000..c8bff8bbd --- /dev/null +++ b/data/migrations/versions/5232a5610a0a_add_logentry_repo_datetime_kind_index.py @@ -0,0 +1,26 @@ +"""Add LogEntry repo-datetime-kind index + +Revision ID: 5232a5610a0a +Revises: 437ee6269a9d +Create Date: 2015-07-31 13:25:41.877733 + +""" + +# revision identifiers, used by Alembic. +revision = '5232a5610a0a' +down_revision = '437ee6269a9d' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(tables): + ### commands auto generated by Alembic - please adjust! ### + op.create_index('logentry_repository_id_datetime_kind_id', 'logentry', ['repository_id', 'datetime', 'kind_id'], unique=False) + ### end Alembic commands ### + + +def downgrade(tables): + ### commands auto generated by Alembic - please adjust! ### + op.drop_index('logentry_repository_id_datetime_kind_id', table_name='logentry') + ### end Alembic commands ### diff --git a/data/model/log.py b/data/model/log.py index 75bab526d..361a0c408 100644 --- a/data/model/log.py +++ b/data/model/log.py @@ -5,18 +5,13 @@ from datetime import datetime, timedelta, date from data.database import LogEntry, LogEntryKind, User - -def list_logs(start_time, end_time, performer=None, repository=None, namespace=None): - Performer = User.alias() +def _logs_query(selections, start_time, end_time, performer=None, repository=None, namespace=None): joined = (LogEntry - .select(LogEntry, LogEntryKind, User, Performer) - .join(User) - .switch(LogEntry) - .join(Performer, JOIN_LEFT_OUTER, - on=(LogEntry.performer == Performer.id).alias('performer')) + .select(*selections) .switch(LogEntry) .join(LogEntryKind) - .switch(LogEntry)) + .switch(LogEntry) + .where(LogEntry.datetime >= start_time, LogEntry.datetime < end_time)) if repository: joined = joined.where(LogEntry.repository == repository) @@ -25,10 +20,32 @@ def list_logs(start_time, end_time, performer=None, repository=None, namespace=N joined = joined.where(LogEntry.performer == performer) if namespace: - joined = joined.where(User.username == namespace) + joined = joined.join(User).where(User.username == namespace) - return list(joined.where(LogEntry.datetime >= start_time, - LogEntry.datetime < end_time).order_by(LogEntry.datetime.desc())) + return joined + + +def get_aggregated_logs(start_time, end_time, performer=None, repository=None, namespace=None): + selections = [LogEntryKind, fn.date(LogEntry.datetime, '%d'), fn.Count(LogEntry.id).alias('count')] + query = _logs_query(selections, start_time, end_time, performer, repository, namespace) + return query.group_by(fn.date(LogEntry.datetime, '%d'), LogEntryKind) + + +def list_logs(start_time, end_time, performer=None, repository=None, namespace=None, page=None, + count=None): + + Performer = User.alias() + selections = [LogEntry, LogEntryKind, Performer] + + query = _logs_query(selections, start_time, end_time, performer, repository, namespace) + query = (query.switch(LogEntry) + .join(Performer, JOIN_LEFT_OUTER, + on=(LogEntry.performer == Performer.id).alias('performer'))) + + if page and count: + query = query.paginate(page, count) + + return list(query.order_by(LogEntry.datetime.desc())) def log_action(kind_name, user_or_organization_name, performer=None, repository=None, diff --git a/endpoints/api/logs.py b/endpoints/api/logs.py index e1685a611..01eab8c33 100644 --- a/endpoints/api/logs.py +++ b/endpoints/api/logs.py @@ -14,6 +14,7 @@ from data import model from auth import scopes from app import avatar +LOGS_PER_PAGE = 500 def log_view(log): view = { @@ -33,8 +34,16 @@ def log_view(log): return view +def aggregated_log_view(log): + view = { + 'kind': log.kind.name, + 'count': log.count, + 'datetime': format_date(log.datetime) + } -def get_logs(start_time, end_time, performer_name=None, repository=None, namespace=None): + return view + +def _validate_logs_arguments(start_time, end_time, performer_name): performer = None if performer_name: performer = model.user.get_user(performer_name) @@ -58,12 +67,31 @@ def get_logs(start_time, end_time, performer_name=None, repository=None, namespa if not end_time: end_time = datetime.today() + return (start_time, end_time, performer) + + +def get_logs(start_time, end_time, performer_name=None, repository=None, namespace=None, page=None): + (start_time, end_time, performer) = _validate_logs_arguments(start_time, end_time, performer_name) + page = page if page else 1 logs = model.log.list_logs(start_time, end_time, performer=performer, repository=repository, - namespace=namespace) + namespace=namespace, page=page, count=LOGS_PER_PAGE + 1) + return { 'start_time': format_date(start_time), 'end_time': format_date(end_time), - 'logs': [log_view(log) for log in logs] + 'logs': [log_view(log) for log in logs[0:LOGS_PER_PAGE]], + 'page': page, + 'has_additional': len(logs) > LOGS_PER_PAGE, + } + +def get_aggregate_logs(start_time, end_time, performer_name=None, repository=None, namespace=None): + (start_time, end_time, performer) = _validate_logs_arguments(start_time, end_time, performer_name) + + aggregated_logs = model.log.get_aggregated_logs(start_time, end_time, performer=performer, + repository=repository, namespace=namespace) + + return { + 'aggregated': [aggregated_log_view(log) for log in aggregated_logs] } @@ -76,6 +104,7 @@ class RepositoryLogs(RepositoryParamResource): @parse_args @query_param('starttime', 'Earliest time from which to get logs (%m/%d/%Y %Z)', type=str) @query_param('endtime', 'Latest time to which to get logs (%m/%d/%Y %Z)', type=str) + @query_param('page', 'The page number for the logs', type=int, default=1) def get(self, args, namespace, repository): """ List the logs for the specified repository. """ repo = model.repository.get_repository(namespace, repository) @@ -84,7 +113,7 @@ class RepositoryLogs(RepositoryParamResource): start_time = args['starttime'] end_time = args['endtime'] - return get_logs(start_time, end_time, repository=repo, namespace=namespace) + return get_logs(start_time, end_time, repository=repo, page=args['page']) @resource('/v1/user/logs') @@ -97,6 +126,7 @@ class UserLogs(ApiResource): @query_param('starttime', 'Earliest time from which to get logs. (%m/%d/%Y %Z)', type=str) @query_param('endtime', 'Latest time to which to get logs. (%m/%d/%Y %Z)', type=str) @query_param('performer', 'Username for which to filter logs.', type=str) + @query_param('page', 'The page number for the logs', type=int, default=1) def get(self, args): """ List the logs for the current user. """ performer_name = args['performer'] @@ -104,7 +134,8 @@ class UserLogs(ApiResource): end_time = args['endtime'] user = get_authenticated_user() - return get_logs(start_time, end_time, performer_name=performer_name, namespace=user.username) + return get_logs(start_time, end_time, performer_name=performer_name, namespace=user.username, + page=args['page']) @resource('/v1/organization//logs') @@ -117,6 +148,7 @@ class OrgLogs(ApiResource): @query_param('starttime', 'Earliest time from which to get logs. (%m/%d/%Y %Z)', type=str) @query_param('endtime', 'Latest time to which to get logs. (%m/%d/%Y %Z)', type=str) @query_param('performer', 'Username for which to filter logs.', type=str) + @query_param('page', 'The page number for the logs', type=int, default=1) @require_scope(scopes.ORG_ADMIN) def get(self, args, orgname): """ List the logs for the specified organization. """ @@ -126,6 +158,73 @@ class OrgLogs(ApiResource): start_time = args['starttime'] end_time = args['endtime'] - return get_logs(start_time, end_time, namespace=orgname, performer_name=performer_name) + return get_logs(start_time, end_time, namespace=orgname, performer_name=performer_name, + page=args['page']) + + raise Unauthorized() + + +@resource('/v1/repository//aggregatelogs') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') +class RepositoryAggregateLogs(RepositoryParamResource): + """ Resource for fetching aggregated logs for the specific repository. """ + @require_repo_admin + @nickname('getAggregateRepoLogs') + @parse_args + @query_param('starttime', 'Earliest time from which to get logs (%m/%d/%Y %Z)', type=str) + @query_param('endtime', 'Latest time to which to get logs (%m/%d/%Y %Z)', type=str) + def get(self, args, namespace, repository): + """ Returns the aggregated logs for the specified repository. """ + repo = model.repository.get_repository(namespace, repository) + if not repo: + raise NotFound() + + start_time = args['starttime'] + end_time = args['endtime'] + return get_aggregate_logs(start_time, end_time, repository=repo) + + +@resource('/v1/user/aggregatelogs') +@internal_only +class UserAggregateLogs(ApiResource): + """ Resource for fetching aggregated logs for the current user. """ + @require_user_admin + @nickname('getAggregateUserLogs') + @parse_args + @query_param('starttime', 'Earliest time from which to get logs. (%m/%d/%Y %Z)', type=str) + @query_param('endtime', 'Latest time to which to get logs. (%m/%d/%Y %Z)', type=str) + @query_param('performer', 'Username for which to filter logs.', type=str) + def get(self, args): + """ Returns the aggregated logs for the current user. """ + performer_name = args['performer'] + start_time = args['starttime'] + end_time = args['endtime'] + + user = get_authenticated_user() + return get_aggregate_logs(start_time, end_time, performer_name=performer_name, + namespace=user.username) + + +@resource('/v1/organization//aggregatelogs') +@path_param('orgname', 'The name of the organization') +@related_user_resource(UserLogs) +class OrgAggregateLogs(ApiResource): + """ Resource for fetching aggregate logs for the entire organization. """ + @nickname('getAggregateOrgLogs') + @parse_args + @query_param('starttime', 'Earliest time from which to get logs. (%m/%d/%Y %Z)', type=str) + @query_param('endtime', 'Latest time to which to get logs. (%m/%d/%Y %Z)', type=str) + @query_param('performer', 'Username for which to filter logs.', type=str) + @require_scope(scopes.ORG_ADMIN) + def get(self, args, orgname): + """ Gets the aggregated logs for the specified organization. """ + permission = AdministerOrganizationPermission(orgname) + if permission.can(): + performer_name = args['performer'] + start_time = args['starttime'] + end_time = args['endtime'] + + return get_aggregate_logs(start_time, end_time, namespace=orgname, + performer_name=performer_name) raise Unauthorized() diff --git a/static/css/directives/ui/logs-view.css b/static/css/directives/ui/logs-view.css index db77c9b23..4175821d3 100644 --- a/static/css/directives/ui/logs-view.css +++ b/static/css/directives/ui/logs-view.css @@ -71,7 +71,7 @@ height: 12px; border-radius: 50%; margin-right: 6px; - margin-top: 6px; + margin-top: 7px; vertical-align: middle; float: left; } diff --git a/static/directives/logs-view.html b/static/directives/logs-view.html index 6e6532a5e..3fab56d6a 100644 --- a/static/directives/logs-view.html +++ b/static/directives/logs-view.html @@ -14,29 +14,27 @@
+
-
-
+
@@ -44,14 +42,15 @@ - + - +
DescriptionUser/Token/App
- - + + {{ log.datetime }} - +
- + - + - {{ log.metadata.token }} + - + (anonymous)
+
diff --git a/static/js/app.js b/static/js/app.js index 26a779c6e..a0c4fb3fd 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -37,7 +37,7 @@ quayPages.constant('pages', { quayDependencies = ['ngRoute', 'chieffancypants.loadingBar', 'cfp.hotkeys', 'angular-tour', 'restangular', 'angularMoment', 'mgcrea.ngStrap', 'ngCookies', 'ngSanitize', 'angular-md5', 'pasvaz.bindonce', 'ansiToHtml', 'debounce', - 'core-ui', 'core-config-setup', 'quayPages']; + 'core-ui', 'core-config-setup', 'quayPages', 'infinite-scroll']; if (window.__config && window.__config.MIXPANEL_KEY) { quayDependencies.push('angulartics'); diff --git a/static/js/directives/ui/feedback-bar.js b/static/js/directives/ui/feedback-bar.js index ede1d9e3b..d26ab0b25 100644 --- a/static/js/directives/ui/feedback-bar.js +++ b/static/js/directives/ui/feedback-bar.js @@ -17,7 +17,7 @@ angular.module('quay').directive('feedbackBar', function () { $scope.$watch('feedback', function(feedback) { if (feedback) { - $scope.formattedMessage = StringBuilderService.buildString(feedback.message, feedback.data || {}, 'span'); + $scope.formattedMessage = StringBuilderService.buildTrustedString(feedback.message, feedback.data || {}, 'span'); $scope.viewCounter++; } else { $scope.viewCounter = 0; diff --git a/static/js/directives/ui/logs-view.js b/static/js/directives/ui/logs-view.js index 0ce684106..44b021a41 100644 --- a/static/js/directives/ui/logs-view.js +++ b/static/js/directives/ui/logs-view.js @@ -22,7 +22,8 @@ angular.module('quay').directive('logsView', function () { $scope.logs = null; $scope.kindsAllowed = null; $scope.chartVisible = true; - $scope.logsPath = ''; + $scope.chartLoading = true; + $scope.currentPage = 1; $scope.options = {}; @@ -253,6 +254,29 @@ angular.module('quay').directive('logsView', function () { return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days); }; + var getUrl = function(suffix) { + var url = UtilService.getRestUrl('user/' + suffix); + if ($scope.organization) { + url = UtilService.getRestUrl('organization', $scope.organization.name, suffix); + } + if ($scope.repository) { + url = UtilService.getRestUrl('repository', $scope.repository.namespace, $scope.repository.name, suffix); + } + + if ($scope.allLogs) { + url = UtilService.getRestUrl('superuser', suffix) + } + + url += '?starttime=' + encodeURIComponent(getDateString($scope.options.logStartDate)); + url += '&endtime=' + encodeURIComponent(getDateString($scope.options.logEndDate)); + + if ($scope.performer) { + url += '&performer=' + encodeURIComponent($scope.performer.name); + } + + return url; + }; + var update = function() { var hasValidUser = !!$scope.user; var hasValidOrg = !!$scope.organization; @@ -269,44 +293,44 @@ angular.module('quay').directive('logsView', function () { $scope.options.logStartDate = twoWeeksAgo; } + $scope.chartLoading = true; + + var aggregateUrl = getUrl('aggregatelogs') + + var loadAggregate = Restangular.one(aggregateUrl); + loadAggregate.customGET().then(function(resp) { + $scope.chart = new LogUsageChart(logKinds); + $($scope.chart).bind('filteringChanged', function(e) { + $scope.$apply(function() { $scope.kindsAllowed = e.allowed; }); + }); + + $scope.chart.draw('bar-chart', resp.aggregated, $scope.options.logStartDate, + $scope.options.logEndDate); + $scope.chartLoading = false; + }); + + $scope.currentPage = 0; + $scope.hasAdditional = true; + $scope.loading = false; + $scope.logs = []; + $scope.nextPage(); + }; + + $scope.nextPage = function() { + if ($scope.loading) { return; } + $scope.loading = true; - // Note: We construct the URLs here manually because we also use it for the download - // path. - var url = UtilService.getRestUrl('user/logs'); - if ($scope.organization) { - url = UtilService.getRestUrl('organization', $scope.organization.name, 'logs'); - } - if ($scope.repository) { - url = UtilService.getRestUrl('repository', $scope.repository.namespace, $scope.repository.name, 'logs'); - } - - if ($scope.allLogs) { - url = UtilService.getRestUrl('superuser', 'logs') - } - - url += '?starttime=' + encodeURIComponent(getDateString($scope.options.logStartDate)); - url += '&endtime=' + encodeURIComponent(getDateString($scope.options.logEndDate)); - - if ($scope.performer) { - url += '&performer=' + encodeURIComponent($scope.performer.name); - } - - var loadLogs = Restangular.one(url); + var logsUrl = getUrl('logs') + '&page=' + ($scope.currentPage + 1); + var loadLogs = Restangular.one(logsUrl); loadLogs.customGET().then(function(resp) { - $scope.logsPath = '/api/v1/' + url; + resp.logs.forEach(function(log) { + $scope.logs.push(log); + }); - if (!$scope.chart) { - $scope.chart = new LogUsageChart(logKinds); - $($scope.chart).bind('filteringChanged', function(e) { - $scope.$apply(function() { $scope.kindsAllowed = e.allowed; }); - }); - } - - $scope.chart.draw('bar-chart', resp.logs, $scope.options.logStartDate, $scope.options.logEndDate); - $scope.kindsAllowed = null; - $scope.logs = resp.logs; $scope.loading = false; + $scope.currentPage = resp.page; + $scope.hasAdditional = resp.has_additional; }); }; @@ -318,8 +342,9 @@ angular.module('quay').directive('logsView', function () { return allowed == null || allowed.hasOwnProperty(kind); }; - $scope.getColor = function(kind) { - return $scope.chart.getColor(kind); + $scope.getColor = function(kind, chart) { + if (!chart) { return ''; } + return chart.getColor(kind); }; $scope.getDescription = function(log) { diff --git a/static/js/graphing.js b/static/js/graphing.js index 82ed57cc0..94f97fa20 100644 --- a/static/js/graphing.js +++ b/static/js/graphing.js @@ -1616,22 +1616,22 @@ UsageChart.prototype.draw = function(container) { function LogUsageChart(titleMap) { this.titleMap_ = titleMap; this.colorScale_ = d3.scale.category20(); + this.entryMap_ = {}; } /** * Builds the D3-representation of the data. */ -LogUsageChart.prototype.buildData_ = function(logs) { +LogUsageChart.prototype.buildData_ = function(aggregatedLogs) { var parseDate = d3.time.format("%a, %d %b %Y %H:%M:%S %Z").parse // Build entries for each kind of event that occurred, on each day. We have one // entry per {kind, day} pair. - var map = {}; var entries = []; - for (var i = 0; i < logs.length; ++i) { - var log = logs[i]; - var title = this.titleMap_[log.kind] || log.kind; - var datetime = parseDate(log.datetime); + for (var i = 0; i < aggregatedLogs.length; ++i) { + var aggregated = aggregatedLogs[i]; + var title = this.titleMap_[aggregated.kind] || aggregated.kind; + var datetime = parseDate(aggregated.datetime); var dateDay = datetime.getDate(); if (dateDay < 10) { dateDay = '0' + dateDay; @@ -1640,24 +1640,18 @@ LogUsageChart.prototype.buildData_ = function(logs) { var formatted = (datetime.getMonth() + 1) + '/' + dateDay; var adjusted = new Date(datetime.getFullYear(), datetime.getMonth(), datetime.getDate()); var key = title + '_' + formatted; - var found = map[key]; - if (!found) { - found = { - 'kind': log.kind, - 'title': title, - 'adjusted': adjusted, - 'formatted': datetime.getDate(), - 'count': 0 - }; + var entry = { + 'kind': aggregated.kind, + 'title': title, + 'adjusted': adjusted, + 'formatted': datetime.getDate(), + 'count': aggregated.count + }; - map[key] = found; - entries.push(found); - } - found['count']++; + entries.push(entry); + this.entryMap_[key] = entry; } - this.entries_ = map; - // Build the data itself. We create a single entry for each possible kind of data, and then add (x, y) pairs // for the number of times that kind of event occurred on a particular day. var dataArray = []; @@ -1727,7 +1721,7 @@ LogUsageChart.prototype.renderTooltip_ = function(d, e) { } var key = d + '_' + e; - var entry = this.entries_[key]; + var entry = this.entryMap_[key]; if (!entry) { entry = {'count': 0}; } diff --git a/static/js/services/notification-service.js b/static/js/services/notification-service.js index 7c28d9efd..08a7381a6 100644 --- a/static/js/services/notification-service.js +++ b/static/js/services/notification-service.js @@ -174,7 +174,7 @@ function($rootScope, $interval, UserService, ApiService, StringBuilderService, P if (!kindInfo) { return '(Unknown notification kind: ' + notification['kind'] + ')'; } - return StringBuilderService.buildString(kindInfo['message'], notification['metadata']); + return StringBuilderService.buildTrustedString(kindInfo['message'], notification['metadata']); }; notificationService.getClass = function(notification) { diff --git a/static/js/services/string-builder-service.js b/static/js/services/string-builder-service.js index 333ea88f5..9cd229a6b 100644 --- a/static/js/services/string-builder-service.js +++ b/static/js/services/string-builder-service.js @@ -39,6 +39,10 @@ angular.module('quay').factory('StringBuilderService', ['$sce', 'UtilService', f return url; }; + stringBuilderService.buildTrustedString = function(value_or_func, metadata, opt_codetag) { + return $sce.trustAsHtml(stringBuilderService.buildString(value_or_func, metadata, opt_codetag)); + }; + stringBuilderService.buildString = function(value_or_func, metadata, opt_codetag) { var fieldIcons = { 'inviter': 'user', @@ -113,7 +117,7 @@ angular.module('quay').factory('StringBuilderService', ['$sce', 'UtilService', f '<' + codeTag + ' title="' + safe + '">' + markedDown + ''); } } - return $sce.trustAsHtml(description.replace('\n', '
')); + return description.replace('\n', '
'); }; return stringBuilderService; diff --git a/static/lib/LICENSES b/static/lib/LICENSES index 498a26df9..77450b6e0 100644 --- a/static/lib/LICENSES +++ b/static/lib/LICENSES @@ -22,6 +22,7 @@ jquery.overscroll - MIT (https://github.com/azoff/overscroll/blob/master/mit.lic URI.js - MIT (https://github.com/medialize/URI.js) angular-hotkeys - MIT (https://github.com/chieffancypants/angular-hotkeys/blob/master/LICENSE) angular-debounce - MIT (https://github.com/shahata/angular-debounce/blob/master/LICENSE) +infinite-scroll - MIT (https://github.com/sroze/ngInfiniteScroll/blob/master/LICENSE) Issues: >>>>> jquery.spotlight - GPLv3 (https://github.com/jameshalsall/jQuery-Spotlight) \ No newline at end of file diff --git a/static/lib/ng-infinite-scroll.min.js b/static/lib/ng-infinite-scroll.min.js new file mode 100644 index 000000000..cbe2f801f --- /dev/null +++ b/static/lib/ng-infinite-scroll.min.js @@ -0,0 +1,2 @@ +/* ng-infinite-scroll - v1.2.0 - 2015-02-14 */ +var mod;mod=angular.module("infinite-scroll",[]),mod.value("THROTTLE_MILLISECONDS",null),mod.directive("infiniteScroll",["$rootScope","$window","$interval","THROTTLE_MILLISECONDS",function(a,b,c,d){return{scope:{infiniteScroll:"&",infiniteScrollContainer:"=",infiniteScrollDistance:"=",infiniteScrollDisabled:"=",infiniteScrollUseDocumentBottom:"=",infiniteScrollListenForEvent:"@"},link:function(e,f,g){var h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y;return y=angular.element(b),t=null,u=null,i=null,j=null,q=!0,x=!1,w=null,p=function(a){return a=a[0]||a,isNaN(a.offsetHeight)?a.document.documentElement.clientHeight:a.offsetHeight},r=function(a){return a[0].getBoundingClientRect&&!a.css("none")?a[0].getBoundingClientRect().top+s(a):void 0},s=function(a){return a=a[0]||a,isNaN(window.pageYOffset)?a.document.documentElement.scrollTop:a.ownerDocument.defaultView.pageYOffset},o=function(){var b,c,d,g,h;return j===y?(b=p(j)+s(j[0].document.documentElement),d=r(f)+p(f)):(b=p(j),c=0,void 0!==r(j)&&(c=r(j)),d=r(f)-c+p(f)),x&&(d=p((f[0].ownerDocument||f[0].document).documentElement)),g=d-b,h=g<=p(j)*t+1,h?(i=!0,u?e.$$phase||a.$$phase?e.infiniteScroll():e.$apply(e.infiniteScroll):void 0):i=!1},v=function(a,b){var d,e,f;return f=null,e=0,d=function(){var b;return e=(new Date).getTime(),c.cancel(f),f=null,a.call(),b=null},function(){var g,h;return g=(new Date).getTime(),h=b-(g-e),0>=h?(clearTimeout(f),c.cancel(f),f=null,e=g,a.call()):f?void 0:f=c(d,h,1)}},null!=d&&(o=v(o,d)),e.$on("$destroy",function(){return j.unbind("scroll",o),null!=w?(w(),w=null):void 0}),m=function(a){return t=parseFloat(a)||0},e.$watch("infiniteScrollDistance",m),m(e.infiniteScrollDistance),l=function(a){return u=!a,u&&i?(i=!1,o()):void 0},e.$watch("infiniteScrollDisabled",l),l(e.infiniteScrollDisabled),n=function(a){return x=a},e.$watch("infiniteScrollUseDocumentBottom",n),n(e.infiniteScrollUseDocumentBottom),h=function(a){return null!=j&&j.unbind("scroll",o),j=a,null!=a?j.bind("scroll",o):void 0},h(y),e.infiniteScrollListenForEvent&&(w=a.$on(e.infiniteScrollListenForEvent,o)),k=function(a){if(null!=a&&0!==a.length){if(a instanceof HTMLElement?a=angular.element(a):"function"==typeof a.append?a=angular.element(a[a.length-1]):"string"==typeof a&&(a=angular.element(document.querySelector(a))),null!=a)return h(a);throw new Exception("invalid infinite-scroll-container attribute.")}},e.$watch("infiniteScrollContainer",k),k(e.infiniteScrollContainer||[]),null!=g.infiniteScrollParent&&h(angular.element(f.parent())),null!=g.infiniteScrollImmediateCheck&&(q=e.$eval(g.infiniteScrollImmediateCheck)),c(function(){return q?o():void 0},0,1)}}}]); \ No newline at end of file