myexperiment-hackers
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[myexperiment-hackers] [2025] trunk: Merged invitation_throttling branch


From: noreply
Subject: [myexperiment-hackers] [2025] trunk: Merged invitation_throttling branch (revisions 2007-2024).
Date: Mon, 1 Dec 2008 13:41:04 -0500 (EST)

Revision
2025
Author
alekses6
Date
2008-12-01 13:41:03 -0500 (Mon, 01 Dec 2008)

Log Message

Merged invitation_throttling branch (revisions 2007-2024).

Modified Paths

Added Paths

Diff

Modified: trunk/app/controllers/application.rb (2024 => 2025)


--- trunk/app/controllers/application.rb	2008-12-01 18:18:56 UTC (rev 2024)
+++ trunk/app/controllers/application.rb	2008-12-01 18:41:03 UTC (rev 2025)
@@ -38,7 +38,28 @@
     return res
   end
   
+
+  def formatted_timespan(time_period)
+    # Takes a period of time in seconds and returns it in human-readable form (down to minutes)
+    # from (http://www.postal-code.com/binarycode/category/devruby/)
+    out_str = ""
+        
+    interval_array = [ [:weeks, 604800], [:days, 86400], [:hours, 3600], [:minutes, 60] ]
+    interval_array.each do |sub|
+      if time_period >= sub[1]
+        time_val, time_period = time_period.divmod(sub[1])
+            
+        time_val == 1 ? name = sub[0].to_s.singularize : name = sub[0].to_s
+              
+        ( sub[0] != :minutes ? out_str += ", " : out_str += " and " ) if out_str != ''
+        out_str += time_val.to_s + " #{name}"
+      end
+    end
+   
+    return out_str  
+  end
   
+  
   # this method is only intended to check if entry
   # in "viewings" or "downloads" table needs to be
   # created for current access - and this is *only*

Modified: trunk/app/controllers/messages_controller.rb (2024 => 2025)


--- trunk/app/controllers/messages_controller.rb	2008-12-01 18:18:56 UTC (rev 2024)
+++ trunk/app/controllers/messages_controller.rb	2008-12-01 18:41:03 UTC (rev 2025)
@@ -81,7 +81,11 @@
         flash[:error] = "You cannot send a message to yourself"
         format.html { redirect_to new_message_url }
       end
