Skip to content

Commit

Permalink
Add CommunityMember.
Browse files Browse the repository at this point in the history
Allow multiple organizers per community.  There is now just 1 leader of an community.

Allow stepping up if there's no organizer.

Add ability to remove memberships.
  • Loading branch information
openbrian committed Jul 31, 2024
1 parent 468d58c commit 54c8ae8
Show file tree
Hide file tree
Showing 28 changed files with 964 additions and 58 deletions.
15 changes: 12 additions & 3 deletions app/abilities/ability.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def initialize(user)
can :index, ChangesetComment
can [:index, :show], Community
can [:index], CommunityLink
can [:index], CommunityMember
can [:confirm, :confirm_resend, :confirm_email], :confirmation
can [:index, :rss, :show], DiaryEntry
can :index, DiaryComment
Expand Down Expand Up @@ -49,9 +50,17 @@ def initialize(user)
can [:create], DiaryComment
can [:make_friend, :remove_friend], Friendship
can [:new, :create, :reply, :show, :inbox, :outbox, :muted, :mark, :unmute, :destroy], Message
can [:create, :new], Community
can [:edit, :update], Community, { :organizer_id => user.id }
can [:edit, :create, :destroy, :new, :update], CommunityLink, { :community => { :organizer_id => user.id } }
user_is_community_organizer = {
:community_members => {
:user_id => user.id,
:role => CommunityMember::Roles::ORGANIZER
}
}
can [:create, :new, :step_up], Community
can [:edit, :update], Community, user_is_community_organizer
can [:edit, :create, :destroy, :new, :update], CommunityLink, { :community => user_is_community_organizer }
can [:create], CommunityMember, { :user_id => user.id }
can [:destroy, :edit, :update], CommunityMember, { :community => user_is_community_organizer }
can [:close, :reopen], Note
can [:show, :edit, :update], :preference
can [:edit, :update], :profile
Expand Down
31 changes: 31 additions & 0 deletions app/assets/stylesheets/common.scss
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,37 @@ img.user_thumbnail_tiny {
object-fit: contain;
}

.user_card {
@extend .card;
width: 100px;
display: inline-block;
> img {
@extend .card-img-top;
}
> div {
@extend .card-body;
/*
Some display_names are long and break the uniform display of user cards.
For now, hide the overflow. This does make some user names ambiguous.
TODO: Fix this.
*/
overflow: hidden;
white-space: nowrap;
padding: 0.5rem; /* override bootstrap's 1.25rem */
> h5 {
@extend .card-title;
font-size: 0.9rem; /* override bootstrap's 1.25 rem */
}
}
}


/* Rules for geo microformats */

abbr.geo {
border-bottom: none;
}

/* General styles for action lists / subnavs */

