First version of blocking feature. Allows both time-based (for map protection) and...
authorMatt Amos <zerebubuth@gmail.com>
Mon, 28 Sep 2009 16:01:00 +0000 (16:01 +0000)
committerMatt Amos <zerebubuth@gmail.com>
Mon, 28 Sep 2009 16:01:00 +0000 (16:01 +0000)
33 files changed:
app/controllers/application_controller.rb
app/controllers/user_blocks_controller.rb [new file with mode: 0644]
app/controllers/user_controller.rb
app/controllers/user_roles_controller.rb [new file with mode: 0644]
app/models/user.rb
app/models/user_block.rb [new file with mode: 0644]
app/models/user_role.rb [new file with mode: 0644]
app/views/user/view.html.erb
app/views/user_blocks/_block.html.erb [new file with mode: 0644]
app/views/user_blocks/_blocks.html.erb [new file with mode: 0644]
app/views/user_blocks/blocks_by.html.erb [new file with mode: 0644]
app/views/user_blocks/blocks_on.html.erb [new file with mode: 0644]
app/views/user_blocks/edit.html.erb [new file with mode: 0644]
app/views/user_blocks/index.html.erb [new file with mode: 0644]
app/views/user_blocks/new.html.erb [new file with mode: 0644]
app/views/user_blocks/revoke.html.erb [new file with mode: 0644]
app/views/user_blocks/show.html.erb [new file with mode: 0644]
app/views/user_roles/edit.html.erb [new file with mode: 0644]
app/views/user_roles/index.html.erb [new file with mode: 0644]
app/views/user_roles/new.html.erb [new file with mode: 0644]
app/views/user_roles/show.html.erb [new file with mode: 0644]
config/locales/en.yml
config/routes.rb
db/migrate/044_create_user_roles.rb [new file with mode: 0644]
db/migrate/045_create_user_blocks.rb [new file with mode: 0644]
public/403.html [new file with mode: 0644]
public/images/roles/administrator.png [new file with mode: 0644]
public/images/roles/blank_administrator.png [new file with mode: 0644]
public/images/roles/blank_moderator.png [new file with mode: 0644]
public/images/roles/moderator.png [new file with mode: 0644]
test/fixtures/user_roles.yml [new file with mode: 0644]
test/fixtures/users.yml
test/functional/user_roles_controller_test.rb [new file with mode: 0644]

index c43271b93a81faf5b3426a75097ac5fc1053f0e5..012ba2446787d5d313c983f59371ba5850789386 100644 (file)
@@ -78,6 +78,12 @@ class ApplicationController < ActionController::Base
         @user = User.authenticate(:username => username, :password => passwd) # basic auth
       end
     end
+
+    # check if the user has been banned
+    unless @user.nil? or @user.blocks.empty?
+      # NOTE: need slightly more helpful message than this.
+      render :text => "You got banned!", :status => :forbidden
+    end
   end
 
   def authorize(realm='Web Password', errormessage="Couldn't authenticate you") 
