Skip to content

Commit

Permalink
Merge pull request #561 from senid231/529-improve-admin-ui-cdr-export…
Browse files Browse the repository at this point in the history
…s-creation

Improve CDR exports creation via Admin UI
  • Loading branch information
dmitry-sinina authored Oct 29, 2019
2 parents c399462 + fb1863b commit 09cbd87
Show file tree
Hide file tree
Showing 12 changed files with 326 additions and 70 deletions.
85 changes: 66 additions & 19 deletions app/admin/cdr/cdr_exports.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,52 @@
column :status
column :rows_count
column :fields
column :filters
column :filters do |r|
r.filters.as_json
end
column :callback_url
column :created_at
column :updated_at
actions
end

# id :integer not null, primary key
# status :string not null
# fields :string default([]), not null, is an Array
# filters :json not null
# callback_url :string
# type :string not null
# created_at :datetime
# updated_at :datetime
# rows_count :integer
show do
columns do
column do
attributes_table do
row :id
row :status
row :callback_url
row :type
row :created_at
row :updated_at
row :rows_count
end
active_admin_comments
end

column do
panel 'Fields' do
ul do
resource.fields.each { |field| li field }
end
end
panel 'Filters' do
attributes_table_for(resource.filters, *CdrExport::FiltersModel.attribute_types.keys)
end
end
end
end

member_action :download do
response.headers['X-Accel-Redirect'] = "/x-redirect/cdr_export/#{resource.id}.csv"
response.headers['Content-Type'] = 'text/csv; charset=utf-8'
Expand All @@ -48,31 +87,39 @@

controller do
def build_new_resource
build_params = resource_params[0].to_h
return super unless build_params.any?

# build filters
filters = {}
filters['time_start_gteq'] = build_params['time_start_gteq']
filters['time_start_lteq'] = build_params['time_start_lteq']
filters['customer_acc_id_eq'] = build_params['customer_acc_id_eq'] if build_params['customer_acc_id_eq'].present?
filters['is_last_cdr_eq'] = build_params['is_last_cdr_eq'] == 'true' if build_params['is_last_cdr_eq'].present?
scoped_collection.send method_for_build, build_params.merge(filters: filters)
record = super
if params[:action] == 'new'
record.fields = CdrExport.last&.fields || []
end
record
end
end

permit_params :time_start_gteq, :time_start_lteq,
:customer_acc_id_eq, :is_last_cdr_eq, fields: []
permit_params filters: %i[time_start_gteq time_start_lteq customer_acc_id_eq is_last_cdr_eq],
fields: []

form do |f|
f.semantic_errors(*f.object.errors.keys)
f.inputs do
f.input :fields, as: :select, multiple: true, collection: CdrExport.allowed_fields, input_html: { class: 'chosen' }
f.input :fields,
as: :select,
multiple: true,
collection: CdrExport.allowed_fields,
input_html: { class: 'chosen' }
end
f.inputs('Filters') do
f.input 'time_start_gteq', as: :date_time_picker
f.input :time_start_lteq, as: :date_time_picker
f.input :customer_acc_id_eq, as: :select, collection: Account.order(:name), input_html: { class: 'chosen' }
f.input :is_last_cdr_eq, as: :select, collection: [['Any', nil], ['Yes', true], ['No', false]], input_html: { class: 'chosen' }
f.inputs 'Filters', for: [:filters, f.object.filters] do |ff|
ff.input :time_start_gteq, as: :date_time_picker
ff.input :time_start_lteq, as: :date_time_picker

ff.input :customer_acc_id_eq,
as: :select,
collection: Account.order(:name),
input_html: { class: 'chosen' }

ff.input :is_last_cdr_eq,
as: :select,
collection: [['Any', nil], ['Yes', true], ['No', false]],
input_html: { class: 'chosen' }
end
f.actions
end
Expand Down
52 changes: 52 additions & 0 deletions app/lib/json_attribute_model.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