nav.secondary-actions {
Expand Down
56 changes: 49 additions & 7 deletions app/controllers/communities_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,48 @@ class CommunitiesController < ApplicationController
layout "site"
before_action :authorize_web

before_action :set_community, :only => [:edit, :show, :update]
before_action :set_community, :only => [:edit, :show, :step_up, :update]

helper_method :recent_changesets

load_and_authorize_resource :except => [:create, :new]
authorize_resource

def index
@critical_mass = 2
display_name = params[:user_display_name]
if display_name
@user = User.active.find_by(:display_name => display_name)
if @user
@title = t ".title", :display_name => @user.display_name
@communities_organized = @user.communities_organized
@communities_leading = @user.communities_lead
else
render_unknown_user display_name
return
end
elsif current_user
@title = t ".title", :display_name => current_user.display_name
@communities_organized = current_user.communities_organized
@communities_leading = current_user.communities_lead
end

@all_communities = Community.order(:name)
# Only list out communities that have at least n members in order to mitigate spam. In order to get
# a community listed, the organizer must find n members and give them the link to the page manually.
@all_communities = Community
.joins(:community_members)
.group("communities.id")
.having("COUNT(communities.id) > #{@critical_mass}")

@my_communities = current_user ? current_user.communities : []
end

# GET /communities/mycity
# GET /communities/mycity.json
def show; end
def show
# for existing or new member
@current_user_membership = CommunityMember.find_or_initialize_by(
:community => @community, :user_id => current_user&.id
)
end

def new
@title = t ".title"
Expand All @@ -43,8 +56,8 @@ def edit; end

def create
@community = Community.new(community_params)
@community.organizer = current_user
if @community.save
@community.leader = current_user
if @community.save && add_first_organizer
redirect_to @community, :notice => t(".success")
else
render "new"
Expand All @@ -60,8 +73,34 @@ def update
end
end

def step_up
message = nil
if @community.organizers.empty?
if @community.member?(current_user)
message = t ".you_have_stepped_up"
add_first_organizer
else
message = t ".only_members_can_step_up"
end
else
message = t ".already_has_organizer"
end
redirect_to @community, :notice => message
end

private

def add_first_organizer
membership = CommunityMember.new(
{
:community_id => @community.id,
:user_id => current_user.id,
:role => CommunityMember::Roles::ORGANIZER
}
)
membership.save
end

def recent_changesets
bbox = @community.bbox.to_scaled
Changeset
Expand All @@ -72,6 +111,9 @@ def recent_changesets

def set_community
@community = Community.friendly.find(params[:id])
rescue ActiveRecord::RecordNotFound
@not_found_community = params[:id]
render "no_such_community", :status => :not_found
end

def community_params
Expand Down
68 changes: 68 additions & 0 deletions app/controllers/community_members_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
class CommunityMembersController < ApplicationController
layout "site"
before_action :authorize_web
before_action :set_community_member, :only => [:destroy, :edit, :update]
load_and_authorize_resource :except => [:create]
authorize_resource

def index
@community = Community.friendly.find(params[:community_id])
@roles = CommunityMember::Roles::ALL_ROLES.map(&:pluralize)
rescue ActiveRecord::RecordNotFound
@not_found_community = params[:community_id]
render :template => "communities/no_such_community", :status => :not_found
end

def edit; end

def create
# membership = CommunityMember.new(create_params)
# If there's no given user, default to the current_user.
membership = CommunityMember.new(create_params.reverse_merge!(:user_id => current_user.id))
membership.role = CommunityMember::Roles::MEMBER
if membership.save
redirect_to community_path(membership.community), :notice => t(".success")
else
# There are 2 reasons we may get here.
# 1. database failure / disk full
# 2. the community does not exist
# Either way, sending the user to the communities list page is ok.
redirect_to communities_path, :alert => t(".failure")
end
end

def update
if @community_member.update(update_params)
redirect_to @community_member.community, :notice => t(".success")
else
flash.now[:alert] = t(".failure")
render :edit
end
end

def destroy
issues = @community_member.can_be_deleted
if issues.empty? && @community_member.destroy
redirect_to @community_member.community, :notice => t(".success")
else
issues = issues.map { |i| t("activerecord.errors.models.community_member.#{i}") }
issues = issues.to_sentence.capitalize
flash[:error] = "#{t('.failure')} #{issues}."
redirect_to community_community_members_path(@community_member.community)
end
end

private

def set_community_member
@community_member = CommunityMember.find(params[:id])
end

def create_params
params.require(:community_member).permit(:community_id, :user_id)
end

def update_params
params.require(:community_member).permit(:role)
end
end
29 changes: 25 additions & 4 deletions app/models/community.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# id :bigint(8) not null, primary key
# name :string not null
# description :text not null
# organizer_id :bigint(8) not null
# leader_id :bigint(8) not null
# slug :string not null
# location :string not null
# latitude :float not null
Expand All @@ -27,14 +27,19 @@
# fk_rails_... (organizer_id => users.id)
#

# At this time a community has one organizer. The first organizer is
# the user that created the community.
# At this time a community has one leader. The leader of a community starts out
# being the user that created the community. The creator of a community also
# is an organizer member.

class Community < ApplicationRecord
extend FriendlyId
friendly_id :name, :use => :slugged

belongs_to :organizer, :class_name => "User"
# Organizers before members, a tad hacky, but works for now.
has_many :community_members, -> { order(:user_id) }, :inverse_of => :community
has_many :users, :through => :community_members # TODO: counter_cache

belongs_to :leader, :class_name => "User"
has_many :community_links

validates :name, :presence => true, :length => 1..255, :characters => true
Expand All @@ -59,6 +64,22 @@ def max_lon=(longitude)
super(OSM.normalize_longitude(longitude))
end

def member?(user)
community_members.where(:user_id => user.id).count.positive?
end

def members
community_members.where(:role => CommunityMember::Roles::MEMBER)
end

def organizer?(user)
community_members.where(:user_id => user.id, :role => CommunityMember::Roles::ORGANIZER).count.positive?
end

def organizers
community_members.where(:role => CommunityMember::Roles::ORGANIZER)
end

def bbox
BoundingBox.new(min_lon, min_lat, max_lon, max_lat)
end
Expand Down
46 changes: 46 additions & 0 deletions app/models/community_member.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# == Schema Information
#
# Table name: community_members
#
# id :bigint(8) not null, primary key
# community_id :integer not null
# user_id :integer not null
# role :string(64) not null
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_community_members_on_community_id (community_id)
# index_community_members_on_community_id_and_user_id_and_role (community_id,user_id,role) UNIQUE
# index_community_members_on_user_id (user_id)
#

class CommunityMember < ApplicationRecord
module Roles
ORGANIZER = "organizer".freeze
MEMBER = "member".freeze
ALL_ROLES = [ORGANIZER, MEMBER].freeze
end

belongs_to :community
belongs_to :user

scope :organizers, -> { where(:role => Roles::ORGANIZER) }
scope :members, -> { where(:role => Roles::MEMBER) }

validates :community, :associated => true
validates :user, :associated => true
validates :role, :inclusion => { :in => Roles::ALL_ROLES }

# TODO: validate uniqueness of user's role in each community.

# We assume this user already belongs to this community.
def can_be_deleted
issues = []
# The user may also be an organizer under a separate membership.
issues.append(:is_organizer) if CommunityMember.exists?(:community_id => community_id, :user_id => user_id, :role => Roles::ORGANIZER)

issues
end
end
2 changes: 1 addition & 1 deletion app/models/issue.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def set_reported_user
when "User"
reportable
when "Community"
reportable.organizer
reportable.leader
when "Note"
reportable.author
else
Expand Down
4 changes: 3 additions & 1 deletion app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,9 @@ class User < ApplicationRecord

has_many :reports

has_many :communities_organized, :class_name => "Community", :foreign_key => :organizer_id, :inverse_of => :organizer
has_many :communities_lead, :class_name => "Community", :foreign_key => :leader_id, :inverse_of => :leader
has_many :community_members
has_many :communities, :through => :community_members

scope :visible, -> { where(:status => %w[pending active confirmed]) }
scope :active, -> { where(:status => %w[active confirmed]) }
Expand Down
7 changes: 5 additions & 2 deletions app/views/communities/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@
</nav>
<% end %>
<% if @communities_organized %>
<%= render :partial => "index_list", :locals => { :communities => @communities_organized, :header => t(".communities_organized") } %>
<% if @communities_leading %>
<%= render :partial => "index_list", :locals => { :communities => @communities_leading, :header => t(".communities_leading") } %>
<% end %>
<%= render :partial => "index_list", :locals => { :communities => @my_communities, :header => t(".my_communities") } %>

<p>
<%= t(".sorted_by") %>
<%= t(".critical_mass", :n => @critical_mass) %>
</p>

<%= render :partial => "index_list", :locals => { :communities => @all_communities, :header => t(".all") } %>
8 changes: 8 additions & 0 deletions app/views/communities/no_such_community.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<% content_for :heading do %>
<h1>
<%= t ".heading", :community => @not_found_community %>
</h1>
<% end %>
<p>
<%= t ".body", :community => @not_found_community %>
</p>
Loading

0 comments on commit 54c8ae8

Please sign in to comment.