diff --git a/app/assets/javascripts/group.js b/app/assets/javascripts/group.js new file mode 100644 index 0000000000..e8b7424398 --- /dev/null +++ b/app/assets/javascripts/group.js @@ -0,0 +1,27 @@ +$(document).ready(function () { + var map = L.map("map", { + attributionControl: false, + zoomControl: false + }).addLayer(new L.OSM.Mapnik()); + + L.OSM.zoom() + .addTo(map); + + var userMarkers = [] + + $("[data-user]").each(function () { + var user = $(this).data('user'); + if (user.lon && user.lat) { + userMarkers.push( + L.marker([user.lat, user.lon], {icon: getUserIcon(user.icon)}) + .addTo(map) + .bindPopup(user.description) + ); + } + }); + + if (userMarkers.length > 0) { + var userLayer = L.featureGroup(userMarkers); + map.fitBounds(userLayer.getBounds()); + } +}); diff --git a/app/assets/javascripts/index.js b/app/assets/javascripts/index.js index 55e612f176..b6df0d182d 100644 --- a/app/assets/javascripts/index.js +++ b/app/assets/javascripts/index.js @@ -288,6 +288,7 @@ $(document).ready(function () { "/note/new": OSM.NewNote(map), "/history/friends": history, "/history/nearby": history, + "/history/group/:group_id": history, "/history": history, "/user/:display_name/history": history, "/note/:id": OSM.Note(map), diff --git a/app/assets/stylesheets/common.css.scss b/app/assets/stylesheets/common.css.scss index 088d4e07a1..fa15f837b3 100644 --- a/app/assets/stylesheets/common.css.scss +++ b/app/assets/stylesheets/common.css.scss @@ -1359,7 +1359,7 @@ header .search_form { } } -/* Rules for the user profile page */ +/* Rules for the user profile and group pages */ #userinformation { @@ -1405,11 +1405,12 @@ header .search_form { margin-bottom: 0; } -#friends-container .contact-activity ul { +#friends-container .contact-activity ul, +#members-container .contact-activity ul { margin-left: 70px; } -.user-view { +.user-view, .groups-show { // Silly exception; remove when user page is redesigned. .content-inner { max-width: none; diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index 735fa73a88..b5d7eb44b2 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -287,6 +287,13 @@ def list changesets = changesets.where(:user_id => @user.friend_users.public) elsif params[:nearby] && @user changesets = changesets.where(:user_id => @user.nearby) + elsif params[:group_id] + @group = Group.find_by_id(params[:group_id]) + if @group + changesets = changesets.where(:user_id => @group.users.public) + else + changesets = changesets.where("false") + end end if params[:max_id] diff --git a/app/controllers/diary_entry_controller.rb b/app/controllers/diary_entry_controller.rb index 091744e222..7bc2650f69 100644 --- a/app/controllers/diary_entry_controller.rb +++ b/app/controllers/diary_entry_controller.rb @@ -96,6 +96,15 @@ def list require_user return end + elsif params[:group_id] + @group = Group.find_by_id(params[:group_id]) + if @group + @title = t 'diary_entry.list.group_title', :group => @group.title + @entries = DiaryEntry.where(:group_id => @group.id) + else + render :action => "no_such_entry", :status => :not_found + return + end else @entries = DiaryEntry.joins(:user).where(:users => { :status => ["active", "confirmed"] }) @@ -184,7 +193,7 @@ def comments ## # return permitted diary entry parameters def entry_params - params.require(:diary_entry).permit(:title, :body, :language_code, :latitude, :longitude) + params.require(:diary_entry).permit(:title, :body, :language_code, :latitude, :longitude, :group_id) end ## diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb new file mode 100644 index 0000000000..09b1177efb --- /dev/null +++ b/app/controllers/groups_controller.rb @@ -0,0 +1,154 @@ +class GroupsController < ApplicationController + layout 'site' + + before_filter :authorize_web + before_filter :check_api_readable + before_filter :set_locale + after_filter :compress_output + around_filter :api_call_handle_error, + :api_call_timeout + before_filter :require_user, + :except => [ + :index, + :show + ] + + before_filter :find_group, + :only => [ + :show, + :edit, + :update, + :destroy, + :join, + :leave, + :become_leader, + :resign_leader + ] + + ## + # An index of Groups. + def index + @groups = Group.where('') + end + + ## + # The form for creating a new group. + # + def new + @group = Group.new + end + + ## + # Process the POST'ing of the new group form. + def create + @group = Group.new(group_params) + if @group.save + if defined?(@user) + @group.users << @user + @group.group_memberships.find_by_user_id(@user.id).set_role(GroupMembership::Roles::LEADER) + end + flash[:notice] = t 'group.create.success', + :title => @group.title + redirect_to group_url(@group) + else + render :action => "new" + end + end + + ## + # Details page for one group. + def show + end + + ## + # Form to edit an existing group, + def edit + end + + ## + # Process the PUT'ing of an existing group. + def update + if @group.update_attributes(params[:group]) + flash[:notice] = t 'group.update.success', :title => @group.title + redirect_to group_url(@group) + else + render :action => "edit" + end + end + + ## + # Delete an entire group. + def destroy + if @group.destroy + flash[:notice] = t 'group.destroy.success', :title => @group.title + else + flash[:error] = t 'group.destroy.error', :title => @group.title + end + redirect_to groups_url + end + + ## + # Add a new member to a group. + def join + if @group.users << @user + flash[:notice] = t 'group.join.success', :title => @group.title + else + flash[:error] = t 'group.join.error', :title => @group.title + end + redirect_to :back + end + + ## + # Remove a member from a group. + def leave + group_membership = @group.group_memberships.find_by_user_id(@user.id) + if group_membership.blank? + flash[:error] = t 'group.leave.not_in_group', :title => @group.title + elsif group_membership.destroy + flash[:notice] = t 'group.leave.success', :title => @group.title + else + flash[:error] = t 'group.leave.error', :title => @group.title + end + redirect_to :back + end + + ## + # + def become_leader + group_membership = @group.group_memberships.find_by_user_id(@user.id) + if group_membership.blank? + flash[:error] = t 'group.lead.not_in_group', :title => @group.title + elsif group_membership.set_role(GroupMembership::Roles::LEADER) + flash[:notice] = t 'group.lead.success', :title => @group.title + else + flash[:error] = t 'group.lead.error', :title => @group.title + end + redirect_to :back + end + + ## + # + def resign_leader + group_membership = @group.group_memberships.find_by_user_id(@user.id) + if group_membership.blank? || !group_membership.has_role?(GroupMembership::Roles::LEADER) + flash[:error] = t 'group.resign.not_leader', :title => @group.title + elsif group_membership.set_role(GroupMembership::Roles::MEMBER) + flash[:notice] = t 'group.resign.success', :title => @group.title + else + flash[:error] = t 'group.resign.error', :title => @group.title + end + redirect_to :back + end + +private + + ## + # return permitted message parameters + def group_params + params.require(:group).permit(:title, :description, :group_memberships_attributes) + end + + def find_group + @group = Group.find(params[:id]) + end +end diff --git a/app/controllers/message_controller.rb b/app/controllers/message_controller.rb index 38c9b2f3db..b59f30574b 100644 --- a/app/controllers/message_controller.rb +++ b/app/controllers/message_controller.rb @@ -33,6 +33,41 @@ def new end end + # Allow leaders of a group to send messages to all group members + def new_to_group + @group = Group.find(params[:group_id]) + + if request.post? && params[:message] + if !@group.leadership_includes?(@user) + flash[:error] = t 'message.new.not_group_leader' + elsif @user.sent_messages.where("sent_on >= ?", Time.now.getutc - 1.hour).count >= MAX_MESSAGES_PER_HOUR + flash[:error] = t 'message.new.limit_exceeded' + else + recipients = @group.users - [@user] + recipients.each do |user| + @message = Message.new(params[:message]) + @message.body = @message.body + <<-FOOTER.strip_heredoc + + + --- + + #{t 'message.new.footer_on_messages_to_group', :title => @group.title, :url => group_url(@group)} + FOOTER + @message.to_user_id = user.id + @message.from_user_id = @user.id + @message.sent_on = Time.now.getutc + if @message.save! + Notifier.message_notification(@message).deliver + end + end + flash[:notice] = t 'message.new.message_sent_to_entire_group', :number => recipients.count + redirect_to :controller => 'groups', :action => 'show', :id => @group.id + end + else + @title = t 'message.new.title' + end + end + # Allow the user to reply to another message. def reply message = Message.find(params[:message_id]) diff --git a/app/controllers/presets_controller.rb b/app/controllers/presets_controller.rb new file mode 100644 index 0000000000..f395245697 --- /dev/null +++ b/app/controllers/presets_controller.rb @@ -0,0 +1,72 @@ +class PresetsController < ApplicationController + + layout false + before_filter :check_api_readable + before_filter :check_api_writable + before_filter :setup_user_auth + before_filter :authorize + before_filter :set_locale + around_filter :api_call_handle_error, :api_call_timeout + after_filter :compress_output + before_action :set_preset, only: [:show, :edit, :update, :destroy] + + # GET /presets + def index + @presets = Preset.all + + respond_to do |format| + format.json { render :action => :index } +# format.xml { render :action => :show } + end + end + + # GET /presets/1 + def show + end + + # GET /presets/new + #def new + # @preset = Preset.new + #end + + # GET /presets/1/edit + #def edit + #end + + # POST /presets + def create + raise OSM::APIBadUserInput.new("No json was given") unless params[:json] + @preset = Preset.new(preset_params) + + @preset.save! + + respond_to do |format| + format.json { render :action => :show } +# format.xml { render :action => :show } + end + end + + # PATCH/PUT /presets/1 + def update + if @preset.update(preset_params) + render action: 'show' + end + end + + # DELETE /presets/1 + def destroy + @preset.destroy + #render action: 'index' + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_preset + @preset = Preset.find(params[:id]) + end + + # Only allow a trusted parameter "white list" through. + def preset_params + params.permit(:json) + end +end diff --git a/app/models/diary_entry.rb b/app/models/diary_entry.rb index 58f8710f95..3be107cb76 100644 --- a/app/models/diary_entry.rb +++ b/app/models/diary_entry.rb @@ -1,6 +1,7 @@ class DiaryEntry < ActiveRecord::Base belongs_to :user, :counter_cache => true belongs_to :language, :foreign_key => 'language_code' + belongs_to :group has_many :comments, -> { order(:id).preload(:user) }, :class_name => "DiaryComment" has_many :visible_comments, -> { joins(:user).where(:visible => true, :users => { :status => ["active", "confirmed"] }).order(:id) }, :class_name => "DiaryComment" diff --git a/app/models/group.rb b/app/models/group.rb new file mode 100644 index 0000000000..716a27dde3 --- /dev/null +++ b/app/models/group.rb @@ -0,0 +1,33 @@ +class Group < ActiveRecord::Base + has_many :group_memberships, :dependent => :destroy + has_many :users, :through => :group_memberships + has_many :leaders, + :class_name => 'User', + :source => :user, + :through => :group_memberships, + :conditions => { + :group_memberships => { + :role => GroupMembership::Roles::LEADER + } + } + + accepts_nested_attributes_for :group_memberships, :allow_destroy => true + + validates :title, :length => { :in => 3..250 } + validates :description, :length => { :in => 2..1000 } + + after_initialize :set_defaults + + def leadership_includes?(user) + group_memberships.where(:role => GroupMembership::Roles::LEADER, :user_id => user.id).count > 0 + end + + def description + RichText.new(read_attribute(:description_format), read_attribute(:description)) + end + +private + def set_defaults + self.description_format = "markdown" unless self.attribute_present?(:description_format) + end +end diff --git a/app/models/group_membership.rb b/app/models/group_membership.rb new file mode 100644 index 0000000000..3c55ed9d29 --- /dev/null +++ b/app/models/group_membership.rb @@ -0,0 +1,29 @@ +class GroupMembership < ActiveRecord::Base + belongs_to :group + belongs_to :user + + validates_uniqueness_of :user_id, :scope => :group_id + + ## + # a simple role system; possible to expand in the future + module Roles + LEADER = "Leader" + MEMBER = "" + + ALL_ROLES = [MEMBER, LEADER] + end + + #attr_accessible :role + + def set_role(new_role) + update_attribute(:role, new_role) + end + + def has_role?(test_role) + role == test_role + end + + def is_a_leader? + has_role? Roles::LEADER + end +end diff --git a/app/models/preset.rb b/app/models/preset.rb new file mode 100644 index 0000000000..7f166a3dbe --- /dev/null +++ b/app/models/preset.rb @@ -0,0 +1,2 @@ +class Preset < ActiveRecord::Base +end diff --git a/app/models/user.rb b/app/models/user.rb index b520076026..c63cb7b333 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -28,6 +28,9 @@ class User < ActiveRecord::Base scope :active, -> { where(:status => ["active", "confirmed"]) } scope :public, -> { where(:data_public => true) } + has_many :group_memberships, :dependent => :destroy + has_many :groups, :through => :group_memberships + 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' diff --git a/app/views/diary_entry/_diary_entry.html.erb b/app/views/diary_entry/_diary_entry.html.erb index 410e130476..15221c630b 100644 --- a/app/views/diary_entry/_diary_entry.html.erb +++ b/app/views/diary_entry/_diary_entry.html.erb @@ -4,10 +4,13 @@ <%= user_thumbnail diary_entry.user %> <% end %> -
<%= t 'group.index.about' %>
<%= @group.description.to_html %>