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