Class: EdgarController

Inherits:
ApplicationController
  • Object
show all
Includes:
Edgar::ControllerCommon, Edgar::PermissionMixin, Edgar::RescueMixin
Defined in:
app/controllers/edgar_controller.rb

Overview

Generic CRUD(Create/Read/Update/Delte) controller with Ajax.

EdgarController v.s. ApplicationController

When concreate controller (e.g. CustomersController) inherits EdgarController, 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 EdgarController

For example, when want to add Post model:

  1. generate edgar_scaffold:

    ./script/generate edgar_scaffold post name:string body:text published:boolean
  2. add controller entry to CONTROLLER_MENU (config/initializers/edgar.rb)

It will take about ~3 min.

For the detail of customization, please see:

http://sourceforge.net/apps/trac/jjedgar/wiki/customize

Architecture

see architecture (OpenOffice Presentation)

Access Control

There are two dimentions of access control:

  1. Page level (Controller level) access control.

    • Edgar::UserGroup with kind==ROLE and Edgar::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_class 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 Edgar::ModelPermission for more detail.

  2. 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 Edgar::Sssn, but it can be overwritten 'scope_context' method at the target controller.

    • See Author.user_scoped as an example.

SEE ALSO

EdgarPopupController

'belongs_to' popup for EdgarController

Direct Known Subclasses

Edgar::AddressesController, Edgar::ModelPermissionsController, Edgar::SssnsController, Edgar::UserGroupUsersController, Edgar::UserGroupsController, Edgar::UsersController, Edgar::ZipAddressesController

Constant Summary

READ_ACTIONS =
%w(
index show clear search search_clear search_save search_load
zip_complete csv_download file_download map view_status_save)

Instance Method Summary (collapse)

Methods included from Edgar::RescueMixin

included, #rescue_404, #rescue_edgar_error

Methods included from Edgar::PermissionMixin

#current_model_permissions, #current_user_roles, included, #require_create_permission, #require_delete_permission, #require_other_permission, #require_read_permission, #require_update_permission, #require_x_permission, #respond_to_permission_error

Methods included from Edgar::ControllerCommon

#prepare_list, #scope_context, #user_scoped, #view_status_save

Instance Method Details

- (Object) clear

Ajax method to clear form

Permission

ModelPermission::READ on this controller is required.



185
186
187
# File 'app/controllers/edgar_controller.rb', line 185

def clear
  @model = model_class.new
end

- (Object) create

save new record

Permission

ModelPermission::CREATE on this controller is required.



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'app/controllers/edgar_controller.rb', line 113

def create
  upsert do
    # NOTE: create!() is not used because assign to @model to draw form.
    # Otherwise, @model would be in nil so failure at edgar/_form rendering.
    #
    # NOTE2: valid? after create() calls validate_on_update.  This is not
    # an expected behavior.  So, new, valid?, then save.
    @model = model_class.new(params[model_name])
    on_upsert
    upsert_files
    raise ActiveRecord::RecordNotSaved if !@model.valid?
    @model.save

    # clear @model values for next data-entry
    @model = model_class.new
  end
end

- (Object) csv_download

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.



304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'app/controllers/edgar_controller.rb', line 304

def csv_download
  filename    = sprintf("%s-%s.csv",
                    model_name,
                    Time.now.strftime("%Y%m%d-%H%M%S"))
  file        = Tempfile.new(filename, Rails.root + Edgar::CSV_DIR)
  csv_visitor = EdgarHelper::CsvVisitor.new(view_context)
  find_args   = {:conditions=>view_status.model.conditions}
  if !view_status.order_by.blank?
    find_args.merge!(:order => view_status.order_by + ' ' + view_status.dir)
  end
  file.write CSV.generate_line(model_class.columns.map{|c| c.name})
  for rec in user_scoped.find(:all, find_args) do
    array = []
    for col in model_class.columns do
      array << csv_visitor.visit_column(rec, col)
    end
    file.write CSV.generate_line(array)
  end
  file.close 
  send_file(file.path, {
      type:     'text/csv',
      filename: filename})
 #file.close(true)
end

- (Object) destroy

Permission

ModelPermission::DELETE on this controller is required.



171
172
173
174
175
176
177
178
# File 'app/controllers/edgar_controller.rb', line 171

