diff --git a/mod-picker/app/assets/javascripts/BackendAPI/configFilesService.js b/mod-picker/app/assets/javascripts/BackendAPI/configFilesService.js index f1f7c39f0..9cc657379 100644 --- a/mod-picker/app/assets/javascripts/BackendAPI/configFilesService.js +++ b/mod-picker/app/assets/javascripts/BackendAPI/configFilesService.js @@ -1,6 +1,32 @@ app.service('configFilesService', function() { var service = this; + this.groupHasChildren = function(group) { + for (var i = 0; i < group.children.length; i++) { + var child = group.children[i]; + if (!child._destroy) return true; + } + return false; + }; + + this.firstAvailableConfig = function(group) { + return group.children.find(function(child) { + return !child._destroy; + }); + }; + + this.recoverConfigFileGroups = function(model) { + model.forEach(function(group) { + if (group._destroy && service.groupHasChildren(group)) { + delete group._destroy; + if (!group.activeConfig) { + group.activeConfig = service.firstAvailableConfig(group); + group.activeConfig.active = true; + } + } + }); + }; + this.addConfigFile = function(model, configFile) { var foundGroup = model.find(function(group) { return group.id == configFile.config_file.mod.id; @@ -43,6 +69,9 @@ app.service('configFilesService', function() { service.addCustomConfigFile(model, customConfigFile); }); model.forEach(function(group) { + group.children.sort(function(a, b) { + return a.filename - b.filename; + }); group.activeConfig = group.children[0]; group.activeConfig.active = true; }); diff --git a/mod-picker/app/assets/javascripts/BackendAPI/modListService.js b/mod-picker/app/assets/javascripts/BackendAPI/modListService.js index 76360e1a3..88a2e092a 100644 --- a/mod-picker/app/assets/javascripts/BackendAPI/modListService.js +++ b/mod-picker/app/assets/javascripts/BackendAPI/modListService.js @@ -180,6 +180,7 @@ app.service('modListService', function(backend, $q, userTitleService, contributi this.newModListMod = function(mod_list_mod) { var action = $q.defer(); backend.post('/mod_list_mods', {mod_list_mod: mod_list_mod}).then(function(data) { + modService.associateModImage(data.mod_list_mod.mod); userTitleService.associateTitles(data.mod_compatibility_notes); userTitleService.associateTitles(data.plugin_compatibility_notes); userTitleService.associateTitles(data.install_order_notes); diff --git a/mod-picker/app/assets/javascripts/BackendAPI/modService.js b/mod-picker/app/assets/javascripts/BackendAPI/modService.js index 433eceb42..4d2eeeaf5 100644 --- a/mod-picker/app/assets/javascripts/BackendAPI/modService.js +++ b/mod-picker/app/assets/javascripts/BackendAPI/modService.js @@ -1,4 +1,4 @@ -app.service('modService', function(backend, $q, pageUtils, objectUtils, contributionService, userTitleService, reviewSectionService, recordGroupService, pluginService, assetUtils) { +app.service('modService', function(backend, $q, pageUtils, objectUtils, contributionService, userTitleService, reviewSectionService, recordGroupService, pluginService, assetUtils, modOptionUtils) { var service = this; this.retrieveMods = function(options, pageInformation) { @@ -110,20 +110,15 @@ app.service('modService', function(backend, $q, pageUtils, objectUtils, contribu this.retrieveModAnalysis = function(modId) { var output = $q.defer(); backend.retrieve('/mods/' + modId + '/' + 'analysis').then(function(analysis) { - // create nestedAssets tree - analysis.nestedAssets = assetUtils.getNestedAssets(analysis.assets); - assetUtils.sortNestedAssets(analysis.nestedAssets); - // prepare plugin data for display recordGroupService.associateGroups(analysis.plugins); pluginService.combineAndSortMasters(analysis.plugins); pluginService.associateOverrides(analysis.plugins); pluginService.sortErrors(analysis.plugins); - // set default options to active - analysis.mod_options.forEach(function(option) { - option.active = option.default; - }); + // create nested mod options tree + modOptionUtils.activateDefaultModOptions(analysis.mod_options); + analysis.nestedOptions = modOptionUtils.getNestedModOptions(analysis.mod_options); output.resolve(analysis); }, function(response) { diff --git a/mod-picker/app/assets/javascripts/BackendAPI/notificationService.js b/mod-picker/app/assets/javascripts/BackendAPI/notificationService.js index 82b787178..389bed463 100644 --- a/mod-picker/app/assets/javascripts/BackendAPI/notificationService.js +++ b/mod-picker/app/assets/javascripts/BackendAPI/notificationService.js @@ -10,6 +10,17 @@ app.service('notificationService', function($q, backend, pageUtils) { return action.promise; }; + this.retrieveEvents = function(options, pageInformation) { + var action = $q.defer(); + backend.post('/events', options).then(function(data) { + pageUtils.getPageInformation(data, pageInformation, options.page); + action.resolve(data); + }, function(response) { + action.reject(response); + }); + return action.promise; + }; + this.retrieveRecent = function() { return backend.retrieve('/notifications/recent'); }; diff --git a/mod-picker/app/assets/javascripts/BackendAPI/tagService.js b/mod-picker/app/assets/javascripts/BackendAPI/tagService.js index 7e528be54..26cf82172 100644 --- a/mod-picker/app/assets/javascripts/BackendAPI/tagService.js +++ b/mod-picker/app/assets/javascripts/BackendAPI/tagService.js @@ -1,13 +1,30 @@ -app.service('tagService', function(backend, $q) { - this.retrieveTags = function() { - var tags = $q.defer(); - var postData = { - filters: {} - }; - backend.post('/tags', postData).then(function(data) { - tags.resolve(data); +app.service('tagService', function(backend, $q, pageUtils) { + this.retrieveAllTags = function() { + var params = { game: window._current_game_id }; + return backend.retrieve('/all_tags', params); + }; + + this.retrieveTags = function(options, pageInformation) { + var action = $q.defer(); + backend.post('/tags', options).then(function(data) { + // resolve page information and data + pageUtils.getPageInformation(data, pageInformation, options.page); + action.resolve(data); + }, function(response) { + action.reject(response); }); - return tags.promise; + return action.promise; + }; + + this.hideTag = function(tagId, hidden) { + return backend.post('/tags/' + tagId + '/hide', {hidden: hidden}); + }; + + this.updateTag = function(tag) { + var params = { + tag: { text: tag.text } + }; + return backend.update('/tags/' + tag.id, params); }; this.updateModTags = function(mod, tags) { diff --git a/mod-picker/app/assets/javascripts/Directives/base/notification.js b/mod-picker/app/assets/javascripts/Directives/base/notification.js index 6f4fe8ed1..ad0e27fd3 100644 --- a/mod-picker/app/assets/javascripts/Directives/base/notification.js +++ b/mod-picker/app/assets/javascripts/Directives/base/notification.js @@ -12,7 +12,11 @@ app.directive('notification', function() { app.controller('notificationController', function($scope, $sce, $interpolate, notificationsFactory) { angular.inherit($scope, 'notification'); - $scope.event = $scope.notification.event; + if ($scope.notification.hasOwnProperty('event')) { + $scope.event = $scope.notification.event; + } else { + $scope.event = $scope.notification; + } $scope.content = $scope.event.content; // get template based on event type diff --git a/mod-picker/app/assets/javascripts/Directives/contributions/readMore.js b/mod-picker/app/assets/javascripts/Directives/contributions/readMore.js index 9779be9ab..819876327 100644 --- a/mod-picker/app/assets/javascripts/Directives/contributions/readMore.js +++ b/mod-picker/app/assets/javascripts/Directives/contributions/readMore.js @@ -11,16 +11,20 @@ app.directive('readMore', function() { }); app.controller('readMoreController', function($scope) { - $scope.reducedText = $scope.text.reduceText($scope.numWords); - - // TODO: add methods to contribution models to trim beginning/end whitespace - // for text_body; current regex is to stop read more from showing up unnecessarily. - $scope.text = $scope.text.trim(); - $scope.expandable = $scope.text.wordCount() > $scope.numWords * 1.25 && - $scope.reducedText.length < $scope.text.length; - $scope.expanded = false; $scope.toggleExpansion = function() { $scope.expanded = !$scope.expanded; }; + + $scope.getReducedText = function() { + $scope.reducedText = $scope.text.reduceText($scope.numWords); + // TODO: add methods to contribution models to trim beginning/end whitespace + // for text_body; current regex is to stop read more from showing up unnecessarily. + $scope.text = $scope.text.trim(); + $scope.expandable = $scope.text.wordCount() > $scope.numWords * 1.25 && + $scope.reducedText.length < $scope.text.length; + }; + + $scope.$watch('text', $scope.getReducedText); + $scope.getReducedText(); }); diff --git a/mod-picker/app/assets/javascripts/Directives/editMod/modAnalysisManager.js b/mod-picker/app/assets/javascripts/Directives/editMod/modAnalysisManager.js index b2267c84c..2f4559a4e 100644 --- a/mod-picker/app/assets/javascripts/Directives/editMod/modAnalysisManager.js +++ b/mod-picker/app/assets/javascripts/Directives/editMod/modAnalysisManager.js @@ -23,6 +23,17 @@ app.controller('modAnalysisManagerController', function($scope, $rootScope, plug document.getElementById('analysis-input').click(); }; + $scope.clearAnalysis = function() { + var modOptions = $scope.mod.analysis ? $scope.mod.analysis.mod_options : $scope.mod.mod_options; + modOptions.forEach(function(modOption) { + if (modOption.id) { + modOption._destroy = true; + } else { + $scope.removeOption(modOption); + } + }); + }; + $scope.removeOption = function(option) { var modOptions = $scope.mod.analysis.mod_options; var index = modOptions.indexOf(option); @@ -100,11 +111,11 @@ app.controller('modAnalysisManagerController', function($scope, $rootScope, plug }; $scope.optionNamesMatch = function(option, oldOption) { - return option.name === oldOption.name; + return option.name === oldOption.name && !oldOption._destroy; }; $scope.optionSizesMatch = function(option, oldOption) { - return option.size == oldOption.size; + return option.size == oldOption.size && !oldOption._destroy; }; $scope.findOldOption = function(oldOptions, option) { diff --git a/mod-picker/app/assets/javascripts/Directives/shared/editTagModal.js b/mod-picker/app/assets/javascripts/Directives/shared/editTagModal.js new file mode 100644 index 000000000..4e9ace7a3 --- /dev/null +++ b/mod-picker/app/assets/javascripts/Directives/shared/editTagModal.js @@ -0,0 +1,31 @@ +app.directive('editTagModal', function() { + return { + restrict: 'E', + templateUrl: '/resources/directives/shared/editTagModal.html', + controller: 'editTagModalController', + scope: false + } +}); + +app.controller('editTagModalController', function($scope, tagService, formUtils, eventHandlerFactory) { + // inherited functions + $scope.unfocusTagModal = formUtils.unfocusModal($scope.toggleTagModal); + + // shared function setup + eventHandlerFactory.buildModalMessageHandlers($scope); + + $scope.saveTag = function() { + tagService.updateTag($scope.activeTag).then(function() { + $scope.$emit('modalSuccessMessage', 'Updated tag "' + $scope.activeTag.text + '"successfully'); + $scope.$applyAsync(function() { + $scope.originalTag.text = $scope.activeTag.text; + }); + }, function(response) { + var params = { + text: 'Error updating tag: '+$scope.activeTag.text, + response: response + }; + $scope.$emit('modalErrorMessage', params); + }); + }; +}); diff --git a/mod-picker/app/assets/javascripts/Directives/shared/modOptionTree.js b/mod-picker/app/assets/javascripts/Directives/shared/modOptionTree.js new file mode 100644 index 000000000..cdf2b426b --- /dev/null +++ b/mod-picker/app/assets/javascripts/Directives/shared/modOptionTree.js @@ -0,0 +1,27 @@ +app.directive('modOptionTree', function() { + return { + restrict: 'E', + templateUrl: '/resources/directives/shared/modOptionTree.html', + scope: { + modOptions: '=' + }, + controller: 'modOptionTreeController' + } +}); + +app.controller('modOptionTreeController', function($scope) { + $scope.toggleModOption = function(option) { + // recurse into children + option.children && option.children.forEach(function(child) { + if (child.active && !option.active) { + child.active = false; + } + }); + // emit message + $scope.$emit('toggleModOption', option); + }; + + $scope.toggleExpansion = function(option) { + option.expanded = !option.expanded; + } +}); diff --git a/mod-picker/app/assets/javascripts/Directives/shared/tagSelector.js b/mod-picker/app/assets/javascripts/Directives/shared/tagSelector.js index 74448f6af..109f67a41 100644 --- a/mod-picker/app/assets/javascripts/Directives/shared/tagSelector.js +++ b/mod-picker/app/assets/javascripts/Directives/shared/tagSelector.js @@ -18,11 +18,12 @@ app.directive('tagSelector', function() { } }); -app.controller('tagSelectorController', function($scope, tagService) { +app.controller('tagSelectorController', function($scope, $element, $timeout, tagService) { $scope.rawNewTags = []; $scope.removedTags = []; + var addTagButton = $element[0].firstChild.lastElementChild; - tagService.retrieveTags().then(function(data) { + tagService.retrieveAllTags().then(function(data) { $scope.tags = data; }); @@ -36,6 +37,12 @@ app.controller('tagSelectorController', function($scope, tagService) { } }; + $scope.focusAddTag = function() { + $timeout(function() { + addTagButton.focus(); + }, 50); + }; + $scope.removeTag = function($index) { $scope.rawNewTags.splice($index, 1); $scope.storeTags(); diff --git a/mod-picker/app/assets/javascripts/Factories/actionsFactory.js b/mod-picker/app/assets/javascripts/Factories/actionsFactory.js index e76e1acd0..2667f858b 100644 --- a/mod-picker/app/assets/javascripts/Factories/actionsFactory.js +++ b/mod-picker/app/assets/javascripts/Factories/actionsFactory.js @@ -236,4 +236,38 @@ app.service('actionsFactory', function() { }] }]; }; + + /* tag actions */ + this.tagIndexActions = function() { + return [{ + caption: "Edit", + title: "Edit this tag's text", + hidden: function($scope, item) { + return item.hidden || !$scope.permissions.canModerate; + }, + execute: function($scope, item) { + $scope.$emit('editTag', item); + } + }, { + caption: "Recover", + title: "This tag is hidden. Click to recover it.", + class: 'green-box', + hidden: function($scope, item) { + return !item.hidden; + }, + execute: function($scope, item) { + $scope.$emit('recoverTag', item); + } + }, { + caption: "Hide", + title: "This tag is publicly visible. Click to hide it.", + class: 'yellow-box', + hidden: function($scope, item) { + return item.hidden; + }, + execute: function($scope, item) { + $scope.$emit('hideTag', item); + } + }] + }; }); \ No newline at end of file diff --git a/mod-picker/app/assets/javascripts/Factories/baseFactory.js b/mod-picker/app/assets/javascripts/Factories/baseFactory.js index 1007565cf..400377485 100644 --- a/mod-picker/app/assets/javascripts/Factories/baseFactory.js +++ b/mod-picker/app/assets/javascripts/Factories/baseFactory.js @@ -120,6 +120,7 @@ app.service('baseFactory', function() { index: 0, group_id: 0, name: "", + url: "", description: "", is_utility: false } diff --git a/mod-picker/app/assets/javascripts/Factories/columnsFactory.js b/mod-picker/app/assets/javascripts/Factories/columnsFactory.js index 730ff6bb8..537d2f23b 100644 --- a/mod-picker/app/assets/javascripts/Factories/columnsFactory.js +++ b/mod-picker/app/assets/javascripts/Factories/columnsFactory.js @@ -488,11 +488,11 @@ app.service('columnsFactory', function() { link: function(item) { if (item.mod) { return "#/mod/" + item.mod.id; + } else if (item.url) { + return item.url; } }, class: "primary-column", - sortData: "mods.name", - invertSort: true, dynamic: true }, { @@ -500,32 +500,28 @@ app.service('columnsFactory', function() { visibility: false, label: "Aliases", data: "mod.aliases", - class: "aliases-column", - invertSort: true + class: "aliases-column" }, { group: "General", visibility: true, label: "Authors", data: "mod.authors", - class: "author-column", - invertSort: true + class: "author-column" }, { group: "General", visibility: true, label: "Primary Category", data: "mod.primary_category.name", - class: "category-column", - unsortable: true + class: "category-column" }, { group: "General", visibility: false, label: "Secondary Category", data: "mod.secondary_category.name", - class: "category-column", - unsortable: true + class: "category-column" }, { group: "General", @@ -1092,4 +1088,45 @@ app.service('columnsFactory', function() { ]; }; + this.tagColumns = function() { + return [ + { + group: "General", + visibility: true, + required: true, + label: "Text", + data: "text", + invertSort: true, + dynamic: true + }, + { + group: "General", + visibility: true, + label: "Submitter", + data: "submitter.username", + link: function(item) { + return "#/user/" + item.submitter.id; + }, + invertSort: true + }, + { + group: "General", + visibility: true, + label: "Mods Count", + data: "mods_count", + filter: "number" + }, + { + group: "General", + visibility: true, + label: "Mod Lists Count", + data: "mod_lists_count", + filter: "number" + } + ] + }; + + this.tagColumnGroups = function() { + return ["General"]; + }; }); diff --git a/mod-picker/app/assets/javascripts/Factories/filtersFactory.js b/mod-picker/app/assets/javascripts/Factories/filtersFactory.js index c66f7d21a..8550f46eb 100644 --- a/mod-picker/app/assets/javascripts/Factories/filtersFactory.js +++ b/mod-picker/app/assets/javascripts/Factories/filtersFactory.js @@ -1556,5 +1556,42 @@ app.service("filtersFactory", function() { ); }; + this.tagGeneralFilters = function() { + return [ + factory.searchFilter, + factory.submitterFilter, + factory.hiddenFilter, + factory.unhiddenFilter + ] + }; + + this.tagStatisticFilters = function() { + return [ + { + label: "Mods Count", + common: true, + data: "mods_count", + type: "Range", + max: 100, + param: "mc" + }, + { + label: "Mod Lists Count", + common: true, + data: "mod_lists_count", + type: "Range", + max: 100, + param: "mlc" + } + ] + }; + + this.tagFilters = function() { + return Array.prototype.concat( + factory.tagGeneralFilters(), + factory.tagStatisticFilters() + ); + }; + return factory; }); diff --git a/mod-picker/app/assets/javascripts/Factories/listMetaFactory.js b/mod-picker/app/assets/javascripts/Factories/listMetaFactory.js index 3e18e7abd..b4699e617 100644 --- a/mod-picker/app/assets/javascripts/Factories/listMetaFactory.js +++ b/mod-picker/app/assets/javascripts/Factories/listMetaFactory.js @@ -1,9 +1,11 @@ app.service('listMetaFactory', function($q, $timeout, modListService, categoryService, listUtils, sortUtils) { this.buildModelFunctions = function ($scope, label, dataKey, nameKey, customName) { var capLabel = label.capitalize(); + var capDataKey = dataKey.capitalize(); var pluralLabel = label + 's'; var countKey = pluralLabel + '_count'; - var idKey = label + '_id'; + var customCountKey = 'custom_' + pluralLabel + '_count'; + var idKey = dataKey + '_id'; var $rootScope = $scope.$root; // ITEM RECOVERY @@ -15,11 +17,37 @@ app.service('listMetaFactory', function($q, $timeout, modListService, categorySe $scope.updateTabs(); }; + var incrementCustomCounter = function() { + $scope.mod_list[customCountKey] += 1; + $scope.updateTabs(); + }; + + var incrementAppropriateCounter = function(modListItem) { + if (modListItem.hasOwnProperty(dataKey)) { + incrementCounter(); + } else { + incrementCustomCounter(); + } + }; + var decrementCounter = function() { $scope.mod_list[countKey] -= 1; $scope.updateTabs(); }; + var decrementCustomCounter = function() { + $scope.mod_list[customCountKey] -= 1; + $scope.updateTabs(); + }; + + var decrementAppropriateCounter = function(modListItem) { + if (modListItem.hasOwnProperty(dataKey)) { + decrementCounter(); + } else { + decrementCustomCounter(); + } + }; + var customCallback = function(functionLabel, arg) { var callbackLabel = functionLabel + 'Callback'; if ($scope[callbackLabel]) { @@ -61,7 +89,7 @@ app.service('listMetaFactory', function($q, $timeout, modListService, categorySe // destroy the item delete modListItem._destroy; - incrementCounter(); + incrementAppropriateCounter(modListItem); var itemId = modListItem[dataKey]&& modListItem[dataKey].id; $rootScope.$broadcast(recoverMessage, itemId); customCallback(recoverLabel, modListItem); @@ -75,8 +103,8 @@ app.service('listMetaFactory', function($q, $timeout, modListService, categorySe // ADDING NEW ITEMS var addNewLabel = 'addNew' + capLabel; var addNewItemLabel = addNewLabel + 'Item'; - var newModListItemKey = 'newModList' + capLabel; - var mod_list_item_key = 'mod_list_' + label; + var newModListItemKey = 'newModList' + capDataKey; + var mod_list_item_key = 'mod_list_' + dataKey; var itemAddedMessage = dataKey + 'Added'; $scope[addNewItemLabel] = function(modListItem) { @@ -116,10 +144,10 @@ app.service('listMetaFactory', function($q, $timeout, modListService, categorySe // ADD CUSTOM ITEM var addCustomLabel = 'addCustom' + capLabel; - var newModListCustomItemKey = 'newModListCustom' + dataKey.capitalize(); + var newModListCustomItemKey = 'newModListCustom' + capDataKey; var mod_list_custom_item_key = 'mod_list_custom_' + dataKey; var customKey = 'custom_' + pluralLabel; - var customItemAddedMessage = 'custom' + dataKey.capitalize() + 'Added'; + var customItemAddedMessage = 'custom' + capDataKey + 'Added'; $scope[addCustomLabel] = function(noteId) { var custom_item = {}; @@ -134,7 +162,7 @@ app.service('listMetaFactory', function($q, $timeout, modListService, categorySe $scope.mod_list[customKey].push(modListCustomItem); $scope.model[pluralLabel].push(modListCustomItem); $scope.originalModList[customKey].push(angular.copy(modListCustomItem)); - incrementCounter(); + incrementCustomCounter(); // update modules $scope.$broadcast(customItemAddedMessage); @@ -179,7 +207,7 @@ app.service('listMetaFactory', function($q, $timeout, modListService, categorySe $scope[removeLabel] = function(modListItem) { modListItem._destroy = true; - decrementCounter(); + decrementAppropriateCounter(modListItem); // update modules var itemId = modListItem[dataKey] && modListItem[dataKey].id; diff --git a/mod-picker/app/assets/javascripts/Factories/notificationsFactory.js b/mod-picker/app/assets/javascripts/Factories/notificationsFactory.js index f6da4bbba..a332c32a9 100644 --- a/mod-picker/app/assets/javascripts/Factories/notificationsFactory.js +++ b/mod-picker/app/assets/javascripts/Factories/notificationsFactory.js @@ -6,6 +6,8 @@ app.service('notificationsFactory', function() { return "A new "+label+" has been added to ((contentLink))"; }; this.added = { + Mod: "A new mod, ((contentLink)), has been submitted", + ModList: "A new mod list, ((contentLink)), has been created", Review: contributionAddedTemplate("review"), CompatibilityNote: contributionAddedTemplate("compatibility note"), InstallOrderNote: contributionAddedTemplate("install order note"), @@ -15,7 +17,7 @@ app.service('notificationsFactory', function() { ModTag: contributionAddedTemplate("tag"), ModListTag: contributionAddedTemplate("tag"), ModAuthor: "((authorUserClause)) been added as ((authorRole)) for ((contentLink))", - ReputationLink: "((endorser)) has endorsed you" + ReputationLink: "((endorser)) has endorsed ((endorsee))" }; this.updated = { @@ -24,10 +26,10 @@ app.service('notificationsFactory', function() { }; this.removed = { - ModTag: "Your tag ((tagText)) was removed from ((contentLink))", - ModListTag: "Your tag ((tagText)) was removed from ((contentLink))", + ModTag: "((ownershipClause)) tag ((tagText)) was removed from ((contentLink))", + ModListTag: "((ownershipClause)) tag ((tagText)) was removed from ((contentLink))", ModAuthor: "((authorUserClause)) been removed as ((authorRole)) for ((contentLink))", - ReputationLink: "((endorser)) has unendorsed you" + ReputationLink: "((endorser)) has unendorsed ((endorsee))" }; // Handles hidden, unhidden, approved, and unapproved events @@ -64,7 +66,7 @@ app.service('notificationsFactory', function() { this.milestones = { Mod: "((contentLink)) has ((getMilestoneValue)) stars.", ModList: "((contentLink)) has ((getMilestoneValue)) stars.", - UserReputation: "You have ((getMilestoneValue)) reputation. ((getPermissions))" + UserReputation: "((userClause)) ((getMilestoneValue)) reputation. ((getPermissions))" }; this.permissions = [ @@ -103,6 +105,7 @@ app.service('notificationsFactory', function() { ModAuthor: associatedModLink, Mod: '{{content.name}}', ModList: '{{content.name}}', + UserReputation: '{{content.user.username}} has', Comment: { key: "commentable", Article: '{{content.commentable.title}}', @@ -128,10 +131,10 @@ app.service('notificationsFactory', function() { var noteCorrectionDescription = function(noteType) { var noteTypeDashed = noteType.replace(' ', '-'); - return 'Your correction on {{content.correctable.submitter.username}}\'s '+noteType+' note'; + return '((ownershipClause)) correction on {{content.correctable.submitter.username}}\'s '+noteType+' note'; }; this.correctionDescriptions = { - Mod: 'Your appeal to mark {{content.correctable.name}} as {{content.mod_status}}', + Mod: '((ownershipClause)) appeal to mark {{content.correctable.name}} as {{content.mod_status}}', CompatibilityNote: noteCorrectionDescription('compatibility'), InstallOrderNote: noteCorrectionDescription('install order'), LoadOrderNote: noteCorrectionDescription('load order') @@ -258,12 +261,23 @@ app.service('notificationsFactory', function() { return '{{content.source_user.username}}'; }; + this.endorsee = function(event) { + if (content.target_user.id == factory.currentUserID) { + return 'you'; + } else { + return '{{content.target_user.username}}' + } + }; + this.changeVerb = function(event) { return event.event_type; }; this.correctionDescription = function(event) { - return factory.correctionDescriptions[event.content.correctable_type]; + var description = factory.correctionDescriptions[event.content.correctable_type]; + var bIsOwner = event.content.submitted_by == factory.currentUserID; + var ownershipClause = bIsOwner ? 'Your' : 'The'; + return description.replace('((ownershipClause))', ownershipClause); }; this.statusChange = function(event) { @@ -302,5 +316,13 @@ app.service('notificationsFactory', function() { } }; + this.userClause = function(event) { + if (factory.currentUserID == event.content.user.id) { + return 'You have'; + } else { + return factory.contentLink(event); + } + }; + return this; }); \ No newline at end of file diff --git a/mod-picker/app/assets/javascripts/Services/assetUtils.js b/mod-picker/app/assets/javascripts/Services/assetUtils.js index 2d6d17219..cea3c46d0 100644 --- a/mod-picker/app/assets/javascripts/Services/assetUtils.js +++ b/mod-picker/app/assets/javascripts/Services/assetUtils.js @@ -21,32 +21,42 @@ app.service('assetUtils', function(fileUtils) { } }; + this.newLevel = function(currentLevel, levelName) { + var folderExt = fileUtils.getFileExtension(levelName).toLowerCase(); + currentLevel.unshift({ + name: levelName, + iconClass: service.getIconClass(folderExt), + children: [] + }); + return currentLevel[0].children; + }; + + this.generateLevels = function(splitPath, currentLevel) { + splitPath.forEach(function(levelName) { + var foundLevel = currentLevel.find(function(item) { + return item.name.toLowerCase() === levelName.toLowerCase(); + }); + if (foundLevel) { + if (!foundLevel.children) foundLevel.children = []; + currentLevel = foundLevel.children; + } else { + currentLevel = service.newLevel(currentLevel, levelName); + } + }); + + return currentLevel; + }; + this.getNestedAssets = function(assetPaths) { var nestedAssets = []; assetPaths.forEach(function(assetPath) { - var paths = assetPath.split('\\'); - var fileName = paths.pop(); + var splitPath = assetPath.split('\\'); + var fileName = splitPath.pop(); var fileExt = fileUtils.getFileExtension(fileName).toLowerCase(); var currentLevel = nestedAssets; // traverse/generate levels as needed - paths.forEach(function(folderName) { - var folderExt = fileUtils.getFileExtension(folderName).toLowerCase(); - var foundFolder = currentLevel.find(function(item) { - return item.name.toLowerCase() === folderName.toLowerCase(); - }); - if (foundFolder) { - if (!foundFolder.children) foundFolder.children = []; - currentLevel = foundFolder.children; - } else { - currentLevel.unshift({ - name: folderName, - iconClass: service.getIconClass(folderExt), - children: [] - }); - currentLevel = currentLevel[0].children; - } - }); + currentLevel = service.generateLevels(splitPath, currentLevel); // push the file onto the current level if it isn't already present var foundFile = currentLevel.find(function(item) { @@ -65,7 +75,7 @@ app.service('assetUtils', function(fileUtils) { this.sortNestedAssets = function(nestedAssets) { nestedAssets.sort(function(a, b) { - if (a.children || !b.children) { + if (!a.children == !b.children) { if (a.name < b.name) return -1; if (a.name > b.name) return 1; return 0; diff --git a/mod-picker/app/assets/javascripts/Services/formUtils.js b/mod-picker/app/assets/javascripts/Services/formUtils.js index 547496a06..6cd36132d 100644 --- a/mod-picker/app/assets/javascripts/Services/formUtils.js +++ b/mod-picker/app/assets/javascripts/Services/formUtils.js @@ -1,6 +1,10 @@ app.service('formUtils', function($document) { + var lastFocusedInput; + this.focusText = function($event) { + if ($event.target === lastFocusedInput) return; $event.target.select(); + lastFocusedInput = $event.target; }; this.hideWhenDocumentClicked = function(viewBoolean) { diff --git a/mod-picker/app/assets/javascripts/Services/modOptionUtils.js b/mod-picker/app/assets/javascripts/Services/modOptionUtils.js new file mode 100644 index 000000000..a70e5c211 --- /dev/null +++ b/mod-picker/app/assets/javascripts/Services/modOptionUtils.js @@ -0,0 +1,52 @@ +app.service('modOptionUtils', function() { + var service = this; + + this.findParentOption = function(modOptions, modOption) { + if (modOption.md5_hash) { + return modOptions.find(function(option) { + return !option.is_installer_option && option.md5_hash == modOption.md5_hash; + }); + } else if (modOptions.length == 1) { + return modOptions[0]; + } + }; + + this.addChildModOption = function(parentOption, modOption) { + if (!parentOption.hasOwnProperty('children')) { + parentOption.children = []; + } + parentOption.children.push(modOption); + }; + + this.getNestedModOptions = function(modOptions) { + var nestedModOptions = [], archiveOptions = [], installerOptions = []; + modOptions.forEach(function(modOption) { + if (modOption.is_installer_option) { + modOption.iconClass = 'fa-gear'; + modOption.tooltip = 'Installer mod option'; + installerOptions.push(modOption); + } else { + modOption.iconClass = 'fa-file-archive-o'; + modOption.tooltip = 'Archive mod option'; + archiveOptions.push(modOption); + nestedModOptions.push(modOption); + } + }); + installerOptions.forEach(function(modOption) { + var parentOption = service.findParentOption(archiveOptions, modOption); + if (parentOption) { + service.addChildModOption(parentOption, modOption) + } else { + nestedModOptions.push(modOption); + } + }); + + return nestedModOptions; + }; + + this.activateDefaultModOptions = function(modOptions) { + modOptions.forEach(function(option) { + option.active = option.default; + }); + } +}); diff --git a/mod-picker/app/assets/javascripts/Views/base/activity.js b/mod-picker/app/assets/javascripts/Views/base/activity.js new file mode 100644 index 000000000..125407adb --- /dev/null +++ b/mod-picker/app/assets/javascripts/Views/base/activity.js @@ -0,0 +1,49 @@ +app.config(['$stateProvider', function($stateProvider) { + $stateProvider.state('base.activity', { + templateUrl: '/resources/partials/base/activity.html', + controller: 'activityController', + url: '/activity', + resolve: { + permissions: function($q, currentUser, errorService) { + var permissions = $q.defer(); + if (currentUser.permissions.canModerate) { + permissions.resolve(currentUser.permissions); + } else { + var errorObj = errorService.frontendError('Error accessing Site Activity.', 'base.activity', 403, 'Not Authorized'); + permissions.reject(errorObj); + } + return permissions.promise; + } + } + }); +}]); + +app.controller('activityController', function($scope, $rootScope, permissions, notificationService, helpFactory, notificationsFactory) { + // initialize local variables + $scope.errors = {}; + $scope.pages = {}; + + // set page title + $scope.$emit('setPageTitle', 'Site Activity'); + $scope.permissions = permissions; + // set help context + helpFactory.setHelpContexts($scope, []); + + // prepare notifications factory + notificationsFactory.setCurrentUserID($rootScope.currentUser.id); + + // data retrieval + $scope.retrieveEvents = function(page) { + var options = { + page: page || 1 + }; + notificationService.retrieveEvents(options, $scope.pages).then(function(data) { + $scope.events = data.events; + }, function(response) { + $scope.errors.events = response; + }); + }; + + // retrieve data when we first visit the page + $scope.retrieveEvents(); +}); diff --git a/mod-picker/app/assets/javascripts/Views/browse/notifications.js b/mod-picker/app/assets/javascripts/Views/browse/notifications.js index e6e514011..cadefb10e 100644 --- a/mod-picker/app/assets/javascripts/Views/browse/notifications.js +++ b/mod-picker/app/assets/javascripts/Views/browse/notifications.js @@ -9,9 +9,7 @@ app.config(['$stateProvider', function($stateProvider) { app.controller('notificationsController', function($scope, $rootScope, notificationService, helpFactory, notificationsFactory) { // initialize local variables $scope.errors = {}; - $scope.pages = { - notifications: {} - }; + $scope.pages = {}; // set page title $scope.$emit('setPageTitle', 'Notifications'); @@ -26,7 +24,7 @@ app.controller('notificationsController', function($scope, $rootScope, notificat var options = { page: page || 1 }; - notificationService.retrieveNotifications(options, $scope.pages.notifications).then(function(data) { + notificationService.retrieveNotifications(options, $scope.pages).then(function(data) { $scope.notifications = data.notifications; }, function(response) { $scope.errors.notifications = response; diff --git a/mod-picker/app/assets/javascripts/Views/browse/tags.js b/mod-picker/app/assets/javascripts/Views/browse/tags.js new file mode 100644 index 000000000..358718453 --- /dev/null +++ b/mod-picker/app/assets/javascripts/Views/browse/tags.js @@ -0,0 +1,79 @@ +app.run(function($futureState, indexFactory, filtersFactory) { + // dynamically construct and apply state + var filterPrototypes = filtersFactory.tagFilters(); + var state = indexFactory.buildState('text', 'ASC', 'tags', filterPrototypes); + state.controller = 'tagsIndexController'; + $futureState.futureState(state); +}); + +app.controller('tagsIndexController', function($scope, $rootScope, $stateParams, $state, tagService, helpFactory, columnsFactory, filtersFactory, actionsFactory, indexService, indexFactory, eventHandlerFactory) { + // get parent variables + $scope.currentUser = $rootScope.currentUser; + $scope.permissions = $rootScope.permissions; + + // set page title + $scope.$emit('setPageTitle', 'Browse Tags'); + // set help context + helpFactory.setHelpContexts($scope, [helpFactory.indexPage]); + + /* helper functions */ + // toggles the tag modal + $scope.toggleTagModal = function(visible) { + $scope.$emit('toggleModal', visible); + $scope.showTagModal = visible; + }; + + // opens the edit tag modal for a given tag + $scope.$on('editTag', function(event, tag) { + $scope.activeTag = angular.copy(tag); + $scope.originalTag = tag; + $scope.toggleTagModal(true); + }); + + // hides a tag + $scope.$on('hideTag', function(event, tag) { + tagService.hideTag(tag.id, true).then(function() { + tag.hidden = true; + tag.mods_count = 0; + tag.mod_lists_count = 0; + $scope.$emit('successMessage', tag.text + ' has been hidden.'); + }, function(response) { + var params = { + label: 'Error hiding tag: '+tag.text+'.', + response: response + }; + $scope.$emit('errorMessage', params); + }); + }); + + // recovers a tag + $scope.$on('recoverTag', function(event, tag) { + tagService.hideTag(tag.id, false).then(function() { + tag.hidden = false; + $scope.$emit('successMessage', tag.text + ' has been recovered.'); + }, function(response) { + var params = { + label: 'Error recovering tag: '+tag.text+'.', + response: response + }; + $scope.$emit('errorMessage', params); + }); + }); + + // columns for view + $scope.columns = columnsFactory.tagColumns(true); + $scope.columnGroups = columnsFactory.tagColumnGroups(); + + // filters for view + $scope.filterPrototypes = filtersFactory.tagFilters(); + $scope.statFilters = filtersFactory.tagStatisticFilters(); + + // build generic controller stuff + $scope.route = 'tags'; + $scope.retrieve = tagService.retrieveTags; + indexFactory.buildIndex($scope, $stateParams, $state); + eventHandlerFactory.buildMessageHandlers($scope); + + // override some data from the generic controller + $scope.actions = actionsFactory.tagIndexActions(); +}); diff --git a/mod-picker/app/assets/javascripts/Views/mod/modAnalysis.js b/mod-picker/app/assets/javascripts/Views/mod/modAnalysis.js index 2b3034263..f80621196 100644 --- a/mod-picker/app/assets/javascripts/Views/mod/modAnalysis.js +++ b/mod-picker/app/assets/javascripts/Views/mod/modAnalysis.js @@ -1,4 +1,4 @@ -app.controller('modAnalysisController', function($scope, $stateParams, $state, modService) { +app.controller('modAnalysisController', function($scope, $stateParams, $state, modService, assetUtils) { $scope.updateParams = function() { var newState = {}; if ($scope.optionIds) { @@ -44,11 +44,31 @@ app.controller('modAnalysisController', function($scope, $stateParams, $state, m } }; + $scope.buildNestedAssets = function() { + var assetPaths = []; + $scope.mod.options.forEach(function(option) { + if (option.active) { + assetPaths = assetPaths.concat(option.asset_file_paths); + } + }); + + // create nestedAssets tree + var nestedAssets = assetUtils.getNestedAssets(assetPaths); + assetUtils.sortNestedAssets(nestedAssets); + + // apply nested assets to scope + $scope.$applyAsync(function() { + $scope.mod.nestedAssets = nestedAssets; + $scope.mod.assets = assetPaths; + }); + }; + $scope.toggleOption = function() { $scope.updateOptionIds(); $scope.updateOptionPlugins(); $scope.updateCurrentPlugin($stateParams.plugin); $scope.updateParams(); + $scope.buildNestedAssets(); }; $scope.toggleShowBenignErrors = function() { @@ -95,9 +115,9 @@ app.controller('modAnalysisController', function($scope, $stateParams, $state, m modService.retrieveModAnalysis($stateParams.modId).then(function(analysis) { $scope.mod.analysis = analysis; $scope.mod.options = analysis.mod_options; + $scope.mod.nestedOptions = analysis.nestedOptions; $scope.mod.plugins = analysis.plugins; $scope.mod.assets = analysis.assets; - $scope.mod.nestedAssets = analysis.nestedAssets; // set current option and plugin $scope.setCurrentSelection(); @@ -116,4 +136,8 @@ app.controller('modAnalysisController', function($scope, $stateParams, $state, m $scope.showLess = function(master) { master.max_overrides -= 1000; }; + + $scope.$on('toggleModOption', function() { + $scope.toggleOption(); + }); }); diff --git a/mod-picker/app/assets/javascripts/Views/modList/modList.js b/mod-picker/app/assets/javascripts/Views/modList/modList.js index c2e206c87..e949cf479 100644 --- a/mod-picker/app/assets/javascripts/Views/modList/modList.js +++ b/mod-picker/app/assets/javascripts/Views/modList/modList.js @@ -126,9 +126,6 @@ app.controller('modListController', function($scope, $rootScope, $q, $state, $st $scope.originalModList = angular.copy($scope.mod_list); $scope.removedModIds = []; - // default to editing modlist if it's the current user's active modlist - $scope.editing = $scope.activeModList; - // initialize local variables $scope.tabs = tabsFactory.buildModListTabs($scope.mod_list); $scope.pages = { @@ -187,6 +184,9 @@ app.controller('modListController', function($scope, $rootScope, $q, $state, $st $scope.target = $scope.mod_list; $scope.isActive = $scope.activeModList && $scope.activeModList.id == $scope.mod_list.id; + // default to editing modlist if it's the current user's active modlist + $scope.editing = $scope.isActive; + // set page title $scope.$emit('setPageTitle', 'View Mod List'); diff --git a/mod-picker/app/assets/javascripts/Views/modList/modListConfig.js b/mod-picker/app/assets/javascripts/Views/modList/modListConfig.js index 9ff89de48..c8f42ee6e 100644 --- a/mod-picker/app/assets/javascripts/Views/modList/modListConfig.js +++ b/mod-picker/app/assets/javascripts/Views/modList/modListConfig.js @@ -59,6 +59,7 @@ app.controller('modListConfigController', function($scope, $q, $timeout, modList if (modListConfig._destroy) { delete modListConfig._destroy; $scope.mod_list.config_files_count += 1; + configFilesService.recoverConfigFileGroups($scope.model.config_files); $scope.updateTabs(); // success message @@ -74,7 +75,9 @@ app.controller('modListConfigController', function($scope, $q, $timeout, modList $scope.removeConfig = function(group, index) { var configToRemove = group.children[index]; if (configToRemove._destroy) return; + var wasActiveConfig = configToRemove.active; configToRemove._destroy = true; + configToRemove.active = false; // update counts if (configToRemove.config_file) { @@ -88,7 +91,7 @@ app.controller('modListConfigController', function($scope, $q, $timeout, modList // switch to the first available config if the user destroyed the active config for (var i = 0; i < group.children.length; i++) { if (!group.children[i]._destroy) { - if (configToRemove.active) { + if (wasActiveConfig) { $timeout(function() { $scope.selectConfig(group, group.children[i]); }); @@ -98,7 +101,7 @@ app.controller('modListConfigController', function($scope, $q, $timeout, modList } // no configs in the group to switch to, destroy the group! - configToRemove.active = false; + delete group.activeConfig; group._destroy = true; }; diff --git a/mod-picker/app/assets/stylesheets/components/modOptionTree.scss b/mod-picker/app/assets/stylesheets/components/modOptionTree.scss new file mode 100644 index 000000000..0f3aabe8e --- /dev/null +++ b/mod-picker/app/assets/stylesheets/components/modOptionTree.scss @@ -0,0 +1,71 @@ +mod-option-tree { + ul { + list-style: none; + padding-left: 10px; + + li { + margin: 8px 0; + } + } + + .mod-option { + display: flex; + + .mod-option-icon { + font-size: 32px; + margin-right: 10px; + } + + .mod-option-details { + padding-right: 10px; + + .mod-option-title { + display: flex; + font-size: 20px; + + input { + margin: 6px 2px 0 0; + min-width: 16px; + } + } + + .mod-option-stats { + padding-left: 18px; + } + } + } + + .children-container { + padding-left: 37px; + + .children-note { + cursor: pointer; + + .fa { + min-width: 16px; + } + } + } + + mod-option-tree { + ul { + padding-right: 5px; + + li { + display: inline-block; + min-width: 350px; + max-width: 350px; + } + } + + .mod-option .mod-option-details { + .mod-option-title { + font-size: 18px; + } + + .mod-option-stats { + font-size: 14px; + } + } + } +} \ No newline at end of file diff --git a/mod-picker/app/assets/stylesheets/general/buttons.scss b/mod-picker/app/assets/stylesheets/general/buttons.scss index 3d9b9f6e1..d96f82141 100644 --- a/mod-picker/app/assets/stylesheets/general/buttons.scss +++ b/mod-picker/app/assets/stylesheets/general/buttons.scss @@ -110,6 +110,11 @@ input[type="button"], input[type="submit"] { background-color: $bar_highlight_color; } + &.no-action:focus, &.no-action:hover { + color: $action_text_color; + background-color: $action_color; + } + &.disabled { background-color: $hard_border_color; cursor: default; @@ -149,6 +154,10 @@ div.action-box { &:hover { background-color: $action_color; } + + &.no-action:hover { + background-color: $error_secondary; + } } .green-box { @@ -161,6 +170,10 @@ div.action-box { &:hover { background-color: $action_color; } + + &.no-action:hover { + background-color: $success_secondary; + } } .yellow-box { @@ -173,4 +186,8 @@ div.action-box { &:hover { background-color: $action_color; } + + &.no-action:hover { + background-color: $warning_secondary; + } } \ No newline at end of file diff --git a/mod-picker/app/assets/stylesheets/help.scss b/mod-picker/app/assets/stylesheets/help.scss index e998280fb..fac283e99 100644 --- a/mod-picker/app/assets/stylesheets/help.scss +++ b/mod-picker/app/assets/stylesheets/help.scss @@ -1,4 +1,6 @@ @import +'roboto', +'font-awesome.min', 'simplemde.min', 'general/mixins', 'general/buttons', diff --git a/mod-picker/app/assets/stylesheets/landing.scss b/mod-picker/app/assets/stylesheets/landing.scss index 586ece13f..5f9c464fe 100644 --- a/mod-picker/app/assets/stylesheets/landing.scss +++ b/mod-picker/app/assets/stylesheets/landing.scss @@ -1,4 +1,6 @@ @import +'roboto', +'font-awesome.min', 'general/mixins', 'general/buttons', 'general/forms', diff --git a/mod-picker/app/assets/stylesheets/main.scss b/mod-picker/app/assets/stylesheets/main.scss index 04fc0615a..c5a0f38f1 100644 --- a/mod-picker/app/assets/stylesheets/main.scss +++ b/mod-picker/app/assets/stylesheets/main.scss @@ -1,4 +1,6 @@ @import +'roboto', +'font-awesome.min', 'rzslider.min', 'simplemde.min', 'general/mixins', @@ -28,6 +30,7 @@ 'components/gameSelect', 'components/logo', 'components/messages', +'components/modOptionTree', 'components/gridItems', 'components/helper', 'components/notes', diff --git a/mod-picker/app/assets/stylesheets/pages/modList.scss b/mod-picker/app/assets/stylesheets/pages/modList.scss index 0c5580bb3..771f30914 100644 --- a/mod-picker/app/assets/stylesheets/pages/modList.scss +++ b/mod-picker/app/assets/stylesheets/pages/modList.scss @@ -173,4 +173,10 @@ ul.required-list { background-color: $primary_background; } } +} + +mod-details-modal { + .custom-mod-name { + font-size: 20px; + } } \ No newline at end of file diff --git a/mod-picker/app/assets/stylesheets/pages/showMod.scss b/mod-picker/app/assets/stylesheets/pages/showMod.scss index 1434d821a..af0f2efac 100644 --- a/mod-picker/app/assets/stylesheets/pages/showMod.scss +++ b/mod-picker/app/assets/stylesheets/pages/showMod.scss @@ -427,12 +427,6 @@ display: block; margin-bottom: 8px; } - - label { - display: inline-block; - margin-right: 16px; - min-width: 365px; - } } .content-block { diff --git a/mod-picker/app/builders/mod_builder.rb b/mod-picker/app/builders/mod_builder.rb index a4c532f86..ec2977f4d 100644 --- a/mod-picker/app/builders/mod_builder.rb +++ b/mod-picker/app/builders/mod_builder.rb @@ -78,6 +78,7 @@ def save! def before_save set_config_file_game_ids validate_sources + manage_custom_mods end def after_save @@ -229,6 +230,25 @@ def swap_mod_list_mods_tools_counts end end + def substitute_custom_mods + mod.sources_array.each do |source| + ModListCustomMod.substitute_for_url(source.url, mod) + end + end + + def make_custom_mods + mod.mod_list_mods.find_each do |mod_list_mod| + ModListCustomMod.create_from_mod_list_mod(mod_list_mod) + mod_list_mod.destroy + end + end + + def manage_custom_mods + if mod.was_visible != mod.visible + mod.visible ? substitute_custom_mods : make_custom_mods + end + end + def self.model_name 'Mod' end diff --git a/mod-picker/app/controllers/application_controller.rb b/mod-picker/app/controllers/application_controller.rb index 33e7cbd94..58f238a48 100644 --- a/mod-picker/app/controllers/application_controller.rb +++ b/mod-picker/app/controllers/application_controller.rb @@ -5,12 +5,19 @@ class ApplicationController < ActionController::Base before_action :configure_permitted_parameters, if: :devise_controller? - # Render 401 or 403 as appropriate - - rescue_from ::StandardError do |exception| + # Render errors as appropriate + def render_standard_error(exception, status) error_hash = { error: exception.message } error_hash[:backtrace] = exception.backtrace unless Rails.env.production? - render json: error_hash, status: 500 + render json: error_hash, status: status + end + + rescue_from ::StandardError do |exception| + render_standard_error(exception, 500) + end + + rescue_from ActiveRecord::RecordNotFound do |exception| + render_standard_error(exception, 404) end rescue_from CanCan::AccessDenied do |exception| diff --git a/mod-picker/app/controllers/events_controller.rb b/mod-picker/app/controllers/events_controller.rb new file mode 100644 index 000000000..ab63adbf3 --- /dev/null +++ b/mod-picker/app/controllers/events_controller.rb @@ -0,0 +1,14 @@ +class EventsController < ApplicationController + # POST /events + def index + authorize! :index, Event + @events = Event.paginate(page: params[:page]).order(:id => :DESC) + count = Event.count + + render json: { + events: @events, + max_entries: count, + entries_per_page: Event.per_page + } + end +end \ No newline at end of file diff --git a/mod-picker/app/controllers/mod_lists_controller.rb b/mod-picker/app/controllers/mod_lists_controller.rb index c0ab1c7ae..5ce7dba39 100644 --- a/mod-picker/app/controllers/mod_lists_controller.rb +++ b/mod-picker/app/controllers/mod_lists_controller.rb @@ -369,7 +369,7 @@ def filtering_params def mod_list_params params.require(:mod_list).permit(:game_id, :name, :description, :status, :visibility, :is_collection, :disable_comments, :lock_tags, :hidden, mod_list_mods_attributes: [:id, :group_id, :mod_id, :index, :_destroy, mod_list_mod_options_attributes: [:id, :mod_option_id, :_destroy]], - custom_mods_attributes: [:id, :group_id, :is_utility, :index, :name, :description, :_destroy], + custom_mods_attributes: [:id, :group_id, :is_utility, :index, :name, :url, :description, :_destroy], mod_list_plugins_attributes: [:id, :group_id, :plugin_id, :index, :cleaned, :merged, :_destroy], custom_plugins_attributes: [:id, :group_id, :index, :cleaned, :merged, :compatibility_note_id, :filename, :description, :_destroy], mod_list_groups_attributes: [:id, :index, :tab, :color, :name, :description, :_destroy, diff --git a/mod-picker/app/controllers/mods_controller.rb b/mod-picker/app/controllers/mods_controller.rb index 47fabf622..51de045a9 100644 --- a/mod-picker/app/controllers/mods_controller.rb +++ b/mod-picker/app/controllers/mods_controller.rb @@ -95,9 +95,9 @@ def update # POST /mods/1/hide def hide - authorize! :hide, @mod - @mod.hidden = params[:hidden] - if @mod.save + authorize! :hide, @mod, :message => "You are not allowed to hide/unhide this mod." + builder = ModBuilder.new(current_user, hide_params) + if builder.update render json: {status: :ok} else render json: @mod.errors, status: :unprocessable_entity @@ -106,9 +106,9 @@ def hide # POST /mods/1/approve def approve - authorize! :approve, @mod - @mod.approved = params[:approved] - if @mod.save + authorize! :approve, @mod, :message => "You are not allowed to approve/unapprove this mod." + builder = ModBuilder.new(current_user, approve_params) + if builder.update render json: {status: :ok} else render json: @mod.errors, status: :unprocessable_entity @@ -324,9 +324,8 @@ def load_order_notes def analysis authorize! :read, @mod render json: { - mod_options: @mod.mod_options, - plugins: json_format(@mod.plugins, :show), - assets: @mod.asset_file_paths + mod_options: json_format(@mod.mod_options, :show), + plugins: json_format(@mod.plugins, :show) } end @@ -359,6 +358,20 @@ def sorting_params params.fetch(:sort, {}).permit(:column, :direction) end + def approve_params + { + id: params[:id], + approved: params[:approved] + } + end + + def hide_params + { + id: params[:id], + hidden: params[:hidden] + } + end + # Params we allow filtering on def filtering_params # construct valid filters array diff --git a/mod-picker/app/controllers/notifications_controller.rb b/mod-picker/app/controllers/notifications_controller.rb index d19b01555..e4bda6a71 100644 --- a/mod-picker/app/controllers/notifications_controller.rb +++ b/mod-picker/app/controllers/notifications_controller.rb @@ -1,5 +1,5 @@ class NotificationsController < ApplicationController - # GET /notifications/:page + # POST /notifications def index @notifications = current_user.notifications.paginate(page: params[:page]) count = current_user.notifications.count diff --git a/mod-picker/app/controllers/tags_controller.rb b/mod-picker/app/controllers/tags_controller.rb index 44903f3a1..bd66bf35e 100644 --- a/mod-picker/app/controllers/tags_controller.rb +++ b/mod-picker/app/controllers/tags_controller.rb @@ -1,16 +1,39 @@ class TagsController < ApplicationController - before_action :set_tag, only: [:destroy] + before_action :set_tag, only: [:update, :hide] - # GET /tags - def index - @tags = Tag.filter(filtering_params) + # GET /all_tags + def all + @tags = Tag.game(params[:game]) render json: @tags end - # DELETE /mods/1 - def destroy - authorize! :destroy, @tag - if @tag.destroy + # POST/GET /tags + def index + @tags = Tag.eager_load(:submitter).accessible_by(current_ability).filter(filtering_params).sort(params[:sort]).paginate(page: params[:page]) + count = Tag.eager_load(:submitter).accessible_by(current_ability).filter(filtering_params).count + + render json: { + tags: json_format(@tags), + max_entries: count, + entries_per_page: Tag.per_page + } + end + + # PATCH/PUT /tags/:id + def update + authorize! :update, @tag + if @tag.update(tag_update_params) + render json: {status: :ok} + else + render json: @tag.errors, status: :unprocessable_entity + end + end + + # POST /tags/:id/hide + def hide + authorize! :hide, @tag + @tag.hidden = params[:hidden] + if @tag.save render json: {status: :ok} else render json: @tag.errors, status: :unprocessable_entity @@ -23,8 +46,12 @@ def set_tag @tag = Tag.find(params[:id]) end + def tag_update_params + params.require(:tag).permit(:text) + end + # Params we allow filtering on def filtering_params - params[:filters].slice(:game); + params[:filters].slice(:game, :hidden, :search, :submitter, :mods_count, :mod_lists_count) end end \ No newline at end of file diff --git a/mod-picker/app/models/ability.rb b/mod-picker/app/models/ability.rb index a1a174609..b1891493d 100644 --- a/mod-picker/app/models/ability.rb +++ b/mod-picker/app/models/ability.rb @@ -17,6 +17,7 @@ def initialize(user) can [:assign_roles, :adjust_rep, :invite], User can :set_avatar, User, id: user.id can :set_custom_title, User, id: user.id + can :index, Event # can create and update help pages can [:approve, :create, :update], HelpPage diff --git a/mod-picker/app/models/concerns/record_enhancements.rb b/mod-picker/app/models/concerns/record_enhancements.rb index a19497bdb..43ee99f0b 100644 --- a/mod-picker/app/models/concerns/record_enhancements.rb +++ b/mod-picker/app/models/concerns/record_enhancements.rb @@ -13,4 +13,9 @@ def save_columns!(*names) update_columns(column_values) unless column_values.empty? changes_applied end + + def next_id + connection = ActiveRecord::Base.connection + connection.execute("SELECT MIN(t1.id + 1) AS nextID FROM #{self.class.table_name} t1 LEFT JOIN #{self.class.table_name} t2 ON t1.id + 1 = t2.id WHERE t2.id IS NULL;").first[0] + end end \ No newline at end of file diff --git a/mod-picker/app/models/correction.rb b/mod-picker/app/models/correction.rb index f95e7aebc..560b42b87 100644 --- a/mod-picker/app/models/correction.rb +++ b/mod-picker/app/models/correction.rb @@ -67,19 +67,19 @@ class Correction < ActiveRecord::Base # METHODS def self.close(id) correction = Correction.find(id) - if correction.status == :open + if correction.status == "open" correction.passed? ? correction.pass : correction.fail correction.save end end def pass - self.status = :passed + self.status = "passed" correctable.correction_passed(self) end def fail - self.status = :failed + self.status = "failed" end def has_minimum_votes? diff --git a/mod-picker/app/models/event.rb b/mod-picker/app/models/event.rb index 6ca5ba007..b95fd81f0 100644 --- a/mod-picker/app/models/event.rb +++ b/mod-picker/app/models/event.rb @@ -3,7 +3,7 @@ class Event < ActiveRecord::Base # ATTRIBUTES enum event_type: [:added, :updated, :removed, :hidden, :unhidden, :approved, :unapproved, :status, :action_soon, :message, :unused, :milestone1, :milestone2, :milestone3, :milestone4, :milestone5, :milestone6, :milestone7, :milestone8, :milestone9, :milestone10] - self.per_page = 50 + self.per_page = 100 # DATE COLUMNS date_column :created diff --git a/mod-picker/app/models/mod.rb b/mod-picker/app/models/mod.rb index 2bd4b0758..1a2541803 100644 --- a/mod-picker/app/models/mod.rb +++ b/mod-picker/app/models/mod.rb @@ -172,8 +172,25 @@ class Mod < ActiveRecord::Base validates :name, :aliases, length: {maximum: 128} # CALLBACKS + before_create :set_id before_save :touch_updated + def visible + approved && !hidden + end + + def was_visible + attribute_was(:approved) && !attribute_was(:hidden) + end + + def url + sources_array.first.url + end + + def sources_array + [nexus_infos, workshop_infos, lover_infos, custom_sources].flatten.compact + end + def correction_passed(correction) update_columns(status: Mod.statuses[correction.mod_status]) end @@ -258,4 +275,8 @@ def touch_updated self.updated += 1.second end end + + def set_id + self.id = next_id + end end diff --git a/mod-picker/app/models/mod_list_custom_mod.rb b/mod-picker/app/models/mod_list_custom_mod.rb index 1980f950c..ffd344b41 100644 --- a/mod-picker/app/models/mod_list_custom_mod.rb +++ b/mod-picker/app/models/mod_list_custom_mod.rb @@ -16,6 +16,27 @@ class ModListCustomMod < ActiveRecord::Base validates :is_utility, inclusion: [true, false] validates :description, length: {maximum: 4096} + def self.substitute_for_url(url, mod) + uri = URI.parse(url) + url = uri.host.gsub('\Awww\.', '') + uri.path + ModListCustomMod.where("url LIKE ?", "%#{url}%").find_each do |custom_mod| + ModListMod.create_from_custom_mod(custom_mod, mod) + custom_mod.destroy + end + end + + def self.create_from_mod_list_mod(mod_list_mod) + create({ + mod_list_id: mod_list_mod.mod_list_id, + group_id: mod_list_mod.group_id, + index: mod_list_mod.index, + is_utility: mod_list_mod.is_utility, + name: mod_list_mod.mod.name, + url: mod_list_mod.mod.url, + description: "Automatically created #{DateTime.now.to_s}" + }) + end + def copy_attributes(mod_list_id, index, group_id) attributes.except("id").merge({ mod_list_id: mod_list_id, index: index, group_id: group_id }) end diff --git a/mod-picker/app/models/mod_list_mod.rb b/mod-picker/app/models/mod_list_mod.rb index 79b8d4c57..5f2083cf3 100644 --- a/mod-picker/app/models/mod_list_mod.rb +++ b/mod-picker/app/models/mod_list_mod.rb @@ -29,6 +29,16 @@ class ModListMod < ActiveRecord::Base before_create :set_index_and_is_utility before_destroy :destroy_mod_list_plugins + def self.create_from_custom_mod(custom_mod, mod) + create({ + mod_list_id: custom_mod.mod_list_id, + group_id: custom_mod.group_id, + mod_id: mod.id, + index: custom_mod.index, + is_utility: custom_mod.is_utility + }) + end + def mod_compatibility_notes mod_ids = mod_list.mod_list_mod_ids return [] if mod_ids.empty? diff --git a/mod-picker/app/models/sortable_columns/tags.json b/mod-picker/app/models/sortable_columns/tags.json new file mode 100644 index 000000000..8bd0fda98 --- /dev/null +++ b/mod-picker/app/models/sortable_columns/tags.json @@ -0,0 +1,8 @@ +{ + "except": ["game_id", "submitted_by"], + "include": { + "submitter": { + "only": ["username"] + } + } +} \ No newline at end of file diff --git a/mod-picker/app/models/tag.rb b/mod-picker/app/models/tag.rb index 561d885f4..7833241a2 100644 --- a/mod-picker/app/models/tag.rb +++ b/mod-picker/app/models/tag.rb @@ -1,8 +1,15 @@ class Tag < ActiveRecord::Base - include Filterable, RecordEnhancements, CounterCache, Reportable, ScopeHelpers, BetterJson + include Filterable, Sortable, RecordEnhancements, CounterCache, Reportable, ScopeHelpers, BetterJson + + # ATTRIBUTES + self.per_page = 100 # SCOPES + hash_scope :hidden, alias: 'hidden' game_scope + search_scope :text, alias: 'search' + user_scope :submitter + range_scope :mods_count, :mod_lists_count # ASSOCIATIONS belongs_to :submitter, :class_name => 'User', :foreign_key => :submitted_by, :inverse_of => 'tags' @@ -22,4 +29,14 @@ class Tag < ActiveRecord::Base validates :game_id, :submitted_by, :text, presence: true validates :text, length: {in: 2..32} validates :hidden, inclusion: [true, false] + + # CALLBACKS + before_save :destroy_mod_and_mod_list_tags + + private + def destroy_mod_and_mod_list_tags + return unless hidden + mod_list_tags.destroy_all + mod_tags.destroy_all + end end diff --git a/mod-picker/app/views/devise/mailer/confirmation_instructions.html.erb b/mod-picker/app/views/devise/mailer/confirmation_instructions.html.erb index dc55f64f6..82418b1bb 100644 --- a/mod-picker/app/views/devise/mailer/confirmation_instructions.html.erb +++ b/mod-picker/app/views/devise/mailer/confirmation_instructions.html.erb @@ -1,5 +1,104 @@ -

Welcome <%= @email %>!

+ + +
+

Hello <%= @resource.username %>,

+ +

Welcome to Mod Picker! You can confirm your account through the link below:

+ +
+ <%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token), :class => "button" %> +
+ +

If you recieved this email in error, please ignore it or contact us at modpicker@gmail.com.

+ +
+

Sincerely,
+ The Mod Picker Team
+ www.modpicker.com +

+
diff --git a/mod-picker/app/views/devise/mailer/reset_password_instructions.html.erb b/mod-picker/app/views/devise/mailer/reset_password_instructions.html.erb index f667dc12f..72c98f273 100644 --- a/mod-picker/app/views/devise/mailer/reset_password_instructions.html.erb +++ b/mod-picker/app/views/devise/mailer/reset_password_instructions.html.erb @@ -1,8 +1,106 @@ -

Hello <%= @resource.email %>!

+ + +
+

Hello <%= @resource.username %>,

+ +

Someone has requested a link to change your password. You can do this through the link below:

+ + +
+ <%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token), :class => "button" %> +
+ +

If you didn't request this, please ignore this email.

+

Your password won't change until you access the link above and create a new one.

+ +
+

Sincerely,
+ The Mod Picker Team
+ www.modpicker.com +

+
\ No newline at end of file diff --git a/mod-picker/app/views/layouts/application.html.erb b/mod-picker/app/views/layouts/application.html.erb index 465a9424b..60dc65b4c 100644 --- a/mod-picker/app/views/layouts/application.html.erb +++ b/mod-picker/app/views/layouts/application.html.erb @@ -3,9 +3,6 @@ - - - <%= javascript_include_tag 'modpicker-vendor' %> " ng-href="{{currentTheme}}" /> - + <% if Rails.env.production? %> + + <% end %> <%= yield %> + <%= javascript_include_tag 'modpicker-vendor' %> <%= javascript_include_tag 'modpicker' %> diff --git a/mod-picker/app/views/layouts/help.html.erb b/mod-picker/app/views/layouts/help.html.erb index 1a23cb998..1cf67e15e 100644 --- a/mod-picker/app/views/layouts/help.html.erb +++ b/mod-picker/app/views/layouts/help.html.erb @@ -4,20 +4,19 @@ <%= yield :head %> - - <%= stylesheet_link_tag 'help/High Hrothgar' %> - <%= javascript_include_tag 'helpcenter' %> - + <% if Rails.env.production? %> + + <% end %>
@@ -73,5 +72,7 @@ +<%= javascript_include_tag 'helpcenter' %> + \ No newline at end of file diff --git a/mod-picker/app/views/layouts/landing.html.erb b/mod-picker/app/views/layouts/landing.html.erb index 093e7f064..858ed5857 100644 --- a/mod-picker/app/views/layouts/landing.html.erb +++ b/mod-picker/app/views/layouts/landing.html.erb @@ -16,17 +16,17 @@ - - - <%= javascript_include_tag 'landing' %> <%= stylesheet_link_tag 'landing/High Hrothgar' %> - + <% if Rails.env.production? %> + + <% end %> <%= yield %> +<%= javascript_include_tag 'landing' %> \ No newline at end of file diff --git a/mod-picker/app/views/mod_options/show.json b/mod-picker/app/views/mod_options/show.json new file mode 100644 index 000000000..470ab4a73 --- /dev/null +++ b/mod-picker/app/views/mod_options/show.json @@ -0,0 +1,4 @@ +{ + "except": ["mod_id"], + "methods": "asset_file_paths" +} \ No newline at end of file diff --git a/mod-picker/app/views/tags/index.json b/mod-picker/app/views/tags/index.json new file mode 100644 index 000000000..919b58373 --- /dev/null +++ b/mod-picker/app/views/tags/index.json @@ -0,0 +1,8 @@ +{ + "except": ["game_id", "submitted_by"], + "include": { + "submitter": { + "format": "base" + } + } +} \ No newline at end of file diff --git a/mod-picker/app/views/user_reputations/notification.json b/mod-picker/app/views/user_reputations/notification.json index d0313d79d..97cc5eee1 100644 --- a/mod-picker/app/views/user_reputations/notification.json +++ b/mod-picker/app/views/user_reputations/notification.json @@ -1,3 +1,8 @@ { - "only": ["overall"] + "only": ["overall"], + "include": { + "user": { + "format": "search" + } + } } \ No newline at end of file diff --git a/mod-picker/config/routes.rb b/mod-picker/config/routes.rb index c5c216642..d72d051b2 100644 --- a/mod-picker/config/routes.rb +++ b/mod-picker/config/routes.rb @@ -39,14 +39,19 @@ match '/notifications/read', to: 'notifications#read', via: [:post] match '/notifications', to: 'notifications#index', via: [:post, :get] + # events + match '/events', to: 'events#index', via: [:post, :get] + # scraping match '/nexus_infos/:id', to: 'nexus_infos#show', via: [:get] match '/lover_infos/:id', to: 'lover_infos#show', via: [:get] match '/workshop_infos/:id', to: 'workshop_infos#show', via: [:get] # tags - match '/tags', to: 'tags#index', via: [:post] - match '/tags/:id', to: 'tags#destroy', via: [:delete] + match '/all_tags', to: 'tags#all', via: [:get] + match '/tags', to: 'tags#index', via: [:get, :post] + match '/tags/:id/hide', to: 'tags#hide', via: [:post] + match '/tags/:id', to: 'tags#update', via: [:patch, :put] match '/mods/:id/tags', to: 'mods#update_tags', via: [:patch, :put] match '/mod_lists/:id/tags', to: 'mod_lists#update_tags', via: [:patch, :put] diff --git a/mod-picker/db/migrate/20161116190426_add_url_to_mod_list_custom_mods.rb b/mod-picker/db/migrate/20161116190426_add_url_to_mod_list_custom_mods.rb new file mode 100644 index 000000000..2f39b8463 --- /dev/null +++ b/mod-picker/db/migrate/20161116190426_add_url_to_mod_list_custom_mods.rb @@ -0,0 +1,5 @@ +class AddUrlToModListCustomMods < ActiveRecord::Migration + def change + add_column :mod_list_custom_mods, :url, :string, after: :name + end +end diff --git a/mod-picker/db/schema.rb b/mod-picker/db/schema.rb index 4098eb09e..a99de93de 100644 --- a/mod-picker/db/schema.rb +++ b/mod-picker/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20161116000543) do +ActiveRecord::Schema.define(version: 20161116190426) do create_table "agreement_marks", id: false, force: :cascade do |t| t.integer "correction_id", limit: 4, null: false @@ -438,6 +438,7 @@ t.integer "index", limit: 2, null: false t.boolean "is_utility", default: false, null: false t.string "name", limit: 255, null: false + t.string "url", limit: 255 t.text "description", limit: 65535 end diff --git a/mod-picker/public/images/fallout4.png b/mod-picker/public/images/fallout4.png index 6120ceafe..1d48dbbae 100644 Binary files a/mod-picker/public/images/fallout4.png and b/mod-picker/public/images/fallout4.png differ diff --git a/mod-picker/public/images/skyrim.png b/mod-picker/public/images/skyrim.png index 64dfb380a..bcadb4e2b 100644 Binary files a/mod-picker/public/images/skyrim.png and b/mod-picker/public/images/skyrim.png differ diff --git a/mod-picker/public/resources/directives/browse/tableResults.html b/mod-picker/public/resources/directives/browse/tableResults.html index 3fcae364e..bc2631535 100644 --- a/mod-picker/public/resources/directives/browse/tableResults.html +++ b/mod-picker/public/resources/directives/browse/tableResults.html @@ -24,11 +24,20 @@ - - - - - + + + + + + + + + + + + + + diff --git a/mod-picker/public/resources/directives/editMod/modAnalysisManager.html b/mod-picker/public/resources/directives/editMod/modAnalysisManager.html index a01eafc5e..aeb386490 100644 --- a/mod-picker/public/resources/directives/editMod/modAnalysisManager.html +++ b/mod-picker/public/resources/directives/editMod/modAnalysisManager.html @@ -21,5 +21,6 @@

\ No newline at end of file diff --git a/mod-picker/public/resources/directives/modList/gridItem.html b/mod-picker/public/resources/directives/modList/gridItem.html index 3a39b3e55..db712cfcc 100644 --- a/mod-picker/public/resources/directives/modList/gridItem.html +++ b/mod-picker/public/resources/directives/modList/gridItem.html @@ -9,13 +9,18 @@
- -

News and Announcements

-