} }); var NodeClient = oop.extend(base.BaseClient, { /** * Return a promise that resolves to an Array of Node objects. * @param {object} params * {number} pageSize */ list: function(params) { params = params || {}; var ret = $.Deferred(); // TODO: page numbber, filtering etc. var query = params.pageSize != null ? 'page[size]=' + params.pageSize : ''; this._request({url: '/nodes/', query: query}) .done(function(resp) { var nodes = $.map(resp.data, function(nodeData) { return new Node(nodeData); }); ret.resolve(nodes); }).fail(base.captureError('Could not fetch nodes list.')); return ret.promise(); } // TODO: detail(nodeID) }); module.exports = { Node: Node, NodeClient: NodeClient
var NodeCategorySettings = oop.extend( ChangeMessageMixin, { constructor: function(category, categories, updateUrl, disabled) { this.super.constructor.call(this); var self = this; self.disabled = disabled || false; self.UPDATE_SUCCESS_MESSAGE = 'Category updated successfully'; self.UPDATE_ERROR_MESSAGE = 'Error updating category, please try again. If the problem persists, email ' + '<a href="mailto:support@osf.io">support@osf.io</a>.'; self.UPDATE_ERROR_MESSAGE_RAVEN = 'Error updating Node.category'; self.INSTANTIATION_ERROR_MESSAGE = 'Trying to instatiate NodeCategorySettings view model without an update URL'; self.MESSAGE_SUCCESS_CLASS = 'text-success'; self.MESSAGE_ERROR_CLASS = 'text-danger'; if (!updateUrl) { throw new Error(self.INSTANTIATION_ERROR_MESSAGE); } self.categories = categories; self.category = ko.observable(category); self.updateUrl = updateUrl; self.selectedCategory = ko.observable(category); self.dirty = ko.observable(false); self.selectedCategory.subscribe(function(value) { if (value !== self.category()) { self.dirty(true); } }); }, updateSuccess: function(newcategory) { var self = this; self.changeMessage(self.UPDATE_SUCCESS_MESSAGE, self.MESSAGE_SUCCESS_CLASS); self.category(newcategory); self.dirty(false); }, updateError: function(xhr, status, error) { var self = this; self.changeMessage(self.UPDATE_ERROR_MESSAGE, self.MESSAGE_ERROR_CLASS); Raven.captureMessage(self.UPDATE_ERROR_MESSAGE_RAVEN, { url: self.updateUrl, textStatus: status, err: error }); }, updateCategory: function() { var self = this; return $osf.putJSON(self.updateUrl, { category: self.selectedCategory() }) .then(function(response) { return response.updated_fields.category; }) .done(self.updateSuccess.bind(self)) .fail(self.updateError.bind(self)); }, cancelUpdateCategory: function() { var self = this; self.selectedCategory(self.category()); self.dirty(false); self.resetMessage(); } });
var TokenDetailViewModel = oop.extend(ChangeMessageMixin, { constructor: function (urls) { this.super.constructor.call(this); var placeholder = new TokenData(); this.tokenData = ko.observable(placeholder); // Track whether data has changed, and whether user is allowed to leave page anyway this.originalValues = ko.observable(placeholder.serialize()); this.dirty = ko.computed(function(){ return JSON.stringify(this.originalValues()) !== JSON.stringify(this.tokenData().serialize()); }.bind(this)); this.allowExit = ko.observable(false); // Set up data access client this.webListUrl = urls.webListUrl; this.client = new TokenDataClient(urls.apiListUrl); // Toggle hiding token id (in detail view) this.showToken = ko.observable(false); // Toggle display of validation messages this.showMessages = ko.observable(false); // // If no detail url provided, render view as though it was a creation form. Otherwise, treat as READ/UPDATE. this.apiDetailUrl = ko.observable(urls.apiDetailUrl); this.isCreateView = ko.computed(function () { return !this.apiDetailUrl(); }.bind(this)); }, init: function () { if (!this.isCreateView()) { // Add listener to prevent user from leaving page if there are unsaved changes $(window).on('beforeunload', function () { if (this.dirty() && !this.allowExit()) { return 'There are unsaved changes on this page.'; } }.bind(this)); var request = this.client.fetchOne(this.apiDetailUrl()); request.done(function (dataObj) { this.tokenData(dataObj); this.originalValues(dataObj.serialize()); }.bind(this)); request.fail(function(xhr, status, error) { $osf.growl('Error', language.apiOauth2Token.dataFetchError, 'danger'); Raven.captureMessage('Error fetching token data', { extra: { url: this.apiDetailUrl(), status: status, error: error } }); }.bind(this)); } }, updateToken: function () { if (!this.dirty()){ // No data needs to be sent to the server, but give the illusion that form was submitted this.changeMessage( language.apiOauth2Token.dataUpdated, 'text-success', 5000); return; } var request = this.client.updateOne(this.tokenData()); request.done(function (dataObj) { this.tokenData(dataObj); this.originalValues(dataObj.serialize()); this.changeMessage( language.apiOauth2Token.dataUpdated, 'text-success', 5000); }.bind(this)); request.fail(function (xhr, status, error) { $osf.growl('Error', language.apiOauth2Token.dataSendError, 'danger'); Raven.captureMessage('Error updating instance', { extra: { url: this.apiDetailUrl, status: status, error: error } }); }.bind(this)); return request; }, createToken: function () { var request = this.client.createOne(this.tokenData()); request.done(function (dataObj) { this.tokenData(dataObj); this.originalValues(dataObj.serialize()); this.showToken(true); this.changeMessage(language.apiOauth2Token.creationSuccess, 'text-success'); this.apiDetailUrl(dataObj.apiDetailUrl); // Toggle ViewModel --> act like a display view now. historyjs.replaceState({}, '', dataObj.webDetailUrl); // Update address bar to show new detail page }.bind(this)); request.fail(function (xhr, status, error) { $osf.growl('Error', language.apiOauth2Token.dataSendError, 'danger'); Raven.captureMessage('Error registering new OAuth2 personal access token', { extra: { url: this.apiDetailUrl, status: status, error: error } }); }.bind(this)); }, submit: function () { // Validate and dispatch form to correct handler based on view type if (!this.tokenData().isValid()) { // Turn on display of validation messages this.showMessages(true); } else { this.showMessages(false); if (this.isCreateView()) { this.createToken(); } else { this.updateToken(); } } }, deleteToken: function () { var tokenData = this.tokenData(); bootbox.confirm({ title: 'Deactivate token?', message: language.apiOauth2Token.deactivateConfirm, callback: function (confirmed) { if (confirmed) { var request = this.client.deleteOne(tokenData ); request.done(function () { this.allowExit(true); // Don't let user go back to a deleted token page historyjs.replaceState({}, '', this.webListUrl); this.visitList(); }.bind(this)); request.fail(function () { $osf.growl('Error', language.apiOauth2Token.deactivateError, 'danger'); }.bind(this)); } }.bind(this), buttons:{ confirm:{ label:'Deactivate', className:'btn-danger' } } }); }, visitList: function () { window.location = this.webListUrl; }, cancelChange: function () { if (!this.dirty()) { this.visitList(); } else { bootbox.confirm({ title: 'Discard changes?', message: language.apiOauth2Token.discardUnchanged, callback: function(confirmed) { if (confirmed) { this.allowExit(true); this.visitList(); } }.bind(this), buttons: { confirm: { label:'Discard', className:'btn-danger' } } }); } }, });
var UserProfileViewModel = oop.extend(ChangeMessageMixin, { constructor: function() { this.super.constructor.call(this); this.client = new UserProfileClient(); this.profile = ko.observable(new UserProfile()); this.emailInput = ko.observable(''); }, init: function () { this.client.fetch().done( function(profile) { this.profile(profile); }.bind(this) ); }, addEmail: function () { this.changeMessage('', 'text-info'); var newEmail = this.emailInput().toLowerCase().trim(); if(newEmail){ var email = new UserEmail({ address: newEmail }); // ensure email isn't already in the list for (var i=0; i<this.profile().emails().length; i++) { if (this.profile().emails()[i].address() === email.address()) { this.changeMessage('Duplicate Email', 'text-warning'); this.emailInput(''); return; } } this.profile().emails.push(email); this.client.update(this.profile()).done(function (profile) { this.profile(profile); var emails = profile.emails(); for (var i=0; i<emails.length; i++) { if (emails[i].address() === email.address()) { this.emailInput(''); var addrText = $osf.htmlEscape(email.address()); $osf.growl('<em>' + addrText + '</em> added to your account.','You will receive a confirmation email at <em>' + addrText + '</em>. Please check your email and confirm.', 'success'); return; } } }.bind(this)).fail(function(){ this.profile().emails.remove(email); }.bind(this)); } else { this.changeMessage('Email cannot be empty.', 'text-danger'); } }, resendConfirmation: function(email){ var self = this; self.changeMessage('', 'text-info'); var addrText = $osf.htmlEscape(email.address()); bootbox.confirm({ title: 'Resend Email Confirmation?', message: 'Are you sure that you want to resend email confirmation to ' + '<em>' + addrText + '</em>?', callback: function (confirmed) { if (confirmed) { self.client.update(self.profile(), email).done(function () { $osf.growl( 'Email confirmation resent to <em>' + addrText + '</em>', 'You will receive a new confirmation email at <em>' + addrText + '</em>. Please check your email and confirm.', 'success'); }); } }, buttons:{ confirm:{ label:'Resend' } } }); }, removeEmail: function (email) { var self = this; self.changeMessage('', 'text-info'); if (self.profile().emails().indexOf(email) !== -1) { var addrText = $osf.htmlEscape(email.address()); bootbox.confirm({ title: 'Remove Email?', message: 'Are you sure that you want to remove ' + '<em>' + addrText + '</em>' + ' from your email list?', callback: function (confirmed) { if (confirmed) { self.profile().emails.remove(email); self.client.update(self.profile()).done(function () { $osf.growl('Email Removed', '<em>' + addrText + '</em>', 'success'); }); } }, buttons:{ confirm:{ label:'Remove', className:'btn-danger' } } }); } else { $osf.growl('Error', 'Please refresh the page and try again.', 'danger'); } }, makeEmailPrimary: function (email) { this.changeMessage('', 'text-info'); if (this.profile().emails().indexOf(email) !== -1) { this.profile().primaryEmail().isPrimary(false); email.isPrimary(true); this.client.update(this.profile()).done(function () { var addrText = $osf.htmlEscape(email.address()); $osf.growl('Made Primary', '<em>' + addrText + '<em>', 'success'); }); } else { $osf.growl('Error', 'Please refresh the page and try again.', 'danger'); } } });
var AddPointerViewModel = oop.extend(Paginator, { constructor: function(nodeTitle) { this.super.constructor.call(this); var self = this; this.nodeTitle = nodeTitle; this.submitEnabled = ko.observable(true); this.searchAllProjectsSubmitText = ko.observable(SEARCH_ALL_SUBMIT_TEXT); this.searchMyProjectsSubmitText = ko.observable(SEARCH_MY_PROJECTS_SUBMIT_TEXT); this.query = ko.observable(); this.results = ko.observableArray(); this.selection = ko.observableArray(); this.errorMsg = ko.observable(''); this.totalPages = ko.observable(0); this.includePublic = ko.observable(true); this.searchWarningMsg = ko.observable(''); this.submitWarningMsg = ko.observable(''); this.loadingResults = ko.observable(false); this.foundResults = ko.pureComputed(function() { return self.query() && self.results().length; }); this.noResults = ko.pureComputed(function() { return self.query() && !self.results().length; }); }, searchAllProjects: function() { this.includePublic(true); this.pageToGet(0); this.searchAllProjectsSubmitText('Searching...'); this.fetchResults(); }, searchMyProjects: function() { this.includePublic(false); this.pageToGet(0); this.searchMyProjectsSubmitText('Searching...'); this.fetchResults(); }, fetchResults: function() { var self = this; self.errorMsg(''); self.searchWarningMsg(''); if (self.query()) { self.results([]); // clears page for spinner self.loadingResults(true); // enables spinner osfHelpers.postJSON( '/api/v1/search/node/', { query: self.query(), nodeId: nodeId, includePublic: self.includePublic(), page: self.pageToGet() } ).done(function(result) { if (!result.nodes.length) { self.errorMsg('No results found.'); } else { result.nodes.forEach(function(each) { if (each.isRegistration) { each.dateRegistered = new osfHelpers.FormattableDate(each.dateRegistered); } else { each.dateCreated = new osfHelpers.FormattableDate(each.dateCreated); each.dateModified = new osfHelpers.FormattableDate(each.dateModified); } }); } self.results(result.nodes); self.currentPage(result.page); self.numberOfPages(result.pages); self.addNewPaginators(); }).fail(function(xhr) { self.searchWarningMsg(xhr.responseJSON && xhr.responseJSON.message_long); }).always( function (){ self.searchAllProjectsSubmitText(SEARCH_ALL_SUBMIT_TEXT); self.searchMyProjectsSubmitText(SEARCH_MY_PROJECTS_SUBMIT_TEXT); self.loadingResults(false); }); } else { self.results([]); self.currentPage(0); self.totalPages(0); self.searchAllProjectsSubmitText(SEARCH_ALL_SUBMIT_TEXT); self.searchMyProjectsSubmitText(SEARCH_MY_PROJECTS_SUBMIT_TEXT); } }, addTips: function(elements, data) { elements.forEach(function(element) { var titleText = ''; if (data.isRegistration) { titleText = 'Registered: ' + data.dateRegistered.local; } else { titleText = 'Created: ' + data.dateCreated.local + '\nModified: ' + data.dateModified.local; } $(element).tooltip({ title: titleText }); }); }, add: function(data) { this.selection.push(data); // Hack: Hide and refresh tooltips $('.tooltip').hide(); $('.pointer-row').tooltip(); }, remove: function(data) { var self = this; self.selection.splice( self.selection.indexOf(data), 1 ); // Hack: Hide and refresh tooltips $('.tooltip').hide(); $('.pointer-row').tooltip(); }, addAll: function() { var self = this; $.each(self.results(), function(idx, result) { if (self.selection().indexOf(result) === -1) { self.add(result); } }); }, removeAll: function() { var self = this; $.each(self.selection(), function(idx, selected) { self.remove(selected); }); }, selected: function(data) { var self = this; for (var idx = 0; idx < self.selection().length; idx++) { if (data.id === self.selection()[idx].id) { return true; } } return false; }, submit: function() { var self = this; self.submitEnabled(false); self.submitWarningMsg(''); var nodeIds = osfHelpers.mapByProperty(self.selection(), 'id'); osfHelpers.postJSON( nodeApiUrl + 'pointer/', { nodeIds: nodeIds } ).done(function() { window.location.reload(); }).fail(function(data) { self.submitEnabled(true); self.submitWarningMsg(data.responseJSON && data.responseJSON.message_long); }); }, clear: function() { this.query(''); this.results([]); this.selection([]); this.searchWarningMsg(''); this.submitWarningMsg(''); }, authorText: function(node) { var rv = node.firstAuthor; if (node.etal) { rv += ' et al.'; } return rv; } });
var ViewModel = oop.extend(OauthAddonFolderPicker,{ constructor: function(addonName, url, selector, folderPicker, opts, tbOpts) { var self = this; // TODO: [OSF-7069] self.super.super.constructor.call(self, addonName, url, selector, folderPicker, tbOpts); self.super.construct.call(self, addonName, url, selector, folderPicker, opts, tbOpts); // Non-Oauth fields: self.username = ko.observable(''); self.password = ko.observable(''); self.hosts = ko.observableArray([]); self.selectedHost = ko.observable(); // Host specified in select element self.customHost = ko.observable(); // Host specified in input element self.savedHost = ko.observable(); // Configured host var otherString = 'Other (Please Specify)'; // Designated host, specified from select or input element self.host = ko.pureComputed(function() { return self.useCustomHost() ? self.customHost() : self.selectedHost(); }); // Hosts visible in select element. Includes presets and 'Other' option self.visibleHosts = ko.pureComputed(function() { return self.hosts().concat([otherString]); }); // Whether to use select element or input element for host designation self.useCustomHost = ko.pureComputed(function() { return (self.selectedHost() === otherString || !self.hasDefaultHosts()); }); self.credentialsChanged = ko.pureComputed(function() { return self.nodeHasAuth() && !self.validCredentials(); }); self.showCredentialInput = ko.pureComputed(function() { return (self.credentialsChanged() && self.userIsOwner()) || (!self.userHasAuth() && !self.nodeHasAuth() && self.loadedSettings()); }); self.hasDefaultHosts = ko.pureComputed(function() { return Boolean(self.hosts().length); }); }, _updateCustomFields: function(settings) { var self = this; self.hosts(settings.hosts); }, clearModal : function() { var self = this; self.selectedHost(null); self.customHost(null); }, connectAccount : function() { var self = this; if( self.hasDefaultHosts() && !self.selectedHost() ){ if (self.useCustomHost()){ self.changeMessage('Please enter an ownCloud server.', 'text-danger'); } else { self.changeMessage('Please select an ownCloud server.', 'text-danger'); } return; } if ( !self.useCustomHost() && !self.username() && !self.password() ){ self.changeMessage('Please enter a username and password.', 'text-danger'); return; } if ( self.useCustomHost() && ( !self.customHost() || !self.username() || !self.password() ) ) { self.changeMessage('Please enter an ownCloud host and credentials.', 'text-danger'); return; } var url = self.urls().auth; return osfHelpers.postJSON( url, ko.toJS({ host: self.host, password: self.password, username: self.username }) ).done(function() { self.clearModal(); $modal.modal('hide'); self.updateAccounts().then(function() { try{ $osf.putJSON( self.urls().importAuth, { external_account_id: self.accounts()[0].id } ).done(self.onImportSuccess.bind(self) ).fail(self.onImportError.bind(self)); self.changeMessage(self.messages.connectAccountSuccess(), 'text-success', 3000); } catch(err){ self.changeMessage(self.messages.connectAccountDenied(), 'text-danger', 6000); } }); }).fail(function(xhr, textStatus, error) { var errorMessage = (xhr.status === 401) ? language.authInvalid : language.authError; self.changeMessage(errorMessage, 'text-danger'); Raven.captureMessage('Could not authenticate with ownCloud', { url: url, textStatus: textStatus, error: error }); }); }, formatExternalName: function(item) { return { text: $osf.htmlEscape(item.name) + ' - ' + $osf.htmlEscape(item.profile), value: item.id }; } });
var AddPointerViewModel = oop.extend(Paginator, { constructor: function(nodeTitle) { this.super.constructor.call(this); var self = this; this.nodeTitle = nodeTitle; this.submitEnabled = ko.observable(true); this.query = ko.observable(); this.results = ko.observableArray(); this.selection = ko.observableArray(); this.errorMsg = ko.observable(''); this.totalPages = ko.observable(0); this.includePublic = ko.observable(true); this.foundResults = ko.pureComputed(function() { return self.query() && self.results().length; }); this.noResults = ko.pureComputed(function() { return self.query() && !self.results().length; }); }, searchAllProjects: function() { this.includePublic(true); this.fetchResults(); }, searchMyProjects: function() { this.includePublic(false); this.fetchResults(); }, fetchResults: function() { var self = this; self.errorMsg(''); if (self.query()) { osfHelpers.postJSON( '/api/v1/search/node/', { query: self.query(), nodeId: nodeId, includePublic: self.includePublic(), page: self.currentPage() } ).done(function(result) { if (!result.nodes.length) { self.errorMsg('No results found.'); } self.results(result.nodes); self.currentPage(result.page); self.numberOfPages(result.pages); self.addNewPaginators(); }).fail( osfHelpers.handleJSONError ); } else { self.results([]); self.currentPage(0); self.totalPages(0); } }, addTips: function(elements) { elements.forEach(function(element) { $(element).find('.contrib-button').tooltip(); }); }, add: function(data) { this.selection.push(data); // Hack: Hide and refresh tooltips $('.tooltip').hide(); $('.contrib-button').tooltip(); }, remove: function(data) { var self = this; self.selection.splice( self.selection.indexOf(data), 1 ); // Hack: Hide and refresh tooltips $('.tooltip').hide(); $('.contrib-button').tooltip(); }, addAll: function() { var self = this; $.each(self.results(), function(idx, result) { if (self.selection().indexOf(result) === -1) { self.add(result); } }); }, removeAll: function() { var self = this; $.each(self.selection(), function(idx, selected) { self.remove(selected); }); }, selected: function(data) { var self = this; for (var idx = 0; idx < self.selection().length; idx++) { if (data.id === self.selection()[idx].id) { return true; } } return false; }, submit: function() { var self = this; self.submitEnabled(false); var nodeIds = osfHelpers.mapByProperty(self.selection(), 'id'); osfHelpers.postJSON( nodeApiUrl + 'pointer/', { nodeIds: nodeIds } ).done(function() { window.location.reload(); }).fail(function(data) { self.submitEnabled(true); osfHelpers.handleJSONError(data); }); }, clear: function() { this.query(''); this.results([]); this.selection([]); }, authorText: function(node) { var rv = node.firstAuthor; if (node.etal) { rv += ' et al.'; } return rv; } });
AddContributorViewModel = oop.extend(Paginator, { constructor: function (title, nodeId, parentId, parentTitle, options) { this.super.constructor.call(this); var self = this; self.title = title; self.nodeId = nodeId; self.nodeApiUrl = '/api/v1/project/' + self.nodeId + '/'; self.parentId = parentId; self.parentTitle = parentTitle; self.async = options.async || false; self.callback = options.callback || function () { }; self.nodesOriginal = {}; //state of current nodes self.childrenToChange = ko.observableArray(); self.nodesState = ko.observable(); self.canSubmit = ko.observable(true); //nodesState is passed to nodesSelectTreebeard which can update it and key off needed action. self.nodesState.subscribe(function (newValue) { //The subscribe causes treebeard changes to change which nodes will be affected var childrenToChange = []; for (var key in newValue) { newValue[key].changed = newValue[key].checked !== self.nodesOriginal[key].checked; if (newValue[key].changed && key !== self.nodeId) { childrenToChange.push(key); } } self.childrenToChange(childrenToChange); m.redraw(true); }); //list of permission objects for select. self.permissionList = [ {value: 'read', text: 'Read'}, {value: 'write', text: 'Read + Write'}, {value: 'admin', text: 'Administrator'} ]; self.page = ko.observable('whom'); self.pageTitle = ko.computed(function () { return { whom: 'Add Contributors', which: 'Select Components', invite: 'Add Unregistered Contributor' }[self.page()]; }); self.query = ko.observable(); self.results = ko.observableArray([]); self.contributors = ko.observableArray([]); self.selection = ko.observableArray(); self.contributorIDsToAdd = ko.pureComputed(function () { return self.selection().map(function (user) { return user.id; }); }); self.notification = ko.observable(''); self.inviteError = ko.observable(''); self.doneSearching = ko.observable(false); self.parentImport = ko.observable(false); self.totalPages = ko.observable(0); self.childrenToChange = ko.observableArray(); self.emailSearch = ko.pureComputed(function () { var emailRegex = new RegExp('[^\\s]+@[^\\s]+\\.[^\\s]'); if (emailRegex.test(String(self.query()))) { return true; } else { return false; } }); self.foundResults = ko.pureComputed(function () { return self.query() && self.results().length && !self.parentImport(); }); self.parentPagination = ko.pureComputed(function () { return self.doneSearching() && self.parentImport(); }); self.noResults = ko.pureComputed(function () { return self.query() && !self.results().length && self.doneSearching(); }); self.showLoading = ko.pureComputed(function () { return !self.doneSearching() && !!self.query(); }); self.addAllVisible = ko.pureComputed(function () { var selected_ids = self.selection().map(function (user) { return user.id; }); var contributors = self.contributors(); return ($osf.any( $.map(self.results(), function (result) { return contributors.indexOf(result.id) === -1 && selected_ids.indexOf(result.id) === -1; }) )); }); self.removeAllVisible = ko.pureComputed(function () { return self.selection().length > 0; }); self.inviteName = ko.observable(); self.inviteEmail = ko.observable(); self.addingSummary = ko.computed(function () { var names = $.map(self.selection(), function (result) { return result.fullname; }); return names.join(', '); }); }, hide: function () { $('.modal').modal('hide'); }, selectWhom: function () { this.page('whom'); }, selectWhich: function () { //when the next button is hit by the user, the nodes to change and disable are decided var self = this; var nodesState = self.nodesState(); for (var key in nodesState) { var i; var node = nodesState[key]; var enabled = nodesState[key].isAdmin; var checked = nodesState[key].checked; if (enabled) { var nodeContributors = []; for (i = 0; i < node.contributors.length; i++) { nodeContributors.push(node.contributors[i].id); } for (i = 0; i < self.contributorIDsToAdd().length; i++) { if (nodeContributors.indexOf(self.contributorIDsToAdd()[i]) < 0) { enabled = true; break; } else { checked = true; enabled = false; } } } nodesState[key].enabled = enabled; nodesState[key].checked = checked; } self.nodesState(nodesState); this.page('which'); }, gotoInvite: function () { var self = this; self.inviteName(self.query()); self.inviteError(''); self.inviteEmail(''); self.page('invite'); }, goToPage: function (page) { this.page(page); }, /** * A simple Contributor model that receives data from the * contributor search endpoint. Adds an additional displayProjectsinCommon * attribute which is the human-readable display of the number of projects the * currently logged-in user has in common with the contributor. */ startSearch: function () { this.parentImport(false); this.pageToGet(0); this.fetchResults(); }, fetchResults: function () { if (this.parentImport()){ this.importFromParent(); } else { var self = this; self.doneSearching(false); self.notification(false); if (self.query()) { return $.getJSON( '/api/v1/user/search/', { query: self.query(), page: self.pageToGet }, function (result) { var contributors = result.users.map(function (userData) { userData.added = (self.contributors().indexOf(userData.id) !== -1); return new Contributor(userData); }); self.doneSearching(true); self.results(contributors); self.currentPage(result.page); self.numberOfPages(result.pages); self.addNewPaginators(false); } ); } else { self.results([]); self.currentPage(0); self.totalPages(0); self.doneSearching(true); } } }, getContributors: function () { var self = this; self.notification(false); var url = $osf.apiV2Url('nodes/' + window.contextVars.node.id + '/contributors/'); return $.ajax({ url: url, type: 'GET', dataType: 'json', contentType: 'application/vnd.api+json;', crossOrigin: true, xhrFields: {withCredentials: true}, processData: false }).done(function (response) { var contributors = response.data.map(function (contributor) { // contrib ID has the form <nodeid>-<userid> return contributor.id.split('-')[1]; }); self.contributors(contributors); }); }, startSearchParent: function () { this.parentImport(true); this.importFromParent(); }, importFromParent: function () { var self = this; self.doneSearching(false); self.notification(false); return $.getJSON( self.nodeApiUrl + 'get_contributors_from_parent/', {}, function (result) { var contributors = result.contributors.map(function (user) { var added = (self.contributors().indexOf(user.id) !== -1); var updatedUser = $.extend({}, user, {added: added}); return updatedUser; }); var pageToShow = []; var startingSpot = (self.pageToGet() * 5); if (contributors.length > startingSpot + 5){ for (var iterate = startingSpot; iterate < startingSpot + 5; iterate++) { pageToShow.push(contributors[iterate]); } } else { for (var iterateTwo = startingSpot; iterateTwo < contributors.length; iterateTwo++) { pageToShow.push(contributors[iterateTwo]); } } self.doneSearching(true); self.results(pageToShow); self.currentPage(self.pageToGet()); self.numberOfPages(Math.ceil(contributors.length/5)); self.addNewPaginators(true); } ); }, addTips: function (elements) { elements.forEach(function (element) { $(element).find('.contrib-button').tooltip(); }); }, afterRender: function (elm, data) { var self = this; self.addTips(elm, data); }, makeAfterRender: function () { var self = this; return function (elm, data) { return self.afterRender(elm, data); }; }, /** Validate the invite form. Returns a string error message or * true if validation succeeds. */ validateInviteForm: function () { var self = this; // Make sure Full Name is not blank if (!self.inviteName().trim().length) { return 'Full Name is required.'; } if (self.inviteEmail() && !$osf.isEmail(self.inviteEmail())) { return 'Not a valid email address.'; } // Make sure that entered email is not already in selection for (var i = 0, contrib; contrib = self.selection()[i]; ++i) { if (contrib.email) { var contribEmail = contrib.email.toLowerCase().trim(); if (contribEmail === self.inviteEmail().toLowerCase().trim()) { return self.inviteEmail() + ' is already in queue.'; } } } return true; }, postInvite: function () { var self = this; self.inviteError(''); self.canSubmit(false); var validated = self.validateInviteForm(); if (typeof validated === 'string') { self.inviteError(validated); return false; } return self.postInviteRequest(self.inviteName(), self.inviteEmail()); }, add: function (data) { var self = this; data.permission = ko.observable(self.permissionList[1]); //default permission write // All manually added contributors are visible data.visible = true; this.selection.push(data); // Hack: Hide and refresh tooltips $('.tooltip').hide(); $('.contrib-button').tooltip(); }, remove: function (data) { this.selection.splice( this.selection.indexOf(data), 1 ); // Hack: Hide and refresh tooltips $('.tooltip').hide(); $('.contrib-button').tooltip(); }, addAll: function () { var self = this; var selected_ids = self.selection().map(function (user) { return user.id; }); $.each(self.results(), function (idx, result) { if (selected_ids.indexOf(result.id) === -1 && self.contributors().indexOf(result.id) === -1) { self.add(result); } }); }, removeAll: function () { var self = this; $.each(self.selection(), function (idx, selected) { self.remove(selected); }); }, selected: function (data) { var self = this; for (var idx = 0; idx < self.selection().length; idx++) { if (data.id === self.selection()[idx].id) { return true; } } return false; }, selectAllNodes: function () { //select all nodes to add a contributor to. THe changed variable is set here for timing between // treebeard and knockout var self = this; var nodesState = ko.toJS(self.nodesState()); for (var key in nodesState) { if (nodesState[key].enabled) { nodesState[key].checked = true; } } self.nodesState(nodesState); }, selectNoNodes: function () { //select no nodes to add a contributor to. THe changed variable is set here for timing between // treebeard and knockout var self = this; var nodesState = ko.toJS(self.nodesState()); for (var key in nodesState) { if (nodesState[key].enabled && nodesState[key].checked) { nodesState[key].checked = false; } } self.nodesState(nodesState); }, submit: function () { var self = this; $osf.block(); var url = self.nodeApiUrl + 'contributors/'; return $osf.postJSON( url, { users: ko.utils.arrayMap(self.selection(), function (user) { var permission = user.permission().value; //removes the value from the object var tUser = JSON.parse(ko.toJSON(user)); //The serialized user minus functions tUser.permission = permission; //shoving the permission value into permission return tUser; //user with simplified permissions }), node_ids: self.childrenToChange() } ).done(function (response) { if (self.async) { self.contributors($.map(response.contributors, function (contrib) { return contrib.id; })); self.hide(); $osf.unblock(); if (self.callback) { self.callback(response); } } else { window.location.reload(); } }).fail(function (xhr, status, error) { self.hide(); $osf.unblock(); var errorMessage = lodashGet(xhr, 'responseJSON.message') || ('There was a problem trying to add contributors.' + osfLanguage.REFRESH_OR_SUPPORT); $osf.growl('Could not add contributors', errorMessage); Raven.captureMessage('Error adding contributors', { extra: { url: url, status: status, error: error } }); }); }, clear: function () { var self = this; self.page('whom'); self.parentImport(false); self.query(''); self.results([]); self.selection([]); self.childrenToChange([]); self.notification(false); }, postInviteRequest: function (fullname, email) { var self = this; return $osf.postJSON( self.nodeApiUrl + 'invite_contributor/', { 'fullname': fullname, 'email': email } ).done( self.onInviteSuccess.bind(self) ).fail( self.onInviteError.bind(self) ); }, onInviteSuccess: function (result) { var self = this; self.query(''); self.results([]); self.page('whom'); self.add(result.contributor); self.canSubmit(true); }, onInviteError: function (xhr) { var self = this; var response = JSON.parse(xhr.responseText); // Update error message self.inviteError(response.message); self.canSubmit(true); }, hasChildren: function() { var self = this; return (Object.keys(self.nodesOriginal).length > 1); }, /** * get node tree for treebeard from API V1 */ fetchNodeTree: function (treebeardUrl) { var self = this; return $.ajax({ url: treebeardUrl, type: 'GET', dataType: 'json' }).done(function (response) { self.nodesOriginal = projectSettingsTreebeardBase.getNodesOriginal(response[0], self.nodesOriginal); var nodesState = $.extend(true, {}, self.nodesOriginal); var nodeParent = response[0].node.id; //parent node is changed by default nodesState[nodeParent].checked = true; //parent node cannot be changed nodesState[nodeParent].isAdmin = false; self.nodesState(nodesState); }).fail(function (xhr, status, error) { $osf.growl('Error', 'Unable to retrieve project settings'); Raven.captureMessage('Could not GET project settings.', { extra: { url: treebeardUrl, status: status, error: error } }); }); } });
var $osf = require('js/osfHelpers'); var FolderPickerNodeConfigVM = require('js/folderPickerNodeConfig'); var FolderPicker = require('js/folderpicker'); var testUtils = require('./folderPickerTestUtils.js'); var onPickFolderSpy = new sinon.spy(); var resolveLazyloadUrlSpy = new sinon.spy(); var TestSubclassVM = oop.extend(FolderPickerNodeConfigVM, { constructor: function(addonName, url, selector, folderPicker) { this.super.constructor.call(this, addonName, url, selector, folderPicker); this.customField = ko.observable(''); this.messages.submitSettingsSuccess = ko.pureComputed(function(){ return 'SUCCESS'; }); }, _updateCustomFields: function(settings) { this.customField(settings.customField); }, _serializeSettings: function(settings) { return this.folder().name.toUpperCase(); } }); describe('FolderPickerNodeConfigViewModel', () => { var settingsUrl = '/api/v1/12345/addon/config/'; var endpoints = [{ method: 'GET', url: settingsUrl, response: {
var AddContributorViewModel = oop.extend(Paginator, { constructor: function(title, parentId, parentTitle) { this.super.constructor(); var self = this; self.permissions = ['read', 'write', 'admin']; self.title = title; self.parentId = parentId; self.parentTitle = parentTitle; self.page = ko.observable('whom'); self.pageTitle = ko.computed(function() { return { whom: 'Add Contributors', which: 'Select Components', invite: 'Add Unregistered Contributor' }[self.page()]; }); self.query = ko.observable(); self.results = ko.observableArray([]); self.selection = ko.observableArray(); self.notification = ko.observable(''); self.inviteError = ko.observable(''); self.totalPages = ko.observable(0); self.nodes = ko.observableArray([]); self.nodesToChange = ko.observableArray(); $.getJSON( nodeApiUrl + 'get_editable_children/', {}, function(result) { $.each(result.children || [], function(idx, child) { child.margin = NODE_OFFSET + child.indent * NODE_OFFSET + 'px'; }); self.nodes(result.children); } ); self.foundResults = ko.computed(function() { return self.query() && self.results().length; }); self.noResults = ko.computed(function() { return self.query() && !self.results().length; }); self.inviteName = ko.observable(); self.inviteEmail = ko.observable(); self.addingSummary = ko.computed(function() { var names = $.map(self.selection(), function(result) { return result.fullname; }); return names.join(', '); }); }, selectWhom: function() { this.page('whom'); }, selectWhich: function() { this.page('which'); }, gotoInvite: function() { var self = this; self.inviteName(self.query()); self.inviteError(''); self.inviteEmail(''); self.page('invite'); }, goToPage: function(page) { this.page(page); }, /** * A simple Contributor model that receives data from the * contributor search endpoint. Adds an addiitonal displayProjectsinCommon * attribute which is the human-readable display of the number of projects the * currently logged-in user has in common with the contributor. */ startSearch: function() { this.currentPage(0); this.search(); }, search: function() { var self = this; self.notification(false); if (self.query()) { return $.getJSON( '/api/v1/user/search/', { query: self.query(), excludeNode: nodeId, page: self.currentPage }, function(result) { var contributors = result.users.map(function(userData) { return new Contributor(userData); }); self.results(contributors); self.currentPage(result.page); self.numberOfPages(result.pages); self.addNewPaginators(); } ); } else { self.results([]); self.currentPage(0); self.totalPages(0); } }, importFromParent: function() { self.notification(false); $.getJSON( nodeApiUrl + 'get_contributors_from_parent/', {}, function(result) { if (!result.contributors.length) { self.notification({ 'message': 'All contributors from parent already included.', 'level': 'info' }); } self.results(result.contributors); } ); }, recentlyAdded: function() { var self = this; self.notification(false); var url = nodeApiUrl + 'get_recently_added_contributors/?max=' + MAX_RECENT.toString(); return $.getJSON( url, {}, function(result) { if (!result.contributors.length) { self.notification({ 'message': 'All recent collaborators already included.', 'level': 'info' }); } var contribs = []; var numToDisplay = result.contributors.length; for (var i = 0; i < numToDisplay; i++) { contribs.push(new Contributor(result.contributors[i])); } self.results(contribs); self.numberOfPages(1); } ).fail(function(xhr, textStatus, error) { self.notification({ 'message': 'OSF was unable to resolve your request. If this issue persists, ' + 'please report it to <a href="mailto:support@osf.io">support@osf.io</a>.', 'level': 'warning' }); Raven.captureMessage('Could not GET recentlyAdded contributors.', { url: url, textStatus: textStatus, error: error }); }); }, mostInCommon: function() { var self = this; self.notification(false); var url = nodeApiUrl + 'get_most_in_common_contributors/?max=' + MAX_RECENT.toString(); return $.getJSON( url, {}, function(result) { if (!result.contributors.length) { self.notification({ 'message': 'All frequent collaborators already included.', 'level': 'info' }); } var contribs = []; var numToDisplay = result.contributors.length; for (var i = 0; i < numToDisplay; i++) { contribs.push(new Contributor(result.contributors[i])); } self.results(contribs); self.numberOfPages(1); } ).fail(function(xhr, textStatus, error) { self.notification({ 'message': 'OSF was unable to resolve your request. If this issue persists, ' + 'please report it to <a href="mailto:support@osf.io">support@osf.io</a>.', 'level': 'warning' }); Raven.captureMessage('Could not GET mostInCommon contributors.', { url: url, textStatus: textStatus, error: error }); }); }, addTips: function(elements) { elements.forEach(function(element) { $(element).find('.contrib-button').tooltip(); }); }, setupEditable: function(elm, data) { var $elm = $(elm); var $editable = $elm.find('.permission-editable'); $editable.editable({ showbuttons: false, value: 'admin', source: [{ value: 'read', text: 'Read' }, { value: 'write', text: 'Read + Write' }, { value: 'admin', text: 'Administrator' }], success: function(response, value) { data.permission(value); } }); }, afterRender: function(elm, data) { var self = this; self.addTips(elm, data); self.setupEditable(elm, data); }, makeAfterRender: function() { var self = this; return function(elm, data) { return self.afterRender(elm, data); }; }, /** Validate the invite form. Returns a string error message or * true if validation succeeds. */ validateInviteForm: function() { var self = this; // Make sure Full Name is not blank if (!self.inviteName().trim().length) { return 'Full Name is required.'; } if (self.inviteEmail() && !$osf.isEmail(self.inviteEmail())) { return 'Not a valid email address.'; } // Make sure that entered email is not already in selection for (var i = 0, contrib; contrib = self.selection()[i]; ++i) { var contribEmail = contrib.email.toLowerCase().trim(); if (contribEmail === self.inviteEmail().toLowerCase().trim()) { return self.inviteEmail() + ' is already in queue.'; } } return true; }, postInvite: function() { var self = this; self.inviteError(''); var validated = self.validateInviteForm(); if (typeof validated === 'string') { self.inviteError(validated); return false; } return self.postInviteRequest(self.inviteName(), self.inviteEmail()); }, add: function(data) { data.permission = ko.observable('admin'); // All manually added contributors are visible data.visible = true; this.selection.push(data); // Hack: Hide and refresh tooltips $('.tooltip').hide(); $('.contrib-button').tooltip(); }, remove: function(data) { this.selection.splice( this.selection.indexOf(data), 1 ); // Hack: Hide and refresh tooltips $('.tooltip').hide(); $('.contrib-button').tooltip(); }, addAll: function() { $.each(this.results(), function(idx, result) { if (this.selection().indexOf(result) === -1) { this.add(result); } }); }, removeAll: function() { $.each(this.selection(), function(idx, selected) { this.remove(selected); }); }, cantSelectNodes: function() { return this.nodesToChange().length === this.nodes().length; }, cantDeselectNodes: function() { return this.nodesToChange().length === 0; }, selectNodes: function() { this.nodesToChange($osf.mapByProperty(this.nodes(), 'id')); }, deselectNodes: function() { this.nodesToChange([]); }, selected: function(data) { for (var idx = 0; idx < this.selection().length; idx++) { if (data.id === this.selection()[idx].id) { return true; } } return false; }, submit: function() { var self = this; $osf.block(); return $osf.postJSON( nodeApiUrl + 'contributors/', { users: self.selection().map(function(user) { return ko.toJS(user); }), node_ids: self.nodesToChange() } ).done(function() { window.location.reload(); }).fail(function() { $('.modal').modal('hide'); $osf.unblock(); $osf.growl('Error', 'Add contributor failed.'); }); }, clear: function() { var self = this; self.page('whom'); self.query(''); self.results([]); self.selection([]); self.nodesToChange([]); self.notification(false); }, postInviteRequest: function(fullname, email) { var self = this; return $osf.postJSON( nodeApiUrl + 'invite_contributor/', { 'fullname': fullname, 'email': email } ).done( self.onInviteSuccess.bind(self) ).fail( self.onInviteError.bind(self) ); }, onInviteSuccess: function(result) { var self = this; self.query(''); self.results([]); self.page('whom'); self.add(result.contributor); }, onInviteError: function(xhr) { var response = JSON.parse(xhr.responseText); // Update error message this.inviteError(response.message); } });
var s3FolderPickerViewModel = oop.extend(OauthAddonFolderPicker, { bucketLocations: s3Settings.bucketLocations, constructor: function(addonName, url, selector, folderPicker, opts, tbOpts) { var self = this; self.super.constructor(addonName, url, selector, folderPicker, tbOpts); // Non-OAuth fields self.accessKey = ko.observable(''); self.secretKey = ko.observable(''); // Treebeard config self.treebeardOptions = $.extend( {}, OauthAddonFolderPicker.prototype.treebeardOptions, { // TreeBeard Options columnTitles: function() { return [{ title: 'Buckets', width: '75%', sort: false }, { title: 'Select', width: '25%', sort: false }]; }, resolveToggle: function(item) { return ''; }, resolveIcon: function(item) { return m('i.fa.fa-folder-o', ' '); }, }, tbOpts ); }, connectAccount: function() { var self = this; if( !self.accessKey() && !self.secretKey() ){ self.changeMessage('Please enter both an API access key and secret key.', 'text-danger'); return; } if (!self.accessKey() ){ self.changeMessage('Please enter an API access key.', 'text-danger'); return; } if (!self.secretKey() ){ self.changeMessage('Please enter an API secret key.', 'text-danger'); return; } $osf.block(); return $osf.postJSON( self.urls().create, { secret_key: self.secretKey(), access_key: self.accessKey() } ).done(function(response) { $osf.unblock(); self.clearModal(); $('#s3InputCredentials').modal('hide'); self.changeMessage('Successfully added S3 credentials.', 'text-success', null, true); self.updateFromData(response); self.importAuth(); }).fail(function(xhr, status, error) { $osf.unblock(); var message = ''; var response = JSON.parse(xhr.responseText); if (response && response.message) { message = response.message; } self.changeMessage(message, 'text-danger'); Raven.captureMessage('Could not add S3 credentials', { url: self.urls().importAuth, textStatus: status, error: error }); }); }, /** * Tests if the given string is a valid Amazon S3 bucket name. Supports two modes: strict and lax. * Strict is for bucket creation and follows the guidelines at: * * http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html#bucketnamingrules * * However, the US East (N. Virginia) region currently permits much laxer naming rules. The S3 * docs claim this will be changed at some point, but to support our user's already existing * buckets, we provide the lax mode checking. * * Strict checking is the default. * * @param {String} bucketName user-provided name of bucket to validate * @param {Boolean} laxChecking whether to use the more permissive validation */ isValidBucketName: function(bucketName, laxChecking) { if (laxChecking === true) { return /^[a-zA-Z0-9.\-_]{1,255}$/.test(bucketName); } var label = '[a-z0-9]+(?:[a-z0-9\-]*[a-z0-9])?'; var strictBucketName = new RegExp('^' + label + '(?:\\.' + label + ')*$'); var isIpAddress = /^[0-9]+(?:\.[0-9]+){3}$/; return bucketName.length >= 3 && bucketName.length <= 63 && strictBucketName.test(bucketName) && !isIpAddress.test(bucketName); }, /** Reset all fields from S3 credentials input modal */ clearModal: function() { var self = this; self.message(''); self.messageClass('text-info'); self.secretKey(null); self.accessKey(null); }, createBucket: function(self, bucketName, bucketLocation) { $osf.block(); bucketName = bucketName.toLowerCase(); return $osf.postJSON( self.urls().createBucket, { bucket_name: bucketName, bucket_location: bucketLocation } ).done(function(response) { $osf.unblock(); self.loadedFolders(false); self.activatePicker(); var msg = 'Successfully created bucket "' + $osf.htmlEscape(bucketName) + '". You can now select it from the list.'; var msgType = 'text-success'; self.changeMessage(msg, msgType, null, true); }).fail(function(xhr) { var resp = JSON.parse(xhr.responseText); var message = resp.message; var title = resp.title || 'Problem creating bucket'; $osf.unblock(); if (!message) { message = 'Looks like that name is taken. Try another name?'; } bootbox.confirm({ title: $osf.htmlEscape(title), message: $osf.htmlEscape(message), callback: function(result) { if (result) { self.openCreateBucket(); } }, buttons:{ confirm:{ label:'Try again' } } }); }); }, openCreateBucket: function() { var self = this; // Generates html options for key-value pairs in BUCKET_LOCATION_MAP function generateBucketOptions(locations) { var options = ''; for (var location in locations) { if (self.bucketLocations.hasOwnProperty(location)) { options = options + ['<option value="', location, '">', $osf.htmlEscape(locations[location]), '</option>', '\n'].join(''); } } return options; } bootbox.dialog({ title: 'Create a new bucket', message: '<div class="row"> ' + '<div class="col-md-12"> ' + '<form class="form-horizontal" onsubmit="return false"> ' + '<div class="form-group"> ' + '<label class="col-md-4 control-label" for="bucketName">Bucket Name</label> ' + '<div class="col-md-8"> ' + '<input id="bucketName" name="bucketName" type="text" placeholder="Enter bucket name" class="form-control" autofocus> ' + '<div>' + '<span id="bucketModalErrorMessage" ></span>' + '</div>'+ '</div>' + '</div>' + '<div class="form-group"> ' + '<label class="col-md-4 control-label" for="bucketLocation">Bucket Location</label> ' + '<div class="col-md-8"> ' + '<select id="bucketLocation" name="bucketLocation" class="form-control"> ' + generateBucketOptions(self.bucketLocations) + '</select>' + '</div>' + '</div>' + '</form>' + '<span>For more information on locations, click ' + '<a href="http://www.bucketexplorer.com/documentation/amazon-s3--amazon-s3-buckets-and-regions.html">here</a>' + '</span>' + '</div>' + '</div>', buttons: { cancel: { label: 'Cancel', className: 'btn-default' }, confirm: { label: 'Create', className: 'btn-success', callback: function () { var bucketName = $('#bucketName').val(); var bucketLocation = $('#bucketLocation').val(); if (!bucketName) { var errorMessage = $('#bucketModalErrorMessage'); errorMessage.text('Bucket name cannot be empty'); errorMessage[0].classList.add('text-danger'); return false; } else if (!self.isValidBucketName(bucketName, false)) { bootbox.confirm({ title: 'Invalid bucket name', message: 'Amazon S3 buckets can contain lowercase letters, numbers, and hyphens separated by' + ' periods. Please try another name.', callback: function (result) { if (result) { self.openCreateBucket(); } }, buttons: { confirm: { label: 'Try again' } } }); } else { self.createBucket(self, bucketName, bucketLocation); } } } } }); } });
var ProjectSettings = oop.extend( ChangeMessageMixin, { constructor: function(params) { this.super.constructor.call(this); var self = this; self.title = ko.observable(params.currentTitle).extend({ required: { params: true, message: 'Title cannot be blank.' }}); self.description = ko.observable(params.currentDescription); self.titlePlaceholder = params.currentTitle; self.descriptionPlaceholder = params.currentDescription; self.categoryOptions = params.categoryOptions; self.categoryPlaceholder = params.category; self.selectedCategory = ko.observable(params.category); self.disabled = params.disabled || false; if (!params.updateUrl) { throw new Error(language.instantiationErrorMessage); } self.updateUrl = params.updateUrl; self.node_id = params.node_id; self.originalProjectSettings = ko.observable(self.serialize()); self.dirty = ko.pureComputed(function(){ return JSON.stringify(self.originalProjectSettings()) !== JSON.stringify(self.serialize()); }); }, /*error handler*/ updateError: function(xhr, status, error) { var self = this; var errorMessage; if (error === 'BAD REQUEST') { self.changeMessage(language.updateErrorMessage400, 'text-danger'); errorMessage = language.updateErrorMessage400; } else { self.changeMessage(language.updateErrorMessage, 'text-danger'); errorMessage = language.updateErrorMessage; } Raven.captureMessage(errorMessage, { url: self.updateUrl, textStatus: status, err: error }); }, /*update handler*/ updateAll: function() { var self = this; if (!self.dirty()){ self.changeMessage(language.updateSuccessMessage, 'text-success'); return; } var requestPayload = self.serialize(); var request = $osf.ajaxJSON('patch', self.updateUrl, { data: requestPayload, isCors: true }); request.done(function(response) { self.categoryPlaceholder = response.data.attributes.category; self.titlePlaceholder = response.data.attributes.title; self.descriptionPlaceholder = response.data.attributes.description; self.selectedCategory(self.categoryPlaceholder); self.title(self.titlePlaceholder); self.description(self.descriptionPlaceholder); self.originalProjectSettings(self.serialize()); self.changeMessage(language.updateSuccessMessage, 'text-success'); }); request.fail(self.updateError.bind(self)); return request; }, /*cancel handler*/ cancelAll: function() { var self = this; self.selectedCategory(self.categoryPlaceholder); self.title(self.titlePlaceholder); self.description(self.descriptionPlaceholder); self.resetMessage(); }, serialize: function() { var self = this; return { data: { type: 'nodes', id: self.node_id, attributes: { title: self.title(), category: self.selectedCategory(), description: self.description(), } } }; } });
var RegistrationRetractionViewModel = oop.extend( ChangeMessageMixin, { constructor: function(submitUrl, registrationTitle) { this.super.constructor.call(this); var self = this; self.submitUrl = submitUrl; self.registrationTitle = $osf.htmlDecode(registrationTitle); // Truncate title to around 50 chars var parts = self.registrationTitle.slice(0, 50).split(' '); if (parts.length > 1) { self.truncatedTitle = parts.slice(0, -1).join(' '); } else { self.truncatedTitle = parts[0]; } self.justification = ko.observable('').extend({ maxLength: 2048 }); self.confirmationText = ko.observable().extend({ required: true, mustEqual: self.truncatedTitle }); self.disableSave = ko.observable(false); self.valid = ko.computed(function(){ return !self.disableSave() && self.confirmationText.isValid(); }); }, SUBMIT_ERROR_MESSAGE: 'Error submitting your retraction request, please try again. If the problem ' + 'persists, email <a href="mailto:support@osf.iop">support@osf.io</a>', CONFIRMATION_ERROR_MESSAGE: 'Please enter the registration title before clicking Retract Registration', JUSTIFICATON_ERROR_MESSAGE: 'Your justification is too long, please enter a justification with no more ' + 'than 2048 characters long.', MESSAGE_ERROR_CLASS: 'text-danger', onSubmitSuccess: function(response) { window.location = response.redirectUrl; }, onSubmitError: function(xhr, status, errorThrown) { var self = this; self.disableSave(false); self.changeMessage(self.SUBMIT_ERROR_MESSAGE, self.MESSAGE_ERROR_CLASS); Raven.captureMessage('Could not submit registration retraction.', { xhr: xhr, status: status, error: errorThrown }); }, submit: function() { var self = this; self.disableSave(true); // Show errors if invalid if (!self.confirmationText.isValid()) { self.changeMessage(self.CONFIRMATION_ERROR_MESSAGE, self.MESSAGE_ERROR_CLASS); return false; } else if (!self.justification.isValid()) { self.changeMessage(self.JUSTIFICATON_ERROR_MESSAGE, self.MESSAGE_ERROR_CLASS); return false; } else { // Else Submit return $osf.postJSON(self.submitUrl, ko.toJS(self)) .done(self.onSubmitSuccess.bind(self)) .fail(self.onSubmitError.bind(self)); } } });
var LogsViewModel = oop.extend(Paginator, { constructor: function(logs, url) { this.super.constructor.call(this); var self = this; self.loading = ko.observable(false); self.logs = ko.observableArray(logs); self.url = url; self.tzname = ko.pureComputed(function() { var logs = self.logs(); if (logs.length) { var tz = moment(logs[0].date.date).format('ZZ'); return tz; } return ''; }); }, //send request to get more logs when the more button is clicked fetchResults: function(){ var self = this; self.loading(true); return $.ajax({ type: 'get', url: self.url, data:{ page: self.currentPage() }, cache: false }).done(function(response) { self.loading(false); // Initialize LogViewModel self.logs.removeAll(); var logModelObjects = createLogs(response.logs); // Array of Log model objects for (var i=0; i<logModelObjects.length; i++) { self.logs.push(logModelObjects[i]); } self.currentPage(response.page); self.numberOfPages(response.pages); self.addNewPaginators(); }).fail( $osf.handleJSONError ).fail(function() { self.loading(false); }); } });
/* * Maintains the base class for knockoutJS form ViewModels */ 'use strict'; var ko = require('knockout'); var $osf = require('js/osfHelpers'); var oop = require('js/oop'); var ValidationError = oop.extend(Error, { constructor: function (messages, header, level) { this.super.constructor.call(this); this.messages = messages || []; this.header = header || 'Error'; this.level = level || 'warning'; } }); /** * Base class KO viewmodel based forms should inherit from. * * Note: isValid needs to be implemented by subclasses and onError can * optionally be implemented by subclasses to handle ValidationError(s) as desired. */ var FormViewModel = oop.defclass({ constructor: function() {}, isValid: function() { throw new Error('FormViewModel subclass must implement isValid'); }, onError: function(validationError) {
var ChartUniqueVisits = oop.extend(UserFacingChart, { constructor: function(params) { this.super.constructor.call(this, params); }, baseQuery: function() { var self = this; return { type: 'count_unique', params: { event_collection: 'pageviews', interval: 'daily', target_property: 'anon.id' } }; }, _initDataviz: function() { var self = this; return self.super._initDataviz.call(self) .chartType('line') .dateFormat('%b %d') .chartOptions({ tooltip: { format: { title: function(x) { return x.toDateString(); }, name: function() { return 'Visits'; } } }, axis: { y: { tick: { format: self._helpers.hideNonIntegers, } }, x: { tick: { fit: false, }, }, }, }); }, });
/*global describe, it, expect, example, before, after, beforeEach, afterEach, mocha, sinon*/ 'use strict'; var Paginator = require('js/paginator'); var oop = require('js/oop'); var assert = require('chai').assert; sinon.assert.expose(assert, {prefix: ''}); var spy = new sinon.spy(); var TestPaginator = oop.extend(Paginator, { constructor: function(){ this.super.constructor(); }, fetchResults: spy, configure: function(config){ config(this); } }); describe.skip('Paginator', () => { var paginator; var numberOfPages; var currentPage; var pageToGet; beforeEach(() => { paginator = new TestPaginator(); });
var AddonFolderPickerViewModel = oop.extend(FolderPickerViewModel, { constructor: function(addonName, url, selector, folderPicker, opts) { var self = this; self.super.constructor.call(self, addonName, url, selector, folderPicker); // externalAccounts self.accounts = ko.observable([]); self.selectedFolderType = ko.pureComputed(function() { var userHasAuth = self.userHasAuth(); var selected = self.selected(); return (userHasAuth && selected) ? selected.type : ''; }); self.messages.submitSettingsSuccess = ko.pureComputed(function() { var name = self.options.decodeFolder($osf.htmlEscape(self.folder().name)); return 'Successfully linked "' + name + '". Go to the <a href="' + self.urls().files + '">Files page</a> to view your content.'; }); // Overrides var defaults = { onPickFolder: function(evt, item) { evt.preventDefault(); var name = item.data.path !== '/' ? item.data.path : '/ (Full ' + self.addonName + ')'; self.selected({ name: name, path: item.data.path, id: item.data.id }); return false; // Prevent event propagation }, connectAccount: function() { window.location.href = this.urls().auth; }, decodeFolder: function(folder_name) { return folder_name; } }; // Overrides self.options = $.extend({}, defaults, opts); // Treebeard config self.treebeardOptions = $.extend( {}, FolderPickerViewModel.prototype.treebeardOptions, { onPickFolder: function(evt, item) { return this.options.onPickFolder.call(this, evt, item); }.bind(this), resolveLazyloadUrl: function(item) { return item.data.urls.folders; }, decodeFolder: function(item) { return this.options.decodeFolder.call(this, item); }.bind(this) } ); self.folderName = ko.pureComputed(function () { var nodeHasAuth = self.nodeHasAuth(); var folder = self.folder(); var folder_name = self.options.decodeFolder((nodeHasAuth && folder && folder.name) ? folder.name.trim() : ''); return folder_name; }); self.selectedFolderName = ko.pureComputed(function() { var userIsOwner = self.userIsOwner(); var selected = self.selected(); var name = selected.name || 'None'; var folder_name = self.options.decodeFolder(userIsOwner ? name : ''); return folder_name; }); }, afterUpdate: function() { var self = this; if (self.nodeHasAuth() && !self.validCredentials()) { var message; if (self.userIsOwner()) { message = self.messages.invalidCredOwner(); } else { message = self.messages.invalidCredNotOwner(); } self.changeMessage(message, 'text-danger'); } }, _updateCustomFields: function(settings){ var self = this; self.validCredentials(settings.validCredentials); }, /** * Allows a user to create an access token from the nodeSettings page */ connectAccount: function() { this.options.connectAccount.call(this); } });
var CitationsFolderPickerViewModel = oop.extend(FolderPickerViewModel, { constructor: function(addonName, url, selector, folderPicker) { var self = this; self.super.constructor.call(self, addonName, url, selector, folderPicker); self.userAccountId = ko.observable(''); // externalAccounts self.accounts = ko.observable([]); self.messages.submitSettingsSuccess = ko.pureComputed(function(){ return 'Successfully linked "' + $osf.htmlEscape(self.folder().name) + '". Go to the <a href="' + self.urls().files + '">Overview page</a> to view your citations.'; }); self.treebeardOptions = $.extend( {}, FolderPickerViewModel.prototype.treebeardOptions, { /** Callback for chooseFolder action. * Just changes the ViewModel's self.selected observable to the selected * folder. */ onPickFolder: function(evt, item){ evt.preventDefault(); this.selected({ name: item.data.name, id: item.data.id }); return false; // Prevent event propagation }.bind(this), lazyLoadPreprocess: function(data) { return data.contents.filter(function(item) { return item.kind === 'folder'; }); }, resolveLazyloadUrl: function(item) { return this.urls().folders + item.data.id + '/?view=folders'; }.bind(this) }); }, fetchAccounts: function() { var self = this; var ret = $.Deferred(); var request = $.get(self.urls().accounts); request.then(function(data) { ret.resolve(data.accounts); }); request.fail(function(xhr, textStatus, error) { self.changeMessage(self.messages.updateAccountsError(), 'text-danger'); Raven.captureMessage('Could not GET ' + self.addonName + ' accounts for user', { url: self.url, textStatus: textStatus, error: error }); ret.reject(xhr, textStatus, error); }); return ret.promise(); }, updateAccounts: function() { var self = this; return self.fetchAccounts() .done(function(accounts) { self.accounts( $.map(accounts, function(account) { return { name: account.display_name, id: account.id }; }) ); }); }, /** * Allows a user to create an access token from the nodeSettings page */ connectAccount: function() { var self = this; window.oauthComplete = function(res) { // Update view model based on response self.changeMessage(self.messages.connectAccountSuccess(), 'text-success', 3000); self.userHasAuth(true); self.importAuth.call(self); }; window.open(self.urls().auth); }, connectExistingAccount: function(account_id) { var self = this; return $osf.putJSON( self.urls().importAuth, { external_account_id: account_id } ).then(self.onImportSuccess.bind(self), self.onImportError.bind(self)); }, _updateCustomFields: function(settings){ this.userAccountId(settings.userAccountId); }, _serializeSettings: function(){ return { external_account_id: this.userAccountId(), external_list_id: this.selected().id, external_list_name: this.selected().name }; }, importAuth: function() { var self = this; self.updateAccounts() .then(function(){ if (self.accounts().length > 1) { bootbox.prompt({ title: 'Choose ' + self.addonName + ' Access Token to Import', inputType: 'select', inputOptions: ko.utils.arrayMap( self.accounts(), function(item) { return { text: item.name, value: item.id }; } ), value: self.accounts()[0].id, callback: function(accountId) { if (accountId) { self.connectExistingAccount.call(self, (accountId)); } }, buttons:{ confirm:{ label: 'Import' } } }); } else { bootbox.confirm({ title: 'Import ' + self.addonName + ' access token', message: self.messages.confirmAuth(), callback: function(confirmed) { if (confirmed) { self.connectExistingAccount.call(self, (self.accounts()[0].id)); } }, buttons:{ confirm:{ label:'Import' } } }); } }); } });
var OauthAddonFolderPickerViewModel = oop.extend(FolderPickerViewModel, { constructor: function(addonName, url, selector, folderPicker, opts) { var self = this; self.super.constructor.call(self, addonName, url, selector, folderPicker); // externalAccounts self.accounts = ko.observableArray(); self.selectedFolderType = ko.pureComputed(function() { var userHasAuth = self.userHasAuth(); var selected = self.selected(); return (userHasAuth && selected) ? selected.type : ''; }); self.messages.submitSettingsSuccess = ko.pureComputed(function() { return 'Successfully linked "' + $osf.htmlEscape(self.options.decodeFolder(self.folder().name)) + '". Go to the <a href="' + self.urls().files + '">Files page</a> to view your content.'; }); var defaults = { onPickFolder: function(evt, item) { evt.preventDefault(); var name = item.data.path !== '/' ? item.data.path : '/ (Full ' + self.addonName + ')'; self.selected({ name: name, path: item.data.path, id: item.data.id }); return false; // Prevent event propagation }, /** * Allows a user to create an access token from the nodeSettings page */ connectAccount: function() { var self = this; window.oauthComplete = function(res) { // Update view model based on response self.updateAccounts().then(function() { try{ $osf.putJSON( self.urls().importAuth, { external_account_id: self.accounts()[0].id } ).done(self.onImportSuccess.bind(self) ).fail(self.onImportError.bind(self)); self.changeMessage(self.messages.connectAccountSuccess(), 'text-success', 3000); } catch(err){ self.changeMessage(self.messages.connectAccountDenied(), 'text-danger', 6000); } }); }; window.open(self.urls().auth); }, decodeFolder: function(folder_name) { return folder_name; } }; // Overrides self.options = $.extend({}, defaults, opts); // Treebeard config self.treebeardOptions = $.extend( {}, FolderPickerViewModel.prototype.treebeardOptions, { onPickFolder: function(evt, item) { return this.options.onPickFolder.call(this, evt, item); }.bind(this), resolveLazyloadUrl: function(item) { return item.data.urls.folders; } } ); }, afterUpdate: function() { var self = this; if (self.nodeHasAuth() && !self.validCredentials()) { var message; if (self.userIsOwner()) { message = self.messages.invalidCredOwner(); } else { message = self.messages.invalidCredNotOwner(); } self.changeMessage(message, 'text-danger'); } }, _updateCustomFields: function(settings){ this.validCredentials(settings.validCredentials); }, /** * Allows a user to create an access token from the nodeSettings page */ connectAccount: function() { this.options.connectAccount.call(this); }, /** * Imports addon settings from user's account. If multiple addon accounts are connected, allow user to pick between them. */ importAuth: function(){ var self = this; self.updateAccounts().then(function () { if (self.accounts().length > 1) { bootbox.prompt({ title: 'Choose ' + $osf.htmlEscape(self.addonName) + ' Account to Import', inputType: 'select', inputOptions: ko.utils.arrayMap( self.accounts(), function(item) { return { text: $osf.htmlEscape(item.name), value: item.id }; } ), value: self.accounts()[0].id, callback: (self.connectExistingAccount.bind(self)), buttons: { confirm:{ label:'Import', } } }); } else { bootbox.confirm({ title: 'Import ' + self.addonName + ' Account?', message: self.messages.confirmAuth(), callback: function(confirmed) { if (confirmed) { self.connectExistingAccount.call(self, (self.accounts()[0].id)); } }, buttons: { confirm: { label:'Import', } } }); } }); }, /** * Associates selected external account with this node, or handles error. */ connectExistingAccount: function(account_id) { var self = this; if (account_id !== null) { return $osf.putJSON( self.urls().importAuth, { external_account_id: account_id } ).done(self.onImportSuccess.bind(self) ).fail(self.onImportError.bind(self)); } return; }, updateAccounts: function() { var self = this; var request = $.get(self.urls().accounts); return request.done(function(data) { self.accounts(data.accounts.map(function(account) { return { name: account.display_name, id: account.id }; })); }).fail(function(xhr, textStatus, error) { self.changeMessage(self.messages.UPDATE_ACCOUNTS_ERROR(), 'text-warning'); Raven.captureMessage('Could not GET ' + self.addonName + ' accounts for user', { extra: { url: self.url, textStatus: textStatus, error: error } }); }); }, });
require('knockout.validation'); var $osf = require('js/osfHelpers'); var oop = require('js/oop'); var formViewModel = require('js/formViewModel'); var ForgotPasswordViewModel = oop.extend(formViewModel.FormViewModel, { constructor: function() { var self = this; self.super.constructor.call(self); self.username = ko.observable('').extend({ required: true, email: true }); }, isValid: function() { var validationError = new formViewModel.ValidationError(); if (!this.username.isValid()) { validationError.messages.push('Please enter a valid email address.'); throw validationError; } else { return true; } } }); var ForgotPassword = function(selector) { this.viewModel = new ForgotPasswordViewModel(); $osf.applyBindings(this.viewModel, selector); };