PageSourceSearch

https://bio.tools/dist/app.min.js

js bio.tools collected 2026-10-02 03:20:16 UTC 330,543 bytes, 9,370 lines download raw bytes

1'use strict';
2
3var elixir_front = angular.module('elixir_front', [
4	'elixir_front.filters',
5	'elixir_front.services',
6	'elixir_front.directives',
7	'elixir_front.controllers',
8	'ui.bootstrap',
9	'ngCookies',
10	'ngRoute',
11	'ngSanitize',
12	'ngAnimate',
13	'ngResource',
14	'ui.router',
15	'angularBootstrapNavTree',
16	'treeControl',
17	'720kb.tooltips',
18	'yaru22.angular-timeago',
19	'angularDjangoRegistrationAuthApp',
20	'ngTagsInput',
21	'pasvaz.bindonce',
22	'ui.grid',
23	'ui.grid.resizeColumns',
24	'ui.grid.autoResize',
25	'ui.grid.saveState',
26	'ui.grid.moveColumns',
27	'chart.js',
28	'ngMeta'
29], function($rootScopeProvider) {})
30
31// setting up router and states
32.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {
33	// default state
34	$urlRouterProvider.when('', '/');
35	// for any unmatched url, send to 404
36	$urlRouterProvider.otherwise('/404');
37	$stateProvider
38	.state('404', {
39		url: "/404",
40		templateUrl: "partials/404.html",
41		data: {
42			meta: {
43				'title': 'Page not found'
44		},
45			roles: []
46		},
47		resolve: {}
48	})
49	.state('orcid_callback', {
50		url: "/orcid/callback/",
51		templateUrl: "partials/orcidCallback.html",
52		controller: 'OrcidCallbackController'
53	})
54	.state('home', {
55		url: "/",
56		templateUrl: "components/home/home.html",
57		data: {
58			meta: {
59				'title': 'bio.tools · Bioinformatics Tools and Services Discovery Portal',
60				'description': 'A registry of bioinformatics software resources including biological databases, analytical tools and data services',
61				'titleSuffix': '',
62				'og:title': 'bio.tools · Bioinformatics Tools and Services Discovery Portal',
63				'og:description': 'A registry of bioinformatics software resources including biological databases, analytical tools and data services',
64				'og:image': 'https://bio.tools/img/ELIXIR_logo_white_background_small.png'
65			},
66			roles: []
67		},
68		resolve: {}
69	})
70	.state('about', {
71		url: "/about",
72		templateUrl: "components/about/about.html",
73		data: {
74			meta: {
75				'title': 'About',
76				'description': 'A registry of bioinformatics software resources including biological databases, analytical tools and data services'
77			},
78			roles: []
79		},
80		resolve: {}
81	})
82	.state('community', {
83		url: "/community",
84		templateUrl: "components/community/community.html",
85		data: {
86			meta: {
87				'title': 'Communities in bio.tools',
88				'description': 'bio.tools and communities'
89			},
90			roles: []
91		},
92		resolve: {}
93	})
94	.state('communities', {
95		url: "/communities",
96		templateUrl: "components/community/community.html",
97		data: {
98			meta: {
99				'title': 'Communities',
100				'description': 'bio.tools and communities'
101			},
102			roles: []
103		},
104		resolve: {}
105	})
106	.state('schema', {
107		url: "/schema",
108		templateUrl: "partials/schema.html",
109		data: {
110			roles: []
111		},
112		resolve: {}
113	})
114	.state('stats', {
115		url: "/stats",
116		templateUrl: "components/stats/stats.html",
117		data: {
118			meta: {
119				'title': 'Stats',
120				'description': 'bio.tools content statistics.'
121			},
122			roles: []
123		},
124		controller: "StatsController",
125		resolve: {}
126	})
127	.state('search', {
128		url: "/t?page&q&biotoolsID&name&topic&function&operation&input&inputDataFormat&inputDataType&output&outputDataFormat&outputDataType&homepage&description&version&accessibility&toolType&collectionID&maturity&operatingSystem&language&cost&license&documentation&link&download&publication&credit&owner&sort&ord&domain&topicID&operationID&dataType&dataTypeID&dataFormat&dataFormatID&inputID&inputDataTypeID&inputDataFormatID&outputID&outputDataTypeID&outputDataFormatID&creditName&creditTypeRole&creditTypeEntity&creditOrcidID&publicationID&publicationType&publicationVersion&linkType&documentationType&downloadType&downloadVersion&otherID&otherIDType&otherIDVersion&otherIDValue&elixirPlatform&elixirNode&elixirCommunity&creditGridID&creditRORID&creditFundRefID",
129		templateUrl: "partials/search_results.html",
130		data: {
131			meta: {
132				'title': 'bio.tools · Bioinformatics Tools and Services Discovery Portal',
133				'description': 'A registry of bioinformatics software resources including biological databases, analytical tools and data services',
134				'titleSuffix': '',
135				'og:title': 'bio.tools · Bioinformatics Tools and Services Discovery Portal',
136				'og:description': 'A registry of bioinformatics software resources including biological databases, analytical tools and data services',
137				'og:image': 'https://bio.tools/img/ELIXIR_logo_white_background_small.png'
138			},
139			roles: []
140		},
141		controller: "SearchResultController"
142	})
143	.state('domains', {
144		url: "/domains",
145		templateUrl: "partials/subdomains/domains.html",
146		data: {
147			meta: {
148				'title': 'bio.tools domains',
149				'description': 'Explore and search all bio.tools domains and manage your own domains.',
150				'og:title': 'bio.tools domains',
151				'og:description': 'Explore and search all bio.tools domains and manage your own domains.'
152			},
153			roles: []
154		},
155		controller: "DomainListController",
156		resolve: {}
157	})
158	.state('register', {
159		url: "/register",
160		templateUrl: "partials/toolEdit.html",
161		data: {
162			meta: {
163				'title': 'Register new resource',
164				'description': 'Register a tool or database in bio.tools.',
165				'og:title': 'Register new resource',
166				'og:description': 'Register a tool or database in bio.tools.'
167		},
168			roles: ['User']
169		},
170		controller: "ToolCreateController",
171		resolve: {}
172	})
173	.state('edit-subdomain', {
174		url: "/edit-subdomain/:id",
175		templateUrl: "partials/subdomains/subdomainEdit.html",
176		data: {
177			meta: {
178				'title': 'Edit domain',
179				'og:title': 'Edit domain'
180			},
181			roles: ['User']
182		},
183		controller: "SubdomainController",
184		resolve: {}
185	})
186	.state('edit-domain', {
187		url: "/edit-domain/:id",
188		templateUrl: "partials/subdomains/subdomainEdit.html",
189		data: {
190			meta: {
191				'title': 'Edit domain',
192				'og:title': 'Edit domain'
193			},
194			roles: ['User']
195		},
196		controller: "SubdomainController",
197		resolve: {}
198	})
199	.state('admin-subdomain', {
200		url: "/subdomain",
201		templateUrl: "partials/subdomains/subdomainPage.html",
202		data: {
203			meta: {
204				'title': 'Domain',
205				'og:title': 'Domain'
206			},
207			roles: ['User']
208		},
209		controller: "SubdomainAdminController",
210		resolve: {}
211	})
212	.state('admin-domain', {
213		url: "/domain-manager",
214		templateUrl: "partials/subdomains/subdomainPage.html",
215		data: {
216			meta: {
217				'title': 'Domain',
218				'og:title': 'Domain'
219			},
220			roles: ['User']
221		},
222		controller: "SubdomainAdminController",
223		resolve: {}
224	})
225	.state('login', {
226		url: "/login",
227		templateUrl: "partials/login.html",
228		data: {
229			meta: {
230				'title': 'Log in',
231				'description': 'Log in to bio.tools.',
232				'og:title': 'Log in',
233				'og:description': 'Log in to bio.tools.'
234			},
235			roles: []
236		},
237		controller: "LoginController",
238		resolve: {}
239	})
240	.state('signup', {
241		url: "/signup",
242		templateUrl: "partials/signup.html",
243		data: {
244			meta: {
245				'title': 'Sign up',
246				'description': 'Sign up for bio.tools.',
247				'og:title': 'Sign up',
248				'og:description': 'Sign up for bio.tools.'
249			},
250			roles: []
251		},
252		controller: "SignupController"
253	})
254	.state('signup.success', {
255		url: "/success",
256		templateUrl: "partials/signupSuccess.html",
257		data: {
258			meta: {
259				'title': 'Sign up success'
260			},
261			roles: []
262		}
263	})
264	.state('signup.verify-email', {
265		abstract: true,
266		url: "/verify-email",
267		template: "<ui-view/>",
268		data: {
269			meta: {
270				'title': 'Verify email'
271			},
272			roles: []
273		}
274	})
275	.state('signup.verify-email.success', {
276		url: "/success",
277		templateUrl: "partials/signupVerifyEmailSuccess.html",
278		data: {
279			meta: {
280				'title': 'Verification success'
281			},
282			roles: []
283		}
284	})
285	.state('signup.verify-email.key', {
286		url: "/:key",
287		templateUrl: "partials/signupVerifyEmailKey.html",
288		data: {
289			meta: {
290				'title': 'Verify email'
291			},
292			roles: []
293		},
294		controller: "SignupVerifyEmailKeyController"
295	})
296	.state('reset-password', {
297		url: "/reset-password",
298		templateUrl: "partials/resetPassword.html",
299		data: {
300			meta: {
301				'title': 'Reset password'
302			},
303			roles: []
304		},
305		controller: "ResetPasswordController"
306	})
307	.state('reset-password.confirm', {
308		url: "/confirm?uid&token",
309		templateUrl: "partials/resetPasswordConfirm.html",
310		data: {
311			meta: {
312				'title': 'Reset password'
313			},
314			roles: []
315		},
316		controller: "ResetPasswordConfirmController"
317	})
318	.state('reset-password.confirm.success', {
319		url: "/success",
320		templateUrl: "partials/resetPasswordConfirmSuccess.html",
321		data: {
322			meta: {
323				'title': 'Reset password success'
324			},
325			roles: []
326		}
327	})
328	.state('profile', {
329		url: "/profile",
330		templateUrl: "components/profile/profile.html",
331		data: {
332			meta: {
333				'title': 'My profile'
334			},
335			roles: []
336		},
337		controller: "ProfileController"
338	})
339	.state('subdomain', {
340		url: "/t?:domain"
341	})
342	.state('requests', {
343		url: "/requests",
344		templateUrl: "partials/requests.html",
345		data: {
346			roles: []
347		},
348		controller: "RequestsController"
349	})
350	.state('workflows', {
351		url: "/workflows",
352		templateUrl: "partials/workflows/workflows.html",
353		data: {
354			roles: []
355		},
356		controller: "WorkflowController"
357	})
358	.state('tool-explicit', {
359		url: "/tool/:id",
360		templateUrl: "components/toolPage/toolPage.html",
361		data: {
362			roles: []
363		},
364		controller: "ToolPageController",
365		resolve: {}
366	})
367	.state('tool-explicit-short', {
368		url: "/t/:id",
369		templateUrl: "components/toolPage/toolPage.html",
370		data: {
371			roles: []
372		},
373		controller: "ToolPageController",
374		resolve: {}
375	})
376	.state('tool', {
377		url: "/:id",
378		templateUrl: "components/toolPage/toolPage.html",
379		data: {
380			roles: []
381		},
382		controller: "ToolPageController",
383		resolve: {}
384	})
385	.state('tool.edit', {
386		url: "/edit",
387		templateUrl: "partials/toolEdit.html",
388		data: {
389			roles: ['User']
390		},
391		controller: "ToolUpdateController",
392		resolve: {}
393	})
394	// redirects
395	.state('governance', {
396		url: '/governance',
397		redirect: 'http://biotools.readthedocs.io/en/latest/governance.html',
398		external: true,
399		data: {
400			roles: []
401		},
402		resolve: {}
403	})
404	.state('events', {
405		url: '/events',
406		redirect: 'https://docs.google.com/document/d/1K-6IG_7a-4amstSSOxYjjJ3uDUEg4-WGSSt9jvvO7Ik/edit#heading=h.twop51kvyu80',
407		external: true,
408		data: {
409			roles: []
410		},
411		resolve: {}
412	})
413	.state('mail', {
414		url: '/mail',
415		redirect: 'http://elixirmail.cbs.dtu.dk/mailman/listinfo',
416		external: true,
417		data: {
418			roles: []
419		},
420		resolve: {}
421	})
422	.state('faq', {
423		url: '/faq',
424		redirect: 'https://docs.google.com/document/d/1WdcY0WQJVdRHz0BAhII7s8gygxKRJaHZC4gOnAC1eqs/edit',
425		external: true,
426		data: {
427			roles: []
428		},
429		resolve: {}
430	})
431	.state('apidoc', {
432		url: '/apidoc',
433		redirect: 'https://docs.google.com/document/d/1c1zridWLBNSrWcYyR2fnBfgjWfrUSrT7HJ6Ql6ZCkS4/edit#heading=h.9a2zbl9ka0z0',
434		external: true,
435		data: {
436			roles: []
437		},
438		resolve: {}
439	})
440	.state('changelog', {
441		url: '/changelog',
442		redirect: 'https://docs.google.com/document/d/1C5KFu2t4OIAuUpx6dS7Djsa9y3zgicd8naOsAs9QPAY/edit#heading=h.eu5jkfa77et',
443		external: true,
444		data: {
445			roles: []
446		},
447		resolve: {}
448	})
449	.state('helpdesk', {
450		url: '/helpdesk',
451		redirect: 'https://elixir-registry.atlassian.net/servicedesk/customer/portal/2/user/login?destination=portal%2F2',
452		external: true,
453		data: {
454			roles: []
455		},
456		resolve: {}
457	})
458	.state('projects', {
459		url: '/projects',
460		redirect: 'https://docs.google.com/document/d/1QBIZHBzHxxmvSnOG3xK6t3SCeft5g4AH2rVICLRLmDw/edit#heading=h.a6i4b9p7xdw7',
461		external: true,
462		data: {
463			roles: []
464		},
465		resolve: {}
466	})
467	.state('roadmap', {
468		url: '/roadmap',
469		redirect: 'https://docs.google.com/document/d/17iH72hmwVo205QVEkptv7GTMlqFojatIwMmxVOyuPy8/edit#heading=h.m1jhkecru8xq',
470		external: true,
471		data: {
472			roles: []
473		},
474		resolve: {}
475	})
476}])
477
478// enable HTML5 mode which removes the leading # from URL
479.config(function($locationProvider) {
480	$locationProvider.html5Mode(true).hashPrefix('!')
481})
482
483// default settings for tooltips
484.config(function(tooltipsConfigProvider) {
485	tooltipsConfigProvider.options({
486		speed: 'fast',
487		try: 0
488	})
489})
490
491// make redirect states work
492.run(function($rootScope, $window) {
493	$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
494		if (toState.external) {
495			event.preventDefault();
496			$window.open(toState.redirect, '_self');
497		}
498	});
499})
500
501// make ngMeta run and configure
502.run(['ngMeta', function(ngMeta) { 
503  ngMeta.init();
504}])
505.config(function(ngMetaProvider) {
506
507    ngMetaProvider.useTitleSuffix(true);
508    ngMetaProvider.setDefaultTitle('bio.tools');
509    // ngMetaProvider.setDefaultTitleSuffix(' · bio.tools');
510    // ngMetaProvider.setDefaultTag('author', 'John Smith');
511})
512
513// user authentication
514.run(['$rootScope', '$state', '$timeout', 'User', 'djangoAuth', '$http', function($rootScope, $state, $timeout, User, djangoAuth, $http) {
515	User.authenticated = false;
516
517	// what happens on User login
518	var logInCallback = function(){
519		User.authenticated = true;
520		// get user profile
521		djangoAuth.profile().then(function (response) {
522			User.current = response;
523		});
524	};
525
526	// what happens on authentication errors
527	var wrongAuthCallback = function() {
528		delete $http.defaults.headers.common.Authorization;
529		delete localStorage.token;
530		User.authenticated = false;
531		User.current = {};
532	};
533
534	// what happens on User logout
535	var logOutCallback = function() {
536		wrongAuthCallback();
537		$state.go('home');
538	}
539
540	// get initial authentication status
541	var authenticationPromise = djangoAuth.authenticationStatus(true).then(logInCallback, wrongAuthCallback);
542
543	// set relevant callbacks for log in/out events
544	$rootScope.$on('djangoAuth.logged_in', logInCallback);
545	$rootScope.$on('djangoAuth.logged_out', logOutCallback);
546
547	// checking authentication on state change
548	$rootScope.$on('$stateChangeStart', function(event, toState, toStateParams, fromState, fromStateParams) {
549		// save current state transitions
550		if (toState.name != 'login') {
551			$rootScope.toState = toState;
552			$rootScope.toStateParams = toStateParams;
553		}
554		$rootScope.fromState = fromState;
555
556		// pausing the state change, so that the user authentication has a chance to proceed before state change
557		if (typeof toState.resolve == 'undefined') { toState.resolve = {} };
558		toState.resolve.pauseStateChange = ['$q', function($q) {
559				var defer = $q.defer();
560				authenticationPromise.then(function () {
561					if (!User.authenticated && _.includes(toState.data.roles, 'User')) {
562						event.preventDefault();
563						// save destination states to return to them on proper login
564						$rootScope.returnToState = $rootScope.toState;
565						$rootScope.returnToStateParams = $rootScope.toStateParams;
566						// direct unauthenticated user to login
567						$state.go('login');
568					}
569					defer.resolve();
570				});
571				return defer.promise;
572			}
573		]
574	});
575	// on error go to 404
576	$rootScope.$on('$stateChangeError', function(event) {
577		$state.go('404');
578	});
579}])
580
581// filling in title and description meta attributes in header
582.run(["$rootScope", "$timeout", "$state", function($rootScope, $timeout, $state) {
583	function getTextValue(text) {
584		return angular.isFunction(text) ? text() : text;
585	}
586
587	$rootScope.$on("$stateChangeSuccess", function() {
588		var title = getTextValue($state.$current.locals.globals.$title);
589		var description = getTextValue($state.$current.locals.globals.$description);
590		if(title) {
591			$timeout(function() {
592				$rootScope.$title = title;
593			});
594		}
595		if(description) {
596			$timeout(function() {
597				$rootScope.$description = description;
598			});
599		}
600	});
601}])
602
603// Configuration file for environment-specific settings
604angular.module('elixir_front').constant('AppConfig', {
605	ORCID_CLIENT_ID: 'APP-YNOM1N01SXS79JZ9',
606	ORCID_REDIRECT_URI: 'https://bio.tools/orcid/callback/',
607	ORCID_BASE_URL: 'https://orcid.org'
608});
609
610'use strict';
611
612/* Controllers */
613
614angular
615    .module('elixir_front.controllers', [])
616    .controller('ToolGridController', [
617        '$scope',
618        '$timeout',
619        'ToolList',
620        'ToolTableDataSource',
621        function ($scope, $timeout, ToolList, ToolTableDataSource) {
622            $scope.ToolList = ToolList;
623            $scope.savedStateExists = function () {
624                return localStorage.getItem('gridState') ? true : false;
625            };
626            $scope.saveState = function () {
627                var state = $scope.gridApi.saveState.save();
628                localStorage.setItem('gridState', JSON.stringify(state));
629            };
630            $scope.restoreState = function () {
631                $timeout(function () {
632                    var state = localStorage.getItem('gridState');
633                    if (state) $scope.gridApi.saveState.restore($scope, JSON.parse(state));
634                });
635            };
636            $scope.gridOptions = {
637                enableSorting: false,
638                enableColumnMenus: false,
639                enableColumnResizing: true,
640                enableVerticalScrollbar: 1,
641                enableHorizontalScrollbar: true,
642                columnVirtualizationThreshold: ToolTableDataSource.columnsDescription([]).length,
643                data: ToolList.list,
644                columnDefs: ToolTableDataSource.columnsDescription(
645                    $scope.savedStateExists() == true
646                        ? []
647                        : [
648                              'Name',
649                              'Description',
650                              'Homepage',
651                              'Credits',
652                              'Operation',
653                              'Topic',
654                              'Input',
655                              'Output',
656                          ]
657                ),
658                rowHeight: 135,
659                enableGridMenu: true,
660                onRegisterApi: function (gridApi) {
661                    $scope.gridApi = gridApi;
662                    $scope.restoreState();
663                    // Setup events so we're notified when grid state changes.
664                    $scope.gridApi.core.on.columnVisibilityChanged($scope, function (column) {
665                        $scope.saveState();
666                    });
667                    $scope.gridApi.colResizable.on.columnSizeChanged($scope, function (column) {
668                        $scope.saveState();
669                    });
670                    $scope.gridApi.colMovable.on.columnPositionChanged($scope, function (column) {
671                        $scope.saveState();
672                    });
673                },
674            };
675            $scope.tableHeight = function () {
676                return {
677                    height:
678                        Math.min($scope.ToolList.count, 4) * $scope.gridOptions.rowHeight +
679                        33 +
680                        'px',
681                };
682            };
683        },
684    ])
685    .controller('ToolGridCellController', [
686        '$scope',
687        function ($scope) {
688            $scope.init = function (columnName, rowIndex) {
689                $scope.columnName = columnName;
690                $scope.rowIndex = rowIndex;
691            };
692            $scope.RowIdentifier = function () {
693                return $scope.columnName + $scope.rowIndex;
694            };
695            $scope.RowName = function () {
696                return $scope.columnName;
697            };
698            $scope.RowHeight = function () {
699                var element = document.getElementById($scope.RowIdentifier());
700                return element ? element.offsetHeight : 0;
701            };
702            $scope.DefaultRowHeight = function () {
703                return 135;
704            };
705            $scope.CellWidth = function () {
706                return document.getElementById($scope.RowIdentifier()).offsetWidth;
707            };
708            $scope.CellStyle = function () {
709                return {
710                    width: CellWidthDescription(),
711                    'background-color': CellColorDescription(),
712                };
713            };
714            function CellWidthDescription() {
715                return $scope.CellWidth() + 2 + 'px';
716            }
717            function CellColorDescription() {
718                return $scope.rowIndex % 2 != 0 ? '#f8f8f8' : 'white';
719            }
720        },
721    ])
722    .controller('SearchResultController', [
723        '$scope',
724        '$state',
725        'ToolList',
726        'ToolTableDataSource',
727        'DisplayModeSelector',
728        'Domain',
729        function ($scope, $state, ToolList, ToolTableDataSource, DisplayModeSelector, Domain) {
730            function quoteQueryStringValue(v) {
731                return '"' + v + '"';
732            }
733
734            function stripEdam(t) {
735                return t.replace('http://edamontology.org/', '');
736            }
737
738            $scope.topicNameClicked = function (topic) {
739                //$state.go('search', {'topic': topic.term}, {reload: true});
740                $state.transitionTo(
741                    'search',
742                    { topicID: quoteQueryStringValue(stripEdam(topic.uri)) },
743                    {
744                        reload: true,
745                        inherit: false,
746                        notify: true,
747                    }
748                );
749            };
750
751            $scope.operationNameClicked = function (operation) {
752                //$state.go('search', {'topic': topic.term}, {reload: true});
753                $state.transitionTo(
754                    'search',
755                    { operationID: quoteQueryStringValue(stripEdam(operation.uri)) },
756                    {
757                        reload: true,
758                        inherit: false,
759                        notify: true,
760                    }
761                );
762            };
763            $scope.collectionNameClicked = function (collection) {
764                //$state.go('search', {'topic': topic.term}, {reload: true});
765                $state.transitionTo(
766                    'search',
767                    { collectionID: quoteQueryStringValue(collection) },
768                    {
769                        reload: true,
770                        inherit: false,
771                        notify: true,
772                    }
773                );
774            };
775
776            $scope.shouldLicenseBeALink = function (license) {
777                return !_.includes(['Freeware', 'Proprietary', 'Other', 'Not licensed'], license);
778            };
779
780            $scope.getFlatOperations = function (functions) {
781                var operations = {};
782                for (var i = 0; i < functions.length; i++) {
783                    for (var j = 0; j < functions[i].operation.length; j++) {
784                        var o = functions[i].operation[j];
785                        operations[o.term] = o;
786                    }
787                }
788
789                var arr = Object.keys(operations);
790                var r = [];
791                for (var i = 0; i < arr.length; i++) {
792                    r.push(operations[arr[i]]);
793                }
794
795                return r;
796            };
797
798            $scope.Domain = Domain;
799            $scope.ToolTableDataSource = ToolTableDataSource;
800            $scope.ToolList = ToolList;
801            $scope.DisplayModeSelector = DisplayModeSelector;
802            // Get data initially
803            ToolList.refresh();
804        },
805    ])
806    .controller('AlertsController', [
807        '$scope',
808        'Alert',
809        'EnvironmentChecker',
810        function ($scope, Alert, EnvironmentChecker) {
811            $scope.Alert = Alert;
812            $scope.dev_alert = false;
813
814            // check if the current setup id dev or prod.
815            EnvironmentChecker.getEnvironment().then(function (data) {
816                if (data == 'Development') {
817                    $scope.dev_alert = true;
818                }
819            });
820
821            // check if welcome message was disabled for this user
822            if ('welcome_message' in localStorage) {
823                $scope.welcome_message = localStorage.welcome_message === 'true';
824            } else {
825                $scope.welcome_message = true;
826            }
827
828            $scope.closeWelcomeMessageButtonClick = function () {
829                $scope.welcome_message = false;
830                localStorage.welcome_message = false;
831            };
832        },
833    ])
834    .controller('ToolEditController', [
835        '$scope',
836        '$controller',
837        '$state',
838        '$stateParams',
839        'Ontology',
840        'Attribute',
841        'CheckUserEditingRights',
842        'User',
843        '$timeout',
844        'UsedTerms',
845        '$q',
846        '$uibModal',
847        function (
848            $scope,
849            $controller,
850            $state,
851            $stateParams,
852            Ontology,
853            Attribute,
854            CheckUserEditingRights,
855            User,
856            $timeout,
857            UsedTerms,
858            $q,
859            $uibModal
860        ) {
861            // reference the service
862            $scope.Attribute = Attribute;
863            $scope.CheckUserEditingRights = CheckUserEditingRights;
864            $scope.$state = $state;
865            $scope.form = {};
866            $scope.canEditTool = false;
867            $scope.canEditToolPermissions = false;
868            $scope.User = User;
869            $scope.orderby = 'text';
870
871            $scope.registeringInProgress = false;
872
873            // for storing validation and saving progess
874            ($scope.validationProgress = {}),
875                ($scope.savingProgress = {}),
876                ($scope.deletingProgress = {});
877
878            $scope.initializePermissions = function () {
879                $scope.canEditTool = false;
880                $scope.canEditToolPermissions = false;
881                // Owner can edit anything.
882                if (!_.isEmpty($scope.software)) {
883                    if ($scope.software.owner == $scope.User.getUsername()) {
884                        $scope.canEditTool = true;
885                        $scope.canEditToolPermissions = true;
886                    } else if ($scope.software.editPermission != undefined) {
887                        $scope.canEditTool = $scope.CheckUserEditingRights.canEdit($scope.software);
888                        $scope.canEditToolPermissions = false;
889                        delete $scope.software.editPermission;
890                    }
891                }
892            };
893
894            // handle sending the resource to either validation or saving endpoints
895            $scope.sendResource = function (service, progress, isRemoval, action) {
896                progress.success = false;
897                progress.error = false;
898                progress.inProgress = true;
899                $scope.registrationErrorPayload = null;
900
901                service(
902                    $stateParams,
903                    $scope.software,
904                    function (response) {
905                        // handle success
906                        progress.inProgress = false;
907                        progress.success = true;
908                        if (isRemoval) {
909                            alert('Resource removed succesfully.');
910                            $state.go('search');
911                        }
912
913                        if (action == 'create') {
914                            $state.go('tool.edit', { id: response.biotoolsID });
915                        }
916                    },
917                    function (response) {
918                        // handle error
919                        progress.error = true;
920                        progress.inProgress = false;
921                        $scope.registrationErrorPayload = response.data;
922                    }
923                );
924            };
925
926            // modals
927            $scope.openModal = function (edam, type, suggestions) {
928                var ontoMap = {
929                    data: $scope.EDAM_data,
930                    format: $scope.EDAM_format,
931                    operation: $scope.EDAM_operation,
932                };
933                var onto = ontoMap[type] || $scope.EDAM_data;
934
935                var modalInstance = $uibModal.open({
936                    templateUrl: 'partials/tool_edit/toolEditEdamModal.html',
937                    controllerAs: 'vm',
938                    controller: [
939                        '$uibModalInstance',
940                        'edam',
941                        'onto',
942                        'type',
943                        'suggestions',
944                        EdamModalCtrl,
945                    ],
946                    resolve: {
947                        edam: function () {
948                            return edam;
949                        },
950                        onto: function () {
951                            return onto;
952                        },
953                        type: function () {
954                            return type;
955                        },
956                        suggestions: function () {
957                            return suggestions;
958                        },
959                    },
960                });
961
962                modalInstance.result.then(
963                    function (updatedEdam) {
964                        angular.copy(updatedEdam, edam);
965                    },
966                    function () {}
967                );
968
969                return modalInstance.result;
970            };
971
972            $scope.findObjectByUri = function (obj, targetUri) {
973                function search(current) {
974                    if (current.data && current.data.uri === targetUri) {
975                        return current;
976                    }
977                    if (current.children) {
978                        for (var i = 0; i < current.children.length; i++) {
979                            var result = search(current.children[i]);
980                            if (result) return result;
981                        }
982                    }
983                    return null;
984                }
985
986                for (var i = 0; i < obj.length; i++) {
987                    var result = search(obj[i]);
988                    if (result) return result;
989                }
990                return null;
991            };
992
993            $scope.flattenObject = function (obj) {
994                var result = [];
995                var stack = [obj];
996
997                while (stack.length > 0) {
998                    var current = stack.pop();
999                    for (var key in current) {
1000                        if (current.hasOwnProperty(key)) {
1001                            var value = current[key];
1002                            if (typeof value === 'object' && value !== null) {
1003                                stack.push(value);
1004                            } else {
1005                                result.push(value);
1006                            }
1007                        }
1008                    }
1009                }
1010                return result;
1011            };
1012
1013            $scope.recommend_terms = function (edam, type) {
1014                var edamArray = $scope.flattenObject(edam).filter(function (word) {
1015                    return (
1016                        typeof word === 'string' && word.indexOf('http://edamontology.org/') !== -1
1017                    );
1018                });
1019
1020                if (type === 'operation') return null; // No recommendations for operations
1021
1022                var ontoMap = {
1023                    format: $scope.EDAM_data,
1024                    input: $scope.EDAM_operation,
1025                    output: $scope.EDAM_operation,
1026                };
1027                var onto = ontoMap[type] || $scope.EDAM_operation;
1028
1029                var suggestions = [];
1030
1031                for (var i = 0; i < edamArray.length; i++) {
1032                    var element = edamArray[i];
1033                    var edamObj = $scope.findObjectByUri(onto, element);
1034                    if (!edamObj) continue;
1035
1036                    var appendSuggestions = getSuggestions(type, edamObj, edam);
1037                    if (appendSuggestions.length) {
1038                        suggestions = suggestions.concat(appendSuggestions);
1039                    }
1040                }
1041
1042                return mapSuggestions(suggestions, type);
1043            };
1044
1045            function getSuggestions(type, edamObj, edam) {
1046                switch (type) {
1047                    case 'input':
1048                        return getInputSuggestions(edamObj, edam);
1049                    case 'output':
1050                        return getOutputSuggestions(edamObj, edam);
1051                    default:
1052                        return [];
1053                }
1054            }
1055
1056            function getInputSuggestions(edamObj, edam) {
1057                if (!edamObj.has_input) return [];
1058                if (!edam.hasOwnProperty('input')) return edamObj.has_input;
1059                return edamObj.has_input.filter(function (input) {
1060                    return !edam.input.some(function (existingInput) {
1061                        return existingInput.data.uri === input;
1062                    });
1063                });
1064            }
1065
1066            function getOutputSuggestions(edamObj, edam) {
1067                if (!edamObj.has_output) return [];
1068                if (!edam.hasOwnProperty('output')) return edamObj.has_output;
1069                return edamObj.has_output.filter(function (output) {
1070                    return !edam.output.some(function (existingOutput) {
1071                        return existingOutput.data.uri === output;
1072                    });
1073                });
1074            }
1075
1076            function mapSuggestions(suggestions, type) {
1077                var ontoMap = {
1078                    format: $scope.EDAM_format,
1079                    output: $scope.EDAM_data,
1080                    input: $scope.EDAM_data,
1081                };
1082                var onto = ontoMap[type];
1083
1084                return suggestions.map(function (element) {
1085                    var edamObj = $scope.findObjectByUri(onto, element);
1086                    return {
1087                        uri: element,
1088                        term: edamObj ? edamObj.text : '',
1089                    };
1090                });
1091            }
1092
1093            $scope.addWithModal = function (type, edam) {
1094                var pickerTypeMap = {
1095                    input: 'data',
1096                    output: 'data',
1097                    format: 'format',
1098                    function: 'operation',
1099                    operation: 'operation',
1100                };
1101                var pickertype = pickerTypeMap[type] || '';
1102
1103                var suggestions = $scope.recommend_terms(edam, type);
1104
1105                var modalPromise = $scope.openModal({}, pickertype, suggestions);
1106
1107                modalPromise.then(
1108                    function (newEdam) {
1109                        handleModalResult(type, edam, newEdam);
1110                    },
1111                    function () {}
1112                );
1113            };
1114
1115            function handleModalResult(type, edam, newEdam) {
1116                switch (type) {
1117                    case 'format':
1118                        $scope.addButtonClick('format', edam, true, true);
1119                        edam.format[edam.format.length - 1] = newEdam;
1120                        break;
1121                    case 'output':
1122                        $scope.addButtonClick('output', edam, true, true);
1123                        edam.output[edam.output.length - 1] = newEdam;
1124                        break;
1125                    case 'input':
1126                        $scope.addButtonClick('input', edam, true, true);
1127                        edam.input[edam.input.length - 1] = newEdam;
1128                        break;
1129                    case 'function':
1130                        $scope.addButtonClick('function', edam, true, true);
1131                        edam.function[edam.function.length - 1].operation = [newEdam];
1132                        break;
1133                    case 'operation':
1134                        $scope.addButtonClick('function', edam, true, true);
1135                        edam.push(newEdam);
1136                        break;
1137                }
1138            }
1139
1140            // reset success flags when changes are made
1141            $scope.$watch(
1142                'software',
1143                function (newVal, oldVal) {
1144                    if (newVal !== oldVal) {
1145                        $scope.savingProgress.success = false;
1146                        $scope.validationProgress.success = false;
1147                    }
1148                },
1149                true
1150            );
1151
1152            // used terms (biotoolsID) for searching in relations
1153            function getBiotoolsIDs() {
1154                var d = $q.defer();
1155                var params = {
1156                    usedTermName: 'biotoolsID',
1157                };
1158                UsedTerms.get(params, function (response) {
1159                    d.resolve(response.data);
1160                });
1161                return d.promise;
1162            }
1163
1164            $scope.loadBiotoolsIDs = function (query) {
1165                return getBiotoolsIDs().then(function (list) {
1166                    return list
1167                        .filter(function (str) {
1168                            return str.toLowerCase().includes(query.toLowerCase());
1169                        })
1170                        .slice(0, 50)
1171                        .sort();
1172                });
1173            };
1174
1175            // used terms (collectionID) for searching in collections
1176            function getCollectionIDs() {
1177                var d = $q.defer();
1178                var params = {
1179                    usedTermName: 'collectionID',
1180                };
1181                UsedTerms.get(params, function (response) {
1182                    d.resolve(response.data);
1183                });
1184                return d.promise;
1185            }
1186
1187            $scope.loadCollectionIDs = function (query) {
1188                return getCollectionIDs().then(function (list) {
1189                    return list
1190                        .filter(function (str) {
1191                            return str.toLowerCase().includes(query.toLowerCase());
1192                        })
1193                        .slice(0, 10)
1194                        .sort();
1195                });
1196            };
1197
1198            // used terms (credit names) for searching in credits
1199            function getCreditNames() {
1200                var d = $q.defer();
1201                var params = {
1202                    usedTermName: 'credit',
1203                };
1204                UsedTerms.get(params, function (response) {
1205                    d.resolve(response.data);
1206                });
1207                return d.promise;
1208            }
1209
1210            $scope.loadCreditNames = function (query) {
1211                return getCreditNames().then(function (list) {
1212                    return list
1213                        .filter(function (str) {
1214                            return str.toLowerCase().includes(query.toLowerCase());
1215                        })
1216                        .slice(0, 10)
1217                        .sort();
1218                });
1219            };
1220
1221            // add attribute or list entry
1222            $scope.addButtonClick = function (_what, _where, _isList, _isObject) {
1223                if (_isList) {
1224                    // if array does not exist create it
1225                    if (typeof _where[_what] == 'undefined') {
1226                        _where[_what] = [];
1227                    }
1228                    // add either an object or string to array
1229                    _where[_what].push(_isObject ? {} : '');
1230                } else {
1231                    // if object does not exist create it
1232                    if (typeof _where[_what] == 'undefined') {
1233                        _where[_what] = _isObject ? {} : '';
1234                    }
1235                }
1236            };
1237
1238            $scope.addObjectClick = function (_what, _parent, _where) {
1239                _parent[_where] = {};
1240                _parent[_where][_what] = {};
1241            };
1242
1243            $scope.removeObjectClick = function (_what, _parent, _where) {
1244                var message = _parent[_what][_index].term
1245                    ? `Are you sure you want to remove ${_parent[_what][_index].term}?`
1246                    : 'Are you sure you want to remove this element?';
1247
1248                if (confirm(message)) {
1249                    if (_parent[_where][_what]) {
1250                        delete _parent[_where][_what];
1251                    }
1252                    if (Object.keys(_parent[_where]).length === 0) {
1253                        delete _parent[_where];
1254                    }
1255                }
1256            };
1257
1258            // remove attribute or list entry
1259            $scope.removeButtonClick = function (_what, _parent, _index, _event) {
1260                var message = _parent[_what][_index].term
1261                    ? `Are you sure you want to remove ${_parent[_what][_index].term}?`
1262                    : 'Are you sure you want to remove this element?';
1263
1264                if (_parent[_what][_index] ? confirm(message) : true) {
1265                    // remove jstree if exists
1266                    if (_event) {
1267                        $(_event.target).closest('div').find('.jstree').jstree('destroy').remove();
1268                    }
1269
1270                    _parent[_what].splice(_index, 1);
1271
1272                    // if last instance in array delete entire attribute from the parent object
1273                    if (_parent[_what].length == 0) {
1274                        delete _parent[_what];
1275
1276                        // If we're removing an operation and it's the last one, remove the entire function
1277                        if (_what === 'operation') {
1278                            var functionIndex = $scope.software.function.indexOf(_parent);
1279                            if (functionIndex > -1) {
1280                                $scope.software.function.splice(functionIndex, 1);
1281                            }
1282                        }
1283                    }
1284                }
1285            };
1286
1287            $scope.moveItem = function (key, index, direction) {
1288                var array = $scope.software[key];
1289                var newIndex = index + direction;
1290                if (newIndex < 0 || newIndex >= array.length) return;
1291                // Swap the elements
1292                var temp = array[newIndex];
1293                array[newIndex] = array[index];
1294                array[index] = temp;
1295            };
1296
1297            // create connections between entries
1298            $scope.errorConnections = {
1299                name: ['id'],
1300            };
1301
1302            // reset error on change
1303            $scope.resetError = function (_what, _parent, _index) {
1304                // select appropriate error type handling
1305                if (_index != undefined && _index >= 0) {
1306                    $scope.resetListError(_what, _parent, _index);
1307                } else {
1308                    // remove all connected error warnings
1309                    var whatConnections = $scope.errorConnections[_what];
1310                    if (whatConnections) {
1311                        whatConnections.forEach(function (connection) {
1312                            if (_parent != undefined && _parent[connection]) {
1313                                delete _parent[connection];
1314                            }
1315                        });
1316                    }
1317                    // remove the actual error
1318                    if (_parent != undefined && _parent[_what]) {
1319                        delete _parent[_what];
1320                    }
1321                }
1322            };
1323
1324            $scope.resetListError = function (_what, _parent, _index) {
1325                if (_parent[_what] && _parent[_what][_index]) {
1326                    _parent[_what][_index] = {};
1327                }
1328            };
1329
1330            $scope.isString = function (value) {
1331                if (typeof value === 'string') {
1332                    return true;
1333                } else {
1334                    return false;
1335                }
1336            };
1337
1338            // get ontology objects for the various widgets
1339            Ontology.get({ name: 'EDAM_topic' }, function (response) {
1340                $scope.EDAM_topic = response.data.children;
1341            });
1342            Ontology.get({ name: 'EDAM_data' }, function (response) {
1343                $scope.EDAM_data = response.data.children;
1344            });
1345            Ontology.get({ name: 'EDAM_format' }, function (response) {
1346                $scope.EDAM_format = response.data.children;
1347            });
1348            Ontology.get({ name: 'EDAM_operation' }, function (response) {
1349                $scope.EDAM_operation = response.data.children;
1350            });
1351
1352            // populate the JSON edit textarea
1353            $scope.$watch(
1354                'software',
1355                function (newVal, oldVal) {
1356                    $scope.jsonEdit.model = angular.toJson($scope.software, 2);
1357                    // Check permissions
1358                    if ($scope.canEditTool == false) {
1359                        $scope.initializePermissions();
1360                    }
1361                },
1362                true
1363            );
1364
1365            // parse the edited JSON for errors
1366            $scope.jsonEdit = {};
1367            var initializing = true;
1368            $scope.$watch('jsonEdit.model', function (newVal, oldVal) {
1369                if (!initializing) {
1370                    try {
1371                        $scope.software = angular.fromJson($scope.jsonEdit.model);
1372                        $scope.jsonEdit.error = null;
1373                    } catch (exp) {
1374                        $scope.jsonEdit.error = exp.message;
1375                    }
1376                } else {
1377                    initializing = false;
1378                }
1379            });
1380
1381            // download JSON from the editor
1382            $scope.downloadTool = function (_pretty) {
1383                var hiddenElement = document.createElement('a');
1384                if (_pretty) {
1385                    hiddenElement.href =
1386                        'data:attachment/json,' + encodeURI(angular.toJson($scope.software, 2));
1387                } else {
1388                    hiddenElement.href =
1389                        'data:attachment/json,' + encodeURI(angular.toJson($scope.software));
1390                }
1391                hiddenElement.target = '_blank';
1392                hiddenElement.download =
1393                    ($scope.software.name ? $scope.software.name : 'resource') + '.json';
1394                document.body.appendChild(hiddenElement);
1395                hiddenElement.click();
1396            };
1397
1398            // settings for the EDAM tree widget
1399            $scope.treeOptions = {
1400                nodeChildren: 'children',
1401                dirSelectable: true,
1402            };
1403
1404            // set term and uri when picked from EDAM widget
1405            $scope.ontologyOnSelect = function (_object, _index, _node) {
1406                // if _object[_index] is not an object, make it one
1407                if (!(_object[_index] === Object(_object[_index]))) {
1408                    _object[_index] = {};
1409                }
1410                _object[_index].term = _node.text;
1411                _object[_index].uri = _node.data.uri;
1412            };
1413
1414            // fetch data from Europe PMC and update the publication object
1415            function fetchEuropePMCData(pub, id_type, identifier) {
1416                var params = [];
1417
1418                if (id_type == 'doi') params.push('query=DOI:' + identifier);
1419                else if (id_type == 'pmid') params.push('query=EXT_ID:' + identifier);
1420                else if (id_type == 'pmcid') params.push('query=PMC:' + identifier);
1421                else return;
1422
1423                var url =
1424                    'https://www.ebi.ac.uk/europepmc/webservices/rest/search?' +
1425                    params.join('&') +
1426                    '&format=json';
1427                fetch(url)
1428                    .then(function (response) {
1429                        return response.json();
1430                    })
1431                    .then(function (data) {
1432                        if (
1433                            data.resultList &&
1434                            data.resultList.result &&
1435                            data.resultList.result.length > 0
1436                        ) {
1437                            var rec = data.resultList.result[0];
1438                            if (rec.doi) pub.doi = rec.doi;
1439                            if (rec.pmid) pub.pmid = rec.pmid;
1440                            if (rec.pmcid) pub.pmcid = rec.pmcid;
1441                            $scope.$apply();
1442                        }
1443                    })
1444                    .catch(function (err) {
1445                        /*  handle error */
1446                    });
1447            }
1448
1449            $scope.onPublicationIdChange = function (idType, value, index) {
1450                if (value && value.trim() !== '') {
1451                    fetchEuropePMCData($scope.software.publication[index], idType, value.trim());
1452                }
1453            };
1454
1455            $scope.latestOptions = [
1456                { value: 1, text: 'Yes' },
1457                { value: 0, text: 'No' },
1458            ];
1459
1460            $scope.contactRoleOptions = [
1461                { value: 'General', text: 'General' },
1462                { value: 'Developer', text: 'Developer' },
1463                { value: 'Technical', text: 'Technical' },
1464                { value: 'Scientific', text: 'Scientific' },
1465                { value: 'Helpdesk', text: 'Helpdesk' },
1466                { value: 'Maintainer', text: 'Maintainer' },
1467            ];
1468
1469            $scope.licenseOptions = [
1470                { value: 'Freeware', text: 'Freeware' },
1471                { value: 'Proprietary', text: 'Proprietary' },
1472                { value: 'Other', text: 'Other' },
1473                { value: 'Not licensed', text: 'Not licensed' },
1474                { value: '0BSD', text: '0BSD' },
1475                { value: 'AAL', text: 'AAL' },
1476                { value: 'Abstyles', text: 'Abstyles' },
1477                { value: 'Adobe-2006', text: 'Adobe-2006' },
1478                { value: 'Adobe-Glyph', text: 'Adobe-Glyph' },
1479                { value: 'ADSL', text: 'ADSL' },
1480                { value: 'AFL-1.1', text: 'AFL-1.1' },
1481                { value: 'AFL-1.2', text: 'AFL-1.2' },
1482                { value: 'AFL-2.0', text: 'AFL-2.0' },
1483                { value: 'AFL-2.1', text: 'AFL-2.1' },
1484                { value: 'AFL-3.0', text: 'AFL-3.0' },
1485                { value: 'Afmparse', text: 'Afmparse' },
1486                { value: 'AGPL-1.0', text: 'AGPL-1.0' },
1487                { value: 'AGPL-1.0-or-later', text: 'AGPL-1.0-or-later' },
1488                { value: 'AGPL-3.0', text: 'AGPL-3.0' },
1489                { value: 'AGPL-3.0-or-later', text: 'AGPL-3.0-or-later' },
1490                { value: 'Aladdin', text: 'Aladdin' },
1491                { value: 'AMDPLPA', text: 'AMDPLPA' },
1492                { value: 'AML', text: 'AML' },
1493                { value: 'AMPAS', text: 'AMPAS' },
1494                { value: 'ANTLR-PD', text: 'ANTLR-PD' },
1495                { value: 'ANTLR-PD-fallback', text: 'ANTLR-PD-fallback' },
1496                { value: 'Apache-1.0', text: 'Apache-1.0' },
1497                { value: 'Apache-1.1', text: 'Apache-1.1' },
1498                { value: 'Apache-2.0', text: 'Apache-2.0' },
1499                { value: 'APAFML', text: 'APAFML' },
1500                { value: 'APL-1.0', text: 'APL-1.0' },
1501                { value: 'APSL-1.0', text: 'APSL-1.0' },
1502                { value: 'APSL-1.1', text: 'APSL-1.1' },
1503                { value: 'APSL-1.2', text: 'APSL-1.2' },
1504                { value: 'APSL-2.0', text: 'APSL-2.0' },
1505                { value: 'Artistic-1.0', text: 'Artistic-1.0' },
1506                { value: 'Artistic-1.0-cl8', text: 'Artistic-1.0-cl8' },
1507                { value: 'Artistic-1.0-Perl', text: 'Artistic-1.0-Perl' },
1508                { value: 'Artistic-2.0', text: 'Artistic-2.0' },
1509                { value: 'Bahyph', text: 'Bahyph' },
1510                { value: 'Barr', text: 'Barr' },
1511                { value: 'Beerware', text: 'Beerware' },
1512                { value: 'BitTorrent-1.0', text: 'BitTorrent-1.0' },
1513                { value: 'BitTorrent-1.1', text: 'BitTorrent-1.1' },
1514                { value: 'blessing', text: 'blessing' },
1515                { value: 'BlueOak-1.0.0', text: 'BlueOak-1.0.0' },
1516                { value: 'Borceux', text: 'Borceux' },
1517                { value: 'BSD-1-Clause', text: 'BSD-1-Clause' },
1518                { value: 'BSD-2-Clause', text: 'BSD-2-Clause' },
1519                { value: 'BSD-2-Clause-FreeBSD', text: 'BSD-2-Clause-FreeBSD' },
1520                { value: 'BSD-2-Clause-NetBSD', text: 'BSD-2-Clause-NetBSD' },
1521                { value: 'BSD-2-Clause-Patent', text: 'BSD-2-Clause-Patent' },
1522                { value: 'BSD-2-Clause-Views', text: 'BSD-2-Clause-Views' },
1523                { value: 'BSD-3-Clause', text: 'BSD-3-Clause' },
1524                { value: 'BSD-3-Clause-Attribution', text: 'BSD-3-Clause-Attribution' },
1525                { value: 'BSD-3-Clause-Clear', text: 'BSD-3-Clause-Clear' },
1526                { value: 'BSD-3-Clause-LBNL', text: 'BSD-3-Clause-LBNL' },
1527                { value: 'BSD-3-Clause-Modification', text: 'BSD-3-Clause-Modification' },
1528                { value: 'BSD-3-Clause-No-Military-License', text: 'BSD-3-Clause-No-Military-License' },
1529                { value: 'BSD-3-Clause-No-Nuclear-License', text: 'BSD-3-Clause-No-Nuclear-License' },
1530                { value: 'BSD-3-Clause-No-Nuclear-License-2014', text: 'BSD-3-Clause-No-Nuclear-License-2014' },
1531                { value: 'BSD-3-Clause-No-Nuclear-Warranty', text: 'BSD-3-Clause-No-Nuclear-Warranty' },
1532                { value: 'BSD-3-Clause-Open-MPI', text: 'BSD-3-Clause-Open-MPI' },
1533                { value: 'BSD-4-Clause', text: 'BSD-4-Clause' },
1534                { value: 'BSD-4-Clause-Shortened', text: 'BSD-4-Clause-Shortened' },
1535                { value: 'BSD-4-Clause-UC', text: 'BSD-4-Clause-UC' },
1536                { value: 'BSD-Protection', text: 'BSD-Protection' },
1537                { value: 'BSD-Source-Code', text: 'BSD-Source-Code' },
1538                { value: 'BSL-1.0', text: 'BSL-1.0' },
1539                { value: 'BUSL-1.1', text: 'BUSL-1.1' },
1540                { value: 'bzip2-1.0.5', text: 'bzip2-1.0.5' },
1541                { value: 'bzip2-1.0.6', text: 'bzip2-1.0.6' },
1542                { value: 'C-UDA-1.0', text: 'C-UDA-1.0' },
1543                { value: 'CAL-1.0', text: 'CAL-1.0' },
1544                { value: 'CAL-1.0-Combined-Work-Exception', text: 'CAL-1.0-Combined-Work-Exception' },
1545                { value: 'Caldera', text: 'Caldera' },
1546                { value: 'CATOSL-1.1', text: 'CATOSL-1.1' },
1547                { value: 'CC-BY-1.0', text: 'CC-BY-1.0' },
1548                { value: 'CC-BY-2.0', text: 'CC-BY-2.0' },
1549                { value: 'CC-BY-2.5', text: 'CC-BY-2.5' },
1550                { value: 'CC-BY-3.0', text: 'CC-BY-3.0' },
1551                { value: 'CC-BY-3.0-AT', text: 'CC-BY-3.0-AT' },
1552                { value: 'CC-BY-3.0-US', text: 'CC-BY-3.0-US' },
1553                { value: 'CC-BY-4.0', text: 'CC-BY-4.0' },
1554                { value: 'CC-BY-NC-1.0', text: 'CC-BY-NC-1.0' },
1555                { value: 'CC-BY-NC-2.0', text: 'CC-BY-NC-2.0' },
1556                { value: 'CC-BY-NC-2.5', text: 'CC-BY-NC-2.5' },
1557                { value: 'CC-BY-NC-3.0', text: 'CC-BY-NC-3.0' },
1558                { value: 'CC-BY-NC-4.0', text: 'CC-BY-NC-4.0' },
1559                { value: 'CC-BY-NC-ND-1.0', text: 'CC-BY-NC-ND-1.0' },
1560                { value: 'CC-BY-NC-ND-2.0', text: 'CC-BY-NC-ND-2.0' },
1561                { value: 'CC-BY-NC-ND-2.5', text: 'CC-BY-NC-ND-2.5' },
1562                { value: 'CC-BY-NC-ND-3.0', text: 'CC-BY-NC-ND-3.0' },
1563                { value: 'CC-BY-NC-ND-3.0-IGO', text: 'CC-BY-NC-ND-3.0-IGO' },
1564                { value: 'CC-BY-NC-ND-4.0', text: 'CC-BY-NC-ND-4.0' },
1565                { value: 'CC-BY-NC-SA-1.0', text: 'CC-BY-NC-SA-1.0' },
1566                { value: 'CC-BY-NC-SA-2.0', text: 'CC-BY-NC-SA-2.0' },
1567                { value: 'CC-BY-NC-SA-2.5', text: 'CC-BY-NC-SA-2.5' },
1568                { value: 'CC-BY-NC-SA-3.0', text: 'CC-BY-NC-SA-3.0' },
1569                { value: 'CC-BY-NC-SA-4.0', text: 'CC-BY-NC-SA-4.0' },
1570                { value: 'CC-BY-ND-1.0', text: 'CC-BY-ND-1.0' },
1571                { value: 'CC-BY-ND-2.0', text: 'CC-BY-ND-2.0' },
1572                { value: 'CC-BY-ND-2.5', text: 'CC-BY-ND-2.5' },
1573                { value: 'CC-BY-ND-3.0', text: 'CC-BY-ND-3.0' },
1574                { value: 'CC-BY-ND-4.0', text: 'CC-BY-ND-4.0' },
1575                { value: 'CC-BY-SA-1.0', text: 'CC-BY-SA-1.0' },
1576                { value: 'CC-BY-SA-2.0', text: 'CC-BY-SA-2.0' },
1577                { value: 'CC-BY-SA-2.0-UK', text: 'CC-BY-SA-2.0-UK' },
1578                { value: 'CC-BY-SA-2.1-JP', text: 'CC-BY-SA-2.1-JP' },
1579                { value: 'CC-BY-SA-2.5', text: 'CC-BY-SA-2.5' },
1580                { value: 'CC-BY-SA-3.0', text: 'CC-BY-SA-3.0' },
1581                { value: 'CC-BY-SA-3.0-AT', text: 'CC-BY-SA-3.0-AT' },
1582                { value: 'CC-BY-SA-4.0', text: 'CC-BY-SA-4.0' },
1583                { value: 'CC-PDDC', text: 'CC-PDDC' },
1584                { value: 'CC0-1.0', text: 'CC0-1.0' },
1585                { value: 'CDDL-1.0', text: 'CDDL-1.0' },
1586                { value: 'CDDL-1.1', text: 'CDDL-1.1' },
1587                { value: 'CDL-1.0', text: 'CDL-1.0' },
1588                { value: 'CDLA-Permissive-1.0', text: 'CDLA-Permissive-1.0' },
1589                { value: 'CDLA-Sharing-1.0', text: 'CDLA-Sharing-1.0' },
1590                { value: 'CECILL-1.0', text: 'CECILL-1.0' },
1591                { value: 'CECILL-1.1', text: 'CECILL-1.1' },
1592                { value: 'CECILL-2.0', text: 'CECILL-2.0' },
1593                { value: 'CECILL-2.1', text: 'CECILL-2.1' },
1594                { value: 'CECILL-B', text: 'CECILL-B' },
1595                { value: 'CECILL-C', text: 'CECILL-C' },
1596                { value: 'CERN-OHL-1.1', text: 'CERN-OHL-1.1' },
1597                { value: 'CERN-OHL-1.2', text: 'CERN-OHL-1.2' },
1598                { value: 'CERN-OHL-P-2.0', text: 'CERN-OHL-P-2.0' },
1599                { value: 'CERN-OHL-S-2.0', text: 'CERN-OHL-S-2.0' },
1600                { value: 'CERN-OHL-W-2.0', text: 'CERN-OHL-W-2.0' },
1601                { value: 'ClArtistic', text: 'ClArtistic' },
1602                { value: 'CNRI-Jython', text: 'CNRI-Jython' },
1603                { value: 'CNRI-Python', text: 'CNRI-Python' },
1604                { value: 'CNRI-Python-GPL-Compatible', text: 'CNRI-Python-GPL-Compatible' },
1605                { value: 'Condor-1.1', text: 'Condor-1.1' },
1606                { value: 'copyleft-next-0.3.0', text: 'copyleft-next-0.3.0' },
1607                { value: 'copyleft-next-0.3.1', text: 'copyleft-next-0.3.1' },
1608                { value: 'CPAL-1.0', text: 'CPAL-1.0' },
1609                { value: 'CPL-1.0', text: 'CPL-1.0' },
1610                { value: 'CPOL-1.02', text: 'CPOL-1.02' },
1611                { value: 'Crossword', text: 'Crossword' },
1612                { value: 'CrystalStacker', text: 'CrystalStacker' },
1613                { value: 'CUA-OPL-1.0', text: 'CUA-OPL-1.0' },
1614                { value: 'Cube', text: 'Cube' },
1615                { value: 'curl', text: 'curl' },
1616                { value: 'D-FSL-1.0', text: 'D-FSL-1.0' },
1617                { value: 'diffmark', text: 'diffmark' },
1618                { value: 'DOC', text: 'DOC' },
1619                { value: 'Dotseqn', text: 'Dotseqn' },
1620                { value: 'DRL-1.0', text: 'DRL-1.0' },
1621                { value: 'DSDP', text: 'DSDP' },
1622                { value: 'dvipdfm', text: 'dvipdfm' },
1623                { value: 'ECL-1.0', text: 'ECL-1.0' },
1624                { value: 'ECL-2.0', text: 'ECL-2.0' },
1625                { value: 'EFL-1.0', text: 'EFL-1.0' },
1626                { value: 'EFL-2.0', text: 'EFL-2.0' },
1627                { value: 'eGenix', text: 'eGenix' },
1628                { value: 'Entessa', text: 'Entessa' },
1629                { value: 'EPICS', text: 'EPICS' },
1630                { value: 'EPL-1.0', text: 'EPL-1.0' },
1631                { value: 'EPL-2.0', text: 'EPL-2.0' },
1632                { value: 'ErlPL-1.1', text: 'ErlPL-1.1' },
1633                { value: 'etalab-2.0', text: 'etalab-2.0' },
1634                { value: 'EUDatagrid', text: 'EUDatagrid' },
1635                { value: 'EUPL-1.0', text: 'EUPL-1.0' },
1636                { value: 'EUPL-1.1', text: 'EUPL-1.1' },
1637                { value: 'EUPL-1.2', text: 'EUPL-1.2' },
1638                { value: 'Eurosym', text: 'Eurosym' },
1639                { value: 'Fair', text: 'Fair' },
1640                { value: 'Frameworx-1.0', text: 'Frameworx-1.0' },
1641                { value: 'FreeBSD-DOC', text: 'FreeBSD-DOC' },
1642                { value: 'FreeImage', text: 'FreeImage' },
1643                { value: 'FSFAP', text: 'FSFAP' },
1644                { value: 'FSFUL', text: 'FSFUL' },
1645                { value: 'FSFULLR', text: 'FSFULLR' },
1646                { value: 'FTL', text: 'FTL' },
1647                { value: 'GD', text: 'GD' },
1648                { value: 'GFDL-1.1', text: 'GFDL-1.1' },
1649                { value: 'GFDL-1.1-invariants-only', text: 'GFDL-1.1-invariants-only' },
1650                { value: 'GFDL-1.1-invariants-or-later', text: 'GFDL-1.1-invariants-or-later' },
1651                { value: 'GFDL-1.1-no-invariants-only', text: 'GFDL-1.1-no-invariants-only' },
1652                { value: 'GFDL-1.1-no-invariants-or-later', text: 'GFDL-1.1-no-invariants-or-later' },
1653                { value: 'GFDL-1.1-or-later', text: 'GFDL-1.1-or-later' },
1654                { value: 'GFDL-1.2', text: 'GFDL-1.2' },
1655                { value: 'GFDL-1.2-invariants-only', text: 'GFDL-1.2-invariants-only' },
1656                { value: 'GFDL-1.2-invariants-or-later', text: 'GFDL-1.2-invariants-or-later' },
1657                { value: 'GFDL-1.2-no-invariants-only', text: 'GFDL-1.2-no-invariants-only' },
1658                { value: 'GFDL-1.2-no-invariants-or-later', text: 'GFDL-1.2-no-invariants-or-later' },
1659                { value: 'GFDL-1.2-or-later', text: 'GFDL-1.2-or-later' },
1660                { value: 'GFDL-1.3', text: 'GFDL-1.3' },
1661                { value: 'GFDL-1.3-invariants-only', text: 'GFDL-1.3-invariants-only' },
1662                { value: 'GFDL-1.3-invariants-or-later', text: 'GFDL-1.3-invariants-or-later' },
1663                { value: 'GFDL-1.3-no-invariants-only', text: 'GFDL-1.3-no-invariants-only' },
1664                { value: 'GFDL-1.3-no-invariants-or-later', text: 'GFDL-1.3-no-invariants-or-later' },
1665                { value: 'GFDL-1.3-or-later', text: 'GFDL-1.3-or-later' },
1666                { value: 'Giftware', text: 'Giftware' },
1667                { value: 'GL2PS', text: 'GL2PS' },
1668                { value: 'Glide', text: 'Glide' },
1669                { value: 'Glulxe', text: 'Glulxe' },
1670                { value: 'GLWTPL', text: 'GLWTPL' },
1671                { value: 'gnuplot', text: 'gnuplot' },
1672                { value: 'GPL-1.0', text: 'GPL-1.0' },
1673                { value: 'GPL-1.0-or-later', text: 'GPL-1.0-or-later' },
1674                { value: 'GPL-2.0', text: 'GPL-2.0' },
1675                { value: 'GPL-2.0-or-later', text: 'GPL-2.0-or-later' },
1676                { value: 'GPL-3.0', text: 'GPL-3.0' },
1677                { value: 'GPL-3.0-or-later', text: 'GPL-3.0-or-later' },
1678                { value: 'gSOAP-1.3b', text: 'gSOAP-1.3b' },
1679                { value: 'HaskellReport', text: 'HaskellReport' },
1680                { value: 'Hippocratic-2.1', text: 'Hippocratic-2.1' },
1681                { value: 'HPND', text: 'HPND' },
1682                { value: 'HPND-sell-variant', text: 'HPND-sell-variant' },
1683                { value: 'HTMLTIDY', text: 'HTMLTIDY' },
1684                { value: 'IBM-pibs', text: 'IBM-pibs' },
1685                { value: 'ICU', text: 'ICU' },
1686                { value: 'IJG', text: 'IJG' },
1687                { value: 'ImageMagick', text: 'ImageMagick' },
1688                { value: 'iMatix', text: 'iMatix' },
1689                { value: 'Imlib2', text: 'Imlib2' },
1690                { value: 'Info-ZIP', text: 'Info-ZIP' },
1691                { value: 'Intel', text: 'Intel' },
1692                { value: 'Intel-ACPI', text: 'Intel-ACPI' },
1693                { value: 'Interbase-1.0', text: 'Interbase-1.0' },
1694                { value: 'IPA', text: 'IPA' },
1695                { value: 'IPL-1.0', text: 'IPL-1.0' },
1696                { value: 'ISC', text: 'ISC' },
1697                { value: 'JasPer-2.0', text: 'JasPer-2.0' },
1698                { value: 'JPNIC', text: 'JPNIC' },
1699                { value: 'JSON', text: 'JSON' },
1700                { value: 'LAL-1.2', text: 'LAL-1.2' },
1701                { value: 'LAL-1.3', text: 'LAL-1.3' },
1702                { value: 'Latex2e', text: 'Latex2e' },
1703                { value: 'Leptonica', text: 'Leptonica' },
1704                { value: 'LGPL-2.0', text: 'LGPL-2.0' },
1705                { value: 'LGPL-2.0-or-later', text: 'LGPL-2.0-or-later' },
1706                { value: 'LGPL-2.1', text: 'LGPL-2.1' },
1707                { value: 'LGPL-2.1-or-later', text: 'LGPL-2.1-or-later' },
1708                { value: 'LGPL-3.0', text: 'LGPL-3.0' },
1709                { value: 'LGPL-3.0-or-later', text: 'LGPL-3.0-or-later' },
1710                { value: 'LGPLLR', text: 'LGPLLR' },
1711                { value: 'Libpng', text: 'Libpng' },
1712                { value: 'libpng-2.0', text: 'libpng-2.0' },
1713                { value: 'libselinux-1.0', text: 'libselinux-1.0' },
1714                { value: 'libtiff', text: 'libtiff' },
1715                { value: 'LiLiQ-P-1.1', text: 'LiLiQ-P-1.1' },
1716                { value: 'LiLiQ-R-1.1', text: 'LiLiQ-R-1.1' },
1717                { value: 'LiLiQ-Rplus-1.1', text: 'LiLiQ-Rplus-1.1' },
1718                { value: 'Linux-OpenIB', text: 'Linux-OpenIB' },
1719                { value: 'LPL-1.0', text: 'LPL-1.0' },
1720                { value: 'LPL-1.02', text: 'LPL-1.02' },
1721                { value: 'LPPL-1.0', text: 'LPPL-1.0' },
1722                { value: 'LPPL-1.1', text: 'LPPL-1.1' },
1723                { value: 'LPPL-1.2', text: 'LPPL-1.2' },
1724                { value: 'LPPL-1.3a', text: 'LPPL-1.3a' },
1725                { value: 'LPPL-1.3c', text: 'LPPL-1.3c' },
1726                { value: 'MakeIndex', text: 'MakeIndex' },
1727                { value: 'MirOS', text: 'MirOS' },
1728                { value: 'MIT', text: 'MIT' },
1729                { value: 'MIT-0', text: 'MIT-0' },
1730                { value: 'MIT-advertising', text: 'MIT-advertising' },
1731                { value: 'MIT-CMU', text: 'MIT-CMU' },
1732                { value: 'MIT-enna', text: 'MIT-enna' },
1733                { value: 'MIT-feh', text: 'MIT-feh' },
1734                { value: 'MIT-Modern-Variant', text: 'MIT-Modern-Variant' },
1735                { value: 'MIT-open-group', text: 'MIT-open-group' },
1736                { value: 'MITNFA', text: 'MITNFA' },
1737                { value: 'Motosoto', text: 'Motosoto' },
1738                { value: 'mpich2', text: 'mpich2' },
1739                { value: 'MPL-1.0', text: 'MPL-1.0' },
1740                { value: 'MPL-1.1', text: 'MPL-1.1' },
1741                { value: 'MPL-2.0', text: 'MPL-2.0' },
1742                { value: 'MPL-2.0-no-copyleft-exception', text: 'MPL-2.0-no-copyleft-exception' },
1743                { value: 'MS-PL', text: 'MS-PL' },
1744                { value: 'MS-RL', text: 'MS-RL' },
1745                { value: 'MTLL', text: 'MTLL' },
1746                { value: 'MulanPSL-1.0', text: 'MulanPSL-1.0' },
1747                { value: 'MulanPSL-2.0', text: 'MulanPSL-2.0' },
1748                { value: 'Multics', text: 'Multics' },
1749                { value: 'Mup', text: 'Mup' },
1750                { value: 'NAIST-2003', text: 'NAIST-2003' },
1751                { value: 'NASA-1.3', text: 'NASA-1.3' },
1752                { value: 'Naumen', text: 'Naumen' },
1753                { value: 'NBPL-1.0', text: 'NBPL-1.0' },
1754                { value: 'NCGL-UK-2.0', text: 'NCGL-UK-2.0' },
1755                { value: 'NCSA', text: 'NCSA' },
1756                { value: 'Net-SNMP', text: 'Net-SNMP' },
1757                { value: 'NetCDF', text: 'NetCDF' },
1758                { value: 'Newsletr', text: 'Newsletr' },
1759                { value: 'NGPL', text: 'NGPL' },
1760                { value: 'NIST-PD', text: 'NIST-PD' },
1761                { value: 'NIST-PD-fallback', text: 'NIST-PD-fallback' },
1762                { value: 'NLOD-1.0', text: 'NLOD-1.0' },
1763                { value: 'NLPL', text: 'NLPL' },
1764                { value: 'Nokia', text: 'Nokia' },
1765                { value: 'NOSL', text: 'NOSL' },
1766                { value: 'Noweb', text: 'Noweb' },
1767                { value: 'NPL-1.0', text: 'NPL-1.0' },
1768                { value: 'NPL-1.1', text: 'NPL-1.1' },
1769                { value: 'NPOSL-3.0', text: 'NPOSL-3.0' },
1770                { value: 'NRL', text: 'NRL' },
1771                { value: 'NTP', text: 'NTP' },
1772                { value: 'Nunit', text: 'Nunit' },
1773                { value: 'O-UDA-1.0', text: 'O-UDA-1.0' },
1774                { value: 'OCCT-PL', text: 'OCCT-PL' },
1775                { value: 'OCLC-2.0', text: 'OCLC-2.0' },
1776                { value: 'ODbL-1.0', text: 'ODbL-1.0' },
1777                { value: 'ODC-By-1.0', text: 'ODC-By-1.0' },
1778                { value: 'OFL-1.0', text: 'OFL-1.0' },
1779                { value: 'OFL-1.0-no-RFN', text: 'OFL-1.0-no-RFN' },
1780                { value: 'OFL-1.0-RFN', text: 'OFL-1.0-RFN' },
1781                { value: 'OFL-1.1', text: 'OFL-1.1' },
1782                { value: 'OFL-1.1-no-RFN', text: 'OFL-1.1-no-RFN' },
1783                { value: 'OFL-1.1-RFN', text: 'OFL-1.1-RFN' },
1784                { value: 'OGC-1.0', text: 'OGC-1.0' },
1785                { value: 'OGDL-Taiwan-1.0', text: 'OGDL-Taiwan-1.0' },
1786                { value: 'OGL-Canada-2.0', text: 'OGL-Canada-2.0' },
1787                { value: 'OGL-UK-1.0', text: 'OGL-UK-1.0' },
1788                { value: 'OGL-UK-2.0', text: 'OGL-UK-2.0' },
1789                { value: 'OGL-UK-3.0', text: 'OGL-UK-3.0' },
1790                { value: 'OGTSL', text: 'OGTSL' },
1791                { value: 'OLDAP-1.1', text: 'OLDAP-1.1' },
1792                { value: 'OLDAP-1.2', text: 'OLDAP-1.2' },
1793                { value: 'OLDAP-1.3', text: 'OLDAP-1.3' },
1794                { value: 'OLDAP-1.4', text: 'OLDAP-1.4' },
1795                { value: 'OLDAP-2.0', text: 'OLDAP-2.0' },
1796                { value: 'OLDAP-2.0.1', text: 'OLDAP-2.0.1' },
1797                { value: 'OLDAP-2.1', text: 'OLDAP-2.1' },
1798                { value: 'OLDAP-2.2', text: 'OLDAP-2.2' },
1799                { value: 'OLDAP-2.2.1', text: 'OLDAP-2.2.1' },
1800                { value: 'OLDAP-2.2.2', text: 'OLDAP-2.2.2' },
1801                { value: 'OLDAP-2.3', text: 'OLDAP-2.3' },
1802                { value: 'OLDAP-2.4', text: 'OLDAP-2.4' },
1803                { value: 'OLDAP-2.5', text: 'OLDAP-2.5' },
1804                { value: 'OLDAP-2.6', text: 'OLDAP-2.6' },
1805                { value: 'OLDAP-2.7', text: 'OLDAP-2.7' },
1806                { value: 'OLDAP-2.8', text: 'OLDAP-2.8' },
1807                { value: 'OML', text: 'OML' },
1808                { value: 'OpenSSL', text: 'OpenSSL' },
1809                { value: 'OPL-1.0', text: 'OPL-1.0' },
1810                { value: 'OSET-PL-2.1', text: 'OSET-PL-2.1' },
1811                { value: 'OSL-1.0', text: 'OSL-1.0' },
1812                { value: 'OSL-1.1', text: 'OSL-1.1' },
1813                { value: 'OSL-2.0', text: 'OSL-2.0' },
1814                { value: 'OSL-2.1', text: 'OSL-2.1' },
1815                { value: 'OSL-3.0', text: 'OSL-3.0' },
1816                { value: 'Parity-6.0.0', text: 'Parity-6.0.0' },
1817                { value: 'Parity-7.0.0', text: 'Parity-7.0.0' },
1818                { value: 'PDDL-1.0', text: 'PDDL-1.0' },
1819                { value: 'PHP-3.0', text: 'PHP-3.0' },
1820                { value: 'PHP-3.01', text: 'PHP-3.01' },
1821                { value: 'Plexus', text: 'Plexus' },
1822                { value: 'PolyForm-Noncommercial-1.0.0', text: 'PolyForm-Noncommercial-1.0.0' },
1823                { value: 'PolyForm-Small-Business-1.0.0', text: 'PolyForm-Small-Business-1.0.0' },
1824                { value: 'PostgreSQL', text: 'PostgreSQL' },
1825                { value: 'PSF-2.0', text: 'PSF-2.0' },
1826                { value: 'psfrag', text: 'psfrag' },
1827                { value: 'psutils', text: 'psutils' },
1828                { value: 'Python-2.0', text: 'Python-2.0' },
1829                { value: 'Qhull', text: 'Qhull' },
1830                { value: 'QPL-1.0', text: 'QPL-1.0' },
1831                { value: 'Rdisc', text: 'Rdisc' },
1832                { value: 'RHeCos-1.1', text: 'RHeCos-1.1' },
1833                { value: 'RPL-1.1', text: 'RPL-1.1' },
1834                { value: 'RPL-1.5', text: 'RPL-1.5' },
1835                { value: 'RPSL-1.0', text: 'RPSL-1.0' },
1836                { value: 'RSA-MD', text: 'RSA-MD' },
1837                { value: 'RSCPL', text: 'RSCPL' },
1838                { value: 'Ruby', text: 'Ruby' },
1839                { value: 'SAX-PD', text: 'SAX-PD' },
1840                { value: 'Saxpath', text: 'Saxpath' },
1841                { value: 'SCEA', text: 'SCEA' },
1842                { value: 'Sendmail', text: 'Sendmail' },
1843                { value: 'Sendmail-8.23', text: 'Sendmail-8.23' },
1844                { value: 'SGI-B-1.0', text: 'SGI-B-1.0' },
1845                { value: 'SGI-B-1.1', text: 'SGI-B-1.1' },
1846                { value: 'SGI-B-2.0', text: 'SGI-B-2.0' },
1847                { value: 'SHL-0.5', text: 'SHL-0.5' },
1848                { value: 'SHL-0.51', text: 'SHL-0.51' },
1849                { value: 'SimPL-2.0', text: 'SimPL-2.0' },
1850                { value: 'SISSL', text: 'SISSL' },
1851                { value: 'SISSL-1.2', text: 'SISSL-1.2' },
1852                { value: 'Sleepycat', text: 'Sleepycat' },
1853                { value: 'SMLNJ', text: 'SMLNJ' },
1854                { value: 'SMPPL', text: 'SMPPL' },
1855                { value: 'SNIA', text: 'SNIA' },
1856                { value: 'Spencer-86', text: 'Spencer-86' },
1857                { value: 'Spencer-94', text: 'Spencer-94' },
1858                { value: 'Spencer-99', text: 'Spencer-99' },
1859                { value: 'SPL-1.0', text: 'SPL-1.0' },
1860                { value: 'SSH-OpenSSH', text: 'SSH-OpenSSH' },
1861                { value: 'SSH-short', text: 'SSH-short' },
1862                { value: 'SSPL-1.0', text: 'SSPL-1.0' },
1863                { value: 'SugarCRM-1.1.3', text: 'SugarCRM-1.1.3' },
1864                { value: 'SWL', text: 'SWL' },
1865                { value: 'TAPR-OHL-1.0', text: 'TAPR-OHL-1.0' },
1866                { value: 'TCL', text: 'TCL' },
1867                { value: 'TCP-wrappers', text: 'TCP-wrappers' },
1868                { value: 'TMate', text: 'TMate' },
1869                { value: 'TORQUE-1.1', text: 'TORQUE-1.1' },
1870                { value: 'TOSL', text: 'TOSL' },
1871                { value: 'TU-Berlin-1.0', text: 'TU-Berlin-1.0' },
1872                { value: 'TU-Berlin-2.0', text: 'TU-Berlin-2.0' },
1873                { value: 'UCL-1.0', text: 'UCL-1.0' },
1874                { value: 'Unicode-DFS-2015', text: 'Unicode-DFS-2015' },
1875                { value: 'Unicode-DFS-2016', text: 'Unicode-DFS-2016' },
1876                { value: 'Unicode-TOU', text: 'Unicode-TOU' },
1877                { value: 'Unlicense', text: 'Unlicense' },
1878                { value: 'UPL-1.0', text: 'UPL-1.0' },
1879                { value: 'Vim', text: 'Vim' },
1880                { value: 'VOSTROM', text: 'VOSTROM' },
1881                { value: 'VSL-1.0', text: 'VSL-1.0' },
1882                { value: 'W3C', text: 'W3C' },
1883                { value: 'W3C-19980720', text: 'W3C-19980720' },
1884                { value: 'Watcom-1.0', text: 'Watcom-1.0' },
1885                { value: 'Wsuipa', text: 'Wsuipa' },
1886                { value: 'WTFPL', text: 'WTFPL' },
1887                { value: 'X11', text: 'X11' },
1888                { value: 'Xerox', text: 'Xerox' },
1889                { value: 'XFree86-1.1', text: 'XFree86-1.1' },
1890                { value: 'xinetd', text: 'xinetd' },
1891                { value: 'Xnet', text: 'Xnet' },
1892                { value: 'xpp', text: 'xpp' },
1893                { value: 'XSkat', text: 'XSkat' },
1894                { value: 'YPL-1.0', text: 'YPL-1.0' },
1895                { value: 'YPL-1.1', text: 'YPL-1.1' },
1896                { value: 'Zed', text: 'Zed' },
1897                { value: 'Zend-2.0', text: 'Zend-2.0' },
1898                { value: 'Zimbra-1.3', text: 'Zimbra-1.3' },
1899                { value: 'Zimbra-1.4', text: 'Zimbra-1.4' },
1900                { value: 'Zlib', text: 'Zlib' },
1901                { value: 'zlib-acknowledgement', text: 'zlib-acknowledgement' },
1902                { value: 'ZPL-1.1', text: 'ZPL-1.1' },
1903                { value: 'ZPL-2.0', text: 'ZPL-2.0' },
1904                { value: 'ZPL-2.1', text: 'ZPL-2.1' },
1905            ];
1906
1907            $scope.costOptions = [
1908                { value: 'Free of charge', text: 'Free of charge' },
1909                {
1910                    value: 'Free of charge (with restrictions)',
1911                    text: 'Free of charge (with restrictions)',
1912                },
1913                { value: 'Commercial', text: 'Commercial' },
1914            ];
1915
1916            $scope.languageOptions = [
1917                { value: 'ActionScript', text: 'ActionScript' },
1918                { value: 'Ada', text: 'Ada' },
1919                { value: 'AppleScript', text: 'AppleScript' },
1920                { value: 'Assembly language', text: 'Assembly language' },
1921                { value: 'AWK', text: 'AWK' },
1922                { value: 'Bash', text: 'Bash' },
1923                { value: 'C', text: 'C' },
1924                { value: 'C#', text: 'C#' },
1925                { value: 'C++', text: 'C++' },
1926                { value: 'Clojure', text: 'Clojure' },
1927                { value: 'COBOL', text: 'COBOL' },
1928                { value: 'ColdFusion', text: 'ColdFusion' },
1929                { value: 'Cython', text: 'Cython' },
1930                { value: 'CUDA', text: 'CUDA' },
1931                { value: 'CWL', text: 'CWL' },
1932                { value: 'D', text: 'D' },
1933                { value: 'Delphi', text: 'Delphi' },
1934                { value: 'Dylan', text: 'Dylan' },
1935                { value: 'Eiffel', text: 'Eiffel' },
1936                { value: 'Elm', text: 'Elm' },
1937                { value: 'F#', text: 'F#' },
1938                { value: 'Forth', text: 'Forth' },
1939                { value: 'Fortran', text: 'Fortran' },
1940                { value: 'Go', text: 'Go' },
1941                { value: 'Groovy', text: 'Groovy' },
1942                { value: 'Haskell', text: 'Haskell' },
1943                { value: 'Java', text: 'Java' },
1944                { value: 'JavaScript', text: 'JavaScript' },
1945                { value: 'JSP', text: 'JSP' },
1946                { value: 'Julia', text: 'Julia' },
1947                { value: 'Jython', text: 'Jython' },
1948                { value: 'Kotlin', text: 'Kotlin' },
1949                { value: 'LabVIEW', text: 'LabVIEW' },
1950                { value: 'Lisp', text: 'Lisp' },
1951                { value: 'Lua', text: 'Lua' },
1952                { value: 'Maple', text: 'Maple' },
1953                { value: 'Mathematica', text: 'Mathematica' },
1954                { value: 'MATLAB', text: 'MATLAB' },
1955                { value: 'MLXTRAN', text: 'MLXTRAN' },
1956                { value: 'NMTRAN', text: 'NMTRAN' },
1957                { value: 'OCaml', text: 'OCaml' },
1958                { value: 'Pascal', text: 'Pascal' },
1959                { value: 'Perl', text: 'Perl' },
1960                { value: 'PHP', text: 'PHP' },
1961                { value: 'Prolog', text: 'Prolog' },
1962                { value: 'PyMOL', text: 'PyMOL' },
1963                { value: 'Python', text: 'Python' },
1964                { value: 'Q#', text: 'Q#' },
1965                { value: 'QCL', text: 'QCL' },
1966                { value: 'R', text: 'R' },
1967                { value: 'Racket', text: 'Racket' },
1968                { value: 'REXX', text: 'REXX' },
1969                { value: 'Ruby', text: 'Ruby' },
1970                { value: 'Rust', text: 'Rust' },
1971                { value: 'SAS', text: 'SAS' },
1972                { value: 'Scala', text: 'Scala' },
1973                { value: 'Scheme', text: 'Scheme' },
1974                { value: 'Shell', text: 'Shell' },
1975                { value: 'Smalltalk', text: 'Smalltalk' },
1976                { value: 'SQL', text: 'SQL' },
1977                { value: 'Swift', text: 'Swift' },
1978                { value: 'Turing', text: 'Turing' },
1979                { value: 'TypeScript', text: 'TypeScript' },
1980                { value: 'Verilog', text: 'Verilog' },
1981                { value: 'VHDL', text: 'VHDL' },
1982                { value: 'Visual Basic', text: 'Visual Basic' },
1983                { value: 'XAML', text: 'XAML' },
1984                { value: 'Other', text: 'Other' },
1985            ];
1986
1987            $scope.platformOptions = [
1988                { value: 'Mac', text: 'Mac' },
1989                { value: 'Linux', text: 'Linux' },
1990                { value: 'Windows', text: 'Windows' },
1991                { value: 'Android', text: 'Android' },
1992                { value: 'iOS', text: 'iOS' },
1993            ];
1994
1995            $scope.accessibilityOptions = [
1996                { value: 'Open access', text: 'Open access' },
1997                {
1998                    value: 'Open access (with restrictions)',
1999                    text: 'Open access (with restrictions)',
2000                },
2001                { value: 'Restricted access', text: 'Restricted access' },
2002            ];
2003
2004            $scope.maturityOptions = [
2005                { value: 'Emerging', text: 'Emerging' },
2006                { value: 'Mature', text: 'Mature' },
2007                { value: 'Legacy', text: 'Legacy' },
2008            ];
2009
2010            $scope.toolTypeOptions = [
2011                { value: 'Bioinformatics portal', text: 'Bioinformatics portal' },
2012                { value: 'Command-line tool', text: 'Command-line tool' },
2013                { value: 'Database portal', text: 'Database portal' },
2014                { value: 'Desktop application', text: 'Desktop application' },
2015                { value: 'Library', text: 'Library' },
2016                { value: 'Mobile application', text: 'Mobile application' },
2017                { value: 'Ontology', text: 'Ontology' },
2018                { value: 'Plug-in', text: 'Plug-in' },
2019                { value: 'Script', text: 'Script' },
2020                { value: 'SPARQL endpoint', text: 'SPARQL endpoint' },
2021                { value: 'Suite', text: 'Suite' },
2022                { value: 'Web application', text: 'Web application' },
2023                { value: 'Web API', text: 'Web API' },
2024                { value: 'Web service', text: 'Web service' },
2025                { value: 'Workbench', text: 'Workbench' },
2026                { value: 'Workflow', text: 'Workflow' },
2027            ];
2028
2029            $scope.linkTypeOptions = [
2030                { value: 'Discussion forum', text: 'Discussion forum' },
2031                { value: 'Galaxy service', text: 'Galaxy service' },
2032                { value: 'Helpdesk', text: 'Helpdesk' },
2033                { value: 'Issue tracker', text: 'Issue tracker' },
2034                { value: 'Mailing list', text: 'Mailing list' },
2035                { value: 'Mirror', text: 'Mirror' },
2036                { value: 'Repository', text: 'Repository' },
2037                { value: 'Service', text: 'Service' },
2038                { value: 'Social media', text: 'Social media' },
2039                { value: 'Software catalogue', text: 'Software catalogue' },
2040                { value: 'Technical monitoring', text: 'Technical monitoring' },
2041                { value: 'Other', text: 'Other' },
2042            ];
2043
2044            $scope.downloadTypeOptions = [
2045                { value: 'Downloads page', text: 'Downloads page' },
2046                { value: 'API specification', text: 'API specification' },
2047                { value: 'Biological data', text: 'Biological data' },
2048                { value: 'Binaries', text: 'Binaries' },
2049                { value: 'Command-line specification', text: 'Command-line specification' },
2050                { value: 'Container file', text: 'Container file' },
2051                { value: 'Icon', text: 'Icon' },
2052                { value: 'Screenshot', text: 'Screenshot' },
2053                { value: 'Software package', text: 'Software package' },
2054                { value: 'Source code', text: 'Source code' },
2055                { value: 'Test data', text: 'Test data' },
2056                { value: 'Test script', text: 'Test script' },
2057                { value: 'Tool wrapper (CWL)', text: 'Tool wrapper (CWL)' },
2058                { value: 'Tool wrapper (Galaxy)', text: 'Tool wrapper (Galaxy)' },
2059                { value: 'Tool wrapper (Taverna)', text: 'Tool wrapper (Taverna)' },
2060                { value: 'Tool wrapper (Other)', text: 'Tool wrapper (Other)' },
2061                { value: 'VM Image', text: 'VM Image' },
2062                { value: 'Other', text: 'Other' },
2063            ];
2064
2065            $scope.documentationTypeOptions = [
2066                { value: 'API documentation', text: 'API documentation' },
2067                { value: 'Citation instructions', text: 'Citation instructions' },
2068                { value: 'Code of conduct', text: 'Code of conduct' },
2069                { value: 'Command-line options', text: 'Command-line options' },
2070                { value: 'Contributions policy', text: 'Contributions policy' },
2071                { value: 'FAQ', text: 'FAQ' },
2072                { value: 'General', text: 'General' },
2073                { value: 'Governance', text: 'Governance' },
2074                { value: 'Installation instructions', text: 'Installation instructions' },
2075                { value: 'Quick start guide', text: 'Quick start guide' },
2076                { value: 'Release notes', text: 'Release notes' },
2077                { value: 'Terms of use', text: 'Terms of use' },
2078                { value: 'Training material', text: 'Training material' },
2079                { value: 'User manual', text: 'User manual' },
2080                { value: 'Other', text: 'Other' },
2081            ];
2082
2083            $scope.publicationTypeOptions = [
2084                { value: 'Primary', text: 'Primary' },
2085                { value: 'Benchmarking study', text: 'Benchmarking study' },
2086                { value: 'Method', text: 'Method' },
2087                { value: 'Usage', text: 'Usage' },
2088                { value: 'Review', text: 'Review' },
2089                { value: 'Preprint', text: 'Preprint' },
2090                { value: 'Other', text: 'Other' },
2091            ];
2092
2093            $scope.entityTypeOptions = [
2094                { value: 'Person', text: 'Person' },
2095                { value: 'Project', text: 'Project' },
2096                { value: 'Division', text: 'Division' },
2097                { value: 'Institute', text: 'Institute' },
2098                { value: 'Consortium', text: 'Consortium' },
2099                { value: 'Funding agency', text: 'Funding agency' },
2100            ];
2101
2102            $scope.roleTypeOptions = [
2103                { value: 'Primary contact', text: 'Primary contact' },
2104                { value: 'Contributor', text: 'Contributor' },
2105                { value: 'Developer', text: 'Developer' },
2106                { value: 'Documentor', text: 'Documentor' },
2107                { value: 'Maintainer', text: 'Maintainer' },
2108                { value: 'Provider', text: 'Provider' },
2109                { value: 'Support', text: 'Support' },
2110            ];
2111
2112            $scope.elixirPlatformOptions = [
2113                { value: 'Data', text: 'Data' },
2114                { value: 'Tools', text: 'Tools' },
2115                { value: 'Compute', text: 'Compute' },
2116                { value: 'Interoperability', text: 'Interoperability' },
2117                { value: 'Training', text: 'Training' },
2118            ];
2119
2120            $scope.elixirNodeOptions = [
2121                { value: 'Belgium', text: 'Belgium' },
2122                { value: 'Czech Republic', text: 'Czech Republic' },
2123                { value: 'Denmark', text: 'Denmark' },
2124                { value: 'EMBL', text: 'EMBL' },
2125                { value: 'Estonia', text: 'Estonia' },
2126                { value: 'Finland', text: 'Finland' },
2127                { value: 'France', text: 'France' },
2128                { value: 'Germany', text: 'Germany' },
2129                { value: 'Greece', text: 'Greece' },
2130                { value: 'Hungary', text: 'Hungary' },
2131                { value: 'Ireland', text: 'Ireland' },
2132                { value: 'Israel', text: 'Israel' },
2133                { value: 'Italy', text: 'Italy' },
2134                { value: 'Luxembourg', text: 'Luxembourg' },
2135                { value: 'Netherlands', text: 'Netherlands' },
2136                { value: 'Norway', text: 'Norway' },
2137                { value: 'Portugal', text: 'Portugal' },
2138                { value: 'Slovenia', text: 'Slovenia' },
2139                { value: 'Spain', text: 'Spain' },
2140                { value: 'Sweden', text: 'Sweden' },
2141                { value: 'Switzerland', text: 'Switzerland' },
2142                { value: 'UK', text: 'UK' },
2143            ];
2144
2145            $scope.elixirCommunityOptions = [
2146                { value: '3D-BioInfo', text: '3D-BioInfo', link: '3d-bioinfo' },
2147                { value: 'Biodiversity', text: 'Biodiversity', link: 'biodiversity' },
2148                { value: 'Cancer Data', text: 'Cancer Data', link: 'cancer-data' },
2149                { value: 'Federated Human Data', text: 'Federated Human Data', link: 'human-data' },
2150                {
2151                    value: 'Food and Nutrition',
2152                    text: 'Food and Nutrition',
2153                    link: 'food-and-nutrition',
2154                },
2155                { value: 'Galaxy', text: 'Galaxy', link: 'galaxy' },
2156                {
2157                    value: 'Human Copy Number Variation',
2158                    text: 'Human Copy Number Variation',
2159                    link: 'hcnv',
2160                },
2161                {
2162                    value: 'Intrinsically Disordered Proteins',
2163                    text: 'Intrinsically Disordered Proteins',
2164                    link: 'intrinsically-disordered-proteins',
2165                },
2166                {
2167                    value: 'Marine Metagenomics',
2168                    text: 'Marine Metagenomics',
2169                    link: 'marine-metagenomics',
2170                },
2171                { value: 'Metabolomics', text: 'Metabolomics', link: 'metabolomics' },
2172                {
2173                    value: 'Microbial Biotechnology',
2174                    text: 'Microbial Biotechnology',
2175                    link: 'microbial-biotechnology',
2176                },
2177                { value: 'Microbiome', text: 'Microbiome', link: 'microbiome' },
2178                { value: 'Plant Sciences', text: 'Plant Sciences', link: 'plant-sciences' },
2179                { value: 'Proteomics', text: 'Proteomics', link: 'proteomics' },
2180                { value: 'Rare Diseases', text: 'Rare Diseases', link: 'rare-diseases' },
2181                {
2182                    value: 'Research Data Management',
2183                    text: 'Research Data Management',
2184                    link: 'research-data-management',
2185                },
2186                {
2187                    value: 'Single-cell Omics',
2188                    text: 'Single-cell Omics',
2189                    link: 'single-cell-omics',
2190                },
2191                { value: 'Systems Biology', text: 'Systems Biology', link: 'systems-biology' },
2192                { value: 'Toxicology', text: 'Toxicology', link: 'toxicology' },
2193            ];
2194
2195            $scope.otherIdTypeOptions = [
2196                { value: 'doi', text: 'doi' },
2197                { value: 'rrid', text: 'rrid' },
2198                { value: 'cpe', text: 'cpe' },
2199            ];
2200
2201            $scope.relationTypeOptions = [
2202                { value: 'isNewVersionOf', text: 'isNewVersionOf' },
2203                { value: 'hasNewVersion', text: 'hasNewVersion' },
2204                { value: 'uses', text: 'uses' },
2205                { value: 'usedBy', text: 'usedBy' },
2206                { value: 'includes', text: 'includes' },
2207                { value: 'includedIn', text: 'includedIn' },
2208            ];
2209
2210            $scope.confidenceOptions = [
2211                { value: 'tool', text: 'tool' },
2212                { value: 'high', text: 'high' },
2213                { value: 'medium', text: 'medium' },
2214                { value: 'low', text: 'low' },
2215                { value: 'very low', text: 'very low' },
2216            ];
2217
2218            $scope.$watch(
2219                'software',
2220                function () {
2221                    angular.forEach($scope.software, function (value, key) {
2222                        if (value === null || value === '') {
2223                            delete $scope.software[key];
2224                        }
2225                    });
2226                },
2227                true
2228            );
2229        },
2230    ])
2231    .controller('ToolUpdateController', [
2232        '$scope',
2233        '$controller',
2234        '$timeout',
2235        '$state',
2236        '$stateParams',
2237        'Tool',
2238        'ToolUpdateValidator',
2239        'CommunityCollection',
2240        function (
2241            $scope,
2242            $controller,
2243            $timeout,
2244            $state,
2245            $stateParams,
2246            Tool,
2247            ToolUpdateValidator,
2248            CommunityCollection
2249        ) {
2250            // inherit common controller
2251            $controller('ToolEditController', { $scope: $scope });
2252
2253            // sets which controller is in use, so the HTML can adapt
2254            $scope.controller = 'update';
2255
2256            // set the ID to not autoupdate when name is changed
2257            $scope.autoUpdateId = false;
2258            $scope.CommunityCollection = CommunityCollection;
2259            $scope.validateButtonClick = function () {
2260                $timeout(function () {
2261                    $scope.sendResource(
2262                        ToolUpdateValidator.update,
2263                        $scope.validationProgress,
2264                        false,
2265                        'update-validate'
2266                    );
2267                }, 100);
2268            };
2269
2270            $scope.registerButtonClick = function () {
2271                $timeout(function () {
2272                    if (confirm('Are you sure you want to update the resource? ')) {
2273                        $scope.sendResource(Tool.update, $scope.savingProgress, false, 'update');
2274                    }
2275                }, 100);
2276            };
2277
2278            $scope.deleteButtonClick = function () {
2279                $timeout(function () {
2280                    if (confirm('Are you sure you want to remove the resource? ')) {
2281                        if (
2282                            confirm(
2283                                'This will remove the resource and cannot be undone. Are you sure you want to continue? '
2284                            )
2285                        ) {
2286                            $scope.sendResource(
2287                                Tool.remove,
2288                                $scope.deletingProgress,
2289                                true,
2290                                'delete'
2291                            );
2292                        }
2293                    }
2294                }, 100);
2295            };
2296
2297            $scope.naviagateToTool = function (biotoolsID) {
2298                $timeout(function () {
2299                    if (
2300                        confirm(
2301                            'Make sure you save before navigating away! Are you sure you want to leave? '
2302                        )
2303                    ) {
2304                        $state.go('tool', { id: biotoolsID }, { reload: true });
2305                    }
2306                }, 100);
2307            };
2308
2309            // when a tool is being updated, display the current URL
2310            // $scope.$watch('software', function() {
2311            // 	$scope.setURL();
2312            // })
2313        },
2314    ])
2315    .controller('ToolCreateController', [
2316        '$scope',
2317        '$controller',
2318        '$timeout',
2319        'ToolListConnection',
2320        'ToolCreateValidator',
2321        'User',
2322        '$stateParams',
2323        'CommunityCollection',
2324        function (
2325            $scope,
2326            $controller,
2327            $timeout,
2328            ToolListConnection,
2329            ToolCreateValidator,
2330            User,
2331            $stateParams,
2332            CommunityCollection
2333        ) {
2334            // inherit common controller
2335            $controller('ToolEditController', { $scope: $scope });
2336            $scope.orderby = 'text';
2337            // sets which controller is in use, so the HTML can adapt
2338            $scope.controller = 'create';
2339
2340            // initially set the ID to change automatically when name is modified
2341            $scope.biotoolsIDDisabled = true;
2342            $scope.editIdButtonText = 'Edit ID';
2343            $scope.CommunityCollection = CommunityCollection;
2344            // remove or replace all URL unsafe characters and set softw
2344are.id
2345            $scope.makeIdURLSafe = function (value) {
2346                if (typeof value != 'undefined' && $scope.biotoolsIDDisabled) {
2347                    $scope.software.biotoolsID = value
2348                        .replace(/[^a-zA-Z0-9_~ .-]*/g, '')
2349                        .replace(/[ ]+/g, '_')
2350                        .toLowerCase();
2351                } else if ($scope.biotoolsIDDisabled) {
2352                    $scope.software.biotoolsID = '';
2353                }
2354            };
2355
2356            $scope.editIdToggleButtonClick = function () {
2357                $scope.biotoolsIDDisabled = !$scope.biotoolsIDDisabled;
2358                $scope.makeIdURLSafe($scope.software.name);
2359
2360                if ($scope.biotoolsIDDisabled) {
2361                    $scope.editIdButtonText = 'Edit ID';
2362                } else {
2363                    $scope.editIdButtonText = 'From Name';
2364                }
2365            };
2366
2367            $scope.validateButtonClick = function () {
2368                $timeout(function () {
2369                    $scope.sendResource(
2370                        ToolCreateValidator.save,
2371                        $scope.validationProgress,
2372                        false,
2373                        'create-validate'
2374                    );
2375                }, 100);
2376            };
2377
2378            $scope.registerButtonClick = function () {
2379                if (
2380                    confirm(
2381                        'Are you sure you want to save the resource?\nOnce saved the tool ID cannot be changed!'
2382                    )
2383                ) {
2384                    $timeout(function () {
2385                        $scope.sendResource(
2386                            ToolListConnection.save,
2387                            $scope.savingProgress,
2388                            false,
2389                            'create'
2390                        );
2391                    }, 100);
2392                }
2393            };
2394
2395            // TODO: needs to keep it DRY and in a service
2396            // function to clean all nulls from tool gotten from API
2397            function cleanNulls(object) {
2398                for (var key in object) {
2399                    if (object[key] == null) {
2400                        delete object[key];
2401                    } else if (object[key].constructor === Array) {
2402                        if (object[key].length == 0) {
2403                            delete object[key];
2404                        } else {
2405                            for (var i in object[key]) {
2406                                cleanNulls(object[key][i]);
2407                            }
2408                        }
2409                    } else if (typeof object[key] === 'object') {
2410                        cleanNulls(object[key]);
2411                    }
2412                }
2413            }
2414
2415            if (typeof $stateParams.newVersionOf !== 'undefined') {
2416                $scope.newVersion = true;
2417                $scope.software = ToolLatest.get(
2418                    { id: $stateParams.newVersionOf },
2419                    function (response) {
2420                        // success handler
2421                        cleanNulls($scope.software);
2422                        $scope.software.version = null;
2423                    },
2424                    function (response) {
2425                        // error handler
2426                        if (response.status == 404) {
2427                            $scope.notFound = true;
2428                        }
2429                    }
2430                );
2431            } else {
2432                $scope.newVersion = false;
2433                // create the 'empty' software object
2434                $timeout(function () {
2435                    $scope.software = {
2436                        owner: $scope.User.getUsername(),
2437                        name: '',
2438                        description: '',
2439                        homepage: '',
2440                    };
2441                }, 100);
2442            }
2443        },
2444    ])
2445    .controller('OrcidCallbackController', [
2446        '$scope',
2447        '$state',
2448        'djangoAuth',
2449        '$location',
2450        'OrcidAuth',
2451        function ($scope, $state, djangoAuth, $location, OrcidAuth) {
2452            var code = $location.search().code;
2453            const stateParams = $location.search().state;
2454
2455            const stateCheck = OrcidAuth.consumeState(stateParams);
2456            if (!stateCheck.ok) {
2457                $scope.loginErrors = 'ORCID authentication failed: invali
2457d state parameter';
2458                return;
2459            }
2460
2461            function goAfterAuth() {
2462                if (stateCheck.payload && stateCheck.payload.name) {
2463                    $state.go(stateCheck.payload.name, stateCheck.payload.params || {});
2464                } else {
2465                    $state.go('search');
2466                }
2467            }
2468
2469            // if user is authenticated call djangoAuth.orcidConnect
2470            if (djangoAuth.authenticated) {
2471                djangoAuth.orcidConnect(code).then(
2472                    function () {
2473                        $state.go('profile');
2474                    },
2475                    function () {
2476                        $scope.loginErrors = 'ORCID connection failed';
2477                    }
2478                );
2479            } else {
2480                djangoAuth.orcidLogin(code).then(
2481                    function () {
2482                        goAfterAuth();
2483                    },
2484                    function () {
2485                        var error_description = $location.search().error_description;
2486                        $scope.loginErrors = error_description || 'ORCID login failed';
2487                    }
2488                );
2489            }
2490        },
2491    ])
2492    .controller('LoginController', [
2493        '$scope',
2494        '$state',
2495        'djangoAuth',
2496        '$rootScope',
2497        'OrcidAuth',
2498        function ($scope, $state, djangoAuth, $rootScope, OrcidAuth) {
2499            $scope.credentials = {};
2500
2501            $scope.loginButtonClick = function () {
2502                djangoAuth.login($scope.credentials.username, $scope.credentials.password).then(
2503                    function (response) {
2504                        // go to states set before redirection to login
2505                        if (
2506                            typeof $rootScope.toState == 'undefined' ||
2507                            /signup/.test($rootScope.toState.name) ||
2508                            /reset-password/.test($rootScope.toState.name)
2509                        ) {
2510                            $state.go('search');
2511                        } else {
2512                            $state.go($rootScope.toState.name, $rootScope.toStateParams);
2513                        }
2514                    },
2515                    function (response) {
2516                        $scope.loginErrors = response.general_errors;
2517                    }
2518                );
2519            };
2520
2521            // clean errors when credentials are changed
2522            $scope.$watch(
2523                'credentials',
2524                function () {
2525                    if ($scope.loginErrors) {
2526                        $scope.loginErrors.pop();
2527                        delete $scope.loginErrors;
2528                    }
2529                },
2530                true
2531            );
2532
2533            $scope.orcidLoginButtonClick = function () {
2534                OrcidAuth.start();
2535            };
2536        },
2537    ])
2538    .controller('SignupController', [
2539        '$scope',
2540        '$state',
2541        'djangoAuth',
2542        '$rootScope',
2543        '$timeout',
2544        'OrcidAuth',
2545        function ($scope, $state, djangoAuth, $rootScope, $timeout, OrcidAuth) {
2546            $scope.credentials = {};
2547            $scope.error_message = {};
2548            $scope.error_message.username = '';
2549            $scope.error_message.email = '';
2550            $scope.error_message.creation = '';
2551
2552            // check if username is taken
2553            var initializing_username = true;
2554            $scope.$watch('credentials.username', function (newValue, oldValue, scope) {
2555                if (!initializing_username) {
2556                    $scope.error_message.username = '';
2557                    djangoAuth.register($scope.credentials.username, null, null, null, null).then(
2558                        function (response) {
2559                            // success never happens since parameters are missing
2560                        },
2561                        function (response) {
2562                            if (response.hasOwnProperty('username')) {
2563                                $scope.error_message.username = 'Username is already taken.';
2564                            }
2565                        }
2566                    );
2567                } else {
2568                    initializing_username = false;
2569                }
2570            });
2571
2572            // check if email is taken
2573            var initializing_email = true;
2574            $scope.$watch('credentials.email', function (newValue, oldValue, scope) {
2575                if (!initializing_email) {
2576                    $scope.error_message.email = '';
2577                    djangoAuth.register(null, null, null, $scope.credentials.email, null).then(
2578                        function (response) {
2579                            // success never happens since parameters are missing
2580                        },
2581                        function (response) {
2582                            if (response.hasOwnProperty('email')) {
2583                                $scope.error_message.email = 'Email is invalid or already taken.';
2584                            }
2585                        }
2586                    );
2587                } else {
2588                    initializing_email = false;
2589                }
2590            });
2591
2592            $scope.signupButtonClick = function () {
2593                $scope.loading = true;
2594                $timeout(function () {
2595                    djangoAuth
2596                        .register(
2597                            $scope.credentials.username,
2598                            $scope.credentials.password,
2599                            $scope.credentials.password,
2600                            $scope.credentials.email,
2601                            null
2602                        )
2603                        .then(
2604                            function (response) {
2605                                $state.go('signup.success');
2606                                $scope.loading = false;
2607                            },
2608                            function (response) {
2609                                $scope.error_message.email = response.email.join();
2610                                $scope.error_message.username = response.username.join();
2611                                $scope.error_message.creation = response.message;
2612                                $scope.loading = false;
2613                            }
2614                        );
2615                }, 100);
2616            };
2617
2618            $scope.orcidSignupButtonClick = function () {
2619                OrcidAuth.start();
2620            };
2621        },
2622    ])
2623    .controller('SignupVerifyEmailKeyController', [
2624        '$scope',
2625        '$state',
2626        '$stateParams',
2627        'djangoAuth',
2628        function ($scope, $state, $stateParams, djangoAuth) {
2629            $scope.error_message = '';
2630
2631            djangoAuth.verify($stateParams.key).then(
2632                function (response) {
2633                    $state.go('signup.verify-email.success');
2634                },
2635                function (response) {
2636                    $scope.error_message = response.message;
2637                }
2638            );
2639        },
2640    ])
2641    .controller('ResetPasswordController', [
2642        '$scope',
2643        '$state',
2644        '$stateParams',
2645        'djangoAuth',
2646        function ($scope, $state, $stateParams, djangoAuth) {
2647            $scope.credentials = {};
2648            $scope.error_message = '';
2649            $scope.success_message = '';
2650
2651            $scope.loading = false;
2652            $scope.resetButtonClick = function () {
2653                $scope.error_message = '';
2654                $scope.success_message = '';
2655                $scope.loading = true;
2656                djangoAuth.resetPassword($scope.credentials.email).then(
2657                    function (response) {
2658                        $scope.success_message =
2659                            'If this email is linked to an account, a reset link will be sent.';
2660                        $scope.loading = false;
2661                    },
2662                    function (response) {
2663                        $scope.error_message = response;
2664                        $scope.loading = false;
2665                    }
2666                );
2667            };
2668        },
2669    ])
2670    .controller('ResetPasswordConfirmController', [
2671        '$scope',
2672        '$state',
2673        '$stateParams',
2674        'djangoAuth',
2675        function ($scope, $state, $stateParams, djangoAuth) {
2676            $scope.credentials = {};
2677            $scope.error_message = false;
2678            var uid = $stateParams.uid;
2679            var token = $stateParams.token;
2680
2681            $scope.loading = false;
2682            $scope.resetButtonClick = function () {
2683                $scope.error_message = false;
2684                $scope.loading = true;
2685                djangoAuth
2686                    .confirmReset(
2687                        uid,
2688                        token,
2689                        $scope.credentials.password,
2690                        $scope.credentials.password
2691                    )
2692                    .then(
2693                        function (response) {
2694                            $state.go('reset-password.confirm.success');
2695                            $scope.loading = false;
2696                        },
2697                        function (response) {
2698                            $scope.error_message = true;
2699                            $scope.loading = false;
2700                        }
2701                    );
2702            };
2703        },
2704    ]);
2705
2706function EdamModalCtrl($uibModalInstance, edam, onto, type, suggestions) {
2707    var vm = this;
2708    vm.data = angular.copy(edam);
2709    vm.onto = onto;
2710    vm.self = $uibModalInstance;
2711    vm.type = type;
2712    vm.suggestions = suggestions;
2713
2714    vm.saveData = function () {
2715        if (isEmptyObject(vm.data)) {
2716            $uibModalInstance.dismiss('cancel');
2717            return;
2718        }
2719        $uibModalInstance.close(vm.data);
2720    };
2721
2722    vm.apply_suggestion = function (suggestion) {
2723        vm.predicate = suggestion.term;
2724    };
2725
2726    vm.customOrder = function (node) {
2727        if (!vm.suggestions) {
2728            return node.text.toLowerCase();
2729        }
2730
2731        var isSuggested = containsSuggestion(node);
2732        return (isSuggested ? '0' : '1') + node.text.toLowerCase();
2733    };
2734
2735    vm.isSuggested = function (node) {
2736        return vm.suggestions && containsSuggestion(node);
2737    };
2738
2739    vm.cancel = function () {
2740        $uibModalInstance.dismiss('cancel');
2741    };
2742
2743    function isEmptyObject(obj) {
2744        return angular.equals(obj, {});
2745    }
2746
2747    function containsSuggestion(node) {
2748        return vm.suggestions.some(function (suggestion) {
2749            return suggestion.term === node.text;
2750        });
2751    }
2752}
2753
2754'use strict';
2755
2756/* Directives */
2757
2758angular.module('elixir_front.directives', [])
2759.directive('toolList', ['ToolList', 'Highlighting', function(ToolList, Highlighting) {
2760	return {
2761		restrict: 'A',
2762		templateUrl: 'partials/directives/toolList.html',
2763		link: function(scope, element, attrs) {
2764			// reference the ToolList in the scope
2765			scope.ToolList = ToolList;
2766			// reference Highlighting in the scope
2767			scope.Highlighting = Highlighting;
2768		}
2769	}
2770}])
2771.directive('showMore',
2772	[function(){
2773		return {
2774			templateUrl: 'showMore.html',
2775			restrict: 'A',
2776			transclude: true,
2777			scope:{
2778				'showMoreHeight': '@'
2779			},
2780			controller: ['$scope', '$element', '$interval', function($scope, $element, $interval) {
2781
2782				$scope.expanded = false;
2783
2784				$interval(function(){
2785					renderStyles();
2786				}, 300);
2787
2788				$scope.expandable = false;
2789				function renderStyles(){
2790					if($element.height() >= $scope.showMoreHeight && $scope.expanded === false){
2791						$scope.expandable = true;
2792					}
2793				}
2794
2795				$scope.showLessStyle = {
2796					'max-height': $scope.showMoreHeight + 'px',
2797					'overflow': 'hidden'
2798				};
2799
2800			}]
2801		};
2802	}])
2803.directive('toolListTable', ['ToolList', 'ToolTableDataSource', 'Highlighting', function(ToolList, ToolTableDataSource, Highlighting) {
2804	return {
2805		restrict: 'A',
2806		templateUrl: 'partials/directives/toolListTable.html',
2807		link: function(scope, element, attrs) {
2808			// reference Highlighting in the scope
2809			scope.Highlighting = Highlighting;
2810			scope.ToolTableDataSource = ToolTableDataSource;
2811		}
2812	}
2813}])
2814.directive('toolSorter', ['ToolList', '$state','$stateParams', '$timeout', 'ToolSorter', 'Query', '$rootScope', function(ToolList, $state, $stateParams, $timeout, ToolSorter, Query, $rootScope) {
2815	return {
2816		restrict: 'A',
2817		templateUrl: 'partials/directives/sorter.html',
2818		link: function(scope, element, attrs) {
2819			// reference the ToolPaginator in the scope
2820			scope.ToolSorter = ToolSorter;
2821
2822			// put sorting attributes in the url
2823			scope.addToURL = function() {
2824				var params = $stateParams;
2825				// set the sorting url parameters
2826				if (ToolSorter.sortBy.attrName == 'lastUpdate' && ToolSorter.order == true) {
2827					// set defaults as null
2828					params['sort'] = null;
2829					params['ord'] = null;
2830				} else {
2831					params['sort'] = ToolSorter.sortBy.attrName;
2832					params['ord'] = ToolSorter.order ? 'desc' : 'asc';
2833				}
2834				$state.transitionTo('search', params, { notify: false });
2835			}
2836
2837			// ran on changing the dropdown value
2838			scope.sortingChangedByDropdown = function() {
2839				scope.addToURL();
2840				$timeout( function() {
2841					ToolList.refresh();
2842				});
2843			}
2844
2845			scope.validateSelectedSortOption = function() {
2846  				if (Query.current.length > 0) {
2847					ToolSorter.addScore();
2848				} else {
2849					ToolSorter.removeScore();
2850				}
2851				ToolSorter.setSortOption($stateParams['sort']);
2852  			}
2853
2854			// read current sorting attribute from url
2855			if ($stateParams['sort'] != null) {
2856				if ($stateParams['sort'] == 'score') {
2857					ToolSorter.addScore();
2858				}
2859				ToolSorter.sortBy = ToolSorter.list[_.findIndex(ToolSorter.list, {"attrName": $stateParams['sort']}) || 0];
2860			}
2861			// read current sorting order from url
2862			if ($stateParams['ord'] != null) {
2863				if ($stateParams['ord'] == 'desc') {
2864					ToolSorter.order = true;
2865				} else if($stateParams['ord'] == 'asc'){
2866					ToolSorter.order = false;
2867				}
2868			}
2869
2870			$rootScope.$on('$stateChangeSuccess', function(event, toState, toStateParams, fromState, fromStateParams) {
2871				scope.validateSelectedSortOption();
2872			});
2873
2874			$rootScope.$on('search-bar-tags-changed', function handler() {
2875    			scope.validateSelectedSortOption();
2876  			});
2877
2878
2879		}
2880	}
2881}])
2882.directive('displayModeSelector', ['ToolList', '$state','$stateParams', '$timeout', 'DisplayModeSelector', function(ToolList, $state, $stateParams, $timeout, DisplayModeSelector) {
2883	return {
2884		restrict: 'A',
2885		templateUrl: 'partials/directives/displaySelector.html',
2886		link: function(scope, element, attrs) {
2887			scope.DisplayModeSelector = DisplayModeSelector;
2888
2889			// put sorting attributes in the url
2890			scope.modeChanged = function() {
2891				$timeout( function() {
2892					ToolList.refresh();
2893				});
2894			}
2895		}
2896	}
2897}])
2898.directive('toolPaginator', ['ToolList', '$state','$stateParams', 'Query', '$timeout', 'ToolPaginator', function(ToolList, $state, $stateParams, Query, $timeout, ToolPaginator) {
2899	return {
2900		restrict: 'A',
2901		template: '<div style="display: flex; align-items: center; margin-top: 25px; margin-bottom: 25px;"><ul uib-pagination total-items="ToolList.count" ng-model="ToolPaginator.currentPage" items-per-page="ToolPaginator.pageSize" max-size="ToolPaginator.maxSize" ng-change="pageChanged()" class="pagination-sm pagination-top" boundary-links="true" rotate="true" ng-disabled="ToolList.loading"></ul><div style="margin-left: 15px; display: flex; align-items: center;"><label for="pageInput" style="margin-right: 5px;">Go to page:</label><input type="number" id="pageInput" ng-model="inputPage" min="1" max="{{Math.ceil(ToolList.count / ToolPaginator.pageSize)}}" ng-keypress="handlePageInput($event)" style="width: 60px;" /><label style="margin-left: 5px; margin-right: 5px;">Page size:</label><select ng-model="ToolPaginator.pageSize" ng-change="pageSizeChanged()"><option ng-repeat="size in ToolPaginator.availablePageSizes" ng-value="size">{{size}}</option></select></div></div>',
2902		link: function(scope, element, attrs) {
2903			// reference the Query in the scope
2904			scope.Query = Query;
2905			// reference the ToolPaginator in the scope
2906			scope.ToolPaginator = ToolPaginator;
2907
2908			// read current page from url
2909			if ($stateParams['page'] != null) {
2910				ToolPaginator.currentPage = parseInt($stateParams['page']);
2911			} else {
2912				ToolPaginator.currentPage = 1;
2913			}
2914
2915			// when page changes, store the new page in the url and refresh the list of tools
2916			scope.pageChanged = function() {
2917				var params = $stateParams;
2918				$timeout( function() {
2919					// set the 'page' URL parameter according to currentPage
2920					params['page'] = ToolPaginator.currentPage != 1 ? ToolPaginator.currentPage : null;
2921					$state.transitionTo('search', params, { notify: false });
2922					ToolList.refresh();
2923				});
2924			}
2925
2926			// Allow direct page navigation via input
2927            scope.handlePageInput = function(event) {
2928                if (event.key === 'Enter') { // Navigate on pressing Enter
2929                    if (
2930                        scope.inputPage >= 1 &&
2931                        scope.inputPage <= Math.ceil(ToolList.count / ToolPaginator.pageSize)
2932                    ) {
2933                        ToolPaginator.currentPage = scope.inputPage;
2934                        scope.pageChanged();
2935                    } else {
2936                        alert('Invalid page number.');
2937                    }
2938                }
2939            };
2940
2941			// page size 
2942			scope.pageSizeChanged = function() {
2943				ToolPaginator.currentPage = 1;
2944				var params = $stateParams;
2945				params['per_page'] = ToolPaginator.pageSize;
2946				params['page'] = null;
2947				$state.transitionTo('search', params, { notify: false });
2948				ToolList.refresh();
2949			};
2950
2951			// riddiculous hax, otherwise it keeps changing to 1
2952			var initializing = true;
2953			scope.$watch('ToolPaginator.currentPage', function(newVal, oldVal) {
2954				if (initializing) {
2955					if (newVal != oldVal && newVal == 1) {ToolPaginator.currentPage = oldVal};
2956					$timeout(function() { initializing = false; });
2957				}
2958			});
2959
2960			// reset page to 1 when query changes
2961			scope.$watch('Query.current', function() {
2962				ToolPaginator.currentPage = 1;
2963			}, true);
2964
2965		}
2966	}
2967}])
2968
2969// inserts a link to a publication resolving page, depending on the type of publication
2970.directive("insertPublicationLink", function($compile){
2971	return {
2972		restrict: 'A',
2973		scope: {
2974			publication: '='
2975		},
2976		compile: function CompilingFunction($templateElement, $templateAttributes) {
2977			return function LinkingFunction($scope, $element, $attrs) {
2978				var html = '';
2979				// PUBMED
2980				if (/^\d{7,8}$/.test($scope.publication)) {
2981					html = '<span>PUBMED:</span> <a href="http://www.ncbi.nlm.nih.gov/pubmed/' + $scope.publication + '">' + $scope.publication + '</a>';
2982				// PMC
2983			} else if (/^PMC\d{6,8}$/.test($scope.publication)) {
2984				html = '<span>PMC:</span> <a href="http://www.ncbi.nlm.nih.gov/pmc/' + $scope.publication + '">' + $scope.publication + '</a>';
2985				// DOI
2986			} else if (/^(doi:)?\d{2}.\d{4}/.test($scope.publication)) {
2987				var publication = $scope.publication.replace('doi:','');
2988				html = '<span>DOI:</span> <a href="https://dx.doi.org/' + publication + '">' + publication + '</a>';
2989			}
2990			var e = $compile(html)($scope);
2991			$element.replaceWith(e);
2992		};
2993	}
2994}
2995})
2996// inserts a link to a publication resolving page, depending on the type of publication
2997.directive("insertPublicationLinkIcon", function($compile){
2998	return {
2999		restrict: 'A',
3000		scope: {
3001			publication: '='
3002		},
3003		compile: function CompilingFunction($templateElement, $templateAttributes) {
3004			return function LinkingFunction($scope, $element, $attrs) {
3005				var html = '';
3006				// PUBMED
3007				if (/^\d{7,8}$/.test($scope.publication)) {
3008					html = '<a href="http://www.ncbi.nlm.nih.gov/pubmed/' + $scope.publication + '"><i class="fa fa-external-link" aria-hidden="true" uib-tooltip="PUBMED" tooltip-append-to-body="true"></i></a>';
3009				// PMC
3010			} else if (/^PMC\d{6,8}$/.test($scope.publication)) {
3011				html = '<a href="http://www.ncbi.nlm.nih.gov/pmc/' + $scope.publication + '"><i class="fa fa-external-link" aria-hidden="true" uib-tooltip="PMC" tooltip-append-to-body="true"></i></a>';
3012				// DOI
3013			} else if (/^(doi:)?\d{2}.\d{4}/.test($scope.publication)) {
3014				var publication = $scope.publication.replace('doi:','');
3015				html = '<a href="https://dx.doi.org/' + publication + '"><i class="fa fa-external-link" aria-hidden="true" uib-tooltip="DOI" tooltip-append-to-body="true"></i></a>';
3016			}
3017			var e = $compile(html)($scope);
3018			$element.replaceWith(e);
3019		};
3020	}
3021}
3022})
3023// makes this element focused
3024.directive('focus', function () {
3025	return {
3026		restrict: 'A',
3027		link: function(scope, iElm, iAttrs, controller) {
3028			iElm[0].focus();
3029		}
3030	}
3031})
3032// creates an animated spinner
3033.directive('spinner', function() {
3034	return {
3035		restrict: 'A',
3036		replace: 'true',
3037		template: '<div class="adjust"><div class="spinner"></div></div>'
3038
3039	}
3040})
3041// creates a resource not found message
3042.directive('resourceNotFoundDialog', function() {
3043	return {
3044		restrict: 'A',
3045		replace: 'true',
3046		template: '<div class="col-md-12 info-dialog-background info-dialog info-dialog-text">We\'re sorry but we couldn\'t find what you were looking for. You can try searching for the resource you need <a ui-sref="search()">here</a>.</div>'
3047	}
3048})
3049// creates an error message
3050.directive('errorDialog', function() {
3051	return {
3052		restrict: 'A',
3053		replace: true,
3054		template: function(elem, attr){
3055			return '<div class="col-md-offset-2 col-md-8 bs-callout bs-callout-danger" style="margin-top: 80px;"><h4><i class="fa fa-exclamation-circle" aria-hidden="true"></i> Error {{' + attr.errorcode + '}}</h4>{{' + attr.errormessage + '}}</div>'
3056		},
3057		transclude: true
3058	}
3059})
3060// make this element ignore pressing the 'Enter' key
3061.directive('ignoreEnter', function() {
3062	return function (scope, element, attrs) {
3063		element.bind("keydown keypress", function (event) {
3064			if (event.which === 13) {
3065				event.preventDefault();
3066			}
3067		});
3068	}
3069})
3070.directive('validateEditResourceField', function () {
3071	return {
3072		restrict: 'A',
3073		require: ['^form','ngModel'],
3074		link: function (scope, element, attrs, ctrls) {
3075			var ngForm = ctrls[0];
3076			var ngModel = ctrls[1];
3077			// Initialization - create the data path based on ng-model path.
3078			var dataPath = attrs.ngModel.replace("software.", "").replace("$parent.$index", attrs.parentIndex).replace("$index", attrs.index).replace("[", ".").replace("]", "").split(".");
3079			// Remove errors once values have been edited.
3080			ngModel.$parsers.push(function(value) {
3081				_.set(scope.registrationErrorPayload, dataPath, null);
3082				ngModel.errorMessages = [];
3083				ngModel.$setValidity('server_validation_passed', true);
3084				return value;
3085			});
3086			ngModel.$viewChangeListeners.push(function() {
3087				// Prevent non-mandatory fields from becoming mandatory,
3088				// by assigning empty strings to the keys in the object to be
3089				// sent to the server.
3090				var value = _.get(scope.software, dataPath);
3091				if (value === "") {
3092					_.unset(scope.software, dataPath);
3093				}
3094			});
3095			// Observe for changes in the server-side validation
3096			// of a resource appeared - i.e. user pressed validate in resource edit
3097			scope.$watch('registrationErrorPayload', function(date) {
3098				// Find the error and set the input validity based on the results.
3099				var error = _.result(scope.registrationErrorPayload, dataPath);
3100				if (_.isEmpty(error) == false) {
3101					// Error exist - server validation failed.
3102					ngModel.$setValidity('server_validation_passed', false);
3103					ngModel.errorMessages = error;
3104				}
3105				else {
3106					// Error does not exist - server validation passed.
3107					ngModel.$setValidity('server_validation_passed', true);
3108					ngModel.errorMessages = [];
3109				}
3110				// Find the error and set the form validity based on the results.
3111				var formError = extractFormErrors();
3112				// There are errors in the form.
3113				if (formError.length > 0) {
3114					ngForm.$setValidity('server_validation_passed', false);
3115					ngForm.errorMessages = _.union(ngForm.errorMessages, formError);
3116				}
3117				else {
3118					ngForm.$setValidity('server_validation_passed', true);
3119					ngForm.errorMessages = [];
3120				}
3121			});
3122			var extractFormErrors = function() {
3123				var formErrors = [];
3124				for (var i = 1; i < dataPath.length; i++) {
3125					var subDataPath = dataPath.slice(0,i);
3126					subDataPath.push("general_errors");
3127					var error = _.result(scope.registrationErrorPayload, subDataPath);
3128					if (_.isEmpty(error) == false) {
3129						formErrors = formErrors.concat(error);
3130					}
3131				}
3132				return formErrors;
3133			}
3134		}
3135	}
3136})
3137.directive("dividerLabel", function(){
3138	return {
3139		restrict: 'A',
3140		transclude: true,
3141		template: function(elem, attr){
3142			var callout = 'class="tool-page-diver-label"';
3143			return callout;
3144		},
3145		replace: true
3146	};
3147})
3148// creates an animated small spinner
3149.directive('smallSpinner', function() {
3150	return {
3151		restrict: 'A',
3152		replace: 'true',
3153		template: '<div style="text-align:center; color: #7f7f7f; font-size: 12px;"><i class="fa fa-circle-o-notch fa-spin"></i> Processing...</div>'
3154	}
3155})
3156.directive('largeSpinner', function() {
3157	return {
3158		restrict: 'A',
3159		replace: 'true',
3160		template: '<div style="text-align:center; color: #cccccc; font-size: 60px;"><i class="fa fa-circle-o-notch fa-spin"></i></div>'
3161	}
3162});
3163
3164
3165
3166
3167'use strict';
3168
3169/* Filters */
3170
3171angular.module('elixir_front.filters', []).
3172// necessary for paging
3173filter('startFrom', function() {
3174	return function(input, start) {
3175			start = +start; //parse to int
3176			if (input) {
3177				return input.slice(start);
3178			} else {
3179				return;
3180			}
3181	}
3182}).
3183// filtering of a list of object by an object - used in filters in query interface
3184filter('filterByObject', function($filter) {
3185	return function (list, filter) {
3186		// recursively go through list entry and filter objects to find a match
3187		function filterMatch(entry, filter) {
3188			if (typeof filter === 'undefined') {
3189				return true;
3190			} else if (entry == null) {
3191				return false;
3192			}
3193			var result = false;
3194			for (var _key in filter) {
3195				result = false;
3196				// return true if filter empty
3197				if (typeof filter[_key] === 'undefined' || filter[_key] == '') {
3198					result = true;
3199				// skip if entry does not have the key from the filter
3200				} else if (typeof entry[_key] === 'undefined') {
3201					result = false;
3202				// if filter is not a deeper object nor an array just perform normal comparison
3203				} else if (typeof filter[_key] !== 'object') {
3204					// if entry is not a deeper object nor an array just perform normal comparison
3205					if (typeof entry[_key] !== 'object') {
3206						result = entry[_key].toLowerCase().indexOf(filter[_key].toLowerCase()) > -1
3207					// if entry is a deeper object perform deeper checking
3208					} else {
3209						// if entry is an array
3210						if (Array.isArray(entry[_key])) {
3211							// need a boolean to store whether any of the former array elements matched the filter; need this otherwise result could be overwritten with latter elements, even if the former ones matched
3212							var result_inner = false;
3213							// go through the whole entry array and check each element against the filter
3214							for (var j = 0; j < entry[_key].length; j++) {
3215								// if entry is a string, just perform normal comparison
3216								if (typeof entry[_key][j] == "string") {
3217									result_inner = entry[_key][j].toLowerCase().indexOf(filter[_key].toLowerCase()) > -1
3218								// if entry is an ontology only compare the term and not the URI to the filter
3219								} else if ('term' in entry[_key][j]) {
3220									result_inner = entry[_key][j].term.toLowerCase().indexOf(filter[_key].toLowerCase()) > -1
3221								// if entry is a different object then search for filter within it
3222								} else {
3223									// need to go through all properties of an entry to see if any matches are found
3224									for (var property in entry[_key][j]) {
3225										if(entry[_key][j].hasOwnProperty(property)) {
3226											if (JSON.stringify(entry[_key][j][property]).toLowerCase().indexOf(filter[_key].toLowerCase()) > -1) {
3227												result_inner = true;
3228												break;
3229											}
3230										}
3231									}
3232								}
3233								// if any of array elements matches, set result to true; this way even if the former elements match, the unmatching latter ones will not overwrite result
3234								if (result_inner) {
3235									result = true;
3236									break;
3237								}
3238							}
3239						// if entry is not an array
3240						} else {
3241							// if entry is an ontology only compare the term and not the URI to the filter
3242							if ('term' in entry[_key]) {
3243								result = entry[_key].term.toLowerCase().indexOf(filter[_key].toLowerCase()) > -1
3244							// if entry is a different object then search for filter within it
3245							} else {
3246								// need to go through all properties of an entry to see if any matches are found
3247								for (var property in entry[_key]) {
3248									if(entry[_key].hasOwnProperty(property)) {
3249										if (JSON.stringify(entry[_key][property]).toLowerCase().indexOf(filter[_key].toLowerCase()) > -1) {
3250											result = true;
3251											break;
3252										}
3253									}
3254								}
3255							}
3256						}
3257					}
3258				// if entry is an array and not an object
3259				} else if (Array.isArray(entry[_key])) {
3260					// go through the array and run the filter recursively
3261					for (var j = 0; j < entry[_key].length; j++) {
3262						if (filterMatch(entry[_key][j], filter[_key])) {
3263							result = true;
3264							break;
3265						}
3266					}
3267				} else {
3268					// if filter is a deeper object, call the same function recursively
3269					result = filterMatch(entry[_key], filter[_key]);
3270				}
3271				// if one of the filters didn't match return false
3272				if (!result) {
3273					return false;		
3274				}
3275			}
3276			// return true only if all the elements matched
3277			return true;
3278		}
3279		
3280		// list of matching elements
3281		var return_list = [];
3282		if (list) {
3283			// go through all elements
3284			for (var i=0; i < list.length; i++) {
3285				// add element to list if filter matches
3286				if ( filterMatch(list[i], filter) == true ) {
3287					return_list.push(list[i]);   
3288				}
3289			}
3290		}
3291		return return_list;
3292	};
3293}).
3294// query fuzzy matching which produces the relevance score
3295filter('filterQuery', function() {
3296	return function (list, filter) {
3297		// if filter empty return all resources
3298		if (filter == "" || filter == null) {
3299			return list;
3300		}
3301
3302		// list of matching elements
3303		var return_list = [];
3304		if (list && list.length > 0) {
3305			// go through all elements
3306			for (var i=0; i < list.length; i++) {
3307				if (!_.isEmpty(list[i])){
3308					// calculate the fuzzy score of some of the attributes of each resource vs the filter
3309					var search_score = (list[i].name + " " + list[i].description + " " + list[i].topic + " " + list[i].function + " " + list[i].affiliation).score(filter);
3310					// if there is a non-zero match
3311					if ( search_score > 0 ) {
3312						// invert the score for sorting purposes
3313						list[i].search_score = 1.0 - search_score;
3314						return_list.push(list[i]);   
3315					}
3316				}
3317			}
3318		}
3319		return return_list;
3320	}
3321}).
3322// filter that highlights matches
3323filter('highlight', function($sce) {
3324	return function(str, termsToHighlight) {
3325		if (str && termsToHighlight.length > 0) {
3326			// Sort terms by length
3327			termsToHighlight.sort(function(a, b) {
3328				return b.length - a.length;
3329			});
3330			// Regex to simultaneously replace terms
3331			var regex = new RegExp('(' + termsToHighlight.join('|') + ')', 'gi');
3332			return $sce.trustAsHtml(str.replace(regex, '<span class="highlightedTerm">$&</span>'));
3333		} else {
3334			return str;
3335		}
3336	};
3337});
3338// Controllers
3339angular.module('elixir_front.controllers').controller('LoginMangerController', ['$scope', '$uibModal', '$rootScope', '$state', function($scope, $uibModal, $rootScope, $state) {
3340	var vm = this;
3341
3342	vm.openLoginModal = function() {
3343		$uibModal.open({
3344			templateUrl: 'partials/modals/loginModal.html',
3345			controller: ['$uibModalInstance', 'djangoAuth', '$rootScope', '$state', 'OrcidAuth', LoginModalController],
3346			controllerAs: 'vm',
3347			windowClass: 'login-modal-window'
3348		});
3349	};
3350}]);
3351
3352function LoginModalController($uibModalInstance, djangoAuth, $rootScope, $state, OrcidAuth) {
3353	var vm = this;
3354	vm.error = {};
3355
3356	vm.loginButtonPressed = function(username, password) {
3357		vm.error.username = "";
3358		vm.error.password = "";
3359		vm.error.general = [];
3360		if (!username && !password) {
3361			vm.error.username = "Username is required";
3362			vm.error.password = "Password is required";
3363			vm.error.general = ["Username and password are required"];
3364		}
3365		else if (!username) {
3366			vm.error.username = "Username is required";
3367			vm.error.general = ["Username is required"];
3368		}
3369		else if (!password) {
3370			vm.error.password = "Password is required";
3371			vm.error.general = ["Password is required"];
3372		}
3373		else {
3374			djangoAuth.login(username, password).then(function (response) {
3375			// go to states set before redirection to login
3376			if(typeof $rootScope.toState == 'undefined' || /signup/.test($rootScope.toState.name) || /reset-password/.test($rootScope.toState.name)) {
3377				$state.go('search');
3378			} else {
3379				$state.go($rootScope.toState.name, $rootScope.toStateParams);
3380			}
3381			$uibModalInstance.close();
3382		}, function (response) {
3383			vm.error.general = response.general_errors;
3384		});
3385		}
3386	}
3387
3388	vm.registerButtonPressed = function() {
3389		$uibModalInstance.close();
3390		$state.go('signup');
3391	}
3392
3393	vm.forgotButtonPressed = function() {
3394		$uibModalInstance.close();
3395		$state.go('reset-password');
3396	}
3397
3398	vm.orcidLoginPressed = function() {
3399		OrcidAuth.start();
3400	}
3401}
3402
3403(function() {
3404  'use strict';
3405
3406  angular.module('elixir_front')
3407  .service('OrcidAuth', ['AppConfig', '$window', '$rootScope', function(AppConfig, $window, $rootScope) {
3408
3409    function randomState() {
3410      try {
3411        var arr = new Uint8Array(16);
3412        ($window.crypto || $window.msCrypto).getRandomValues(arr);
3413        return Array.prototype.map.call(arr, function(b){ return ('00' + b.toString(16)).slice(-2); }).join('');
3414      } catch (e) {
3415        return Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
3416      }
3417    }
3418
3419    function saveState(stateToken) {
3420      // Persist state + intended return route
3421      sessionStorage.setItem('orcid_oauth_state', stateToken);
3422
3423      var payload = null;
3424      if ($rootScope.toState && $rootScope.toState.name &&
3425          !/signup/.test($rootScope.toState.name) &&
3426          !/reset-password/.test($rootScope.toState.name)) {
3427        payload = { name: $rootScope.toState.name, params: $rootScope.toStateParams || {} };
3428      }
3429      sessionStorage.setItem('orcid_oauth_state_payload', JSON.stringify(payload));
3430    }
3431
3432    function buildAuthorizeUrl(stateToken) {
3433      var base = AppConfig.ORCID_BASE_URL || 'https://orcid.org';
3434      var params = new URLSearchParams({
3435        client_id: AppConfig.ORCID_CLIENT_ID || '',
3436        response_type: 'code',
3437        scope: '/authenticate',
3438        redirect_uri: AppConfig.ORCID_REDIRECT_URI || '',
3439        state: stateToken
3440      });
3441      return base.replace(/\/+$/,'') + '/oauth/authorize?' + params.toString();
3442    }
3443
3444    this.start = function() {
3445      var state = randomState();
3446      saveState(state);
3447      var url = buildAuthorizeUrl(state);
3448      $window.location.replace(url);
3449    };
3450
3451    this.consumeState = function(received) {
3452      var expected = sessionStorage.getItem('orcid_oauth_state');
3453      var payload = sessionStorage.getItem('orcid_oauth_state_payload');
3454
3455      // cleanup
3456      sessionStorage.removeItem('orcid_oauth_state');
3457      sessionStorage.removeItem('orcid_oauth_state_payload');
3458
3459      return {
3460        ok: !!received && received === expected,
3461        payload: payload ? JSON.parse(payload) : null
3462      };
3463    };
3464  }]);
3465})();
3466// Controllers
3467angular.module('elixir_front.controllers').controller('PermissionsController', ['$scope', 'UserSuggestionsProvider', function($scope, UserSuggestionsProvider) {
3468	var vm = this;
3469
3470	// Typeahead Support
3471	$scope.userSuggestions = function(prefix) {
3472		return UserSuggestionsProvider.getSuggestions(prefix).then(function(data) {
3473			var suggestions = _.map(data, function(obj){
3474				return obj.username;
3475			});
3476			return _.difference(suggestions, $scope.software.editPermission.authors);
3477		});
3478	};
3479
3480	$scope.userSelected = function($item, $model, $label) {
3481		// Initialize authors if not present.
3482		if ($scope.software.editPermission.authors == undefined) {
3483			$scope.software.editPermission.authors = [];
3484		}
3485		// Clear the input field on selection.
3486		$scope.userSuggestion = '';
3487		$scope.software.editPermission.authors.push($model);
3488	};
3489
3490	$scope.isSoftwareOwner = function() {
3491		if ($scope.software.owner == $scope.User.getUsername()) {
3492			return true;
3493		}
3494		return false;
3495	}
3496
3497	$scope.deleteUser = function(index) {
3498		$scope.software.editPermission.authors.splice(index, 1);
3499	}
3500}]);
3501
3502// Services and Factories
3503var elixir_front = angular.module('elixir_front');
3504elixir_front.factory('UserSuggestionsProvider', ['$http', function ($http) {
3505	return {
3506		getSuggestions: function(prefix) {
3507			return $http({
3508				method: 'GET',
3509				url: '/api/user-list',
3510				params: {term: prefix}
3511			}).then(function successCallback(response) {
3512				return response.data;
3513			}, function errorCallback(response) {
3514				return {};
3515			})
3516		}
3517	};
3518}]);
3519
3520// Controllers
3521angular.module('elixir_front.controllers').controller('RequestsController', ['$scope', 'ResourceRequestProvider', 'User', function($scope, ResourceRequestProvider, User) {
3522	var vm = this;
3523	$scope.requests = {};
3524	$scope.received = [];
3525	$scope.sent = [];
3526	$scope.loading = true;
3527	$scope.error = {}
3528	$scope.error.status = 0;
3529	$scope.error.message = "";
3530	$scope.filters = {};
3531	$scope.filters.showCompletedReceived = false;
3532	$scope.filters.showCompletedSent = false;
3533
3534	// Data feteching
3535	vm.loadRequests = function($item, $model, $label) {
3536		$scope.loading = true;
3537		ResourceRequestProvider.getRequests().then(function successCallback(response) {
3538			$scope.requests = response.data.requests;
3539			$scope.filterSentRequests();
3540			$scope.filterReceivedRequests();
3541			$scope.loading = false;
3542		}, function errorCallback(response) {
3543			$scope.error.status = response.status;
3544			$scope.error.message = "There was an error communicating with the server. Try refreshing this website.";
3545			$scope.loading = false;
3546		});
3547	};	
3548
3549	$scope.filterSentRequests = function() {
3550		if ($scope.filters.showCompletedSent == false) {
3551			$scope.sent = $scope.requests.sent.filter(vm.checkIfNotCompleted);
3552		}
3553		else {
3554			$scope.sent = $scope.requests.sent;
3555		}
3556	}
3557
3558	$scope.filterReceivedRequests = function() {
3559		if ($scope.filters.showCompletedReceived == false) {
3560			$scope.received = $scope.requests.received.filter(vm.checkIfNotCompleted);
3561		}
3562		else {
3563			$scope.received = $scope.requests.received;
3564		}
3565	}
3566
3567	//Operations
3568	vm.checkIfNotCompleted = function(request) {
3569		return !request.completed;
3570	}
3571
3572	vm.requestsAffectedByAcceptingRequest = function(request) {
3573		// Accept ownership for not own request.
3574		if (request.type == "ownership" && request.username != User.current.username) {
3575			var affectedRequests = [];
3576			for (var index = 0; index < $scope.requests.received.length; ++index) {
3577				var checkRequest = $scope.requests.received[index];
3578				if (checkRequest.completed == false && checkRequest.requestId != request.requestId && checkRequest.resourceId == request.resourceId) {
3579					affectedRequests.push(checkRequest);
3580				}
3581			}
3582			return affectedRequests;
3583		}
3584		return []; 
3585	}
3586
3587	$scope.typeDescription = function(type) {
3588		if (type == "editing") {
3589			return "Editing rights";
3590		}
3591		return "Resource ownership";
3592	}
3593
3594	$scope.acceptRequest = function(request) {
3595		var affectedRequests = vm.requestsAffectedByAcceptingRequest(request);
3596		var resourceIndex = $scope.requests.received.indexOf(request);
3597		if (affectedRequests.length == 0) {
3598			vm.processAcceptRequest(resourceIndex, request, affectedRequests);
3599		}
3600		else {
3601			if (confirm("Are you sure you want to accept this request? " + affectedRequests.length + " of the exisitng requests will be sent to the new owner.")) {
3602				vm.processAcceptRequest(resourceIndex, request, affectedRequests);
3603			}
3604		}
3605	}	
3606
3607	vm.processAcceptRequest = function(index, request, affectedRequests) {
3608		var resourceIndex = index;
3609		$scope.requests.received[resourceIndex].processing = true
3610		request.processing = false
3611		ResourceRequestProvider.acceptRequest(request.requestId).then(function successCallback(response) {
3612			$scope.requests.received[resourceIndex].processing = false
3613			$scope.requests.received[resourceIndex].completed = true
3614			$scope.requests.received[resourceIndex].accepted = true
3615			$scope.requests.received = _.difference($scope.requests.received, affectedRequests);
3616			$scope.received = _.difference($scope.received, affectedRequests); 
3617			request.processing = false
3618			request.completed = true
3619			request.accepted = true
3620			User.current.requests_count = User.current.requests_count - 1 - affectedRequests.length;
3621		}, function errorCallback(response) {
3622			vm.loadRequests();
3623		});
3624	}
3625
3626	$scope.declineRequest = function(request) {
3627		var resourceIndex = $scope.requests.received.indexOf(request);
3628		request.processing = false
3629		$scope.requests.received[resourceIndex].processing = true
3630		ResourceRequestProvider.declineRequest(request.requestId).then(function successCallback(response) {
3631			$scope.requests.received[resourceIndex].processing = false
3632			$scope.requests.received[resourceIndex].completed = true
3633			$scope.requests.received[resourceIndex].accepted = false
3634			request.processing = false
3635			request.completed = true
3636			request.accepted = false
3637			User.current.requests_count = User.current.requests_count - 1;
3638		}, function errorCallback(response) {
3639			vm.loadRequests();
3640		});
3641	}
3642
3643	// Initialization
3644	vm.loadRequests();
3645}]);
3646
3647// Services and Factories
3648var elixir_front = angular.module('elixir_front');
3649elixir_front.factory('ResourceRequestProvider', ['$http', function ($http) {
3650	return {
3651		getRequests: function() {
3652			return $http({
3653				method: 'GET',
3654				url: '/api/request',
3655				params: {}
3656			})
3657		},
3658		acceptRequest: function(requestId) {
3659			return $http({
3660				method: 'POST',
3661				url: '/api/request/conclude',
3662				data: {'requestId': requestId, accept: true}
3663			})
3664		},
3665		declineRequest: function(requestId) {
3666			return $http({
3667				method: 'POST',
3668				url: '/api/request/conclude',
3669				data: {'requestId': requestId, accept: false}
3670			})
3671		},
3672		requestOwnership: function(resourceId) {
3673			return $http({
3674				method: 'PUT',
3675				url: '/api/request',
3676				data: {'resourceId': resourceId, type: 'ownership'}
3677			})
3678		},
3679		requestEditingRights: function(resourceId) {
3680			return $http({
3681				method: 'PUT',
3682				url: '/api/request',
3683				data: {'resourceId': resourceId, type: 'editing'}
3684			})
3685		}
3686	};
3687}]);
3688'use strict';
3689
3690/* Services */
3691
3692angular.module('elixir_front.services', [])
3693.service('Alert', function(){
3694	this.list = [];
3695	this.add = function(_message, _type) {
3696		this.list.push({msg: _message, type: _type, ttl:2});
3697	};
3698	this.remove = function(_index) {
3699		this.list.splice(_index, 1);
3700	}
3701})
3702.service('User', [function(){
3703	this.current = {};
3704	return {
3705		isLoggedIn: function () {
3706			return this.authenticated == true;                   
3707		},
3708		getUsername: function () {
3709			if (!_.isEmpty(this.current)) {
3710				return this.current.username;
3711			}
3712		},
3713		isSuperuser: function () {
3714			if (!_.isEmpty(this.current)) {
3715				return this.current.is_superuser;
3716			}
3717		},
3718
3719	}
3720}])
3721.service('Query', [function(){
3722	this.current = [];
3723}])
3724.service('DomainConnection', ['$resource', function($resource){
3725	return $resource('/api/d/', null, {
3726		'query': {
3727			isArray: true,
3728			method:'GET'
3729		},
3730		'create': {
3731			method:'POST'
3732		}
3733	})
3734}])
3735.service('DomainDetailConnection', ['$resource', function($resource){
3736	return $resource('/api/d/:domain', {'domain': '@domain'}, {
3737		'query': {
3738			isArray:false,
3739			method:'GET'
3740		},
3741		'update': {
3742			method:'PUT'
3743		},
3744		'delete': {
3745			method:'DELETE'
3746		}
3747	})
3748}])
3749.service('Domain', ['DomainDetailConnection', function(DomainDetailConnection){
3750	var _this = this;
3751	this.current = {};
3752	this.loaded = false;
3753	this.set = function(domain) {
3754		_this.current = domain;
3755	}
3756	this.load = function(domain) {
3757		if (typeof domain != 'undefined') {
3758			var response = DomainDetailConnection.query({'domain':domain}, function(response) {
3759				_this.set(response.data);
3760				_this.loaded = true;
3761			});
3762		} else {
3763			_this.loaded = false;
3764		}
3765	}
3766	this.isLoaded = function() {
3767		return _this.loaded;
3768	}
3769	this.hasSubdomain = function() {
3770		return !_.isEmpty(_this.current);
3771	}
3772	this.hasTitle = function() {
3773		return _this.current.title !== undefined;
3774	}
3775	this.hasSubTitle = function() {
3776		return _this.current.sub_title !== undefined;
3777	}
3778	this.hasDescription = function() {
3779		return _this.current.description !== undefined;
3780	}
3781}])
3782.service('Highlighting', [function(){
3783	var _this = this;
3784	this.terms = [];
3785	this.purge = function() {
3786		while (_this.terms.length > 0) {
3787			_this.terms.pop();
3788		}
3789	}
3790	this.set = function(query_array) {
3791		_this.purge();
3792		for (var i in query_array) {
3793			_this.terms.push(query_array[i].text.replace(/['"]/g,""));
3794		}
3795	}
3796}])
3797.service('ToolListConnection', ['$resource', function($resource){
3798	return $resource('/api/t/', null, {
3799		'query': {
3800			isArray:false,
3801			method:'GET'
3802		},
3803		'update': {
3804			method:'PUT'
3805		}
3806	})
3807}])
3808.service('ToolListOverviewConnection', ['$resource', function($resource){
3809	return $resource('/api/tool-list/', null, {
3810		'query': {
3811			isArray:true,
3812			method:'GET'
3813		}
3814	})
3815}])
3816.service('ToolList', ['$stateParams', 'ToolListConnection', 'ToolPaginator', function($stateParams, ToolListConnection, ToolPaginator){
3817	var _this = this;
3818	this.count = 0;
3819	this.list = [];
3820	this.loading = true;
3821
3822	this.refresh = function() {
3823		// enable spinner
3824		this.loading = true;
3825
3826		// prepare API parameters including per_page
3827		var apiParams = angular.copy($stateParams);
3828		if (ToolPaginator.pageSize && ToolPaginator.pageSize !== 50) {
3829			apiParams.per_page = ToolPaginator.pageSize;
3830		}
3831
3832		// get tools
3833		var response = ToolListConnection.query(apiParams, function() {
3834			// disable spinner
3835			_this.loading = false;
3836			_this.count = response.count;
3837			_this.list = response.list;
3838		});
3839	}
3840}])
3841.factory('MetaService', function($document, $window) {
3842    function setAlternate(type, href, title) {
3843        if (!href) return;
3844        // Remove any existing for the same type
3845        var selector = 'link[rel="alternate"][type="' + type + '"]';
3846        var existing = $document[0].head.querySelectorAll(selector);
3847        existing.forEach ? existing.forEach(function(n){ n.remove(); })
3848            : Array.prototype.forEach.call(existing, function(n){ n.parentNode.removeChild(n); });
3849        var link = $document[0].createElement('link');
3850        link.setAttribute('rel', 'alternate');
3851        link.setAttribute('type', type);
3852        link.setAttribute('href', href);
3853        if (title) link.setAttribute('title', title);
3854        $document[0].head.appendChild(link);
3855    }
3856    return {
3857        setAlternatesForTool: function(biotoolsID) {
3858            if (!biotoolsID) return;
3859            var base = $window.location.origin;
3860            setAlternate('application/json', base + '/api/' + biotoolsID + '?format=json', 'JSON');
3861            setAlternate('application/xml', base + '/api/' + biotoolsID + '?format=xml', 'XML');
3862        }
3863    };
3864})
3865.factory('ToolTableDataSource', function() {
3866	var columnDescriptionForKey = function(key, hidden) {
3867		var widthCharMultiplier = 12;
3868		var columnDescription = {};
3869		if (key == 'Name') {
3870			columnDescription = {field: 'name', pinnedLeft: true, displayName: 'Name', cellTemplate: '/partials/grid_cells/nameCell.html', width: '150', resizable: true};
3871		}
3872		else if (key == 'Operating System') {
3873			columnDescription = {field: 'operatingSystem', displayName: 'Operating system', cellTemplate: '/partials/grid_cells/platformCell.html', width: '100', resizable: true};
3874		}
3875		else if (key == 'Description') {
3876			columnDescription = {field: 'description', displayName: 'Description', width: '*', resizable: true, cellTemplate: '/partials/grid_cells/defaultCell.html'};
3877		}
3878		else if (key == 'Operation') {
3879			columnDescription = {field: 'function', displayName: 'Operation', width: '220', resizable: true, cellTemplate: '/partials/grid_cells/functionCell.html'};
3880		}
3881		else if (key == 'Input') {
3882			columnDescription = {field: 'function', displayName: 'Input', width: '240', resizable: true, cellTemplate: '/partials/grid_cells/inputCell.html'};
3883		}
3884		else if (key == 'Output') {
3885			columnDescription = {field: 'function', displayName: 'Output', width: '240', resizable: true, cellTemplate: '/partials/grid_cells/outputCell.html'};
3886		}
3887		else if (key == 'Topic') {
3888			columnDescription = {field: 'topic', displayName: 'Topic', width: '200', resizable: true, cellTemplate: '/partials/grid_cells/topicCell.html'};
3889		}
3890		else if (key == 'Homepage') {
3891			columnDescription = {field: 'homepage', displayName: 'Homepage', resizable: true, width: '100', cellTemplate: '/partials/grid_cells/urlCell.html'};
3892		}
3893		else if (key == 'Tool Type') {
3894			columnDescription = {field: 'toolType', displayName: 'Tool type', width: '150', resizable: true, cellTemplate: '/partials/grid_cells/listCell.html'};
3895		}
3896		else if (key == 'Publications') {
3897			columnDescription = {field: 'publication', displayName: 'Publications', width: '250', resizable: true, cellTemplate: '/partials/grid_cells/publicationsCell.html'};
3898		}
3899		else if (key == 'Collection') {
3900			columnDescription = {field: 'collectionID', displayName: 'Collection', width: '150', resizable: true, cellTemplate: '/partials/grid_cells/listCell.html'};
3901		}
3902		else if (key == 'bio.tools ID') {
3903			columnDescription = {field: 'biotoolsID', displayName: 'bio.tools ID', width: '120', resizable: true, cellTemplate: '/partials/grid_cells/defaultCell.html'};
3904		}
3905		else if (key == 'Documentation') {
3906			columnDescription = {field: 'documentation', displayName: 'Documentation', width: '160', resizable: true, cellTemplate: '/partials/grid_cells/linkListCell.html'};
3907		}
3908		else if (key == 'Link') {
3909			columnDescription = {field: 'link', displayName: 'Links', width: '160', resizable: true, cellTemplate: '/partials/grid_cells/linkListCell.html'};
3910		}
3911		else if (key == 'Download') {
3912			columnDescription = {field: 'download', displayName: 'Downloads', width: '160', resizable: true, cellTemplate: '/partials/grid_cells/linkListCell.html'};
3913		}
3914		else if (key == 'Language') {
3915			columnDescription = {field: 'language', displayName: 'Language', width: '120', resizable: true, cellTemplate: '/partials/grid_cells/listCell.html'};
3916		}
3917		else if (key == 'Credits') {
3918			columnDescription = {field: 'credit', displayName: 'Credits & Support', width: '150', resizable: true, cellTemplate: '/partials/grid_cells/creditsCell.html'};
3919		}
3920		else if (key == 'Accessibility') {
3921			columnDescription = {field: key.toLowerCase(), displayName: key, width: '150', resizable: true, cellTemplate: '/partials/grid_cells/defaultCell.html'};
3922		}
3923		else if (key == 'Cost') {
3924			columnDescription = {field: key.toLowerCase(), displayName: key, width: '90', resizable: true, cellTemplate: '/partials/grid_cells/defaultCell.html'};
3925		}
3926		else if (key == 'Version') {
3927			columnDescription = {field: key.toLowerCase(), displayName: key, width: '90', resizable: true, cellTemplate: '/partials/grid_cells/listCell.html'};
3928		}
3929		else if (key == 'Added') {
3930			columnDescription = {field: 'additionDate', displayName: key, width: '100', resizable: true, cellTemplate: '/partials/grid_cells/dateCell.html'};
3931		}
3932		else if (key == 'Updated') {
3933			columnDescription = {field: 'lastUpdate', displayName: key, width: '100', resizable: true, cellTemplate: '/partials/grid_cells/dateCell.html'};
3934		}
3935		else {
3936			columnDescription = {field: key.toLowerCase(), displayName: key, width: key.length * widthCharMultiplier, resizable: true, cellTemplate: '/partials/grid_cells/defaultCell.html'};
3937		}
3938		columnDescription.enableHiding = true;
3939		columnDescription.visible = hidden;
3940		columnDescription["minWidth"] = 120;
3941		return columnDescription;
3942	}
3943
3944	return {
3945		columnsDescription : function(visibleColumns) {
3946			return [
3947			columnDescriptionForKey('Name', visibleColumns.indexOf('Name') != -1),
3948			columnDescriptionForKey('Description', visibleColumns.indexOf('Description') != -1),
3949			columnDescriptionForKey('Homepage', visibleColumns.indexOf('Homepage') != -1),
3950			columnDescriptionForKey('Version', visibleColumns.indexOf('Homepage') != -1),
3951			columnDescriptionForKey('bio.tools ID', visibleColumns.indexOf('bio.tools ID') != -1),
3952			columnDescriptionForKey('Tool Type', visibleColumns.indexOf('Tool Type') != -1),
3953			columnDescriptionForKey('Topic', visibleColumns.indexOf('Topic') != -1),
3954			columnDescriptionForKey('Publications', visibleColumns.indexOf('Publications') != -1),
3955			columnDescriptionForKey('Credits', visibleColumns.indexOf('Credits') != -1),
3956			columnDescriptionForKey('Operation', visibleColumns.indexOf('Operation') != -1),
3957			columnDescriptionForKey('Input', visibleColumns.indexOf('Input') != -1),
3958			columnDescriptionForKey('Output', visibleColumns.indexOf('Output') != -1),
3959			columnDescriptionForKey('Documentation', visibleColumns.indexOf('Documentation') != -1),
3960			columnDescriptionForKey('Operating System', visibleColumns.indexOf('Operating System') != -1),
3961			columnDescriptionForKey('Language', visibleColumns.indexOf('Language') != -1),
3962			columnDescriptionForKey('License', visibleColumns.indexOf('License') != -1),
3963			columnDescriptionForKey('Link', visibleColumns.indexOf('Link') != -1),
3964			columnDescriptionForKey('Accessibility', visibleColumns.indexOf('Accessibility') != -1),
3965			columnDescriptionForKey('Download', visibleColumns.indexOf('Download') != -1),
3966			columnDescriptionForKey('Collection', visibleColumns.indexOf('Collection') != -1),
3967			columnDescriptionForKey('Cost', visibleColumns.indexOf('Cost') != -1),
3968			columnDescriptionForKey('Maturity', visibleColumns.indexOf('Maturity') != -1),
3969			columnDescriptionForKey('Added', visibleColumns.indexOf('Added') != -1),
3970			columnDescriptionForKey('Updated', visibleColumns.indexOf('Updated') != -1)
3971			];
3972		}
3973	}
3974})
3975.service('ToolPaginator', function(){
3976	this.currentPage = 1;
3977	// pagination settings
3978	this.maxSize = 5;
3979	this.pageSize = 50;
3980	this.availablePageSizes = [10, 25, 50, 100]; // New option
3981})
3982.service('ToolSorter', function(){
3983	var _this = this;
3984	this.order = true;
3985	this.list = [
3986	{"attrName": "lastUpdate", "text": "Updated"},
3987	{"attrName": "additionDate", "text": "Added"},
3988	{"attrName": "name", "text": "Name"},
3989	{"attrName": "citationCount", "text": "Citation Count"},
3990	{"attrName": "citationDate", "text": "Publication Date"}
3991	];
3992	this.sortBy = this.list[0];
3993
3994	var scoreEntry = {"attrName": "score", "text": "Score"};
3995
3996	this.setSortOption = function(parameter) {
3997		if (parameter == undefined) {
3998			this.sortBy = this.list[0];
3999		}
4000		else {
4001			for (var i = 0; i < this.list.length; i++) {
4002				if (this.list[i]['attrName'] == parameter) {
4003					this.sortBy = this.list[i];
4004				}
4005			}
4006		}
4007	} 
4008
4009	// add 'Score' to the list of sortable attributes
4010	this.addScore = function () {
4011		if (!_.isEqual(scoreEntry, _this.list[0])) {
4012			_this.list.unshift(scoreEntry);
4013			_this.sortBy = _this.list[0];
4014		}
4015	}
4016
4017	// remove 'Score' from the list of sortable attributes
4018	this.removeScore = function () {
4019		if (_.isEqual(scoreEntry, _this.list[0])) {
4020			_this.list.splice(0,1);
4021			_this.sortBy = _this.list[0];
4022		}
4023	}
4024})
4025.service('DisplayModeSelector', function(){
4026	this.list = [
4027	{"attrName": "cards", "text": "Cards"},
4028	{"attrName": "grid", "text": "Table"}
4029	];
4030	this.mode = this.list[0];
4031})
4032.factory('Tool', ['$resource', function($resource){
4033	return $resource('/api/t/:id', null, {
4034		'query': {
4035			isArray:false,
4036			method:'GET'
4037		},
4038		'update': {
4039			method:'PUT'
4040		}
4041	})
4042}])
4043.factory('DisownToolService', ['$resource', function($resource){
4044	return $resource('/api/t/:id/disown', {'id': '@id'}, {
4045		'disown': {
4046			method:'POST'
4047		}
4048	})
4049}])
4050.factory('ToolCreateValidator', ['$resource', function($resource){
4051	return $resource('/api/t/:id/validate', null, {
4052		'update': {
4053			method:'PUT'
4054		}
4055	})
4056}])
4057.factory('ToolUpdateValidator', ['$resource', function($resource){
4058	return $resource('/api/t/:id/validate', null, {
4059		'update': {
4060			method:'PUT'
4061		}
4062	})
4063}])
4064.factory('Ontology', ['$resource', function($resource){
4065	return $resource('/api/o/:name', null, {})
4066}])
4067.factory('UsedTerms', ['$resource', function($resource){
4068	return $resource('/api/used-terms/:usedTermName', null, {
4069		'query': {
4070			isArray:true,
4071			method:'GET'
4072		},
4073	})
4074}])
4075.service('Attribute', function() {
4076	this.description = {
4077		everything: {
4078			label: "Everything"
4079		},
4080		name: {
4081			description: "Tool name.",
4082			label: "Name"
4083		},
4084		biotoolsID: {
4085			description: "Unique, persistent bio.tools identifier.",
4086			label: "biotoolsID"
4087		},
4088		toolType: {
4089			description: "A type of application software: a tool can have more than one type.",
4090			label: "Tool type"
4091		},
4092		version: {
4093			description: "Tool version information. Use 'Add version' to enter individual version numbers (if practical).",
4094			label: "Current version"
4095		},
4096		description: {
4097			description: "Tool description, e.g. a few sentences adapted from the tool publication abstract or homepage.",
4098			label: "Description"
4099		},
4100		operation: {
4101			description: "A basic operation performed by this tool function.",
4102			label: "Operation"
4103		},
4104		functionNote: {
4105			description: "Concise comment about this function, if not apparent from the description and EDAM annotations.",
4106			label: "Function description"
4107		},
4108		functionName: {
4109			description: "Name of the function (EDAM term)",
4110			label: "Function Name"
4111		},
4112		input: {
4113			description: "Details of input data for this function.",
4114			label: "Input"
4115		},
4116		inputDataType: {
4117			description: "Type of input data.",
4118			label: "Data type"
4119		},
4120		inputDataFormat: {
4121			description: "Allowed format(s) of the input data.",
4122			label: "Data format"
4123		},
4124		outputDataType: {
4125			description: "Type of output data.",
4126			label: "Data type"
4127		},
4128		outputDataFormat: {
4129			description: "Allowed format(s) of the output data.",
4130			label: "Data format"
4131		},
4132		dataDescription: {
4133			description: "Free text description of the data.",
4134			label: "Data description"
4135		},
4136		output: {
4137			description: "Details of output data for this function.",
4138			label: "Output"
4139		},
4140		topic: {
4141			description: "A scientific domain that the tool serves, or another general category.",
4142			label: "Topic"
4143		},
4144		homepage: {
4145			description: "Homepage of the tool, or some URL that best serves this purpose.",
4146			label: "Homepage URL"
4147		},
4148		contact: {
4149			description: "Primary points of contact, e.g. helpdesk or an individual",
4150			label: "Contact"
4151		},
4152		contactName: {
4153			description: "Name of contact",
4154			label: "Name"
4155		},
4156		contactEmail: {
4157			description: "Email address of contact",
4158			label: "E-mail"
4159		},
4160		resourceType: {
4161			description: "Basic resource type: Tool, Database etc.",
4162			label: "Resource type"
4163		},
4164		interface: {
4165			description: "Resource interfaces: Web UI, Command line etc.",
4166			label: "Interface"
4167		},
4168		publications: {
4169			description: "Publications relevant to the resource (PMCID, PMID or DOI)",
4170			label: "Publications"
4171		},
4172		publicationsPrimaryID: {
4173			description: "PMCID, PMID or DOI of the publication",
4174			label: "ID of the primary publication"
4175		},
4176		publicationsOtherID: {
4177			description: "PMCID, PMID or DOI of other relevant publications",
4178			label: "ID of other publications"
4179		},
4180		docs: {
4181			description: "Links to the documentation",
4182			label: "Documentation"
4183		},
4184		docsHome: {
4185			description: "Main page of documentation",
4186			label: "Main page"
4187		},
4188		docsTermsOfUse: {
4189			description: "Link to the Terms Of Use",
4190			label: "Terms of Use"
4191		},
4192		docsDownload: {
4193			description: "Link to the download instructions",
4194			label: "Download"
4195		},
4196		docsCitationInstructions: {
4197			description: "Link to the citation instructions",
4198			label: "Citation instructions"
4199		},
4200		docsDownloadSource: {
4201			description: "Source code downloads page (URL)",
4202			label: "Download source"
4203		},
4204		docsDownloadBinaries: {
4205			description: "Software binaries downloads page (URL)",
4206			label: "Download binaries"
4207		},
4208		docsGithub: {
4209			description: "Github page (URL)",
4210			label: "Github page"
4211		},
4212		affiliation: {
4213			description: "Entry owner",
4214			label: "Affiliation"
4215		},
4216		mirror: {
4217			description: "Mirror homepage (URL)",
4218			label: "Mirror"
4219		},
4220		collectionID: {
4221			description: "A tag which assigns the tool to a collection in bio.tools.",
4222			label: "Collection"
4223		},
4224		sourceRegistry: {
4225			description: "Link to the registry (or other collection) from which the tool was imported (URL)",
4226			label: "Source registry"
4227		},
4228		canonicalID: {
4229			description: "Canonical Identifier (typically a URI) of the resource, if one is available",
4230			label: "Canonical ID"
4231		},
4232		cost: {
4233			description: "Monetary cost of acquiring the tool.",
4234			label: "Cost"
4235		},
4236		elixirInfo: {
4237			description: "Information specific to ELIXIR services",
4238			label: "ELIXIR info"
4239		},
4240		elixirStatus: {
4241			description: "ELIXIR Core Service",
4242			label: "ELIXIR status"
4243		},
4244		elixirNode: {
4245			description: "Name of one of countries participating in ELIXIR",
4246			label: "ELIXIR node"
4247		},
4248		maturity: {
4249			description: "Development stage of the software product.",
4250			label: "Maturity"
4251		},
4252		platform: {
4253			description: "Platforms supported by a downloadable software package",
4254			label: "Platform"
4255		},
4256		language: {
4257			description: "Name of a programming language, e.g. used for the tool source code.",
4258			label: "Language"
4259		},
4260		license: {
4261			description: "Software or data usage license. Use \"Unlicensed\", \"Proprietary\" (for commercial licenses) or \"Other\" in special cases. ",
4262			label: "License"
4263		},
4264		credit: {
4265			description: "Entities that should be credited",
4266			label: "Credit"
4267		},
4268		creditsDeveloper: {
4269			description: "Name of person that developed the resource",
4270			label: "Developer"
4271		},
4272		creditsContributor: {
4273			description: "Name of person contributing to the resource",
4274			label: "Contributor"
4275		},
4276		creditsInstitution: {
4277			description: "Name of the institution that developed or provide the resource",
4278			label: "Institution"
4279		},
4280		creditsInfrastructure: {
4281			description: "Research infrastructure in which the resource was developed or provided",
4282			label: "Infrastructure"
4283		},
4284		creditsFunding: {
4285			description: "Details of grant funding supporting the resource",
4286			label: "Funding"
4287		},
4288		uses: {
4289			description: "Other resources this resource uses, e.g. as a data source, or auxillary program",
4290			label: "Uses"
4291		},
4292		usesName: {
4293			description: "Name of a resource that this resource uses",
4294			label: "Name"
4295		},
4296		usesHomepage: {
4297			description: "Homepage of a resource that this resource uses",
4298			label: "Homepage"
4299		},
4300		usesVersion: {
4301			description: "Version number of a resource that this resources uses",
4302			label: "Version"
4303		},
4304		operatingSystem: {
4305			description: "The operating system supported by a downloadable software.",
4306			label: "Operating system"
4307		},
4308		accessibility: {
4309			description: "How freely available the software is for use.",
4310			label: "Accessibility"
4311		},
4312		cmd: {
4313			description: "Relevant command, command-line fragment or option for executing this function or running the tool in this mode.",
4314			label: "Command"
4315		},
4316		linkURL: {
4317			description: "A link of some relevance to the tool (URL).",
4318			label: "linkURL"
4319		},
4320		linkType: {
4321			description: "The type of data, information or system that is obtained when the link is resolved.",
4322			label: "linkType"
4323		},
4324		linkNote: {
4325			description: "Comment about the link.",
4326			label: "linkNote"
4327		},
4328		downloadURL: {
4329			description: "Link to download (or repository providing a download) for the tool.",
4330			label: "downloadURL"
4331		},
4332		downloadType: {
4333			description: "Type of downloadable resource.",
4334			label: "downloadType"
4335		},
4336		downloadNote: {
4337			description: "Comment about the download.",
4338			label: "downloadNote"
4339		},
4340		downloadVersion: {
4341			description: "Version information (typically a version number) of the software applicable to this download.",
4342			label: "linkNote"
4343		},
4344		documentationURL: {
4345			description: "Link to documentation for the tool (URL).",
4346			label: "documentationURL"
4347		},
4348		documentationType: {
4349			description: "Type of documentation that is linked to.",
4350			label: "documentationType"
4351		},
4352		documentationNote: {
4353			description: "Comment about the documentation.",
4354			label: "documentationNote"
4355		},
4356		publicationPMCID: {
4357			description: "PubMed Central Identifier (PMCID) of a publication about the tool.",
4358			label: "publicationPMCID"
4359		},
4360		publicationPMID: {
4361			description: "PubMed Identifier (PMID) of a publication about the tool.",
4362			label: "publicationPMID"
4363		},
4364		publicationDOI: {
4365			description: "Digital Object Identifier (DOI) of a publication about the tool.",
4366			label: "publicationDOI"
4367		},
4368		publicationType: {
4369			description: "Type of publication.",
4370			label: "publicationType"
4371		},
4372		publicationVersion: {
4373			description: "Version information (typically a version number) of the software applicable to this publication.",
4374			label: "publicationVersion"
4375		},
4376		publicationNote: {
4377			description: "Comment about the publication.",
4378			label: "publicationNote"
4379		},
4380		creditName: {
4381			description: "Name of the entity that is credited.",
4382			label: "creditName"
4383		},
4384		creditEmail: {
4385			description: "Email address of the entity that is credited.",
4386			label: "creditEmail"
4387		},
4388		creditURL: {
4389			description: "URL for the entity that is credited, e.g. homepage of an institute.",
4390			label: "creditURL"
4391		},
4392		creditOrcidID: {
4393			description: "Unique identifier (ORCID iD) of a person that is credited.",
4394			label: "creditOrcidID"
4395		},
4396		creditGridID: {
4397			description: "Unique identifier (GRID ID) of an organisation that is credited.",
4398			label: "creditGridID"
4399		},
4400		creditRORID: {
4401			description: "Unique identifier (ROR ID) of an organisation that is credited.",
4402			label: "creditRORID"
4403		},
4404		creditFundRefID: {
4405			description: "Unique identifier (FundRef ID) of an organisation that is credited.",
4406			label: "creditFundRefID"
4407		},
4408		creditEntityType: {
4409			description: "Type of entity that is credited.",
4410			label: "creditEntityType"
4411		},
4412		creditEntityRole: {
4413			description: "Role performed by entity that is credited.",
4414			label: "creditEntityRole"
4415		},
4416		creditNote: {
4417			description: "A comment about the credit.",
4418			label: "creditNote"
4419		},
4420		elixirPlatform: {
4421			description: "Name of one of countries participating in ELIXIR",
4422			label: "elixirPlatform"
4423		},
4424		elixirCommunity: {
4425			description: "Name of the community participating in ELIXIR",
4426			label: "elixirCommunity"
4427		},
4428		otherID: {
4429			description: "A unique identifier of the tool, typically assigned by an ID-assignment authority other than bio.tools, e.g. “RRID:SCR_015644”",
4430			label: "otherID"
4431		},
4432		otherIDValue: {
4433			description: "Value of tool identifier, e.g. “RRID:SCR_001156”",
4434			label: "otherIDValue"
4435		},
4436		otherIDType: {
4437			description: "Type of tool identifier, e.g. “rrid”",
4438			label: "otherIDType"
4439		},
4440		otherIDVersion: {
4441			description: "Version information (typically a version number) of the softw
4441are applicable to this identifier, e.g. “1.4”",
4442			label: "otherIDVersion"
4443		},
4444		relationBiotoolsID: {
4445			description: 'bio.tools ID of an existing bio.tools entry to which this tool is related, e.g. "needle"',
4446			label: "relationBiotoolsID"
4447		},
4448		relationType: {
4449			description: 'Type of relation between this and another registered tool, e.g. "isNewVersionOf"',
4450			label: "relationType"
4451		},
4452		confidence: {
4453			description: 'Related to automatically generated entries from publications: describes the confidence score of an entry being a valid bio.tools entry.',
4454			label: "Tool confidence"
4455		},
4456		bioLibAppName: {
4457			description: 'Application Name of an existing BioLib app',
4458			label: "App name"
4459		},
4460		bioLibAuthorUsername: {
4461			description: 'Username of the user that created the BioLib app',
4462			label: "Author username"
4463		},
4464		bioLibAuthorName: {
4465			description: 'The display name of the author that created the BioLib app',
4466			label: "Author name"
4467		}
4468	};
4469})
4470.factory('CheckUserEditingRights', ['User', function(User) {
4471	return {
4472		canEdit: function(resource) {
4473
4474			if (User.isSuperuser()) return true;
4475
4476			if (User.getUsername() == resource.owner) {
4477				return true;
4478			}
4479			else if (resource.editPermission != undefined) {
4480				if (resource.editPermission.type == "public") {
4481					return true;
4482				}
4483				else if (resource.editPermission.type == "group") {
4484					for (var index in resource.editPermission.authors) {
4485						if (User.getUsername() == resource.editPermission.authors[index]) {
4486							return true;
4487						}
4488					}
4489				}
4490			}
4491			return false;
4492		},
4493		isOwner: function(resource) {
4494			if (User.getUsername() == resource.owner) {
4495				return true;
4496			}
4497			return false;
4498		}
4499	};
4500}])
4501.factory('ToolVersionProvider', ['$http', function ($http) {
4502	return {
4503		getVersions: function(toolId) {
4504			return $http({
4505				method: 'GET',
4506				url: '/api/tool/' + toolId + '/version'
4507			}).then(function successCallback(response) {
4508				return response.data;
4509			}, function errorCallback(response) {
4510				return [];
4511			})
4512		}
4513	};
4514}])
4515.factory('EnvironmentChecker', ['$http', function ($http) {
4516	return {
4517		getEnvironment: function() {
4518			return $http({
4519				method: 'GET',
4520				url: '/api/env/'
4521			}).then(function successCallback(response) {
4522				return response.data;
4523			}, function errorCallback(response) {
4524				return [];
4525			})
4526		}
4527	};
4528}])
4529.service('CommunityCollection', function(){
4530	this.hasCommunityCollection = function(collectionList, communityCollection){
4531		if (collectionList && Array.isArray(collectionList) && collectionList.length > 0){
4532			return collectionList.includes(communityCollection);
4533		}
4534		return false;
4535	}
4536
4537	this.addCommunityCollection = function(collectionList, communityCollection){
4538		if (!this.hasCommunityCollection(collectionList, communityCollection)){
4539			
4540			// if the collection property is not set or is set but is not an array
4541			if (!collectionList || (collectionList && !Array.isArray(collectionList))){
4542				return [communityCollection];
4543			}
4544			if (Array.isArray(collectionList)){
4545				collectionList.push(communityCollection);
4546				return collectionList;
4547			}
4548		}
4549	}
4550
4551	this.removeCommunityCollection = function(collectionList, communityCollection){
4552		var result = [];
4553		if (this.hasCommunityCollection(collectionList, communityCollection)){
4554			result = collectionList.filter(function(c){
4555				return c != communityCollection;
4556			});
4557		}
4558		if (result.length == 0){
4559			return undefined
4560		}
4561		return result;
4562	}
4563});
4564
4565// Controllers
4566angular.module('elixir_front.controllers')
4567.controller('SubdomainAdminController', ['$scope',  '$stateParams', 'DomainConnection', 'DomainDetailConnection', 'User', function($scope, $stateParams, DomainConnection, DomainDetailConnection, User) {
4568	var vm = this;
4569	$scope.User = User;
4570	$scope.isLoadingSubdomains = true;
4571	$scope.subdomains = [];
4572
4573	$scope.deleteSubdomain = function(name){
4574		if (confirm("Are you sure you want to remove this subdomain?")){
4575			// COVID-19 subdomain hack
4576			if (name.toLowerCase() == 'covid-19'){
4577				alert('Cannot delete the covid-19 subdomain');
4578				return;
4579			}
4580			
4581			var deleteResponse = DomainDetailConnection.delete({'domain': name}, function(data) {
4582				$scope.subdomains.splice($scope.subdomains.findIndex(function(d) { return d.domain === name; }), 1);
4583				// TODO: Handle responses
4584			}, function(error) {
4585				// TODO: Handle errors
4586			});
4587		}
4588	}
4589	
4590
4591	vm.loadSubdomains = function() {
4592		$scope.isLoadingSubdomains = true;
4593		$scope.subdomains = [];
4594		var domainResponse = DomainConnection.query({}, function(data) {
4595			$scope.subdomains = data;
4596			$scope.isLoadingSubdomains = false;
4597		}, function(errorData) {
4598			// TODO: Handle errors
4599		});
4600	};
4601
4602	// Initialization
4603	vm.loadSubdomains()
4604}])
4605.controller('DomainListController', ['$scope', 'DomainDetailConnection', '$state', function($scope, DomainDetailConnection, $state){
4606	var vm = this;
4607	$scope.domains = [];
4608	$scope.domainSearchText = "";
4609	
4610	$scope.search_title = true;
4611	$scope.search_subtitle = true;
4612	$scope.search_collection = true;
4613	$scope.search_tags = true;
4614	$scope.search_description = false;
4615	$scope.search_tools = false;
4616
4617	$scope.updateKeys = function(){
4618		$scope.initialKeys = [
4619			{
4620				"name":"domain",
4621				"weight": 1
4622			},
4623			{
4624				"name":"title",
4625				"weight": $scope.search_title ? 0.9 : 0
4626			},
4627			{
4628				"name":"sub_title",
4629				"weight": $scope.search_subtitle ? 0.7 : 0
4630			},
4631			{
4632				"name":"tag",
4633				"weight": $scope.search_tags ? 0.9 : 0
4634			},
4635			{
4636				"name":"collection",
4637				"weight": $scope.search_collection ? 0.9 : 0
4638			}, 
4639			{
4640				"name":"description",
4641				"weight": $scope.search_description ? 0.3 : 0
4642			},
4643			{
4644				"name":"resources.name",
4645				"weight": $scope.search_tools ? 0.2 : 0
4646			},
4647			{
4648				"name":"resources.biotoolsID",
4649				"weight": $scope.search_tools ? 0.2 : 0
4650			}
4651		];
4652		
4653		$scope.fuseSearchKeys = $scope.initialKeys.filter(function(x){
4654			return x.weight > 0;
4655		});
4656
4657		$scope.fuseOptions = {
4658			threshold: 0.2,
4659			shouldSort: true,
4660			minMatchCharLength: 2,
4661			// location: 0, default 0
4662			tokenize: true,
4663			// matchAllTokens: true,
4664			keys: $scope.fuseSearchKeys 
4665	
4666		};
4667	}
4668
4669	$scope.updateKeys();
4670
4671	DomainDetailConnection.query({'domain':'all'}, function(response) {
4672		$scope.domains = response.data;
4673		$scope.domains.sort(function(a,b){
4674			var d_a = a.domain.toLowerCase();
4675			var d_b = b.domain.toLowerCase();
4676
4677			if (d_a < d_b) {
4678				return -1;
4679			  }
4680			  if (d_a > d_b) {
4681				return 1;
4682			  }
4683			  return 0; 
4684		})
4685		$scope.filteredDomains = $scope.domains;
4686		$scope.fuse = new Fuse($scope.domains, $scope.fuseOptions);
4687	});
4688
4689	$scope.collectionNameClicked = function(collectionName) {
4690		$state.go('search', {'collectionID': '"'+ collectionName + '"'});
4691	}
4692
4693	$scope.domainSearch = function(){
4694		if ($scope.domainSearchText.length == 0){
4695			$scope.filteredDomains = $scope.domains;
4696		}
4697		else {
4698			$scope.updateKeys();
4699			$scope.fuse = new Fuse($scope.domains, $scope.fuseOptions);
4700			console.log($scope.fuseOptions);
4701			$scope.filteredDomains = $scope.fuse.search($scope.domainSearchText);
4702		}
4703	}
4704
4705
4706}])
4707.controller('SubdomainController', ['$scope', '$state',  '$stateParams', 'ToolListOverviewConnection', 'DomainDetailConnection', 'DomainConnection', '$q', 'UsedTerms', 'UserSuggestionsProvider', 'User', function($scope, $state, $stateParams, ToolListOverviewConnection, DomainDetailConnection, DomainConnection, $q, UsedTerms, UserSuggestionsProvider, User) {
4708	var vm = this;
4709	$scope.ToolListOverviewConnection = ToolListOverviewConnection;
4710	$scope.User = User;
4711	$scope.updating = false;
4712	$scope.loading = ($stateParams.id != "");
4713	$scope.toolsPerPageCount = 10; 
4714
4715	$scope.search = {};
4716	$scope.search.searchOption = 'name';
4717	$scope.search.loadingSearchResults = false;
4718	$scope.search.searchResults = [];
4719	$scope.search.searchString = '';
4720	$scope.search.noResults = false;
4721	$scope.search.currentPage = 1;
4722	$scope.search.totalPages = 1;
4723
4724	$scope.subdomain = {};	
4725	$scope.subdomain.exists = ($stateParams.id != "");
4726	$scope.subdomain.domain = $stateParams.id;
4727	$scope.subdomain.title = '';
4728	$scope.subdomain.subtitle = '';
4729	$scope.subdomain.description = '';
4730	$scope.subdomain.editors = [];
4731	$scope.subdomain.toolList = [];
4732	$scope.subdomain.resources = [];
4733	$scope.subdomain.tag = [];
4734	$scope.subdomain.collection = [];
4735	$scope.subdomain.is_private = true;
4736	$scope.subdomain.currentPage = 1;
4737	$scope.subdomain.totalPages = 1;
4738
4739	$scope.errors = {};
4740	$scope.errors.general = '';
4741	$scope.errors.domain = '';
4742	$scope.errors.loading = '';
4743	$scope.errors.data = {}
4744
4745
4746	$scope.response = {};
4747	$scope.response.general = '';
4748
4749
4750	$scope.gotoDomain = function(d){
4751		if (confirm('Make sure to save before leaving the page. Leave?')){
4752			$state.go('subdomain', {domain: d});
4753		}
4754		
4755	}
4756
4757	// Handle users search
4758	$scope.userSuggestions = function(prefix) {
4759		return UserSuggestionsProvider.getSuggestions(prefix).then(function(data) {
4760			var suggestions = _.map(data, function(obj){
4761				return obj.username;
4762			});
4763			return _.difference(suggestions, $scope.subdomain.editors);
4764		});
4765	};
4766
4767	$scope.userSelected = function($item, $model, $label) {
4768		// Initialize authors if not present.
4769		if ($scope.subdomain.editors == undefined) {
4770			$scope.subdomain.editors = [];
4771		}
4772		// Clear the input field on selection.
4773		$scope.userSuggestion = '';
4774		$scope.subdomain.editors.push($model);
4775	};
4776
4777	$scope.isDomainOwner = function() {
4778		if ($scope.subdomain.owner == User.current.username) {
4779			return true;
4780		}
4781		return false;
4782	}
4783
4784	$scope.deleteUser = function(index) {
4785		$scope.subdomain.editors.splice(index, 1);
4786	}
4787
4788	// Handle tool search
4789	$scope.clearButtonPressed = function() {
4790		$scope.search.currentPage = 1;
4791		var searchQuery = {}
4792		searchQuery[$scope.search.searchOption] = $scope.search.searchString;
4793		$scope.search.searchResults = [];
4794	};
4795
4796	$scope.searchButtonPressed = function() {
4797		$scope.search.currentPage = 1;
4798		var searchQuery = {}
4799		searchQuery[$scope.search.searchOption] = $scope.search.searchString;
4800		vm.performSearch(searchQuery);
4801	};
4802
4803	$scope.toolInSubdomain = function(toolID, versionID) {
4804		for (var index in $scope.subdomain.resources) {
4805			var currentTool = $scope.subdomain.resources[index];
4806			
4807			if (currentTool['biotoolsID'] == toolID){// && currentTool['versionId'] == versionID) {
4808				return true;
4809			}
4810		}
4811		return false;
4812	}
4813
4814	$scope.addToSubdomain = function(index) {
4815		var selectedTool = $scope.search.searchResults[index];
4816		
4817		var selectedDict = { "biotoolsID": selectedTool.biotoolsID, "versionId": selectedTool.versionId };
4818		$scope.subdomain.toolList.push(selectedTool);
4819		$scope.subdomain.resources.push(selectedDict);
4820		$scope.subdomain.totalPages = ($scope.subdomain.toolList.length / $scope.toolsPerPageCount) * 10;
4821	}
4822
4823	$scope.domainAddButtonClick = function (_what, _where, _isList, _isObject) {
4824		if (_isList) {
4825			// if array does not exist create it
4826			if (typeof _where[_what] == 'undefined') {
4827				_where[_what] = [];
4828			}
4829			// add either an object or string to array
4830			_where[_what].push(_isObject ? {} : '');
4831		} else {
4832			// if object does not exist create it
4833			if (typeof _where[_what] == 'undefined') {
4834				_where[_what] = _isObject ? {} : '';
4835			}
4836		}
4837	}
4838
4839	$scope.removeButtonClick = function (_what, _parent, _index, _event){
4840		$scope.removeButtonClickAux(_what, _parent, _index, _event);
4841
4842		if ($scope.errors.data[_what] && $scope.errors.data[_what][_index]){
4843			$scope.removeButtonClickAux(_what, $scope.errors.data, _index, _event);
4844		}
4845		
4846	}
4847
4848	// remove attribute or list entry
4849	$scope.removeButtonClickAux = function (_what, _parent, _index, _event) {
4850		if (_parent[_what][_index] ? confirm("Are you sure you want to remove this element?") : 1) {
4851			// remove jstree if exists
4852			if (_event) {
4853				$(_event.target).closest('div').find('.jstree').jstree("destroy").remove();
4854			}
4855			_parent[_what].splice(_index, 1);
4856			// if last instance in array delete entire attribute from the softw
4856are object
4857			if (_parent[_what].length == 0) {
4858				delete _parent[_what];
4859			}
4860		}
4861	}
4862
4863	// used terms (collectionID) for searching in collections
4864	function getCollectionIDs(){
4865		var d = $q.defer();
4866		var params = {
4867			"usedTermName": "collectionID"
4868		};
4869		UsedTerms.get(params, function(response) {
4870			d.resolve(response.data);
4871		});
4872		return d.promise;
4873	}
4874	
4875	$scope.loadCollectionIDs = function(query) {
4876		return getCollectionIDs().then(function(list) {
4877			return list.filter(function (str) { return str.toLowerCase().includes(query.toLowerCase()); }).slice(0,10).sort();
4878		});
4879	}
4880
4881	$scope.addAllToSubdomain = function() {
4882		for (var tool in $scope.search.searchResults) {
4883			var selectedTool = $scope.search.searchResults[tool];
4884			if (selectedTool.biotoolsID && $scope.toolInSubdomain(selectedTool.biotoolsID, selectedTool.versionId) == false) {	
4885				$scope.subdomain.toolList.push(selectedTool);
4886				$scope.subdomain.resources.push({ "biotoolsID": selectedTool.biotoolsID, "versionId": selectedTool.versionId });
4887			}
4888		}
4889		$scope.subdomain.totalPages = ($scope.subdomain.toolList.length / $scope.toolsPerPageCount) * 10;
4890	}
4891
4892	$scope.removeFromSubdomain = function(index) {
4893		$scope.subdomain.toolList.splice(index, 1); 
4894		$scope.subdomain.resources.splice(index, 1);
4895		$scope.subdomain.totalPages = ($scope.subdomain.toolList.length / $scope.toolsPerPageCount) * 10;
4896	}
4897
4898	$scope.saveSubdomain = function() {
4899		$scope.updating = true;
4900		$scope.errors.general = '';
4901		$scope.response.general = '';
4902		var createResponse = DomainConnection.create(vm.subdomainQuery(), function(data) {
4903			$scope.subdomain.exists = true;
4904			$scope.subdomain.owner = data.owner;
4905
4906			$scope.response.general = "Domain '" + $scope.subdomain.domain + "' was created.";
4907			$scope.updating = false;
4908		}, function(error) {
4909			$scope.errors.general = error.data.detail;
4910			console.log(error);
4911			$scope.updating = false;
4912			$scope.errors.data = error.data;
4913		});
4914	}
4915	
4916	$scope.deleteSubdomain = function(name){
4917		if (confirm("Are you sure you want to remove this subdomain?")){
4918			// COVID-19 subdomain hack
4919			if (name.toLowerCase() == 'covid-19'){
4920				alert('Cannot delete the covid-19 subdomain');
4921				return;
4922			}
4923			
4924			var deleteResponse = DomainDetailConnection.delete({'domain': name}, function(data) {
4925				$scope.response.general = "Domain '" + name + "' was deleted.";
4926				$state.go('domains'); 
4927			}, function(error) {
4928				$scope.errors.general = error.data.detail || "Failed to delete domain.";
4929			});
4930		}
4931	}
4932
4933	vm.fetchSubdomain = function() {
4934		$scope.errors.general = '';
4935		$scope.response.general = '';
4936		var updateResponse = DomainDetailConnection.query({'domain': $scope.subdomain.domain}, function(data) {
4937			$scope.subdomain.title = data.data['title'];
4938			$scope.subdomain.subtitle = data.data['sub_title'];
4939			$scope.subdomain.description = data.data['description'];
4940			for (var index in data.data.resources) {
4941				var tool = data.data.resources[index];
4942				tool['biotoolsID'] = tool['biotoolsID'];
4943				$scope.subdomain.toolList.push(tool);
4944				$scope.subdomain.resources.push(tool);
4945			}
4946
4947			for (var i in data.data.tag){
4948				$scope.subdomain.tag.push(data.data.tag[i]);
4949			}
4950		
4951			for (var i in data.data.collection){
4952				$scope.subdomain.collection.push(data.data.collection[i]);
4953			}
4954
4955			$scope.subdomain.is_private = data.data.is_private;
4956			
4957			if (data.data.editors) {
4958				$scope.subdomain.editors = data.data.editors;
4959			}
4960			if (data.data.owner) {
4961				$scope.subdomain.owner = data.data.owner;
4962			}
4963
4964			$scope.loading = false;
4965			$scope.updating = false;
4966			$scope.subdomain.totalPages = ($scope.subdomain.toolList.length / $scope.toolsPerPageCount) * 10;
4967		}, function(error) {
4968			// Handle error
4969			$scope.errors.loading = 'Data failed to load. Please try to refresh this website. ' + error.data.details;
4970		});
4971	}
4972
4973	$scope.updateSubdomain = function() {
4974		$scope.updating = true;
4975		$scope.errors.general = '';
4976		$scope.response.general = '';
4977		var updateResponse = DomainDetailConnection.update(vm.subdomainQuery(), function(data) {
4978			$scope.response.general = "Domain '" + $scope.subdomain.domain + "' was updated with " + $scope.subdomain.resources.length + " entries." ;
4979			$scope.updating = false;
4980		}, function(error) {
4981			$scope.errors.general = error.data.detail;
4982			$scope.updating = false;
4983			$scope.errors.data = error.data;
4984		});
4985	}
4986
4987	// remove or replace characters not allowed for subdomains
4988	// See https://tools.ietf.org/html/rfc952 for guidelines
4989	$scope.makeSubdomainURLSafe = function(value) {
4990		$scope.errors.domain = "";
4991		if (typeof value != 'undefined') {
4992			var id = value.replace(/[^a-zA-Z0-9_-]*/g,'').replace(/[ ]+/g, '-').replace(/[_]+/g, '-').toLowerCase();
4993		}
4994		if (value != id) {
4995			$scope.errors.domain = "Entered value '" + value + "' has been modified. Only alphanumeric characters and dashes can be used for the subdomain name.";
4996		}
4997		$scope.subdomain.domain = id;
4998	}
4999
5000
5001	vm.performSearch = function(query) {
5002		$scope.search.loadingSearchResults = true;
5003		$scope.search.noResults = false;
5004		var response = ToolListOverviewConnection.query(query, function() {
5005			$scope.search.currentPage = 1;
5006			$scope.search.searchResults = response;
5007			$scope.search.loadingSearchResults = false;
5008			$scope.search.totalPages = ($scope.search.searchResults.length / $scope.toolsPerPageCount) * 10;
5009			if (response.length == 0) {
5010				$scope.search.noResults = true;
5011			}
5012		});
5013	};
5014
5015	vm.subdomainQuery = function() {
5016		return {'domain': $scope.subdomain.domain,
5017		'title': $scope.subdomain.title,
5018		'sub_title': $scope.subdomain.subtitle,
5019		'description': $scope.subdomain.description,
5020		'resources': $scope.subdomain.resources,
5021		'tag': $scope.subdomain.tag,
5022		'collection': $scope.subdomain.collection,
5023		'is_private': $scope.subdomain.is_private,
5024		'editors': $scope.subdomain.editors,
5025	};
5026}
5027
5028	// Initialization
5029	if ($scope.loading) {
5030		vm.fetchSubdomain()
5031	}
5032}]);
5033
5034
5035// Services and Factories
5036angular.module('elixir_front').factory('UserSuggestionsProvider', ['$http', function ($http) {
5037	return {
5038		getSuggestions: function(prefix) {
5039			return $http({
5040				method: 'GET',
5041				url: '/api/user-list',
5042				params: {term: prefix}
5043			}).then(function successCallback(response) {
5044				return response.data;
5045			}, function errorCallback(response) {
5046				return {};
5047			})
5048		}
5049	};
5050}]);
5051
5052// Controllers
5053angular.module('elixir_front.controllers')
5054.controller('WorkflowController', ['$scope',  '$stateParams', '$uibModal', 'WorkflowProvider', function($scope, $stateParams, $uibModal, WorkflowProvider) {
5055	var vm = this;
5056	$scope.loading = true
5057	$scope.filteredWorkflows = []
5058	$scope.workflows = []
5059
5060	// Data feteching
5061	vm.loadRequests = function($item, $model, $label) {
5062		$scope.loading = true;
5063		WorkflowProvider.getWorkflows().then(function successCallback(response) {
5064			$scope.workflows = response.data;
5065			$scope.filterWorkflows();
5066			$scope.loading = false;
5067		}, function errorCallback(response) {
5068			//TODO: Handle error
5069		});
5070	};	
5071
5072	$scope.filterWorkflows = function() {
5073		var searchString = $scope.search.searchString;
5074		if (searchString == undefined || searchString == '') {
5075			$scope.filteredWorkflows = $scope.workflows;
5076		}
5077		else {
5078			$scope.filteredWorkflows = $scope.workflows.filter(function(x) {
5079				var foundAnnotations = x.annotations.filter(function(annotation) {
5080					var regularExpression = new RegExp(searchString,"i");
5081					if (annotation.title.match(regularExpression) || annotation.title.match(regularExpression) || annotation.edam_term.match(regularExpression)) {
5082						return true;
5083					}
5084					return false;
5085				});
5086				return foundAnnotations.length > 0;
5087			});
5088		}
5089	}
5090
5091	$scope.openModal = function(workflow) {
5092		$uibModal.open({
5093			templateUrl: 'partials/workflows/modal.html',
5094			controller: ['$uibModalInstance', 'workflow', WorkflowModalCtrl],
5095			controllerAs: 'vm',
5096			windowClass: 'workflow-modal-window',
5097			resolve: {
5098				workflow: function () { return workflow },
5099			}
5100		});
5101	};
5102
5103	$scope.handleAnnotationClick = function(annotation) {
5104		console.log(annotation);
5105	}
5106
5107	$scope.search = function() {
5108		console.log(string);
5109	};
5110
5111	// Initialization
5112	vm.loadRequests();
5113}]);
5114
5115function WorkflowModalCtrl($uibModalInstance, workflow) {
5116	var vm = this;
5117	vm.workflow = workflow;
5118	vm.currentTitle = "";
5119	vm.currentDescription = "";
5120	vm.currentEdam = "";
5121
5122	vm.mapDimensions = function(annotation) {
5123		return vm.mapXDimension(annotation.startX) + "," + vm.mapYDimension(annotation.startY) + "," + vm.mapXDimension(annotation.endX) + "," + vm.mapYDimension(annotation.endY);
5124	}
5125
5126	vm.imageHeight = function() {
5127		var img = document.getElementById('image'); 
5128		return img.clientHeight;
5129	}
5130
5131	vm.imageWidth = function() {
5132		var img = document.getElementById('image'); 
5133		return img.clientWidth;
5134	}
5135
5136	vm.mapXDimension = function(dim_x) {
5137		var multiplier_x = vm.imageWidth() / vm.workflow.width
5138		return dim_x * multiplier_x
5139	}
5140
5141	vm.mapYDimension = function(dim_y) {
5142		var multiplier_y = vm.imageHeight() / vm.workflow.height
5143		return dim_y * multiplier_y
5144	}
5145
5146	vm.hover = function(annotation) {
5147		vm.currentTitle = annotation.title;
5148		vm.currentDescription = annotation.description;
5149		vm.currentEdam = annotation.edam_term;
5150	}
5151
5152	vm.endHover = function(annotation) {
5153		vm.currentTitle = "";
5154		vm.currentDescription = "";
5155		vm.currentEdam = "";
5156	}
5157}
5158
5159// Services and Factories
5160var elixir_front = angular.module('elixir_front');
5161elixir_front.factory('WorkflowProvider', ['$http', function ($http) {
5162	return {
5163		getWorkflows: function() {
5164			return $http({
5165				method: 'GET',
5166				url: '/api/w',
5167				params: {}
5168			})
5169		}
5170	};
5171}]);
5172(function() {
5173  var module,
5174    __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
5175
5176  module = angular.module('angularBootstrapNavTree', []);
5177
5178  module.directive('abnTree', [
5179    '$timeout', function($timeout) {
5180      return {
5181        restrict: 'E',
5182        template: "<ul class=\"nav nav-list nav-pills nav-stacked abn-tree\">\n  <li tooltips tooltip-side=\"right\" tooltip-content=\"{{ resourceModel[row.branch.name].description }}\" ng-repeat=\"row in tree_rows | filter:{visible:true} track by row.branch.uid\" ng-animate=\"'abn-tree-animate'\" ng-class=\"'level-' + {{ row.level }} + (row.branch.selected ? ' active':'') + ' ' +row.classes.join(' ')\" class=\"abn-tree-row\"><a ng-click=\"user_clicks_branch(row.branch)\"><i ng-class=\"row.tree_icon\" ng-click=\"row.branch.expanded = !row.branch.expanded\" class=\"indented tree-icon\"> </i><span class=\"indented tree-label\"> {{ resourceModel[row.branch.name].label }} </span></a></li>\n</ul>",
5183        replace: true,
5184        scope: {
5185          treeData: '=',
5186          onSelect: '&',
5187          initialSelection: '@',
5188          treeControl: '=',
5189          resourceModel: '='
5190        },
5191        link: function(scope, element, attrs) {
5192          var error, expand_all_parents, expand_level, for_all_ancestors, for_each_branch, get_parent, n, on_treeData_change, select_branch, selected_branch, tree;
5193          error = function(s) {
5194            
5195            debugger;
5196            return void 0;
5197          };
5198          if (attrs.iconExpand == null) {
5199            attrs.iconExpand = 'icon-plus  glyphicon glyphicon-plus  fa fa-plus';
5200          }
5201          if (attrs.iconCollapse == null) {
5202            attrs.iconCollapse = 'icon-minus glyphicon glyphicon-minus fa fa-minus';
5203          }
5204          if (attrs.iconLeaf == null) {
5205            attrs.iconLeaf = 'icon-file  glyphicon glyphicon-file  fa fa-file';
5206          }
5207          if (attrs.expandLevel == null) {
5208            attrs.expandLevel = '3';
5209          }
5210          expand_level = parseInt(attrs.expandLevel, 10);
5211          if (!scope.treeData) {
5212            alert('no treeData defined for the tree!');
5213            return;
5214          }
5215          if (scope.treeData.length == null) {
5216            if (treeData.label != null) {
5217              scope.treeData = [treeData];
5218            } else {
5219              alert('treeData should be an array of root branches');
5220              return;
5221            }
5222          }
5223          for_each_branch = function(f) {
5224            var do_f, root_branch, _i, _len, _ref, _results;
5225            do_f = function(branch, level) {
5226              var child, _i, _len, _ref, _results;
5227              f(branch, level);
5228              if (branch.children != null) {
5229                _ref = branch.children;
5230                _results = [];
5231                for (_i = 0, _len = _ref.length; _i < _len; _i++) {
5232                  child = _ref[_i];
5233                  _results.push(do_f(child, level + 1));
5234                }
5235                return _results;
5236              }
5237            };
5238            _ref = scope.treeData;
5239            _results = [];
5240            for (_i = 0, _len = _ref.length; _i < _len; _i++) {
5241              root_branch = _ref[_i];
5242              _results.push(do_f(root_branch, 1));
5243            }
5244            return _results;
5245          };
5246          selected_branch = null;
5247          select_branch = function(branch) {
5248            if (!branch) {
5249              if (selected_branch != null) {
5250                selected_branch.selected = false;
5251              }
5252              selected_branch = null;
5253              return;
5254            }
5255            if (branch !== selected_branch) {
5256              if (selected_branch != null) {
5257                selected_branch.selected = false;
5258              }
5259              branch.selected = true;
5260              selected_branch = branch;
5261              expand_all_parents(branch);
5262              if (branch.onSelect != null) {
5263                return $timeout(function() {
5264                  return branch.onSelect(branch);
5265                });
5266              } else {
5267                if (scope.onSelect != null) {
5268                  return $timeout(function() {
5269                    return scope.onSelect({
5270                      branch: branch
5271                    });
5272                  });
5273                }
5274              }
5275            }
5276          };
5277          scope.user_clicks_branch = function(branch) {
5278            if (branch !== selected_branch) {
5279              return select_branch(branch);
5280            }
5281          };
5282          get_parent = function(child) {
5283            var parent;
5284            parent = void 0;
5285            if (child.parent_uid) {
5286              for_each_branch(function(b) {
5287                if (b.uid === child.parent_uid) {
5288                  return parent = b;
5289                }
5290              });
5291            }
5292            return parent;
5293          };
5294          for_all_ancestors = function(child, fn) {
5295            var parent;
5296            parent = get_parent(child);
5297            if (parent != null) {
5298              fn(parent);
5299              return for_all_ancestors(parent, fn);
5300            }
5301          };
5302          expand_all_parents = function(child) {
5303            return for_all_ancestors(child, function(b) {
5304              return b.expanded = true;
5305            });
5306          };
5307          scope.tree_rows = [];
5308          on_treeData_change = function() {
5309            var add_branch_to_list, root_branch, _i, _len, _ref, _results;
5310            for_each_branch(function(b, level) {
5311              if (!b.uid) {
5312                return b.uid = "" + Math.random();
5313              }
5314            });
5315            
5316            for_each_branch(function(b) {
5317              var child, _i, _len, _ref, _results;
5318              if (angular.isArray(b.children)) {
5319                _ref = b.children;
5320                _results = [];
5321                for (_i = 0, _len = _ref.length; _i < _len; _i++) {
5322                  child = _ref[_i];
5323                  _results.push(child.parent_uid = b.uid);
5324                }
5325                return _results;
5326              }
5327            });
5328            scope.tree_rows = [];
5329            for_each_branch(function(branch) {
5330              var child, f;
5331              if (branch.children) {
5332                if (branch.children.length > 0) {
5333                  f = function(e) {
5334                    if (typeof e === 'string') {
5335                      return {
5336                        label: e,
5337                        children: []
5338                      };
5339                    } else {
5340                      return e;
5341                    }
5342                  };
5343                  return branch.children = (function() {
5344                    var _i, _len, _ref, _results;
5345                    _ref = branch.children;
5346                    _results = [];
5347                    for (_i = 0, _len = _ref.length; _i < _len; _i++) {
5348                      child = _ref[_i];
5349                      _results.push(f(child));
5350                    }
5351                    return _results;
5352                  })();
5353                }
5354              } else {
5355                return branch.children = [];
5356              }
5357            });
5358            add_branch_to_list = function(level, branch, visible) {
5359              var child, child_visible, tree_icon, _i, _len, _ref, _results;
5360              if (branch.expanded == null) {
5361                branch.expanded = false;
5362              }
5363              if (branch.classes == null) {
5364                branch.classes = [];
5365              }
5366              if (!branch.noLeaf && (!branch.children || branch.children.length === 0)) {
5367                tree_icon = attrs.iconLeaf;
5368                if (__indexOf.call(branch.classes, "leaf") < 0) {
5369                  branch.classes.push("leaf");
5370                }
5371              } else {
5372                if (branch.expanded) {
5373                  tree_icon = attrs.iconCollapse;
5374                } else {
5375                  tree_icon = attrs.iconExpand;
5376                }
5377              }
5378              scope.tree_rows.push({
5379                level: level,
5380                branch: branch,
5381                label: branch.label,
5382                classes: branch.classes,
5383                tree_icon: tree_icon,
5384                visible: visible
5385              });
5386              if (branch.children != null) {
5387                _ref = branch.children;
5388                _results = [];
5389                for (_i = 0, _len = _ref.length; _i < _len; _i++) {
5390                  child = _ref[_i];
5391                  child_visible = visible && branch.expanded;
5392                  _results.push(add_branch_to_list(level + 1, child, child_visible));
5393                }
5394                return _results;
5395              }
5396            };
5397            _ref = scope.treeData;
5398            _results = [];
5399            for (_i = 0, _len = _ref.length; _i < _len; _i++) {
5400              root_branch = _ref[_i];
5401              _results.push(add_branch_to_list(1, root_branch, true));
5402            }
5403            return _results;
5404          };
5405          scope.$watch('treeData', on_treeData_change, true);
5406          if (attrs.initialSelection != null) {
5407            for_each_branch(function(b) {
5408              if (b.label === attrs.initialSelection) {
5409                return $timeout(function() {
5410                  return select_branch(b);
5411                });
5412              }
5413            });
5414          }
5415          n = scope.treeData.length;
5416          
5417          for_each_branch(function(b, level) {
5418            b.level = level;
5419            return b.expanded = b.level < expand_level;
5420          });
5421          if (scope.treeControl != null) {
5422            if (angular.isObject(scope.treeControl)) {
5423              tree = scope.treeControl;
5424              tree.expand_all = function() {
5425                return for_each_branch(function(b, level) {
5426                  return b.expanded = true;
5427                });
5428              };
5429              tree.collapse_all = function() {
5430                return for_each_branch(function(b, level) {
5431                  return b.expanded = false;
5432                });
5433              };
5434              tree.get_first_branch = function() {
5435                n = scope.treeData.length;
5436                if (n > 0) {
5437                  return scope.treeData[0];
5438                }
5439              };
5440              tree.select_first_branch = function() {
5441                var b;
5442                b = tree.get_first_branch();
5443                return tree.select_branch(b);
5444              };
5445              tree.get_selected_branch = function() {
5446                return selected_branch;
5447              };
5448              tree.get_parent_branch = function(b) {
5449                return get_parent(b);
5450              };
5451              tree.select_branch = function(b) {
5452                select_branch(b);
5453                return b;
5454              };
5455              tree.get_children = function(b) {
5456                return b.children;
5457              };
5458              tree.select_parent_branch = function(b) {
5459                var p;
5460                if (b == null) {
5461                  b = tree.get_selected_branch();
5462                }
5463                if (b != null) {
5464                  p = tree.get_parent_branch(b);
5465                  if (p != null) {
5466                    tree.select_branch(p);
5467                    return p;
5468                  }
5469                }
5470              };
5471              tree.add_branch = function(parent, new_branch) {
5472                if (parent != null) {
5473                  parent.children.push(new_branch);
5474                  parent.expanded = true;
5475                } else {
5476                  scope.treeData.push(new_branch);
5477                }
5478                return new_branch;
5479              };
5480              tree.add_root_branch = function(new_branch) {
5481                tree.add_branch(null, new_branch);
5482                return new_branch;
5483              };
5484              tree.expand_branch = function(b) {
5485                if (b == null) {
5486                  b = tree.get_selected_branch();
5487                }
5488                if (b != null) {
5489                  b.expanded = true;
5490                  return b;
5491                }
5492              };
5493              tree.collapse_branch = function(b) {
5494                if (b == null) {
5495                  b = selected_branch;
5496                }
5497                if (b != null) {
5498                  b.expanded = false;
5499                  return b;
5500                }
5501              };
5502              tree.get_siblings = function(b) {
5503                var p, siblings;
5504                if (b == null) {
5505                  b = selected_branch;
5506                }
5507                if (b != null) {
5508                  p = tree.get_parent_branch(b);
5509                  if (p) {
5510                    siblings = p.children;
5511                  } else {
5512                    siblings = scope.treeData;
5513                  }
5514                  return siblings;
5515                }
5516              };
5517              tree.get_next_sibling = function(b) {
5518                var i, siblings;
5519                if (b == null) {
5520                  b = selected_branch;
5521                }
5522                if (b != null) {
5523                  siblings = tree.get_siblings(b);
5524                  n = siblings.length;
5525                  i = siblings.indexOf(b);
5526                  if (i < n) {
5527                    return siblings[i + 1];
5528                  }
5529                }
5530              };
5531              tree.get_prev_sibling = function(b) {
5532                var i, siblings;
5533                if (b == null) {
5534                  b = selected_branch;
5535                }
5536                siblings = tree.get_siblings(b);
5537                n = siblings.length;
5538                i = siblings.indexOf(b);
5539                if (i > 0) {
5540                  return siblings[i - 1];
5541                }
5542              };
5543              tree.select_next_sibling = function(b) {
5544                var next;
5545                if (b == null) {
5546                  b = selected_branch;
5547                }
5548                if (b != null) {
5549                  next = tree.get_next_sibling(b);
5550                  if (next != null) {
5551                    return tree.select_branch(next);
5552                  }
5553                }
5554              };
5555              tree.select_prev_sibling = function(b) {
5556                var prev;
5557                if (b == null) {
5558                  b = selected_branch;
5559                }
5560                if (b != null) {
5561                  prev = tree.get_prev_sibling(b);
5562                  if (prev != null) {
5563                    return tree.select_branch(prev);
5564                  }
5565                }
5566              };
5567              tree.get_first_child = function(b) {
5568                var _ref;
5569                if (b == null) {
5570                  b = selected_branch;
5571                }
5572                if (b != null) {
5573                  if (((_ref = b.children) != null ? _ref.length : void 0) > 0) {
5574                    return b.children[0];
5575                  }
5576                }
5577              };
5578              tree.get_closest_ancestor_next_sibling = function(b) {
5579                var next, parent;
5580                next = tree.get_next_sibling(b);
5581                if (next != null) {
5582                  return next;
5583                } else {
5584                  parent = tree.get_parent_branch(b);
5585                  return tree.get_closest_ancestor_next_sibling(parent);
5586                }
5587              };
5588              tree.get_next_branch = function(b) {
5589                var next;
5590                if (b == null) {
5591                  b = selected_branch;
5592                }
5593                if (b != null) {
5594                  next = tree.get_first_child(b);
5595                  if (next != null) {
5596                    return next;
5597                  } else {
5598                    next = tree.get_closest_ancestor_next_sibling(b);
5599                    return next;
5600                  }
5601                }
5602              };
5603              tree.select_next_branch = function(b) {
5604                var next;
5605                if (b == null) {
5606                  b = selected_branch;
5607                }
5608                if (b != null) {
5609                  next = tree.get_next_branch(b);
5610                  if (next != null) {
5611                    tree.select_branch(next);
5612                    return next;
5613                  }
5614                }
5615              };
5616              tree.last_descendant = function(b) {
5617                var last_child;
5618                if (b == null) {
5619                  debugger;
5620                }
5621                n = b.children.length;
5622                if (n === 0) {
5623                  return b;
5624                } else {
5625                  last_child = b.children[n - 1];
5626                  return tree.last_descendant(last_child);
5627                }
5628              };
5629              tree.get_prev_branch = function(b) {
5630                var parent, prev_sibling;
5631                if (b == null) {
5632                  b = selected_branch;
5633                }
5634                if (b != null) {
5635                  prev_sibling = tree.get_prev_sibling(b);
5636                  if (prev_sibling != null) {
5637                    return tree.last_descendant(prev_sibling);
5638                  } else {
5639                    parent = tree.get_parent_branch(b);
5640                    return parent;
5641                  }
5642                }
5643              };
5644              return tree.select_prev_branch = function(b) {
5645                var prev;
5646                if (b == null) {
5647                  b = selected_branch;
5648                }
5649                if (b != null) {
5650                  prev = tree.get_prev_branch(b);
5651                  if (prev != null) {
5652                    tree.select_branch(prev);
5653                    return prev;
5654                  }
5655                }
5656              };
5657            }
5658          }
5659        }
5660      };
5661    }
5662  ]);
5663
5664}).call(this);
5665
5666'use strict';
5667
5668angular.module('angularDjangoRegistrationAuthApp', ['ngCookies', 'ngResource', 'ngSanitize', 'ngRoute'])
5669.run(function(djangoAuth){
5670	djangoAuth.initialize('/api/rest-auth', false);
5671});
5672
5673angular.module('angularDjangoRegistrationAuthApp')
5674  .service('djangoAuth', function djangoAuth($q, $http, $cookies, $rootScope) {
5675	// AngularJS will instantiate a singleton by calling "new" on this function
5676	var service = {
5677		/* START CUSTOMIZATION HERE */
5678		// Change this to point to your Django REST Auth API
5679		// e.g. /api/rest-auth  (DO NOT INCLUDE ENDING SLASH)
5680		'API_URL': '/api/rest-auth',
5681		// Set use_session to true to use Django sessions to store security token.
5682		// Set use_session to false to store the security token locally and transmit it as a custom header.
5683		'use_session': false,
5684		/* END OF CUSTOMIZATION */
5685		'authenticated': null,
5686		'authPromise': null,
5687		'request': function(args) {
5688			// Let's retrieve the token from the cookie, if available
5689			if(localStorage.token){
5690				$http.defaults.headers.common.Authorization = 'Token ' + localStorage.token;
5691			}
5692			// Continue
5693			params = args.params || {}
5694			args = args || {};
5695			var deferred = $q.defer(),
5696				url = this.API_URL + args.url,
5697				method = args.method || "GET",
5698				params = params,
5699				data = args.data || {};
5700			// Fire the request, as configured.
5701			$http({
5702				url: url,
5703				withCredentials: this.use_session,
5704				method: method.toUpperCase(),
5705				headers: {'X-CSRFToken': $cookies['csrftoken']},
5706				params: params,
5707				data: data
5708			})
5709			.success(angular.bind(this,function(data, status, headers, config) {
5710				deferred.resolve(data, status);
5711			}))
5712			.error(angular.bind(this,function(data, status, headers, config) {
5713				console.log("error syncing with: " + url);
5714				// Set request status
5715				if(data){
5716					data.status = status;
5717				}
5718				if(status == 0){
5719					if(data == ""){
5720						data = {};
5721						data['status'] = 0;
5722						data['non_field_errors'] = ["Could not connect. Please try again."];
5723					}
5724					// or if the data is null, then there was a timeout.
5725					if(data == null){
5726						// Inject a non field error alerting the user
5727						// that there's been a timeout error.
5728						data = {};
5729						data['status'] = 0;
5730						data['non_field_errors'] = ["Server timed out. Please try again."];
5731					}
5732				}
5733				deferred.reject(data, status, headers, config);
5734			}));
5735			return deferred.promise;
5736		},
5737		'register': function(username,password1,password2,email,more){
5738			var data = {
5739				'username':username,
5740				'password1':password1,
5741				'password2':password2,
5742				'email':email
5743			}
5744			data = angular.extend(data,more);
5745			return this.request({
5746				'method': "POST",
5747				'url': "/registration/",
5748				'data' :data
5749			});
5750		},
5751		'login': function(username,password){
5752			var djangoAuth = this;
5753			return this.request({
5754				'method': "POST",
5755				'url': "/login/",
5756				'data':{
5757					'username':username,
5758					'password':password
5759				}
5760			}).then(function(data){
5761				if(!djangoAuth.use_session){
5762					$http.defaults.headers.common.Authorization = 'Token ' + data.key;
5763					localStorage.token = data.key;
5764				}
5765				djangoAuth.authenticated = true;
5766				$rootScope.$broadcast("djangoAuth.logged_in", data);
5767			});
5768		},
5769		'orcidLogin': function(code){
5770			// POST request at rest-auth/orcid/login/
5771			var djangoAuth = this;
5772			return this.request({
5773				'method': "POST",
5774				'url': "/orcid/",
5775				'data': {
5776					'code': code
5777				}
5778			}).then(function(data){
5779				// Handle successful ORCID login
5780				if(!djangoAuth.use_session){
5781					$http.defaults.headers.common.Authorization = 'Token ' + data.key;
5782					localStorage.token = data.key;
5783				}
5784				djangoAuth.authenticated = true;
5785				$rootScope.$broadcast("djangoAuth.logged_in", data);
5786				return data;
5787			}, function(error) {
5788				console.error("ORCID login failed:", error);
5789			});
5790		},
5791		'orcidConnect': function(code){ //TODO
5792			var djangoAuth = this;
5793			return this.request({
5794				'method': "POST",
5795				'url': "/orcid/connect/",
5796				'data': {
5797					'code': code
5798				}
5799			}).then(function(data){
5800				// Handle successful ORCID connection
5801				$rootScope.$broadcast("djangoAuth.orcid_connected", data);
5802				return data;
5803			}, function(error) {
5804				console.error("ORCID connection failed:", error);
5805			});
5806		},
5807		'disconnectSocial': function(id){
5808			return this.request({
5809				'method': "POST",
5810				'url': "/socialaccounts/"+id+"/disconnect/"
5811			});
5812		},
5813		'logout': function(){
5814			var djangoAuth = this;
5815			return this.request({
5816				'method': "POST",
5817				'url': "/logout/"
5818			}).then(function(data){
5819				delete $http.defaults.headers.common.Authorization;
5820				delete localStorage.token;
5821				djangoAuth.authenticated = false;
5822				$rootScope.$broadcast("djangoAuth.logged_out");
5823			});
5824		},
5825		'changePassword': function(old_password, password1, password2){
5826			return this.request({
5827				'method': "POST",
5828				'url': "/password/change/",
5829				'data':{
5830					'old_password':old_password,
5831					'new_password1':password1,
5832					'new_password2':password2
5833				}
5834			});
5835		},
5836		'resetPassword': function(email){
5837			return this.request({
5838				'method': "POST",
5839				'url': "/password/reset/",
5840				'data':{
5841					'email':email
5842				}
5843			});
5844		},
5845		'profile': function(){
5846			return this.request({
5847				'method': "GET",
5848				'url': "/user/"
5849			}); 
5850		},
5851		'updateProfile': function(data){
5852			return this.request({
5853				'method': "PATCH",
5854				'url': "/user/",
5855				'data':data
5856			}); 
5857		},
5858		'verify': function(key){
5859			return this.request({
5860				'method': "POST",
5861				'url': "/registration/verify-email/",
5862				'data': {'key': key} 
5863			});            
5864		},
5865		'resendEmail': function(email){
5866			return this.request({
5867				'method': "POST",
5868				'url': "/registration/resend-email/",
5869				'data': {'email': email} 
5870			});            
5871		},
5872		'confirmReset': function(uid,token,password1,password2){
5873			return this.request({
5874				'method': "POST",
5875				'url': "/password/reset/confirm/",
5876				'data':{
5877					'uid': uid,
5878					'token': token,
5879					'new_password1':password1,
5880					'new_password2':password2
5881				}
5882			});
5883		},
5884		'authenticationStatus': function(restrict, force){
5885			// Set restrict to true to reject the promise if not logged in
5886			// Set to false or omit to resolve when status is known
5887			// Set force to true to ignore stored value and query API
5888			restrict = restrict || false;
5889			force = force || false;
5890			if(this.authPromise == null || force){
5891				this.authPromise = this.request({
5892					'method': "GET",
5893					'url': "/user/"
5894				})
5895			}
5896			var da = this;
5897			var getAuthStatus = $q.defer();
5898			if(this.authenticated != null && !force){
5899				// We have a stored value which means we can pass it back right away.
5900				if(this.authenticated == false && restrict){
5901					getAuthStatus.reject("User is not logged in.");
5902				}else{
5903					getAuthStatus.resolve();
5904				}
5905			}else{
5906				// There isn't a stored value, or we're forcing a request back to
5907				// the API to get the authentication status.
5908				this.authPromise.then(function(){
5909					da.authenticated = true;
5910					getAuthStatus.resolve();
5911				},function(){
5912					da.authenticated = false;
5913					if(restrict){
5914						getAuthStatus.reject("User is not logged in.");
5915					}else{
5916						getAuthStatus.resolve();
5917					}
5918				});
5919			}
5920			return getAuthStatus.promise;
5921		},
5922		'initialize': function(url, sessions){
5923			this.API_URL = url;
5924			this.use_session = sessions;
5925			return this.authenticationStatus();
5926		}
5927
5928	}
5929	return service;
5930  });
5931
5932/**
5933 * @license
5934 * Fuse - Lightweight fuzzy-search
5935 *
5936 * Copyright (c) 2012-2016 Kirollos Risk <[email protected]>.
5937 * All Rights Reserved. Apache Software License 2.0
5938 *
5939 * Licensed under the Apache License, Version 2.0 (the "License")
5940 * you may not use this file except in compliance with the License.
5941 * You may obtain a copy of the License at
5942 *
5943 * http://www.apache.org/licenses/LICENSE-2.0
5944 *
5945 * Unless required by applicable law or agreed to in writing, software
5946 * distributed under the License is distributed on an "AS IS" BASIS,
5947 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5948 * See the License for the specific language governing permissions and
5949 * limitations under the License.
5950 */
5951;(function (global) {
5952  'use strict'
5953
5954  /** @type {function(...*)} */
5955  function log () {
5956    console.log.apply(console, arguments)
5957  }
5958
5959  var defaultOptions = {
5960    // The name of the identifier property. If specified, the returned result will be a list
5961    // of the items' dentifiers, otherwise it will be a list of the items.
5962    id: null,
5963
5964    // Indicates whether comparisons should be case sensitive.
5965
5966    caseSensitive: false,
5967
5968    // An array of values that should be included from the searcher's output. When this array
5969    // contains elements, each result in the list will be of the form `{ item: ..., include1: ..., include2: ... }`.
5970    // Values you can include are `score`, `matchedLocations`
5971    include: [],
5972
5973    // Whether to sort the result list, by score
5974    shouldSort: true,
5975
5976    // The search function to use
5977    // Note that the default search function ([[Function]]) must conform to the following API:
5978    //
5979    //  @param pattern The pattern string to search
5980    //  @param options The search option
5981    //  [[Function]].constructor = function(pattern, options)
5982    //
5983    //  @param text: the string to search in for the pattern
5984    //  @return Object in the form of:
5985    //    - isMatch: boolean
5986    //    - score: Int
5987    //  [[Function]].prototype.search = function(text)
5988    searchFn: BitapSearcher,
5989
5990    // Default sort function
5991    sortFn: function (a, b) {
5992      return a.score - b.score
5993    },
5994
5995    // The get function to use when fetching an object's properties.
5996    // The default will search nested paths *ie foo.bar.baz*
5997    getFn: deepValue,
5998
5999    // List of properties that will be searched. This also supports nested properties.
6000    keys: [],
6001
6002    // Will print to the console. Useful for debugging.
6003    verbose: false,
6004
6005    // When true, the search algorithm will search individual words **and** the full string,
6006    // computing the final score as a function of both. Note that when `tokenize` is `true`,
6007    // the `threshold`, `distance`, and `location` are inconsequential for individual tokens.
6008    tokenize: false,
6009
6010    // When true, the result set will only include records that match all tokens. Will only work
6011    // if `tokenize` is also true.
6012    matchAllTokens: false,
6013
6014    // Regex used to separate words when searching. Only applicable when `tokenize` is `true`.
6015    tokenSeparator: / +/g,
6016
6017    // Minimum number of characters that must be matched before a result is considered a match
6018    minMatchCharLength: 1,
6019
6020    // When true, the algorithm continues searching to the end of the input even if a perfect
6021    // match is found before the end of the same input.
6022    findAllMatches: false
6023  }
6024
6025  /**
6026   * @constructor
6027   * @param {!Array} list
6028   * @param {!Object<string, *>} options
6029   */
6030  function Fuse (list, options) {
6031    var key
6032
6033    this.list = list
6034    this.options = options = options || {}
6035
6036    for (key in defaultOptions) {
6037      if (!defaultOptions.hasOwnProperty(key)) {
6038        continue;
6039      }
6040      // Add boolean type options
6041      if (typeof defaultOptions[key] === 'boolean') {
6042        this.options[key] = key in options ? options[key] : defaultOptions[key];
6043      // Add all other options
6044      } else {
6045        this.options[key] = options[key] || defaultOptions[key]
6046      }
6047    }
6048  }
6049
6050  Fuse.VERSION = '2.6.2'
6051
6052  /**
6053   * Sets a new list for Fuse to match against.
6054   * @param {!Array} list
6055   * @return {!Array} The newly set list
6056   * @public
6057   */
6058  Fuse.prototype.set = function (list) {
6059    this.list = list
6060    return list
6061  }
6062
6063  Fuse.prototype.search = function (pattern) {
6064    if (this.options.verbose) log('\nSearch term:', pattern, '\n')
6065
6066    this.pattern = pattern
6067    this.results = []
6068    this.resultMap = {}
6069    this._keyMap = null
6070
6071    this._prepareSearchers()
6072    this._startSearch()
6073    this._computeScore()
6074    this._sort()
6075
6076    var output = this._format()
6077    return output
6078  }
6079
6080  Fuse.prototype._prepareSearchers = function () {
6081    var options = this.options
6082    var pattern = this.pattern
6083    var searchFn = options.searchFn
6084    var tokens = pattern.split(options.tokenSeparator)
6085    var i = 0
6086    var len = tokens.length
6087
6088    if (this.options.tokenize) {
6089      this.tokenSearchers = []
6090      for (; i < len; i++) {
6091        this.tokenSearchers.push(new searchFn(tokens[i], options))
6092      }
6093    }
6094    this.fullSeacher = new searchFn(pattern, options)
6095  }
6096
6097  Fuse.prototype._startSearch = function () {
6098    var options = this.options
6099    var getFn = options.getFn
6100    var list = this.list
6101    var listLen = list.length
6102    var keys = this.options.keys
6103    var keysLen = keys.length
6104    var key
6105    var weight
6106    var item = null
6107    var i
6108    var j
6109
6110    // Check the first item in the list, if it's a string, then we assume
6111    // that every item in the list is also a string, and thus it's a flattened array.
6112    if (typeof list[0] === 'string') {
6113      // Iterate over every item
6114      for (i = 0; i < listLen; i++) {
6115        this._analyze('', list[i], i, i)
6116      }
6117    } else {
6118      this._keyMap = {}
6119      // Otherwise, the first item is an Object (hopefully), and thus the searching
6120      // is done on the values of the keys of each item.
6121      // Iterate over every item
6122      for (i = 0; i < listLen; i++) {
6123        item = list[i]
6124        // Iterate over every key
6125        for (j = 0; j < keysLen; j++) {
6126          key = keys[j]
6127          if (typeof key !== 'string') {
6128            weight = (1 - key.weight) || 1
6129            this._keyMap[key.name] = {
6130              weight: weight
6131            }
6132            if (key.weight <= 0 || key.weight > 1) {
6133              throw new Error('Key weight has to be > 0 and <= 1')
6134            }
6135            key = key.name
6136          } else {
6137            this._keyMap[key] = {
6138              weight: 1
6139            }
6140          }
6141          this._analyze(key, getFn(item, key, []), item, i)
6142        }
6143      }
6144    }
6145  }
6146
6147  Fuse.prototype._analyze = function (key, text, entity, index) {
6148    var options = this.options
6149    var words
6150    var scores
6151    var exists = false
6152    var existingResult
6153    var averageScore
6154    var finalScore
6155    var scoresLen
6156    var mainSearchResult
6157    var tokenSearcher
6158    var termScores
6159    var word
6160    var tokenSearchResult
6161    var hasMatchInText
6162    var checkTextMatches
6163    var i
6164    var j
6165
6166    // Check if the text can be searched
6167    if (text === undefined || text === null) {
6168      return
6169    }
6170
6171    scores = []
6172
6173    var numTextMatches = 0
6174
6175    if (typeof text === 'string') {
6176      words = text.split(options.tokenSeparator)
6177
6178      if (options.verbose) log('---------\nKey:', key)
6179
6180      if (this.options.tokenize) {
6181        for (i = 0; i < this.tokenSearchers.length; i++) {
6182          tokenSearcher = this.tokenSearchers[i]
6183
6184          if (options.verbose) log('Pattern:', tokenSearcher.pattern)
6185
6186          termScores = []
6187          hasMatchInText = false
6188
6189          for (j = 0; j < words.length; j++) {
6190            word = words[j]
6191            tokenSearchResult = tokenSearcher.search(word)
6192            var obj = {}
6193            if (tokenSearchResult.isMatch) {
6194              obj[word] = tokenSearchResult.score
6195              exists = true
6196              hasMatchInText = true
6197              scores.push(tokenSearchResult.score)
6198            } else {
6199              obj[word] = 1
6200              if (!this.options.matchAllTokens) {
6201                scores.push(1)
6202              }
6203            }
6204            termScores.push(obj)
6205          }
6206
6207          if (hasMatchInText) {
6208            numTextMatches++
6209          }
6210
6211          if (options.verbose) log('Token scores:', termScores)
6212        }
6213
6214        averageScore = scores[0]
6215        scoresLen = scores.length
6216        for (i = 1; i < scoresLen; i++) {
6217          averageScore += scores[i]
6218        }
6219        averageScore = averageScore / scoresLen
6220
6221        if (options.verbose) log('Token score average:', averageScore)
6222      }
6223
6224      mainSearchResult = this.fullSeacher.search(text)
6225      if (options.verbose) log('Full text score:', mainSearchResult.score)
6226
6227      finalScore = mainSearchResult.score
6228      if (averageScore !== undefined) {
6229        finalScore = (finalScore + averageScore) / 2
6230      }
6231
6232      if (options.verbose) log('Score average:', finalScore)
6233
6234      checkTextMatches = (this.options.tokenize && this.options.matchAllTokens) ? numTextMatches >= this.tokenSearchers.length : true
6235
6236      if (options.verbose) log('Check Matches', checkTextMatches)
6237
6238      // If a match is found, add the item to <rawResults>, including its score
6239      if ((exists || mainSearchResult.isMatch) && checkTextMatches) {
6240        // Check if the item already exists in our results
6241        existingResult = this.resultMap[index]
6242
6243        if (existingResult) {
6244          // Use the lowest score
6245          // existingResult.score, bitapResult.score
6246          existingResult.output.push({
6247            key: key,
6248            score: finalScore,
6249            matchedIndices: mainSearchResult.matchedIndices
6250          })
6251        } else {
6252          // Add it to the raw result list
6253          this.resultMap[index] = {
6254            item: entity,
6255            output: [{
6256              key: key,
6257              score: finalScore,
6258              matchedIndices: mainSearchResult.matchedIndices
6259            }]
6260          }
6261
6262          this.results.push(this.resultMap[index])
6263        }
6264      }
6265    } else if (isArray(text)) {
6266      for (i = 0; i < text.length; i++) {
6267        this._analyze(key, text[i], entity, index)
6268      }
6269    }
6270  }
6271
6272  Fuse.prototype._computeScore = function () {
6273    var i
6274    var j
6275    var keyMap = this._keyMap
6276    var totalScore
6277    var output
6278    var scoreLen
6279    var score
6280    var weight
6281    var results = this.results
6282    var bestScore
6283    var nScore
6284
6285    if (this.options.verbose) log('\n\nComputing score:\n')
6286
6287    for (i = 0; i < results.length; i++) {
6288      totalScore = 0
6289      output = results[i].output
6290      scoreLen = output.length
6291
6292      bestScore = 1
6293
6294      for (j = 0; j < scoreLen; j++) {
6295        score = output[j].score
6296        weight = keyMap ? keyMap[output[j].key].weight : 1
6297
6298        nScore = score * weight
6299
6300        if (weight !== 1) {
6301          bestScore = Math.min(bestScore, nScore)
6302        } else {
6303          totalScore += nScore
6304          output[j].nScore = nScore
6305        }
6306      }
6307
6308      if (bestScore === 1) {
6309        results[i].score = totalScore / scoreLen
6310      } else {
6311        results[i].score = bestScore
6312      }
6313
6314      if (this.options.verbose) log(results[i])
6315    }
6316  }
6317
6318  Fuse.prototype._sort = function () {
6319    var options = this.options
6320    if (options.shouldSort) {
6321      if (options.verbose) log('\n\nSorting....')
6322      this.results.sort(options.sortFn)
6323    }
6324  }
6325
6326  Fuse.prototype._format = function () {
6327    var options = this.options
6328    var getFn = options.getFn
6329    var finalOutput = []
6330    var item
6331    var i
6332    var len
6333    var results = this.results
6334    var replaceValue
6335    var getItemAtIndex
6336    var include = options.include
6337
6338    if (options.verbose) log('\n\nOutput:\n\n', results)
6339
6340    // Helper function, here for speed-up, which replaces the item with its value,
6341    // if the options specifies it,
6342    replaceValue = options.id ? function (index) {
6343      results[index].item = getFn(results[index].item, options.id, [])[0]
6344    } : function () {}
6345
6346    getItemAtIndex = function (index) {
6347      var record = results[index]
6348      var data
6349      var j
6350      var output
6351      var _item
6352      var _result
6353
6354      // If `include` has values, put the item in the result
6355      if (include.length > 0) {
6356        data = {
6357          item: record.item
6358        }
6359        if (include.indexOf('matches') !== -1) {
6360          output = record.output
6361          data.matches = []
6362          for (j = 0; j < output.length; j++) {
6363            _item = output[j]
6364            _result = {
6365              indices: _item.matchedIndices
6366            }
6367            if (_item.key) {
6368              _result.key = _item.key
6369            }
6370            data.matches.push(_result)
6371          }
6372        }
6373
6374        if (include.indexOf('score') !== -1) {
6375          data.score = results[index].score
6376        }
6377
6378      } else {
6379        data = record.item
6380      }
6381
6382      return data
6383    }
6384
6385    // From the results, push into a new array only the item identifier (if specified)
6386    // of the entire item.  This is because we don't want to return the <results>,
6387    // since it contains other metadata
6388    for (i = 0, len = results.length; i < len; i++) {
6389      replaceValue(i)
6390      item = getItemAtIndex(i)
6391      finalOutput.push(item)
6392    }
6393
6394    return finalOutput
6395  }
6396
6397  // Helpers
6398
6399  function deepValue (obj, path, list) {
6400    var firstSegment
6401    var remaining
6402    var dotIndex
6403    var value
6404    var i
6405    var len
6406
6407    if (!path) {
6408      // If there's no path left, we've gotten to the object we care about.
6409      list.push(obj)
6410    } else {
6411      dotIndex = path.indexOf('.')
6412
6413      if (dotIndex !== -1) {
6414        firstSegment = path.slice(0, dotIndex)
6415        remaining = path.slice(dotIndex + 1)
6416      } else {
6417        firstSegment = path
6418      }
6419
6420      value = obj[firstSegment]
6421      if (value !== null && value !== undefined) {
6422        if (!remaining && (typeof value === 'string' || typeof value === 'number')) {
6423          list.push(value)
6424        } else if (isArray(value)) {
6425          // Search each item in the array.
6426          for (i = 0, len = value.length; i < len; i++) {
6427            deepValue(value[i], remaining, list)
6428          }
6429        } else if (remaining) {
6430          // An object. Recurse further.
6431          deepValue(value, remaining, list)
6432        }
6433      }
6434    }
6435
6436    return list
6437  }
6438
6439  function isArray (obj) {
6440    return Object.prototype.toString.call(obj) === '[object Array]'
6441  }
6442
6443  /**
6444   * Adapted from "Diff, Match and Patch", by Google
6445   *
6446   *   http://code.google.com/p/google-diff-match-patch/
6447   *
6448   * Modified by: Kirollos Risk <[email protected]>
6449   * -----------------------------------------------
6450   * Details: the algorithm and structure was modified to allow the creation of
6451   * <Searcher> instances with a <search> method which does the actual
6452   * bitap search. The <pattern> (the string that is searched for) is only defined
6453   * once per instance and thus it eliminates redundant re-creation when searching
6454   * over a list of strings.
6455   *
6456   * Licensed under the Apache License, Version 2.0 (the "License")
6457   * you may not use this file except in compliance with the License.
6458   *
6459   * @constructor
6460   */
6461  function BitapSearcher (pattern, options) {
6462    options = options || {}
6463    this.options = options
6464    this.options.location = options.location || BitapSearcher.defaultOptions.location
6465    this.options.distance = 'distance' in options ? options.distance : BitapSearcher.defaultOptions.distance
6466    this.options.threshold = 'threshold' in options ? options.threshold : BitapSearcher.defaultOptions.threshold
6467    this.options.maxPatternLength = options.maxPatternLength || BitapSearcher.defaultOptions.maxPatternLength
6468
6469    this.pattern = options.caseSensitive ? pattern : pattern.toLowerCase()
6470    this.patternLen = pattern.length
6471
6472    if (this.patternLen <= this.options.maxPatternLength) {
6473      this.matchmask = 1 << (this.patternLen - 1)
6474      this.patternAlphabet = this._calculatePatternAlphabet()
6475    }
6476  }
6477
6478  BitapSearcher.defaultOptions = {
6479    // Approximately where in the text is the pattern expected to be found?
6480    location: 0,
6481
6482    // Determines how close the match must be to the fuzzy location (specified above).
6483    // An exact letter match which is 'distance' characters away from the fuzzy location
6484    // would score as a complete mismatch. A distance of '0' requires the match be at
6485    // the exact location specified, a threshold of '1000' would require a perfect match
6486    // to be within 800 characters of the fuzzy location to be found using a 0.8 threshold.
6487    distance: 100,
6488
6489    // At what point does the match algorithm give up. A threshold of '0.0' requires a perfect match
6490    // (of both letters and location), a threshold of '1.0' would match anything.
6491    threshold: 0.6,
6492
6493    // Machine word size
6494    maxPatternLength: 32
6495  }
6496
6497  /**
6498   * Initialize the alphabet for the Bitap algorithm.
6499   * @return {Object} Hash of character locations.
6500   * @private
6501   */
6502  BitapSearcher.prototype._calculatePatternAlphabet = function () {
6503    var mask = {},
6504      i = 0
6505
6506    for (i = 0; i < this.patternLen; i++) {
6507      mask[this.pattern.charAt(i)] = 0
6508    }
6509
6510    for (i = 0; i < this.patternLen; i++) {
6511      mask[this.pattern.charAt(i)] |= 1 << (this.pattern.length - i - 1)
6512    }
6513
6514    return mask
6515  }
6516
6517  /**
6518   * Compute and return the score for a match with `e` errors and `x` location.
6519   * @param {number} errors Number of errors in match.
6520   * @param {number} location Location of match.
6521   * @return {number} Overall score for match (0.0 = good, 1.0 = bad).
6522   * @private
6523   */
6524  BitapSearcher.prototype._bitapScore = function (errors, location) {
6525    var accuracy = errors / this.patternLen,
6526      proximity = Math.abs(this.options.location - location)
6527
6528    if (!this.options.distance) {
6529      // Dodge divide by zero error.
6530      return proximity ? 1.0 : accuracy
6531    }
6532    return accuracy + (proximity / this.options.distance)
6533  }
6534
6535  /**
6536   * Compute and return the result of the search
6537   * @param {string} text The text to search in
6538   * @return {{isMatch: boolean, score: number}} Literal containing:
6539   *                          isMatch - Whether the text is a match or not
6540   *                          score - Overall score for the match
6541   * @public
6542   */
6543  BitapSearcher.prototype.search = function (text) {
6544    var options = this.options
6545    var i
6546    var j
6547    var textLen
6548    var findAllMatches
6549    var location
6550    var threshold
6551    var bestLoc
6552    var binMin
6553    var binMid
6554    var binMax
6555    var start, finish
6556    var bitArr
6557    var lastBitArr
6558    var charMatch
6559    var score
6560    var locations
6561    var matches
6562    var isMatched
6563    var matchMask
6564    var matchedIndices
6565    var matchesLen
6566    var match
6567
6568    text = options.caseSensitive ? text : text.toLowerCase()
6569
6570    if (this.pattern === text) {
6571      // Exact match
6572      return {
6573        isMatch: true,
6574        score: 0,
6575        matchedIndices: [[0, text.length - 1]]
6576      }
6577    }
6578
6579    // When pattern length is greater than the machine word length, just do a a regex comparison
6580    if (this.patternLen > options.maxPatternLength) {
6581      matches = text.match(new RegExp(this.pattern.replace(options.tokenSeparator, '|')))
6582      isMatched = !!matches
6583
6584      if (isMatched) {
6585        matchedIndices = []
6586        for (i = 0, matchesLen = matches.length; i < matchesLen; i++) {
6587          match = matches[i]
6588          matchedIndices.push([text.indexOf(match), match.length - 1])
6589        }
6590      }
6591
6592      return {
6593        isMatch: isMatched,
6594        // TODO: revisit this score
6595        score: isMatched ? 0.5 : 1,
6596        matchedIndices: matchedIndices
6597      }
6598    }
6599
6600    findAllMatches = options.findAllMatches
6601
6602    location = options.location
6603    // Set starting location at beginning text and initialize the alphabet.
6604    textLen = text.length
6605    // Highest score beyond which we give up.
6606    threshold = options.threshold
6607    // Is there a nearby exact match? (speedup)
6608    bestLoc = text.indexOf(this.pattern, location)
6609
6610    // a mask of the matches
6611    matchMask = []
6612    for (i = 0; i < textLen; i++) {
6613      matchMask[i] = 0
6614    }
6615
6616    if (bestLoc != -1) {
6617      threshold = Math.min(this._bitapScore(0, bestLoc), threshold)
6618      // What about in the other direction? (speed up)
6619      bestLoc = text.lastIndexOf(this.pattern, location + this.patternLen)
6620
6621      if (bestLoc != -1) {
6622        threshold = Math.min(this._bitapScore(0, bestLoc), threshold)
6623      }
6624    }
6625
6626    bestLoc = -1
6627    score = 1
6628    locations = []
6629    binMax = this.patternLen + textLen
6630
6631    for (i = 0; i < this.patternLen; i++) {
6632      // Scan for the best match; each iteration allows for one more error.
6633      // Run a binary search to determine how far from the match location we can stray
6634      // at this error level.
6635      binMin = 0
6636      binMid = binMax
6637      while (binMin < binMid) {
6638        if (this._bitapScore(i, location + binMid) <= threshold) {
6639          binMin = binMid
6640        } else {
6641          binMax = binMid
6642        }
6643        binMid = Math.floor((binMax - binMin) / 2 + binMin)
6644      }
6645
6646      // Use the result from this iteration as the maximum for the next.
6647      binMax = binMid
6648      start = Math.max(1, location - binMid + 1)
6649      if (findAllMatches) {
6650        finish = textLen;
6651      } else {
6652        finish = Math.min(location + binMid, textLen) + this.patternLen
6653      }
6654
6655      // Initialize the bit array
6656      bitArr = Array(finish + 2)
6657
6658      bitArr[finish + 1] = (1 << i) - 1
6659
6660      for (j = finish; j >= start; j--) {
6661        charMatch = this.patternAlphabet[text.charAt(j - 1)]
6662
6663        if (charMatch) {
6664          matchMask[j - 1] = 1
6665        }
6666
6667        if (i === 0) {
6668          // First pass: exact match.
6669          bitArr[j] = ((bitArr[j + 1] << 1) | 1) & charMatch
6670        } else {
6671          // Subsequent passes: fuzzy match.
6672          bitArr[j] = ((bitArr[j + 1] << 1) | 1) & charMatch | (((lastBitArr[j + 1] | lastBitArr[j]) << 1) | 1) | lastBitArr[j + 1]
6673        }
6674        if (bitArr[j] & this.matchmask) {
6675          score = this._bitapScore(i, j - 1)
6676
6677          // This match will almost certainly be better than any existing match.
6678          // But check anyway.
6679          if (score <= threshold) {
6680            // Indeed it is
6681            threshold = score
6682            bestLoc = j - 1
6683            locations.push(bestLoc)
6684
6685            if (bestLoc > location) {
6686              // When passing loc, don't exceed our current distance from loc.
6687              start = Math.max(1, 2 * location - bestLoc)
6688            } else {
6689              // Already passed loc, downhill from here on in.
6690              break
6691            }
6692          }
6693        }
6694      }
6695
6696      // No hope for a (better) match at greater error levels.
6697      if (this._bitapScore(i + 1, location) > threshold) {
6698        break
6699      }
6700      lastBitArr = bitArr
6701    }
6702
6703    matchedIndices = this._getMatchedIndices(matchMask)
6704
6705    // Count exact matches (those with a score of 0) to be "almost" exact
6706    return {
6707      isMatch: bestLoc >= 0,
6708      score: score === 0 ? 0.001 : score,
6709      matchedIndices: matchedIndices
6710    }
6711  }
6712
6713  BitapSearcher.prototype._getMatchedIndices = function (matchMask) {
6714    var matchedIndices = []
6715    var start = -1
6716    var end = -1
6717    var i = 0
6718    var match
6719    var len = matchMask.length
6720    for (; i < len; i++) {
6721      match = matchMask[i]
6722      if (match && start === -1) {
6723        start = i
6724      } else if (!match && start !== -1) {
6725        end = i - 1
6726        if ((end - start) + 1 >= this.options.minMatchCharLength) {
6727            matchedIndices.push([start, end])
6728        }
6729        start = -1
6730      }
6731    }
6732    if (matchMask[i - 1]) {
6733      if ((i-1 - start) + 1 >= this.options.minMatchCharLength) {
6734        matchedIndices.push([start, i - 1])
6735      }
6736    }
6737    return matchedIndices
6738  }
6739
6740  // Export to Common JS Loader
6741  if (typeof exports === 'object') {
6742    // Node. Does not work with strict CommonJS, but
6743    // only CommonJS-like environments that support module.exports,
6744    // like Node.
6745    module.exports = Fuse
6746  } else if (typeof define === 'function' && define.amd) {
6747    // AMD. Register as an anonymous module.
6748    define(function () {
6749      return Fuse
6750    })
6751  } else {
6752    // Browser globals (root is window)
6753    global.Fuse = Fuse
6754  }
6755
6756})(this);
6757
6758/*!
6759 * ngTagsInput v3.0.0
6760 * http://mbenford.github.io/ngTagsInput
6761 *
6762 * Copyright (c) 2013-2015 Michael Benford
6763 * License: MIT
6764 *
6765 * Generated at 2015-07-13 02:08:11 -0300
6766 */
6767(function() {
6768'use strict';
6769
6770var KEYS = {
6771    backspace: 8,
6772    tab: 9,
6773    enter: 13,
6774    escape: 27,
6775    space: 32,
6776    up: 38,
6777    down: 40,
6778    left: 37,
6779    right: 39,
6780    delete: 46,
6781    comma: 188
6782};
6783
6784var MAX_SAFE_INTEGER = 9007199254740991;
6785var SUPPORTED_INPUT_TYPES = ['text', 'email', 'url'];
6786
6787var tagsInput = angular.module('ngTagsInput', []);
6788
6789/**
6790 * @ngdoc directive
6791 * @name tagsInput
6792 * @module ngTagsInput
6793 *
6794 * @description
6795 * Renders an input box with tag editing support.
6796 *
6797 * @param {string} ngModel Assignable Angular expression to data-bind to.
6798 * @param {string=} [template=NA] URL or id of a custom template for rendering each tag.
6799 * @param {string=} [displayProperty=text] Property to be rendered as the tag label.
6800 * @param {string=} [keyProperty=text] Property to be used as a unique identifier for the tag.
6801 * @param {string=} [type=text] Type of the input element. Only 'text', 'email' and 'url' are supported values.
6802 * @param {string=} [text=NA] Assignable Angular expression for data-binding to the element's text.
6803 * @param {number=} tabindex Tab order of the control.
6804 * @param {string=} [placeholder=Add a tag] Placeholder text for the control.
6805 * @param {number=} [minLength=3] Minimum length for a new tag.
6806 * @param {number=} [maxLength=MAX_SAFE_INTEGER] Maximum length allowed for a new tag.
6807 * @param {number=} [minTags=0] Sets minTags validation error key if the number of tags added is less than minTags.
6808 * @param {number=} [maxTags=MAX_SAFE_INTEGER] Sets maxTags validation error key if the number of tags added is greater
6809 *    than maxTags.
6810 * @param {boolean=} [allowLeftoverText=false] Sets leftoverText validation error key if there is any leftover text in
6811 *    the input element when the directive loses focus.
6812 * @param {string=} [removeTagSymbol=×] (Obsolete) Symbol character for the remove tag button.
6813 * @param {boolean=} [addOnEnter=true] Flag indicating that a new tag will be added on pressing the ENTER key.
6814 * @param {boolean=} [addOnSpace=false] Flag indicating that a new tag will be added on pressing the SPACE key.
6815 * @param {boolean=} [addOnComma=true] Flag indicating that a new tag will be added on pressing the COMMA key.
6816 * @param {boolean=} [addOnBlur=true] Flag indicating that a new tag will be added when the input field loses focus.
6817 * @param {boolean=} [addOnPaste=false] Flag indicating that the text pasted into the input field will be split into tags.
6818 * @param {string=} [pasteSplitPattern=,] Regular expression used to split the pasted text into tags.
6819 * @param {boolean=} [replaceSpacesWithDashes=true] Flag indicating that spaces will be replaced with dashes.
6820 * @param {string=} [allowedTagsPattern=.+] Regular expression that determines whether a new tag is valid.
6821 * @param {boolean=} [enableEditingLastTag=false] Flag indicating that the last tag will be moved back into the new tag
6822 *    input box instead of being removed when the backspace key is pressed and the input box is empty.
6823 * @param {boolean=} [addFromAutocompleteOnly=false] Flag indicating that only tags coming from the autocomplete list
6824 *    will be allowed. When this flag is true, addOnEnter, addOnComma, addOnSpace and addOnBlur values are ignored.
6825 * @param {boolean=} [spellcheck=true] Flag indicating whether the browser's spellcheck is enabled for the input field or not.
6826 * @param {expression=} [onTagAdding=NA] Expression to evaluate that will be invoked before adding a new tag. The new
6827 *    tag is available as $tag. This method must return either true or false. If false, the tag will not be added.
6828 * @param {expression=} [onTagAdded=NA] Expression to evaluate upon adding a new tag. The new tag is available as $tag.
6829 * @param {expression=}
6829 [onInvalidTag=NA] Expression to evaluate when a tag is invalid. The invalid tag is available as $tag.
6830 * @param {expression=} [onTagRemoving=NA] Expression to evaluate that will be invoked before removing a tag. The tag
6831 *    is available as $tag. This method must return either true or false. If false, the tag will not be removed.
6832 * @param {expression=} [onTagRemoved=NA] Expression to evaluate upon removing an existing tag. The removed tag is
6833 *    available as $tag.
6834 * @param {expression=} [onTagClicked=NA] Expression to evaluate upon clicking an existing tag. The clicked tag is available as $tag.
6835 */
6836tagsInput.directive('tagsInput', ["$timeout", "$document", "$window", "tagsInputConfig", "tiUtil", function($timeout, $document, $window, tagsInputConfig, tiUtil) {
6837    function TagList(options, events, onTagAdding, onTagRemoving) {
6838        var self = {}, getTagText, setTagText, tagIsValid;
6839
6840        getTagText = function(tag) {
6841            return tiUtil.safeToString(tag[options.displayProperty]);
6842        };
6843
6844        setTagText = function(tag, text) {
6845            tag[options.displayProperty] = text;
6846        };
6847
6848        tagIsValid = function(tag) {
6849            var tagText = getTagText(tag);
6850
6851            return tagText &&
6852                   tagText.length >= options.minLength &&
6853                   tagText.length <= options.maxLength &&
6854                   options.allowedTagsPattern.test(tagText) &&
6855                   !tiUtil.findInObjectArray(self.items, tag, options.keyProperty || options.displayProperty) &&
6856                   onTagAdding({ $tag: tag });
6857        };
6858
6859        self.items = [];
6860
6861        self.addText = function(text) {
6862            var tag = {};
6863            setTagText(tag, text);
6864            return self.add(tag);
6865        };
6866
6867        self.add = function(tag) {
6868            var tagText = getTagText(tag);
6869
6870            if (options.replaceSpacesWithDashes) {
6871                tagText = tiUtil.replaceSpacesWithDashes(tagText);
6872            }
6873
6874            setTagText(tag, tagText);
6875
6876            if (tagIsValid(tag)) {
6877                self.items.push(tag);
6878                events.trigger('tag-added', { $tag: tag });
6879            }
6880            else if (tagText) {
6881                events.trigger('invalid-tag', { $tag: tag });
6882            }
6883
6884            return tag;
6885        };
6886
6887        self.remove = function(index) {
6888            var tag = self.items[index];
6889
6890            if (onTagRemoving({ $tag: tag }))  {
6891                self.items.splice(index, 1);
6892                self.clearSelection();
6893                events.trigger('tag-removed', { $tag: tag });
6894                return tag;
6895            }
6896        };
6897
6898        self.select = function(index) {
6899            if (index < 0) {
6900                index = self.items.length - 1;
6901            }
6902            else if (index >= self.items.length) {
6903                index = 0;
6904            }
6905
6906            self.index = index;
6907            self.selected = self.items[index];
6908        };
6909
6910        self.selectPrior = function() {
6911            self.select(--self.index);
6912        };
6913
6914        self.selectNext = function() {
6915            self.select(++self.index);
6916        };
6917
6918        self.removeSelected = function() {
6919            return self.remove(self.index);
6920        };
6921
6922        self.clearSelection = function() {
6923            self.selected = null;
6924            self.index = -1;
6925        };
6926
6927        self.clearSelection();
6928
6929        return self;
6930    }
6931
6932    function validateType(type) {
6933        return SUPPORTED_INPUT_TYPES.indexOf(type) !== -1;
6934    }
6935
6936    return {
6937        restrict: 'E',
6938        require: 'ngModel',
6939        scope: {
6940            tags: '=ngModel',
6941            text: '=?',
6942            onTagAdding: '&',
6943            onTagAdded: '&',
6944            onInvalidTag: '&',
6945            onTagRemoving: '&',
6946            onTagRemoved: '&',
6947            onTagClicked: '&',
6948            attributeList: '='
6949        },
6950        replace: false,
6951        transclude: true,
6952        templateUrl: 'ngTagsInput/tags-input.html',
6953        controller: ["$scope", "$attrs", "$element", function($scope, $attrs, $element) {
6954            $scope.events = tiUtil.simplePubSub();
6955
6956            tagsInputConfig.load('tagsInput', $scope, $attrs, {
6957                template: [String, 'ngTagsInput/tag-item.html'],
6958                type: [String, 'text', validateType],
6959                placeholder: [String, 'Add a tag'],
6960                tabindex: [Number, null],
6961                removeTagSymbol: [String, String.fromCharCode(215)],
6962                replaceSpacesWithDashes: [Boolean, true],
6963                minLength: [Number, 3],
6964                maxLength: [Number, MAX_SAFE_INTEGER],
6965                addOnEnter: [Boolean, true],
6966                addOnSpace: [Boolean, false],
6967                addOnComma: [Boolean, true],
6968                addOnBlur: [Boolean, true],
6969                addOnPaste: [Boolean, false],
6970                pasteSplitPattern: [RegExp, /,/],
6971                allowedTagsPattern: [RegExp, /.+/],
6972                enableEditingLastTag: [Boolean, false],
6973                minTags: [Number, 0],
6974                maxTags: [Number, MAX_SAFE_INTEGER],
6975                displayProperty: [String, 'text'],
6976                keyProperty: [String, ''],
6977                allowLeftoverText: [Boolean, false],
6978                addFromAutocompleteOnly: [Boolean, false],
6979                spellcheck: [Boolean, true]
6980            });
6981
6982            $scope.tagList = new TagList($scope.options, $scope.events,
6983                tiUtil.handleUndefinedResult($scope.onTagAdding, true),
6984                tiUtil.handleUndefinedResult($scope.onTagRemoving, true));
6985
6986            this.registerAutocomplete = function() {
6987                var input = $element.find('input');
6988
6989                return {
6990                    addTag: function(tag) {
6991                        return $scope.tagList.add(tag);
6992                    },
6993                    focusInput: function() {
6994                        input[0].focus();
6995                    },
6996                    getTags: function() {
6997                        return $scope.tagList.items;
6998                    },
6999                    getCurrentTagText: function() {
7000                        return $scope.newTag.text();
7001                    },
7002                    getOptions: function() {
7003                        return $scope.options;
7004                    },
7005                    on: function(name, handler) {
7006                        $scope.events.on(name, handler);
7007                        return this;
7008                    }
7009                };
7010            };
7011
7012            this.registerTagItem = function() {
7013                return {
7014                    getOptions: function() {
7015                        return $scope.options;
7016                    },
7017                    removeTag: function(index) {
7018                        if ($scope.disabled) {
7019                            return;
7020                        }
7021                        $scope.tagList.remove(index);
7022                    }
7023                };
7024            };
7025        }],
7026        link: function(scope, element, attrs, ngModelCtrl) {
7027            var hotkeys = [KEYS.enter, KEYS.comma, KEYS.space, KEYS.backspace, KEYS.delete, KEYS.left, KEYS.right],
7028                tagList = scope.tagList,
7029                events = scope.events,
7030                options = scope.options,
7031                input = element.find('input'),
7032                validationOptions = ['minTags', 'maxTags', 'allowLeftoverText'],
7033                setElementValidity;
7034
7035            setElementValidity = function() {
7036                ngModelCtrl.$setValidity('maxTags', tagList.items.length <= options.maxTags);
7037                ngModelCtrl.$setValidity('minTags', tagList.items.length >= options.minTags);
7038                ngModelCtrl.$setValidity('leftoverText', scope.hasFocus || options.allowLeftoverText ? true : !scope.newTag.text());
7039            };
7040
7041            ngModelCtrl.$isEmpty = function(value) {
7042                return !value || !value.length;
7043            };
7044
7045            scope.newTag = {
7046                text: function(value) {
7047                    if (angular.isDefined(value)) {
7048                        scope.text = value;
7049                        events.trigger('input-change', value);
7050                    }
7051                    else {
7052                        return scope.text || '';
7053                    }
7054                },
7055                invalid: null
7056            };
7057
7058            scope.track = function(tag) {
7059                return tag[options.keyProperty || options.displayProperty];
7060            };
7061
7062            scope.$watch('tags', function(value) {
7063                if (value) {
7064                    tagList.items = tiUtil.makeObjectArray(value, options.displayProperty);
7065                    scope.tags = tagList.items;
7066                }
7067                else {
7068                    tagList.items = [];
7069                }
7070            });
7071
7072            scope.$watch('tags.length', function() {
7073                setElementValidity();
7074
7075                // ngModelController won't trigger validators when the model changes (because it's an array),
7076                // so we need to do it ourselves. Unfortunately this won't trigger any registered formatter.
7077                ngModelCtrl.$validate();
7078            });
7079
7080            attrs.$observe('disabled', function(value) {
7081                scope.disabled = value;
7082            });
7083
7084            scope.eventHandlers = {
7085                input: {
7086                    keydown: function($event) {
7087                        events.trigger('input-keydown', $event);
7088                    },
7089                    focus: function() {
7090                        if (scope.hasFocus) {
7091                            return;
7092                        }
7093
7094                        scope.hasFocus = true;
7095                        events.trigger('input-focus');
7096                    },
7097                    blur: function() {
7098                        $timeout(function() {
7099                            var activeElement = $document.prop('activeElement'),
7100                                lostFocusToBrowserWindow = activeElement === input[0],
7101                                lostFocusToChildElement = element[0].contains(activeElement);
7102
7103                            if (lostFocusToBrowserWindow || !lostFocusToChildElement) {
7104                                scope.hasFocus = false;
vendor: 14,422 bytes, lines 7105-7458
7105                                events.trigger('input-blur');
7106                            }
7107                        });
7108                    },
7109                    paste: function($event) {
7110                        $event.getTextData = function() {
7111                            var clipboardData = $event.clipboardData || ($event.originalEvent && $event.originalEvent.clipboardData);
7112                            return clipboardData ? clipboardData.getData('text/plain') : $window.clipboardData.getData('Text');
7113                        };
7114                        events.trigger('input-paste', $event);
7115                    }
7116                },
7117                host: {
7118                    click: function() {
7119                        if (scope.disabled) {
7120                            return;
7121                        }
7122                        input[0].focus();
7123                    }
7124                },
7125                tag: {
7126                    click: function(tag) {
7127                        events.trigger('tag-clicked', { $tag: tag });
7128                    }
7129                }
7130            };
7131
7132            events
7133                .on('tag-added', scope.onTagAdded)
7134                .on('invalid-tag', scope.onInvalidTag)
7135                .on('tag-removed', scope.onTagRemoved)
7136                .on('tag-clicked', scope.onTagClicked)
7137                .on('tag-added', function() {
7138                    scope.newTag.text('');
7139                })
7140                .on('tag-added tag-removed', function() {
7141                    scope.tags = tagList.items;
7142                    // Ideally we should be able call $setViewValue here and let it in turn call $setDirty and $validate
7143                    // automatically, but since the model is an array, $setViewValue does nothing and it's up to us to do it.
7144                    // Unfortunately this won't trigger any registered $parser and there's no safe way to do it.
7145                    ngModelCtrl.$setDirty();
7146                })
7147                .on('invalid-tag', function() {
7148                    scope.newTag.invalid = true;
7149                })
7150                .on('option-change', function(e) {
7151                    if (validationOptions.indexOf(e.name) !== -1) {
7152                        setElementValidity();
7153                    }
7154                })
7155                .on('input-change', function() {
7156                    tagList.clearSelection();
7157                    scope.newTag.invalid = null;
7158                })
7159                .on('input-focus', function() {
7160                    element.triggerHandler('focus');
7161                    ngModelCtrl.$setValidity('leftoverText', true);
7162                })
7163                .on('input-blur', function() {
7164                    if (options.addOnBlur && !options.addFromAutocompleteOnly) {
7165                        tagList.addText(scope.newTag.text());
7166                    }
7167                    element.triggerHandler('blur');
7168                    setElementValidity();
7169                })
7170                .on('input-keydown', function(event) {
7171                    var key = event.keyCode,
7172                        addKeys = {},
7173                        shouldAdd, shouldRemove, shouldSelect, shouldEditLastTag;
7174
7175                    if (tiUtil.isModifierOn(event) || hotkeys.indexOf(key) === -1) {
7176                        return;
7177                    }
7178
7179                    addKeys[KEYS.enter] = options.addOnEnter;
7180                    addKeys[KEYS.comma] = options.addOnComma;
7181                    addKeys[KEYS.space] = options.addOnSpace;
7182
7183                    shouldAdd = !options.addFromAutocompleteOnly && addKeys[key];
7184                    shouldRemove = (key === KEYS.backspace || key === KEYS.delete) && tagList.selected;
7185                    shouldEditLastTag = key === KEYS.backspace && scope.newTag.text().length === 0 && options.enableEditingLastTag;
7186                    shouldSelect = (key === KEYS.backspace || key === KEYS.left || key === KEYS.right) && scope.newTag.text().length === 0 && !options.enableEditingLastTag;
7187
7188                    if (shouldAdd) {
7189                        tagList.addText(scope.newTag.text());
7190                    }
7191                    else if (shouldEditLastTag) {
7192                        var tag;
7193
7194                        tagList.selectPrior();
7195                        tag = tagList.removeSelected();
7196
7197                        if (tag) {
7198                            scope.newTag.text(tag[options.displayProperty]);
7199                        }
7200                    }
7201                    else if (shouldRemove) {
7202                        tagList.removeSelected();
7203                    }
7204                    else if (shouldSelect) {
7205                        if (key === KEYS.left || key === KEYS.backspace) {
7206                            tagList.selectPrior();
7207                        }
7208                        else if (key === KEYS.right) {
7209                            tagList.selectNext();
7210                        }
7211                    }
7212
7213                    if (shouldAdd || shouldSelect || shouldRemove || shouldEditLastTag) {
7214                        event.preventDefault();
7215                    }
7216                })
7217                .on('input-paste', function(event) {
7218                    if (options.addOnPaste) {
7219                        var data = event.getTextData();
7220                        var tags = data.split(options.pasteSplitPattern);
7221
7222                        if (tags.length > 1) {
7223                            tags.forEach(function(tag) {
7224                                tagList.addText(tag);
7225                            });
7226                            event.preventDefault();
7227                        }
7228                    }
7229                });
7230        }
7231    };
7232}]);
7233
7234
7235/**
7236 * @ngdoc directive
7237 * @name tiTagItem
7238 * @module ngTagsInput
7239 *
7240 * @description
7241 * Represents a tag item. Used internally by the tagsInput directive.
7242 */
7243tagsInput.directive('tiTagItem', ["tiUtil", function(tiUtil) {
7244    return {
7245        restrict: 'E',
7246        require: '^tagsInput',
7247        template: '<ng-include src="$$template"></ng-include>',
7248        scope: { data: '=' },
7249        link: function(scope, element, attrs, tagsInputCtrl) {
7250            var tagsInput = tagsInputCtrl.registerTagItem(),
7251                options = tagsInput.getOptions();
7252
7253            scope.$$template = options.template;
7254            scope.$$removeTagSymbol = options.removeTagSymbol;
7255            scope.$getDisplayText = function() {
7256                return tiUtil.safeToString(scope.data[options.displayProperty]);
7257            };
7258            scope.$removeTag = function() {
7259                tagsInput.removeTag(scope.$index);
7260            };
7261
7262            scope.$watch('$parent.$index', function(value) {
7263                scope.$index = value;
7264            });
7265        }
7266    };
7267}]);
7268
7269
7270/**
7271 * @ngdoc directive
7272 * @name autoComplete
7273 * @module ngTagsInput
7274 *
7275 * @description
7276 * Provides autocomplete support for the tagsInput directive.
7277 *
7278 * @param {expression} source Expression to evaluate upon changing the input content. The input value is available as
7279 *    $query. The result of the expression must be a promise that eventually resolves to an array of strings.
7280 * @param {string=} [template=NA] URL or id of a custom template for rendering each element of the autocomplete list.
7281 * @param {string=} [displayProperty=tagsInput.displayText] Property to be rendered as the autocomplete label.
7282 * @param {number=} [debounceDelay=100] Amount of time, in milliseconds, to wait before evaluating the expression in
7283 *    the source option after the last keystroke.
7284 * @param {number=} [minLength=3] Minimum number of characters that must be entered before evaluating the expression
7285 *    in the source option.
7286 * @param {boolean=} [highlightMatchedText=true] Flag indicating that the matched text will be highlighted in the
7287 *    suggestions list.
7288 * @param {number=} [maxResultsToShow=10] Maximum number of results to be displayed at a time.
7289 * @param {boolean=} [loadOnDownArrow=false] Flag indicating that the source option will be evaluated when the down arrow
7290 *    key is pressed and the suggestion list is closed. The current input value is available as $query.
7291 * @param {boolean=} [loadOnEmpty=false] Flag indicating that the source option will be evaluated when the input content
7292 *    becomes empty. The $query variable will be passed to the expression as an empty string.
7293 * @param {boolean=} [loadOnFocus=false] Flag indicating that the source option will be evaluated when the input element
7294 *    gains focus. The current input value is available as $query.
7295 * @param {boolean=} [selectFirstMatch=true] Flag indicating that the first match will be automatically selected once
7296 *    the suggestion list is shown.
7297 */
7298tagsInput.directive('autoComplete', ["$document", "$timeout", "$sce", "$q", "tagsInputConfig", "tiUtil", function($document, $timeout, $sce, $q, tagsInputConfig, tiUtil) {
7299    function SuggestionList(loadFn, options, events) {
7300        var self = {}, getDifference, lastPromise, getTagId;
7301
7302        getTagId = function() {
7303            return options.tagsInput.keyProperty || options.tagsInput.displayProperty;
7304        };
7305
7306        getDifference = function(array1, array2) {
7307            return array1.filter(function(item) {
7308                return !tiUtil.findInObjectArray(array2, item, getTagId(), function(a, b) {
7309                    if (options.tagsInput.replaceSpacesWithDashes) {
7310                        a = tiUtil.replaceSpacesWithDashes(a);
7311                        b = tiUtil.replaceSpacesWithDashes(b);
7312                    }
7313                    return tiUtil.defaultComparer(a, b);
7314                });
7315            });
7316        };
7317
7318        self.reset = function() {
7319            lastPromise = null;
7320
7321            self.items = [];
7322            self.visible = false;
7323            self.index = -1;
7324            self.selected = null;
7325            self.query = null;
7326        };
7327        self.show = function() {
7328            if (options.selectFirstMatch) {
7329                self.select(0);
7330            }
7331            else {
7332                self.selected = null;
7333            }
7334            self.visible = true;
7335        };
7336        self.load = tiUtil.debounce(function(query, tags) {
7337            self.query = query;
7338
7339            var promise = $q.when(loadFn({ $query: query }));
7340            lastPromise = promise;
7341
7342            promise.then(function(items) {
7343                if (promise !== lastPromise) {
7344                    return;
7345                }
7346
7347                items = tiUtil.makeObjectArray(items.data || items, getTagId());
7348                items = getDifference(items, tags);
7349                self.items = items.slice(0, options.maxResultsToShow);
7350
7351                if (self.items.length > 0) {
7352                    self.show();
7353                }
7354                else {
7355                    self.reset();
7356                    self.visible = true;
7357                }
7358            });
7359        }, options.debounceDelay);
7360
7361        self.selectNext = function() {
7362            self.select(++self.index);
7363        };
7364        self.selectPrior = function() {
7365            self.select(--self.index);
7366        };
7367        self.select = function(index) {
7368            if (index < 0) {
7369                index = self.items.length - 1;
7370            }
7371            else if (index >= self.items.length) {
7372                index = 0;
7373            }
7374            self.index = index;
7375            self.selected = self.items[index];
7376            events.trigger('suggestion-selected', index);
7377        };
7378
7379        self.reset();
7380
7381        return self;
7382    }
7383
7384    function scrollToElement(root, index) {
7385        var element = root.find('li').eq(index),
7386            parent = element.parent(),
7387            elementTop = element.prop('offsetTop'),
7388            elementHeight = element.prop('offsetHeight'),
7389            parentHeight = parent.prop('clientHeight'),
7390            parentScrollTop = parent.prop('scrollTop');
7391
7392        if (elementTop < parentScrollTop) {
7393            parent.prop('scrollTop', elementTop);
7394        }
7395        else if (elementTop + elementHeight > parentHeight + parentScrollTop) {
7396            parent.prop('scrollTop', elementTop + elementHeight - parentHeight);
7397        }
7398    }
7399
7400    return {
7401        restrict: 'E',
7402        require: '^tagsInput',
7403        scope: { 
7404            source: '&', 
7405            filter: '=',
7406            attributeList: '='
7407        },
7408        templateUrl: 'ngTagsInput/auto-complete.html',
7409        controller: ["$scope", "$element", "$attrs", function($scope, $element, $attrs) {
7410            $scope.events = tiUtil.simplePubSub();
7411
7412            tagsInputConfig.load('autoComplete', $scope, $attrs, {
7413                template: [String, 'ngTagsInput/auto-complete-match.html'],
7414                debounceDelay: [Number, 100],
7415                minLength: [Number, 3],
7416                highlightMatchedText: [Boolean, true],
7417                maxResultsToShow: [Number, 10],
7418                loadOnDownArrow: [Boolean, false],
7419                loadOnEmpty: [Boolean, false],
7420                loadOnFocus: [Boolean, false],
7421                selectFirstMatch: [Boolean, true],
7422                displayProperty: [String, '']
7423            });
7424
7425            $scope.suggestionList = new SuggestionList($scope.source, $scope.options, $scope.events);
7426
7427            this.registerAutocompleteMatch = function() {
7428                return {
7429                    getOptions: function() {
7430                        return $scope.options;
7431                    },
7432                    getQuery: function() {
7433                        return $scope.suggestionList.query;
7434                    }
7435                };
7436            };
7437        }],
7438        link: function(scope, element, attrs, tagsInputCtrl) {
7439            var hotkeys = [KEYS.enter, KEYS.tab, KEYS.escape, KEYS.up, KEYS.down],
7440                suggestionList = scope.suggestionList,
7441                tagsInput = tagsInputCtrl.registerAutocomplete(),
7442                options = scope.options,
7443                events = scope.events,
7444                shouldLoadSuggestions;
7445
7446            options.tagsInput = tagsInput.getOptions();
7447
7448            shouldLoadSuggestions = function(value) {
7449                return value && value.length >= options.minLength || !value && options.loadOnEmpty;
7450            };
7451
7452            scope.addSuggestionByIndex = function(index) {
7453                suggestionList.select(index);
7454                scope.addSuggestion();
7455            };
7456
7457            scope.addSuggestion = function() {
7458                var added = false;
7459
7460                if (suggestionList.selected) {
7461                    suggestionList.selected.text = '\'' + suggestionList.selected.text + '\'';
7462                    tagsInput.addTag(angular.copy(suggestionList.selected));
7463                    suggestionList.reset();
7464                    tagsInput.focusInput();
7465
7466                    added = true;
7467                }
7468                return added;
7469            };
7470
7471            scope.track = function(item) {
7472                return item[options.tagsInput.keyProperty || options.tagsInput.displayProperty];
7473            };
7474
7475            tagsInput
7476                .on('tag-added tag-removed invalid-tag input-blur', function() {
7477                    suggestionList.reset();
7478                })
7479                .on('input-change', function(value) {
7480                    if (shouldLoadSuggestions(value)) {
7481                        suggestionList.load(value, tagsInput.getTags());
7482                    }
7483                    else {
7484                        suggestionList.reset();
7485                    }
7486                })
7487                .on('input-focus', function() {
7488                    var value = tagsInput.getCurrentTagText();
7489                    if (options.loadOnFocus && shouldLoadSuggestions(value)) {
7490                        suggestionList.load(value, tagsInput.getTags());
7491                    }
7492                })
7493                .on('input-keydown', function(event) {
7494                    var key = event.keyCode,
7495                        handled = false;
7496
7497                    if (tiUtil.isModifierOn(event) || hotkeys.indexOf(key) === -1) {
7498                        return;
7499                    }
7500
7501                    if (suggestionList.visible) {
7502
7503                        if (key === KEYS.down) {
7504                            suggestionList.selectNext();
7505                            handled = true;
7506                        }
7507                        else if (key === KEYS.up) {
7508                            suggestionList.selectPrior();
7509                            handled = true;
7510                        }
7511                        else if (key === KEYS.escape) {
7512                            suggestionList.reset();
7513                            handled = true;
7514                        }
7515                        else if (key === KEYS.enter || key === KEYS.tab) {
7516                            handled = scope.addSuggestion();
7517                        }
7518                    }
7519                    else {
7520                        if (key === KEYS.down && scope.options.loadOnDownArrow) {
7521                            suggestionList.load(tagsInput.getCurrentTagText(), tagsInput.getTags());
7522                            handled = true;
7523                        }
7524                    }
7525
7526                    if (handled) {
7527                        event.preventDefault();
7528                        event.stopImmediatePropagation();
7529                        return false;
7530                    }
7531                });
7532
7533            events.on('suggestion-selected', function(index) {
7534                scrollToElement(element, index);
7535            });
7536
7537            var initializing = true;
7538            scope.$watch('filter.selected', function() {
7539                if (initializing) {
7540                    $timeout(function() { initializing = false; });
7541                } else {
7542                    $timeout(function() {
7543                        suggestionList.load(tagsInput.getCurrentTagText(), tagsInput.getTags());
7544                    });
7545                }
7546            });
7547        }
7548    };
7549}]);
7550
7551
7552/**
7553 * @ngdoc directive
7554 * @name tiAutocompleteMatch
7555 * @module ngTagsInput
7556 *
7557 * @description
7558 * Represents an autocomplete match. Used internally by the autoComplete directive.
7559 */
7560tagsInput.directive('tiAutocompleteMatch', ["$sce", "tiUtil", function($sce, tiUtil) {
7561    return {
7562        restrict: 'E',
7563        require: '^autoComplete',
7564        template: '<ng-include src="$$template"></ng-include>',
7565        scope: { data: '='
vendor: 7,646 bytes, lines 7565-7791
7565 },
7566        link: function(scope, element, attrs, autoCompleteCtrl) {
7567            var autoComplete = autoCompleteCtrl.registerAutocompleteMatch(),
7568                options = autoComplete.getOptions();
7569
7570            scope.$$template = options.template;
7571            scope.$index = scope.$parent.$index;
7572
7573            scope.$highlight = function(text) {
7574                if (options.highlightMatchedText) {
7575                    text = tiUtil.safeHighlight(text, autoComplete.getQuery());
7576                }
7577                return $sce.trustAsHtml(text);
7578            };
7579            scope.$getDisplayText =  function() {
7580                return tiUtil.safeToString(scope.data[options.displayProperty || options.tagsInput.displayProperty]);
7581            };
7582        }
7583    };
7584}]);
7585
7586
7587/**
7588 * @ngdoc directive
7589 * @name tiTranscludeAppend
7590 * @module ngTagsInput
7591 *
7592 * @description
7593 * Re-creates the old behavior of ng-transclude. Used internally by tagsInput directive.
7594 */
7595tagsInput.directive('tiTranscludeAppend', function() {
7596    return function(scope, element, attrs, ctrl, transcludeFn) {
7597        transcludeFn(function(clone) {
7598            element.append(clone);
7599        });
7600    };
7601});
7602
7603/**
7604 * @ngdoc directive
7605 * @name tiAutosize
7606 * @module ngTagsInput
7607 *
7608 * @description
7609 * Automatically sets the input's width so its content is always visible. Used internally by tagsInput directive.
7610 */
7611tagsInput.directive('tiAutosize', ["tagsInputConfig", function(tagsInputConfig) {
7612    return {
7613        restrict: 'A',
7614        require: 'ngModel',
7615        link: function(scope, element, attrs, ctrl) {
7616            var threshold = tagsInputConfig.getTextAutosizeThreshold(),
7617                span, resize;
7618
7619            span = angular.element('<span class="input"></span>');
7620            span.css('display', 'none')
7621                .css('visibility', 'hidden')
7622                .css('width', 'auto')
7623                .css('white-space', 'pre');
7624
7625            element.parent().append(span);
7626
7627            resize = function(originalValue) {
7628                var value = originalValue, width;
7629
7630                if (angular.isString(value) && value.length === 0) {
7631                    value = attrs.placeholder;
7632                }
7633
7634                if (value) {
7635                    span.text(value);
7636                    span.css('display', '');
7637                    width = span.prop('offsetWidth');
7638                    span.css('display', 'none');
7639                }
7640
7641                element.css('width', width ? width + threshold + 'px' : '');
7642
7643                return originalValue;
7644            };
7645
7646            ctrl.$parsers.unshift(resize);
7647            ctrl.$formatters.unshift(resize);
7648
7649            attrs.$observe('placeholder', function(value) {
7650                if (!ctrl.$modelValue) {
7651                    resize(value);
7652                }
7653            });
7654        }
7655    };
7656}]);
7657
7658/**
7659 * @ngdoc directive
7660 * @name tiBindAttrs
7661 * @module ngTagsInput
7662 *
7663 * @description
7664 * Binds attributes to expressions. Used internally by tagsInput directive.
7665 */
7666tagsInput.directive('tiBindAttrs', function() {
7667    return function(scope, element, attrs) {
7668        scope.$watch(attrs.tiBindAttrs, function(value) {
7669            angular.forEach(value, function(value, key) {
7670                attrs.$set(key, value);
7671            });
7672        }, true);
7673    };
7674});
7675
7676/**
7677 * @ngdoc service
7678 * @name tagsInputConfig
7679 * @module ngTagsInput
7680 *
7681 * @description
7682 * Sets global configuration settings for both tagsInput and autoComplete directives. It's also used internally to parse and
7683 *  initialize options from HTML attributes.
7684 */
7685tagsInput.provider('tagsInputConfig', function() {
7686    var globalDefaults = {},
7687        interpolationStatus = {},
7688        autosizeThreshold = 3;
7689
7690    /**
7691     * @ngdoc method
7692     * @name tagsInputConfig#setDefaults
7693     * @description Sets the default configuration option for a directive.
7694     *
7695     * @param {string} directive Name of the directive to be configured. Must be either 'tagsInput' or 'autoComplete'.
7696     * @param {object} defaults Object containing options and their values.
7697     *
7698     * @returns {object} The service itself for chaining purposes.
7699     */
7700    this.setDefaults = function(directive, defaults) {
7701        globalDefaults[directive] = defaults;
7702        return this;
7703    };
7704
7705    /**
7706     * @ngdoc method
7707     * @name tagsInputConfig#setActiveInterpolation
7708     * @description Sets active interpolation for a set of options.
7709     *
7710     * @param {string} directive Name of the directive to be configured. Must be either 'tagsInput' or 'autoComplete'.
7711     * @param {object} options Object containing which options should have interpolation turned on at all times.
7712     *
7713     * @returns {object} The service itself for chaining purposes.
7714     */
7715    this.setActiveInterpolation = function(directive, options) {
7716        interpolationStatus[directive] = options;
7717        return this;
7718    };
7719
7720    /**
7721     * @ngdoc method
7722     * @name tagsInputConfig#setTextAutosizeThreshold
7723     * @description Sets the threshold used by the tagsInput directive to re-size the inner input field element based on its contents.
7724     *
7725     * @param {number} threshold Threshold value, in pixels.
7726     *
7727     * @returns {object} The service itself for chaining purposes.
7728     */
7729    this.setTextAutosizeThreshold = function(threshold) {
7730        autosizeThreshold = threshold;
7731        return this;
7732    };
7733
7734    this.$get = ["$interpolate", function($interpolate) {
7735        var converters = {};
7736        converters[String] = function(value) { return value; };
7737        converters[Number] = function(value) { return parseInt(value, 10); };
7738        converters[Boolean] = function(value) { return value.toLowerCase() === 'true'; };
7739        converters[RegExp] = function(value) { return new RegExp(value); };
7740
7741        return {
7742            load: function(directive, scope, attrs, options) {
7743                var defaultValidator = function() { return true; };
7744
7745                scope.options = {};
7746
7747                angular.forEach(options, function(value, key) {
7748                    var type, localDefault, validator, converter, getDefault, updateValue;
7749
7750                    type = value[0];
7751                    localDefault = value[1];
7752                    validator = value[2] || defaultValidator;
7753                    converter = converters[type];
7754
7755                    getDefault = function() {
7756                        var globalValue = globalDefaults[directive] && globalDefaults[directive][key];
7757                        return angular.isDefined(globalValue) ? globalValue : localDefault;
7758                    };
7759
7760                    updateValue = function(value) {
7761                        scope.options[key] = value && validator(value) ? converter(value) : getDefault();
7762                    };
7763
7764                    if (interpolationStatus[directive] && interpolationStatus[directive][key]) {
7765                        attrs.$observe(key, function(value) {
7766                            updateValue(value);
7767                            scope.events.trigger('option-change', { name: key, newValue: value });
7768                        });
7769                    }
7770                    else {
7771                        updateValue(attrs[key] && $interpolate(attrs[key])(scope.$parent));
7772                    }
7773                });
7774            },
7775            getTextAutosizeThreshold: function() {
7776                return autosizeThreshold;
7777            }
7778        };
7779    }];
7780});
7781
7782
7783/***
7784 * @ngdoc service
7785 * @name tiUtil
7786 * @module ngTagsInput
7787 *
7788 * @description
7789 * Helper methods used internally by the directive. Should not be called directly from user code.
7790 */
7791tagsInput.factory('tiUtil', ["$timeout", 
vendor: 2,218 bytes, lines 7791-7865
7791function($timeout) {
7792    var self = {};
7793
7794    self.debounce = function(fn, delay) {
7795        var timeoutId;
7796        return function() {
7797            var args = arguments;
7798            $timeout.cancel(timeoutId);
7799            timeoutId = $timeout(function() { fn.apply(null, args); }, delay);
7800        };
7801    };
7802
7803    self.makeObjectArray = function(array, key) {
7804        if (!angular.isArray(array) || array.length === 0 || angular.isObject(array[0])) {
7805            return array;
7806        }
7807
7808        var newArray = [];
7809        array.forEach(function(item) {
7810            var obj = {};
7811            obj[key] = item;
7812            newArray.push(obj);
7813        });
7814        return newArray;
7815    };
7816
7817    self.findInObjectArray = function(array, obj, key, comparer) {
7818        var item = null;
7819        comparer = comparer || self.defaultComparer;
7820
7821        array.some(function(element) {
7822            if (comparer(element[key], obj[key])) {
7823                item = element;
7824                return true;
7825            }
7826        });
7827
7828        return item;
7829    };
7830
7831    self.defaultComparer = function(a, b) {
7832        // I'm aware of the internationalization issues regarding toLowerCase()
7833        // but I couldn't come up with a better solution right now
7834        return self.safeToString(a).toLowerCase() === self.safeToString(b).toLowerCase();
7835    };
7836
7837    self.safeHighlight = function(str, value) {
7838        if (!value) {
7839            return str;
7840        }
7841
7842        function escapeRegexChars(str) {
7843            return str.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
7844        }
7845
7846        str = self.encodeHTML(str);
7847        value = self.encodeHTML(value);
7848
7849        var expression = new RegExp('&[^;]+;|' + escapeRegexChars(value), 'gi');
7850        return str.replace(expression, function(match) {
7851            return match.toLowerCase() === value.toLowerCase() ? '<em>' + match + '</em>' : match;
7852        });
7853    };
7854
7855    self.safeToString = function(value) {
7856        return angular.isUndefined(value) || value == null ? '' : value.toString().trim();
7857    };
7858
7859    self.encodeHTML = function(value) {
7860        return self.safeToString(value)
7861            .replace(/&/g, '&amp;')
7862            .replace(/</g, '&lt;')
7863            .replace(/>/g, '&gt;');
7864    };
7865
7866    self.handleUndefinedResult = function(fn, valueIfUndefined) {
7867        return function() {
7868            var result = fn.apply(null, arguments);
7869            return angular.isUndefined(result) ? valueIfUndefined : result;
7870        };
7871    };
7872
7873    self.replaceSpacesWithDashes = function(str) {
7874        return self.safeToString(str).replace(/\s/g, '-');
7875    };
7876
7877    self.isModifierOn = function(event) {
7878        return event.shiftKey || event.ctrlKey || event.altKey || event.metaKey;
7879    };
7880
7881    self.simplePubSub = function() {
7882        var events = {};
7883        return {
7884            on: function(names, handler) {
7885                names.split(' ').forEach(function(name) {
7886                    if (!events[name]) {
7887                        events[name] = [];
7888                    }
7889                    events[name].push(handler);
7890                });
7891                return this;
7892            },
7893            trigger: function(name, args) {
7894                var handlers = events[name] || [];
7895                handlers.every(function(handler) {
7896                    return self.handleUndefinedResult(handler, true)(args);
7897                });
7898                return this;
7899            }
7900        };
7901    };
7902
7903    return self;
7904}]);
7905
7906/* HTML templates */
7907tagsInput.run(["$templateCache", function($templateCache) {
7908    $templateCache.put('ngTagsInput/tags-input.html',
7909    "<div class=\"host\" tabindex=\"-1\" ng-click=\"eventHandlers.host.click()\" ti-transclude-append><div class=\"tags\" ng-class=\"{focused: hasFocus}\"><ul class=\"tag-list\"><li class=\"tag-item\" ng-repeat=\"tag in tagList.items track by track(tag)\" ng-class=\"{ selected: tag == tagList.selected }\" ng-click=\"eventHandlers.tag.click(tag)\"><ti-tag-item data=\"::tag\"></ti-tag-item></li></ul><input class=\"input\" autocomplete=\"off\" ng-model=\"newTag.text\" ng-model-options=\"{getterSetter: true}\" ng-keydown=\"eventHandlers.input.keydown($event)\" ng-focus=\"eventHandlers.input.focus($event)\" ng-blur=\"eventHandlers.input.blur($event)\" ng-paste=\"eventHandlers.input.paste($event)\" ng-trim=\"false\" ng-class=\"{'invalid-tag': newTag.invalid}\" ng-disabled=\"disabled\" ti-bind-attrs=\"{type: options.type, placeholder: options.placeholder, tabindex: options.tabindex, spellcheck: options.spellcheck}\" ti-autosize></div></div>"
7910  );
7911
7912  $templateCache.put('ngTagsInput/tag-item.html',
7913    "<span ng-bind=\"$getDisplayText()\"></span> <a class=\"remove-button\" ng-click=\"$removeTag()\" ng-bind=\"::$$removeTagSymbol\"></a>"
7914  );
7915
7916  $templateCache.put('ngTagsInput/auto-complete.html',
7917    "<div class=\"autocomplete\" ng-if=\"suggestionList.visible\"><ul class=\"filter-list\"><li class=\"filter-item\" ng-class=\"{selected: $parent.filter.selected == filter}\" ng-repeat=\"filter in filter.list\" ng-click=\"$parent.filter.selected=filter\">{{attributeList[filter].label}}</li></ul><hr class=\"hr-slim\"><div class=\"clearfix\" style=\"height:10px;\"></div><ul class=\"suggestion-list\"><li class=\"suggestion-item\" ng-show=\"suggestionList.items.length < 1\">No suggestions</li></ul><ul class=\"suggestion-list\"><li class=\"suggestion-item\" ng-repeat=\"item in suggestionList.items track by track(item)\" ng-class=\"{selected: item == suggestionList.selected}\" ng-click=\"addSuggestionByIndex($index)\" ng-mouseenter=\"suggestionList.select($index)\"><ti-autocomplete-match data=\"::item\"></ti-autocomplete-match></li></ul></div>"
7918  );
7919
7920  $templateCache.put('ngTagsInput/auto-complete-match.html',
7921    "<span ng-bind-html=\"$highlight($getDisplayText())\"></span>"
7922  );
7923}]);
7924
7925}());
7926'use strict';
7927
7928angular.module('angularDjangoRegistrationAuthApp')
7929  .service('Validate', function Validate() {
7930    return {
7931        'message': {
7932            'minlength': 'This value is not long enough.',
7933            'maxlength': 'This value is too long.',
7934            'email': 'A properly formatted email address is required.',
7935            'required': 'This field is required.'
7936        },
7937        'more_messages': {
7938            'demo': {
7939                'required': 'Here is a sample alternative required message.'
7940            }
7941        },
7942        'check_more_messages': function(name,error){
7943            return (this.more_messages[name] || [])[error] || null;
7944        },
7945        validation_messages: function(field,form,error_bin){
7946            var messages = [];
7947            for(var e in form[field].$error){
7948                if(form[field].$error[e]){
7949                    var special_message = this.check_more_messages(field,e);
7950                    if(special_message){
7951                        messages.push(special_message);
7952                    }else if(this.message[e]){
7953                        messages.push(this.message[e]);
7954                    }else{
7955                        messages.push("Error: " + e)
7956                    }
7957                }
7958            }
7959            var deduped_messages = [];
7960            angular.forEach(messages, function(el, i){
7961                if(deduped_messages.indexOf(el) === -1) deduped_messages.push(el);
7962            });
7963            if(error_bin){
7964                error_bin[field] = deduped_messages;
7965            }
7966        },
7967        'form_validation': function(form,error_bin){
7968            for(var field in form){
7969                if(field.substr(0,1) != "$"){
7970                    this.validation_messages(field,form,error_bin);
7971                }
7972            }
7973        }
7974    }
7975});
7976angular.module('elixir_front')
7977.controller('AboutController', ['$scope', function($scope) {
7978	var vm = this;
7979	window.scrollTo(0, 0);
7980}]);
7981
7982
7983angular.module('elixir_front')
7984.directive('homeInfo', ['$state','$stateParams', function($state, $stateParams) {
7985	return {
7986		restrict: 'EA',
7987		templateUrl: 'components/about/aboutContact.html',
7988		scope: true,
7989		link: function(scope, element, attrs) {
7990			scope.name = attrs["name"];
7991			scope.initials = attrs["initials"];
7992			scope.role = attrs["role"];
7993			scope.image = attrs["image"];
7994
7995		}
7996	}
7997}]);
7998angular.module('elixir_front')
7999.controller('CommunityController', ['$scope', function($scope) {
8000	var vm = this;
8001    window.scrollTo(0, 0);
8002}]);
8003angular.module('elixir_front')
8004.directive('detailsBar', [function() {
8005	return {
8006		restrict: 'A',
8007		templateUrl: 'components/detailsBar/detailsBar.html',
8008		link: function(scope, element, attrs) {
8009			scope.showToolbar = scope.toState.name == "search";
8010			scope.$watch(function(){
8011				return scope.toState.name
8012			}, function(newVal, oldVal){
8013    			scope.showToolbar = (scope.toState.name == "search");
8014			}) 
8015		}
8016	}
8017}])
8018.controller('DetailsBarController', ['$rootScope', '$scope', '$state', '$stateParams', function($rootScope, $scope, $state, $stateParams) {
8019	var vm = this;
8020}]);
8021angular.module('elixir_front')
8022.controller('DomainBarController', ['$scope', 'djangoAuth', '$stateParams', '$timeout', 'Domain', function($scope, djangoAuth, $stateParams, $timeout, Domain) {
8023	// reference the service
8024	var vm = this;
8025	vm.Domain = Domain;
8026	
8027	$timeout(function() {
8028		Domain.load($stateParams['domain']);
8029	}, 500);
8030}])
8031.service('DomainConnection', ['$resource', function($resource){
8032	return $resource('/api/d/', null, {
8033		'query': {
8034			isArray: true,
8035			method:'GET'
8036		},
8037		'create': {
8038			method:'POST'
8039		}
8040	})
8041}])
8042.service('DomainDetailConnection', ['$resource', function($resource){
8043	return $resource('/api/d/:domain', {'domain': '@domain'}, {
8044		'query': {
8045			isArray:false,
8046			method:'GET'
8047		},
8048		'update': {
8049			method:'PUT'
8050		},
8051		'delete': {
8052			method:'DELETE'
8053		}
8054	})
8055}])
8056.service('Domain', ['DomainDetailConnection', function(DomainDetailConnection){
8057	var _this = this;
8058	this.current = {};
8059	this.loaded = false;
8060	this.set = function(domain) {
8061		_this.current = domain;
8062	}
8063	this.load = function(domain) {
8064		if (typeof domain != 'undefined') {
8065			var response = DomainDetailConnection.query({'domain':domain}, function(response) {
8066				_this.set(response.data);
8067				_this.loaded = true;
8068			});
8069		} else {
8070			_this.loaded = false;
8071		}
8072	}
8073	this.isLoaded = function() {
8074		return _this.loaded;
8075	}
8076	this.hasSubdomain = function() {
8077		return !_.isEmpty(_this.current);
8078	}
8079	this.hasTitle = function() {
8080		return _this.current.title !== undefined;
8081	}
8082	this.hasSubTitle = function() {
8083		return _this.current.sub_title !== undefined;
8084	}
8085	this.hasDescription = function() {
8086		return _this.current.description !== undefined;
8087	}
8088}]);
8089angular.module('elixir_front')
8090.service('DomainConnection', ['$resource', function($resource){
8091	return $resource('/api/d/', null, {
8092		'query': {
8093			isArray: true,
8094			method:'GET'
8095		},
8096		'create': {
8097			method:'POST'
8098		}
8099	})
8100}])
8101.service('DomainDetailConnection', ['$resource', function($resource){
8102	return $resource('/api/d/:domain', {'domain': '@domain'}, {
8103		'query': {
8104			isArray:false,
8105			method:'GET'
8106		},
8107		'update': {
8108			method:'PUT'
8109		},
8110		'delete': {
8111			method:'DELETE'
8112		}
8113	})
8114}])
8115.service('Domain', ['DomainDetailConnection', '$rootScope', function(DomainDetailConnection, $rootScope){
8116	var _this = this;
8117	this.current = {};
8118	this.loaded = false;
8119	this.set = function(domain) {
8120		_this.current = domain;
8121	}
8122	this.load = function(domain) {
8123		if (typeof domain != 'undefined') {
8124			var response = DomainDetailConnection.query({'domain':domain}, function(response) {
8125				_this.set(response.data);
8126				_this.loaded = true;
8127			});
8128		} else {
8129			_this.loaded = false;
8130		}
8131	}
8132	this.isLoaded = function() {
8133		return _this.loaded;
8134	}
8135	this.hasSubdomain = function() {
8136		return !_.isEmpty(_this.current);
8137	}
8138	this.hasTitle = function() {
8139		return _this.current.title !== undefined;
8140	}
8141	this.hasSubTitle = function() {
8142		return _this.current.sub_title !== undefined;
8143	}
8144	this.hasDescription = function() {
8145		return _this.current.description !== undefined;
8146	}
8147	this.unload = function() {
8148		if (this.isLoaded() == true) {
8149			this.current = {};
8150			this.loaded = false;
8151		}
8152	}
8153}]);
8154angular.module('elixir_front')
8155.controller('HomeController', ['$scope','$state', '$stateParams', function($scope, $state, $stateParams) {
8156	var vm = this;
8157	window.scrollTo(0, 0);
8158}]);
8159
8160	angular.module('elixir_front')
8161.directive('homeSearch', ['Query', 'ToolList', 'Highlighting', 'Attribute', '$state', '$stateParams', '$timeout', 'UsedTerms', '$q', 'filterFilter', 'Domain', '$rootScope', '$location', '$anchorScroll', function(Query, ToolList, Highlighting, Attribute, $state, $stateParams, $timeout, UsedTerms, $q, filterFilter, Domain, $rootScope, $location, $anchorScroll) {
8162	return {
8163		restrict: 'A',
8164		templateUrl: 'components/home/homeSearch.html',
8165		link: function(scope, element, attrs) {
8166			// reference the Query in the scope
8167			scope.Query = Query;
8168			scope.params = null;
8169			scope.lastParam = null;
8170			// reference the Attribute in the scope
8171			scope.Attribute = Attribute;
8172			scope.ToolList = ToolList;
8173
8174			var initializing = true
8175
8176			/* function updateQuery() {
8177				Query.current = [];
8178				for (var prop in $stateParams) {
8179				// iterate through all parameters, skip 'page'
8180				if ($stateParams.hasOwnProperty(prop)
8181					&& !_.includes(['page', 'sort', 'ord', 'domain'], prop)
8182					&& typeof $stateParams[prop] != 'undefined') {
8183					// Remove tool id from the query
8184					if (prop == 'q') {
8185						var list = $stateParams[prop].split('+');
8186						for (var i in list) {
8187							if (_.isEmpty(list[i]) == false) {
8188								Query.current.push({text:list[i], filter:'everything'});
8189							}
8190						}
8191					} else {
8192						Query.current.push({text:$stateParams[prop], filter:prop});
8193					}
8194				}
8195				}
8196				// initial highlighting
8197				Highlighting.set(Query.current);
8198			}*/
8199
8200
8201			/*$rootScope.$on('$stateChangeSuccess', function(event, toState, toStateParams, fromState, fromStateParams) {
8202				if (toState.name === 'tool' || toState.name === 'home') { 
8203					Query.current = [];
8204				}
8205				else if (toState.name === 'search') {
8206					updateQuery();
8207				}
8208			});*/
8209
8210			//updateQuery();
8211
8212			function quoteQueryStringValue(v){
8213				return '"' + v + '"'
8214			}
8215
8216			scope.topicNameClicked = function(topic) {
8217				$state.transitionTo('search', 
8218					{'topic': quoteQueryStringValue(topic),
8219					 'page': 1,
8220					 'sort':'score'
8221					},
8222				{
8223		    	reload: true,
8224		    	inherit: false,
8225		    	notify: true
8226				});
8227			}
8228
8229			scope.operationNameClicked = function(operation) {
8230				$state.transitionTo('search', 
8231					{'operation': quoteQueryStringValue(operation),
8232					 'page': 1,
8233					 'sort':'score'
8234					},
8235				{
8236		    	reload: true,
8237		    	inherit: false,
8238		    	notify: true
8239				});
8240			}
8241
8242			scope.navElement = function(elementid) {
8243				$anchorScroll.yOffset = 75;
8244				// set the location.hash
8245				$location.hash(elementid);
8246			 
8247				$anchorScroll();
8248				$location.hash(null);
8249			}
8250
8251			// helper function for getting used terms
8252			function getUsedTerms(term, params) {
8253				var d = $q.defer();
8254				params["usedTermName"] = term;
8255				UsedTerms.get(params, function(response) {
8256					d.resolve(response.data);
8257				});
8258				return d.promise;
8259			}
8260
8261			// necessary for filtering suggestions for the autocomplete
8262			scope.loadSuggestions = function(term, query) {
8263				var deferred = $q.defer();
8264				// fetch suggestions if not loaded
8265				scope.getSuggestionsFor(term);
8266				scope.usedTerms[term].then(function(list) {
8267					deferred.resolve(filterFilter(list, query));
8268				});
8269				return deferred.promise;
8270			}
8271
8272			// get used terms for typeahead
8273			scope.usedTerms = {};
8274			scope.usedTermsLoaded = {};
8275		
8276			scope.getSuggestionsFor = function(term) {
8277				// only fetch if not already loaded
8278				if (!scope.usedTermsLoaded[term]) {
8279					var params = _.clone($stateParams);
8280					params['page'] = null;
8281					params['sort'] = null;
8282					params['ord'] = null;
8283			
8284					scope.usedTerms[term] = getUsedTerms(term === 'everything' ? 'all' : term, params);
8285					scope.usedTermsLoaded[term] = true;
8286				}
8287			};
8288
8289			// custom object necessary for ngTagsInput
8290			scope.filter = {};
8291			// initially selected filter
8292			scope.filter.selected = 'everything';
8293			// list of filters to display
8294			scope.filter.list = [
8295			"everything",
8296			"topic",
8297			"operation",
8298			"input",
8299			"output",
8300			"toolType",
8301			"language",
8302			"accessibility",
8303			"cost",
8304			"license",
8305			"credit",
8306			"collectionID",
8307			"name"	
8308			];
8309
8310			// get initial suggestions
8311			// getSuggestions();
8312
8313			// when tag is added save the filter and reset it to 'everything'
8314			scope.tagAdded = function(tag) {
8315				tag['text'] = scope.normalizeTag(tag);
8316				tag['filter'] = scope.filter.selected;
8317				scope.filter.selected = 'everything';
8318			}
8319
8320			// remove any unwanted characters from the entered tag
8321			scope.normalizeTag = function(tag) {
8322				return tag['text'].replace(/\+/g, "")
8323			}
8324
8325			scope.doHomeSearch = function() {
8326				var params = scope.params;
8327				var no_params = scope.Query.current.length;
8328				if (no_params > 0 && scope.Query.current[no_params-1].text == scope.lastParam && params != null & params.page != undefined){
8329					$state.transitionTo('search', params, { notify: true });
8330				}
8331			}
8332
8333			scope.myFunct = function(keyEvent) {
8334  			if (keyEvent.which === 13){
8335    			scope.doHomeSearch();
8336  			}
8337			}
8338
8339			// saving the query and filtering to url
8340			scope.tagsChanged = function() {
8341				$rootScope.$emit('search-bar-tags-changed', {});
8342				$timeout(function() {
8343					var params =  {};
8344					scope.params = null;
8345					params['page'] = 1;
8346					params['sort'] = $stateParams['sort'];
8347					if (params['sort'] == undefined) {
8348						params['sort'] = 'score';
8349					};
8350					if (scope.Query.current.length == 0) {
8351						params['sort'] = undefined;
8352						params['page'] = undefined;
8353					};
8354					params['ord'] = $stateParams['ord'];
8355					params['domain'] = $stateParams['domain'];
8356							// add each tag from the search bar to the url
8357							for (var i in scope.Query.current) {
8358								scope.lastParam = scope.Query.current[i].text;
8359								// if tag is 'everything' put it under q
8360								if (scope.Query.current[i].filter == 'everything') {
8361									// if there already is a 'everything' tag, add new ones separating them with a dash
8362									if (params['q'] && params['q'].length > 0) {
8363										params['q'] += "+" + scope.Query.current[i].text;
8364									} else {
8365										params['q'] = scope.Query.current[i].text
8366									}
8367								} else {
8368									params[scope.Query.current[i].filter] = scope.Query.current[i].text;
8369								}
8370							}
8371
8372							scope.params = params;
8373							// update url
8374							//$state.transitionTo('search', params, { notify: true });
8375							// add query elements to list of terms to highlight
8376							Highlighting.set(Query.current);
8377							$timeout(function() {
8378								// get more refined suggestions
8379								getSuggestions();
8380								// refresh tool list
8381								ToolList.refresh();
8382							})
8383						})
8384			}
8385		}
8386	}
8387}]);
8388angular.module('elixir_front')
8389.directive('navigationBar', [function() {
8390	return {
8391		restrict: 'A',
8392		templateUrl: 'components/navigationBar/navigationBar.html',
8393		link: function(scope, element, attrs) {
8394			scope.showSearch = scope.toState.name != "home";
8395			scope.$watch(function(){
8396				return scope.toState.name
8397			}, function(newVal, oldVal){
8398    			scope.showSearch = (scope.toState.name != "home");
8399			}) 
8400		}
8401	}
8402}])
8403.controller('NavbarController', ['$scope', 'djangoAuth', 'User', '$stateParams', '$timeout', 'Domain', '$state', function($scope, djangoAuth, User, $stateParams, $timeout, Domain, $state) {
8404	// reference the service
8405	var vm = this;
8406
8407	$scope.User = User;
8408	$scope.Domain = Domain;
8409	
8410	$timeout(function() {
8411		Domain.load($stateParams['domain']);
8412	}, 500);
8413
8414	$scope.logoutButtonClick = function() {
8415		djangoAuth.logout();
8416	};
8417
8418	$scope.homeButtonClicked = function() {
8419		$scope.Domain.unload();
8420		$state.go('home');
8421	};
8422
8423	$scope.aboutButtonClicked = function() {
8424		$scope.Domain.unload();
8425		$state.go('about');
8426	};
8427
8428
8429}]);
8430angular.module('elixir_front')
8431.directive('pageFooter', [function() {
8432	return {
8433		restrict: 'A',
8434		templateUrl: 'components/pageFooter/pageFooter.html',
8435		link: function(scope, element, attrs) {
8436
8437		}
8438	}
8439}])
8440angular.module('elixir_front')
8441.controller('ChangeEmailController', ['$scope', '$uibModalInstance', 'djangoAuth', function($scope, $uibModalInstance, djangoAuth) {
8442    $scope.submitEmail = function() {
8443        djangoAuth.updateProfile({email: $scope.newEmail})
8444        .then(function(response) {
8445            alert('Email updated successfully!');
8446            $uibModalInstance.close(response);
8447        }, function(error) {
8448            alert('Failed to update email: ' + error.email);
8449        });
8450    };
8451}]);
8452angular.module('elixir_front')
8453.controller('ChangePasswordController', ['$scope', '$uibModalInstance', 'djangoAuth', function($scope, $uibModalInstance, djangoAuth) {
8454
8455    $scope.submitPassword = function() {
8456		var token = localStorage.getItem("authToken");  // Store and retrieve auth token securely
8457        var oldPassword = $scope.oldPassword;
8458        var newPassword1 = $scope.newPassword;
8459        var newPassword2 = $scope.confirmPassword;
8460        
8461        djangoAuth.changePassword(oldPassword, newPassword1, newPassword2)
8462            .then(function(response) {
8463                alert('Password updated successfully!');
8464                $uibModalInstance.close();
8465            })
8466            .catch(function(error) {
8467                console.error("error: ", error);
8468                let firstKey = Object.keys(error)[0]; // First error key (old_password, new_password2...)
8469                alert('Failed to update password: ' + error[firstKey][0]);
8470            });
8471    };
8472}]);
8473angular.module('elixir_front')
8474.controller('ProfileController', ['$scope', '$state', 'djangoAuth', 'DisownToolService', 'User', '$uibModal', 'AppConfig', function($scope, $state, djangoAuth, DisownToolService, User, $uibModal, AppConfig) {
8475	$scope.profile = {};
8476	$scope.loading = true;
8477	$scope.User = User;
8478	djangoAuth.profile()
8479	.then(function (response) {
8480		$scope.profile = response;
8481		$scope.loading = false;
8482	}, function (response) {
8483		$scope.loading = false;
8484	});
8485
8486	$scope.disownEntry = function(entry) {
8487		if (!confirm("Are you sure you want to disown this resource?")){
8488			return;
8489		}
8490
8491		DisownToolService.disown({id: entry.id, version: entry.versionId}, function(response) {
8492			$scope.profile.resources = _.difference($scope.profile.resources, [entry]);
8493		}, function(response) {
8494			alert("Failed to disown " + entry.name + ". Please check your connection and try again later.");
8495		});
8496	};
8497	$scope.openChangePasswordModal = function() {
8498        $uibModal.open({
8499            templateUrl: 'components/profile/changePasswordModal.html',
8500            controller: 'ChangePasswordController'
8501        });
8502    };
8503
8504	$scope.openChangeEmailModal = function() {
8505		var modalInstance = $uibModal.open({
8506			templateUrl: 'components/profile/changeEmailModal.html',
8507			controller: 'ChangeEmailController'
8508		});
8509		
8510		modalInstance.result.then(function(updatedProfile) {
8511			// Update the profile with new email
8512			$scope.profile.email = updatedProfile.email;
8513			$scope.profile.email_verified = updatedProfile.email_verified;
8514		});
8515	};
8516
8517    $scope.resendVerificationEmail = function() {
8518		// If email ends with @biotools.local, do not attempt to resend
8519		if ($scope.profile.email.endsWith('@biotools.local')) {
8520			alert('Cannot send verification email to local test accounts.');
8521			return;
8522		}
8523        djangoAuth.resendEmail($scope.profile.email)
8524        .then(function(response) {
8525            alert('Verification email sent successfully!');
8526        }, function(error) {
8527            alert('Failed to send verification email: ' + (error.data && error.data.message ? error.data.message : 'Please try again later.'));
8528        });
8529    };
8530
8531	$scope.orcidConnect = function() {
8532        var client_id = AppConfig.ORCID_CLIENT_ID;
8533		var redirect_uri = AppConfig.ORCID_REDIRECT_URI;
8534		var orcid_base_url = AppConfig.ORCID_BASE_URL;
8535
8536		window.location.href = orcid_base_url + '/oauth/authorize?client_id=' + client_id + '&response_type=code&scope=/authenticate&redirect_uri=' 
8536+ redirect_uri;
8537	};
8538
8539	$scope.disconnectSocial = function(provider, id) {
8540		if (!confirm("Are you sure you want to disconnect your " + provider + " account?")){
8541			return;
8542		}
8543		djangoAuth.disconnectSocial(id)
8544		.then(function(response) {
8545			// Remove the disconnected account from the profile.socialAccounts array
8546			$scope.profile.socialAccounts = $scope.profile.socialAccounts.filter(function(account) {
8547				return account.id !== id;
8548			});
8549		}, function(error) {
8550			alert("Failed to disconnect your " + provider + " account. Please try again later.");
8551		});
8552	};
8553
8554	// check if social accounts are connected
8555	$scope.hasOrcidAccount = function() {
8556		if (!$scope.profile.socialAccounts) return false;
8557		return $scope.profile.socialAccounts.some(function(account) {
8558			return account.provider === 'orcid';
8559		});
8560	};
8561}]);
8562	angular.module('elixir_front')
8563.directive('searchBar', ['Query', 'ToolList', 'Highlighting', 'Attribute', '$state', '$stateParams', '$timeout', 'UsedTerms', '$q', 'filterFilter', 'Domain', '$rootScope', function(Query, ToolList, Highlighting, Attribute, $state, $stateParams, $timeout, UsedTerms, $q, filterFilter, Domain, $rootScope) {
8564	return {
8565		restrict: 'A',
8566		templateUrl: 'components/searchBar/searchBar.html',
8567		link: function(scope, element, attrs) {
8568
8569			// reference the Query in the scope
8570			scope.Query = Query;
8571			// reference the Attribute in the scope
8572			scope.Attribute = Attribute;
8573			scope.ToolList = ToolList;
8574
8575			var initializing = true
8576
8577			function updateQuery() {
8578				Query.current = [];
8579				for (var prop in $stateParams) {
8580				// iterate through all parameters, skip 'page'
8581				if ($stateParams.hasOwnProperty(prop)
8582					&& !_.includes(['page', 'sort', 'ord', 'domain'], prop)
8583					&& typeof $stateParams[prop] != 'undefined') {
8584					// Remove tool id from the query
8585					if (prop == 'q') {
8586						var list = $stateParams[prop].split('+');
8587						for (var i in list) {
8588							if (_.isEmpty(list[i]) == false) {
8589								Query.current.push({text:list[i], filter:'everything'});
8590							}
8591						}
8592					} else {
8593						Query.current.push({text:$stateParams[prop], filter:prop});
8594					}
8595				}
8596				}
8597				// initial highlighting
8598				Highlighting.set(Query.current);
8599			}
8600
8601
8602			$rootScope.$on('$stateChangeSuccess', function(event, toState, toStateParams, fromState, fromStateParams) {
8603				if (toState.name !== 'search') { 
8604					Query.current = [];
8605					ToolList.refresh();
8606				}
8607				else if (toState.name === 'search') {
8608					updateQuery();
8609				}
8610			});
8611
8612			updateQuery();
8613
8614			// helper function for getting used terms
8615			function getUsedTerms(term, params) {
8616				var d = $q.defer();
8617				var list = []
8618				params["usedTermName"] = term;
8619				UsedTerms.get(params, function(response) {
8620					d.resolve(response.data);
8621				});
8622				return d.promise;
8623			}
8624
8625			// necessary for filtering suggestions for the autocomplete
8626			scope.loadSuggestions = function(term, query) {
8627				var deferred = $q.defer();
8628				scope.usedTerms[term].then(function(list) {
8629					deferred.resolve(filterFilter(list,query));
8630				})
8631				return deferred.promise;
8632			}
8633
8634			function getSuggestions() {
8635				// get used terms for typeahead
8636				scope.usedTerms = {};
8637
8638				// add query and filtering parameters to refine suggestions
8639				var params = _.clone($stateParams);
8640				params['page'] = null;
8641				params['sort'] = null;
8642				params['ord'] = null;
8643
8644				// create promises for all suggestions
8645				scope.usedTerms['everything'] = getUsedTerms('all', params);
8646				scope.usedTerms['topic'] = getUsedTerms('topic', params);
8647				scope.usedTerms['operation'] = getUsedTerms('operation', params);
8648				scope.usedTerms['input'] = getUsedTerms('input', params);
8649				scope.usedTerms['output'] = getUsedTerms('output', params);
8650				scope.usedTerms['toolType'] = getUsedTerms('toolType', params);
8651				scope.usedTerms['language'] = getUsedTerms('language', params);
8652				scope.usedTerms['accessibility'] = getUsedTerms('accessibility', params);
8653				scope.usedTerms['cost'] = getUsedTerms('cost', params);
8654				scope.usedTerms['license'] = getUsedTerms('license', params);
8655				scope.usedTerms['credit'] = getUsedTerms('credit', params);
8656				scope.usedTerms['collectionID'] = getUsedTerms('collectionID', params);
8657				scope.usedTerms['name'] = getUsedTerms('name', params);  
8658			}
8659
8660			// custom object necessary for ngTagsInput
8661			scope.filter = {};
8662			// initially selected filter
8663			scope.filter.selected = 'everything';
8664			// list of filters to display
8665			scope.filter.list = [
8666			"everything",
8667			"topic",
8668			"operation",
8669			"input",
8670			"output",
8671			"toolType",
8672			"language",
8673			"accessibility",
8674			"cost",
8675			"license",
8676			"credit",
8677			"collectionID",
8678			"name"	
8679			];
8680
8681			// get initial suggestions
8682			getSuggestions();
8683
8684			// when tag is added save the filter and reset it to 'everything'
8685			scope.tagAdded = function(tag) {
8686				tag['text'] = scope.normalizeTag(tag);
8687				tag['filter'] = scope.filter.selected;
8688				scope.filter.selected = 'everything';
8689			}
8690
8691			// remove any unwanted characters from the entered tag
8692			scope.normalizeTag = function(tag) {
8693				return tag['text'].replace(/\+/g, "")
8694			}
8695
8696			// saving the query and filtering to url
8697			scope.tagsChanged = function() {
8698				$rootScope.$emit('search-bar-tags-changed', {});
8699				$timeout(function() {
8700					var params =  {};
8701					params['page'] = 1;
8702					params['sort'] = $stateParams['sort'];
8703					if (params['sort'] == undefined) {
8704						params['sort'] = 'score';
8705					};
8706					if (scope.Query.current.length == 0) {
8707						params['sort'] = undefined;
8708						params['page'] = undefined;
8709					};
8710					params['ord'] = $stateParams['ord'];
8711					params['domain'] = $stateParams['domain'];
8712							// add each tag from the search bar to the url
8713							for (var i in scope.Query.current) {
8714								// if tag is 'everything' put it under q
8715								if (scope.Query.current[i].filter == 'everything') {
8716									// if there already is a 'everything' tag, add new ones separating them with a dash
8717									if (params['q'] && params['q'].length > 0) {
8718										params['q'] += "+" + scope.Query.current[i].text;
8719									} else {
8720										params['q'] = scope.Query.current[i].text
8721									}
8722								} else {
8723									params[scope.Query.current[i].filter] = scope.Query.current[i].text;
8724								}
8725							}
8726							// update url
8727							$state.transitionTo('search', params, { notify: true });
8728							// add query elements to list of terms to highlight
8729							Highlighting.set(Query.current);
8730							$timeout(function() {
8731								// get more refined suggestions
8732								getSuggestions();
8733								// refresh tool list
8734								ToolList.refresh();
8735							})
8736						})
8737			}
8738		}
8739	}
8740}]);
8741angular.module('elixir_front.services')
8742// Gets stats info from the server.
8743.factory('Stats', ['$resource', function($resource){
8744	return $resource('/api/stats', null, {
8745		'query': { 
8746			isArray:false,
8747			method:'GET'
8748		}
8749	})
8750}])
8751.factory('StatsTotalEntries', ['$resource', function($resource){
8752	return $resource('/api/stats/total-entries', null, {
8753		'query': { 
8754			isArray:true,
8755			method:'GET'
8756		}
8757	})
8758}])
8759.factory('StatsUserData', ['$resource', function($resource){
8760	return $resource('/api/stats/users', null, {
8761		'query': { 
8762			isArray:true,
8763			method:'GET'
8764		}
8765	})
8766}])
8767.factory('TotalAnnotationsStatsData', ['$resource', function($resource){
8768	return $resource('/api/stats/annotation-count', null, {
8769		'query': { 
8770			isArray:true,
8771			method:'GET'
8772		}
8773	})
8774}])
8775// Get and manage the stats data from the server.
8776.service('StatsDataSource', ['$stateParams', 'Stats', 'StatsTotalEntries', 'StatsUserData', 'TotalAnnotationsStatsData', function($stateParams, Stats, StatsTotalEntries, StatsUserData, TotalAnnotationsStatsData) {
8777	var _this = this;
8778	this.isLoadingData = true;
8779	this.isLoadingTotalEntriesData = true;
8780	this.isLoadingUserStatsData = true;
8781	this.isLoadingTotalAnnotationsStats = true;
8782	this.statsData = {};
8783	this.totalEntriesData = {};
8784	this.userStatsData = {};
8785	this.totalAnnotationsStatsData = {};
8786	this.totalAnnotationsCount = function() {
8787		var totalCount = 0;
8788		for (var key in this.statsData.totalAnnotationCount) {
8789			var value = this.statsData.totalAnnotationCount[key];
8790			totalCount += value;
8791		}
8792		return totalCount;
8793	}
8794	// Operations
8795	this.fetchStatsData = function(completion) {
8796		this.isLoadingData = true;
8797		var response = Stats.query($stateParams, function() {
8798			_this.statsData = response;
8799			_this.isLoadingData = false;
8800			completion()
8801		});
8802	}
8803	this.fetchTotalEntriesData = function(completion) {
8804		this.isLoadingTotalEntriesData = true;
8805		var response = StatsTotalEntries.query($stateParams, function() {
8806			_this.totalEntriesData = response;
8807			_this.isLoadingTotalEntriesData = false;
8808			completion()
8809		});
8810	}
8811	this.fetchUserStatsData = function(completion) {
8812		this.isLoadingUserStatsData = true;
8813		var response = StatsUserData.query($stateParams, function() {
8814			_this.userStatsData = response;
8815			_this.isLoadingUserStatsData = false;
8816			completion();
8817		});
8818	}
8819	this.fetchTotalAnnotationsStatsData = function(completion) {
8820		this.isLoadingTotalAnnotationsStats = true;
8821		var response = TotalAnnotationsStatsData.query($stateParams, function() {
8822			_this.totalAnnotationsStatsData = response;
8823			_this.isLoadingTotalAnnotationsStats = false;
8824			completion();
8825		});
8826	}
8827}])
8828// Controller for the stats website.
8829angular.module('elixir_front.controllers').controller('StatsController', ['$scope', '$http', 'StatsDataSource', function($scope, $http, StatsDataSource) {
8830	// Initialization
8831	var _this = this;
8832	$scope.labels = [];
8833	$scope.labelsUsers = [];
8834	$scope.series = ['Total entries'];
8835	$scope.seriesEDAM = ['EDAM annotations'];
8836	$scope.seriesEDAMBreakdown = ['Data Type annotations', 'Topic annotations', 'Function annotations', 'Format annotations'];
8837	$scope.seriesUsers = ['Total contributors count', 'New contributors this month'];
8838	$scope.seriesTotalAnnotationDetail = ['Name', 'Description', 'Homepage', 'Tool Type', 'Unique Id', 'Topic', 'Publication', 'Contact', 'Operation', 'Documentation', 'Operating System', 'Input output', 'Code availability', 'Accessibility', 'Data format', 'Community', 'Downloads', 'Total'];
8839	$scope.seriesTotalAnnotation = ['Total'];
8840	$scope.data = [[]];
8841	$scope.dataEDAM = [[]];
8842	$scope.dataEDAMBreakdown = [[]];
8843	$scope.dataUsers = [[]];
8844	$scope.dataTotalAnnotation = [[]];
8845	$scope.dataTotalAnnotationDetail = [[]];
8846	$scope.totalAnnotationOptions = {
8847		tooltips: {
8848			titleFontSize: 10,
8849			bodyFontSize: 9,
8850			bodySpacing: 0
8851		}
8852	};
8853	$scope.statsDataSource = StatsDataSource;
8854	$scope.statsDataSource.fetchStatsData(function(){
8855		_this.initializeTopContributorsStatsGraph();
8856	});
8857	$scope.statsDataSource.fetchTotalEntriesData(function() {
8858		_this.initializeTotalEntriesGraph();
8859	});
8860	$scope.statsDataSource.fetchUserStatsData(function() {
8861		_this.initializeUserStatsGraph();
8862	});
8863	$scope.statsDataSource.fetchTotalAnnotationsStatsData(function() {
8864		_this.initializeTotalAnnotationsStatsGraph();
8865		_this.initializeTotalAnnotationsDetailStatsGraph();
8866	});
8867	$scope.topContributorsLabels = [];
8868	$scope.topContributorsData = [];
8869	$scope.topContributorsOptions = {legend: {display: true}};
8870	// Total Entries Grpah handling
8871	this.initializeTotalEntriesGraph = function() {
8872		var data = $scope.statsDataSource.totalEntriesData;
8873		$scope.labels = _.map(data, function(x) {
8874			var date = x.date.split('T')[0];
8875			return moment(date).format("MMM YY");
8876		});
8877		$scope.data = [_.map(data, function(x) {
8878			return x.entriesCount;
8879		})];
8880		$scope.dataEDAM = [_.map(data, function(x) {
8881			return x.edamAnnotationsCount;
8882		})];
8883		$scope.dataEDAMBreakdown = [_.map(data, function(x) {
8884			return x.dataTypeAnnotationsCount;
8885		}),
8886		_.map(data, function(x) {
8887			return x.topicAnnotationsCount;
8888		}),
8889		_.map(data, function(x) {
8890			return x.functionAnnotationsCount;
8891		}),
8892		_.map(data, function(x) {
8893			return x.formatAnnotationsCount;
8894		})];
8895	};
8896	// Total Entries Grpah handling
8897	this.initializeUserStatsGraph = function() {
8898		var data = $scope.statsDataSource.userStatsData;
8899		$scope.labelsUsers = _.map(data, function(x) {
8900			var date = x.date.split('T')[0];
8901			return moment(date).format("MMM YY");
8902		});
8903		$scope.dataUsers = [_.map(data, function(x) {
8904			return x.totalUsersCount;
8905		}),
8906		_.map(data, function(x) {
8907			return x.newUsersCount;
8908		})];
8909	};
8910	// Top contributors data.
8911	this.initializeTopContributorsStatsGraph = function() {
8912		var contributorData = $scope.statsDataSource.statsData.topContributors;
8913		$scope.topContributorsLabels = _.map(contributorData, function(x) {
8914			return x.domain;
8915		});
8916		$scope.topContributorsData = _.map(contributorData, function(x) {
8917			return x.count;
8918		});
8919	};
8920	// Total annotations data.
8921	this.initializeTotalAnnotationsStatsGraph = function() {
8922		var annotationData = $scope.statsDataSource.totalAnnotationsStatsData
8923		$scope.dataTotalAnnotation = [_.map(annotationData, function(x) {
8924			var data = x;
8925			var totalCount = 0;
8926			for (var key in data) {
8927				if (key != 'date' && !isNaN(data[key])) {
8928					totalCount += data[key];
8929				}
8930			}
8931			return totalCount;
8932		})];
8933	};
8934	this.initializeTotalAnnotationsDetailStatsGraph = function() {
8935		var annotationData = $scope.statsDataSource.totalAnnotationsStatsData
8936		$scope.dataTotalAnnotationDetail = [_.map(annotationData, function(x) {
8937			return x.nameAnnotationCount;
8938		}),
8939		_.map(annotationData, function(x) {
8940			return x.descriptionAnnotationCount;
8941		}),
8942		_.map(annotationData, function(x) {
8943			return x.homepageAnnotationCount;
8944		}),
8945		_.map(annotationData, function(x) {
8946			return x.toolTypeAnnotationCount;
8947		}),
8948		_.map(annotationData, function(x) {
8949			return x.uniqueIDAnnotationCount;
8950		}),
8951		_.map(annotationData, function(x) {
8952			return x.topicAnnotationCount;
8953		}),
8954		_.map(annotationData, function(x) {
8955			return x.publicationAnnotationCount;
8956		}),
8957		_.map(annotationData, function(x) {
8958			return x.contactAnnotationCount;
8959		}),
8960		_.map(annotationData, function(x) {
8961			return x.operationAnnotationCount;
8962		}),
8963		_.map(annotationData, function(x) {
8964			return x.documentationAnnotationCount;
8965		}),
8966		_.map(annotationData, function(x) {
8967			return x.operatingSystemAnnotationCount;
8968		}),
8969		_.map(annotationData, function(x) {
8970			return x.inputOutputAnnotationCount;
8971		}),
8972		_.map(annotationData, function(x) {
8973			return x.codeAvailabilityAnnotationCount;
8974		}),
8975		_.map(annotationData, function(x) {
8976			return x.accessibilityAnnotationCount;
8977		}),
8978		_.map(annotationData, function(x) {
8979			return x.dataFormatsAnnotationCount;
8980		}),
8981		_.map(annotationData, function(x) {
8982			return x.communityAnnotationCount;
8983		}),
8984		_.map(annotationData, function(x) {
8985			return x.downloadsAnnotationCount;
8986		})];
8987	};
8988}]);
8989
8990
8991
8992
8993angular.module('elixir_front')
8994.controller('ToolPageController', ['$scope', '$state', '$timeout', '$stateParams', 'Tool', 'User', 'CheckUserEditingRights', 'ResourceRequestProvider', 'ngMeta', 'Query', '$http', 'MetaService',  function($scope, $state, $timeout, $stateParams, Tool, User, CheckUserEditingRights, ResourceRequestProvider, ngMeta, Query, $http, MetaService) {
8995	$scope.notFound = false;
8996	$scope.versions = [];
8997	$scope.CheckUserEditingRights = CheckUserEditingRights;
8998	$scope.editingRequestedSuccess = null;
8999
9000
9001
9002	$scope.User = User;
9003	$scope.elixirCommunityIndex = {
9004		"3D-BioInfo": "3d-bioinfo",
9005		"Biodiversity": "biodiversity",	
9006		"Federated Human Data": "human-data",
9007		"Galaxy": "galaxy",
9008		"Human Copy Number Variation": "hcnv",
9009		"Intrinsically Disordered Proteins": "intrinsically-disordered-proteins",
9010		"Marine Metagenomics": "marine-metagenomics",
9011		"Metabolomics": "metabolomics",
9012		"Microbial Biotechnology": "microbial-biotechnology",
9013		"Plant Sciences": "plant-sciences",
9014		"Proteomics": "proteomics",
9015		"Rare Diseases": "rare-diseases",
9016		"Systems Biology": "systems-biology"
9017	}
9018
9019	$scope.hasRRID = function(){
9020		for (var index in $scope.software.otherID) {
9021			if ($scope.software.otherID[index].type.toLowerCase() == 'rrid') {
9022				return true
9023			}
9024		}
9025
9026		return false;
9027	}
9028
9029	$scope.confidenceClass = function() {
9030		if ($scope.software.confidence_flag === 'high'){
9031			return 'alert-info'
9032		}
9033
9034		if ($scope.software.confidence_flag === 'medium'){
9035			return 'alert-warning'
9036		}
9037
9038		return 'alert-danger';
9039	}
9040
9041	function quoteQueryStringValue(v){
9042		return '"' + v + '"'
9043	}
9044
9045	function stripEdam(t){
9046		return t.replace("http://edamontology.org/", "");
9047	}
9048	// go to 404 in special case when URL is /tool/
9049	if ($stateParams.id.length == 0) {
9050		$state.go('404');
9051	}
9052
9053	$scope.topicNameClicked = function(topic) {
9054		$state.go('search', {'topicID': quoteQueryStringValue(stripEdam(topic.uri))});
9055	}
9056
9057	$scope.functionNameClicked = function(functionName) {
9058		$state.go('search', {'operationID': quoteQueryStringValue(stripEdam(functionName.uri))});
9059	}
9060
9061	$scope.inputNameClicked = function(inputName) {
9062		//$state.go('search', {'inputDataType': inputName.data.term});
9063		$state.go('search', {'inputDataTypeID': quoteQueryStringValue(stripEdam(inputName.data.uri)) });
9064	}
9065
9066	$scope.inputFormatNameClicked = function(formatName) {
9067		//$state.go('search', {'inputDataFormat': formatName.term});
9068		$state.go('search', {'inputDataFormatID': quoteQueryStringValue(stripEdam(formatName.uri)) });
9069	}
9070
9071	$scope.outputFormatNameClicked = function(formatName) {
9072		$state.go('search', {'outputDataFormatID': quoteQueryStringValue(stripEdam(formatName.uri))});
9073	}
9074
9075	$scope.outputNameClicked = function(outputName) {
9076		$state.go('search', {'outputDataTypeID': quoteQueryStringValue(stripEdam(outputName.data.uri))});
9077	}
9078
9079	$scope.toolsNameClicked = function(functionName) {
9080		$state.go('search', {'name': $scope.software.name});
9081	}
9082
9083	$scope.collectionNameClicked = function(collectionName) {
9084		$state.go('search', {'collectionID': '"'+ collectionName + '"'});
9085	}
9086
9087	$scope.creditNameClicked = function(creditName) {
9088		var url = $state.href('search', {'credit': '"'+ creditName + '"'});
9089		window.open(url,'_blank');
9090	}
9091
9092	$scope.publicationDetailsExist = function() {
9093		for (var index in $scope.software.publication) {
9094			if ($scope.software.publication[index].metadata != null) {
9095				return true;
9096			}
9097		}
9098		return false;
9099	}
9100
9101	// TODO: needs to keep it DRY and in a service
9102	// function to clean all nulls from tool gotten from API
9103	function cleanNulls (object) {
9104		for (var key in object) {
9105			if (object[key] == null) {
9106				delete object[key]
9107			} else if (object[key].constructor === Array) {
9108				if (object[key].length == 0) {
9109					delete object[key]
9110				} else {
9111					for (var i in object[key]) {
9112						cleanNulls(object[key][i])
9113					}
9114				}
9115			} else if (typeof object[key] === 'object') {
9116				cleanNulls(object[key]);
9117			}
9118		}
9119	}
9120
9121	var initAltmetricsScore = function() {
9122		var script = document.createElement('script');
9123		script.type = 'text/javascript';
9124		script.src = "https://d1bxh8uas1mnw7.cloudfront.net/assets/embed.js";
9125		document.body.appendChild(script);
9126	}
9127
9128	var initBipScripts = function() {
9129		var script = document.createElement('script');
9130		script.type = 'text/javascript';
9131		script.src = "https://cdn.jsdelivr.net/gh/athenarc/bip-plugin@main/embed.min.js";
9132		document.body.appendChild(script);
9133	}
9134
9135	var initDimensionsAI = function() {
9136		var script = document.createElement('script');
9137		script.type = 'text/javascript';
9138		script.src = "https://badge.dimensions.ai/badge.js";
9139		document.body.appendChild(script);
9140	}
9141
9142	$scope.altMetricsScorePublication = function() {
9143		var publication = ""
9144		for (var index in $scope.software.publication) {
9145			var publication = $scope.software.publication[index];
9146			if (publication.type == "Primary" && publication.doi) {
9147				publication = publication;
9148				break;
9149			}
9150			else {
9151				publication = publication;
9152			}
9153		}
9154		return publication;
9155	}
9156
9157	$scope.versionSelected = function(versionId) {
9158		$state.go('tool', {id: $scope.software.id, version: versionId});
9159	}
9160
9161	$scope.shouldLicenseBeALink = function(license) {
9162			return !_.includes(['Freeware','Proprietary', 'Other','Not licensed'], license);
9163	}
9164
9165	$scope.setMetadataForSoftware = function(software) {
9166		ngMeta.setTitle(software.name, ' · bio.tools');
9167		ngMeta.setTag('description', software.description);
9168		ngMeta.setTag('og:title', software.name);
9169		ngMeta.setTag('og:description', software.description);
9170        MetaService.setAlternatesForTool(software.biotoolsID);
9171
9172        const published = software.additionDate;
9173        const modified = software.lastUpdate;
9174        ngMeta.setTag('datePublished', published, false);
9175        ngMeta.setTag('dateModified', modified, false);
9176    }
9177
9178	// get tool
9179	$scope.software = Tool.get($stateParams, function(response) {
9180		// success handler
9181		initAltmetricsScore();
9182		initBipScripts();
9183		initDimensionsAI();
9184		//window.__dimensions_embed.addBadges();
9185		//window.__dimensions_embed.addBadges();
9186		cleanNulls($scope.software);
9187		$scope.setMetadataForSoftware($scope.software);
9188		//$timeout(function(){ window.OpEB_widgets.OpEB.apply(); }, 100);*/
9189
9190		$timeout(function(){ window.__dimensions_embed.addBadges(); }, 100);
9191		/*$timeout(function(){ window.OpEB_widgets.OpEB.apply(); }, 100);*/
9192	}, function(response) {
9193		// error handler
9194		if (response.status == 404) {
9195			$scope.notFound = true;
9196		}
9197	});
9198
9199	$scope.requestOwnership = function(resourceId) {
9200		if (confirm("We recommend that only the tool developers/maintainers request ownership.\nAll other users should request edit-rights.\nAre you sure you want to request ownership of this tool?")){
9201			$scope.ownershipRequestedSuccess = null;
9202			ResourceRequestProvider.requestOwnership(resourceId).then(function successCallback(response) {
9203				$scope.ownershipRequestedSuccess = true;
9204			}, function errorCallback(response) {
9205				$scope.ownershipRequestedSuccess = false;
9206			});
9207		}
9208	}
9209
9210	$scope.requestEditing = function(resourceId) {
9211		$scope.editingRequestedSuccess = null;
9212		ResourceRequestProvider.requestEditingRights(resourceId).then(function successCallback(response) {
9213			$scope.editingRequestedSuccess = true;
9214		}, function errorCallback(response) {
9215			$scope.editingRequestedSuccess = false;
9216		});
9217	}
9218
9219	// Call the API t retrieve and inject JSON-LD semantic annotations (Bioschemas tool profile)
9220	$scope.software.$promise.then(function(data) {
9221			$http.get("/api/"+data.biotoolsID+"?format=jsonld").then(function(response) {
9222				var script = document.createElement('script');
9223				script.type = 'application/ld+json';
9224				script.innerHTML = JSON.stringify(response.data);
9225
9226				document.getElementsByTagName('head')[0].appendChild(script);
9227
9228			 });
9229		}, function (reason) {
9230     		console.log('ERROR: '+JSON.stringify(reason));
9231   	});
9232
9233}])
9234.directive("publicationDetailCallout", function(){
9235	return {
9236		restrict: 'A',
9237		transclude: true,
9238		templateUrl: 'components/toolPage/partials/publicationDetailCallout.html',
9239		controller: ['$scope', function($scope) {
9240			$scope.status = "... More";
9241			$scope.expandable_status = "expandable-summary";
9242
9243			var initDimensionsAI = function() {
9244				var script = document.createElement('script');
9245				script.type = 'text/javascript';
9246				script.src = "https://badge.dimensions.ai/badge.js";
9247				document.body.appendChild(script);
9248			}
9249
9250			//initDimensionsAI();
9251			$scope.toggleStatus = function(){
9252				if ($scope.status == "... More"){
9253					$scope.status = "Less";
9254					$scope.expandable_status = "";
9255				}else{
9256					$scope.status = "... More";
9257					$scope.expandable_status = "expandable-summary";
9258				}
9259			}
9260
9261			$scope.getDoi = function(){
9262				return $scope.publication.doi;
9263			}
9264		}],
9265		link: function($scope, element, attrs) {
9266
9267				//window.__dimensions_embed.addBadges();
9268
9269		}
9270	};
9271})
9272.directive("toolPageLinkCallout", function(){
9273	return {
9274		restrict: 'A',
9275		transclude: true,
9276		template: function(elem, attr){
9277			return '<div class="bs-callout-sm bs-callout-primary" style="border-left-color: ' + attr.color + ';">' +
9278			'<div class="tool-page-callout-header" style="color: black;">' +
9279			'<a target="_blank" href="' + attr.url + '" tooltips tooltip-side="top" tooltip-content="' + attr.url + '">' + attr.name + ' › </a>' +
9280			'<i ng-show="' +  attr.toshow  + '" class="fa fa-question-circle" aria-hidden="true" style="font-size: 100%; margin-left: 0.5em; color: ' + attr.color + ';" tooltips tooltip-side="top" tooltip-content="' + attr.comment + '"></i>' +
9281			'</div>' +
9282			'</div>';
9283		},
9284		replace: true
9285	};
9286})
9287.directive("toolPageNewLinkCallout", function(){
9288	return {
9289		restrict: 'A',
9290		scope:{
9291			'url': '=?',
9292			'types':'=?',
9293			'toshow':'@?',
9294			'color':'@?',
9295			'cssclass':"@?",
9296			'note':'=?',
9297			'hovertext':'@?'
9298		},
9299		templateUrl: 'components/toolPage/partials/toolPageNewLinkCallout.html',
9300		replace: true
9301	};
9302})
9303.directive("toolPagePublicationCallout", function(){
9304	return {
9305		restrict: 'A',
9306		scope:{
9307				'pmcid': '=?',
9308				'doi':'=?',
9309				'pmid':'=?',
9310				'types':'=?',
9311				'color':'@?',
9312				'note':'=?'
9313		},
9314		templateUrl: 'components/toolPage/partials/toolPagePublicationCalloutHTML.html',
9315		replace: true
9316	};
9317})
9318.directive("toolPageContactCallout", function(){
9319	return {
9320		restrict: 'A',
9321		transclude: true,
9322		template: function(elem, attr){
9323			var callout = '<div class="bs-callout-sm bs-callout-primary" style="border-left-color: ' + attr.color + ';">';
9324			callout += '<div ng-show="' + attr.name + '" class="tool-page-callout-header" style="color: black;">{{::' + attr.name + '}}</div>';
9325			callout += '<div class="tool-page-callout-text">';
9326			callout += '<span ng-show="' + attr.phone + '">{{::' + attr.phone + '}}';
9327			callout += '<span ng-show="' + attr.email + ' || ' + attr.url + '" style="color: #CCCCCC;"> | </span></span>';
9328			callout += '<span ng-show="' + attr.email + '"><i class="fa fa-envelope-o" aria-hidden="true"></i> {{' + attr.email +'.replace(\'@\', \' at \') }}';
9329			callout += '<span ng-show="' + attr.url + '" style="color: #CCCCCC;"> | </span></span>';
9330			callout += '<span ng-show="' + attr.url + '"><a href="{{::' + attr.url + '}}">Link › </a></span>';
9331			callout += '</div>';
9332			callout += '</div>';
9333			return callout;
9334		},
9335		replace: true
9336	};
9337})
9338.directive("toolPageCreditCallout", function(){
9339	return {
9340		restrict: 'A',
9341		transclude: true,
9342		template: function(elem, attr){
9343			var callout = '<div class="bs-callout-sm bs-callout-primary" style="border-left-color: ' + attr.color + ';">';
9344			callout += '<div ng-show="' + attr.name + '" class="tool-page-callout-header" style="color: black;"><a href="" ng-click="creditNameClicked(credit.name)">{{' + attr.name + '}}</a>';
9345			callout += '<i ng-show="' +  attr.toshow  + '" class="fa fa-question-circle" aria-hidden="true" style="font-size: 100%; margin-left: 0.5em; color: ' + attr.color + ';" tooltips tooltip-side="top" tooltip-content="' + attr.comment + '"></i>';
9346			callout += '<span ng-show="' + attr.typeentity + ' && ' + attr.typeentity + ' != \'Person\'">';
9347			callout += '<span ng-show="' + attr.typeentity + '" style="color: #CCCCCC;"> | </span>';
9348			callout += '<span ng-show="' + attr.typeentity + '">{{' + attr.typeentity + '}}</span>';
9349			callout += '</div>';
9350			callout += '<div class="tool-page-callout-text">';
9351			callout += '<span ng-show="' + attr.typerole.count + ' != 0" ng-repeat="role in ' + attr.typerole + '">{{role}}{{$last ? "" : ", "}}</span>';
9352			callout += '<span ng-show="' + attr.email + ' || ' + attr.url + ' || ' + attr.orcidid + ' || ' + attr.gridid + '" style="color: #CCCCCC;"> | </span>';
9353			callout += '<span ng-show="' + attr.email + '"><i class="fa fa-envelope-o" aria-hidden="true"></i> {{' + attr.email +'.replace(\'@\', \' at \') }}';
9354			callout += '<span ng-show="' + attr.url + ' || ' + attr.orcidid + ' || ' + attr.gridid + '" style="color: #CCCCCC;"> | </span></span>';
9355			callout += '<span ng-show="' + attr.url + '"><a href="{{' + attr.url + '}}" target="_blank">Link › </a>';
9356			callout += '<span ng-show="' + attr.orcidid + ' || ' + attr.gridid + '" style="color: #CCCCCC;"> | </span></span>';
9357			callout += '<span ng-show="' + attr.orcidid + '"><a href="{{' + attr.orcidid + '}}" target="_blank">ORCID ›</a></span>';
9358			callout += '<span ng-show="' + attr.gridid + '" style="color: #CCCCCC;"> | </span></span>';
9359			callout += '<span ng-show="' + attr.gridid + '"><a href="https://www.grid.ac/institutes/{{' + attr.gridid + '}}" target="_blank">gridid ›</a></span>';
9360			callout += '<span ng-show="' + attr.rorid + '" style="color: #CCCCCC;"> | </span></span>';
9361			callout += '<span ng-show="' + attr.rorid + '"><a href="https://ror.org/{{' + attr.rorid + '}}" target="_blank">rorid ›</a></span>';
9362			callout += '<span ng-show="' + attr.fundrefid + '" style="color: #CCCCCC;"> | </span></span>';
9363			callout += '<span ng-show="' + attr.fundrefid + '"><a href="https://dx.doi.org/{{' + attr.fundrefid + '}}" target="_blank">fundrefid ›</a></span>';
9364			callout += '</div>';
9365			callout += '</div>';
9366			return callout;
9367		},
9368		replace: true
9369	};
9370});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.