-    else
+    elsif (allowed_plus_timespan = ActivityLimit.check_limit(current_user, "internal_message", false))[0]
+      # the user is allowed to send messages - limit not yet reached; show the new message screen 
+      # (but the counter is not updated just yet - the user might not send the message after all,
+      #  so this is a mere validation - which saves user from typing the message in and learning that
+      #  it can't be set because of the limit, which is expired)
       if params[:reply_id]
         @message = Message.new(:to => @reply.from,
                                :reply_id => @reply.id,
@@ -90,41 +94,61 @@
       else
         @message = Message.new
       end
+    else
+      # no more messages can be sent because of the activity limit
+      respond_to do |format|
+        error_msg = "You can't send messages - your limit is reached, "
+        if allowed_plus_timespan[1].nil?
+          error_msg += "it will not be reset. Please contact myExperiment administration for details."
+        elsif allowed_plus_timespan[1] <= 60
+          error_msg += "please try again within a couple of minutes"
+        else
+          error_msg += "it will be reset in " + formatted_timespan(allowed_plus_timespan[1])
+        end
+        
+        flash[:error] = error_msg 
+        format.html { redirect_to messages_path }
+      end
     end
   end
 
   # POST /messages
   def create
-    @message = Message.new(params[:message])
-    @message.from ||= current_user.id
+    # check if sending is allowed and increment the message counter
+    sending_allowed = ActivityLimit.check_limit(current_user, "internal_message")[0]
     
-    # set initial datetimes
-    @message.read_at = nil
-    
-    # test for spoofing of "from" field
-    unless @message.from.to_i == current_user.id.to_i
-      errors = true
-      @message.errors.add :from, "must be logged on"
-    end
-    
-    # test for existance of reply_id
-    if @message.reply_id
-      begin
-        reply = Message.find(@message.reply_id) 
+    if sending_allowed
+      @message = Message.new(params[:message])
+      @message.from ||= current_user.id
       
-        # test that user is replying to a message that was actually received by them
-        unless reply.to.to_i == current_user.id.to_i
+      # set initial datetimes
+      @message.read_at = nil
+      
+      # test for spoofing of "from" field
+      unless @message.from.to_i == current_user.id.to_i
+        errors = true
+        @message.errors.add :from, "must be logged on"
+      end
+      
+      # test for existance of reply_id
+      if @message.reply_id
+        begin
+          reply = Message.find(@message.reply_id) 
+        
+          # test that user is replying to a message that was actually received by them
+          unless reply.to.to_i == current_user.id.to_i
+            errors = true
+            @message.errors.add :reply_id, "not addressed to sender"
+          end
+        rescue ActiveRecord::RecordNotFound
           errors = true
-          @message.errors.add :reply_id, "not addressed to sender"
+          @message.errors.add :reply_id, "not found"
         end
-      rescue ActiveRecord::RecordNotFound
-        errors = true
-        @message.errors.add :reply_id, "not found"
       end
     end
     
     respond_to do |format|
-      if !errors and @message.save
+      if sending_allowed && !errors && @message.save
         
         begin
           Notifier.deliver_new_message(@message, base_host) if @message.u_to.send_notifications?
@@ -137,6 +161,12 @@
         
         flash[:notice] = 'Message was successfully sent.'
         format.html { redirect_to messages_url }
+      elsif !sending_allowed
+        # when redirecting, the check will be carried out again, and full error message displayed to the user
+        # (this is an unlikely event - can only happen when the user opens several "new message" pages one
+        #  after another and then posts messages from each of them, rather than opening a new one for each
+        #  message - therefore, it will not have significant performance effect on running the allowance check again)
+        format.html { redirect_to new_message_url }
       else
         format.html { render :action ="" "new" }
       end

Modified: trunk/app/controllers/networks_controller.rb (2024 => 2025)


--- trunk/app/controllers/networks_controller.rb	2008-12-01 18:18:56 UTC (rev 2024)
+++ trunk/app/controllers/networks_controller.rb	2008-12-01 18:41:03 UTC (rev 2025)
@@ -30,7 +30,22 @@
   
   # GET /networks/1;invite
   def invite
+    error_msg = ""
+    sending_allowed_with_reset_timestamp = ActivityLimit.check_limit(current_user, "group_invite", false)
+    unless sending_allowed_with_reset_timestamp[0]
+      # limit of invitation for this user is already exceeded
+      error_msg = "Please note that you can't send email invitations - your limit is reached, "
+      if sending_allowed_with_reset_timestamp[1].nil?
+        error_msg += "it will not be reset. Please contact myExperiment administration for details."
+      elsif sending_allowed_with_reset_timestamp[1] <= 60
+        error_msg += "please try again within a couple of minutes"
+      else
+        error_msg += "it will be reset in " + formatted_timespan(sending_allowed_with_reset_timestamp[1])
+      end
+    end
+    
     respond_to do |format|
+      flash.now[:error] = error_msg unless error_msg.blank?
       format.html # invite.rhtml
     end
   end
@@ -91,9 +106,10 @@
     else
       # captcha verified correctly, can proceed
     
-      addr_count, validated_addr_count, valid_addresses, db_user_addresses, err_addresses, overflow_addresses = Invitation.validate_address_list(params[:invitations][:address_list])
+      addr_count, validated_addr_count, valid_addresses, db_user_addresses, err_addresses = Invitation.validate_address_list(params[:invitations][:address_list], current_user)
       existing_invitation_emails = []
       valid_addresses_tokens = {} # a hash for pairs of 'email' => 'token'
+      overflow_addresses = []
       
       if validated_addr_count > 0
         emails_counter = 0; counter = 0
@@ -103,10 +119,14 @@
           if PendingInvitation.find_by_email_and_request_type_and_request_for(email_addr, "membership", params[:id])
             existing_invitation_emails << email_addr
           else 
-            token_code = Digest::SHA1.hexdigest( email_addr.reverse + SECRET_WORD )
-            valid_addresses_tokens[email_addr] = token_code
-            invitation = PendingInvitation.new(:email => email_addr, :request_type => "membership", :requested_by => current_user.id, :request_for => params[:id], :message => params[:invitations][:msg_text], :token => token_code)
-            invitation.save
+            if ActivityLimit.check_limit(current_user, "group_invite")[0]
+              token_code = Digest::SHA1.hexdigest( email_addr.reverse + SECRET_WORD )
+              valid_addresses_tokens[email_addr] = token_code
+              invitation = PendingInvitation.new(:email => email_addr, :request_type => "membership", :requested_by => current_user.id, :request_for => params[:id], :message => params[:invitations][:msg_text], :token => token_code)
+              invitation.save
+            else
+              overflow_addresses << email_addr
+            end
           end        
         end
           
@@ -143,9 +163,9 @@
       # now display message based on number of valid / invalid addresses..
       error_occurred = true # a flag to select where to redirect from this action
       respond_to do |format|
-        if validated_addr_count == 0 && existing_invitation_emails.empty? && db_user_addresses.empty?
+        if validated_addr_count == 0 && existing_invitation_emails.empty? && db_user_addresses.empty? && overflow_addresses.empty?
           error_msg = "None of the supplied address(es) could be validated, no emails were sent. Please try again!<br/>You have supplied the following address list:<br/>\"#{params[:invitations][:address_list]}\""
-        elsif (addr_count == validated_addr_count) && (!err_addresses || err_addresses.empty?)
+        elsif (addr_count == validated_addr_count) && (!err_addresses || err_addresses.empty?) && (!overflow_addresses || overflow_addresses.empty?)
           error_msg = validated_addr_count.to_s + " Invitation email(s) sent successfully"
           error_occurred = false
         else 
@@ -173,7 +193,17 @@
           end
           
           unless overflow_addresses.empty?
-            error_msg += "<br/><br/>You can only send invitations to #{INVITATION_EMAIL_LIMIT} unique, valid, non-blank email addresses.<br/>The following addresses were not processed because of maximum allowed amount was exceeded:<br/>" + overflow_addresses.join("<br/>")
+            error_msg += "<br/><br/>You have ran out of quota for sending invitations, "
+            reset_quota_after = ActivityLimit.check_limit(current_user, "group_invite", false)[1]
+            if reset_quota_after.nil?
+              error_msg += "it will not be reset. Please contact myExperiment administration for details."
+            elsif reset_quota_after <= 60
+              error_msg += "please try again within a couple of minutes."
+            else
+              error_msg += "it will be reset in " + formatted_timespan(reset_quota_after) + "."
+            end
+            
+            error_msg += "<br/>The following addresses were not processed because maximum allowed amount of invitations was exceeded:<br/>" + overflow_addresses.join("<br/>")
           end
           
           error_msg += "</span>"

Modified: trunk/app/controllers/users_controller.rb (2024 => 2025)


--- trunk/app/controllers/users_controller.rb	2008-12-01 18:18:56 UTC (rev 2024)
+++ trunk/app/controllers/users_controller.rb	2008-12-01 18:41:03 UTC (rev 2025)
@@ -366,8 +366,25 @@
   
   # For sending invitation emails
   def invite
+    sending_allowed_with_reset_timestamp = ActivityLimit.check_limit(current_user, "user_invite", false)
+    
     respond_to do |format|
-      format.html # invite.rhtml
+      if sending_allowed_with_reset_timestamp[0]
+        format.html # invite.rhtml
+      else
+        # limit of invitation for this user is already exceeded
+        error_msg = "You can't send invitations - your limit is reached, "
+        if sending_allowed_with_reset_timestamp[1].nil?
+          error_msg += "it will not be reset. Please contact myExperiment administration for details."
+        elsif sending_allowed_with_reset_timestamp[1] <= 60
+          error_msg += "please try again within a couple of minutes"
+        else
+          error_msg += "it will be reset in " + formatted_timespan(sending_allowed_with_reset_timestamp[1])
+        end
+        
+        flash[:error] = error_msg 
+        format.html { redirect_to user_path(current_user) }
+      end
     end
   end
   
@@ -381,15 +398,21 @@
     else
       # captcha verified correctly, can proceed
       
-      addr_count, validated_addr_count, valid_addresses, db_user_addresses, err_addresses, overflow_addresses = Invitation.validate_address_list(params[:invitations][:addr_to])
+      addr_count, validated_addr_count, valid_addresses, db_user_addresses, err_addresses = Invitation.validate_address_list(params[:invitations][:addr_to], current_user)
       existing_invitation_emails = []
       valid_addresses_tokens = {}     # a hash for pairs of 'email' => 'token'
+      overflow_addresses = []
         
       # if validation found valid addresses, do the sending
+      # (limit on the number of invitation email is only checked where the actual email will be sent)
       if validated_addr_count > 0
         if params[:invitations][:as_friendship].nil?
           valid_addresses.each { |email_addr|
-            valid_addresses_tokens[email_addr] = "" 
+            if ActivityLimit.check_limit(current_user, "user_invite")[0]
+              valid_addresses_tokens[email_addr] = ""
+            else
+              overflow_addresses << email_addr
+            end
           }
           Invitation.send_invitation_emails("invite", base_host, User.find(params[:invitations_user_id]), valid_addresses_tokens, params[:invitations][:msg_text])
         elsif params[:invitations][:as_friendship] == "true"
@@ -399,10 +422,14 @@
             if PendingInvitation.find_by_email_and_request_type_and_request_for(email_addr, "friendship", params[:invitations_user_id])
               existing_invitation_emails << email_addr
             else 
-              token_code = Digest::SHA1.hexdigest( email_addr.reverse + SECRET_WORD )
-              valid_addresses_tokens[email_addr] = token_code
-              invitation = PendingInvitation.new(:email => email_addr, :request_type => "friendship", :requested_by => params[:invitations_user_id], :request_for => params[:invitations_user_id], :message => params[:invitations][:msg_text], :token => token_code)
-              invitation.save
+              if ActivityLimit.check_limit(current_user, "user_invite")[0]
+                token_code = Digest::SHA1.hexdigest( email_addr.reverse + SECRET_WORD )
+                valid_addresses_tokens[email_addr] = token_code
+                invitation = PendingInvitation.new(:email => email_addr, :request_type => "friendship", :requested_by => params[:invitations_user_id], :request_for => params[:invitations_user_id], :message => params[:invitations][:msg_text], :token => token_code)
+                invitation.save
+              else
+                overflow_addresses << email_addr
+              end
             end
           end
           
@@ -447,10 +474,10 @@
       # in future, potentially there's going to be a way to get results of sending;
       # now display message based on number of valid / invalid addresses..
       respond_to do |format|
-        if validated_addr_count == 0 && existing_invitation_emails.empty? && db_user_addresses.empty?
+        if validated_addr_count == 0 && existing_invitation_emails.empty? && db_user_addresses.empty? && overflow_addresses.empty?
           flash.now[:notice] = "None of the supplied address(es) could be validated, no emails were sent.<br/>Please check your input!"
           format.html { render :action ="" 'invite' }
-        elsif (addr_count == validated_addr_count) && (!err_addresses || err_addresses.empty?)
+        elsif (addr_count == validated_addr_count) && (!err_addresses || err_addresses.empty?) && (!overflow_addresses || overflow_addresses.empty?)
           flash[:notice] = validated_addr_count.to_s + " Invitation email(s) sent successfully"
           format.html { redirect_to :action ="" 'show', :id => params[:invitations_user_id] }
         else
@@ -487,7 +514,17 @@
           end
           
           unless overflow_addresses.empty?
-            error_msg += "<br/><br/>You can only send invitations to #{INVITATION_EMAIL_LIMIT} unique, valid, non-blank email addresses.<br/>The following addresses were not processed because of maximum allowed amount was exceeded:<br/>" + overflow_addresses.join("<br/>")
+            error_msg += "<br/><br/>You have ran out of quota for sending invitations, "
+            reset_quota_after = ActivityLimit.check_limit(current_user, "user_invite", false)[1]
+            if reset_quota_after.nil?
+              error_msg += "it will not be reset. Please contact myExperiment administration for details."
+            elsif reset_quota_after <= 60
+              error_msg += "please try again within a couple of minutes."
+            else
+              error_msg += "it will be reset in " + formatted_timespan(reset_quota_after) + "."
+            end
+            
+            error_msg += "<br/>The following addresses were not processed because maximum allowed amount of invitations was exceeded:<br/>" + overflow_addresses.join("<br/>")
           end
           
           error_msg += "</span>"

Copied: trunk/app/models/activity_limit.rb (from rev 2024, branches/invitation_throttling/app/models/activity_limit.rb) (0 => 2025)


--- trunk/app/models/activity_limit.rb	                        (rev 0)
+++ trunk/app/models/activity_limit.rb	2008-12-01 18:41:03 UTC (rev 2025)
@@ -0,0 +1,111 @@
+class ActivityLimit < ActiveRecord::Base
+  
+  # the single point in the application governing validation of all features that have
+  # limited allowance of usage (e.g. 5 messages a day, etc)
+  def self.check_limit(contributor, limit_feature, update_counter=true)
+    time_now = Time.now
+    limit_save_required = false
+    
+    if (limit = ActivityLimit.find(:first, :conditions => ["contributor_type = ? AND contributor_id = ? AND limit_feature = ?", contributor.class.name, contributor.id, limit_feature]))
+      # limit exists - check its validity
+      if (limit.limit_frequency && limit.reset_after && time_now > limit.reset_after)
+        # "reset_after" / "limit_frequency" are not NULL - so the limit is periodic;
+        # now it's the time to reset the counter to zero - no matter what its value was before
+        # (this will never be executed for non-periodic counters)
+        limit.current_count = 0
+        limit.reset_after = time_now + limit.limit_frequency.hours
+        limit_save_required = true
+
+        # also check if the contributor needs to be "promoted" to the next level --
+        # e.g. in the first month of membership on myExperiment one can send 10 messages daily,
+        # but after that can send 15 (because the user becomes more "trusted")
+        if (limit.promote_after && time_now > limit.promote_after)
+          absolute_max_limit_value = eval("#{limit_feature.upcase}_LIMIT_MAX_VALUE")
+          limit_increment_value = eval("#{limit_feature.upcase}_LIMIT_PROMOTE_INCREMENT")
+          promote_every = eval("#{limit_feature.upcase}_LIMIT_PROMOTE_EVERY")
+          
+          if limit_increment_value
+            if absolute_max_limit_value
+              # absolute max value set -->
+              # increase the limit only if not exceeded absolute maximum just yet
+              if (limit.limit_max < absolute_max_limit_value)
+                limit.limit_max += limit_increment_value
+                limit.promote_after = (promote_every ? (time_now + promote_every.days) : nil)
+               else
+                # absolute maximum already reached / exceeded, disable further promotions
+                limit.promote_after = nil
+              end
+              
+              # make sure that it's not set to exceed the absolute maximum
+              # (which can happen if increment is not factor of the absolute maximum value)
+              if (limit.limit_max > absolute_max_limit_value)
+                limit.limit_max = absolute_max_limit_value
+              end
+            else
+              # absolute value not set --> simply increment
+              limit.limit_max += limit_increment_value
+              limit.promote_after = (promote_every ? (time_now + promote_every.days) : nil)
+            end
+          else 
+            # increment not set - this will be a one-time promotion
+            # (if the absolute max value is set - set limit to it; if not - the feature becomes unlimited)
+            limit.limit_max = absolute_max_limit_value
+            limit.promote_after = nil
+            
+            if limit.limit_max.nil?
+              # the feature has become unlimited; no need to reset the counter anymore - 
+              # just keep it running to see usage of the feature by the user
+              limit.limit_frequency = nil
+              limit.reset_after = nil
+            end
+          end   
+        end # END of PROMOTION code
+        
+      end # END of COUNTER RESET code
+      
+    else
+      # limit doesn't exist yet - create it, then proceed to validation and saving
+      limit_frequency = eval("#{limit_feature.upcase}_LIMIT_FREQUENCY")
+      promote_every = eval("#{limit_feature.upcase}_LIMIT_PROMOTE_EVERY")
+      
+      limit = ActivityLimit.new(:contributor_type => contributor.class.name, :contributor_id => contributor.id,
+                                :limit_feature => limit_feature, 
+                                :limit_max => eval("#{limit_feature.upcase}_LIMIT_START_VALUE"),
+                                :limit_frequency => limit_frequency,
+                                :reset_after => (limit_frequency ? (time_now + limit_frequency.hours) : nil),
+                                :promote_after => (promote_every ? (time_now + promote_every.days) : nil),
+                                :current_count => 0)
+                                
+      limit_save_required = true
+    end
+    
+    
+    # decide if the requested action is allowed - check on the current counter value
+    action_allowed = true
+    if limit.limit_max
+      # (NULL in "limit_max" would mean unlimited allowance - not this case)
+      # deny the action if the "current_count" is equal / exceeded the "limit_max" value
+      action_allowed = false if (limit.current_count >= limit.limit_max)
+    end
+    
+    # update counter for the "current" action
+    if action_allowed && update_counter
+      limit.current_count += 1
+      limit_save_required = true
+    end
+    limit.save if limit_save_required # saves all changes (including counter resets, etc) if any were made
+    
+    # return if action is allowed / denied and when the next reset is going to be (nil for non-periodic counters) 
+    return [action_allowed, (limit.reset_after ? (limit.reset_after - time_now) : nil)]
+  end
+  
+  
+  # returns the remaining allowance (and the date/time when it finishes) for the limited feature;
+  # [NIL, NIL] if unlimited or limit doesn't exist 
+  def self.remaining_allowance(contributor, limit_feature)
+    limit = ActivityLimit.find(:first, :conditions => ["contributor_type = ? AND contributor_id = ? AND limit_feature = ?", contributor.class.name, contributor.id, limit_feature])
+    return [nil, nil] unless limit
+    return [(limit.limit_max ? (limit.limit_max - limit.current_count) : nil), limit.reset_after]
+  end
+  
+end

Modified: trunk/app/models/invitation.rb (2024 => 2025)


--- trunk/app/models/invitation.rb	2008-12-01 18:18:56 UTC (rev 2024)
+++ trunk/app/models/invitation.rb	2008-12-01 18:41:03 UTC (rev 2025)
@@ -12,17 +12,12 @@
   # 3) array of valid addresses
   # 4) hash of pairs ("existing_db_address" -> user_id)
   # 5) array of erroneous addresses
-  # 6) array of addresses that didn't fit into the EMAIL_LIMIT boundary
-  def self.validate_address_list (emails_csv_string)
+  def self.validate_address_list (emails_csv_string, current_user)
     
-    # check if need to set a limit on the number of emails to be processed
-    restrict_email_count = (INVITATION_EMAIL_LIMIT == -1 ? false : true)
-    
     # calling code relies on the 'err_addresses' variable being initialized to [] at the beginning
     err_addresses = []
     valid_addresses = []
     db_user_addresses = {}
-    overflow_addresses = []
     
     addr_cnt = 0
     validated_addr_cnt = 0
@@ -44,22 +39,13 @@
         if email_addr.downcase.match(/address@hidden,4}$/)
           # email_addr is validated if we are here;          
 