diff --git a/app/controllers/user_blocks_controller.rb b/app/controllers/user_blocks_controller.rb
new file mode 100644 (file)
index 0000000..2dcac3e
--- /dev/null
@@ -0,0 +1,145 @@
+class UserBlocksController < ApplicationController
+  layout 'site'
+
+  before_filter :authorize_web
+  before_filter :set_locale
+  before_filter :require_user, :only => [:new, :create, :edit, :delete]
+  before_filter :require_moderator, :only => [:new, :create, :edit, :delete]
+
+  def index
+    @user_blocks_pages, @user_blocks = paginate(:user_blocks,
+                                            :include => [:user, :moderator, :revoker],
+                                            :order => "user_blocks.end_at DESC",
+                                            :per_page => 20)
+  end
+
+  def show
+    @user_block = UserBlock.find(params[:id])
+
+    if @user and @user.id == @user_block.user_id
+      @user_block.needs_view = false
+      @user_block.save!
+    end
+  end
+
+  def new
+    @user_block = UserBlock.new
+    @display_name = params[:display_name]
+    @this_user = User.find_by_display_name(@display_name, :conditions => {:visible => true})
+  end
+
+  # GET /user_blocks/1/edit
+  def edit
+    @user_block = UserBlock.find(params[:id])
+    params[:user_block_period] = ((@user_block.end_at - Time.now.getutc) / 1.hour).ceil.to_s
+  end
+
+  def create
+    @display_name = params[:display_name]
+    @this_user = User.find_by_display_name(@display_name, :conditions => {:visible => true})
+    block_period = [UserBlock::PERIODS.max, params[:user_block_period].to_i].min
+
+    @user_block = UserBlock.new(:user_id => @this_user.id,
+                                :moderator_id => @user.id,
+                                :reason => params[:user_block][:reason],
+                                :end_at => Time.now.getutc() + block_period.hours,
+                                :needs_view => params[:user_block][:needs_view])
+    
+    if (@this_user and @user.moderator? and 
+        params[:tried_contacting] == "yes" and
+        params[:tried_waiting] == "yes" and
+        block_period >= 0)
+      if @user_block.save
+        flash[:notice] = t('user_block.create.flash', :name => @display_name)
+        redirect_to @user_block
+      else
+        render :action => "new"
+      end
+    else
+      if !@user.moderator?
+        flash[:notice] = t('user_block.create.not_a_moderator')
+      elsif params[:tried_contacting] != "yes"
+        flash[:notice] = t('user_block.create.try_contacting')
+      elsif params[:tried_waiting] != "yes"
+        flash[:notice] = t('user_block.create.try_waiting')
+      else
+        flash[:notice] = t('user_block.create.bad_parameters')
+      end
+      @display_name = @this_user.nil? ? '' : @this_user.display_name
+
+      render :action => "new"
+    end
+  end
+
+  def update
+    @user_block = UserBlock.find(params[:id])
+    block_period = [72, params[:user_block_period].to_i].min
+    
+    if @user_block.moderator_id != @user.id
+      flash[:notice] = t('user_block.update.only_creator_can_edit')
+      redirect_to(@user_block)
+
+    elsif !@user_block.active?
+      flash[:notice] = t('user_block.update.block_expired')
+      redirect_to(@user_block)
+      
+    elsif @user_block.update_attributes({ :end_at => Time.now.getutc() + block_period.hours,
+                                          :reason => params[:user_block][:reason],
+                                          :needs_view => params[:user_block][:needs_view] })
+      flash[:notice] = t('user_block.update.success')
+      redirect_to(@user_block)
+    else
+      render :action => "edit"
+    end
+  end
+
+  ##
+  # revokes the block, setting the end_time to now
+  def revoke
+    @user_block = UserBlock.find(params[:id])
+    
+    if !@user.moderator?
+      flash[:notice] = t('user_block.create.not_a_moderator')
+      redirect_to @user_block
+
+    elsif params[:confirm]
+      if @user_block.revoke!
+        flash[:notice] = t'user_block.revoke.flash'
+        redirect_to(@user_block)
+      else
+        flash[:notice] = t'user_block.revoke.error'
+        render :action => "edit"
+      end
+    end
+  end
+
+  ##
+  # shows a list of all the blocks on the given user
+  def blocks_on
+    @this_user = User.find_by_display_name(params[:display_name])
+
+    @user_blocks_pages, @user_blocks = paginate(:user_blocks,
+                                                :include => [:user, :moderator, :revoker],
+                                                :conditions => {:user_id => @this_user.id},
+                                                :order => "user_blocks.end_at DESC",
+                                                :per_page => 20)
+  end
+
+  ##
+  # shows a list of all the blocks by the given user.
+  def blocks_by
+    @this_user = User.find_by_display_name(params[:display_name])
+
+    @user_blocks_pages, @user_blocks = paginate(:user_blocks,
+                                            :include => [:user, :moderator, :revoker],
+                                            :conditions => {:moderator_id => @this_user.id},
+                                            :order => "user_blocks.end_at DESC",
+                                            :per_page => 20)
+  end
+
+  private
+  def require_moderator
+    redirect_to "/403.html" unless @user.moderator?
+  end
+
+end
index cee66274f101e8de0e280d24edd68fdbd20d22a0..93cc3a6a5688e480116ee7c84b0ae362086b3e3c 100644 (file)
@@ -142,36 +142,36 @@ class UserController < ApplicationController
   end
 
   def login
-    if session[:user]
-      # The user is logged in already, if the referer param exists, redirect them to that
-      if params[:referer]
-        redirect_to params[:referer]
-      else
-        redirect_to :controller => 'site', :action => 'index'
-      end
-      return
-    end
-
-    @title = t 'user.login.title'
-
-    if params[:user]
+    if params[:user] and session[:user].nil?
       email_or_display_name = params[:user][:email]
       pass = params[:user][:password]
       user = User.authenticate(:username => email_or_display_name, :password => pass)
       if user
         session[:user] = user.id
-        if params[:referer]
-          redirect_to params[:referer]
-        else
-          redirect_to :controller => 'site', :action => 'index'
-        end
-        return
       elsif User.authenticate(:username => email_or_display_name, :password => pass, :inactive => true)
         @notice = t 'user.login.account not active'
       else
         @notice = t 'user.login.auth failure'
       end
     end
+
+    if session[:user]
+      # The user is logged in, if the referer param exists, redirect them to that
+      # unless they've also got a block on them, in which case redirect them to 
+      # the block so they can clear it.
+      user = User.find(session[:user])
+      block = user.blocked_on_view
+      if block
+        redirect_to block, :referrer => params[:referrer]
+      elsif params[:referer]
+        redirect_to params[:referer]
+      else
+        redirect_to :controller => 'site', :action => 'index'
+      end
+      return
+    end
+
+    @title = t 'user.login.title'
   end
 
   def logout
