#1
  1. No Profile Picture
    Contributing User
    Devshed Newbie (0 - 499 posts)

    Join Date
    Nov 2004
    Posts
    84
    Rep Power
    11

    Multiple users working on the same form


    I have a database-driven web app that's used internally by about 25 people. Every now and then, 2 people will end up working on the same <form> simultaneously. The user who submits the form last is the "winner" and their changes are the ones that ultimately get retained.

    For example, consider a table that contains contact information (name, phone number, email, etc). When a user goes to edit existing contact information for a given contact, the page pulls the contact information from the database and populates the form with the current values in the database. When the form is submitted, the record gets updated in the database.

    Now, User A and User B will go to edit the same contact at the same time. User A makes changes the email address and submits the form. After User A submits, User B changes the phone number and submits the form. When it's all said and done, only changes to the phone number are reflected in the database because the change to the email address is overwritten when User B submits their form.

    My question is, what's the best way to go about locking a form to a single user at a time? I've considered keeping a "lockedPages" struct in the application scope. When an edit form is requested, it gets added to the "lockedPages" struct. When the form is submitted, it gets removed from the "lockedPages" struct. If someone requests an edit form that is currently in this struct, they are prevented from seeing the form. However, what if a user requests an edit form and then closes their browser? There's nothing to remove the entry from "lockedPages" and thus it never becomes unlocked. Surely there's a better way to lock down edits, no? Or maybe page-locking is the wrong approach? I'm open to ideas.
    Last edited by rawk; June 28th, 2013 at 05:27 PM.
  2. #2
  3. No Profile Picture
    Lost in code
    Devshed Supreme Being (6500+ posts)

    Join Date
    Dec 2004
    Posts
    8,316
    Rep Power
    7171
    Put a timeout on the lock or allow a user to optionally "steal" the lock from another user.
    PHP FAQ

    Originally Posted by Spad
    Ah USB, the only rectangular connector where you have to make 3 attempts before you get it the right way around
  4. #3
  5. No Profile Picture
    Moderator

    Join Date
    Jun 2002
    Location
    Raleigh, NC
    Posts
    5,286
    Rep Power
    968
    The other option is to send the last modified date when the form is displayed, and send it back to the server on form submit. Compare the timestamp sent earlier to the current last modified date for the record. If it has changed since the timestamp was last sent, you know the record was edited in between somewhere and you can show a warning/failure message.
  6. #4
  7. No Profile Picture
    Contributing User
    Devshed Newbie (0 - 499 posts)

    Join Date
    Nov 2004
    Posts
    84
    Rep Power
    11
    Thanks everyone for your input. Here's how I decided to handle it:

    Since all of my pages have dynamic content, I couldn't use the url as a unique locking key. For example, http://www.website.com/editContactForm is the same url regardless of which contact is being edited. The individual contact to be edited is passed to the form via HTTP POST.

    So I opted to write a custom tags for locking and unlocking a page based on a LockID that gets passed to the tag as an attribute. On the code section that contains the <form>, I call the lock tag while passing it a dynamically generated ID (for example, the ID might be "editContactForm-#contact.id#"). When the <form> is submitted, I call the unlock tag with the same ID. Here's the code for each of those tags:

    lock.cfm:
    Code:
    <!--- stops page execution after this tag to prevent more than one user accessing a page at a time --->
    
    <cfset expiryHours = 8>
    <cfset lockId = attributes.lockid>
    <cfif StructKeyExists(attributes, "expires") AND IsNumeric(attributes.expires)>
    	<cfset expiryHours = attributes.expires>
    </cfif>
    
    <!--- initialize application scope if it doesn't exist --->
    <cfif not StructKeyExists(application, "lockedPages")>
    	<cfset application.lockedPages = StructNew()>
    </cfif>
    
    <!--- delete lock if expired --->
    <cfif StructKeyExists(application.lockedPages, lockId) AND DateDiff("h", application.lockedpages[lockId].stamp, Now()) gte expiryHours>
    	<cfset StructDelete(application.lockedPages, lockId)>
    </cfif>
    
    <!--- initialize lock if it doesn't exist --->
    <cfif not StructKeyExists(application.lockedPages, lockId)>
    	<cfset application.lockedPages[lockId] = StructNew()>
    	<cfset application.lockedPages[lockId].user = session.user>
    	<cfset application.lockedPages[lockId].stamp = Now()>
    	<cfset application.lockedPages[lockId].page = cgi.query_string>
    	
    	<cfset session.heldLock = lockId>
    </cfif>
    
    <!--- if the page is locked by someone other than the current user --->
    <cfif application.lockedPages[lockId].user.id neq session.user.id>
    	<cf_showPageLock lockid="#lockid#">
    	<cfabort>
    </cfif>
    
    <div style="display:none"><cfoutput>|LockId|#lockId#|</cfoutput></div>
    unlock.cfm:
    Code:
    <cfset lockId = attributes.lockid>
    
    <!--- ensure there is a lock for this page --->
    <cfif StructKeyExists(application, "lockedPages") AND StructKeyExists(application.lockedPages, lockId)>
    	<!--- if current user owns the lock, remove the lock --->
    	<cfif application.lockedPages[lockId].user.id eq session.user.id>
    		<cfset StructDelete(application.lockedPages, lockId)>
    		<cfset StructDelete(session, "heldLock")>
    	<!--- else, stop page execution --->
    	<cfelse>
    		<cf_showPageLock lockid="#lockid#">
    		<cfabort>
    	</cfif>
    </cfif>
    
    <div style="display:none"><cfoutput>|LockId|#lockId#|</cfoutput></div>
    In my controller that handles page processing, I had to add a snippet of code to detect if the user navigated away from the page before invoking the unlock tag. "thewholepage" is a string variable that contains the full page content before it gets printed on the page:
    Code:
    <cfif StructKeyExists(session, "heldLock") AND thewholepage does not contain "|LockId|#session.heldLock#|">
    	<cf_unlock lockid="#session.heldLock#">
    </cfif>
    This is just a rough framework for now. I really don't like using a hidden <div> to track the locks across the site, but I was having trouble coming up with another solution. I intend to give some more flexibility with the tag usage. For instance, on some pages I want to fully stop page execution. On other pages, I want to disable one of many forms that are on the page.
  8. #5
  9. No Profile Picture
    Contributing User
    Devshed Newbie (0 - 499 posts)

    Join Date
    May 2008
    Posts
    131
    Rep Power
    7
    One thing to keep in mind about using the application scope is if it restarts, all the lock information stored in that scope is gone. Granted it does not happen that frequently.
  10. #6
  11. No Profile Picture
    Contributing User
    Devshed Newbie (0 - 499 posts)

    Join Date
    Nov 2004
    Posts
    84
    Rep Power
    11
    In the case of my app, losing the application scope is fine because if the application gets reset, so will all of the users' sessions, thereby freeing up any locks and forcing all users to log back in.
  12. #7
  13. No Profile Picture
    Contributing User
    Devshed Newbie (0 - 499 posts)

    Join Date
    May 2008
    Posts
    131
    Rep Power
    7
    Originally Posted by rawk
    In the case of my app, losing the application scope is fine because if the application gets reset, so will all of the users' sessions, thereby freeing up any locks and forcing all users to log back in.
    I'm not so sure that is true. IIRC, sessions are keyed to the application name. So the application ending does not automatically trigger the end of all sessions.
  14. #8
  15. No Profile Picture
    Contributing User
    Devshed Newbie (0 - 499 posts)

    Join Date
    Nov 2004
    Posts
    84
    Rep Power
    11
    "In the case of my app"

    I'm actually using Open BlueDragon. When my app resets, all sessions also reset. Maybe this is specific to OBD. I don't have any code in place to force this to happen.
  16. #9
  17. No Profile Picture
    Contributing User
    Devshed Newbie (0 - 499 posts)

    Join Date
    May 2008
    Posts
    131
    Rep Power
    7
    Ah, okay. I was referring to ACF. You might be right about OBD. I do not remember how it handles application/session end.

IMN logo majestic logo threadwatch logo seochat tools logo