class JsonAttributeModel
# Base class for json attribute.
# @see JsonAttributeType
# @see WithJsonAttributes#json_attribute

include ActiveModel::Model
include ActiveModel::Attributes

class << self
def column_names
attribute_types.keys.map(&:to_s)
end
end

# serializes model as hash of it's attributes
def as_json(options = {})
filled_attributes.with_indifferent_access.as_json(options)
end

# models are equal if classes and attributes values are the same
def ==(other)
return super unless other.is_a?(self.class)

attributes.all? { |name, value| value == other.send(name) }
end

# Allows to call :presence validation on the json_attribute itself
def blank?
attributes.values.all?(&:blank?)
end

def [](attr_name)
attribute(attr_name.to_sym)
end

def []=(attr_name, value)
write_attribute(attr_name.to_sym, value)
end

def inspect
attribute_string = filled_attributes.map { |name, value| "#{name}: #{value.inspect}" }.join(', ')
"#<#{self.class.name} #{attribute_string}>"
end

private

def filled_attributes
attributes.reject { |_, value| value.nil? }
end
end
46 changes: 26 additions & 20 deletions app/models/cdr_export.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,27 @@ class CdrExport < Yeti::ActiveRecord
self.table_name = 'cdr_exports'
self.store_full_sti_class = false

class FiltersModel < JsonAttributeModel
attribute :time_start_gteq, :db_datetime
attribute :time_start_lteq, :db_datetime
attribute :customer_acc_id_eq, :integer
attribute :is_last_cdr_eq, :boolean
attribute :success_eq, :boolean
attribute :customer_auth_external_id_eq, :integer
attribute :failed_resource_type_id_eq, :integer
attribute :src_prefix_in_contains, :string
attribute :dst_prefix_in_contains, :string
attribute :src_prefix_routing_contains, :string
attribute :dst_prefix_routing_contains, :string
attribute :customer_acc_external_id_eq, :integer

private

def write_attribute(attr_name, value)
super
end
end

STATUS_PENDING = 'Pending'
STATUS_COMPLETED = 'Completed'
STATUS_FAILED = 'Failed'
Expand All @@ -34,9 +55,11 @@ class CdrExport < Yeti::ActiveRecord
attr_accessor :customer_acc_id_eq,
:is_last_cdr_eq, :time_start_gteq, :time_start_lteq

json_attribute :filters, class_name: 'CdrExport::FiltersModel'

validates_presence_of :status, :fields, :filters
validate do
if filters['time_start_gteq'].empty? || filters['time_start_lteq'].empty?
if filters.time_start_gteq.nil? || filters.time_start_lteq.nil?
errors.add(:filters, 'requires time_start_lteq & time_start_gteq')
end
end
Expand All @@ -45,10 +68,6 @@ class CdrExport < Yeti::ActiveRecord
if extra_fields.any?
errors.add(:fields, "#{extra_fields.join(', ')} not allowed")
end
extra_filters = filters.keys - self.class.allowed_filters
if extra_filters.any?
errors.add(:filters, "#{extra_filters.join(', ')} not allowed")
end
end

def fields=(f)
Expand All @@ -72,7 +91,7 @@ def fields=(f)
alias_attribute :export_type, :type

def export_sql
Cdr::Cdr.select(fields.join(', ')).order('time_start desc').ransack(filters).result.to_sql
Cdr::Cdr.select(fields.join(', ')).order('time_start desc').ransack(filters.as_json).result.to_sql
end

def completed?
Expand All @@ -84,20 +103,7 @@ def deleted?
end

def self.allowed_filters
%w[
time_start_lteq
time_start_gteq
success_eq
customer_auth_external_id_eq
failed_resource_type_id_eq
src_prefix_in_contains
dst_prefix_in_contains
src_prefix_routing_contains
dst_prefix_routing_contains
customer_acc_external_id_eq
is_last_cdr_eq
customer_acc_id_eq
]
FiltersModel.attribute_types.keys.map(&:to_s)
end