def destroy
  m = model_class.find(user_scoped.find(params[:id]).id)
  m.destroy

  prepare_list
  @model = model_class.new
  @flash_notice = t('delete_ok')
end

- (Object) draw_flash(page) (private)

At public action, just set any message into @flash_notice and/or @flash_error. Then, any render in EdgarController 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.



420
421
422
423
# File 'app/controllers/edgar_controller.rb', line 420

def draw_flash(page)
  page.replace_html 'flash_notice', :text=>@flash_notice
  page.replace_html 'flash_error',  :text=>@flash_error
end

- (Object) enum_cache_stat (private)

# 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 = EdgarHelper::CsvVisitor.new(@template)
  find_args = {:conditions=>view_status.model.conditions}
  if !view_status.order_by.blank?
    find_args.merge!(:order => view_status.order_by + ' ' + view_status.dir)
  end
  output.write CSV.generate_line(model_class.columns.map{|c| c.name}) + "\n"
  for rec in model_class.find(:all, find_args) do
    array = []
    for col in model_class.columns do
      array << csv_visitor.visit_column(rec, col)
    end
    output.write CSV.generate_line(array) + "\n"
  end
end


451
452
453
454
# File 'app/controllers/edgar_controller.rb', line 451

def enum_cache_stat
  logger.debug 'Edgar::EnumCache stat (hit/out/out_of_enum): ' +
      Edgar::EnumCache.instance.stat.inspect
end

- (Object) file_download

To prevent unassociated file access, do:

  1. check if it is in model object

  2. check if it is a edgar_file column

Permission

ModelPermission::READ on this controller is required.



336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'app/controllers/edgar_controller.rb', line 336

