From d0d44147f7678c8427119397ec96e9a1cdc8d49f Mon Sep 17 00:00:00 2001 From: Ethan Nelson Date: Sat, 11 Aug 2018 19:51:27 -0500 Subject: [PATCH 01/11] Add menu item styling --- client/assets/styles/sass/_structure.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/assets/styles/sass/_structure.scss b/client/assets/styles/sass/_structure.scss index 5976abb333..fa474b8e2d 100644 --- a/client/assets/styles/sass/_structure.scss +++ b/client/assets/styles/sass/_structure.scss @@ -199,6 +199,10 @@ } } +.drop__menu-item { + display: block; +} + /* ========================================================================== Body ========================================================================== */ From 36f706b75ecfc4d67fe7c5978db9943f7f65d63a Mon Sep 17 00:00:00 2001 From: Neil Rotstan Date: Thu, 30 Aug 2018 11:14:54 -0700 Subject: [PATCH 02/11] Show missing xmark (close, delete) controls. Add style for xmark controls, such as the control to dismiss the new-message notification and the control to remove users from a private project, to ensure they are displayed (as hot-design-system styles are no longer pulled in). --- client/assets/styles/sass/_helpers.scss | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/client/assets/styles/sass/_helpers.scss b/client/assets/styles/sass/_helpers.scss index 5cf8c98b79..9a4345352e 100644 --- a/client/assets/styles/sass/_helpers.scss +++ b/client/assets/styles/sass/_helpers.scss @@ -169,3 +169,13 @@ hr { .button[disabled]{ cursor: not-allowed; } + +.hot-ds-icon-sm-xmark:before { + width: 12px; + height: 12px; + background-size: 12px 12px; + content: " "; + background-image: url('../../icons/xmark.svg'); + display: inline-block; + margin-right: .3em; +} From b2bacacb195406647fe1040cc7a2f8afaf9e4f95 Mon Sep 17 00:00:00 2001 From: Neil Rotstan Date: Thu, 30 Aug 2018 11:58:52 -0700 Subject: [PATCH 03/11] Configurable comment and chat limits. Closes #970 * Make comment and chat length limits configurable through `maxCommentLength` and `maxChatLength` settings, respectively, in the taskingmanager.config.json file. * Set default limit to 5000 characters for both comments and chats. * Include migration to remove the 250 character limit from the project_chat.message column. --- .../project-chat/project-chat.directive.js | 6 ++-- client/app/project/project.controller.js | 2 +- client/taskingmanager.config.json | 8 ++++-- migrations/versions/251a7638da78_.py | 28 +++++++++++++++++++ server/models/postgis/project_chat.py | 2 +- 5 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 migrations/versions/251a7638da78_.py diff --git a/client/app/components/project-chat/project-chat.directive.js b/client/app/components/project-chat/project-chat.directive.js index 6fd080a8b2..dc80f065b4 100644 --- a/client/app/components/project-chat/project-chat.directive.js +++ b/client/app/components/project-chat/project-chat.directive.js @@ -8,7 +8,7 @@ angular .module('taskingManager') - .controller('projectChatController', ['$scope', '$anchorScroll', '$location', '$timeout', '$interval', 'messageService', 'userService', projectChatController]) + .controller('projectChatController', ['$scope', '$anchorScroll', '$location', '$timeout', '$interval', 'messageService', 'userService', 'configService', projectChatController]) .directive('projectChat', projectChatDirective); /** @@ -34,14 +34,14 @@ return directive; } - function projectChatController($scope, $anchorScroll, $location, $timeout, $interval, messageService, userService) { + function projectChatController($scope, $anchorScroll, $location, $timeout, $interval, messageService, userService, configService) { var vm = this; vm.projectId = 0; vm.author = ''; vm.message = ''; vm.messages = []; - vm.maxlengthComment = 250; + vm.maxlengthComment = configService.maxChatLength; vm.hasScrolled = false; diff --git a/client/app/project/project.controller.js b/client/app/project/project.controller.js index 17697bc328..7208b6ca6e 100644 --- a/client/app/project/project.controller.js +++ b/client/app/project/project.controller.js @@ -19,7 +19,7 @@ vm.lockedByCurrentUserVectorLayer = null; vm.map = null; vm.user = null; - vm.maxlengthComment = 500; + vm.maxlengthComment = configService.maxCommentLength; vm.taskUrl = ''; // tab and view control diff --git a/client/taskingmanager.config.json b/client/taskingmanager.config.json index 11f0c893ed..b913435cc7 100644 --- a/client/taskingmanager.config.json +++ b/client/taskingmanager.config.json @@ -25,7 +25,9 @@ "key": "AioPAglzP9Qw32KN17dOkKYRdSlzj7W5kIQY6zct_UCmGc0WRxAh-QeiRBpRUgrv", "imagerySet": "Aerial" } - ] + ], + "maxCommentLength": 5000, + "maxChatLength": 5000 } }, "release": { @@ -55,7 +57,9 @@ "attribution": "Courtesy of Microsoft Bing", "imagerySet": "Aerial" } - ] + ], + "maxCommentLength": 5000, + "maxChatLength": 5000 } } } diff --git a/migrations/versions/251a7638da78_.py b/migrations/versions/251a7638da78_.py new file mode 100644 index 0000000000..005f99eaa1 --- /dev/null +++ b/migrations/versions/251a7638da78_.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: 251a7638da78 +Revises: deec8123583d +Create Date: 2018-08-30 07:46:49.074078 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '251a7638da78' +down_revision = 'deec8123583d' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('project_chat', 'message', existing_type=sa.String(length=250), type_=sa.String()) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('project_chat', 'message', existing_type=sa.String(), type_=sa.String(length=250)) + # ### end Alembic commands ### diff --git a/server/models/postgis/project_chat.py b/server/models/postgis/project_chat.py index 55be770ef5..f18d67f540 100644 --- a/server/models/postgis/project_chat.py +++ b/server/models/postgis/project_chat.py @@ -13,7 +13,7 @@ class ProjectChat(db.Model): project_id = db.Column(db.Integer, db.ForeignKey('projects.id'), index=True, nullable=False) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) time_stamp = db.Column(db.DateTime, nullable=False, default=timestamp) - message = db.Column(db.String(250), nullable=False) + message = db.Column(db.String, nullable=False) # Relationships posted_by = db.relationship(User, foreign_keys=[user_id]) From 1c9f8c8846a6f84b0ce38009d2fdffe2515de30f Mon Sep 17 00:00:00 2001 From: Neil Rotstan Date: Thu, 30 Aug 2018 13:40:07 -0700 Subject: [PATCH 04/11] Notify mapper if their task was invalidated Send a notification message to mapper if a task they mapped was marked invalid. --- server/services/messaging/message_service.py | 12 ++++++++---- .../templates/invalidation_message_en.txt | 8 ++++++++ server/services/validator_service.py | 19 +++++++++++-------- 3 files changed, 27 insertions(+), 12 deletions(-) create mode 100644 server/services/messaging/templates/invalidation_message_en.txt diff --git a/server/services/messaging/message_service.py b/server/services/messaging/message_service.py index 0223dd92f2..acdc3fca2a 100644 --- a/server/services/messaging/message_service.py +++ b/server/services/messaging/message_service.py @@ -8,6 +8,7 @@ from server import create_app from server.models.dtos.message_dto import MessageDTO from server.models.postgis.message import Message, NotFound +from server.models.postgis.task import TaskStatus from server.services.messaging.smtp_service import SMTPService from server.services.messaging.template_service import get_template, get_profile_url from server.services.project_service import ProjectService @@ -43,16 +44,19 @@ def send_welcome_message(user: User): return welcome_message.id @staticmethod - def send_message_after_validation(validated_by: int, mapped_by: int, task_id: int, project_id: int): - """ Sends mapper a thank you, after their task has been marked as valid """ + def send_message_after_validation(status: int, validated_by: int, mapped_by: int, task_id: int, project_id: int): + """ Sends mapper a notification after their task has been marked valid or invalid """ if validated_by == mapped_by: - return # No need to send a thankyou to yourself + return # No need to send a message to yourself user = UserService.get_user_by_id(mapped_by) if user.validation_message == False: return # No need to send validation message text_template = get_template('validation_message_en.txt') + text_template = get_template('invalidation_message_en.txt' if status == TaskStatus.INVALIDATED \ + else 'validation_message_en.txt') + status_text = 'marked invalid' if status == TaskStatus.INVALIDATED else 'validated' task_link = MessageService.get_task_link(project_id, task_id) text_template = text_template.replace('[USERNAME]', user.username) @@ -61,7 +65,7 @@ def send_message_after_validation(validated_by: int, mapped_by: int, task_id: in validation_message = Message() validation_message.from_user_id = validated_by validation_message.to_user_id = mapped_by - validation_message.subject = f'Your mapping in Project {project_id} on {task_link} has just been validated' + validation_message.subject = f'Your mapping in Project {project_id} on {task_link} has just been {status_text}' validation_message.message = text_template validation_message.add_message() diff --git a/server/services/messaging/templates/invalidation_message_en.txt b/server/services/messaging/templates/invalidation_message_en.txt new file mode 100644 index 0000000000..6e2d2599a4 --- /dev/null +++ b/server/services/messaging/templates/invalidation_message_en.txt @@ -0,0 +1,8 @@ +Hi [USERNAME],
+
+Unfortunately, the task you marked as "Completely Mapped" was just marked as invalid. [TASK_LINK].
+
+This is an automated message, the person who validated it hopefully shared some feedback with you.
+
+Thank you very much for mapping! +
diff --git a/server/services/validator_service.py b/server/services/validator_service.py index 4e7ec06026..665894d02a 100644 --- a/server/services/validator_service.py +++ b/server/services/validator_service.py @@ -97,14 +97,17 @@ def unlock_tasks_after_validation(validated_dto: UnlockAfterValidationDTO) -> Ta # Parses comment to see if any users have been @'d MessageService.send_message_after_comment(validated_dto.user_id, task_to_unlock['comment'], task.id, validated_dto.project_id) - - if task_to_unlock['new_state'] == TaskStatus.VALIDATED and task.mapped_by not in message_sent_to: - # All mappers get a thankyou if their task has been validated :) Only once if multiple tasks mapped - MessageService.send_message_after_validation(validated_dto.user_id, task.mapped_by, task.id, - validated_dto.project_id) - # Set last_validation_date for the mapper to current date - task.mapper.last_validation_date = timestamp() - message_sent_to.append(task.mapped_by) + if task_to_unlock['new_state'] == TaskStatus.VALIDATED or task_to_unlock['new_state'] == TaskStatus.INVALIDATED: + # All mappers get a notification if their task has been validated or invalidated. + # Only once if multiple tasks mapped + if task.mapped_by not in message_sent_to: + MessageService.send_message_after_validation(task_to_unlock['new_state'], validated_dto.user_id, + task.mapped_by, task.id, validated_dto.project_id) + message_sent_to.append(task.mapped_by) + + if task_to_unlock['new_state'] == TaskStatus.VALIDATED: + # Set last_validation_date for the mapper to current date + task.mapper.last_validation_date = timestamp() # Update stats if user setting task to a different state from previous state prev_status = TaskHistory.get_last_status(project_id, task.id) From babd6906c97ce0740a9c27342405b2d2a00666b8 Mon Sep 17 00:00:00 2001 From: Neil Rotstan Date: Fri, 31 Aug 2018 10:57:18 -0700 Subject: [PATCH 05/11] Add opt-out note to invalidation notification --- .../services/messaging/templates/invalidation_message_en.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/services/messaging/templates/invalidation_message_en.txt b/server/services/messaging/templates/invalidation_message_en.txt index 6e2d2599a4..480c19d676 100644 --- a/server/services/messaging/templates/invalidation_message_en.txt +++ b/server/services/messaging/templates/invalidation_message_en.txt @@ -4,5 +4,7 @@ Unfortunately, the task you marked as "Completely Mapped" was just marked as inv
This is an automated message, the person who validated it hopefully shared some feedback with you.

-Thank you very much for mapping! +Thank you very much for mapping!
+
+Please note: You can opt-out of these automated validation messages by visiting your User Profile and clicking "Edit-Add Contact Details."

From 4860568b308aa4590e1468fea080b94d278ddd7f Mon Sep 17 00:00:00 2001 From: Neil Rotstan Date: Thu, 30 Aug 2018 13:59:32 -0700 Subject: [PATCH 06/11] Option to reset project tasks, preserving history. * Add option for project managers to reset all tasks back to ready status while preserving task history. * Add comment to each task indicating it was reset. --- .../edit-project/edit-project.controller.js | 57 ++++++++++++++++++ .../app/admin/edit-project/edit-project.html | 58 +++++++++++++++++++ client/app/services/project.service.js | 23 ++++++++ server/__init__.py | 3 +- server/api/project_admin_api.py | 41 +++++++++++++ server/models/postgis/task.py | 11 ++++ server/services/project_admin_service.py | 17 +++++- tests/server/unit/models/postgis/test_task.py | 28 +++++++++ .../services/test_project_admin_service.py | 35 +++++++++++ 9 files changed, 271 insertions(+), 2 deletions(-) diff --git a/client/app/admin/edit-project/edit-project.controller.js b/client/app/admin/edit-project/edit-project.controller.js index 0eb4f71681..35f843538f 100644 --- a/client/app/admin/edit-project/edit-project.controller.js +++ b/client/app/admin/edit-project/edit-project.controller.js @@ -59,12 +59,17 @@ // Delete vm.showDeleteConfirmationModal = false; + // Reset + vm.showResetConfirmationModal = false; + // Private project/add users vm.addUserEnabled = false; // Error messages vm.deleteProjectFail = false; vm.deleteProjectSuccess = false; + vm.resetProjectFail = false; + vm.resetProjectSuccess = false; vm.invalidateTasksFail = false; vm.invalidateTasksSuccess = false; vm.validateTasksFail = false; @@ -370,6 +375,37 @@ }; + /* + * Set the reset confirmation modal to visible/invisible + * @param showModal + */ + vm.showResetConfirmation = function(showModal){ + vm.showResetConfirmationModal = showModal; + if (!showModal && vm.resetProjectSuccess){ + $location.path('/'); + } + }; + + /** + * Reset a project + */ + vm.resetProject = function(){ + vm.resetProjectFail = false; + vm.resetProjectSuccess = false; + var resultsPromise = projectService.resetProject(vm.project.projectId); + resultsPromise.then(function () { + // Project reset successfully + vm.resetProjectFail = false; + vm.resetProjectSuccess = true; + // Reset the page elements + getProjectMetadata(vm.project.projectId); + }, function(){ + // Project not reset successfully + vm.resetProjectFail = true; + vm.resetProjectSuccess = false; + }); + }; + /** * Set the invalidate confirmation modal to visible/invisible * @param showModal @@ -428,6 +464,27 @@ }) }; + /** + * Reset all tasks on a project + */ + vm.resetAllTasks = function(){ + vm.resetInProgress = true; + vm.resetTasksFail = false; + vm.resetTasksSuccess = false; + var resultsPromise = projectService.resetAllTasks(vm.project.projectId); + resultsPromise.then(function(){ + // Tasks reset successfully + vm.resetTasksFail = false; + vm.resetTasksSuccess = true; + vm.resetInProgress = false; + }, function(){ + // Tasks not reset successfully + vm.resetTasksFail = true; + vm.resetTasksSuccess = false; + vm.resetInProgress = false; + }) + }; + /** * Set the show message contributors modal to visible/invisible */ diff --git a/client/app/admin/edit-project/edit-project.html b/client/app/admin/edit-project/edit-project.html index d4bf836bd1..f508142428 100644 --- a/client/app/admin/edit-project/edit-project.html +++ b/client/app/admin/edit-project/edit-project.html @@ -492,6 +492,17 @@

{{ 'In this area' | translate }}

{{ 'Delete project' | translate }} +
+ +

{{ 'Reset all tasks in the project to ready to map, preserving history.' | translate }}

+ + +

{{ 'This will clone all descriptions, instructions, metadata etc. The Area of Interest, the tasks and the priority areas will not be cloned. You will have to redraw/import these. Your newly cloned project will be in draft status.' | translate }}

@@ -729,6 +740,53 @@

{{ 'Task validation' | translate }}

+ + + + + + + +