diff --git a/app/controllers/user_roles_controller.rb b/app/controllers/user_roles_controller.rb
new file mode 100644 (file)
index 0000000..b1f24c2
--- /dev/null
@@ -0,0 +1,33 @@
+class UserRolesController < ApplicationController
+  layout 'site'
+
+  before_filter :authorize_web
+  before_filter :require_user
+  before_filter :require_administrator
+
+  def grant
+    this_user = User.find_by_display_name(params[:display_name], :conditions => {:visible => true})
+    if this_user and UserRole::ALL_ROLES.include? params[:role]
+      this_user.roles.create(:role => params[:role])
+    else
+      flash[:notice] = t('user_role.grant.fail', :role => params[:role], :name => params[:display_name])
+    end
+    redirect_to :controller => 'user', :action => 'view', :display_name => params[:display_name]
+  end
+
+  def revoke
+    this_user = User.find_by_display_name(params[:display_name], :conditions => {:visible => true})
+    if this_user and UserRole::ALL_ROLES.include? params[:role]
+      UserRole.delete_all({:user_id => this_user.id, :role => params[:role]})
+    else
+      flash[:notice] = t('user_role.revoke.fail', :role => params[:role], :name => params[:display_name])
+    end
+    redirect_to :controller => 'user', :action => 'view', :display_name => params[:display_name]
+  end
+
+  private
+  def require_administrator
+    redirect_to "/403.html" unless @user.administrator?
+  end
+
+end
index 9d7d233b7276f612eb420f56e4991e741e6c3aee..36d3df3b407015cfc8b9f8edf052083689b39708 100644 (file)
@@ -14,6 +14,9 @@ class User < ActiveRecord::Base
   has_many :client_applications
   has_many :oauth_tokens, :class_name => "OauthToken", :order => "authorized_at desc", :include => [:client_application]
 
+  has_many :blocks, :class_name => "UserBlock", :conditions => ["user_blocks.end_at > now() or user_blocks.needs_view"]
+  has_many :roles, :class_name => "UserRole"
+
   validates_presence_of :email, :display_name
   validates_confirmation_of :email#, :message => ' addresses must match'
   validates_confirmation_of :pass_crypt#, :message => ' must match the confirmation password'
@@ -125,6 +128,31 @@ class User < ActiveRecord::Base
     return false
   end
 
+  ##
+  # returns true if the user has the moderator role, false otherwise
+  def moderator?
+    has_role? 'moderator'
+  end
+
+  ##
+  # returns true if the user has the moderator role, false otherwise
+  def administrator?
+    has_role? 'administrator'
+  end
+
+  ##
+  # returns true if the user has the requested role
+  def has_role?(role)
+    roles.inject(false) { |x, r| x or r.role == role }
+  end
+
+  ##
+  # returns the first active block which would require users to view 
+  # a message, or nil if there are none.
+  def blocked_on_view
+    blocks.inject(nil) { |s,x| s || (x.needs_view? ? x : nil) }
+  end
+
   def delete
     self.active = false
     self.display_name = "user_#{self.id}"
diff --git a/app/models/user_block.rb b/app/models/user_block.rb
new file mode 100644 (file)
index 0000000..66b2c81
--- /dev/null
@@ -0,0 +1,36 @@
+class UserBlock < ActiveRecord::Base
+  validate :moderator_permissions
+
+  belongs_to :user, :class_name => "User", :foreign_key => :user_id
+  belongs_to :moderator, :class_name => "User", :foreign_key => :moderator_id
+  belongs_to :revoker, :class_name => "User", :foreign_key => :revoker_id
+  
+  PERIODS = [0, 1, 3, 6, 12, 24, 48, 96]
+
+  ##
+  # returns true if the block is currently active (i.e: the user can't
+  # use the API).
+  def active?
+    needs_view or end_at > Time.now.getutc
+  end
+
+  ##
+  # revokes the block, allowing the user to use the API again. the argument
+  # is the user object who is revoking the ban.
+  def revoke!(revoker)
+    attrs = { :end_at => Time.now.getutc(),
+              :revoker_id => @user.id,
+              :needs_view => false }
+    revoker.moderator? and update_attributes(attrs)
+  end
+
+  private
+  ##
+  # validate that only moderators are allowed to change the
+  # block. this should be caught and dealt with in the controller,
+  # but i've also included it here just in case.
+  def moderator_permissions
+    errors.add_to_base("Must be a moderator to create or update a block.") if moderator_id_changed? and !moderator.moderator?
+    errors.add_to_base("Must be a moderator to revoke a block.") unless revoker_id.nil? or revoker.moderator?
+  end
+end
diff --git a/app/models/user_role.rb b/app/models/user_role.rb
new file mode 100644 (file)
index 0000000..efa540f
--- /dev/null
@@ -0,0 +1,8 @@
+class UserRole < ActiveRecord::Base
+
+  ALL_ROLES = ['administrator', 'moderator']
+
+  validates_inclusion_of :role, :in => ALL_ROLES
+  belongs_to :user
+
+end
index 7769ffaa697e66e641c97811e755e50b813fec21..b0faeaf6427cf820970b47f4df246b2bafb64dcb 100644 (file)
@@ -1,4 +1,15 @@
-<h2><%= h(@this_user.display_name) %></h2>
+<h2><%= h(@this_user.display_name) %>
+<% UserRole::ALL_ROLES.each do |role| %>
+<% if @user and @user.administrator? %>
+<% if @this_user.has_role? role %>
+<%= link_to(image_tag("roles/#{role}.png", :size => "20x20", :border => 0, :alt => t("user.view.role.#{role}")), :controller => 'user_roles', :action => 'revoke', :display_name => @this_user.display_name, :role => role) %>
+<% else %>
+<%= link_to(image_tag("roles/blank_#{role}.png", :size => "20x20", :border => 0, :alt => t("user.view.role.#{role}")), :controller => 'user_roles', :action => 'grant', :display_name => @this_user.display_name, :role => role) %>
+<% end %>
+<% elsif @this_user.has_role? role %>
+<%= image_tag("roles/#{role}.png", :size => "20x20", :border => 0, :alt => t("user.view.role.#{role}")) %>
+<% end %>
+<% end %></h2>
 <div id="userinformation">
 <% if @user and @this_user.id == @user.id %>
 <!-- Displaying user's own profile page -->
 <% else %>
   <%= link_to t('user.view.add as friend'), :controller => 'user', :action => 'make_friend', :display_name => @this_user.display_name %>
 <% end %>