-          # check if we didn't exceed the INVITATION_EMAIL_LIMIT boundary yet 
-          if restrict_email_count && addr_cnt <= INVITATION_EMAIL_LIMIT
-            # if we didn't,
-            # check if it is also present in the DB as registered address of some user -
-            # if so, it needs to be treated differentrly
-            if( u = User.find(:first, :conditions => ["email = ? OR unconfirmed_email = ?", email_addr, email_addr]) )
-              db_user_addresses[email_addr] = u.id    
-            else
-              validated_addr_cnt += 1
-              valid_addresses << email_addr  
-            end
+          # check if it is also present in the DB as registered address of some user -
+          # if so, it needs to be treated differentrly
+          if( u = User.find(:first, :conditions => ["email = ? OR unconfirmed_email = ?", email_addr, email_addr]) )
+            db_user_addresses[email_addr] = u.id    
           else
-            # we did exceed the limit, but the goal is to
-            # only allow sending of not more than INVITATION_EMAIL_LIMIT emails / invitations;
-            # the ones that are after the INVITATION_EMAIL_LIMIT boundary will not be processed at all
-            overflow_addresses << email_addr 
+            validated_addr_cnt += 1
+            valid_addresses << email_addr  
           end
         else
           err_addresses << email_addr
@@ -69,7 +55,7 @@
     }
     
     
