Merge pull request #542 from ZmagoD/zd_SCI_825

Smart annotations and Notifications
This commit is contained in:
Zmago Devetak 2017-04-10 15:10:17 +02:00 committed by GitHub
commit 91793c295a
13 changed files with 396 additions and 1 deletions

View file

@ -524,6 +524,7 @@ function onClickSave() {
// First fetch all the data in input fields
data = {
request_url: $('#samples').data('current-uri'),
sample_id: $(selectedSample).attr("id"),
sample: {},
custom_fields: {}, // These fields are not currently bound to this sample

View file

@ -0,0 +1,155 @@
module StepsActions
extend ActiveSupport::Concern
# generates notification for smart annotations
def create_annotation_notifications(step)
# step description
step_description_annotation(step)
# checklists
step.checklists.each do |checklist|
checklist_name_annotation(step, checklist)
checklist.checklist_items.each do |checklist_item|
checklist_item_annotation(step, checklist_item)
end
end
end
def fetch_new_checklists_data
checklists = []
new_checklists = step_params[:checklists_attributes]
if new_checklists
new_checklists.each do |e|
list = PreviousChecklist.new(
e.second[:id].to_i,
e.second[:name]
)
if e.second[:checklist_items_attributes]
e.second[:checklist_items_attributes].each do |el|
list.add_checklist(
PreviousChecklistItem.new(el.second[:id].to_i, el.second[:text])
)
end
end
checklists << list
end
end
checklists
end
def fetch_old_checklists_data(step)
checklists = []
if step.checklists
step.checklists.each do |e|
list = PreviousChecklist.new(
e.id,
e.name
)
e.checklist_items.each do |el|
list.add_checklist(
PreviousChecklistItem.new(el.id, el.text)
)
end
checklists << list
end
end
checklists
end
# used for step update action it traverse through the input params and
# generates notifications
def update_annotation_notifications(step,
old_description,
new_checklists,
old_checklists)
step_description_annotation(step, old_description)
new_checklists.each do |e|
# generates smart annotaion if the checklist is new
add_new_checklist(step, e) if e.id.zero?
checklist_name_annotation(step, e) unless e.id
# else check if checklist is not deleted and generates
# new notifications
next unless old_checklists.map(&:id).include?(e.id)
old_checklist = old_checklists.select { |i| i.id == e.id }.first
checklist_name_annotation(step, e, old_checklist.name)
e.items.each do |ci|
old_list = old_checklists.select { |i| i.id == e.id }.first
old_item = old_list.items.select { |i| i.id == ci.id }.first if old_list
checklist_item_annotation(step, ci, old_item.text)
end
end
end
def add_new_checklist(step, checklist)
checklist_name_annotation(step, checklist)
checklist.items.each do |ci|
checklist_item_annotation(step, ci)
end
end
def checklist_item_annotation(step, checklist_item, old_text = nil)
smart_annotation_notification(
old_text: old_text,
new_text: checklist_item.text,
title: t('notifications.checklist_title',
user: current_user.full_name,
step: step.name),
message: annotation_message(step)
)
end
def checklist_name_annotation(step, checklist, old_text = nil)
smart_annotation_notification(
old_text: old_text,
new_text: checklist.name,
title: t('notifications.checklist_title',
user: current_user.full_name,
step: step.name),
message: annotation_message(step)
)
end
def step_description_annotation(step, old_text = nil)
smart_annotation_notification(
old_text: old_text,
new_text: step.description,
title: t('notifications.step_description_title',
user: current_user.full_name,
step: step.name),
message: annotation_message(step)
)
end
def annotation_message(step)
return t('notifications.step_annotation_message_html',
project: link_to(
step.my_module.experiment.project.name,
project_url(step.my_module.experiment.project)
),
my_module: link_to(
step.my_module.name,
protocols_my_module_url(step.my_module)
),
step: link_to(
step.name,
protocols_my_module_url(step.my_module)
)) if @protocol.in_module?
t('notifications.protocol_step_annotation_message_html',
protocol: link_to(
@protocol.name, edit_protocol_url(@protocol)
))
end
# temporary data containers
PreviousChecklistItem = Struct.new(:id, :text)
PreviousChecklist = Struct.new(:id, :name, :items) do
def initialize(id, name, items = [])
super(id, name, items)
end
def add_checklist(item)
items << item
end
end
end

View file

@ -3,6 +3,9 @@ class ExperimentsController < ApplicationController
include PermissionHelper
include TeamsHelper
include InputSanitizeHelper
include ActionView::Helpers::TextHelper
include ApplicationHelper
include Rails.application.routes.url_helpers
before_action :set_experiment,
except: [:new, :create]
@ -43,6 +46,8 @@ class ExperimentsController < ApplicationController
@experiment.last_modified_by = current_user
@experiment.project = @project
if @experiment.save
experiment_annotation_notification
Activity.create(
type_of: :create_experiment,
project: @experiment.project,
@ -87,9 +92,12 @@ class ExperimentsController < ApplicationController
end
def update
old_text = @experiment.description
@experiment.update_attributes(experiment_params)
@experiment.last_modified_by = current_user
if @experiment.save
experiment_annotation_notification(old_text)
Activity.create(
type_of: :edit_experiment,
project: @experiment.project,
@ -346,4 +354,19 @@ class ExperimentsController < ApplicationController
def choose_layout
action_name.in?(%w(index archive)) ? 'main' : 'fluid'
end
def experiment_annotation_notification(old_text = nil)
smart_annotation_notification(
old_text: old_text,
new_text: @experiment.description,
title: t('notifications.experiment_annotation_title',
experiment: @experiment.name,
user: current_user.full_name),
message: t('notifications.experiment_annotation_message_html',
project: link_to(@experiment.project.name,
project_url(@experiment.project)),
experiment: link_to(@experiment.name,
canvas_experiment_url(@experiment)))
)
end
end

View file

@ -2,6 +2,7 @@ class MyModuleCommentsController < ApplicationController
include ActionView::Helpers::TextHelper
include InputSanitizeHelper
include ApplicationHelper
include Rails.application.routes.url_helpers
before_action :load_vars
before_action :check_view_permissions, only: :index
@ -51,6 +52,8 @@ class MyModuleCommentsController < ApplicationController
respond_to do |format|
if @comment.save
my_module_comment_annotation_notification
# Generate activity
Activity.create(
type_of: :add_comment_to_module,
@ -102,10 +105,13 @@ class MyModuleCommentsController < ApplicationController
end
def update
old_text = @comment.message
@comment.message = comment_params[:message]
respond_to do |format|
format.json do
if @comment.save
my_module_comment_annotation_notification(old_text)
# Generate activity
Activity.create(
type_of: :edit_module_comment,
@ -190,4 +196,23 @@ class MyModuleCommentsController < ApplicationController
def comment_params
params.require(:comment).permit(:message)
end
def my_module_comment_annotation_notification(old_text = nil)
smart_annotation_notification(
old_text: old_text,
new_text: @comment.message,
title: t('notifications.my_module_comment_annotation_title',
my_module: @my_module.name,
user: current_user.full_name),
message: t('notifications.my_module_annotation_message_html',
project: link_to(@my_module.experiment.project.name,
project_url(@my_module
.experiment
.project)),
my_module: link_to(@my_module.name,
protocols_my_module_url(
@my_module
)))
)
end
end

View file

@ -2,6 +2,7 @@ class ProjectCommentsController < ApplicationController
include ActionView::Helpers::TextHelper
include InputSanitizeHelper
include ApplicationHelper
include Rails.application.routes.url_helpers
before_action :load_vars
before_action :check_view_permissions, only: :index
@ -50,6 +51,7 @@ class ProjectCommentsController < ApplicationController
respond_to do |format|
if @comment.save
project_comment_annotation_notification
# Generate activity
Activity.create(
type_of: :add_comment_to_project,
@ -99,10 +101,13 @@ class ProjectCommentsController < ApplicationController
end
def update
old_text = @comment.message
@comment.message = comment_params[:message]
respond_to do |format|
format.json do
if @comment.save
project_comment_annotation_notification(old_text)
# Generate activity
Activity.create(
type_of: :edit_project_comment,
@ -185,4 +190,16 @@ class ProjectCommentsController < ApplicationController
def comment_params
params.require(:comment).permit(:message)
end
def project_comment_annotation_notification(old_text = nil)
smart_annotation_notification(
old_text: old_text,
new_text: @comment.message,
title: t('notifications.project_comment_annotation_title',
project: @project.name,
user: current_user.full_name),
message: t('notifications.project_annotation_message_html',
project: link_to(@project.name, project_url(@project)))
)
end
end

View file

@ -49,6 +49,7 @@ class ResultCommentsController < ApplicationController
respond_to do |format|
if @comment.save
result_comment_annotation_notification
# Generate activity
Activity.create(
type_of: :add_comment_to_result,
@ -100,10 +101,13 @@ class ResultCommentsController < ApplicationController
end
def update
old_text = @comment.message
@comment.message = comment_params[:message]
respond_to do |format|
format.json do
if @comment.save
result_comment_annotation_notification(old_text)
# Generate activity
Activity.create(
type_of: :edit_result_comment,
@ -192,4 +196,22 @@ class ResultCommentsController < ApplicationController
params.require(:comment).permit(:message)
end
def result_comment_annotation_notification(old_text = nil)
smart_annotation_notification(
old_text: (old_text if old_text),
new_text: @comment.message,
title: t('notifications.result_comment_annotation_title',
result: @result.name,
user: current_user.full_name),
message: t('notifications.result_annotation_message_html',
project: link_to(@result.my_module.experiment.project.name,
project_url(@result.my_module
.experiment
.project)),
my_module: link_to(@result.my_module.name,
protocols_my_module_url(
@result.my_module
)))
)
end
end

View file

@ -1,5 +1,8 @@
class ResultTextsController < ApplicationController
include ResultsHelper
include ActionView::Helpers::UrlHelper
include ApplicationHelper
include Rails.application.routes.url_helpers
before_action :load_vars, only: [:edit, :update, :download]
before_action :load_vars_nested, only: [:new, :create]
@ -38,6 +41,8 @@ class ResultTextsController < ApplicationController
respond_to do |format|
if (@result.save and @result_text.save) then
result_annotation_notification
# Generate activity
Activity.create(
type_of: :add_result,
@ -88,6 +93,7 @@ class ResultTextsController < ApplicationController
end
def update
old_text = @result_text.text
update_params = result_params
@result.last_modified_by = current_user
@result.assign_attributes(update_params)
@ -129,6 +135,9 @@ class ResultTextsController < ApplicationController
)
end
end
result_annotation_notification(old_text) if saved
respond_to do |format|
if saved
format.html {
@ -208,5 +217,22 @@ class ResultTextsController < ApplicationController
)
end
def result_annotation_notification(old_text = nil)
smart_annotation_notification(
old_text: (old_text if old_text),
new_text: @result_text.text,
title: t('notifications.result_annotation_title',
result: @result.name,
user: current_user.full_name),
message: t('notifications.result_annotation_message_html',
project: link_to(@result.my_module.experiment.project.name,
project_url(@result.my_module
.experiment
.project)),
my_module: link_to(@result.my_module.name,
protocols_my_module_url(
@result.my_module
)))
)
end
end

View file

@ -1,5 +1,7 @@
class SamplesController < ApplicationController
include InputSanitizeHelper
include ActionView::Helpers::TextHelper
include ApplicationHelper
before_action :load_vars, only: [:edit, :update, :destroy, :show]
before_action :load_vars_nested, only: [:new, :create]
@ -82,6 +84,8 @@ class SamplesController < ApplicationController
errors[:custom_fields] << {
"#{id}": scf.errors.messages
}
else
sample_annotation_notification(sample, scf)
end
end
end
@ -198,6 +202,7 @@ class SamplesController < ApplicationController
scf = SampleCustomField.where("custom_field_id = ? AND sample_id = ?", id, sample.id).take
if scf
old_text = scf.value
# Well, client was naughty, no XMAS for him this year, update
# existing SCF instead of creating new one
scf.value = val
@ -207,6 +212,8 @@ class SamplesController < ApplicationController
errors[:custom_fields] << {
"#{id}": scf.errors.messages
}
else
sample_annotation_notification(sample, scf, old_text)
end
else
# SCF doesn't exist, create it
@ -220,6 +227,8 @@ class SamplesController < ApplicationController
errors[:custom_fields] << {
"#{id}": scf.errors.messages
}
else
sample_annotation_notification(sample, scf)
end
end
end
@ -235,6 +244,7 @@ class SamplesController < ApplicationController
if val.empty?
scf_to_delete << scf
else
old_text = scf.value
# SCF exists, update away
scf.value = val
@ -242,6 +252,8 @@ class SamplesController < ApplicationController
errors[:sample_custom_fields] << {
"#{id}": scf.errors.messages
}
else
sample_annotation_notification(sample, scf, old_text)
end
end
else
@ -331,4 +343,19 @@ class SamplesController < ApplicationController
:sample_group_id
)
end
def sample_annotation_notification(sample, scf, old_text = nil)
table_url = params.fetch(:request_url) { :request_url_must_be_present }
smart_annotation_notification(
old_text: (old_text if old_text),
new_text: scf.value,
title: t('notifications.sample_annotation_title',
user: current_user.full_name,
column: scf.custom_field.name,
sample: sample.name),
message: t('notifications.sample_annotation_message_html',
sample: link_to(sample.name, table_url),
column: link_to(scf.custom_field.name, table_url))
)
end
end

View file

@ -2,6 +2,7 @@ class StepCommentsController < ApplicationController
include ActionView::Helpers::TextHelper
include InputSanitizeHelper
include ApplicationHelper
include Rails.application.routes.url_helpers
before_action :load_vars
@ -47,6 +48,8 @@ class StepCommentsController < ApplicationController
respond_to do |format|
if @comment.save
step_comment_annotation_notification
# Generate activity (this can only occur in module,
# but nonetheless check if my module is not nil)
if @protocol.in_module?
@ -101,10 +104,13 @@ class StepCommentsController < ApplicationController
end
def update
old_text = @comment.message
@comment.message = comment_params[:message]
respond_to do |format|
format.json do
if @comment.save
step_comment_annotation_notification(old_text)
# Generate activity
if @protocol.in_module?
Activity.create(
@ -198,4 +204,25 @@ class StepCommentsController < ApplicationController
def comment_params
params.require(:comment).permit(:message)
end
def step_comment_annotation_notification(old_text = nil)
smart_annotation_notification(
old_text: (old_text if old_text),
new_text: comment_params[:message],
title: t('notifications.step_comment_annotation_title',
step: @step.name,
user: current_user.full_name),
message: t('notifications.step_annotation_message_html',
project: link_to(@step.my_module.experiment.project.name,
project_url(@step.my_module
.experiment
.project)),
my_module: link_to(@step.my_module.name,
protocols_my_module_url(
@step.my_module
)),
step: link_to(@step.name,
protocols_my_module_url(@step.my_module)))
)
end
end

View file

@ -1,6 +1,7 @@
class StepsController < ApplicationController
include ActionView::Helpers::TextHelper
include ApplicationHelper
include StepsActions
before_action :load_vars, only: [:edit, :update, :destroy, :show]
before_action :load_vars_nested, only: [:new, :create]
@ -56,6 +57,7 @@ class StepsController < ApplicationController
asset.post_process_file(@protocol.team)
end
create_annotation_notifications(@step)
# Generate activity
if @protocol.in_module?
Activity.create(
@ -129,6 +131,9 @@ class StepsController < ApplicationController
def update
respond_to do |format|
old_description = @step.description
old_checklists = fetch_old_checklists_data(@step)
new_checklists = fetch_new_checklists_data
previous_size = @step.space_taken
step_params_all = step_params
@ -156,6 +161,12 @@ class StepsController < ApplicationController
if @step.save
@step.reload
# generates notification on step upadate
update_annotation_notifications(@step,
old_description,
new_checklists,
old_checklists)
# Release team's space taken
team = @protocol.team
team.release_space(previous_size)

View file

@ -58,6 +58,50 @@ module ApplicationHelper
!@experiment.nil?
end
def smart_annotation_notification(options = {})
title = options.fetch(:title) { :title_must_be_present }
message = options.fetch(:message) { :message_must_be_present }
new_text = options.fetch(:new_text) { :new_text_must_be_present }
old_text = options[:old_text] || ''
sa_user = /\[\@(.*?)~([0-9a-zA-Z]+)\]/
# fetch user ids from the previous text
old_user_ids = []
old_text.gsub(sa_user) do |el|
match = el.match(sa_user)
old_user_ids << match[2].base62_decode
end
# fetch user ids from the new text
new_user_ids = []
new_text.gsub(sa_user) do |el|
match = el.match(sa_user)
new_user_ids << match[2].base62_decode
end
# check if the user has been already mentioned
annotated_users = []
new_user_ids.each do |el|
annotated_users << el unless old_user_ids.include?(el)
end
# restrict the list of ids and generate notification
annotated_users.uniq.each do |user_id|
target_user = User.find_by_id(user_id)
next unless target_user
generate_annotation_notification(target_user, title, message)
end
end
def generate_annotation_notification(target_user, title, message)
notification = Notification.create(
type_of: :assignment,
title:
ActionController::Base.helpers.sanitize(title),
message:
ActionController::Base.helpers.sanitize(message)
)
if target_user.assignments_notification
UserNotification.create(notification: notification, user: target_user)
end
end
def smart_annotation_parser(text, team = nil)
new_text = smart_annotation_filter_resources(text)
new_text = smart_annotation_filter_users(new_text, team)

View file

@ -125,6 +125,7 @@
<div class="samples-table">
<table id="samples" class="table"
data-current-uri="<%= request.original_url %>"
data-team-id="<%= @project.team.id %>"
data-user-id="<%= @current_user.id %>"
data-source="<%= @samples_index_link %>"

View file

@ -1516,6 +1516,22 @@ en:
recent_changes: "Recent changes"
system_message: "sciNote system message"
deliver: 'Exportable content'
experiment_annotation_title: "%{user} mentioned you in %{experiment} experiment."
experiment_annotation_message_html: "Project: %{project} | Experiment: %{experiment}"
project_comment_annotation_title: "%{user} mentioned you in %{project} project comment."
project_annotation_message_html: "Project: %{project}"
my_module_comment_annotation_title: "%{user} mentioned you in %{my_module} task comment."
my_module_annotation_message_html: "Project: %{project} | Task: %{my_module}"
step_comment_annotation_title: "%{user} mentioned you in %{step} step comment."
step_description_title: "%{user} mentioned you in %{step} step description."
checklist_title: "%{user} mentioned you in %{step} step checklist."
step_annotation_message_html: "Project: %{project} | Task: %{my_module} | Step: %{step}"
result_annotation_title: "%{user} mentioned you in result %{result}."
result_comment_annotation_title: "%{user} mentioned you in result %{result} comment."
result_annotation_message_html: "Project: %{project} | Task: %{my_module}"
sample_annotation_title: "%{user} mentioned you in Column: %{column} of Sample %{sample}"
sample_annotation_message_html: "Sample: %{sample} | Column: %{column}"
protocol_step_annotation_message_html: "Protocol: %{protocol}"
email_title: "You've received a sciNote notification!"
assign_user_to_team: "<i>%{assigned_user}</i> was added as %{role} to team <strong>%{team}</strong> by <i>%{assigned_by_user}</i>."
unassign_user_from_team: "<i>%{unassigned_user}</i> was removed from team <strong>%{team}</strong> by <i>%{unassigned_by_user}</i>."