+| <%= link_to t('user.view.block_history'), :controller => 'user_blocks', :action => 'blocks_on', :display_name => @this_user.display_name %>
+<% if @this_user.moderator? %>
+| <%= link_to t('user.view.moderator_history'), :controller => 'user_blocks', :action => 'blocks_by', :display_name => @this_user.display_name %>
+<% end %>
+<% if @user and @user.moderator? %>
+| <%= link_to t('user.view.create_block'), :controller => 'user_blocks', :action => 'new', :display_name => @this_user.display_name %>
+<% end %>
 <% end %>
 </div>
 
diff --git a/app/views/user_blocks/_block.html.erb b/app/views/user_blocks/_block.html.erb
new file mode 100644 (file)
index 0000000..c443dce
--- /dev/null
@@ -0,0 +1,34 @@
+<tr>
+  <% c1 = cycle('table0', 'table1') %>
+
+  <% if show_user_name %>
+  <td class="<%= c1 %>"><%= link_to h(block.user.display_name), :controller => 'user', :action => 'view', :display_name => block.user.display_name %></td>
+  <% end %>
+  <% if show_moderator_name %>
+  <td class="<%= c1 %>"><%= link_to h(block.moderator.display_name), :controller => 'user', :action => 'view', :display_name => block.moderator.display_name %></td>
+  <% end %>
+  <td class="<%= c1 %>"><%=h block.reason %></td>
+  <td class="<%= c1 %>">
+    <% if block.active? %>
+      <% if block.needs_view? %>
+        <%= t'user_block.partial.until_login' %>
+      <% else %>
+        <%= t('user_block.partial.time_future', :time => distance_of_time_in_words_to_now(block.end_at)) %>
+      <% end %>
+    <% else %>
+      <%= t'user_block.partial.not_active' %>
+    <% end %>
+  </td>
+  <td class="<%= c1 %>">
+    <% if block.revoker_id.nil? %>
+      <%= t('user_block.partial.not_revoked') %>
+    <% else %>
+      <%= link_to h(block.revoker.display_name), :controller => 'user', :action => 'view', :display_name => block.revoker.display_name %>
+    <% end %>
+  </td>
+  <td class="<%= c1 %>"><%= link_to t('user_block.partial.show'), block %></td>
+  <td class="<%= c1 %>"><% if @user and @user.id == block.moderator_id and block.active? %><%= link_to t('user_block.partial.edit'), edit_user_block_path(block) %><% end %></td>
+  <% if show_revoke_link %>
+  <td class="<%= c1 %>"><% if block.active? %><%= link_to t('user_block.partial.revoke'), block, :confirm => t('user_block.partial.confirm'), :action => :revoke %><% end %></td>
+  <% end %>
+</tr>
diff --git a/app/views/user_blocks/_blocks.html.erb b/app/views/user_blocks/_blocks.html.erb
new file mode 100644 (file)
index 0000000..5338535
--- /dev/null
@@ -0,0 +1,19 @@
+<table id="block_list" cellpadding="3">
+  <tr>
+    <% if show_user_name %>
+    <th><%= t'user_block.partial.display_name' %></th>
+    <% end %>
+    <% if show_moderator_name %>
+    <th><%= t'user_block.partial.moderator_name' %></th>
+    <% end %>
+    <th><%= t'user_block.partial.reason' %></th>
+    <th><%= t'user_block.partial.status' %></th>
+    <th><%= t'user_block.partial.revoker_name' %></th>
+    <th></th>
+    <th></th>
+    <% if show_revoke_link %>
+    <th></th>
+    <% end %>
+  </tr>
+  <%= render :partial => 'block', :locals => {:show_revoke_link => show_revoke_link, :show_user_name => show_user_name, :show_moderator_name => show_moderator_name }, :collection => @user_blocks unless @user_blocks.nil? %>
+</table>
diff --git a/app/views/user_blocks/blocks_by.html.erb b/app/views/user_blocks/blocks_by.html.erb
new file mode 100644 (file)
index 0000000..aaafb52
--- /dev/null
@@ -0,0 +1,3 @@
+<h1><%= t('user_block.blocks_by.heading', :name => @this_user.display_name) %></h1>
+
+<%= render :partial => 'blocks', :locals => { :show_revoke_link => (@user and @user.moderator?), :show_user_name => true, :show_moderator_name => false } %>
diff --git a/app/views/user_blocks/blocks_on.html.erb b/app/views/user_blocks/blocks_on.html.erb
new file mode 100644 (file)
index 0000000..0b3187b
--- /dev/null
@@ -0,0 +1,3 @@
+<h1><%= t('user_block.blocks_on.heading', :name => @this_user.display_name) %></h1>
+
+<%= render :partial => 'blocks', :locals => { :show_revoke_link => (@user and @user.moderator?), :show_user_name => false, :show_moderator_name => true } %>
diff --git a/app/views/user_blocks/edit.html.erb b/app/views/user_blocks/edit.html.erb
new file mode 100644 (file)
index 0000000..3ca0b90
--- /dev/null
@@ -0,0 +1,25 @@
+<h1><%= t('user_block.edit.title', :name => @user_block.user.display_name) %></h1>
+
+<% form_for(@user_block) do |f| %>
+  <%= f.error_messages %>
+
+  <p>
+    <%= f.label :reason, t('user_block.edit.reason', :name => @user_block.user.display_name) %><br />
+    <%= f.text_area :reason %>
+  </p>
+  <p>
+    <%= label_tag 'user_block_period', t('user_block.edit.period') %><br />
+    <%= hidden_field_tag 'what is the period', params[:user_block_period] %>
+    <%= select_tag('user_block_period', options_for_select(UserBlock::PERIODS.collect { |h| [t('user_block.period', :count => h), h.to_s] }, params[:user_block_period])) %>
+  </p>
+  <p>
+    <%= f.check_box :needs_view %>
+    <%= f.label :needs_view, t('user_block.edit.needs_view') %>
+  </p>
+  <p>
+    <%= f.submit t('user_block.edit.submit') %>
+  </p>
+<% end %>
+
+<%= link_to t('user_block.edit.show'), @user_block %> |
+<%= link_to t('user_block.edit.back'), user_blocks_path %>
diff --git a/app/views/user_blocks/index.html.erb b/app/views/user_blocks/index.html.erb
new file mode 100644 (file)
index 0000000..9318a1d
--- /dev/null
@@ -0,0 +1,3 @@
+<h1><%= t('user_block.index.heading') %></h1>
+
+<%= render :partial => 'blocks', :locals => { :show_revoke_link => (@user and @user.moderator?), :show_user_name => true, :show_moderator_name => true } %>
diff --git a/app/views/user_blocks/new.html.erb b/app/views/user_blocks/new.html.erb
new file mode 100644 (file)
index 0000000..99f9252
--- /dev/null
@@ -0,0 +1,32 @@
+<h1><%= t('user_block.new.title', :name => @display_name) %></h1>
+
+<% form_for(@user_block) do |f| %>
+  <%= f.error_messages %>
+
+  <p>
+    <%= check_box_tag 'tried_contacting', 'yes', (params[:tried_contacting] == "yes") %>
+    <%= label_tag 'tried_contacting', t('user_block.new.tried_contacting') %>
+  </p>
+  <p>
+    <%= check_box_tag 'tried_waiting', 'yes', (params[:tried_waiting] == "yes") %>
+    <%= label_tag 'tried_waiting', t('user_block.new.tried_waiting') %>
+  </p>
+  <p>
+    <%= f.label :reason, t('user_block.new.reason', :name => @display_name) %><br />
+    <%= f.text_area :reason %>
+  </p>
+  <p>
+    <%= label_tag 'user_block_period', t('user_block.new.period') %><br />
+    <%= select_tag('user_block_period', options_for_select(UserBlock::PERIODS.collect { |h| [t('user_block.period', :count => h), h.to_s] }, params[:user_block_period] )) %>
+  </p>
+  <p>
+    <%= f.check_box :needs_view %>
+    <%= f.label :needs_view, t('user_block.new.needs_view') %>
+  </p>
+  <p>
+    <%= hidden_field_tag 'display_name', @display_name %>
+    <%= f.submit t('user_block.new.submit') %>
+  </p>
+<% end %>
+
+<%= link_to 'Back', user_blocks_path %>
diff --git a/app/views/user_blocks/revoke.html.erb b/app/views/user_blocks/revoke.html.erb
new file mode 100644 (file)
index 0000000..b1a4dea
--- /dev/null
@@ -0,0 +1,25 @@
+<h1><%= t('user_block.revoke.heading', 
+       :block_on => @user_block.user.display_name, 
+        :block_by => @user_block.moderator.display_name) %></h1>
+
+<% if @user_block.end_at > Time.now %>
+<p><b>
+  <%= t('user_block.revoke.time_future', :time => distance_of_time_in_words_to_now(@user_block.end_at)) %>
+</b></p>
+
+<% form_for :revoke, :url => { :action => "revoke" } do |f| %>
+  <%= f.error_messages %>
+<p>  
+  <%= check_box_tag 'confirm', 'yes' %>
+  <%= label_tag 'confirm', t('user_block.revoke.confirm') %>
+</p>
+<p>
+  <%= submit_tag t('user_block.revoke.revoke') %>
+</p>
+<% end %>
+
+<% else %>
+<p>
+  <%= t('user_block.revoke.past', :time => distance_of_time_in_words_to_now(@user_block.end_at)) %>
+</p>
+<% end %>
diff --git a/app/views/user_blocks/show.html.erb b/app/views/user_blocks/show.html.erb
new file mode 100644 (file)
index 0000000..f8e8b52
--- /dev/null
@@ -0,0 +1,38 @@
+<h1><%= t('user_block.show.heading', 
+       :block_on => @user_block.user.display_name, 
+        :block_by => @user_block.moderator.display_name) %></h1>
+
+<% if @user_block.revoker %>
+<p>
+  <b><%= t'user_block.show.revoker' %></b>
+  <%= link_to h(@user_block.revoker.display_name), :controller => 'user', :action => 'view', :display_name => @user_block.revoker.display_name %>
+</p>
+<% end %>
+
+<p>
+  <% if @user_block.end_at > Time.now %>
+    <%= t('user_block.show.time_future', :time => distance_of_time_in_words_to_now(@user_block.end_at)) %>
+  <% else %>
+    <%= t('user_block.show.time_past', :time => distance_of_time_in_words_to_now(@user_block.end_at)) %>
+  <% end %>
+</p>
+
+<% if @user_block.needs_view %>
+<p><%= t'user_block.show.needs_view' %></p>
+<% end %>
+
+<p>
+  <b><%= t'user_block.show.reason' %></b>
+  <%=h @user_block.reason %>
+</p>
+
+
+<% if @user_block.end_at > Time.now.getutc %>
+<% if @user and @user.id == @user_block.moderator_id %>
+<%= link_to t('user_block.show.edit'), edit_user_block_path(@user_block) %> |
+<% end %>
+<% if @user and @user.moderator? %>
+<%= link_to(t('user_block.show.revoke'),{:controller => 'user_blocks', :action => 'revoke', :id => @user_block.id}) %> |
+<% end %>
+<% end %>
+<%= link_to t('user_block.show.back'), user_blocks_path %>
diff --git a/app/views/user_roles/edit.html.erb b/app/views/user_roles/edit.html.erb
new file mode 100644 (file)
index 0000000..609b426
--- /dev/null
@@ -0,0 +1,20 @@
+<h1>Editing user_role</h1>
+
+<% form_for(@user_role) do |f| %>
+  <%= f.error_messages %>
+
+  <p>
+    <%= f.label :user_id %><br />
+    <%= f.text_field :user_id %>
+  </p>
+  <p>
+    <%= f.label :role %><br />
+    <%= f.text_field :role %>
+  </p>
+  <p>
+    <%= f.submit 'Update' %>
+  </p>
+<% end %>
+
+<%= link_to 'Show', @user_role %> |
+<%= link_to 'Back', user_roles_path %>
\ No newline at end of file
diff --git a/app/views/user_roles/index.html.erb b/app/views/user_roles/index.html.erb
new file mode 100644 (file)
index 0000000..e245d68
--- /dev/null
@@ -0,0 +1,22 @@
+<h1>Listing user_roles</h1>
+
+<table>
+  <tr>
+    <th>User</th>
+    <th>Role</th>
+  </tr>
+
+<% @user_roles.each do |user_role| %>
+  <tr>
+    <td><%=h user_role.user_id %></td>
+    <td><%=h user_role.role %></td>
+    <td><%= link_to 'Show', user_role %></td>
+    <td><%= link_to 'Edit', edit_user_role_path(user_role) %></td>
+    <td><%= link_to 'Destroy', user_role, :confirm => 'Are you sure?', :method => :delete %></td>
+  </tr>
+<% end %>
+</table>
+
+<br />
+
+<%= link_to 'New user_role', new_user_role_path %>
\ No newline at end of file
diff --git a/app/views/user_roles/new.html.erb b/app/views/user_roles/new.html.erb
new file mode 100644 (file)
index 0000000..0791993
--- /dev/null
@@ -0,0 +1,19 @@
+<h1>New user_role</h1>
+
+<% form_for(@user_role) do |f| %>
+  <%= f.error_messages %>
+
+  <p>
+    <%= f.label :user_id %><br />
+    <%= f.text_field :user_id %>
+  </p>
+  <p>
+    <%= f.label :role %><br />
+    <%= f.text_field :role %>
+  </p>
+  <p>
+    <%= f.submit 'Create' %>
+  </p>
+<% end %>
+
+<%= link_to 'Back', user_roles_path %>
\ No newline at end of file
diff --git a/app/views/user_roles/show.html.erb b/app/views/user_roles/show.html.erb
new file mode 100644 (file)
index 0000000..3db11af
--- /dev/null
@@ -0,0 +1,13 @@
+<p>
+  <b>User:</b>
+  <%=h @user_role.user_id %>
+</p>
+
+<p>
+  <b>Role:</b>
+  <%=h @user_role.role %>
+</p>
+
+
+<%= link_to 'Edit', edit_user_role_path(@user_role) %> |
+<%= link_to 'Back', user_roles_path %>
\ No newline at end of file
index 9331e91e7e447cd21d108add4d4eea36db03db81..8220a91e925f4edca6368b25e69c13bd1b243b02 100644 (file)
@@ -944,6 +944,12 @@ en:
       no nearby users: "There are no users who admit to mapping nearby yet."
       change your settings: change your settings
       my_oauth_details: "View my OAuth details"
