From ef6fbe8f2a7f9391ac42047c9fa1aea40ac1a6c9 Mon Sep 17 00:00:00 2001 From: Thorsten Eckel Date: Tue, 31 Jan 2017 18:13:45 +0100 Subject: [PATCH] Refactoring: Splitted ApplicationModel into multiple concerns. Notice: Includes object some method name changes. --- app/controllers/application_controller.rb | 26 +- .../object_manager_attributes_controller.rb | 4 +- app/controllers/organizations_controller.rb | 6 +- app/controllers/ticket_articles_controller.rb | 18 +- app/controllers/tickets_controller.rb | 14 +- app/controllers/users_controller.rb | 34 +- app/models/application_model.rb | 1658 +---------------- ...am_base.rb => activity_stream_loggable.rb} | 33 +- app/models/application_model/assets.rb | 44 - .../background_job_search_index.rb | 16 - app/models/application_model/cacheable.rb | 70 + app/models/application_model/can_lookup.rb | 87 + .../can_lookup_search_index_attributes.rb | 66 + .../checks_attribute_length.rb | 38 + app/models/application_model/cleans_param.rb | 90 + .../application_model/cleans_recent_view.rb | 21 + .../fills_by_user_columns.rb | 64 + .../handles_creates_and_updates.rb | 201 ++ app/models/application_model/has_assets.rb | 153 ++ .../application_model/has_associations.rb | 357 ++++ .../application_model/has_attachments.rb | 74 + .../has_latest_change_timestamp.rb | 46 + ...istory_log_base.rb => history_loggable.rb} | 34 +- app/models/application_model/importable.rb | 19 + .../application_model/search_index_base.rb | 127 -- .../application_model/touches_references.rb | 30 + app/models/calendar.rb | 7 +- app/models/channel/assets.rb | 2 +- app/models/channel/email_parser.rb | 2 +- app/models/concerns/historisable.rb | 140 ++ app/models/concerns/latest_change_observed.rb | 19 + app/models/concerns/logs_activity_stream.rb | 87 + app/models/concerns/notifies_clients.rb | 138 ++ app/models/concerns/search_indexed.rb | 138 ++ app/models/concerns/uniq_named.rb | 31 + app/models/email_address.rb | 4 +- app/models/group.rb | 10 +- app/models/history.rb | 11 - app/models/job.rb | 4 +- app/models/job/assets.rb | 2 +- app/models/macro.rb | 7 +- app/models/object_manager/attribute.rb | 5 +- app/models/organization.rb | 12 +- app/models/overview.rb | 6 +- app/models/overview/assets.rb | 2 +- app/models/permission.rb | 5 +- app/models/role.rb | 11 +- app/models/signature.rb | 7 +- app/models/sla.rb | 5 +- app/models/sla/assets.rb | 2 +- app/models/template.rb | 7 +- app/models/text_module.rb | 7 +- app/models/ticket.rb | 101 +- app/models/ticket/activity_stream_log.rb | 37 - app/models/ticket/article.rb | 59 +- .../ticket/article/activity_stream_log.rb | 38 - app/models/ticket/article/history_log.rb | 32 - app/models/ticket/assets.rb | 2 +- app/models/ticket/history_log.rb | 81 - app/models/ticket/state.rb | 4 +- app/models/ticket/state_type.rb | 7 +- app/models/user.rb | 60 +- app/models/user/assets.rb | 2 +- lib/background_job_search_index.rb | 21 + lib/models.rb | 2 + lib/sessions/backend/base.rb | 1 + lib/sessions/backend/collections/base.rb | 2 +- test/unit/assets_test.rb | 38 +- 68 files changed, 2211 insertions(+), 2277 deletions(-) rename app/models/application_model/{activity_stream_base.rb => activity_stream_loggable.rb} (51%) delete mode 100644 app/models/application_model/assets.rb delete mode 100644 app/models/application_model/background_job_search_index.rb create mode 100644 app/models/application_model/cacheable.rb create mode 100644 app/models/application_model/can_lookup.rb create mode 100644 app/models/application_model/can_lookup_search_index_attributes.rb create mode 100644 app/models/application_model/checks_attribute_length.rb create mode 100644 app/models/application_model/cleans_param.rb create mode 100644 app/models/application_model/cleans_recent_view.rb create mode 100644 app/models/application_model/fills_by_user_columns.rb create mode 100644 app/models/application_model/handles_creates_and_updates.rb create mode 100644 app/models/application_model/has_assets.rb create mode 100644 app/models/application_model/has_associations.rb create mode 100644 app/models/application_model/has_attachments.rb create mode 100644 app/models/application_model/has_latest_change_timestamp.rb rename app/models/application_model/{history_log_base.rb => history_loggable.rb} (73%) create mode 100644 app/models/application_model/importable.rb delete mode 100644 app/models/application_model/search_index_base.rb create mode 100644 app/models/application_model/touches_references.rb create mode 100644 app/models/concerns/historisable.rb create mode 100644 app/models/concerns/latest_change_observed.rb create mode 100644 app/models/concerns/logs_activity_stream.rb create mode 100644 app/models/concerns/notifies_clients.rb create mode 100644 app/models/concerns/search_indexed.rb create mode 100644 app/models/concerns/uniq_named.rb delete mode 100644 app/models/ticket/activity_stream_log.rb delete mode 100644 app/models/ticket/article/activity_stream_log.rb delete mode 100644 app/models/ticket/article/history_log.rb delete mode 100644 app/models/ticket/history_log.rb create mode 100644 lib/background_job_search_index.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index da92d096e..df8dd20b0 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -394,7 +394,7 @@ class ApplicationController < ActionController::Base # remember time accounting time_unit = params[:time_unit] - clean_params = Ticket::Article.param_association_lookup(params) + clean_params = Ticket::Article.association_name_to_id_convert(params) clean_params = Ticket::Article.param_cleanup(clean_params, true) # overwrite params @@ -514,7 +514,7 @@ class ApplicationController < ActionController::Base # model helper def model_create_render(object, params) - clean_params = object.param_association_lookup(params) + clean_params = object.association_name_to_id_convert(params) clean_params = object.param_cleanup(clean_params, true) # create object @@ -524,10 +524,10 @@ class ApplicationController < ActionController::Base generic_object.save! # set relations - generic_object.param_set_associations(params) + generic_object.associations_from_param(params) if params[:expand] - render json: generic_object.attributes_with_relation_names, status: :created + render json: generic_object.attributes_with_association_names, status: :created return end @@ -535,7 +535,7 @@ class ApplicationController < ActionController::Base end def model_create_render_item(generic_object) - render json: generic_object.attributes_with_associations, status: :created + render json: generic_object.attributes_with_association_ids, status: :created end def model_update_render(object, params) @@ -543,7 +543,7 @@ class ApplicationController < ActionController::Base # find object generic_object = object.find(params[:id]) - clean_params = object.param_association_lookup(params) + clean_params = object.association_name_to_id_convert(params) clean_params = object.param_cleanup(clean_params, true) generic_object.with_lock do @@ -552,11 +552,11 @@ class ApplicationController < ActionController::Base generic_object.update_attributes!(clean_params) # set relations - generic_object.param_set_associations(params) + generic_object.associations_from_param(params) end if params[:expand] - render json: generic_object.attributes_with_relation_names, status: :ok + render json: generic_object.attributes_with_association_names, status: :ok return end @@ -564,7 +564,7 @@ class ApplicationController < ActionController::Base end def model_update_render_item(generic_object) - render json: generic_object.attributes_with_associations, status: :ok + render json: generic_object.attributes_with_association_ids, status: :ok end def model_destroy_render(object, params) @@ -581,7 +581,7 @@ class ApplicationController < ActionController::Base if params[:expand] generic_object = object.find(params[:id]) - render json: generic_object.attributes_with_relation_names, status: :ok + render json: generic_object.attributes_with_association_names, status: :ok return end @@ -596,7 +596,7 @@ class ApplicationController < ActionController::Base end def model_show_render_item(generic_object) - render json: generic_object.attributes_with_associations, status: :ok + render json: generic_object.attributes_with_association_ids, status: :ok end def model_index_render(object, params) @@ -620,7 +620,7 @@ class ApplicationController < ActionController::Base if params[:expand] list = [] generic_objects.each { |generic_object| - list.push generic_object.attributes_with_relation_names + list.push generic_object.attributes_with_association_names } render json: list, status: :ok return @@ -642,7 +642,7 @@ class ApplicationController < ActionController::Base generic_objects_with_associations = [] generic_objects.each { |item| - generic_objects_with_associations.push item.attributes_with_associations + generic_objects_with_associations.push item.attributes_with_association_ids } model_index_render_result(generic_objects_with_associations) end diff --git a/app/controllers/object_manager_attributes_controller.rb b/app/controllers/object_manager_attributes_controller.rb index 066fd76d7..5dbf0d281 100644 --- a/app/controllers/object_manager_attributes_controller.rb +++ b/app/controllers/object_manager_attributes_controller.rb @@ -43,7 +43,7 @@ class ObjectManagerAttributesController < ApplicationController position: 1550, editable: true, ) - render json: object_manager_attribute.attributes_with_associations, status: :created + render json: object_manager_attribute.attributes_with_association_ids, status: :created rescue => e raise Exceptions::UnprocessableEntity, e end @@ -64,7 +64,7 @@ class ObjectManagerAttributesController < ApplicationController position: 1550, editable: true, ) - render json: object_manager_attribute.attributes_with_associations, status: :ok + render json: object_manager_attribute.attributes_with_association_ids, status: :ok rescue => e raise Exceptions::UnprocessableEntity, e end diff --git a/app/controllers/organizations_controller.rb b/app/controllers/organizations_controller.rb index 0932fb9ec..9259df98e 100644 --- a/app/controllers/organizations_controller.rb +++ b/app/controllers/organizations_controller.rb @@ -72,7 +72,7 @@ curl http://localhost/api/v1/organizations -v -u #{login}:#{password} if params[:expand] list = [] organizations.each { |organization| - list.push organization.attributes_with_relation_names + list.push organization.attributes_with_association_names } render json: list, status: :ok return @@ -124,7 +124,7 @@ curl http://localhost/api/v1/organizations/#{id} -v -u #{login}:#{password} end if params[:expand] - organization = Organization.find(params[:id]).attributes_with_relation_names + organization = Organization.find(params[:id]).attributes_with_association_names render json: organization, status: :ok return end @@ -256,7 +256,7 @@ curl http://localhost/api/v1/organization/{id} -v -u #{login}:#{password} -H "Co if params[:expand] list = [] organization_all.each { |organization| - list.push organization.attributes_with_relation_names + list.push organization.attributes_with_association_names } render json: list, status: :ok return diff --git a/app/controllers/ticket_articles_controller.rb b/app/controllers/ticket_articles_controller.rb index 20423edc1..bff6d639e 100644 --- a/app/controllers/ticket_articles_controller.rb +++ b/app/controllers/ticket_articles_controller.rb @@ -17,7 +17,7 @@ class TicketArticlesController < ApplicationController article_permission(article) if params[:expand] - result = article.attributes_with_relation_names + result = article.attributes_with_association_names result[:attachments] = article.attachments render json: result, status: :ok return @@ -29,7 +29,7 @@ class TicketArticlesController < ApplicationController return end - render json: article.attributes_with_relation_names + render json: article.attributes_with_association_names end # GET /ticket_articles/by_ticket/1 @@ -46,7 +46,7 @@ class TicketArticlesController < ApplicationController # ignore internal article if customer is requesting next if article.internal == true && current_user.permissions?('ticket.customer') - result = article.attributes_with_relation_names + result = article.attributes_with_association_names # add attachments result[:attachments] = article.attachments @@ -79,7 +79,7 @@ class TicketArticlesController < ApplicationController # ignore internal article if customer is requesting next if article.internal == true && current_user.permissions?('ticket.customer') - articles.push article.attributes_with_relation_names + articles.push article.attributes_with_association_names } render json: articles end @@ -91,7 +91,7 @@ class TicketArticlesController < ApplicationController article = article_create(ticket, params) if params[:expand] - result = article.attributes_with_relation_names + result = article.attributes_with_association_names result[:attachments] = article.attachments render json: result, status: :created return @@ -103,7 +103,7 @@ class TicketArticlesController < ApplicationController return end - render json: article.attributes_with_relation_names, status: :created + render json: article.attributes_with_association_names, status: :created end # PUT /articles/1 @@ -117,13 +117,13 @@ class TicketArticlesController < ApplicationController raise Exceptions::NotAuthorized, 'Not authorized (ticket.agent or admin permission required)!' end - clean_params = Ticket::Article.param_association_lookup(params) + clean_params = Ticket::Article.association_name_to_id_convert(params) clean_params = Ticket::Article.param_cleanup(clean_params, true) article.update_attributes!(clean_params) if params[:expand] - result = article.attributes_with_relation_names + result = article.attributes_with_association_names result[:attachments] = article.attachments render json: result, status: :ok return @@ -135,7 +135,7 @@ class TicketArticlesController < ApplicationController return end - render json: article.attributes_with_relation_names, status: :ok + render json: article.attributes_with_association_names, status: :ok end # DELETE /articles/1 diff --git a/app/controllers/tickets_controller.rb b/app/controllers/tickets_controller.rb index 49c7272c9..e7e1b60ce 100644 --- a/app/controllers/tickets_controller.rb +++ b/app/controllers/tickets_controller.rb @@ -23,7 +23,7 @@ class TicketsController < ApplicationController if params[:expand] list = [] tickets.each { |ticket| - list.push ticket.attributes_with_relation_names + list.push ticket.attributes_with_association_names } render json: list, status: :ok return @@ -54,7 +54,7 @@ class TicketsController < ApplicationController ticket_permission(ticket) if params[:expand] - result = ticket.attributes_with_relation_names + result = ticket.attributes_with_association_names render json: result, status: :ok return end @@ -75,7 +75,7 @@ class TicketsController < ApplicationController # POST /api/v1/tickets def create - clean_params = Ticket.param_association_lookup(params) + clean_params = Ticket.association_name_to_id_convert(params) # overwrite params if !current_user.permissions?('ticket.agent') @@ -166,7 +166,7 @@ class TicketsController < ApplicationController end if params[:expand] - result = ticket.reload.attributes_with_relation_names + result = ticket.reload.attributes_with_association_names render json: result, status: :created return end @@ -186,7 +186,7 @@ class TicketsController < ApplicationController ticket = Ticket.find(params[:id]) ticket_permission(ticket) - clean_params = Ticket.param_association_lookup(params) + clean_params = Ticket.association_name_to_id_convert(params) clean_params = Ticket.param_cleanup(clean_params, true) # overwrite params @@ -204,7 +204,7 @@ class TicketsController < ApplicationController end if params[:expand] - result = ticket.reload.attributes_with_relation_names + result = ticket.reload.attributes_with_association_names render json: result, status: :ok return end @@ -430,7 +430,7 @@ class TicketsController < ApplicationController if params[:expand] list = [] tickets.each { |ticket| - list.push ticket.attributes_with_relation_names + list.push ticket.attributes_with_association_names } render json: list, status: :ok return diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index be9db09a3..a94b00eca 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -34,7 +34,7 @@ class UsersController < ApplicationController if params[:expand] list = [] users.each { |user| - list.push user.attributes_with_relation_names + list.push user.attributes_with_association_names } render json: list, status: :ok return @@ -56,7 +56,7 @@ class UsersController < ApplicationController users_all = [] users.each { |user| - users_all.push User.lookup(id: user.id).attributes_with_associations + users_all.push User.lookup(id: user.id).attributes_with_association_ids } render json: users_all, status: :ok end @@ -79,7 +79,7 @@ class UsersController < ApplicationController permission_check_local if params[:expand] - user = User.find(params[:id]).attributes_with_relation_names + user = User.find(params[:id]).attributes_with_association_names render json: user, status: :ok return end @@ -90,7 +90,7 @@ class UsersController < ApplicationController return end - user = User.find(params[:id]).attributes_with_associations + user = User.find(params[:id]).attributes_with_association_ids user.delete('password') render json: user end @@ -109,10 +109,10 @@ class UsersController < ApplicationController # in case of authentication, set current_user to access later authentication_check_only({}) - clean_params = User.param_association_lookup(params) + clean_params = User.association_name_to_id_convert(params) clean_params = User.param_cleanup(clean_params, true) user = User.new(clean_params) - user.param_set_associations(params) + user.associations_from_param(params) # check if it's first user, the admin user # inital admin account @@ -227,12 +227,12 @@ class UsersController < ApplicationController end if params[:expand] - user = User.find(user.id).attributes_with_relation_names + user = User.find(user.id).attributes_with_association_names render json: user, status: :created return end - user_new = User.find(user.id).attributes_with_associations + user_new = User.find(user.id).attributes_with_association_ids user_new.delete('password') render json: user_new, status: :created end @@ -253,7 +253,7 @@ class UsersController < ApplicationController permission_check_local user = User.find(params[:id]) - clean_params = User.param_association_lookup(params) + clean_params = User.association_name_to_id_convert(params) clean_params = User.param_cleanup(clean_params, true) # permission check @@ -264,29 +264,29 @@ class UsersController < ApplicationController # only allow Admin's if current_user.permissions?('admin.user') && (params[:role_ids] || params[:roles]) user.role_ids = params[:role_ids] - user.param_set_associations({ role_ids: params[:role_ids], roles: params[:roles] }) + user.associations_from_param({ role_ids: params[:role_ids], roles: params[:roles] }) end # only allow Admin's if current_user.permissions?('admin.user') && (params[:group_ids] || params[:groups]) user.group_ids = params[:group_ids] - user.param_set_associations({ group_ids: params[:group_ids], groups: params[:groups] }) + user.associations_from_param({ group_ids: params[:group_ids], groups: params[:groups] }) end # only allow Admin's and Agent's if current_user.permissions?(['admin.user', 'ticket.agent']) && (params[:organization_ids] || params[:organizations]) - user.param_set_associations({ organization_ids: params[:organization_ids], organizations: params[:organizations] }) + user.associations_from_param({ organization_ids: params[:organization_ids], organizations: params[:organizations] }) end if params[:expand] - user = User.find(user.id).attributes_with_relation_names + user = User.find(user.id).attributes_with_association_names render json: user, status: :ok return end end # get new data - user_new = User.find(user.id).attributes_with_associations + user_new = User.find(user.id).attributes_with_association_ids user_new.delete('password') render json: user_new, status: :ok end @@ -318,7 +318,7 @@ class UsersController < ApplicationController def me if params[:expand] - user = current_user.attributes_with_relation_names + user = current_user.attributes_with_association_names render json: user, status: :ok return end @@ -329,7 +329,7 @@ class UsersController < ApplicationController return end - user = current_user.attributes_with_associations + user = current_user.attributes_with_association_ids user.delete('password') render json: user end @@ -390,7 +390,7 @@ class UsersController < ApplicationController if params[:expand] list = [] user_all.each { |user| - list.push user.attributes_with_relation_names + list.push user.attributes_with_association_names } render json: list, status: :ok return diff --git a/app/models/application_model.rb b/app/models/application_model.rb index d7f0bdafd..71a12a3fe 100644 --- a/app/models/application_model.rb +++ b/app/models/application_model.rb @@ -1,1648 +1,22 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ class ApplicationModel < ActiveRecord::Base - include ApplicationModel::Assets - include ApplicationModel::HistoryLogBase - include ApplicationModel::ActivityStreamBase - include ApplicationModel::SearchIndexBase + include ApplicationModel::ActivityStreamLoggable + include ApplicationModel::Cacheable + include ApplicationModel::CanLookup + include ApplicationModel::CanLookupSearchIndexAttributes + include ApplicationModel::ChecksAttributeLength + include ApplicationModel::CleansParam + include ApplicationModel::CleansRecentView + include ApplicationModel::FillsByUserColumns + include ApplicationModel::HandlesCreatesAndUpdates + include ApplicationModel::HasAssets + include ApplicationModel::HasAssociations + include ApplicationModel::HasAttachments + include ApplicationModel::HasLatestChangeTimestamp + include ApplicationModel::Importable + include ApplicationModel::HistoryLoggable + include ApplicationModel::TouchesReferences self.abstract_class = true - - before_create :check_attributes_protected, :check_limits, :cache_delete, :fill_up_user_create - before_update :check_limits, :fill_up_user_update - before_destroy :destroy_dependencies - - after_create :cache_delete - after_update :cache_delete - after_touch :cache_delete - after_destroy :cache_delete - - after_create :attachments_buffer_check - after_update :attachments_buffer_check - - after_create :activity_stream_create - after_update :activity_stream_update - before_destroy :activity_stream_destroy - - after_create :history_create - after_update :history_update - after_destroy :history_destroy - - after_create :search_index_update - after_update :search_index_update - after_touch :search_index_update - after_destroy :search_index_destroy - - before_destroy :recent_view_destroy - - # create instance accessor - class << self - attr_accessor :activity_stream_support_config, :history_support_config, :search_index_support_config, :attributes_with_associations_support_config - end - - attr_accessor :history_changes_last_done - - def check_attributes_protected - - import_class_list = ['Ticket', 'Ticket::Article', 'History', 'Ticket::State', 'Ticket::StateType', 'Ticket::Priority', 'Group', 'User', 'Role' ] - - # do noting, use id as it is - return if !Setting.get('system_init_done') - return if Setting.get('import_mode') && import_class_list.include?(self.class.to_s) - - self[:id] = nil - end - -=begin - -remove all not used model attributes of params - - result = Model.param_cleanup(params) - - for object creation, ignore id's - - result = Model.param_cleanup(params, true) - -returns - - result = params # params with valid attributes of model - -=end - - def self.param_cleanup(params, new_object = false) - - if params.respond_to?('permit!') - params.permit! - end - - if params.nil? - raise ArgumentError, "No params for #{self}!" - end - - data = {} - params.each { |key, value| - data[key.to_sym] = value - } - - # ignore id for new objects - if new_object && params[:id] - data.delete(:id) - end - - # only use object attributes - clean_params = {} - new.attributes.each { |attribute, _value| - next if !data.key?(attribute.to_sym) - - # check reference records, referenced by _id attributes - reflect_on_all_associations.map { |assoc| - class_name = assoc.options[:class_name] - next if !class_name - name = "#{assoc.name}_id".to_sym - next if !data.key?(name) - next if data[name].blank? - next if assoc.klass.lookup(id: data[name]) - raise ArgumentError, "Invalid value for param '#{name}': #{data[name].inspect}" - } - clean_params[attribute.to_sym] = data[attribute.to_sym] - } - - # we do want to set this via database - param_validation(clean_params) - end - -=begin - -set relations of model based on params - - model = Model.find(1) - result = model.param_set_associations(params) - -returns - - result = true|false - -=end - - def param_set_associations(params) - - # set relations by id/verify if ref exists - self.class.reflect_on_all_associations.map { |assoc| - real_ids = assoc.name.to_s[0, assoc.name.to_s.length - 1] + '_ids' - real_ids = real_ids.to_sym - next if !params.key?(real_ids) - list_of_items = params[real_ids] - if params[real_ids].class != Array - list_of_items = [ params[real_ids] ] - end - list = [] - list_of_items.each { |item_id| - next if !item_id - lookup = assoc.klass.lookup(id: item_id) - - # complain if we found no reference - if !lookup - raise ArgumentError, "No value found for '#{assoc.name}' with id #{item_id.inspect}" - end - list.push item_id - } - #p "SEND #{real_ids} = #{list.inspect}" - send("#{real_ids}=", list) - } - - # set relations by name/lookup - self.class.reflect_on_all_associations.map { |assoc| - real_ids = assoc.name.to_s[0, assoc.name.to_s.length - 1] + '_ids' - next if !respond_to?(real_ids) - real_values = assoc.name.to_s[0, assoc.name.to_s.length - 1] + 's' - real_values = real_values.to_sym - next if !respond_to?(real_values) - next if !params[real_values] - next if params[real_values].class != Array - list = [] - class_object = assoc.klass - params[real_values].each { |value| - lookup = nil - if class_object == User - if !lookup - lookup = class_object.lookup(login: value) - end - if !lookup - lookup = class_object.lookup(email: value) - end - else - lookup = class_object.lookup(name: value) - end - - # complain if we found no reference - if !lookup - raise ArgumentError, "No lookup value found for '#{assoc.name}': #{value.inspect}" - end - list.push lookup.id - } - #p "SEND #{real_ids} = #{list.inspect}" - send("#{real_ids}=", list) - } - end - -=begin - -get relations of model based on params - - model = Model.find(1) - attributes = model.attributes_with_associations - -returns - - hash with attributes and association ids - -=end - - def attributes_with_associations - - key = "#{self.class}::aws::#{id}" - cache = Cache.get(key) - return cache if cache - - # get relations - attributes = self.attributes - self.class.reflect_on_all_associations.map { |assoc| - real_ids = assoc.name.to_s[0, assoc.name.to_s.length - 1] + '_ids' - next if self.class.attributes_with_associations_support_config && self.class.attributes_with_associations_support_config[:ignore][real_ids.to_sym] == true - next if !respond_to?(real_ids) - attributes[real_ids] = send(real_ids) - } - Cache.write(key, attributes) - attributes - end - -=begin - -get relation name of model based on params - - model = Model.find(1) - attributes = model.attributes_with_relation_names - -returns - - hash with attributes, association ids, association names and relation name - -=end - - def attributes_with_relation_names - - # get relations - attributes = attributes_with_associations - self.class.reflect_on_all_associations.map { |assoc| - next if !respond_to?(assoc.name) - ref = send(assoc.name) - next if !ref - if ref.respond_to?(:first) - attributes[assoc.name.to_s] = [] - ref.each { |item| - if item[:login] - attributes[assoc.name.to_s].push item[:login] - next - end - next if !item[:name] - attributes[assoc.name.to_s].push item[:name] - } - if ref.count.positive? && attributes[assoc.name.to_s].empty? - attributes.delete(assoc.name.to_s) - end - next - end - if ref[:login] - attributes[assoc.name.to_s] = ref[:login] - next - end - next if !ref[:name] - attributes[assoc.name.to_s] = ref[:name] - } - - # fill created_by/updated_by - { - 'created_by_id' => 'created_by', - 'updated_by_id' => 'updated_by', - }.each { |source, destination| - next if !attributes[source] - user = User.lookup(id: attributes[source]) - next if !user - attributes[destination] = user.login - } - - # remove forbitten attributes - %w(password token tokens token_ids).each { |item| - attributes.delete(item) - } - - attributes - end - -=begin - -remove all not used params of object (per default :updated_at, :created_at, :updated_by_id and :created_by_id) - - result = Model.param_validation(params) - -returns - - result = params # params without listed attributes - -=end - - def self.param_validation(data) - - # we do want to set this via database - [:action, :controller, :updated_at, :created_at, :updated_by_id, :created_by_id, :updated_by, :created_by].each { |key| - data.delete(key) - } - - data - end - -=begin - -do name/login/email based lookup for associations - - params = { - login: 'some login', - firstname: 'some firstname', - lastname: 'some lastname', - email: 'some email', - organization: 'some organization', - roles: ['Agent', 'Admin'], - } - - attributes = Model.param_association_lookup(params) - -returns - - attributes = params # params with possible lookups - - attributes = { - login: 'some login', - firstname: 'some firstname', - lastname: 'some lastname', - email: 'some email', - organization_id: 123, - role_ids: [2,1], - } - -=end - - def self.param_association_lookup(params) - - data = {} - params.each { |key, value| - data[key.to_sym] = value - } - - data.symbolize_keys! - available_attributes = attribute_names - reflect_on_all_associations.map { |assoc| - value = data[assoc.name.to_sym] - next if !value # next if we do not have a value - ref_name = "#{assoc.name}_id" - - # handle _id values - if available_attributes.include?(ref_name) # if we do have an _id attribute - next if data[ref_name.to_sym] # next if we have already the _id filled - - # get association class and do lookup - class_object = assoc.klass - lookup = nil - if class_object == User - if value.class == String - if !lookup - lookup = class_object.lookup(login: value) - end - if !lookup - lookup = class_object.lookup(email: value) - end - else - raise ArgumentError, "String is needed as ref value #{value.inspect} for '#{assoc.name}'" - end - else - lookup = class_object.lookup(name: value) - end - - # complain if we found no reference - if !lookup - raise ArgumentError, "No lookup value found for '#{assoc.name}': #{value.inspect}" - end - - # release data value - data.delete(assoc.name.to_sym) - - # remember id reference - data[ref_name.to_sym] = lookup.id - next - end - - next if value.class != Array - next if value.empty? - next if value[0].class != String - - # handle _ids values - ref_names = "#{assoc.name[0, assoc.name.length - 1]}_ids" - generic_object_tmp = new - next unless generic_object_tmp.respond_to?(ref_names) # if we do have an _ids attribute - next if data[ref_names.to_sym] # next if we have already the _ids filled - - # get association class and do lookup - class_object = assoc.klass - lookup_ids = [] - value.each { |item| - lookup = nil - if class_object == User - if item.class == String - if !lookup - lookup = class_object.lookup(login: item) - end - if !lookup - lookup = class_object.lookup(email: item) - end - else - raise ArgumentError, "String is needed in array ref as ref value #{value.inspect} for '#{assoc.name}'" - end - else - lookup = class_object.lookup(name: item) - end - - # complain if we found no reference - if !lookup - raise ArgumentError, "No lookup value found for '#{assoc.name}': #{item.inspect}" - end - lookup_ids.push lookup.id - } - - # release data value - data.delete(assoc.name.to_sym) - - # remember id reference - data[ref_names.to_sym] = lookup_ids - } - - data - end - -=begin - -reference if association id check - - model = Model.find(123) - attributes = model.association_id_check('attribute_id', value) - -returns - - true | false - -=end - - def association_id_check(attribute_id, value) - return true if value.nil? - - attributes.each { |key, _value| - next if key != attribute_id - - # check if id is assigned - key_short = key[ key.length - 3, key.length ] - next if key_short != '_id' - key_short = key[ 0, key.length - 3 ] - - self.class.reflect_on_all_associations.map { |assoc| - next if assoc.name.to_s != key_short - item = assoc.class_name.constantize - return false if !item.respond_to?(:find_by) - ref_object = item.find_by(id: value) - return false if !ref_object - return true - } - } - true - end - -=begin - -set created_by_id & updated_by_id if not given based on UserInfo (current session) - -Used as before_create callback, no own use needed - - result = Model.fill_up_user_create(params) - -returns - - result = params # params with updated_by_id & created_by_id if not given based on UserInfo (current session) - -=end - - def fill_up_user_create - if self.class.column_names.include? 'updated_by_id' - if UserInfo.current_user_id - if updated_by_id && updated_by_id != UserInfo.current_user_id - logger.info "NOTICE create - self.updated_by_id is different: #{updated_by_id}/#{UserInfo.current_user_id}" - end - self.updated_by_id = UserInfo.current_user_id - end - end - - return if !self.class.column_names.include? 'created_by_id' - - return if !UserInfo.current_user_id - - if created_by_id && created_by_id != UserInfo.current_user_id - logger.info "NOTICE create - self.created_by_id is different: #{created_by_id}/#{UserInfo.current_user_id}" - end - self.created_by_id = UserInfo.current_user_id - end - -=begin - -set updated_by_id if not given based on UserInfo (current session) - -Used as before_update callback, no own use needed - - result = Model.fill_up_user_update(params) - -returns - - result = params # params with updated_by_id & created_by_id if not given based on UserInfo (current session) - -=end - - def fill_up_user_update - return if !self.class.column_names.include? 'updated_by_id' - return if !UserInfo.current_user_id - - self.updated_by_id = UserInfo.current_user_id - end - - def cache_update(o) - cache_delete if respond_to?('cache_delete') - o.cache_delete if o.respond_to?('cache_delete') - end - - def cache_delete - keys = [] - - # delete by id caches - keys.push "#{self.class}::#{id}" - - # delete by id with attributes_with_associations caches - keys.push "#{self.class}::aws::#{id}" - - # delete by name caches - if self[:name] - keys.push "#{self.class}::#{name}" - end - - # delete by login caches - if self[:login] - keys.push "#{self.class}::#{login}" - end - - keys.each { |key| - Cache.delete(key) - } - - # delete old name / login caches - if changed? - if changes.key?('name') - name = changes['name'][0] - key = "#{self.class}::#{name}" - Cache.delete(key) - end - if changes.key?('login') - name = changes['login'][0] - key = "#{self.class}::#{name}" - Cache.delete(key) - end - end - - end - - def self.cache_set(data_id, data) - key = "#{self}::#{data_id}" - Cache.write(key, data) - end - - def self.cache_get(data_id) - key = "#{self}::#{data_id}" - Cache.get(key) - end - -=begin - -generate uniq name (will check name of model and generates _1 sequenze) - -Used as before_update callback, no own use needed - - name = Model.genrate_uniq_name('some name') - -returns - - result = 'some name_X' - -=end - - def self.genrate_uniq_name(name) - return name if !find_by(name: name) - (1..100).each { |counter| - name = "#{name}_#{counter}" - exists = find_by(name: name) - next if exists - break - } - name - end - -=begin - -lookup model from cache (if exists) or retrieve it from db, id, name, login or email possible - - result = Model.lookup(id: 123) - result = Model.lookup(name: 'some name') - result = Model.lookup(login: 'some login') - result = Model.lookup(email: 'some login') - -returns - - result = model # with all attributes - -=end - - def self.lookup(data) - if data[:id] - cache = cache_get(data[:id]) - return cache if cache - - record = find_by(id: data[:id]) - cache_set(data[:id], record) - return record - elsif data[:name] - cache = cache_get(data[:name]) - return cache if cache - - # do lookup with == to handle case insensitive databases - records = if Rails.application.config.db_case_sensitive - where('LOWER(name) = LOWER(?)', data[:name]) - else - where(name: data[:name]) - end - records.each { |loop_record| - if loop_record.name == data[:name] - cache_set(data[:name], loop_record) - return loop_record - end - } - return - elsif data[:login] - cache = cache_get(data[:login]) - return cache if cache - - # do lookup with == to handle case insensitive databases - records = if Rails.application.config.db_case_sensitive - where('LOWER(login) = LOWER(?)', data[:login]) - else - where(login: data[:login]) - end - records.each { |loop_record| - if loop_record.login == data[:login] - cache_set(data[:login], loop_record) - return loop_record - end - } - return - elsif data[:email] - cache = cache_get(data[:email]) - return cache if cache - - # do lookup with == to handle case insensitive databases - records = if Rails.application.config.db_case_sensitive - where('LOWER(email) = LOWER(?)', data[:email]) - else - where(email: data[:email]) - end - records.each { |loop_record| - if loop_record.email == data[:email] - cache_set(data[:email], loop_record) - return loop_record - end - } - return - end - - raise ArgumentError, 'Need name, id, login or email for lookup()' - end - -=begin - -create model if not exists (check exists based on id, name, login, email or locale) - - result = Model.create_if_not_exists(attributes) - -returns - - result = model # with all attributes - -=end - - def self.create_if_not_exists(data) - if data[:id] - record = find_by(id: data[:id]) - return record if record - elsif data[:name] - - # do lookup with == to handle case insensitive databases - records = if Rails.application.config.db_case_sensitive - where('LOWER(name) = LOWER(?)', data[:name]) - else - where(name: data[:name]) - end - records.each { |loop_record| - return loop_record if loop_record.name == data[:name] - } - elsif data[:login] - - # do lookup with == to handle case insensitive databases - records = if Rails.application.config.db_case_sensitive - where('LOWER(login) = LOWER(?)', data[:login]) - else - where(login: data[:login]) - end - records.each { |loop_record| - return loop_record if loop_record.login == data[:login] - } - elsif data[:email] - - # do lookup with == to handle case insensitive databases - records = if Rails.application.config.db_case_sensitive - where('LOWER(email) = LOWER(?)', data[:email]) - else - where(email: data[:email]) - end - records.each { |loop_record| - return loop_record if loop_record.email == data[:email] - } - elsif data[:locale] && data[:source] - - # do lookup with == to handle case insensitive databases - records = if Rails.application.config.db_case_sensitive - where('LOWER(locale) = LOWER(?) AND LOWER(source) = LOWER(?)', data[:locale], data[:source]) - else - where(locale: data[:locale], source: data[:source]) - end - records.each { |loop_record| - return loop_record if loop_record.source == data[:source] - } - end - create(data) - end - -=begin - -Model.create_if_not_exists with ref lookups - - result = Model.create_if_not_exists_with_ref(attributes) - -returns - - result = model # with all attributes - -=end - - def self.create_if_not_exists_with_ref(data) - data = param_association_lookup(data) - create_or_update(data) - end - -=begin - -create or update model (check exists based on id, name, login, email or locale) - - result = Model.create_or_update(attributes) - -returns - - result = model # with all attributes - -=end - - def self.create_or_update(data) - if data[:id] - record = find_by(id: data[:id]) - if record - record.update_attributes(data) - return record - end - record = new(data) - record.save - return record - elsif data[:name] - - # do lookup with == to handle case insensitive databases - records = if Rails.application.config.db_case_sensitive - where('LOWER(name) = LOWER(?)', data[:name]) - else - where(name: data[:name]) - end - records.each { |loop_record| - if loop_record.name == data[:name] - loop_record.update_attributes(data) - return loop_record - end - } - record = new(data) - record.save - return record - elsif data[:login] - - # do lookup with == to handle case insensitive databases - records = if Rails.application.config.db_case_sensitive - where('LOWER(login) = LOWER(?)', data[:login]) - else - where(login: data[:login]) - end - records.each { |loop_record| - if loop_record.login.casecmp(data[:login]).zero? - loop_record.update_attributes(data) - return loop_record - end - } - record = new(data) - record.save - return record - elsif data[:email] - - # do lookup with == to handle case insensitive databases - records = if Rails.application.config.db_case_sensitive - where('LOWER(email) = LOWER(?)', data[:email]) - else - where(email: data[:email]) - end - records.each { |loop_record| - if loop_record.email.casecmp(data[:email]).zero? - loop_record.update_attributes(data) - return loop_record - end - } - record = new(data) - record.save - return record - elsif data[:locale] - - # do lookup with == to handle case insensitive databases - records = if Rails.application.config.db_case_sensitive - where('LOWER(locale) = LOWER(?)', data[:locale]) - else - where(locale: data[:locale]) - end - records.each { |loop_record| - if loop_record.locale.casecmp(data[:locale]).zero? - loop_record.update_attributes(data) - return loop_record - end - } - record = new(data) - record.save - return record - else - raise ArgumentError, 'Need name, login, email or locale for create_or_update()' - end - end - -=begin - -Model.create_or_update with ref lookups - - result = Model.create_or_update(attributes) - -returns - - result = model # with all attributes - -=end - - def self.create_or_update_with_ref(data) - data = param_association_lookup(data) - create_or_update(data) - end - -=begin - -activate latest change on create, update, touch and destroy - -class Model < ApplicationModel - latest_change_support -end - -=end - - def self.latest_change_support - after_create :latest_change_set_from_observer - after_update :latest_change_set_from_observer - after_touch :latest_change_set_from_observer - after_destroy :latest_change_set_from_observer_destroy - end - - def latest_change_set_from_observer - self.class.latest_change_set(updated_at) - end - - def latest_change_set_from_observer_destroy - self.class.latest_change_set(nil) - end - - def self.latest_change_set(updated_at) - key = "#{new.class.name}_latest_change" - expires_in = 31_536_000 # 1 year - - if updated_at.nil? - Cache.delete(key) - else - Cache.write(key, updated_at, { expires_in: expires_in }) - end - end - -=begin - - get latest updated_at object timestamp - - latest_change = Ticket.latest_change - -returns - - result = timestamp - -=end - - def self.latest_change - key = "#{new.class.name}_latest_change" - updated_at = Cache.get(key) - - # if we do not have it cached, do lookup - if !updated_at - o = select(:updated_at).order(updated_at: :desc).limit(1).first - if o - updated_at = o.updated_at - latest_change_set(updated_at) - end - end - updated_at - end - -=begin - -activate client notify support on create, update, touch and destroy - -class Model < ApplicationModel - notify_clients_support -end - -=end - - def self.notify_clients_support - after_create :notify_clients_after_create - after_update :notify_clients_after_update - after_touch :notify_clients_after_touch - after_destroy :notify_clients_after_destroy - end - -=begin - -notify_clients_after_create after model got created - -used as callback in model file - -class OwnModel < ApplicationModel - after_create :notify_clients_after_create - after_update :notify_clients_after_update - after_touch :notify_clients_after_touch - after_destroy :notify_clients_after_destroy - - [...] - -=end - - def notify_clients_after_create - - # return if we run import mode - return if Setting.get('import_mode') - logger.debug "#{self.class.name}.find(#{id}) notify created " + created_at.to_s - class_name = self.class.name - class_name.gsub!(/::/, '') - PushMessages.send( - message: { - event: class_name + ':create', - data: { id: id, updated_at: updated_at } - }, - type: 'authenticated', - ) - end - -=begin - -notify_clients_after_update after model got updated - -used as callback in model file - -class OwnModel < ApplicationModel - after_create :notify_clients_after_create - after_update :notify_clients_after_update - after_touch :notify_clients_after_touch - after_destroy :notify_clients_after_destroy - - [...] - -=end - - def notify_clients_after_update - - # return if we run import mode - return if Setting.get('import_mode') - logger.debug "#{self.class.name}.find(#{id}) notify UPDATED " + updated_at.to_s - class_name = self.class.name - class_name.gsub!(/::/, '') - PushMessages.send( - message: { - event: class_name + ':update', - data: { id: id, updated_at: updated_at } - }, - type: 'authenticated', - ) - end - -=begin - -notify_clients_after_touch after model got touched - -used as callback in model file - -class OwnModel < ApplicationModel - after_create :notify_clients_after_create - after_update :notify_clients_after_update - after_touch :notify_clients_after_touch - after_destroy :notify_clients_after_destroy - - [...] - -=end - - def notify_clients_after_touch - - # return if we run import mode - return if Setting.get('import_mode') - logger.debug "#{self.class.name}.find(#{id}) notify TOUCH " + updated_at.to_s - class_name = self.class.name - class_name.gsub!(/::/, '') - PushMessages.send( - message: { - event: class_name + ':touch', - data: { id: id, updated_at: updated_at } - }, - type: 'authenticated', - ) - end - -=begin - -notify_clients_after_destroy after model got destroyed - -used as callback in model file - -class OwnModel < ApplicationModel - after_create :notify_clients_after_create - after_update :notify_clients_after_update - after_touch :notify_clients_after_touch - after_destroy :notify_clients_after_destroy - - [...] - -=end - def notify_clients_after_destroy - - # return if we run import mode - return if Setting.get('import_mode') - logger.debug "#{self.class.name}.find(#{id}) notify DESTOY " + updated_at.to_s - class_name = self.class.name - class_name.gsub!(/::/, '') - PushMessages.send( - message: { - event: class_name + ':destroy', - data: { id: id, updated_at: updated_at } - }, - type: 'authenticated', - ) - end - -=begin - -serve methode to configure and enable search index support for this model - -class Model < ApplicationModel - search_index_support - ignore_attributes: { - create_article_type_id: true, - create_article_sender_id: true, - article_count: true, - }, - ignore_ids: [1,2,4] - -end - -=end - - def self.search_index_support(data = {}) - @search_index_support_config = data - end - -=begin - -update search index, if configured - will be executed automatically - - model = Model.find(123) - model.search_index_update - -=end - - def search_index_update - config = self.class.search_index_support_config - return if !config - return if config[:ignore_ids] && config[:ignore_ids].include?(id) - - # start background job to transfer data to search index - return if !SearchIndexBackend.enabled? - Delayed::Job.enqueue(ApplicationModel::BackgroundJobSearchIndex.new(self.class.to_s, id)) - end - -=begin - -delete search index object, will be executed automatically - - model = Model.find(123) - model.search_index_destroy - -=end - - def search_index_destroy - config = self.class.search_index_support_config - return if !config - return if config[:ignore_ids] && config[:ignore_ids].include?(id) - - SearchIndexBackend.remove(self.class.to_s, id) - end - -=begin - -reload search index with full data - - Model.search_index_reload - -=end - - def self.search_index_reload - config = @search_index_support_config - return if !config - tolerance = 5 - tolerance_count = 0 - all_ids = select('id').all.order('created_at DESC') - all_ids.each { |item_with_id| - next if config[:ignore_ids] && config[:ignore_ids].include?(item_with_id.id) - item = find(item_with_id.id) - begin - item.search_index_update_backend - rescue => e - logger.error "Unable to send #{item.class}.find(#{item.id}) backend: #{e.inspect}" - tolerance_count += 1 - raise "Unable to send #{item.class}.find(#{item.id}) backend: #{e.inspect}" if tolerance_count == tolerance - end - } - end - -=begin - -serve methode to configure and enable activity stream support for this model - -class Model < ApplicationModel - activity_stream_support permission: 'admin.user' -end - -=end - - def self.activity_stream_support(data = {}) - @activity_stream_support_config = data - end - -=begin - -log object create activity stream, if configured - will be executed automatically - - model = Model.find(123) - model.activity_stream_create - -=end - - def activity_stream_create - return if !self.class.activity_stream_support_config - activity_stream_log('create', self['created_by_id']) - end - -=begin - -log object update activity stream, if configured - will be executed automatically - - model = Model.find(123) - model.activity_stream_update - -=end - - def activity_stream_update - return if !self.class.activity_stream_support_config - - return if !changed? - - # default ignored attributes - ignore_attributes = { - created_at: true, - updated_at: true, - created_by_id: true, - updated_by_id: true, - } - if self.class.activity_stream_support_config[:ignore_attributes] - self.class.activity_stream_support_config[:ignore_attributes].each { |key, value| - ignore_attributes[key] = value - } - end - - log = false - changes.each { |key, _value| - - # do not log created_at and updated_at attributes - next if ignore_attributes[key.to_sym] == true - - log = true - } - - return if !log - - activity_stream_log('update', self['updated_by_id']) - end - -=begin - -delete object activity stream, will be executed automatically - - model = Model.find(123) - model.activity_stream_destroy - -=end - - def activity_stream_destroy - return if !self.class.activity_stream_support_config - ActivityStream.remove(self.class.to_s, id) - end - -=begin - -serve methode to configure and enable history support for this model - -class Model < ApplicationModel - history_support -end - -class Model < ApplicationModel - history_support ignore_attributes: { article_count: true } -end - -=end - - def self.history_support(data = {}) - @history_support_config = data - end - -=begin - -log object create history, if configured - will be executed automatically - - model = Model.find(123) - model.history_create - -=end - - def history_create - return if !self.class.history_support_config - #logger.debug 'create ' + self.changes.inspect - history_log('created', created_by_id) - - end - -=begin - -log object update history with all updated attributes, if configured - will be executed automatically - - model = Model.find(123) - model.history_update - -=end - - def history_update - return if !self.class.history_support_config - - return if !changed? - - # return if it's no update - return if new_record? - - # new record also triggers update, so ignore new records - changes = self.changes - if history_changes_last_done - history_changes_last_done.each { |key, value| - if changes.key?(key) && changes[key] == value - changes.delete(key) - end - } - end - self.history_changes_last_done = changes - #logger.info 'updated ' + self.changes.inspect - - return if changes['id'] && !changes['id'][0] - - # default ignored attributes - ignore_attributes = { - created_at: true, - updated_at: true, - created_by_id: true, - updated_by_id: true, - } - if self.class.history_support_config[:ignore_attributes] - self.class.history_support_config[:ignore_attributes].each { |key, value| - ignore_attributes[key] = value - } - end - - changes.each { |key, value| - - # do not log created_at and updated_at attributes - next if ignore_attributes[key.to_sym] == true - - # get attribute name - attribute_name = key.to_s - if attribute_name[-3, 3] == '_id' - attribute_name = attribute_name[ 0, attribute_name.length - 3 ] - end - - value_id = [] - value_str = [ value[0], value[1] ] - if key.to_s[-3, 3] == '_id' - value_id[0] = value[0] - value_id[1] = value[1] - - if respond_to?(attribute_name) && send(attribute_name) - relation_class = send(attribute_name).class - if relation_class && value_id[0] - relation_model = relation_class.lookup(id: value_id[0]) - if relation_model - if relation_model['name'] - value_str[0] = relation_model['name'] - elsif relation_model.respond_to?('fullname') - value_str[0] = relation_model.send('fullname') - end - end - end - if relation_class && value_id[1] - relation_model = relation_class.lookup(id: value_id[1]) - if relation_model - if relation_model['name'] - value_str[1] = relation_model['name'] - elsif relation_model.respond_to?('fullname') - value_str[1] = relation_model.send('fullname') - end - end - end - end - end - data = { - history_attribute: attribute_name, - value_from: value_str[0].to_s, - value_to: value_str[1].to_s, - id_from: value_id[0], - id_to: value_id[1], - } - #logger.info "HIST NEW #{self.class.to_s}.find(#{self.id}) #{data.inspect}" - history_log('updated', updated_by_id, data) - } - end - -=begin - -delete object history, will be executed automatically - - model = Model.find(123) - model.history_destroy - -=end - - def history_destroy - return if !self.class.history_support_config - History.remove(self.class.to_s, id) - end - -=begin - -serve methode to configure and attributes_with_associations support for this model - -class Model < ApplicationModel - attributes_with_associations(ignore: { user_ids: => true }) -end - -=end - - def self.attributes_with_associations_support(data = {}) - @attributes_with_associations_support_config = data - end - -=begin - -get list of attachments of this object - - item = Model.find(123) - list = item.attachments - -returns - - # array with Store model objects - -=end - - def attachments - Store.list(object: self.class.to_s, o_id: id) - end - -=begin - -store attachments for this object - - item = Model.find(123) - item.attachments = [ Store-Object1, Store-Object2 ] - -=end - - def attachments=(attachments) - self.attachments_buffer = attachments - - # update if object already exists - return if !(id && id.nonzero?) - - attachments_buffer_check - end - -=begin - -return object and assets - - data = Model.full(123) - data = { - id: 123, - assets: assets, - } - -=end - - def self.full(id) - object = find(id) - assets = object.assets({}) - { - id: id, - assets: assets, - } - end - -=begin - -get assets of object list - - list = [ - { - object => 'Ticket', - o_id => 1, - }, - { - object => 'User', - o_id => 121, - }, - ] - - assets = Model.assets_of_object_list(list, assets) - -=end - - def self.assets_of_object_list(list, assets = {}) - list.each { |item| - require item['object'].to_filename - record = Kernel.const_get(item['object']).find(item['o_id']) - assets = record.assets(assets) - if item['created_by_id'] - user = User.find(item['created_by_id']) - assets = user.assets(assets) - end - if item['updated_by_id'] - user = User.find(item['updated_by_id']) - assets = user.assets(assets) - end - } - assets - end - -=begin - -get assets and record_ids of selector - - model = Model.find(123) - - assets = model.assets_of_selector('attribute_name_of_selector', assets) - -=end - - def assets_of_selector(selector, assets = {}) - - # get assets of condition - models = Models.all - send(selector).each { |item, content| - attribute = item.split(/\./) - next if !attribute[1] - begin - attribute_class = attribute[0].to_classname.constantize - rescue => e - logger.error "Unable to get asset for '#{attribute[0]}': #{e.inspect}" - next - end - reflection = attribute[1].sub(/_id$/, '') - #reflection = reflection.to_sym - next if !models[attribute_class] - next if !models[attribute_class][:reflections] - next if !models[attribute_class][:reflections][reflection] - next if !models[attribute_class][:reflections][reflection].klass - attribute_ref_class = models[attribute_class][:reflections][reflection].klass - if content['value'].class == Array - content['value'].each { |item_id| - attribute_object = attribute_ref_class.find_by(id: item_id) - if attribute_object - assets = attribute_object.assets(assets) - end - } - else - attribute_object = attribute_ref_class.find_by(id: content['value']) - if attribute_object - assets = attribute_object.assets(assets) - end - end - } - assets - end - -=begin - -touch references by params - - Model.touch_reference_by_params( - object: 'Ticket', - o_id: 123, - ) - -=end - - def self.touch_reference_by_params(data) - - object_class = Kernel.const_get(data[:object]) - object = object_class.lookup(id: data[:o_id]) - return if !object - object.touch - rescue => e - logger.error e.message - logger.error e.backtrace.inspect - - end - - private - - def attachments_buffer - @attachments_buffer_data - end - - def attachments_buffer=(attachments) - @attachments_buffer_data = attachments - end - - def attachments_buffer_check - - # do nothing if no attachment exists - return 1 if attachments_buffer.nil? - - # store attachments - article_store = [] - attachments_buffer.each do |attachment| - article_store.push Store.add( - object: self.class.to_s, - o_id: id, - data: attachment.content, - filename: attachment.filename, - preferences: attachment.preferences, - created_by_id: created_by_id, - ) - end - attachments_buffer = nil - end - -=begin - -delete object recent viewed list, will be executed automatically - - model = Model.find(123) - model.recent_view_destroy - -=end - - def recent_view_destroy - RecentView.log_destroy(self.class.to_s, id) - end - -=begin - -check string/varchar size and cut them if needed - -=end - - def check_limits - attributes.each { |attribute| - next if !self[ attribute[0] ] - next if self[ attribute[0] ].class != String - next if self[ attribute[0] ].empty? - column = self.class.columns_hash[ attribute[0] ] - next if !column - limit = column.limit - if column && limit - current_length = attribute[1].to_s.length - if limit < current_length - logger.warn "WARNING: cut string because of database length #{self.class}.#{attribute[0]}(#{limit} but is #{current_length}:#{attribute[1]})" - self[ attribute[0] ] = attribute[1][ 0, limit ] - end - end - - # strip 4 bytes utf8 chars if needed - if column && self[ attribute[0] ] - self[attribute[0]] = self[ attribute[0] ].utf8_to_3bytesutf8 - end - } - end - -=begin - -destroy object dependencies, will be executed automatically - -=end - - def destroy_dependencies - end - end diff --git a/app/models/application_model/activity_stream_base.rb b/app/models/application_model/activity_stream_loggable.rb similarity index 51% rename from app/models/application_model/activity_stream_base.rb rename to app/models/application_model/activity_stream_loggable.rb index 61eedb2bc..8a884d23f 100644 --- a/app/models/application_model/activity_stream_base.rb +++ b/app/models/application_model/activity_stream_loggable.rb @@ -1,5 +1,6 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ -module ApplicationModel::ActivityStreamBase +module ApplicationModel::ActivityStreamLoggable + extend ActiveSupport::Concern =begin @@ -25,19 +26,31 @@ returns # return if we run on init mode return if !Setting.get('system_init_done') - permission = self.class.activity_stream_support_config[:permission] + permission = self.class.instance_variable_get(:@activity_stream_permission) updated_at = self.updated_at if force updated_at = Time.zone.now end - ActivityStream.add( - o_id: self['id'], - type: type, - object: self.class.name, - group_id: self['group_id'], - permission: permission, - created_at: updated_at, + + attributes = { + o_id: self['id'], + type: type, + object: self.class.name, + group_id: self['group_id'], + permission: permission, + created_at: updated_at, created_by_id: user_id, - ) + }.merge(activity_stream_log_attributes) + + ActivityStream.add(attributes) + end + + private + + # callback function to overwrite + # default history stream log attributes + # gets called from activity_stream_log + def activity_stream_log_attributes + {} end end diff --git a/app/models/application_model/assets.rb b/app/models/application_model/assets.rb deleted file mode 100644 index 9bb78fe71..000000000 --- a/app/models/application_model/assets.rb +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ -module ApplicationModel::Assets - -=begin - -get all assets / related models for this user - - user = User.find(123) - result = user.assets(assets_if_exists) - -returns - - result = { - :User => { - 123 => user_model_123, - 1234 => user_model_1234, - } - } - -=end - - def assets(data = {}) - - app_model = self.class.to_app_model - - if !data[ app_model ] - data[ app_model ] = {} - end - if !data[ app_model ][ id ] - data[ app_model ][ id ] = attributes_with_associations - end - - return data if !self['created_by_id'] && !self['updated_by_id'] - app_model_user = User.to_app_model - %w(created_by_id updated_by_id).each { |local_user_id| - next if !self[ local_user_id ] - next if data[ app_model_user ] && data[ app_model_user ][ self[ local_user_id ] ] - user = User.lookup(id: self[ local_user_id ]) - next if !user - data = user.assets(data) - } - data - end -end diff --git a/app/models/application_model/background_job_search_index.rb b/app/models/application_model/background_job_search_index.rb deleted file mode 100644 index 584438a3b..000000000 --- a/app/models/application_model/background_job_search_index.rb +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ -class ApplicationModel::BackgroundJobSearchIndex - def initialize(object, o_id) - @object = object - @o_id = o_id - end - - def perform - record = Object.const_get(@object).lookup(id: @o_id) - if !record - Rails.logger.info "Can't index #{@object}.find(#{@o_id}), no such record found" - return - end - record.search_index_update_backend - end -end diff --git a/app/models/application_model/cacheable.rb b/app/models/application_model/cacheable.rb new file mode 100644 index 000000000..a07b7decf --- /dev/null +++ b/app/models/application_model/cacheable.rb @@ -0,0 +1,70 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ +module ApplicationModel::Cacheable + extend ActiveSupport::Concern + + included do + before_create :cache_delete + + after_create :cache_delete + after_update :cache_delete + after_touch :cache_delete + after_destroy :cache_delete + end + + def cache_update(o) + cache_delete if respond_to?('cache_delete') + o.cache_delete if o.respond_to?('cache_delete') + end + + def cache_delete + keys = [] + + # delete by id caches + keys.push "#{self.class}::#{id}" + + # delete by id with attributes_with_association_ids caches + keys.push "#{self.class}::aws::#{id}" + + # delete by name caches + if self[:name] + keys.push "#{self.class}::#{name}" + end + + # delete by login caches + if self[:login] + keys.push "#{self.class}::#{login}" + end + + keys.each { |key| + Cache.delete(key) + } + + # delete old name / login caches + if changed? + if changes.key?('name') + name = changes['name'][0] + key = "#{self.class}::#{name}" + Cache.delete(key) + end + if changes.key?('login') + name = changes['login'][0] + key = "#{self.class}::#{name}" + Cache.delete(key) + end + end + end + + # methods defined here are going to extend the class, not the instance of it + class_methods do + + def cache_set(data_id, data) + key = "#{self}::#{data_id}" + Cache.write(key, data) + end + + def cache_get(data_id) + key = "#{self}::#{data_id}" + Cache.get(key) + end + end +end diff --git a/app/models/application_model/can_lookup.rb b/app/models/application_model/can_lookup.rb new file mode 100644 index 000000000..9fdb097cf --- /dev/null +++ b/app/models/application_model/can_lookup.rb @@ -0,0 +1,87 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ +module ApplicationModel::CanLookup + extend ActiveSupport::Concern + + # methods defined here are going to extend the class, not the instance of it + class_methods do + +=begin + +lookup model from cache (if exists) or retrieve it from db, id, name, login or email possible + + result = Model.lookup(id: 123) + result = Model.lookup(name: 'some name') + result = Model.lookup(login: 'some login') + result = Model.lookup(email: 'some login') + +returns + + result = model # with all attributes + +=end + + def lookup(data) + if data[:id] + cache = cache_get(data[:id]) + return cache if cache + + record = find_by(id: data[:id]) + cache_set(data[:id], record) + return record + elsif data[:name] + cache = cache_get(data[:name]) + return cache if cache + + # do lookup with == to handle case insensitive databases + records = if Rails.application.config.db_case_sensitive + where('LOWER(name) = LOWER(?)', data[:name]) + else + where(name: data[:name]) + end + records.each { |loop_record| + if loop_record.name == data[:name] + cache_set(data[:name], loop_record) + return loop_record + end + } + return + elsif data[:login] + cache = cache_get(data[:login]) + return cache if cache + + # do lookup with == to handle case insensitive databases + records = if Rails.application.config.db_case_sensitive + where('LOWER(login) = LOWER(?)', data[:login]) + else + where(login: data[:login]) + end + records.each { |loop_record| + if loop_record.login == data[:login] + cache_set(data[:login], loop_record) + return loop_record + end + } + return + elsif data[:email] + cache = cache_get(data[:email]) + return cache if cache + + # do lookup with == to handle case insensitive databases + records = if Rails.application.config.db_case_sensitive + where('LOWER(email) = LOWER(?)', data[:email]) + else + where(email: data[:email]) + end + records.each { |loop_record| + if loop_record.email == data[:email] + cache_set(data[:email], loop_record) + return loop_record + end + } + return + end + + raise ArgumentError, 'Need name, id, login or email for lookup()' + end + end +end diff --git a/app/models/application_model/can_lookup_search_index_attributes.rb b/app/models/application_model/can_lookup_search_index_attributes.rb new file mode 100644 index 000000000..d2d906f6e --- /dev/null +++ b/app/models/application_model/can_lookup_search_index_attributes.rb @@ -0,0 +1,66 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ +module ApplicationModel::CanLookupSearchIndexAttributes + extend ActiveSupport::Concern + +=begin + +lookup name of ref. objects + + ticket = Ticket.find(3) + attributes = ticket.search_index_attribute_lookup + +returns + + attributes # object with lookup data + +=end + + def search_index_attribute_lookup + + attributes = self.attributes + self.attributes.each { |key, value| + next if !value + + # get attribute name + attribute_name_with_id = key.to_s + attribute_name = key.to_s + next if attribute_name[-3, 3] != '_id' + attribute_name = attribute_name[ 0, attribute_name.length - 3 ] + + # check if attribute method exists + next if !respond_to?(attribute_name) + + # check if method has own class + relation_class = send(attribute_name).class + next if !relation_class + + # lookup ref object + relation_model = relation_class.lookup(id: value) + next if !relation_model + + # get name of ref object + value = nil + if relation_model.respond_to?('search_index_data') + value = relation_model.send('search_index_data') + end + + if relation_model.respond_to?('name') + value = relation_model.send('name') + end + + next if !value + + # save name of ref object + attributes[ attribute_name ] = value + } + + ignored_attributes = self.class.instance_variable_get(:@search_index_attributes_ignored) || [] + return attributes if ignored_attributes.empty? + + ignored_attributes.each { |attribute| + attributes.delete(attribute.to_s) + } + + attributes + end +end diff --git a/app/models/application_model/checks_attribute_length.rb b/app/models/application_model/checks_attribute_length.rb new file mode 100644 index 000000000..5e49f4fe7 --- /dev/null +++ b/app/models/application_model/checks_attribute_length.rb @@ -0,0 +1,38 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ +module ApplicationModel::ChecksAttributeLength + extend ActiveSupport::Concern + + included do + before_create :check_attribute_length + before_update :check_attribute_length + end + +=begin + +check string/varchar size and cut them if needed + +=end + + def check_attribute_length + attributes.each { |attribute| + next if !self[ attribute[0] ] + next if !self[ attribute[0] ].instance_of?(String) + next if self[ attribute[0] ].empty? + column = self.class.columns_hash[ attribute[0] ] + next if !column + limit = column.limit + if column && limit + current_length = attribute[1].to_s.length + if limit < current_length + logger.warn "WARNING: cut string because of database length #{self.class}.#{attribute[0]}(#{limit} but is #{current_length}:#{attribute[1]})" + self[ attribute[0] ] = attribute[1][ 0, limit ] + end + end + + # strip 4 bytes utf8 chars if needed + if column && self[ attribute[0] ] + self[attribute[0]] = self[ attribute[0] ].utf8_to_3bytesutf8 + end + } + end +end diff --git a/app/models/application_model/cleans_param.rb b/app/models/application_model/cleans_param.rb new file mode 100644 index 000000000..e0b092f38 --- /dev/null +++ b/app/models/application_model/cleans_param.rb @@ -0,0 +1,90 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ +module ApplicationModel::CleansParam + extend ActiveSupport::Concern + + # methods defined here are going to extend the class, not the instance of it + class_methods do + +=begin + +remove all not used model attributes of params + + result = Model.param_cleanup(params) + + for object creation, ignore id's + + result = Model.param_cleanup(params, true) + +returns + + result = params # params with valid attributes of model + +=end + + def param_cleanup(params, new_object = false) + + if params.respond_to?('permit!') + params.permit! + end + + if params.nil? + raise ArgumentError, "No params for #{self}!" + end + + data = {} + params.each { |key, value| + data[key.to_sym] = value + } + + # ignore id for new objects + if new_object && params[:id] + data.delete(:id) + end + + # only use object attributes + clean_params = {} + new.attributes.each { |attribute, _value| + next if !data.key?(attribute.to_sym) + + # check reference records, referenced by _id attributes + reflect_on_all_associations.map { |assoc| + class_name = assoc.options[:class_name] + next if !class_name + name = "#{assoc.name}_id".to_sym + next if !data.key?(name) + next if data[name].blank? + next if assoc.klass.lookup(id: data[name]) + raise ArgumentError, "Invalid value for param '#{name}': #{data[name].inspect}" + } + clean_params[attribute.to_sym] = data[attribute.to_sym] + } + + # we do want to set this via database + filter_unused_params(clean_params) + end + + private + +=begin + +remove all not used params of object (per default :updated_at, :created_at, :updated_by_id and :created_by_id) + + result = Model.filter_unused_params(params) + +returns + + result = params # params without listed attributes + +=end + + def filter_unused_params(data) + + # we do want to set this via database + [:action, :controller, :updated_at, :created_at, :updated_by_id, :created_by_id, :updated_by, :created_by].each { |key| + data.delete(key) + } + + data + end + end +end diff --git a/app/models/application_model/cleans_recent_view.rb b/app/models/application_model/cleans_recent_view.rb new file mode 100644 index 000000000..bd0c5e24a --- /dev/null +++ b/app/models/application_model/cleans_recent_view.rb @@ -0,0 +1,21 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ +module ApplicationModel::CleansRecentView + extend ActiveSupport::Concern + + included do + before_destroy :recent_view_destroy + end + +=begin + +delete object recent viewed list, will be executed automatically + + model = Model.find(123) + model.recent_view_destroy + +=end + + def recent_view_destroy + RecentView.log_destroy(self.class.to_s, id) + end +end diff --git a/app/models/application_model/fills_by_user_columns.rb b/app/models/application_model/fills_by_user_columns.rb new file mode 100644 index 000000000..df2075441 --- /dev/null +++ b/app/models/application_model/fills_by_user_columns.rb @@ -0,0 +1,64 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ +module ApplicationModel::FillsByUserColumns + extend ActiveSupport::Concern + + included do + before_create :fill_up_user_create + before_update :fill_up_user_update + end + +=begin + +set created_by_id & updated_by_id if not given based on UserInfo (current session) + +Used as before_create callback, no own use needed + + result = Model.fill_up_user_create(params) + +returns + + result = params # params with updated_by_id & created_by_id if not given based on UserInfo (current session) + +=end + + def fill_up_user_create + if self.class.column_names.include? 'updated_by_id' + if UserInfo.current_user_id + if updated_by_id && updated_by_id != UserInfo.current_user_id + logger.info "NOTICE create - self.updated_by_id is different: #{updated_by_id}/#{UserInfo.current_user_id}" + end + self.updated_by_id = UserInfo.current_user_id + end + end + + return if !self.class.column_names.include? 'created_by_id' + + return if !UserInfo.current_user_id + + if created_by_id && created_by_id != UserInfo.current_user_id + logger.info "NOTICE create - self.created_by_id is different: #{created_by_id}/#{UserInfo.current_user_id}" + end + self.created_by_id = UserInfo.current_user_id + end + +=begin + +set updated_by_id if not given based on UserInfo (current session) + +Used as before_update callback, no own use needed + + result = Model.fill_up_user_update(params) + +returns + + result = params # params with updated_by_id & created_by_id if not given based on UserInfo (current session) + +=end + + def fill_up_user_update + return if !self.class.column_names.include? 'updated_by_id' + return if !UserInfo.current_user_id + + self.updated_by_id = UserInfo.current_user_id + end +end diff --git a/app/models/application_model/handles_creates_and_updates.rb b/app/models/application_model/handles_creates_and_updates.rb new file mode 100644 index 000000000..ab368ecff --- /dev/null +++ b/app/models/application_model/handles_creates_and_updates.rb @@ -0,0 +1,201 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ +module ApplicationModel::HandlesCreatesAndUpdates + extend ActiveSupport::Concern + + # methods defined here are going to extend the class, not the instance of it + class_methods do + +=begin + +create model if not exists (check exists based on id, name, login, email or locale) + + result = Model.create_if_not_exists(attributes) + +returns + + result = model # with all attributes + +=end + + def create_if_not_exists(data) + if data[:id] + record = find_by(id: data[:id]) + return record if record + elsif data[:name] + + # do lookup with == to handle case insensitive databases + records = if Rails.application.config.db_case_sensitive + where('LOWER(name) = LOWER(?)', data[:name]) + else + where(name: data[:name]) + end + records.each { |loop_record| + return loop_record if loop_record.name == data[:name] + } + elsif data[:login] + + # do lookup with == to handle case insensitive databases + records = if Rails.application.config.db_case_sensitive + where('LOWER(login) = LOWER(?)', data[:login]) + else + where(login: data[:login]) + end + records.each { |loop_record| + return loop_record if loop_record.login == data[:login] + } + elsif data[:email] + + # do lookup with == to handle case insensitive databases + records = if Rails.application.config.db_case_sensitive + where('LOWER(email) = LOWER(?)', data[:email]) + else + where(email: data[:email]) + end + records.each { |loop_record| + return loop_record if loop_record.email == data[:email] + } + elsif data[:locale] && data[:source] + + # do lookup with == to handle case insensitive databases + records = if Rails.application.config.db_case_sensitive + where('LOWER(locale) = LOWER(?) AND LOWER(source) = LOWER(?)', data[:locale], data[:source]) + else + where(locale: data[:locale], source: data[:source]) + end + records.each { |loop_record| + return loop_record if loop_record.source == data[:source] + } + end + create(data) + end + +=begin + +Model.create_or_update with ref lookups + + result = Model.create_or_update(attributes) + +returns + + result = model # with all attributes + +=end + + def create_or_update_with_ref(data) + data = association_name_to_id_convert(data) + create_or_update(data) + end + +=begin + +Model.create_if_not_exists with ref lookups + + result = Model.create_if_not_exists_with_ref(attributes) + +returns + + result = model # with all attributes + +=end + + def create_if_not_exists_with_ref(data) + data = association_name_to_id_convert(data) + create_or_update(data) + end + +=begin + +create or update model (check exists based on id, name, login, email or locale) + + result = Model.create_or_update(attributes) + +returns + + result = model # with all attributes + +=end + + def create_or_update(data) + if data[:id] + record = find_by(id: data[:id]) + if record + record.update_attributes(data) + return record + end + record = new(data) + record.save + return record + elsif data[:name] + + # do lookup with == to handle case insensitive databases + records = if Rails.application.config.db_case_sensitive + where('LOWER(name) = LOWER(?)', data[:name]) + else + where(name: data[:name]) + end + records.each { |loop_record| + if loop_record.name == data[:name] + loop_record.update_attributes(data) + return loop_record + end + } + record = new(data) + record.save + return record + elsif data[:login] + + # do lookup with == to handle case insensitive databases + records = if Rails.application.config.db_case_sensitive + where('LOWER(login) = LOWER(?)', data[:login]) + else + where(login: data[:login]) + end + records.each { |loop_record| + if loop_record.login.casecmp(data[:login]).zero? + loop_record.update_attributes(data) + return loop_record + end + } + record = new(data) + record.save + return record + elsif data[:email] + + # do lookup with == to handle case insensitive databases + records = if Rails.application.config.db_case_sensitive + where('LOWER(email) = LOWER(?)', data[:email]) + else + where(email: data[:email]) + end + records.each { |loop_record| + if loop_record.email.casecmp(data[:email]).zero? + loop_record.update_attributes(data) + return loop_record + end + } + record = new(data) + record.save + return record + elsif data[:locale] + + # do lookup with == to handle case insensitive databases + records = if Rails.application.config.db_case_sensitive + where('LOWER(locale) = LOWER(?)', data[:locale]) + else + where(locale: data[:locale]) + end + records.each { |loop_record| + if loop_record.locale.casecmp(data[:locale]).zero? + loop_record.update_attributes(data) + return loop_record + end + } + record = new(data) + record.save + return record + else + raise ArgumentError, 'Need name, login, email or locale for create_or_update()' + end + end + end +end diff --git a/app/models/application_model/has_assets.rb b/app/models/application_model/has_assets.rb new file mode 100644 index 000000000..a7dff804a --- /dev/null +++ b/app/models/application_model/has_assets.rb @@ -0,0 +1,153 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ +module ApplicationModel::HasAssets + extend ActiveSupport::Concern + +=begin + +get all assets / related models for this user + + user = User.find(123) + result = user.assets(assets_if_exists) + +returns + + result = { + :User => { + 123 => user_model_123, + 1234 => user_model_1234, + } + } + +=end + + def assets(data = {}) + + app_model = self.class.to_app_model + + if !data[ app_model ] + data[ app_model ] = {} + end + if !data[ app_model ][ id ] + data[ app_model ][ id ] = attributes_with_association_ids + end + + return data if !self['created_by_id'] && !self['updated_by_id'] + app_model_user = User.to_app_model + %w(created_by_id updated_by_id).each { |local_user_id| + next if !self[ local_user_id ] + next if data[ app_model_user ] && data[ app_model_user ][ self[ local_user_id ] ] + user = User.lookup(id: self[ local_user_id ]) + next if !user + data = user.assets(data) + } + data + end + +=begin + +get assets and record_ids of selector + + model = Model.find(123) + + assets = model.assets_of_selector('attribute_name_of_selector', assets) + +=end + + def assets_of_selector(selector, assets = {}) + + # get assets of condition + models = Models.all + send(selector).each { |item, content| + attribute = item.split(/\./) + next if !attribute[1] + begin + attribute_class = attribute[0].to_classname.constantize + rescue => e + logger.error "Unable to get asset for '#{attribute[0]}': #{e.inspect}" + next + end + reflection = attribute[1].sub(/_id$/, '') + #reflection = reflection.to_sym + next if !models[attribute_class] + next if !models[attribute_class][:reflections] + next if !models[attribute_class][:reflections][reflection] + next if !models[attribute_class][:reflections][reflection].klass + attribute_ref_class = models[attribute_class][:reflections][reflection].klass + if content['value'].instance_of?(Array) + content['value'].each { |item_id| + attribute_object = attribute_ref_class.find_by(id: item_id) + if attribute_object + assets = attribute_object.assets(assets) + end + } + else + attribute_object = attribute_ref_class.find_by(id: content['value']) + if attribute_object + assets = attribute_object.assets(assets) + end + end + } + assets + end + + # methods defined here are going to extend the class, not the instance of it + class_methods do + +=begin + +return object and assets + + data = Model.full(123) + data = { + id: 123, + assets: assets, + } + +=end + + def full(id) + object = find(id) + assets = object.assets({}) + { + id: id, + assets: assets, + } + end + +=begin + +get assets of object list + + list = [ + { + object => 'Ticket', + o_id => 1, + }, + { + object => 'User', + o_id => 121, + }, + ] + + assets = Model.assets_of_object_list(list, assets) + +=end + + def assets_of_object_list(list, assets = {}) + list.each { |item| + require item['object'].to_filename + record = Kernel.const_get(item['object']).find(item['o_id']) + assets = record.assets(assets) + if item['created_by_id'] + user = User.find(item['created_by_id']) + assets = user.assets(assets) + end + if item['updated_by_id'] + user = User.find(item['updated_by_id']) + assets = user.assets(assets) + end + } + assets + end + end +end diff --git a/app/models/application_model/has_associations.rb b/app/models/application_model/has_associations.rb new file mode 100644 index 000000000..ff4ff47d3 --- /dev/null +++ b/app/models/application_model/has_associations.rb @@ -0,0 +1,357 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ +module ApplicationModel::HasAssociations + extend ActiveSupport::Concern + +=begin + +set relations of model based on params + + model = Model.find(1) + result = model.associations_from_param(params) + +returns + + result = true|false + +=end + + def associations_from_param(params) + + # set relations by id/verify if ref exists + self.class.reflect_on_all_associations.map { |assoc| + assoc_name = assoc.name + real_ids = assoc_name[0, assoc_name.length - 1] + '_ids' + real_ids = real_ids.to_sym + next if !params.key?(real_ids) + list_of_items = params[real_ids] + if !params[real_ids].instance_of?(Array) + list_of_items = [ params[real_ids] ] + end + list = [] + list_of_items.each { |item_id| + next if !item_id + lookup = assoc.klass.lookup(id: item_id) + + # complain if we found no reference + if !lookup + raise ArgumentError, "No value found for '#{assoc_name}' with id #{item_id.inspect}" + end + list.push item_id + } + send("#{real_ids}=", list) + } + + # set relations by name/lookup + self.class.reflect_on_all_associations.map { |assoc| + assoc_name = assoc.name + real_ids = assoc_name[0, assoc_name.length - 1] + '_ids' + next if !respond_to?(real_ids) + real_values = assoc_name[0, assoc_name.length - 1] + 's' + real_values = real_values.to_sym + next if !respond_to?(real_values) + next if !params[real_values] + next if !params[real_values].instance_of?(Array) + list = [] + class_object = assoc.klass + params[real_values].each { |value| + lookup = nil + if class_object == User + if !lookup + lookup = class_object.lookup(login: value) + end + if !lookup + lookup = class_object.lookup(email: value) + end + else + lookup = class_object.lookup(name: value) + end + + # complain if we found no reference + if !lookup + raise ArgumentError, "No lookup value found for '#{assoc_name}': #{value.inspect}" + end + list.push lookup.id + } + send("#{real_ids}=", list) + } + end + +=begin + +get relations of model based on params + + model = Model.find(1) + attributes = model.attributes_with_association_ids + +returns + + hash with attributes and association ids + +=end + + def attributes_with_association_ids + + key = "#{self.class}::aws::#{id}" + cache = Cache.get(key) + return cache if cache + + ignored_attributes = self.class.instance_variable_get(:@association_attributes_ignored) || [] + + # get relations + attributes = self.attributes + self.class.reflect_on_all_associations.map { |assoc| + real_ids = assoc.name.to_s[0, assoc.name.to_s.length - 1] + '_ids' + next if ignored_attributes.include?(real_ids.to_sym) + next if !respond_to?(real_ids) + attributes[real_ids] = send(real_ids) + } + Cache.write(key, attributes) + attributes + end + +=begin + +get relation name of model based on params + + model = Model.find(1) + attributes = model.attributes_with_association_names + +returns + + hash with attributes, association ids, association names and relation name + +=end + + def attributes_with_association_names + + # get relations + attributes = attributes_with_association_ids + self.class.reflect_on_all_associations.map { |assoc| + next if !respond_to?(assoc.name) + ref = send(assoc.name) + next if !ref + if ref.respond_to?(:first) + attributes[assoc.name.to_s] = [] + ref.each { |item| + if item[:login] + attributes[assoc.name.to_s].push item[:login] + next + end + next if !item[:name] + attributes[assoc.name.to_s].push item[:name] + } + if ref.count.positive? && attributes[assoc.name.to_s].empty? + attributes.delete(assoc.name.to_s) + end + next + end + if ref[:login] + attributes[assoc.name.to_s] = ref[:login] + next + end + next if !ref[:name] + attributes[assoc.name.to_s] = ref[:name] + } + + # fill created_by/updated_by + { + 'created_by_id' => 'created_by', + 'updated_by_id' => 'updated_by', + }.each { |source, destination| + next if !attributes[source] + user = User.lookup(id: attributes[source]) + next if !user + attributes[destination] = user.login + } + + # remove forbitten attributes + %w(password token tokens token_ids).each { |item| + attributes.delete(item) + } + + attributes + end + +=begin + +reference if association id check + + model = Model.find(123) + attributes = model.association_id_validation('attribute_id', value) + +returns + + true | false + +=end + + def association_id_validation(attribute_id, value) + return true if value.nil? + + attributes.each { |key, _value| + next if key != attribute_id + + # check if id is assigned + next if !key.end_with?('_id') + key_short = key.chomp('_id') + + self.class.reflect_on_all_associations.map { |assoc| + next if assoc.name.to_s != key_short + item = assoc.class_name.constantize + return false if !item.respond_to?(:find_by) + ref_object = item.find_by(id: value) + return false if !ref_object + return true + } + } + true + end + + # methods defined here are going to extend the class, not the instance of it + class_methods do + +=begin + +serve methode to ignore model attribute associations + +class Model < ApplicationModel + include AssociationConcern + association_attributes_ignored :user_ids +end + +=end + + def association_attributes_ignored(*attributes) + @association_attributes_ignored = attributes + end + +=begin + +do name/login/email based lookup for associations + + params = { + login: 'some login', + firstname: 'some firstname', + lastname: 'some lastname', + email: 'some email', + organization: 'some organization', + roles: ['Agent', 'Admin'], + } + + attributes = Model.association_name_to_id_convert(params) + +returns + + attributes = params # params with possible lookups + + attributes = { + login: 'some login', + firstname: 'some firstname', + lastname: 'some lastname', + email: 'some email', + organization_id: 123, + role_ids: [2,1], + } + +=end + + def association_name_to_id_convert(params) + + data = {} + params.each { |key, value| + data[key.to_sym] = value + } + + data.symbolize_keys! + available_attributes = attribute_names + reflect_on_all_associations.map { |assoc| + + assoc_name = assoc.name + value = data[assoc_name] + next if !value # next if we do not have a value + ref_name = "#{assoc_name}_id" + + # handle _id values + if available_attributes.include?(ref_name) # if we do have an _id attribute + next if data[ref_name.to_sym] # next if we have already the _id filled + + # get association class and do lookup + class_object = assoc.klass + lookup = nil + if class_object == User + if value.instance_of?(String) + if !lookup + lookup = class_object.lookup(login: value) + end + if !lookup + lookup = class_object.lookup(email: value) + end + else + raise ArgumentError, "String is needed as ref value #{value.inspect} for '#{assoc_name}'" + end + else + lookup = class_object.lookup(name: value) + end + + # complain if we found no reference + if !lookup + raise ArgumentError, "No lookup value found for '#{assoc_name}': #{value.inspect}" + end + + # release data value + data.delete(assoc_name) + + # remember id reference + data[ref_name.to_sym] = lookup.id + next + end + + next if !value.instance_of?(Array) + next if value.empty? + next if !value[0].instance_of?(String) + + # handle _ids values + next if !assoc_name.to_s.end_with?('s') + ref_names = "#{assoc_name.to_s.chomp('s')}_ids" + generic_object_tmp = new + next unless generic_object_tmp.respond_to?(ref_names) # if we do have an _ids attribute + next if data[ref_names.to_sym] # next if we have already the _ids filled + + # get association class and do lookup + class_object = assoc.klass + lookup_ids = [] + value.each { |item| + lookup = nil + if class_object == User + if item.instance_of?(String) + if !lookup + lookup = class_object.lookup(login: item) + end + if !lookup + lookup = class_object.lookup(email: item) + end + else + raise ArgumentError, "String is needed in array ref as ref value #{value.inspect} for '#{assoc_name}'" + end + else + lookup = class_object.lookup(name: item) + end + + # complain if we found no reference + if !lookup + raise ArgumentError, "No lookup value found for '#{assoc_name}': #{item.inspect}" + end + lookup_ids.push lookup.id + } + + # release data value + data.delete(assoc_name) + + # remember id reference + data[ref_names.to_sym] = lookup_ids + } + + data + end + end +end diff --git a/app/models/application_model/has_attachments.rb b/app/models/application_model/has_attachments.rb new file mode 100644 index 000000000..c3ca0da15 --- /dev/null +++ b/app/models/application_model/has_attachments.rb @@ -0,0 +1,74 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ +module ApplicationModel::HasAttachments + extend ActiveSupport::Concern + + included do + after_create :attachments_buffer_check + after_update :attachments_buffer_check + end + +=begin + +get list of attachments of this object + + item = Model.find(123) + list = item.attachments + +returns + + # array with Store model objects + +=end + + def attachments + Store.list(object: self.class.to_s, o_id: id) + end + +=begin + +store attachments for this object + + item = Model.find(123) + item.attachments = [ Store-Object1, Store-Object2 ] + +=end + + def attachments=(attachments) + self.attachments_buffer = attachments + + # update if object already exists + return if !(id && id.nonzero?) + + attachments_buffer_check + end + + private + + def attachments_buffer + @attachments_buffer_data + end + + def attachments_buffer=(attachments) + @attachments_buffer_data = attachments + end + + def attachments_buffer_check + + # do nothing if no attachment exists + return 1 if attachments_buffer.nil? + + # store attachments + article_store = [] + attachments_buffer.each do |attachment| + article_store.push Store.add( + object: self.class.to_s, + o_id: id, + data: attachment.content, + filename: attachment.filename, + preferences: attachment.preferences, + created_by_id: created_by_id, + ) + end + attachments_buffer = nil + end +end diff --git a/app/models/application_model/has_latest_change_timestamp.rb b/app/models/application_model/has_latest_change_timestamp.rb new file mode 100644 index 000000000..2d4b8e610 --- /dev/null +++ b/app/models/application_model/has_latest_change_timestamp.rb @@ -0,0 +1,46 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ +module ApplicationModel::HasLatestChangeTimestamp + extend ActiveSupport::Concern + + # methods defined here are going to extend the class, not the instance of it + class_methods do + +=begin + + get latest updated_at object timestamp + + latest_change = Ticket.latest_change + +returns + + result = timestamp + +=end + + def latest_change + key = "#{new.class.name}_latest_change" + updated_at = Cache.get(key) + + # if we do not have it cached, do lookup + if !updated_at + o = select(:updated_at).order(updated_at: :desc).limit(1).first + if o + updated_at = o.updated_at + latest_change_set(updated_at) + end + end + updated_at + end + + def latest_change_set(updated_at) + key = "#{new.class.name}_latest_change" + expires_in = 31_536_000 # 1 year + + if updated_at.nil? + Cache.delete(key) + else + Cache.write(key, updated_at, { expires_in: expires_in }) + end + end + end +end diff --git a/app/models/application_model/history_log_base.rb b/app/models/application_model/history_loggable.rb similarity index 73% rename from app/models/application_model/history_log_base.rb rename to app/models/application_model/history_loggable.rb index a79347655..fcd28d08f 100644 --- a/app/models/application_model/history_log_base.rb +++ b/app/models/application_model/history_loggable.rb @@ -1,5 +1,6 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ -module ApplicationModel::HistoryLogBase +module ApplicationModel::HistoryLoggable + extend ActiveSupport::Concern =begin @@ -14,16 +15,27 @@ returns =end - def history_log (type, user_id, data = {}) - data[:o_id] = self['id'] - data[:history_type] = type - data[:history_object] = self.class.name - data[:related_o_id] = nil - data[:related_history_object] = nil - data[:created_by_id] = user_id - data[:updated_at] = updated_at - data[:created_at] = updated_at - History.add(data) + def history_log(type, user_id, attributes = {}) + + attributes.merge!( + o_id: self['id'], + history_type: type, + history_object: self.class.name, + related_o_id: nil, + related_history_object: nil, + created_by_id: user_id, + updated_at: updated_at, + created_at: updated_at, + ).merge!(history_log_attributes) + + History.add(attributes) + end + + # callback function to overwrite + # default history log attributes + # gets called from history_log + def history_log_attributes + {} end =begin diff --git a/app/models/application_model/importable.rb b/app/models/application_model/importable.rb new file mode 100644 index 000000000..f919b64d4 --- /dev/null +++ b/app/models/application_model/importable.rb @@ -0,0 +1,19 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ +module ApplicationModel::Importable + extend ActiveSupport::Concern + + included do + before_create :check_attributes_protected + end + + def check_attributes_protected + + import_class_list = ['Ticket', 'Ticket::Article', 'History', 'Ticket::State', 'Ticket::StateType', 'Ticket::Priority', 'Group', 'User', 'Role' ] + + # do noting, use id as it is + return if !Setting.get('system_init_done') + return if Setting.get('import_mode') && import_class_list.include?(self.class.to_s) + + self[:id] = nil + end +end diff --git a/app/models/application_model/search_index_base.rb b/app/models/application_model/search_index_base.rb deleted file mode 100644 index 39c661886..000000000 --- a/app/models/application_model/search_index_base.rb +++ /dev/null @@ -1,127 +0,0 @@ -# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ -module ApplicationModel::SearchIndexBase - -=begin - -collect data to index and send to backend - - ticket = Ticket.find(123) - result = ticket.search_index_update_backend - -returns - - result = true # false - -=end - - def search_index_update_backend - return if !self.class.search_index_support_config - - # fill up with search data - attributes = search_index_attribute_lookup - return if !attributes - - # update backend - SearchIndexBackend.add(self.class.to_s, attributes) - end - -=begin - -get data to store in search index - - ticket = Ticket.find(123) - result = ticket.search_index_data - -returns - - result = { - attribute1: 'some value', - attribute2: ['value 1', 'value 2'], - ... - } - -=end - - def search_index_data - attributes = {} - %w(name note).each { |key| - next if !self[key] - next if self[key].respond_to?('empty?') && self[key].empty? - attributes[key] = self[key] - } - return if attributes.empty? - attributes - end - -=begin - -lookup name of ref. objects - - ticket = Ticket.find(3) - attributes = ticket.search_index_attribute_lookup - -returns - - attributes # object with lookup data - -=end - - def search_index_attribute_lookup - - attributes = self.attributes - self.attributes.each { |key, value| - next if !value - - # get attribute name - attribute_name_with_id = key.to_s - attribute_name = key.to_s - next if attribute_name[-3, 3] != '_id' - attribute_name = attribute_name[ 0, attribute_name.length - 3 ] - - # check if attribute method exists - next if !respond_to?(attribute_name) - - # check if method has own class - relation_class = send(attribute_name).class - next if !relation_class - - # lookup ref object - relation_model = relation_class.lookup(id: value) - next if !relation_model - - # get name of ref object - value = nil - if relation_model.respond_to?('search_index_data') - value = relation_model.send('search_index_data') - end - - if relation_model.respond_to?('name') - value = relation_model.send('name') - end - - next if !value - - # save name of ref object - attributes[ attribute_name ] = value - } - - # default ignored attributes - config = self.class.search_index_support_config - if config - ignore_attributes = {} - if config[:ignore_attributes] - config[:ignore_attributes].each { |key, value| - ignore_attributes[key] = value - } - end - - # remove ignored attributes - ignore_attributes.each { |key, value| - next if value != true - attributes.delete(key.to_s) - } - end - - attributes - end -end diff --git a/app/models/application_model/touches_references.rb b/app/models/application_model/touches_references.rb new file mode 100644 index 000000000..aff47ab21 --- /dev/null +++ b/app/models/application_model/touches_references.rb @@ -0,0 +1,30 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ +module ApplicationModel::TouchesReferences + extend ActiveSupport::Concern + + # methods defined here are going to extend the class, not the instance of it + class_methods do + +=begin + +touch references by params + + Model.touch_reference_by_params( + object: 'Ticket', + o_id: 123, + ) + +=end + + def touch_reference_by_params(data) + + object_class = Kernel.const_get(data[:object]) + object = object_class.lookup(id: data[:o_id]) + return if !object + object.touch + rescue => e + logger.error e.message + logger.error e.backtrace.inspect + end + end +end diff --git a/app/models/calendar.rb b/app/models/calendar.rb index b7eb8abd3..9bd526ec1 100644 --- a/app/models/calendar.rb +++ b/app/models/calendar.rb @@ -1,6 +1,9 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ class Calendar < ApplicationModel + include NotifiesClients + include UniqNamed + store :business_hours store :public_holidays @@ -10,8 +13,6 @@ class Calendar < ApplicationModel after_update :sync_default, :min_one_check after_destroy :min_one_check - notify_clients_support - =begin set inital default calendar @@ -38,7 +39,7 @@ returns calendar object calendar_details = Service::GeoCalendar.location(ip) return if !calendar_details - calendar_details['name'] = Calendar.genrate_uniq_name(calendar_details['name']) + calendar_details['name'] = Calendar.generate_uniq_name(calendar_details['name']) calendar_details['default'] = true calendar_details['created_by_id'] = 1 calendar_details['updated_by_id'] = 1 diff --git a/app/models/channel/assets.rb b/app/models/channel/assets.rb index d7097fa16..a61019019 100644 --- a/app/models/channel/assets.rb +++ b/app/models/channel/assets.rb @@ -29,7 +29,7 @@ returns data[ app_model ] = {} end if !data[ app_model ][ id ] - attributes = attributes_with_associations + attributes = attributes_with_association_ids # remove passwords if use is no admin access = false diff --git a/app/models/channel/email_parser.rb b/app/models/channel/email_parser.rb index 288335a8c..656015707 100644 --- a/app/models/channel/email_parser.rb +++ b/app/models/channel/email_parser.rb @@ -602,7 +602,7 @@ returns return if !class_object class_instance = class_object.new - return false if !class_instance.association_id_check(attribute, value) + return false if !class_instance.association_id_validation(attribute, value) true end diff --git a/app/models/concerns/historisable.rb b/app/models/concerns/historisable.rb new file mode 100644 index 000000000..a95623d5f --- /dev/null +++ b/app/models/concerns/historisable.rb @@ -0,0 +1,140 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ +module Historisable + extend ActiveSupport::Concern + + included do + attr_accessor :history_changes_last_done + + after_create :history_create + after_update :history_update + after_destroy :history_destroy + end + +=begin + +log object create history, if configured - will be executed automatically + + model = Model.find(123) + model.history_create + +=end + + def history_create + history_log('created', created_by_id) + end + +=begin + +log object update history with all updated attributes, if configured - will be executed automatically + + model = Model.find(123) + model.history_update + +=end + + def history_update + return if !changed? + + # return if it's no update + return if new_record? + + # new record also triggers update, so ignore new records + changes = self.changes + if history_changes_last_done + history_changes_last_done.each { |key, value| + if changes.key?(key) && changes[key] == value + changes.delete(key) + end + } + end + self.history_changes_last_done = changes + #logger.info 'updated ' + self.changes.inspect + + return if changes['id'] && !changes['id'][0] + + ignored_attributes = self.class.instance_variable_get(:@history_attributes_ignored) || [] + ignored_attributes += %i(created_at updated_at created_by_id updated_by_id) + + changes.each { |key, value| + + next if ignored_attributes.include?(key.to_sym) + + # get attribute name + attribute_name = key.to_s + if attribute_name[-3, 3] == '_id' + attribute_name = attribute_name[ 0, attribute_name.length - 3 ] + end + + value_id = [] + value_str = [ value[0], value[1] ] + if key.to_s[-3, 3] == '_id' + value_id[0] = value[0] + value_id[1] = value[1] + + if respond_to?(attribute_name) && send(attribute_name) + relation_class = send(attribute_name).class + if relation_class && value_id[0] + relation_model = relation_class.lookup(id: value_id[0]) + if relation_model + if relation_model['name'] + value_str[0] = relation_model['name'] + elsif relation_model.respond_to?('fullname') + value_str[0] = relation_model.send('fullname') + end + end + end + if relation_class && value_id[1] + relation_model = relation_class.lookup(id: value_id[1]) + if relation_model + if relation_model['name'] + value_str[1] = relation_model['name'] + elsif relation_model.respond_to?('fullname') + value_str[1] = relation_model.send('fullname') + end + end + end + end + end + data = { + history_attribute: attribute_name, + value_from: value_str[0].to_s, + value_to: value_str[1].to_s, + id_from: value_id[0], + id_to: value_id[1], + } + #logger.info "HIST NEW #{self.class.to_s}.find(#{self.id}) #{data.inspect}" + history_log('updated', updated_by_id, data) + } + end + +=begin + +delete object history, will be executed automatically + + model = Model.find(123) + model.history_destroy + +=end + + def history_destroy + History.remove(self.class.to_s, id) + end + + # methods defined here are going to extend the class, not the instance of it + class_methods do +=begin + +serve methode to ignore model attributes in historization + +class Model < ApplicationModel + include Historisable + history_attributes_ignored :create_article_type_id, :preferences +end + +=end + + def history_attributes_ignored(*attributes) + @history_attributes_ignored = attributes + end + end +end diff --git a/app/models/concerns/latest_change_observed.rb b/app/models/concerns/latest_change_observed.rb new file mode 100644 index 000000000..3e390158c --- /dev/null +++ b/app/models/concerns/latest_change_observed.rb @@ -0,0 +1,19 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ +module LatestChangeObserved + extend ActiveSupport::Concern + + included do + after_create :latest_change_set_from_observer + after_update :latest_change_set_from_observer + after_touch :latest_change_set_from_observer + after_destroy :latest_change_set_from_observer_destroy + end + + def latest_change_set_from_observer + self.class.latest_change_set(updated_at) + end + + def latest_change_set_from_observer_destroy + self.class.latest_change_set(nil) + end +end diff --git a/app/models/concerns/logs_activity_stream.rb b/app/models/concerns/logs_activity_stream.rb new file mode 100644 index 000000000..6c9ac12e9 --- /dev/null +++ b/app/models/concerns/logs_activity_stream.rb @@ -0,0 +1,87 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ +module LogsActivityStream + extend ActiveSupport::Concern + + included do + after_create :activity_stream_create + after_update :activity_stream_update + before_destroy :activity_stream_destroy + end + +=begin + +log object create activity stream, if configured - will be executed automatically + + model = Model.find(123) + model.activity_stream_create + +=end + + def activity_stream_create + activity_stream_log('create', self['created_by_id']) + end + +=begin + +log object update activity stream, if configured - will be executed automatically + + model = Model.find(123) + model.activity_stream_update + +=end + + def activity_stream_update + return if !changed? + + ignored_attributes = self.class.instance_variable_get(:@activity_stream_attributes_ignored) || [] + ignored_attributes += %i(created_at updated_at created_by_id updated_by_id) + + log = false + changes.each { |key, _value| + next if ignored_attributes.include?(key.to_sym) + + log = true + } + + return if !log + + activity_stream_log('update', self['updated_by_id']) + end + +=begin + +delete object activity stream, will be executed automatically + + model = Model.find(123) + model.activity_stream_destroy + +=end + + def activity_stream_destroy + ActivityStream.remove(self.class.to_s, id) + end + + # methods defined here are going to extend the class, not the instance of it + class_methods do + +=begin + +serve methode to ignore model attributes in activity stream and/or limit activity stream permission + +class Model < ApplicationModel + include LogsActivityStream + activity_stream_permission 'admin.user' + activity_stream_attributes_ignored :create_article_type_id, :preferences +end + +=end + + def activity_stream_attributes_ignored(*attributes) + @activity_stream_attributes_ignored = attributes + end + + def activity_stream_permission(permission) + @activity_stream_permission = permission + end + end +end diff --git a/app/models/concerns/notifies_clients.rb b/app/models/concerns/notifies_clients.rb new file mode 100644 index 000000000..e3997c694 --- /dev/null +++ b/app/models/concerns/notifies_clients.rb @@ -0,0 +1,138 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ +module NotifiesClients + extend ActiveSupport::Concern + + included do + after_create :notify_clients_after_create + after_update :notify_clients_after_update + after_touch :notify_clients_after_touch + after_destroy :notify_clients_after_destroy + end + +=begin + +notify_clients_after_create after model got created + +used as callback in model file + +class OwnModel < ApplicationModel + after_create :notify_clients_after_create + after_update :notify_clients_after_update + after_touch :notify_clients_after_touch + after_destroy :notify_clients_after_destroy + + [...] + +=end + + def notify_clients_after_create + + # return if we run import mode + return if Setting.get('import_mode') + logger.debug "#{self.class.name}.find(#{id}) notify created " + created_at.to_s + class_name = self.class.name + class_name.gsub!(/::/, '') + PushMessages.send( + message: { + event: class_name + ':create', + data: { id: id, updated_at: updated_at } + }, + type: 'authenticated', + ) + end + +=begin + +notify_clients_after_update after model got updated + +used as callback in model file + +class OwnModel < ApplicationModel + after_create :notify_clients_after_create + after_update :notify_clients_after_update + after_touch :notify_clients_after_touch + after_destroy :notify_clients_after_destroy + + [...] + +=end + + def notify_clients_after_update + + # return if we run import mode + return if Setting.get('import_mode') + logger.debug "#{self.class.name}.find(#{id}) notify UPDATED " + updated_at.to_s + class_name = self.class.name + class_name.gsub!(/::/, '') + PushMessages.send( + message: { + event: class_name + ':update', + data: { id: id, updated_at: updated_at } + }, + type: 'authenticated', + ) + end + +=begin + +notify_clients_after_touch after model got touched + +used as callback in model file + +class OwnModel < ApplicationModel + after_create :notify_clients_after_create + after_update :notify_clients_after_update + after_touch :notify_clients_after_touch + after_destroy :notify_clients_after_destroy + + [...] + +=end + + def notify_clients_after_touch + + # return if we run import mode + return if Setting.get('import_mode') + logger.debug "#{self.class.name}.find(#{id}) notify TOUCH " + updated_at.to_s + class_name = self.class.name + class_name.gsub!(/::/, '') + PushMessages.send( + message: { + event: class_name + ':touch', + data: { id: id, updated_at: updated_at } + }, + type: 'authenticated', + ) + end + +=begin + +notify_clients_after_destroy after model got destroyed + +used as callback in model file + +class OwnModel < ApplicationModel + after_create :notify_clients_after_create + after_update :notify_clients_after_update + after_touch :notify_clients_after_touch + after_destroy :notify_clients_after_destroy + + [...] + +=end + def notify_clients_after_destroy + + # return if we run import mode + return if Setting.get('import_mode') + logger.debug "#{self.class.name}.find(#{id}) notify DESTOY " + updated_at.to_s + class_name = self.class.name + class_name.gsub!(/::/, '') + PushMessages.send( + message: { + event: class_name + ':destroy', + data: { id: id, updated_at: updated_at } + }, + type: 'authenticated', + ) + end +end diff --git a/app/models/concerns/search_indexed.rb b/app/models/concerns/search_indexed.rb new file mode 100644 index 000000000..febbf6e5e --- /dev/null +++ b/app/models/concerns/search_indexed.rb @@ -0,0 +1,138 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ +module SearchIndexed + extend ActiveSupport::Concern + + included do + after_create :search_index_update + after_update :search_index_update + after_touch :search_index_update + after_destroy :search_index_destroy + end + +=begin + +update search index, if configured - will be executed automatically + + model = Model.find(123) + model.search_index_update + +=end + + def search_index_update + return if ignore_search_indexing?(:update) + + # start background job to transfer data to search index + return if !SearchIndexBackend.enabled? + Delayed::Job.enqueue(BackgroundJobSearchIndex.new(self.class.to_s, id)) + end + +=begin + +delete search index object, will be executed automatically + + model = Model.find(123) + model.search_index_destroy + +=end + + def search_index_destroy + return if ignore_search_indexing?(:destroy) + SearchIndexBackend.remove(self.class.to_s, id) + end + +=begin + +collect data to index and send to backend + + ticket = Ticket.find(123) + result = ticket.search_index_update_backend + +returns + + result = true # false + +=end + + def search_index_update_backend + # fill up with search data + attributes = search_index_attribute_lookup + return if !attributes + + # update backend + SearchIndexBackend.add(self.class.to_s, attributes) + end + +=begin + +get data to store in search index + + ticket = Ticket.find(123) + result = ticket.search_index_data + +returns + + result = { + attribute1: 'some value', + attribute2: ['value 1', 'value 2'], + ... + } + +=end + + def search_index_data + attributes = {} + %w(name note).each { |key| + next if !self[key] + next if self[key].respond_to?('empty?') && self[key].empty? + attributes[key] = self[key] + } + return if attributes.empty? + attributes + end + + def ignore_search_indexing?(_action) + false + end + + # methods defined here are going to extend the class, not the instance of it + class_methods do + +=begin + +serve methode to ignore model attributes in search index + +class Model < ApplicationModel + include SearchIndexed + search_index_attributes_ignored :password, :image +end + +=end + + def search_index_attributes_ignored(*attributes) + @search_index_attributes_ignored = attributes + end + +=begin + +reload search index with full data + + Model.search_index_reload + +=end + + def search_index_reload + tolerance = 5 + tolerance_count = 0 + all.order('created_at DESC').each { |item| + next if item.ignore_search_indexing?(:destroy) + begin + item.search_index_update_backend + rescue => e + logger.error "Unable to send #{item.class}.find(#{item.id}) backend: #{e.inspect}" + tolerance_count += 1 + raise "Unable to send #{item.class}.find(#{item.id}) backend: #{e.inspect}" if tolerance_count == tolerance + end + } + end + end +end diff --git a/app/models/concerns/uniq_named.rb b/app/models/concerns/uniq_named.rb new file mode 100644 index 000000000..4ee4f465f --- /dev/null +++ b/app/models/concerns/uniq_named.rb @@ -0,0 +1,31 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ +module UniqNamed + extend ActiveSupport::Concern + + # methods defined here are going to extend the class, not the instance of it + class_methods do + +=begin + +generate uniq name (will check name of model and generates _1 sequenze) + +Used as before_update callback, no own use needed + + name = Model.generate_uniq_name('some name') + +returns + + result = 'some name_X' + +=end + + def generate_uniq_name(name) + return name if !exists?(name: name) + (1..100).each { |counter| + name = "#{name}_#{counter}" + break if !exists?(name: name) + } + name + end + end +end diff --git a/app/models/email_address.rb b/app/models/email_address.rb index 71acae994..18a7b91f3 100644 --- a/app/models/email_address.rb +++ b/app/models/email_address.rb @@ -1,6 +1,8 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ class EmailAddress < ApplicationModel + include LatestChangeObserved + has_many :groups, after_add: :cache_update, after_remove: :cache_update belongs_to :channel validates :realname, presence: true @@ -12,8 +14,6 @@ class EmailAddress < ApplicationModel after_update :update_email_address_id after_destroy :delete_group_reference - latest_change_support - =begin check and if channel not exists reset configured channels for email addresses diff --git a/app/models/group.rb b/app/models/group.rb index 8547138f3..be8e5c99f 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -1,13 +1,15 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ class Group < ApplicationModel + include LogsActivityStream + include NotifiesClients + include LatestChangeObserved + include Historisable + has_and_belongs_to_many :users, after_add: :cache_update, after_remove: :cache_update belongs_to :email_address belongs_to :signature validates :name, presence: true - activity_stream_support permission: 'admin.group' - history_support - notify_clients_support - latest_change_support + activity_stream_permission 'admin.group' end diff --git a/app/models/history.rb b/app/models/history.rb index a86c69095..6b7c72046 100644 --- a/app/models/history.rb +++ b/app/models/history.rb @@ -308,17 +308,6 @@ returns history_attribute end - private - -=begin - -nothing to do on destroying history entries - -=end - - def destroy_dependencies - end - class Object < ApplicationModel end diff --git a/app/models/job.rb b/app/models/job.rb index 4f13cf913..7d0a010fa 100644 --- a/app/models/job.rb +++ b/app/models/job.rb @@ -1,6 +1,8 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ class Job < ApplicationModel + include NotifiesClients + load 'job/assets.rb' include Job::Assets @@ -12,8 +14,6 @@ class Job < ApplicationModel before_create :updated_matching, :update_next_run_at before_update :updated_matching, :update_next_run_at - notify_clients_support - def self.run jobs = Job.where(active: true, running: false) jobs.each do |job| diff --git a/app/models/job/assets.rb b/app/models/job/assets.rb index 93c12fd82..b02178ba3 100644 --- a/app/models/job/assets.rb +++ b/app/models/job/assets.rb @@ -32,7 +32,7 @@ returns data[ User.to_app_model ] = {} end if !data[ app_model ][ id ] - data[ app_model ][ id ] = attributes_with_associations + data[ app_model ][ id ] = attributes_with_association_ids data = assets_of_selector('condition', data) data = assets_of_selector('perform', data) end diff --git a/app/models/macro.rb b/app/models/macro.rb index 733708e56..af860f659 100644 --- a/app/models/macro.rb +++ b/app/models/macro.rb @@ -1,10 +1,9 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ class Macro < ApplicationModel + include NotifiesClients + include LatestChangeObserved + store :perform validates :name, presence: true - - notify_clients_support - latest_change_support - end diff --git a/app/models/object_manager/attribute.rb b/app/models/object_manager/attribute.rb index 0928b2ed5..243d18465 100644 --- a/app/models/object_manager/attribute.rb +++ b/app/models/object_manager/attribute.rb @@ -1,13 +1,14 @@ class ObjectManager::Attribute < ApplicationModel + include NotifiesClients + self.table_name = 'object_manager_attributes' + belongs_to :object_lookup, class_name: 'ObjectLookup' validates :name, presence: true store :screens store :data_option store :data_option_new - notify_clients_support - =begin list of all attributes diff --git a/app/models/organization.rb b/app/models/organization.rb index d8af2a25c..a9a5c8747 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -1,6 +1,12 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ class Organization < ApplicationModel + include LogsActivityStream + include NotifiesClients + include LatestChangeObserved + include Historisable + include SearchIndexed + load 'organization/permission.rb' include Organization::Permission load 'organization/assets.rb' @@ -16,11 +22,7 @@ class Organization < ApplicationModel before_create :domain_cleanup before_update :domain_cleanup - activity_stream_support permission: 'admin.role' - history_support - search_index_support - notify_clients_support - latest_change_support + activity_stream_permission 'admin.role' private diff --git a/app/models/overview.rb b/app/models/overview.rb index cd00442de..f631aa13a 100644 --- a/app/models/overview.rb +++ b/app/models/overview.rb @@ -1,6 +1,9 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ class Overview < ApplicationModel + include NotifiesClients + include LatestChangeObserved + load 'overview/assets.rb' include Overview::Assets @@ -13,9 +16,6 @@ class Overview < ApplicationModel before_create :fill_link_on_create, :fill_prio before_update :fill_link_on_update - notify_clients_support - latest_change_support - private def fill_prio diff --git a/app/models/overview/assets.rb b/app/models/overview/assets.rb index 2b43a45eb..6a9083a2f 100644 --- a/app/models/overview/assets.rb +++ b/app/models/overview/assets.rb @@ -33,7 +33,7 @@ returns data[ app_model_user ] = {} end if !data[ app_model_overview ][ id ] - data[ app_model_overview ][ id ] = attributes_with_associations + data[ app_model_overview ][ id ] = attributes_with_association_ids if user_ids user_ids.each { |local_user_id| next if data[ app_model_user ][ local_user_id ] diff --git a/app/models/permission.rb b/app/models/permission.rb index 45c0397de..c926fe632 100644 --- a/app/models/permission.rb +++ b/app/models/permission.rb @@ -1,11 +1,12 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ class Permission < ApplicationModel + include NotifiesClients + include LatestChangeObserved + has_and_belongs_to_many :roles validates :name, presence: true store :preferences - notify_clients_support - latest_change_support =begin diff --git a/app/models/role.rb b/app/models/role.rb index 20bbc08be..dfaba9e61 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -1,6 +1,10 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ class Role < ApplicationModel + include LogsActivityStream + include NotifiesClients + include LatestChangeObserved + has_and_belongs_to_many :users, after_add: :cache_update, after_remove: :cache_update has_and_belongs_to_many :permissions, after_add: :cache_update, after_remove: :cache_update validates :name, presence: true @@ -9,10 +13,9 @@ class Role < ApplicationModel before_create :validate_permissions before_update :validate_permissions - attributes_with_associations_support ignore: { user_ids: true } - activity_stream_support permission: 'admin.role' - notify_clients_support - latest_change_support + association_attributes_ignored :user_ids + + activity_stream_permission 'admin.role' =begin diff --git a/app/models/signature.rb b/app/models/signature.rb index 469b6d620..07be9f281 100644 --- a/app/models/signature.rb +++ b/app/models/signature.rb @@ -1,7 +1,8 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ class Signature < ApplicationModel - has_many :groups, after_add: :cache_update, after_remove: :cache_update - validates :name, presence: true - latest_change_support + include LatestChangeObserved + + has_many :groups, after_add: :cache_update, after_remove: :cache_update + validates :name, presence: true end diff --git a/app/models/sla.rb b/app/models/sla.rb index ebbb3ad08..0c31f48b0 100644 --- a/app/models/sla.rb +++ b/app/models/sla.rb @@ -1,6 +1,8 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ class Sla < ApplicationModel + include NotifiesClients + load 'sla/assets.rb' include Sla::Assets @@ -8,7 +10,4 @@ class Sla < ApplicationModel store :data validates :name, presence: true belongs_to :calendar - - notify_clients_support - end diff --git a/app/models/sla/assets.rb b/app/models/sla/assets.rb index abcd257e3..736e38594 100644 --- a/app/models/sla/assets.rb +++ b/app/models/sla/assets.rb @@ -33,7 +33,7 @@ returns data[ app_model_user ] = {} end if !data[ app_model_sla ][ id ] - data[ app_model_sla ][ id ] = attributes_with_associations + data[ app_model_sla ][ id ] = attributes_with_association_ids data = assets_of_selector('condition', data) if calendar_id calendar = Calendar.lookup(id: calendar_id) diff --git a/app/models/template.rb b/app/models/template.rb index dccc33415..70227e0a9 100644 --- a/app/models/template.rb +++ b/app/models/template.rb @@ -1,7 +1,8 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ class Template < ApplicationModel - store :options - validates :name, presence: true - notify_clients_support + include NotifiesClients + + store :options + validates :name, presence: true end diff --git a/app/models/text_module.rb b/app/models/text_module.rb index c86d8f1e0..e95c8689c 100644 --- a/app/models/text_module.rb +++ b/app/models/text_module.rb @@ -1,9 +1,10 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ class TextModule < ApplicationModel - validates :name, presence: true - validates :content, presence: true - notify_clients_support + include NotifiesClients + + validates :name, presence: true + validates :content, presence: true =begin diff --git a/app/models/ticket.rb b/app/models/ticket.rb index 6ea7ddba2..5fd9233e9 100644 --- a/app/models/ticket.rb +++ b/app/models/ticket.rb @@ -1,68 +1,57 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ class Ticket < ApplicationModel + include LogsActivityStream + include NotifiesClients + include LatestChangeObserved + include Historisable + include SearchIndexed + include Ticket::Escalation include Ticket::Subject load 'ticket/permission.rb' include Ticket::Permission load 'ticket/assets.rb' include Ticket::Assets - load 'ticket/history_log.rb' - include Ticket::HistoryLog - load 'ticket/activity_stream_log.rb' - include Ticket::ActivityStreamLog load 'ticket/search_index.rb' include Ticket::SearchIndex extend Ticket::Search - store :preferences - before_create :check_generate, :check_defaults, :check_title, :check_escalation_update - before_update :check_defaults, :check_title, :reset_pending_time, :check_escalation_update - before_destroy :destroy_dependencies + store :preferences + before_create :check_generate, :check_defaults, :check_title, :check_escalation_update + before_update :check_defaults, :check_title, :reset_pending_time, :check_escalation_update + before_destroy :destroy_dependencies validates :group_id, presence: true validates :priority_id, presence: true validates :state_id, presence: true - notify_clients_support + activity_stream_permission 'ticket.agent' - latest_change_support + activity_stream_attributes_ignored :organization_id, # organization_id will channge automatically on user update + :create_article_type_id, + :create_article_sender_id, + :article_count, + :first_response_at, + :first_response_escalation_at, + :first_response_in_min, + :first_response_diff_in_min, + :close_at, + :close_escalation_at, + :close_in_min, + :close_diff_in_min, + :update_escalation_at, + :update_in_min, + :update_diff_in_min, + :last_contact_at, + :last_contact_agent_at, + :last_contact_customer_at, + :preferences - activity_stream_support( - permission: 'ticket.agent', - ignore_attributes: { - organization_id: true, # organization_id will channge automatically on user update - create_article_type_id: true, - create_article_sender_id: true, - article_count: true, - first_response_at: true, - first_response_escalation_at: true, - first_response_in_min: true, - first_response_diff_in_min: true, - close_at: true, - close_escalation_at: true, - close_in_min: true, - close_diff_in_min: true, - update_escalation_at: true, - update_in_min: true, - update_diff_in_min: true, - last_contact_at: true, - last_contact_agent_at: true, - last_contact_customer_at: true, - preferences: true, - } - ) - - history_support( - ignore_attributes: { - create_article_type_id: true, - create_article_sender_id: true, - article_count: true, - preferences: true, - } - ) - - search_index_support + history_attributes_ignored :create_article_type_id, + :create_article_sender_id, + :article_count, + :preferences belongs_to :group, class_name: 'Group' has_many :articles, class_name: 'Ticket::Article', after_add: :cache_update, after_remove: :cache_update @@ -861,6 +850,27 @@ result Ticket::Article.where(ticket_id: id).order(:created_at, :id) end + def history_get(fulldata = false) + list = History.list(self.class.name, self['id'], 'Ticket::Article') + return list if !fulldata + + # get related objects + assets = {} + list.each { |item| + record = Kernel.const_get(item['object']).find(item['o_id']) + assets = record.assets(assets) + + if item['related_object'] + record = Kernel.const_get(item['related_object']).find( item['related_o_id']) + assets = record.assets(assets) + end + } + { + history: list, + assets: assets, + } + end + private def check_generate @@ -915,5 +925,4 @@ result # destroy online notifications OnlineNotification.remove(self.class.to_s, id) end - end diff --git a/app/models/ticket/activity_stream_log.rb b/app/models/ticket/activity_stream_log.rb deleted file mode 100644 index 939d647dd..000000000 --- a/app/models/ticket/activity_stream_log.rb +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ -module Ticket::ActivityStreamLog - -=begin - -log activity for this object - - ticket = Ticket.find(123) - result = ticket.activity_stream_log('create', user_id) - -returns - - result = true # false - -=end - - def activity_stream_log(type, user_id) - - # return if we run import mode - return if Setting.get('import_mode') - - # return if we run on init mode - return if !Setting.get('system_init_done') - - return if !self.class.activity_stream_support_config - permission = self.class.activity_stream_support_config[:permission] - ActivityStream.add( - o_id: self['id'], - type: type, - object: self.class.name, - group_id: self['group_id'], - permission: permission, - created_at: updated_at, - created_by_id: user_id, - ) - end -end diff --git a/app/models/ticket/article.rb b/app/models/ticket/article.rb index f859ac11d..979914b66 100644 --- a/app/models/ticket/article.rb +++ b/app/models/ticket/article.rb @@ -1,11 +1,11 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ class Ticket::Article < ApplicationModel + include LogsActivityStream + include NotifiesClients + include Historisable + load 'ticket/article/assets.rb' include Ticket::Article::Assets - load 'ticket/article/history_log.rb' - include Ticket::Article::HistoryLog - load 'ticket/article/activity_stream_log.rb' - include Ticket::Article::ActivityStreamLog belongs_to :ticket belongs_to :type, class_name: 'Ticket::Article::Type' @@ -16,28 +16,19 @@ class Ticket::Article < ApplicationModel before_create :check_subject, :check_message_id_md5 before_update :check_subject, :check_message_id_md5 - notify_clients_support + activity_stream_permission 'ticket.agent' - activity_stream_support( - permission: 'ticket.agent', - ignore_attributes: { - type_id: true, - sender_id: true, - preferences: true, - } - ) + activity_stream_attributes_ignored :type_id, + :sender_id, + :preferences - history_support( - ignore_attributes: { - type_id: true, - sender_id: true, - preferences: true, - message_id: true, - from: true, - to: true, - cc: true, - } - ) + history_attributes_ignored :type_id, + :sender_id, + :preferences, + :message_id, + :from, + :to, + :cc # fillup md5 of message id to search easier on very long message ids def check_message_id_md5 @@ -220,16 +211,32 @@ returns: subject.gsub!(/\s|\t|\r/, ' ') end + def history_log_attributes + { + related_o_id: self['ticket_id'], + related_history_object: 'Ticket', + } + end + + # callback function to overwrite + # default history stream log attributes + # gets called from activity_stream_log + def activity_stream_log_attributes + { + group_id: Ticket.find(ticket_id).group_id, + } + end + class Flag < ApplicationModel end class Sender < ApplicationModel + include LatestChangeObserved validates :name, presence: true - latest_change_support end class Type < ApplicationModel + include LatestChangeObserved validates :name, presence: true - latest_change_support end end diff --git a/app/models/ticket/article/activity_stream_log.rb b/app/models/ticket/article/activity_stream_log.rb deleted file mode 100644 index 8650b16a3..000000000 --- a/app/models/ticket/article/activity_stream_log.rb +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ -module Ticket::Article::ActivityStreamLog - -=begin - -log activity for this object - - article = Ticket::Article.find(123) - result = article.activity_stream_log('create', user_id) - -returns - - result = true # false - -=end - - def activity_stream_log(type, user_id) - - # return if we run import mode - return if Setting.get('import_mode') - - # return if we run on init mode - return if !Setting.get('system_init_done') - - return if !self.class.activity_stream_support_config - permission = self.class.activity_stream_support_config[:permission] - ticket = Ticket.lookup(id: ticket_id) - ActivityStream.add( - o_id: self['id'], - type: type, - object: self.class.name, - group_id: ticket.group_id, - permission: permission, - created_at: updated_at, - created_by_id: user_id, - ) - end -end diff --git a/app/models/ticket/article/history_log.rb b/app/models/ticket/article/history_log.rb deleted file mode 100644 index 72b324d9c..000000000 --- a/app/models/ticket/article/history_log.rb +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ - -module Ticket::Article::HistoryLog - -=begin - -create log activity for this article - - article = Ticket::Article.find(123) - result = article.history_create( 'created', user_id ) - -returns - - result = true # false - -=end - - def history_log (type, user_id, data = {}) - - # if Ticketdata[:data[:Article has changed, remember related ticket to be able - # to show article changes in ticket history - data[:o_id] = self['id'] - data[:history_type] = type - data[:history_object] = self.class.name - data[:related_o_id] = self['ticket_id'] - data[:related_history_object] = 'Ticket' - data[:created_by_id] = user_id - data[:updated_at] = updated_at - data[:created_at] = updated_at - History.add(data) - end -end diff --git a/app/models/ticket/assets.rb b/app/models/ticket/assets.rb index 6cb5ade65..73e6b3be6 100644 --- a/app/models/ticket/assets.rb +++ b/app/models/ticket/assets.rb @@ -29,7 +29,7 @@ returns data[ app_model_ticket ] = {} end if !data[ app_model_ticket ][ id ] - data[ app_model_ticket ][ id ] = attributes_with_associations + data[ app_model_ticket ][ id ] = attributes_with_association_ids end %w(created_by_id updated_by_id owner_id customer_id).each { |local_user_id| next if !self[ local_user_id ] diff --git a/app/models/ticket/history_log.rb b/app/models/ticket/history_log.rb deleted file mode 100644 index e19a7ec48..000000000 --- a/app/models/ticket/history_log.rb +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ -module Ticket::HistoryLog - -=begin - -create log activity for this ticket - - ticket = Ticket.find(123) - result = ticket.history_create('created', user_id) - -returns - - result = true # false - -=end - - def history_log(type, user_id, data = {}) - data[:o_id] = self['id'] - data[:history_type] = type - data[:history_object] = self.class.name - data[:related_o_id] = nil - data[:related_history_object] = nil - data[:created_by_id] = user_id - data[:updated_at] = updated_at - data[:created_at] = updated_at - History.add(data) - end - -=begin - -get log activity for this ticket - - ticket = Ticket.find(123) - result = ticket.history_get() - -returns - - result = [ - { - type: 'created', - object: 'Ticket', - created_by_id: 3, - created_at: "2013-08-19 20:41:33", - }, - { - type: 'updated', - object: 'Ticket', - attribute: 'priority', - o_id: 1, - id_to: 3, - id_from: 2, - value_from: "low", - value_to: "high", - created_by_id: 3, - created_at: "2013-08-19 20:41:33", - }, - ] - -=end - - def history_get(fulldata = false) - list = History.list(self.class.name, self['id'], 'Ticket::Article') - return list if !fulldata - - # get related objects - assets = {} - list.each { |item| - record = Kernel.const_get(item['object']).find(item['o_id']) - assets = record.assets(assets) - - if item['related_object'] - record = Kernel.const_get(item['related_object']).find( item['related_o_id']) - assets = record.assets(assets) - end - } - { - history: list, - assets: assets, - } - end -end diff --git a/app/models/ticket/state.rb b/app/models/ticket/state.rb index 7059a33b6..ebdb4e749 100644 --- a/app/models/ticket/state.rb +++ b/app/models/ticket/state.rb @@ -1,11 +1,11 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ class Ticket::State < ApplicationModel + include LatestChangeObserved + belongs_to :state_type, class_name: 'Ticket::StateType' belongs_to :next_state, class_name: 'Ticket::State' validates :name, presence: true - latest_change_support - =begin list tickets by customer diff --git a/app/models/ticket/state_type.rb b/app/models/ticket/state_type.rb index b35f16fd8..94278ace3 100644 --- a/app/models/ticket/state_type.rb +++ b/app/models/ticket/state_type.rb @@ -1,6 +1,7 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ class Ticket::StateType < ApplicationModel - has_many :states, class_name: 'Ticket::State' - validates :name, presence: true - latest_change_support + include LatestChangeObserved + + has_many :states, class_name: 'Ticket::State' + validates :name, presence: true end diff --git a/app/models/user.rb b/app/models/user.rb index d35ba3ede..cb75eb530 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -24,6 +24,11 @@ require 'digest/md5' # @property active [Boolean] The flag that shows the active state of the User. # @property note [String] The note or comment stored to the User. class User < ApplicationModel + include LogsActivityStream + include NotifiesClients + include Historisable + include SearchIndexed + load 'user/permission.rb' include User::Permission load 'user/assets.rb' @@ -38,7 +43,6 @@ class User < ApplicationModel after_create :avatar_for_email_check after_update :avatar_for_email_check after_destroy :avatar_destroy - notify_clients_support has_and_belongs_to_many :groups, after_add: :cache_update, after_remove: :cache_update, class_name: 'Group' has_and_belongs_to_many :roles, after_add: [:cache_update, :check_notifications], after_remove: :cache_update, class_name: 'Role' @@ -50,35 +54,31 @@ class User < ApplicationModel store :preferences - activity_stream_support( - permission: 'admin.user', - ignore_attributes: { - last_login: true, - login_failed: true, - image: true, - image_source: true, - preferences: true, - } - ) - history_support( - ignore_attributes: { - password: true, - image: true, - image_source: true, - preferences: true, - } - ) - search_index_support( - ignore_attributes: { - password: true, - image: true, - image_source: true, - source: true, - login_failed: true, - preferences: true, - }, - ignore_ids: [1], - ) + activity_stream_permission 'admin.user' + + activity_stream_attributes_ignored :last_login, + :login_failed, + :image, + :image_source, + :preferences + + history_attributes_ignored :password, + :image, + :image_source, + :preferences + + search_index_attributes_ignored :password, + :image, + :image_source, + :source, + :login_failed, + :preferences + + def ignore_search_indexing?(_action) + # ignore internal user + return true if id == 1 + false + end =begin diff --git a/app/models/user/assets.rb b/app/models/user/assets.rb index ba2aee81e..59d12abc2 100644 --- a/app/models/user/assets.rb +++ b/app/models/user/assets.rb @@ -29,7 +29,7 @@ returns data[ app_model ] = {} end if !data[ app_model ][ id ] - local_attributes = attributes_with_associations + local_attributes = attributes_with_association_ids # do not transfer crypted pw local_attributes['password'] = '' diff --git a/lib/background_job_search_index.rb b/lib/background_job_search_index.rb new file mode 100644 index 000000000..e05833cba --- /dev/null +++ b/lib/background_job_search_index.rb @@ -0,0 +1,21 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ +class BackgroundJobSearchIndex + def initialize(object, o_id) + @object = object + @o_id = o_id + end + + def perform + record = @object.constantize.lookup(id: @o_id) + return if !exists?(record) + record.search_index_update_backend + end + + private + + def exists?(record) + return true if record + Rails.logger.info "Can't index #{@object}.lookup(id: #{@o_id}), no such record found" + false + end +end diff --git a/lib/models.rb b/lib/models.rb index 9e7051ea7..ce4dcffca 100644 --- a/lib/models.rb +++ b/lib/models.rb @@ -32,6 +32,8 @@ returns next if entry =~ %r{channel/}i next if entry =~ %r{observer/}i next if entry =~ %r{store/provider/}i + next if entry =~ %r{models/concerns/}i + entry.gsub!(dir, '') entry = entry.to_classname model_class = load_adapter(entry) diff --git a/lib/sessions/backend/base.rb b/lib/sessions/backend/base.rb index 0c693c220..96a14721c 100644 --- a/lib/sessions/backend/base.rb +++ b/lib/sessions/backend/base.rb @@ -1,4 +1,5 @@ class Sessions::Backend::Base + def initialize(user, asset_lookup, client, client_id, ttl = 30) @user = user @client = client diff --git a/lib/sessions/backend/collections/base.rb b/lib/sessions/backend/collections/base.rb index 6cd72f122..d85c5dd14 100644 --- a/lib/sessions/backend/collections/base.rb +++ b/lib/sessions/backend/collections/base.rb @@ -47,7 +47,7 @@ class Sessions::Backend::Collections::Base < Sessions::Backend::Base # get relations of data all = [] items.each { |item| - all.push item.attributes_with_associations + all.push item.attributes_with_association_ids } # collect assets diff --git a/test/unit/assets_test.rb b/test/unit/assets_test.rb index 0b5144679..1c621ba07 100644 --- a/test/unit/assets_test.rb +++ b/test/unit/assets_test.rb @@ -55,12 +55,12 @@ class AssetsTest < ActiveSupport::TestCase assets = user3.assets({}) org1 = Organization.find(org1.id) - attributes = org1.attributes_with_associations + attributes = org1.attributes_with_association_ids attributes.delete('user_ids') assert( diff(attributes, assets[:Organization][org1.id]), 'check assets') user1 = User.find(user1.id) - attributes = user1.attributes_with_associations + attributes = user1.attributes_with_association_ids attributes['accounts'] = {} attributes['password'] = '' attributes.delete('token_ids') @@ -68,7 +68,7 @@ class AssetsTest < ActiveSupport::TestCase assert( diff(attributes, assets[:User][user1.id]), 'check assets' ) user2 = User.find(user2.id) - attributes = user2.attributes_with_associations + attributes = user2.attributes_with_association_ids attributes['accounts'] = {} attributes['password'] = '' attributes.delete('token_ids') @@ -76,7 +76,7 @@ class AssetsTest < ActiveSupport::TestCase assert( diff(attributes, assets[:User][user2.id]), 'check assets' ) user3 = User.find(user3.id) - attributes = user3.attributes_with_associations + attributes = user3.attributes_with_association_ids attributes['accounts'] = {} attributes['password'] = '' attributes.delete('token_ids') @@ -89,12 +89,12 @@ class AssetsTest < ActiveSupport::TestCase org2.note = "some note...#{rand(9_999_999_999_999)}" org2.save - attributes = org2.attributes_with_associations + attributes = org2.attributes_with_association_ids attributes.delete('user_ids') assert( !diff(attributes, assets[:Organization][org2.id]), 'check assets' ) user1_new = User.find(user1.id) - attributes = user1_new.attributes_with_associations + attributes = user1_new.attributes_with_association_ids attributes['accounts'] = {} attributes['password'] = '' attributes.delete('token_ids') @@ -103,12 +103,12 @@ class AssetsTest < ActiveSupport::TestCase # check new assets lookup assets = user3.assets({}) - attributes = org2.attributes_with_associations + attributes = org2.attributes_with_association_ids attributes.delete('user_ids') assert( diff(attributes, assets[:Organization][org1.id]), 'check assets') user1 = User.find(user1.id) - attributes = user1.attributes_with_associations + attributes = user1.attributes_with_association_ids attributes['accounts'] = {} attributes['password'] = '' attributes.delete('token_ids') @@ -116,7 +116,7 @@ class AssetsTest < ActiveSupport::TestCase assert( diff(attributes, assets[:User][user1.id]), 'check assets' ) user2 = User.find(user2.id) - attributes = user2.attributes_with_associations + attributes = user2.attributes_with_association_ids attributes['accounts'] = {} attributes['password'] = '' attributes.delete('token_ids') @@ -124,7 +124,7 @@ class AssetsTest < ActiveSupport::TestCase assert( diff(attributes, assets[:User][user2.id]), 'check assets' ) user3 = User.find(user3.id) - attributes = user3.attributes_with_associations + attributes = user3.attributes_with_association_ids attributes['accounts'] = {} attributes['password'] = '' attributes.delete('token_ids') @@ -202,12 +202,12 @@ class AssetsTest < ActiveSupport::TestCase org = Organization.find(org.id) assets = org.assets({}) - attributes = org.attributes_with_associations + attributes = org.attributes_with_association_ids attributes.delete('user_ids') assert( diff(attributes, assets[:Organization][org.id]), 'check assets' ) admin1 = User.find(admin1.id) - attributes = admin1.attributes_with_associations + attributes = admin1.attributes_with_association_ids attributes['accounts'] = {} attributes['password'] = '' attributes.delete('token_ids') @@ -215,7 +215,7 @@ class AssetsTest < ActiveSupport::TestCase assert( diff(attributes, assets[:User][admin1.id]), 'check assets' ) user1 = User.find(user1.id) - attributes = user1.attributes_with_associations + attributes = user1.attributes_with_association_ids attributes['accounts'] = {} attributes['password'] = '' attributes.delete('token_ids') @@ -223,7 +223,7 @@ class AssetsTest < ActiveSupport::TestCase assert( diff(attributes, assets[:User][user1.id]), 'check assets' ) user2 = User.find(user2.id) - attributes = user2.attributes_with_associations + attributes = user2.attributes_with_association_ids attributes['accounts'] = {} attributes['password'] = '' attributes.delete('token_ids') @@ -231,7 +231,7 @@ class AssetsTest < ActiveSupport::TestCase assert( diff(attributes, assets[:User][user2.id]), 'check assets' ) user3 = User.find(user3.id) - attributes = user3.attributes_with_associations + attributes = user3.attributes_with_association_ids attributes['accounts'] = {} attributes['password'] = '' attributes.delete('token_ids') @@ -245,11 +245,11 @@ class AssetsTest < ActiveSupport::TestCase user_new_2.save org_new = Organization.find(org.id) - attributes = org_new.attributes_with_associations + attributes = org_new.attributes_with_association_ids attributes.delete('user_ids') assert( !diff(attributes, assets[:Organization][org_new.id]), 'check assets' ) - attributes = user_new_2.attributes_with_associations + attributes = user_new_2.attributes_with_association_ids attributes['accounts'] = {} attributes['password'] = '' attributes.delete('token_ids') @@ -258,11 +258,11 @@ class AssetsTest < ActiveSupport::TestCase # check new assets lookup assets = org_new.assets({}) - attributes = org_new.attributes_with_associations + attributes = org_new.attributes_with_association_ids attributes.delete('user_ids') assert( diff(attributes, assets[:Organization][org_new.id]), 'check assets' ) - attributes = user_new_2.attributes_with_associations + attributes = user_new_2.attributes_with_association_ids attributes['accounts'] = {} attributes['password'] = '' attributes.delete('token_ids')