require 'csv'
module Edgarj
# Generic CRUD(Create/Read/Update/Delte) controller with Ajax.
#
# === EdgarjController v.s. ApplicationController
# When concreate controller (e.g. CustomersController) inherits
# EdgarjController, it has the following features:
#
# * CRUD with default form on Ajax
# * form can be customized.
# * QBE (Query By Example) search form
# * popup-selection on 'belongs_to' column
# * sort on list
# * saving search-conditions and reuse it
#
# If these are not necessary, just inherits ApplicationController.
#
# === Tasks when adding new model which is handled under EdgarjController
# For example, when want to add Post model:
# 1. generate edgarj:scaffold:
# rails generate edgarj:scaffold post name:string body:text published:boolean
# 2. add controller entry to CONTROLLER_MENU (config/initializers/edgarj.rb)
#
# It will take about ~3 min.
#
# For the detail of customization, please see:
# http://sourceforge.net/apps/trac/jjedgarj/wiki/customize
#
# === Architecture
# see {architecture}[link:../architecture.odp] (OpenOffice Presentation)
#
# === Access Control
# There are two dimentions of access control:
# 1. Page level (Controller level) access control.
# * Edgarj::UserGroup with kind==ROLE and Edgarj::ModelPermission
# represents access control on each controller.
# * Admin user, who belongs to 'admin' user_group, can access any page.
# * When a user belongs to a user_group (kind==ROLE) and
# a model_permission belongs to the user_group, the user can
# access the controller which model is the model in model_permission.
# * More precisely, 4 kind of access controls, CREATE,
# READ, UPDATE, and DELETE can be set with any conbination on the
# controller.
# * See Edgarj::ModelPermission for more detail.
# 1. Data scope.
# * data scope access is controlled by 'user_scoped(user, context)'
# scope defined at each model.
# * Where, user is currently accessing person to the model.
# * context is any kind of 2nd parameter. Default is session object of
# Edgarj::Sssn, but it can be overwritten 'scope_context' method
# at the target controller.
# * See Author.user_scoped as an example.
#
# == Naming Convention
#
# * 'record' is an instance of 'Model'.
# * 'drawer' is an instance of 'Drawer' class.
#
# === Implementation Note
# Why not to choose mixin rather than class is because
# it is easier to use edgarj's view at client controller. For example,
# AuthorsController, which inherits from EdgarjController, can
# automatically use edgarj's view by rails view-selection feature.
#
# === SEE ALSO
# PopupController:: 'belongs_to' popup for EdgarjController
#
class EdgarjController < ApplicationController
include ControllerMixinCommon
include PermissionMixin
#include TopicPathControllerMixin
helper_method :model, :model_name, :drawer,
:draw_flash, :csv_proc
READ_ACTIONS = %w(
index show clear search search_clear search_save search_load
zip_complete csv_download file_download map page_info_save)
before_filter :require_create_permission, only: :create
before_filter :require_read_permission, only: READ_ACTIONS
before_filter :require_update_permission, only: :update
before_filter :require_delete_permission, only: :destroy
before_filter :require_other_permission, except: READ_ACTIONS + %w(
create update destroy top)
#after_filter :log_topic_path
after_filter :enum_cache_stat
# This page is for following purposes:
#
# * top page which contain latest info (TBD)
# * any error message on HTML format access
# * on Ajax access, rather edgarj_error_popup is used
def top
render :action => 'top'
end
# draw search result in whole page.
# default update DOM id and template is as follows:
#
# DOM id:: 'edgarj_list'
# template:: 'edgarj/list'
#
# However, these can be replaced by params[:update] and params[:template]
#
# === Permission
# ModelPermission::READ on this controller is required.
#
# === SEE ALSO
# popup():: draw popup
def index
page_info
# update @page_info.page when page is specified.
# Otherwise, reset page to 1.
#
# Just set, not save. It will be done later when saving sssn with
# 'has_many page_infos ... autosave: true'
@page_info.page = (params[:page] || 1)
#clear_topic_path
prepare_list
@search = page_info.record
@record = model.new
end
# save new record
#
# === Permission
# ModelPermission::CREATE on this controller is required.
#
def create
upsert do
# NOTE: create!() is not used because assign to @record to draw form.
# Otherwise, @record would be in nil so failure at edgarj/_form rendering.
#
# NOTE2: valid? after create() calls validate_on_update. This is not
# an expected behavior. So, new, valid?, then save.
@record = model.new(permitted_params(:create))
@record_saved = @record # copy for possible later use
on_upsert
#upsert_files
raise ActiveRecord::RecordNotSaved if !@record.valid?
@record.save
# clear @record values for next data-entry
@record = model.new
end
end
# Show detail of one record. Format of html & js should be supported.
#
# === Permission
# ModelPermission::READ on this controller is required.
#
def show
@record = user_scoped.find(params[:id])
#add_topic_path
respond_to do |format|
format.html {
prepare_list
@search = page_info.record
render :action=>'index'
}
format.js
end
end
# save existence modified record
#
# === Permission
# ModelPermission::UPDATE on this controller is required.
#
def update
upsert do
# NOTE:
# 1. update ++then++ valid to set new values in @record to draw form.
# 1. user_scoped may have joins so that record could be
# ActiveRecord::ReadOnlyRecord so that's why access again from
# model.
@record = model.find(user_scoped.find(params[:id]).id)
#upsert_files
if !@record.update_attributes(permitted_params(:update))
raise ActiveRecord::RecordInvalid.new(@record)
end
end
end
# === Permission
# ModelPermission::DELETE on this controller is required.
def destroy
m = model.find(user_scoped.find(params[:id]).id)
@record_saved = m # copy for possible later use
m.destroy
prepare_list
@record = model.new
@flash_notice = t('delete_ok')
end
# Ajax method to clear form
#
# === Permission
# ModelPermission::READ on this controller is required.
#
def clear
@record = model.new
end
# Ajax method to execute search
#
# Actually, this doesn't execute search. Rather, this just saves
# condition. Search will be executed at any listing action
# like 'index', 'create', or 'update'.
#
# === Permission
# ModelPermission::READ on this controller is required.
#
def search
pi = page_info
pi.record = SearchForm.new(model, params[:edgarj_search_form])
pi.page = 1
pi.save!
@search = pi.record
prepare_list if @search.valid?
end
# Ajax method to clear search conditions
#
# === Permission
# ModelPermission::READ on this controller is required.
#
def search_clear
@search = SearchForm.new(model)
end
# Ajax method to save search conditions
#
# === call flow
# Edgarj.SearchSavePopup.open() (javascript)
# (show $('search_save_popup'))
# Edgarj.SearchSavePopup.submit() (javascript)
# (copy entered name into $('saved_page_info_name') in form)
# call :action=>'search_save'
#
# ==== TRICKY PART
# There are two requirements:
# 1. use modal-dialog to enter name to decrese busy in search form.
# 1. send Search Form with name to server.
#
# To comply these, Edgarj.SearchSavePopup copies the entered name to
# 'saved_page_info_name' hidden field and then sends the form which includes
# the copied name.
#
# === Permission
# ModelPermission::READ on this controller is required.
#
def search_save
SavedVcontext.save(current_user, nil,
params[:saved_page_info_name], page_info)
render :update do |page|
page << "Edgarj.SearchSavePopup.close();"
page.replace 'edgarj_load_condition_menu',
:partial=>'edgarj/load_condition_menu'
end
rescue ActiveRecord::ActiveRecordError
app_rescue
render :update do |page|
page.replace_html 'search_save_popup_flash_error', :text=>t('save_fail')
end
end
# Ajax method to load search condition, lines, order_by, dir, and page.
#
def search_load
@search = current_user.saved_page_infos.find(params[:id]).load(@sssn).model
draw_search_form
end
# zip -> address completion
#
# === INPUTS
# params[:zip]:: key to find address info. hyphen is supported.
# params[:adrs_prefix]:: address fields DOM id prefix. e.g. 'org_adrs_0_'
#
# === call flow
# ==== on drawing
# EdgarjHelper.draw_adrs() app/helpers/edgarj_helper.rb
# app/views/edgarj/_address.html.erb
# Example:
# :
#
# :
#
# ==== on clicking zip->address button
# Edgarj.zip_complete() public/javascripts/edgarj.js
# Ajax.Request()
# EdgarjController.zip_complete app/controllers/edgarj_controller.rb
# inline RJS to fill address info
#
=begin
def zip_complete
zip = params[:zip].gsub(/\D/, '')
@address = ZipAddress.find_by_zip(zip) || ZipAddress.new(
:prefecture => '?',
:city => '',
:other => '')
# sanitize
@adrs_prefix = params[:adrs_prefix].gsub(/[^a-zA-Z_0-9]/, '')
end
=end
# download model under current condition
#
# respond_to...format.csv approach was not used since
# \@list is different as follows:
# * csv returns all of records under the conditions
# * HTML returns *just* in specified 'page'.
#
# === Permission
# ModelPermission::READ on this controller is required.
#
# FIXME: file.close(true) deletes files *BEFORE* actually send file
# so that comment it out. Need to clean these work files.
def csv_download
filename = sprintf("%s-%s.csv",
model_name,
Time.now.strftime("%Y%m%d-%H%M%S"))
file = Tempfile.new(filename, Settings.edgarj.csv_dir)
csv_visitor = EdgarjHelper::CsvVisitor.new(view_context)
file.write CSV.generate_line(model.columns.map{|c| c.name})
for rec in user_scoped.where(page_info.record.conditions).
order(
page_info.order_by.blank? ?
nil :
page_info.order_by + ' ' + page_info.dir) do
array = []
for col in model.columns do
array << csv_visitor.visit_column(rec, col)
end
file.write CSV.generate_line(array)
end
file.close
File.chmod(Settings.edgarj.csv_permission, file.path)
send_file(file.path, {
type: 'text/csv',
filename: filename})
#file.close(true)
end
# To prevent unassociated file access, do:
#
# 1. check if it is in model object
# 1. check if it is a edgarj_file column
#
# === Permission
# ModelPermission::READ on this controller is required.
def file_download
if !model.edgarj_file?(params[:column])
flash[:error] = t('edgarj_file.no_assoc')
return
end
file_info_id = user_scoped.find(params[:id]).send(params[:column])
if file_info_id
file_info = FileInfo.find(file_info_id)
if file_info
send_file(file_info.full_filename, :filename => file_info.filename)
return
end
end
logger.warn 'invalid file_info'
end
# draw Google map.
#
# === Permission
# ModelPermission::READ on this controller is required.
#
def map
render :template=>'edgarj/map', :layout=>false
end
private
# derive model class from this controller.
#
# If controller cannot guess model class, overwrite this.
#
# === Examples:
# * AuthorsController -> Author
# * UsersController -> User
def model
@_model ||=
if self.class == Edgarj::EdgarjController
# dummy model for 'top' action:
Edgarj::Sssn
else
self.class.name.gsub(/Controller$/, '').singularize.constantize
end
end
# return model name.
#
# if each concreate controller cannot guess model name from its
# controller name, overwrite this.
#
# === Examples:
# * UsersController -> 'edgarj_user'
# * AuthorsController -> 'author'
#
def model_name
@_model_name ||= ActiveModel::Naming.param_key(model.new)
end
# return permitted params. Default is all.
#
# Derived Controller *MUST* customize this.
def permitted_params(action, kind=nil)
params.require(model_name).permit!
end
# return search-form object.
#
# called from page_info
def search_form
SearchForm.new(model)
end
# derive drawer class from this controller.
#
# If controller cannot guess drawer class, overwrite this.
#
# === Examples:
# * AuthorsController -> AuthorDrawer
# * UsersController -> UserDrawer
def drawer_class
@_drawer_cache ||=
if self.class == Edgarj::EdgarjController
Edgarj::Drawer::Normal
else
(self.class.name.gsub(/Controller$/, '').singularize +
'Drawer').constantize
end
end
# set drawer instance as drawer for later use on rendering view
def set_drawer
@drawer = drawer_class.new(view_context, params, page_info, model)
end
def drawer
@drawer
end
# additional behavior on upsert (default does nothing).
#
# Derived controller may overwrite this method if necessary, for example:
# * to upsert protected attributes.
# * to upsert server-side calculated values
def on_upsert
#
end
# update/insert common
def upsert(&block)
ActiveRecord::Base.transaction do
yield
@flash_notice = t('save_ok')
end
prepare_list
rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid => ex
logger.info "#{ex.class.to_s}: #{@record.class.to_s}: #{@record.errors.full_messages.join(' / ')}"
app_rescue
@flash_error = t('save_fail')
end
# At public action, just set any message into @flash_notice and/or
# \@flash_error. Then, any render in EdgarjController will display it
# by calling draw_flash().
#
# draw_flash() must be a helper because it is called in render block, which
# is a kind of view.
def draw_flash(page)
page.replace_html 'flash_notice', :text=>@flash_notice
page.replace_html 'flash_error', :text=>@flash_error
end
# insert/update uploaded file and point to it via file_NN column
=begin
def upsert_files
#@record.upsert_files(params[:file_info], params[model_name])
end
# Since 'render :text=>proc...' cannot be tested at functional test
# (see http://lightyearsoftware.com/2009/10/rails-bug-with-render-text-proc-in-tests/),
# move the logic inside the proc here to test
def csv_proc(output)
csv_visitor = EdgarjHelper::CsvVisitor.new(@template)
find_args = {:conditions=>page_info.record.conditions}
if !page_info.order_by.blank?
find_args.merge!(:order => page_info.order_by + ' ' + page_info.dir)
end
output.write CSV.generate_line(model.columns.map{|c| c.name}) + "\n"
for rec in model.find(:all, find_args) do
array = []
for col in model.columns do
array << csv_visitor.visit_column(rec, col)
end
output.write CSV.generate_line(array) + "\n"
end
end
=end
# This works as:
#
# before_render :set_drawer
#
# to set drawer just before rendering.
#
# See http://stackoverflow.com/questions/9281224/filter-to-execute-before-render-but-after-controller
#
def render(*args)
# set_drawer should be called only when finishing before_filters.
set_drawer if current_user
super
end
def enum_cache_stat
logger.debug 'EnumCache stat (hit/out/out_of_enum): ' +
EnumCache.instance.stat.inspect
end
end
end