+      role:
+        administrator: "Administrator"
+        moderator: "Moderator"
+      block_history: "view blocks received"
+      moderator_history: "view blocks given"
+      create_block: "block this user"
     friend_map:
       your location: Your location
       nearby mapper: "Nearby mapper: [[nearby_user]]"
@@ -993,3 +999,78 @@ en:
     remove_friend:
       success: "{{name}} was removed from your friends."
       not_a_friend: "{{name}} is not one of your friends."
+  user_role:
+    grant:
+      fail: "Couldn't grant role `{{role}}' to user `{{name}}'. Please check that the user and role are both valid."
+    revoke:
+      fail: "Couldn't revoke role `{{role}}' from user `{{name}}'. Please check that the user and role are both valid."
+  user_block:
+    new:
+      reason: "The reason why {{name}} is being blocked. Please be as calm and as reasonable as possible, giving as much detail as you can about the situation. Bear in mind that not all users understand the community jargon, so please try to use laymans terms."
+      period: "How long, starting now, the user will be blocked from the API for."
+      submit: "Create block"
+      tried_contacting: "I have contacted the user and asked them to stop."
+      tried_waiting: "I have given a reasonable amount of time for the user to respond to those communications."
+      title: "Creating block on {{name}}"
+      needs_view: "Does the user need to log in before this block will be cleared?"
+    edit:
+      reason: "The reason why {{name}} is being blocked. Please be as calm and as reasonable as possible, giving as much detail as you can about the situation. Bear in mind that not all users understand the community jargon, so please try to use laymans terms."
+      period: "How long, starting now, the user will be blocked from the API for."
+      submit: "Update block"
+      show: "Show"
+      back: "Back"
+      title: "Editing block on {{name}}"
+      needs_view: "Does the user need to log in before this block will be cleared?"
+    create:
+      not_a_moderator: "User block could not be created: you are not a moderator."
+      try_contacting: "Please try contacting the user before blocking them and giving them a reasonable time to respond."
+      try_waiting: "Please try giving the user a reasonable time to respond before blocking them."
+      bad_parameters: "Could not create a new block due to bad parameters. Maybe the blocking period is not valid?"
+      flash: "Created a block on user {{name}}."
+    update:
+      only_creator_can_edit: "Only the moderator who created this block can edit it."
+      block_expired: "The block has already expired and cannot be edited."
+      success: "Block updated."
+    index:
+      heading: "Listing User Blocks"
+    revoke:
+      heading: "Revoking block on {{block_on}} by {{block_by}}"
+      time_future: "This block will end in {{time}}."
+      past: "This block ended {{time}} ago and cannot be revoked now."
+      confirm: "Are you sure you wish to revoke this block?"
+      revoke: "Revoke!"
+      flash: "This block has been revoked."
+    period:
+      one: "1 hour"
+      other: "{{count}} hours"
+    partial:
+      show: "Show"
+      edit: "Edit"
+      revoke: "Revoke!"
+      confirm: "Are you sure?"
+      display_name: "Blocked User"
+      moderator_name: "Moderator"
+      reason: "Reason for block"
+      status: "Status"
+      revoker_name: "Revoked by"
+      not_revoked: "(not revoked)"
+      time_future: "Ends in {{time}}"
+      until_login: "Until the user logs in"
+      not_active: "(not active)"
+    blocks_on:
+      heading: "List blocks on {{name}}"
+    blocks_by:
+      heading: "List blocks by {{name}}"
+    show:
+      heading: "Block on {{block_on}} by {{block_by}}"
+      time_future: "Ends in {{time}}"
+      time_past: "Ended {{time}} ago"
+      show: "Show"
+      edit: "Edit"
+      revoke: "Revoke!"
+      confirm: "Are you sure?"
+      reason: "Reason for block:"
+      back: "Back"
+      revoker: "Revoker:"
+      needs_view: "The user needs to log in before this block will be cleared."
+
index b8d3fa1a089c48c9dd1c8ce917e50616a3cdd45a..6072cdcb1eca5be512e74da67897bea4092d53d0 100644 (file)
@@ -1,5 +1,4 @@
 ActionController::Routing::Routes.draw do |map|