def file_download
  if !model_class.edgar_file?(params[:column])
    flash[:error] = t('edgar_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

- (Object) index

draw search result in whole page. default update DOM id and template is as follows:

DOM id

'edgar_list'

template

'edgar/list'

However, these can be replaced by params and params

Permission

ModelPermission::READ on this controller is required.

SEE ALSO

popup()

draw popup



101
102
103
104
105
106
# File 'app/controllers/edgar_controller.rb', line 101

def index
 #clear_topic_path
  prepare_list
  @search = view_status.model
  @model  = model_class.new
end

- (Object) map

draw Google map.

Permission

ModelPermission::READ on this controller is required.



358
359
360
# File 'app/controllers/edgar_controller.rb', line 358

def map
  render :template=>'edgar/map', :layout=>false
end

- (Object) model_class (private)

derive model class from this controller.

If controller cannot guess model class, overwrite this.

Examples:

  • AuthorsController -> Author

  • Edgar::UsersController -> Edgar::User



370
371
372
# File 'app/controllers/edgar_controller.rb', line 370

def model_class
  @_model_class ||= self.class.name.gsub(/Controller$/, '').singularize.constantize
end

- (Object) model_name (private)

return model name.

if each concreate controller cannot guess model name from its controller name, overwrite this.

Examples:

  • Edgar::UsersController -> 'edgar_user'

  • AuthorsController -> 'author'



383
384
385
# File 'app/controllers/edgar_controller.rb', line 383

def model_name
  @_model_name ||= ActiveModel::Naming.param_key(model_class.new)
end

- (Object) on_upsert (private)

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



392
393
394
# File 'app/controllers/edgar_controller.rb', line 392

def on_upsert
  #
end

- (Object) search

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.



198
199
200
201
202
203
204
205
# File 'app/controllers/edgar_controller.rb', line 198

def search
  vc        = view_status
  vc.model  = Edgar::SearchForm.new(model_class, params[:edgar_search_form])
  vc.update_attribute(:page, 1)
  vc.save!
  @search   = vc.model
  prepare_list if @search.valid?
end

- (Object) search_clear

Ajax method to clear search conditions

Permission

ModelPermission::READ on this controller is required.



212
213
214
# File 'app/controllers/edgar_controller.rb', line 212

def search_clear
  @search   = Edgar::SearchForm.new(model_class)
end

- (Object) search_load

Ajax method to load search condition, lines, order_by, dir, and page.



255
256
257
258
# File 'app/controllers/edgar_controller.rb', line 255

def search_load
  @search = current_user.saved_view_statuss.find(params[:id]).load(@sssn).model
  draw_search_form
end

- (Object) search_save

Ajax method to save search conditions

call flow

Edgar.SearchSavePopup.open() (javascript)
 (show $('search_save_popup'))
  Edgar.SearchSavePopup.submit() (javascript)
   (copy entered name into $('saved_view_status_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.

  2. send Search Form with name to server.

To comply these, Edgar.SearchSavePopup copies the entered name to 'saved_view_status_name' hidden field and then sends the form which includes the copied name.

Permission

ModelPermission::READ on this controller is required.



237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'app/controllers/edgar_controller.rb', line 237

def search_save
  svc = SavedVcontext.save(current_user, nil,
                           params[:saved_view_status_name], view_status)

  render :update do |page|
    page << "Edgar.SearchSavePopup.close();"
    page.replace 'edgar_load_condition_menu',
       :partial=>'edgar/load_condition_menu'
  end
rescue ActiveRecord::ActiveRecordError => ex
  app_rescue
  render :update do |page|
    page.replace_html 'search_save_popup_flash_error', :text=>t('save_fail')
  end
end

- (Object) show

Show detail of one record. Format of html & js should be supported.

Permission

ModelPermission::READ on this controller is required.



136
137
138
139
140
141
142
143
144
145
146
147
# File 'app/controllers/edgar_controller.rb', line 136

def show
  @model = user_scoped.find(params[:id])
 #add_topic_path
  respond_to do |format|
    format.html {
      prepare_list
      @search = view_status.model
      render :action=>'index'
    }
    format.js
  end
end

- (Object) top

This page is for following purposes:

  • top page which contain latest info (TBD)

  • any error message on HTML format access

    • on Ajax access, rather edgar_error_popup is used



84
85
86
# File 'app/controllers/edgar_controller.rb', line 84

def top
  render :action => 'top'
end

- (Object) update

save existence modified record

Permission

ModelPermission::UPDATE on this controller is required.



154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'app/controllers/edgar_controller.rb', line 154

def update
  upsert do
    # NOTE:
    # 1. update ++then++ valid to set new values in @model to draw form.
    # 1. user_scoped may have joins so that record could be
    #    ActiveRecord::ReadOnlyRecord so that's why access again from
    #    model_class.
    @model  = model_class.find(user_scoped.find(params[:id]).id)
    upsert_files
    if !@model.update_attributes(params[model_name])
      raise ActiveRecord::RecordInvalid.new(@model)
    end
  end
end

- (Object) upsert(&block) (private)

update/insert common



397
398
399
400
401
402
403
404
405
406
407
# File 'app/controllers/edgar_controller.rb', line 397

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}: #{@model.class.to_s}: #{@model.errors.full_messages.join(' / ')}"
  app_rescue
  @flash_error = t('save_fail')
end

- (Object) upsert_files (private)

insert/update uploaded file and point to it via file_NN column



426
427
428
# File 'app/controllers/edgar_controller.rb', line 426

def upsert_files
  @model.upsert_files(params[:file_info], params[model_name])
end

- (Object) view_status (private)



409
410
411
412
# File 'app/controllers/edgar_controller.rb', line 409

def view_status
  @view_status ||= Edgar::ViewStatus.intern(@sssn, controller_name,
      Edgar::SearchForm.new(model_class))
end

- (Object) zip_complete

zip -> address completion

INPUTS

params

key to find address info. hyphen is supported.

params

address fields DOM id prefix. e.g. 'org_adrs_0_'

call flow

on drawing

EdgarHelper.draw_adrs()       app/helpers/edgar_helper.rb
                              app/views/edgar/_address.html.erb
    Example:
        :
    <input type=text name='org[adrs_0_zip]'>
        :

on clicking zip->address button

Edgar.zip_complete()          public/javascripts/edgar.js
 Ajax.Request()
  EdgarController.zip_complete  app/controllers/edgar_controller.rb
   inline RJS to fill address info


281
282
283
284
285
286
287
288
289
290
# File 'app/controllers/edgar_controller.rb', line 281

def zip_complete
  zip           = params[:zip].gsub(/\D/, '')
  @address      = Edgar::ZipAddress.find_by_zip(zip) || Edgar::ZipAddress.new(
                    :prefecture => '?',
                    :city       => '',
                    :other      => '')

  # sanitize
  @adrs_prefix  = params[:adrs_prefix].gsub(/[^a-zA-Z_0-9]/, '')
end