Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

Lockable Records

Đang mở
#39 13 bình luận 0 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

Đánh giá

Độ khó
5/5
Thời gian dự kiến
Hơn một tuần
Mức phù hợp với người mới
20/100
Loại issue
Tính năng
Độ rõ ràng
Cần làm rõ
Mức độ hoạt động
Đình trệ
Công nghệ
rails, ruby
Lĩnh vực
backend, frontend

Hướng nghiên cứu

Bắt đầu với các ví dụ form Lockable::Lock ServerOp và HyperComponent được đề xuất trong issue, sau đó kiểm tra các entry point Hyperstack hiện có mà chúng sẽ sử dụng. Không có file hoặc test nào được nêu tên, vì vậy trước tiên hãy xác định phạm vi locking được hỗ trợ và các bước kiểm tra chấp nhận; việc này chỉ được xem là hoàn tất khi đã thống nhất được hướng triển khai, thay vì để pattern vẫn mang tính suy đoán.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

requested-topic

Hyperstack has no built in way to lock records so that only one client can be editing a record at a time. Here is a pattern for doing so, that could be turned into a standalone gem or a gist.

Summary

It is fairly easy to do using stuff already available in Hyperstack. There are some tricky application level user experience (UX) decisions about how to insure that things eventually become unlocked, but these are application design problems, and a lot of the details could be hidden in a nice reusable module.

Locking / Unlocking

Locking implementation is straight forward, you just need to add a belongs_to attribute in the record for the user who has locked the record. You can't lock the record with a normal active record update because you need to prevent race conditions between users. However we can add a generic Lock ServerOp that takes a class name, record id, attribute name, time_out and attempts to lock the record.

module Lockable 
  class Lock < ServerOp 
    param :acting_user  # all remote server ops must include this parameter which is 
                                      # securely filled in by hyperstack before running the operation
    param :record_class
    param :record_id 
    param :lock_attribute
    param :time_out  # this will be used to prevent permanent locking of records
    step do 
      ActiveRecord::Base.transaction do
        record = const_get(params.record_class).find(params.record_id, :lock => true)
        record.lock_attribute = nil if record.updated_at < params.time_out
        fail if record[lock_attribute] && record[lock_attribute] != params.acting_user
        record # use permission system to check if acting user is allowed to update this record
          .check_permission_with_acting_user(params.acting_user, :update)
          .update(lock_attribute => params.acting_user)
      end
    end
  end
end

Note that the Lock server op if called by the same user when the lock is already acquired by that user will simply update the lock time.

Since there is no need to insure mutual exclusion when unlocking the record, we can simply set the lock attribute value to nil using a normal ActiveRecord update. This could be done when the form is saved to avoid extra calls to the server. Alternatively you could include an after_save hook in the Model, so it would be done automatically on save.

class MyDataRecord < ActiveRecord::Base 
  ...
  before_save { self.lock_user = nil }
  ...
end

The normal broadcast mechanism will insure that the lock attribute is updated across all browsers as the lock changes, so now we can use the value the lock attribute to control access to the form.

Basic Button Behavior

The application will have to be responsible for insuring that the "Edit" button is disabled if the lock field value is non-nil. If two users click the edit button at the same time one will acquire the lock, while the other user will see the button disabled, and the click will simply be ignored. In the disabled state the button could have an optional tool tip explaining what is going on. This can all be built easily using the base HTML components, tool tip libraries, styles, and click handlers.

Meanwhile inside the form the save button will just set the lock attribute to nil and save the record with the changed data, while the cancel button will revert the record, and then do an update of the lock attribute.

Note that if the system is one in which users "know about each other" you can add the locking user's name to the tool tip as well since it referenced in the lock attribute.

For any given form you are just adding a very few lines of code and classes to the edit button, and the cancel button. Save works as normal! But it's hard to generalize this as the specific UX will depend on the requirements of the application. For example edit might be a link not a button. The tool tip could be implemented in many ways, etc. So its probably best just to make this a programming pattern rather than try to make it into some kind of reusable component (at least until we have more experience.)

Insuring records don't get permanently locked

The real problem is how do you deal with making sure the record gets unlocked?

The first case to consider is what if the user just closes the browser? In this case the record will stay locked forever. The solution is to have the Edit button check for both the lock attribute, and the last_updated_at value of the record. If the record has not been updated for some period of time, the button can assume the record is lockable.

But kicking a user out from editing a record at some timeout is not the full solution. If the timeout is short, then users might not finish editing. If the timeout is long then you might wait a long time just because somebody else abandoned the edit, without cancelling.

The solution is to pick a first timeout (60 seconds for example) and then to set an every interval timer in the form component's after_mount hook. When ever this interval expires the code will check to see if any edits have been made, and if so the Lock operation is called again which updates the last_updated_at field. If no edits have been made, a modal dialog pops up asking the user to confirm they are still there. If yes the lock is updated, and life goes on, if no then the form edit is cancelled. Finally the dialog is watched by a second timer, and if this timer expires, the form is also cancelled.

class MyForm < HyperComponent

  param :record

  EDIT_TIMEOUT = 60
  CONTINUE_TIMEOUT = 30

  self << class
    def editable?(record)
      !record.lock_user ||
      record.updated_at > Time.now - (EDIT_TIMEOUT + CONTINUE_TIME_OUT)
    end

    def editing?
      record.lock_user.id == Hyperstack::Application.acting_user_id
    end

    def edit!(record)
        Lockable::Lock.run(
          record_class:    MyDataRecord, 
          record_id:         record.id, 
          lock_attribute: :lock_user, 
          user_id:             Hyperstack::Application.acting_user_id,
          time_out:           EDIT_TIMEOUT + CONTINUE_TIME_OUT
        )
    end
  end

  after_mount do 
    every(EDIT_TIMEOUT) do 
      if @form_edited
        self.class.edit!
      else
        show_continue_dialog
      end
      @form_edited = false
    end
  end

  after_update do 
    @form_edited = true
  end

  def show_continue_dialog
    @continue = false
    timer = after(CONTINUE_TIME_OUT) do 
      record.update(lock_user: nil)
    end
    if continue_dialog
      timer.abort!
    else
      record.update(lock_user: nil)
    end
  end

  # continue_dialog is application dependent, by default we raise a confirm box
  # the method should return truthy if the user wishes to continue editing
  def continue_dialog
    confirm("continue editing?")
  end

  # be  sure to update lock_user to nil before saving the record as well...
end

class FormContainer < HyperComponent 
  render do 
    if MyForm.editing?
      MyForm(...)
    else
      BUTTON(disabled: !MyForm.editable?) { 'Edit' }
      .on(:click) { MyForm.edit!(record: record) }
    end
  end
end

You could easily make the contents of "MyForm" above into a nice module builder that could be included like this:

class MyForm < HyperComponent
  include Lockable::ComponentHelper[  # these would be the defaults
    record_accessor: :record,  
    lock_attribute: :lock_user, 
    edit_timeout: 60, 
    confirm_timeout: 30
  ]
  # override the continue_dialog method if desired
end

see https://dejimata.com/2017/5/20/the-ruby-module-builder-pattern for hints how to do this.

Ngôn ngữ chính
CSS
Star
5
Fork
11
Chỉ số merge pull request
Không có pull request nào được merge trong 30 ngày

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của hyperstack-org/website

Tất cả issue của hyperstack-org/website

Issue tương tự

Thêm issue về Backend & API Design

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.