-
   # API
   map.connect "api/capabilities", :controller => 'api', :action => 'capabilities'
   map.connect "api/#{API_VERSION}/capabilities", :controller => 'api', :action => 'capabilities'
@@ -202,6 +201,14 @@ ActionController::Routing::Routes.draw do |map|
   map.access_token '/oauth/access_token', :controller => 'oauth', :action => 'access_token'
   map.test_request '/oauth/test_request', :controller => 'oauth', :action => 'test_request'
 
+  # roles and banning pages
+  map.connect '/user/:display_name/role/:role/grant', :controller => 'user_roles', :action => 'grant'
+  map.connect '/user/:display_name/role/:role/revoke', :controller => 'user_roles', :action => 'revoke'
+  map.connect '/user/:display_name/blocks', :controller => 'user_blocks', :action => 'blocks_on'
+  map.connect '/user/:display_name/blocks_by', :controller => 'user_blocks', :action => 'blocks_by'
+  map.resources :user_blocks, :as => 'blocks'
+  map.connect '/blocks/:id/revoke', :controller => 'user_blocks', :action => 'revoke'
+
   # fall through
   map.connect ':controller/:id/:action'
   map.connect ':controller/:action'
diff --git a/db/migrate/044_create_user_roles.rb b/db/migrate/044_create_user_roles.rb
new file mode 100644 (file)
index 0000000..39e224c
--- /dev/null
@@ -0,0 +1,33 @@
+require 'lib/migrate'
+
+class CreateUserRoles < ActiveRecord::Migration
+  def self.up
+    create_enumeration :user_role_enum, ["administrator", "moderator"]
+
+    create_table :user_roles do |t|
+      t.column :user_id, :bigint, :null => false
+
+      t.timestamps
+    end
+    add_column :user_roles, :role, :user_role_enum, :null => false
+
+    User.all(:conditions => ['administrator = ?', true]).each do |user|
+      UserRole.create(:user_id => user.id, :role => "administrator")
+    end
+    remove_column :users, :administrator
+
+    add_foreign_key :user_roles, [:user_id], :users, [:id]
+    add_index :user_roles, [:user_id]
+  end
+
+  def self.down
+    add_column :users, :administrator, :boolean, :default => false, :null => false
+    UserRole.all(:conditions => ['role = ?', "administrator"]).each do |role|
+      user = User.find(role.user_id)
+      user.administrator = true
+      user.save!
+    end
+    drop_table :user_roles
+    drop_enumeration :user_role_enum
+  end
+end
diff --git a/db/migrate/045_create_user_blocks.rb b/db/migrate/045_create_user_blocks.rb
new file mode 100644 (file)
index 0000000..7276ef1
--- /dev/null
@@ -0,0 +1,26 @@
+require 'lib/migrate'
+
+class CreateUserBlocks < ActiveRecord::Migration
+  def self.up
+    create_table :user_blocks do |t|
+      t.column :user_id,      :bigint,   :null => false
+      t.column :moderator_id, :bigint,   :null => false
+      t.column :reason,       :text,     :null => false
+      t.column :end_at,       :datetime, :null => false
+      t.column :needs_view,   :boolean,  :null => false, :default => false
+      t.column :revoker_id,   :bigint
+
+      t.timestamps
+    end
+
+    add_foreign_key :user_blocks, [:user_id], :users, [:id]
+    add_foreign_key :user_blocks, [:moderator_id], :users, [:id]
+    add_foreign_key :user_blocks, [:revoker_id], :users, [:id]
+
+    add_index :user_blocks, [:user_id]
+  end
+
+  def self.down
+    drop_table :user_blocks
+  end
+end
diff --git a/public/403.html b/public/403.html
new file mode 100644 (file)
index 0000000..667b7c7
--- /dev/null
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+   "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<body>
+  <img src="http://www.openstreetmap.org/images/osm_logo.png" style="float:left; margin:10px">
+  <div style="float:left;">
+    <h1>Forbidden</h1>  
+    <p>The operation you requested on the OpenStreetMap server is only available to administrators (HTTP 403)</p>
+    <p>Feel free to <a href="http://wiki.openstreetmap.org/wiki/Contact" title="Various contact channels explained">contact</a> the OpenStreetMap community if you have found a broken link / bug. Make a note of the exact URL of your request.</p>
+  </div>
+</body>
+</html>
diff --git a/public/images/roles/administrator.png b/public/images/roles/administrator.png
new file mode 100644 (file)
index 0000000..b566dec
Binary files /dev/null and b/public/images/roles/administrator.png differ
diff --git a/public/images/roles/blank_administrator.png b/public/images/roles/blank_administrator.png
new file mode 100644 (file)
index 0000000..c68f9e4
Binary files /dev/null and b/public/images/roles/blank_administrator.png differ
diff --git a/public/images/roles/blank_moderator.png b/public/images/roles/blank_moderator.png
new file mode 100644 (file)
index 0000000..cf5ceea
Binary files /dev/null and b/public/images/roles/blank_moderator.png differ
diff --git a/public/images/roles/moderator.png b/public/images/roles/moderator.png
new file mode 100644 (file)
index 0000000..eaedf95
Binary files /dev/null and b/public/images/roles/moderator.png differ
diff --git a/test/fixtures/user_roles.yml b/test/fixtures/user_roles.yml
new file mode 100644 (file)
index 0000000..fd568da
--- /dev/null
@@ -0,0 +1,9 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+
+administrator:
+  user_id: 6
+  role: administrator
+
+moderator:
+  user_id: 5
+  role: moderator
index ac0029a62eee370c946f262c3db60c0037c3ef33..1849fd4d99aae8ddeb603e51f1d58786aa1e4be1 100644 (file)
@@ -51,3 +51,20 @@ second_public_user:
   home_lon: 87
   home_zoom: 12
 