# any cdr columns
Expand Down
43 changes: 43 additions & 0 deletions app/models/concerns/with_json_attributes.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

module WithJsonAttributes
# Wraps JSON column into a model.
# @see JsonAttributeModel
# @see JsonAttributeType
# Usage:
#
# class JsonAttributeModel::UserConfig < JsonAttributeModel::Base
# attribute :rate_limit, :integer
# attribute :max_per_page, :integer
#
# validates :rate_limit, numericality: { only_integer: true, greater_than_or_equal_to: 1, less_than: 6_000 }
# validates :max_per_page, numericality: { only_integer: true, greater_than_or_equal_to: 1 }
# end
#
# class User < ApplicationRecord
# include WithJsonAttributes
# json_attribute :config, class_name: 'JsonAttributeModel::UserConfig'
# end
#
# customer = customer.create(config: { rate_limit: 6_001, max_per_page: 10 })
# customer.persisted? # false
# customer.errors.messages # { 'config.rate_limit': ['must be less than 6000'] }
# customer.config.rate_limit = 600
# customer.save # true
# customer.where(id: customer.id).pluck(:config).first # "{\"rate_limit\":600,\"max_per_page\":10}"

extend ActiveSupport::Concern

class_methods do
# Defines json model attribute for provided column
# @param name [Symbol] - name of json column (required)
# @param class_name [String] - class name of corresponding model (required)
def json_attribute(name, class_name:)
attribute name, :json_object, class_name: class_name

define_method("build_#{name}") do |attributes = {}|
class_name.constantize.new(attributes)
end
end
end
end
2 changes: 2 additions & 0 deletions app/models/yeti/active_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
class Yeti::ActiveRecord < ActiveRecord::Base
self.abstract_class = true

include WithJsonAttributes

def self.array_belongs_to(name, class_name:, foreign_key:)
define_method(name) do
relation = class_name.is_a?(String) ? class_name.constantize : class_name
Expand Down
4 changes: 4 additions & 0 deletions app/resources/api/rest/admin/cdr/cdr_export_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ class Api::Rest::Admin::Cdr::CdrExportResource < BaseResource
:callback_url,
:export_type

def filters
_model.filters.as_json
end

def self.creatable_fields(_context)
%i[fields filters callback_url export_type]
end
Expand Down
3 changes: 3 additions & 0 deletions config/initializers/active_model_types.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# frozen_string_literal: true

require 'active_model_types/yeti_date_time_type'
require 'active_model_types/json_attribute_type'

ActiveModel::Type.register(:db_datetime, ActiveRecord::ConnectionAdapters::PostgreSQL::OID::DateTime)
59 changes: 59 additions & 0 deletions lib/active_model_types/json_attribute_type.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# frozen_string_literal: true

class JsonAttributeType < ActiveModel::Type::Value
# Type for JSON model attribute.
# @see JsonAttributeModel
# @see WithJsonAttributes#json_attribute

class CastError < StandardError
end

def initialize(class_name:)
@model_class_name = class_name
end

def type
:json
end

def cast_value(value)
case value
when String
decoded = begin
ActiveSupport::JSON.decode(value)
rescue StandardError
nil
end
model_class.new(decoded) unless decoded.nil?
when Hash
model_class.new(value)
when ActionController::Parameters
model_class.new(value.to_unsafe_h)
when model_class
value
else
raise CastError, "failed casting #{value.inspect}, only String, Hash or #{model_class} instances are allowed"
end
end

def serialize(value)
case value
when Hash, model_class
ActiveSupport::JSON.encode(value)
else
super
end
end

def changed_in_place?(raw_old_value, new_value)
cast_value(raw_old_value) != new_value
end

private

def model_class
@model_class ||= @model_class_name.constantize
end
end

ActiveRecord::Type.register :json_object, JsonAttributeType
Loading

0 comments on commit 09cbd87

Please sign in to comment.