-    return [addr_cnt, validated_addr_cnt, valid_addresses, db_user_addresses, err_addresses, overflow_addresses]
+    return [addr_cnt, validated_addr_cnt, valid_addresses, db_user_addresses, err_addresses]
   end
   
   

Modified: trunk/app/views/messages/new.rhtml (2024 => 2025)


--- trunk/app/views/messages/new.rhtml	2008-12-01 18:18:56 UTC (rev 2024)
+++ trunk/app/views/messages/new.rhtml	2008-12-01 18:41:03 UTC (rev 2025)
@@ -1,5 +1,14 @@
 <% @message.to = params[:user_id] if params[:user_id] -%>
 
+<% remaining_allowance, allowance_finishes = ActivityLimit.remaining_allowance(current_user, "internal_message") -%>
+<% if remaining_allowance == 1 -%>
+	<p class="box_currentuser_specific" style="font-weight: bold; text-align: center;">
+		Please note that you can only send one more internal message 
+		<%= allowance_finishes ? "until #{allowance_finishes.strftime("%H:%M on %d/%m/%Y")}" : "; your allowance will not be reset" -%>. 
+	</p>
+<% end -%>
+
+
 <h1><%= params[:reply_id] ? h("Reply to Message") : h("New Message") %></h1>
 
 <%= error_messages_for :message %>