+moderator_user:
+  id: 5
+  email: moderator@example.com
+  active: true
+  pass_crypt: <%= Digest::MD5.hexdigest('test') %>
+  creation_time: "2008-05-01 01:23:45"
+  display_name: moderator
+  data_public: true
+
+administrator_user:
+  id: 6
+  email: administrator@example.com
+  active: true
+  pass_crypt: <%= Digest::MD5.hexdigest('test') %>
+  creation_time: "2008-05-01 01:23:45"
+  display_name: administrator
+  data_public: true
diff --git a/test/functional/user_roles_controller_test.rb b/test/functional/user_roles_controller_test.rb
new file mode 100644 (file)
index 0000000..c2de539
--- /dev/null
@@ -0,0 +1,28 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class UserRolesControllerTest < ActionController::TestCase
+  fixtures :users, :user_roles
+
+  test "grant" do
+    check_redirect(:grant, :public_user, "/403.html")
+    check_redirect(:grant, :moderator_user, "/403.html")
+    check_redirect(:grant, :administrator_user, {:controller => :user, :action => :view})
+  end
+
+  test "revoke" do
+    check_redirect(:revoke, :public_user, "/403.html")
+    check_redirect(:revoke, :moderator_user, "/403.html")
+    check_redirect(:revoke, :administrator_user, {:controller => :user, :action => :view})
+  end
+
+  def check_redirect(action, user, redirect)
+    UserRole::ALL_ROLES.each do |role|
+      u = users(user)
+      basic_authorization(u.email, "test")
+      
+      get(action, {:display_name => users(:second_public_user).display_name, :role => role}, {'user' => u.id})
+      assert_response :redirect
+      assert_redirected_to redirect
+    end
+  end
+end