Skip to content

Commit 3ddca0f

Browse files
committed
Add CommunityMember.
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.
1 parent 468d58c commit 3ddca0f

29 files changed

+1021
-75
lines changed

app/abilities/ability.rb

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ def initialize(user)
1919
can :index, ChangesetComment
2020
can [:index, :show], Community
2121
can [:index], CommunityLink
22+
can [:index], CommunityMember
2223
can [:confirm, :confirm_resend, :confirm_email], :confirmation
2324
can [:index, :rss, :show], DiaryEntry
2425
can :index, DiaryComment
@@ -49,9 +50,17 @@ def initialize(user)
4950
can [:create], DiaryComment
5051
can [:make_friend, :remove_friend], Friendship
5152
can [:new, :create, :reply, :show, :inbox, :outbox, :muted, :mark, :unmute, :destroy], Message
52-
can [:create, :new], Community
53-
can [:edit, :update], Community, { :organizer_id => user.id }
54-
can [:edit, :create, :destroy, :new, :update], CommunityLink, { :community => { :organizer_id => user.id } }
53+
user_is_community_organizer = {
54+
:community_members => {
55+
:user_id => user.id,
56+
:role => CommunityMember::Roles::ORGANIZER
57+
}
58+
}
59+
can [:create, :new, :step_up], Community
60+
can [:edit, :update], Community, user_is_community_organizer
61+
can [:edit, :create, :destroy, :new, :update], CommunityLink, { :community => user_is_community_organizer }
62+
can [:create], CommunityMember, { :user_id => user.id }
63+
can [:destroy, :edit, :update], CommunityMember, { :community => user_is_community_organizer }
5564
can [:close, :reopen], Note
5665
can [:show, :edit, :update], :preference
5766
can [:edit, :update], :profile

app/assets/stylesheets/common.scss

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,37 @@ img.user_thumbnail_tiny {
802802
object-fit: contain;
803803
}
804804

805+
.user_card {
806+
@extend .card;
807+
width: 100px;
808+
display: inline-block;
809+
> img {
810+
@extend .card-img-top;
811+
}
812+
> div {
813+
@extend .card-body;
814+
/*
815+
Some display_names are long and break the uniform display of user cards.
816+
For now, hide the overflow. This does make some user names ambiguous.
817+
TODO: Fix this.
818+
*/
819+
overflow: hidden;
820+
white-space: nowrap;
821+
padding: 0.5rem; /* override bootstrap's 1.25rem */
822+
> h5 {
823+
@extend .card-title;
824+
font-size: 0.9rem; /* override bootstrap's 1.25 rem */
825+
}
826+
}
827+
}
828+
829+
830+
/* Rules for geo microformats */
831+
832+
abbr.geo {
833+
border-bottom: none;
834+
}
835+
805836
/* General styles for action lists / subnavs */
806837