Modified: trunk/app/views/networks/invite.rhtml (2024 => 2025)


--- trunk/app/views/networks/invite.rhtml	2008-12-01 18:18:56 UTC (rev 2024)
+++ trunk/app/views/networks/invite.rhtml	2008-12-01 18:41:03 UTC (rev 2025)
@@ -53,7 +53,8 @@
 			<table>
 		  	<tr>
 		  		<td style="text-align: left;">
-			      <%= info_icon_with_tooltip("To include several email addresses, use commas or semicolons to separate them.<br/>Please note that just #{INVITATION_EMAIL_LIMIT} first unique, valid, non-blank addresses will be processed.") %>
+					  <% remaining_allowance, allowance_finishes = ActivityLimit.remaining_allowance(current_user, "group_invite") -%>
+			      <%= info_icon_with_tooltip("To include several email addresses, use commas or semicolons to separate them.<br/>#{remaining_allowance ? "Please note that your current allowance is to send invitations to #{remaining_allowance} unique, valid, non-blank address(es) in the list; the allowance #{allowance_finishes ? "will be renewed after #{allowance_finishes.strftime("%H:%M on %d/%m/%Y")}" : "will not be reset"}." : "Please note that limitations on the number of invitations to be sent may apply."}") %>
             Email address(es) to send invitations to:<br/>
             <% email_addresses_val = ((params[:invitations].nil? || params[:invitations][:address_list].nil?) ? "" : params[:invitations][:address_list]) %>
             <%= text_field_tag "invitations[address_list]", email_addresses_val, :style => "width: 500px; margin-bottom: 0.8em;" -%><br/>

Modified: trunk/app/views/users/_invite.rhtml (2024 => 2025)


--- trunk/app/views/users/_invite.rhtml	2008-12-01 18:18:56 UTC (rev 2024)
+++ trunk/app/views/users/_invite.rhtml	2008-12-01 18:41:03 UTC (rev 2025)
@@ -8,7 +8,8 @@
   <p style="line-heght: 1.5; margin-left: 1.5em">
   	<%= hidden_field_tag :invitations_user_id, current.id -%>
   	
-		<%= info_icon_with_tooltip("To include several email addresses, use commas or semicolons to separate them.<br/>Please note that just #{INVITATION_EMAIL_LIMIT} first unique, valid, non-blank addresses will be processed.") %>
+		<% remaining_allowance, allowance_finishes = ActivityLimit.remaining_allowance(current_user, "user_invite") -%>
+    <%= info_icon_with_tooltip("To include several email addresses, use commas or semicolons to separate them.<br/>#{remaining_allowance ? "Please note that your current allowance is to send invitations to #{remaining_allowance} unique, valid, non-blank address(es) in the list; the allowance #{allowance_finishes ? "will be renewed after #{allowance_finishes.strftime("%H:%M on %d/%m/%Y")}" : "will not be reset"}." : "Please note that limitations on the number of invitations to be sent may apply."}") %>
 		Email addresses to send invitations to:<br/>
 		<% email_addresses_val = ((params[:invitations].nil? || params[:invitations][:addr_to].nil?) ? "" : params[:invitations][:addr_to]) %>
   	<%= text_field_tag "invitations[addr_to]", email_addresses_val, :style => "width: 500px; margin-bottom: 0.5em;" -%><br/>

Modified: trunk/config/environment_private.rb.pre (2024 => 2025)


--- trunk/config/environment_private.rb.pre	2008-12-01 18:18:56 UTC (rev 2024)
+++ trunk/config/environment_private.rb.pre	2008-12-01 18:41:03 UTC (rev 2025)
@@ -66,10 +66,6 @@
 # The number of seconds for the cache timeout
 CACHE_TIMEOUT_SECS = 120
 
-# The maximum number of invitation emails to send out
-# (if set to '-1' - unlimited)
-INVITATION_EMAIL_LIMIT = 10 
-
 # The maximum file size allowed for workflows
 WORKFLOW_UPLOAD_MAX_BYTES = 20971520
 
@@ -81,4 +77,40 @@
 #
 # It is essential to put patterns into the ignore list in a single quotes - this will enable the
 # patterns to be treated as regular expressions, not just strings
-BOT_IGNORE_LIST = ['Googlebot', 'Slurp', 'msnbot', 'crawler', 'bot', 'heritrix', 'spider', 'Nutch']
\ No newline at end of file
+BOT_IGNORE_LIST = ['Googlebot', 'Slurp', 'msnbot', 'crawler', 'bot', 'heritrix', 'spider', 'Nutch']
+
+
+# =========== Settings for Activity Limits ===========
+
+# Each limited feature will require a set of 5 settings; meanings of each described below.
+# First part of every setting is the name of the feature being limited.
+# <feature_name>_LIMIT_START_VALUE - the initial maximum allowance for the feature (used when the new limit is created)  
+# <feature_name>_LIMIT_MAX_VALUE - absolute maximum allowance for the feature (this can't be exceeded after any promotions);
+#                                  NULL for always increasing allowance
+# <feature_name>_LIMIT_FREQUENCY -- in hours -- the time period over which the allowance is given; for example 5 messages (allowance) for 24 hours (frequency)
+#                                               NULL for non-periodic limits (i.e. limits which won't have their counters reset every <frequency> hours)
+# <feature_name>_LIMIT_PROMOTE_EVERY -- in days -- every <X> days the user will be promoted to the new level, 
+#                                                  where the allowance per frequency period will be adjusted by <feature_name>_LIMIT_PROMOTE_INCREMENT;
+#                                                  NULL to indicate that promotion should never happen
+# <feature_name>_LIMIT_PROMOTE_INCREMENT - should be positive; 
+#                                          0 to indicate that promotion shouldn't expand the allowance (why would this be useful?)
+#                                          NULL to perform a one-time promotion by setting the limit to whatever the value of <feature_name>_LIMIT_MAX_VALUE is;
+#                                          NULL when the <feature_name>_LIMIT_MAX_VALUE is also NULL makes the feature unlimited.
+
+INTERNAL_MESSAGE_LIMIT_START_VALUE = 10
+INTERNAL_MESSAGE_LIMIT_MAX_VALUE = 200
+INTERNAL_MESSAGE_LIMIT_FREQUENCY = 24 # hours
+INTERNAL_MESSAGE_LIMIT_PROMOTE_EVERY = 10 # days
+INTERNAL_MESSAGE_LIMIT_PROMOTE_INCREMENT = 10
+
+USER_INVITE_LIMIT_START_VALUE = 20
+USER_INVITE_LIMIT_MAX_VALUE = nil
+USER_INVITE_LIMIT_FREQUENCY = 24 # hours
+USER_INVITE_LIMIT_PROMOTE_EVERY = nil # days
+USER_INVITE_LIMIT_PROMOTE_INCREMENT = nil
+
+GROUP_INVITE_LIMIT_START_VALUE = 10
+GROUP_INVITE_LIMIT_MAX_VALUE = 100
+GROUP_INVITE_LIMIT_FREQUENCY = 24 # hours
+GROUP_INVITE_LIMIT_PROMOTE_EVERY = 20 # days
+GROUP_INVITE_LIMIT_PROMOTE_INCREMENT = 10

Copied: trunk/db/migrate/071_create_activity_limits.rb (from rev 2024, branches/invitation_throttling/db/migrate/071_create_activity_limits.rb) (0 => 2025)


--- trunk/db/migrate/071_create_activity_limits.rb	                        (rev 0)
+++ trunk/db/migrate/071_create_activity_limits.rb	2008-12-01 18:41:03 UTC (rev 2025)
@@ -0,0 +1,41 @@
+class CreateActivityLimits < ActiveRecord::Migration
+  
+  # This table will hold the state of various limits that can be
+  # imposed on the user / group actions - for example,
+  # limits on the number of internal messages that can be sent over
+  # period of time by some user; or number of invitations that the
+  # user can send 
+  
+  def self.up
+    create_table :activity_limits do |t|
+      # contributor (e.g. user or group that is limited)
+      t.column :contributor_type, :string, :null => false
+      t.column :contributor_id, :integer, :null => false
+      
+      # which action for the contributor is limited
+      t.column :limit_feature, :string, :null => false
+      
+      # "limit_max" - maximum number of times (NULL for unlimited) the action can be executed over
+      # "limit_frequency" period (in hours); "limit_frequency" set to NULL means that the limit is not periodic
+      t.column :limit_max, :integer
+      t.column :limit_frequency, :integer
+      
+      # number of times the action has already been executed since the last reset (governed by "limit_frequency")
+      # (can't be NULL - doesn't make sense to have NULL value for the counter)
+      t.column :current_count, :integer, :null => false
+      
+      # date/time after which "current_count" is to be reset to "limit_max" (for periodic limits - such as daily message limit)
+      # (NULL to indicate that reset should never happen and the limit is absolute, i.e. non-periodic)
+      # (the code will assume that if either --or both-- of "limit_frequency" and "reset_after" are NULLs, the limit is non-periodic)
+      t.column :reset_after, :datetime
+      
+      # date/time after which promotion to the next level (with, probably, higher "limit_max" should happen)
+      # (NULL to indicate that promotion should never happen and the user is to stay at the same level)
+      t.column :promote_after, :datetime
+    end
+  end
+
+  def self.down
+    drop_table :activity_limits
+  end
+end

Copied: trunk/test/fixtures/activity_limits.yml (from rev 2024, branches/invitation_throttling/test/fixtures/activity_limits.yml) (0 => 2025)


--- trunk/test/fixtures/activity_limits.yml	                        (rev 0)
+++ trunk/test/fixtures/activity_limits.yml	2008-12-01 18:41:03 UTC (rev 2025)
@@ -0,0 +1,5 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+one:
+  id: 1
+two:
+  id: 2

Copied: trunk/test/unit/activity_limit_test.rb (from rev 2024, branches/invitation_throttling/test/unit/activity_limit_test.rb) (0 => 2025)


--- trunk/test/unit/activity_limit_test.rb	                        (rev 0)
+++ trunk/test/unit/activity_limit_test.rb	2008-12-01 18:41:03 UTC (rev 2025)
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class ActivityLimitTest < Test::Unit::TestCase
+  fixtures :activity_limits
+
+  # Replace this with your real tests.
+  def test_truth
+    assert true
+  end
+end

reply via email to

[Prev in Thread] Current Thread [Next in Thread]