807838
nav.secondary-actions {

app/controllers/communities_controller.rb

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,48 @@ class CommunitiesController < ApplicationController
44
layout "site"
55
before_action :authorize_web
66

7-
before_action :set_community, :only => [:edit, :show, :update]
7+
before_action :set_community, :only => [:edit, :show, :step_up, :update]
88

99
helper_method :recent_changesets
1010

1111
load_and_authorize_resource :except => [:create, :new]
1212
authorize_resource
1313

1414
def index
15+
@critical_mass = 2
1516
display_name = params[:user_display_name]
1617
if display_name
1718
@user = User.active.find_by(:display_name => display_name)
1819
if @user
1920
@title = t ".title", :display_name => @user.display_name
20-
@communities_organized = @user.communities_organized
21+
@communities_leading = @user.communities_lead
2122
else
2223
render_unknown_user display_name
2324
return
2425
end
2526
elsif current_user
2627
@title = t ".title", :display_name => current_user.display_name
27-
@communities_organized = current_user.communities_organized
28+
@communities_leading = current_user.communities_lead
2829
end
2930

30-
@all_communities = Community.order(:name)
31+
# Only list out communities that have at least n members in order to mitigate spam. In order to get
32+
# a community listed, the organizer must find n members and give them the link to the page manually.
33+
@all_communities = Community
34+
.joins(:community_members)
35+
.group("communities.id")
36+
.having("COUNT(communities.id) > #{@critical_mass}")
37+
38+
@my_communities = current_user ? current_user.communities : []
3139
end
3240

3341
# GET /communities/mycity
3442
# GET /communities/mycity.json
35-
def show; end
43+
def show
44+
# for existing or new member
45+
@current_user_membership = CommunityMember.find_or_initialize_by(
46+
:community => @community, :user_id => current_user&.id
47+
)
48+
end
3649

3750
def new
3851
@title = t ".title"
@@ -43,8 +56,8 @@ def edit; end
4356

4457
def create
4558
@community = Community.new(community_params)
46-
@community.organizer = current_user
47-
if @community.save
59+
@community.leader = current_user
60+
if @community.save && add_first_organizer
4861
redirect_to @community, :notice => t(".success")
4962
else
5063
render "new"
@@ -60,8 +73,34 @@ def update
6073
end
6174
end
6275

76+
def step_up
77+
message = nil
78+
if @community.organizers.empty?
79+
if @community.member?(current_user)
80+
message = t ".you_have_stepped_up"
81+
add_first_organizer
82+
else
83+
message = t ".only_members_can_step_up"
84+
end
85+
else
86+
message = t ".already_has_organizer"
87+
end
88+
redirect_to @community, :notice => message
89+
end
90+
6391
private
6492

93+
def add_first_organizer
94+
membership = CommunityMember.new(
95+
{
96+
:community_id => @community.id,
97+
:user_id => current_user.id,
98+
:role => CommunityMember::Roles::ORGANIZER
99+
}
100+
)
101+
membership.save
102+
end
103+
65104
def recent_changesets
66105
bbox = @community.bbox.to_scaled
67106
Changeset
@@ -72,6 +111,9 @@ def recent_changesets
72111

73112
def set_community
74113
@community = Community.friendly.find(params[:id])
114+
rescue ActiveRecord::RecordNotFound
115+
@not_found_community = params[:id]
116+
render "no_such_community", :status => :not_found
75117
end
76118

77119
def community_params
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
class CommunityMembersController < ApplicationController
2+
layout "site"
3+
before_action :authorize_web
4+
before_action :set_community_member, :only => [:destroy, :edit, :update]
5+
load_and_authorize_resource :except => [:create]
6+
authorize_resource
7+
8+
def index
9+
@community = Community.friendly.find(params[:community_id])
10+
@roles = CommunityMember::Roles::ALL_ROLES.map(&:pluralize)
11+
rescue ActiveRecord::RecordNotFound
12+
@not_found_community = params[:community_id]
13+
render :template => "communities/no_such_community", :status => :not_found
14+
end
15+
16+
def edit; end
17+
18+
def create
19+
# membership = CommunityMember.new(create_params)
20+
# If there's no given user, default to the current_user.
21+
membership = CommunityMember.new(create_params.reverse_merge!(:user_id => current_user.id))
22+
membership.role = CommunityMember::Roles::MEMBER
23+
if membership.save
24+
redirect_to community_path(membership.community), :notice => t(".success")
25+
else
26+
# There are 2 reasons we may get here.
27+
# 1. database failure / disk full
28+
# 2. the community does not exist
29+
# Either way, sending the user to the communities list page is ok.
30+
redirect_to communities_path, :alert => t(".failure")
31+
end
32+
end
33+
34+
def update
35+
if @community_member.update(update_params)
36+
redirect_to @community_member.community, :notice => t(".success")
37+
else
38+
flash.now[:alert] = t(".failure")
39+
render :edit
40+
end
41+
end
42+
43+
def destroy
44+
issues = @community_member.can_be_deleted
45+
if issues.empty? && @community_member.destroy
46+
redirect_to @community_member.community, :notice => t(".success")
47+
else
48+
issues = issues.map { |i| t("activerecord.errors.models.community_member.#{i}") }
49+
issues = issues.to_sentence.capitalize
50+
flash[:error] = "#{t('.failure')} #{issues}."
51+
redirect_to community_community_members_path(@community_member.community)
52+
end
53+
end
54+
55+
private
56+
57+
def set_community_member
58+
@community_member = CommunityMember.find(params[:id])
59+
end
60+
61+
def create_params
62+
params.require(:community_member).permit(:community_id, :user_id)
63+
end
64+
65+
def update_params
66+
params.require(:community_member).permit(:role)
67+
end
68+
end

app/models/community.rb

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,44 @@
22
#
33
# Table name: communities
44
#
5-
# id :bigint(8) not null, primary key
6-
# name :string not null
7-
# description :text not null
8-
# organizer_id :bigint(8) not null
9-
# slug :string not null
10-
# location :string not null
11-
# latitude :float not null
12-
# longitude :float not null
13-
# min_lat :float not null
14-
# max_lat :float not null
15-
# min_lon :float not null
16-
# max_lon :float not null
17-
# created_at :datetime not null
18-
# updated_at :datetime not null
5+
# id :bigint(8) not null, primary key
6+
# name :string not null
7+
# description :text not null
8+
# leader_id :bigint(8) not null
9+
# slug :string not null
10+
# location :string not null
11+
# latitude :float not null
12+
# longitude :float not null
13+
# min_lat :float not null
14+
# max_lat :float not null
15+
# min_lon :float not null
16+
# max_lon :float not null
17+
# created_at :datetime not null
18+
# updated_at :datetime not null
1919
#
2020
# Indexes
2121
#
22-
# index_communities_on_organizer_id (organizer_id)
23-
# index_communities_on_slug (slug) UNIQUE
22+
# index_communities_on_leader_id (leader_id)
23+
# index_communities_on_slug (slug) UNIQUE
2424
#
2525
# Foreign Keys
2626
#
27-
# fk_rails_... (organizer_id => users.id)
27+
# fk_rails_... (leader_id => users.id)
2828
#
2929

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

3334
class Community < ApplicationRecord
3435
extend FriendlyId
3536
friendly_id :name, :use => :slugged
3637

37-
belongs_to :organizer, :class_name => "User"
38+
# Organizers before members, a tad hacky, but works for now.
39+
has_many :community_members, -> { order(:user_id) }, :inverse_of => :community
40+
has_many :users, :through => :community_members # TODO: counter_cache
41+
42+
belongs_to :leader, :class_name => "User"
3843
has_many :community_links
3944

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

67+
def member?(user)
68+
community_members.where(:user_id => user.id).count.positive?
69+
end
70+
71+
def members
72+
community_members.where(:role => CommunityMember::Roles::MEMBER)
73+
end
74+
75+
def organizer?(user)
76+
community_members.where(:user_id => user.id, :role => CommunityMember::Roles::ORGANIZER).count.positive?
77+
end
78+
79+
def organizers
80+
community_members.where(:role => CommunityMember::Roles::ORGANIZER)
81+
end
82+
6283
def bbox
6384
BoundingBox.new(min_lon, min_lat, max_lon, max_lat)
6485
end

app/models/community_member.rb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# == Schema Information
2+
#
3+
# Table name: community_members
4+
#
5+
# id :bigint(8) not null, primary key
6+
# community_id :bigint(8) not null
7+
# user_id :bigint(8) not null
8+
# role :enum default("member"), not null
9+
# created_at :datetime not null
10+
# updated_at :datetime not null
11+
#
12+
# Indexes
13+
#
14+
# index_community_members_on_community_id (community_id)
15+
# index_community_members_on_community_id_and_user_id_and_role (community_id,user_id,role) UNIQUE
16+
# index_community_members_on_user_id (user_id)
17+
#
18+
# Foreign Keys
19+
#
20+
# fk_rails_... (community_id => communities.id)
21+
# fk_rails_... (user_id => users.id)
22+
#
23+
24+
class CommunityMember < ApplicationRecord
25+
module Roles
26+
ORGANIZER = "organizer".freeze
27+
MEMBER = "member".freeze
28+
ALL_ROLES = [ORGANIZER, MEMBER].freeze
29+
end
30+
31+
belongs_to :community
32+
belongs_to :user
33+
34+
scope :organizers, -> { where(:role => Roles::ORGANIZER) }
35+
scope :members, -> { where(:role => Roles::MEMBER) }
36+
37+
validates :community, :associated => true
38+
validates :user, :associated => true
39+
validates :role, :inclusion => { :in => Roles::ALL_ROLES }
40+
41+
# TODO: validate uniqueness of user's role in each community.
42+
43+
# We assume this user already belongs to this community.
44+
def can_be_deleted
45+
issues = []
46+
# The user may also be an organizer under a separate membership.
47+
issues.append(:is_organizer) if CommunityMember.exists?(:community_id => community_id, :user_id => user_id, :role => Roles::ORGANIZER)
48+
49+
issues
50+
end
51+
end

app/models/issue.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def set_reported_user
8787
when "User"
8888
reportable
8989
when "Community"
90-
reportable.organizer
90+
reportable.leader
9191
when "Note"
9292
reportable.author
9393
else

0 commit comments

Comments
 (0)