diff options
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | README.md | 12 | ||||
| -rw-r--r-- | app/controllers/discussions_controller.rb | 84 | ||||
| -rw-r--r-- | app/models/discussion.rb | 118 | ||||
| -rw-r--r-- | app/models/message.rb | 22 | ||||
| -rw-r--r-- | app/models/speaker.rb | 17 | ||||
| -rw-r--r-- | app/views/discussions/_form.html.haml | 10 | ||||
| -rw-r--r-- | app/views/discussions/index.html.haml | 5 | ||||
| -rw-r--r-- | app/views/discussions/new.html.haml | 2 | ||||
| -rw-r--r-- | app/views/discussions/show.html.haml | 5 | ||||
| -rw-r--r-- | config/routes.rb | 11 | ||||
| -rw-r--r-- | inboxes.gemspec | 2 | ||||
| -rw-r--r-- | lib/generators/inboxes/install_generator.rb | 40 | ||||
| -rw-r--r-- | lib/generators/inboxes/templates/install.rb | 36 | ||||
| -rw-r--r-- | lib/inboxes.rb | 8 | ||||
| -rw-r--r-- | lib/inboxes/engine.rb | 4 | ||||
| -rw-r--r-- | lib/inboxes/railtie.rb | 16 |
17 files changed, 389 insertions, 4 deletions
@@ -2,3 +2,4 @@ .bundle Gemfile.lock pkg/* +.DS_Store
\ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..46bbce6 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +#Inboxes + +Inboxes is a young messaging system for Rails app. It: +- provides 3 models for developers: Discussion, Message and Speaker +- read/unread discussions counter +- any user can be invited to discussion by the member of this discussion, so you can chat with unlimited number of users + +#Requirements and recommendations + +Inboxes requires Rails 3.x and [Devise](https://github.com/plataformatec/devise) for user identification (surely, messaging system is not possible without users). + +We recommend you to use it with [Faye](https://github.com/jcoglan/faye), because it's really very useful with it.
\ No newline at end of file diff --git a/app/controllers/discussions_controller.rb b/app/controllers/discussions_controller.rb new file mode 100644 index 0000000..7d5d8ca --- /dev/null +++ b/app/controllers/discussions_controller.rb @@ -0,0 +1,84 @@ +class DiscussionsController < ApplicationController + # load_and_authorize_resource + # before_filter :load_and_check_discussion_recipient, :only => [:create, :new] + + def index + # показываем дискуссии юзера, и те куда его присоеденили + # так как имеем массив дискуссий, его пагинация будет через хак + @discussions = current_user.discussions + + respond_to do |format| + format.html # index.html.erb + format.json { render :json => @discussions } + end + end + + # GET /discussions/1 + # GET /discussions/1.json + def show + @discussion = Discussion.includes(:messages, :speakers).find(params[:id]) + # @discussion.mark_as_read_for(current_user) # сделаем прочтенной для пользователя + + # члены дискуссии - приглашенные в нее + ее создатель + @message = Message.new + + respond_to do |format| + format.html # show.html.erb + format.json { render :json => @discussion } + end + end + + # GET /discussions/new + # GET /discussions/new.json + def new + @discussion = Discussion.new + @discussion.messages.build + end + + # POST /discussions + # POST /discussions.json + def create + @discussion = Discussion.new(params[:discussion]) + @discussion.messages.each do |m| + m.discussion = @discussion + m.user = current_user + end + @discussion.add_recipient_token current_user.id + + respond_to do |format| + if @discussion.save + format.html { redirect_to @discussion, :notice => 'Дискуссия начата.' } + # format.json { render :json => @discussion, :status => :created, :location => @discussion } + else + format.html { render :action => "new" } + # format.json { render :json => @discussion.errors, :status => :unprocessable_entity } + end + end + end + + def leave + @discussion.remove_speaker(current_user) + redirect_to discussions_url, :notice => "Вы успешно покинули дискуссию." + end + + private + + def load_and_check_discussion_recipient + @discussion.recipient_tokens = params[:recipients] if params[:recipients] + + # проверка, существует ли уже дискуссия с этим человеком + if @discussion.recipient_ids && @discussion.recipient_ids.size == 1 + user = User.find(@discussion.recipient_ids.first) + discussion = Discussion.find_between_users(current_user, user) + if discussion + # дискуссия уже существует, добавим в нее написанное сообщение + @discussion.messages.each do |m| + Message.create!(:discussion => discussion, :user => current_user, :body => m.body) if m.body + end + # перекидываем на нее + redirect_to discussion_url(discussion), :notice => "Переписка между вами уже существует." + end + end + + end +end diff --git a/app/models/discussion.rb b/app/models/discussion.rb new file mode 100644 index 0000000..474ad00 --- /dev/null +++ b/app/models/discussion.rb @@ -0,0 +1,118 @@ + +class Discussion < ActiveRecord::Base + attr_accessor :recipient_tokens, :recipient_ids + attr_reader :recipient_ids + + # paginates_per 10 + + # создатель + has_many :messages, :dependent => :destroy + + # участники + has_many :speakers, :dependent => :destroy + has_many :users, :through => :speakers + + # отметки о прочтении юзеров + # has_many :views, :dependent => :destroy, :class_name => "DiscussionView" + + # жутко неоптимизированная часть, возможны баги + # scope :unread_for, lambda { |user_or_user_id| joins(:views, :speakers).where("discussions.updated_at >= discussion_views.updated_at AND speakers.user_id = ?", user_or_user_id.is_a?(User) ? user_or_user_id.id : user_or_user_id ) } + + accepts_nested_attributes_for :messages + + validate :check_that_has_at_least_two_users # не даем создать дискуссию, у которой нет получателей + + # добавляем записи об указанных собеседников + after_save(:on => :create) do + Rails.logger.info("Repicients ids: #{recipient_ids.inspect}") + + if recipient_ids.kind_of?(Array) + recipient_ids.uniq! + recipient_ids.each do |id| + recipient = User.find(id) + add_speaker(recipient) if recipient + end + end + end + + def recipient_tokens=(ids) + self.recipient_ids = ids + end + + def add_recipient_token id + self.recipient_ids << id if self.recipient_ids + end + + def add_speaker(user) + raise ArgumentError, "You can add speaker only to existing Discussion. Save your the Discussion object firstly" if new_record? + Speaker.create(:discussion => self, :user => user) + end + + def remove_speaker(user) + speaker = find_speaker_by_user(user) + speaker.destroy if speaker + end + + def user_invited_at(user) + speaker = find_speaker_by_user(user) + speaker.created_at + end + + def can_participate?(user) + speaker = find_speaker_by_user(user) + speaker ? true : false + end + + # проверяет, есть ли уже беседа между пользователями + # TODO вынести в отдельный метод а в этом возращать true/false, а то неправославно как-то + def self.find_between_users(user, user2) + res = nil + discussions = self.joins(:speakers).includes(:users).where("speakers.user_id = ?", user.id) + Rails.logger.info "Searching for ids: #{user.id}, #{user2.id}" + discussions.each do |discussion| + + res = discussion if discussion.private? && ((discussion.users.first == user && discussion.users.last == user2) || (discussion.users.first == user2 && discussion.users.last == user)) + Rails.logger.info "Searching for ids: #{discussion.users.inspect}" if discussion.private? + end + res + end + + # приватная/групповая + def private? + self.users.size <= 2 + end + + # дата последнего сообщения в дискуссии + def last_message_at + self.messages.last ? self.messages.last.created_at : nil + end + + # проверка, является ли дискуссия непрочитанной для пользователя + # def unread_for?(user) + # flag = self.views.find_by_user_id(user.id) + # if flag + # self.updated_at >= flag.updated_at + # else + # true + # end + # end + + # пометить как прочитанная + def mark_as_read_for(user) + true + # flag = DiscussionView.find_or_create_by_user_id_and_discussion_id(user.id, self.id) + # flag.touch + end + + private + + def find_speaker_by_user user + Speaker.find_by_discussion_id_and_user_id!(self.id, user.id) + end + + def check_that_has_at_least_two_users + Rails.logger.info self.recipient_ids + errors.add :recipient_tokens, "Укажите хотя бы одного получателя" #if !self.recipient_ids || self.recipient_ids.size < 2 + end + +end
\ No newline at end of file diff --git a/app/models/message.rb b/app/models/message.rb new file mode 100644 index 0000000..a38dfb0 --- /dev/null +++ b/app/models/message.rb @@ -0,0 +1,22 @@ +class Message < ActiveRecord::Base + + default_scope order(:created_at) + + belongs_to :discussion, :counter_cache => true + belongs_to :user + + validates :user, :discussion, :body, :presence => true + + # after_save :touch_discussion_and_mark_as_read + + # def visible_for? user + # self.created_at.to_i >= self.discussion.user_invited_at(user).to_i + # end + + private + + def touch_discussion_and_mark_as_read + self.discussion.touch + # self.discussion.mark_as_read_for(self.user) + end +end diff --git a/app/models/speaker.rb b/app/models/speaker.rb new file mode 100644 index 0000000..6105220 --- /dev/null +++ b/app/models/speaker.rb @@ -0,0 +1,17 @@ +class Speaker < ActiveRecord::Base + belongs_to :user + belongs_to :discussion + + validates_uniqueness_of :user_id, :scope => :discussion_id + validates :user, :discussion, :presence => true + + after_destroy :destroy_discussion_view + + private + + def destroy_discussion_view + @view = DiscussionView.find_by_user_id_and_discussion_id(self.user_id, self.discussion_id) + @view.destroy if @view + end + +end diff --git a/app/views/discussions/_form.html.haml b/app/views/discussions/_form.html.haml new file mode 100644 index 0000000..52da4a6 --- /dev/null +++ b/app/views/discussions/_form.html.haml @@ -0,0 +1,10 @@ += form_for @discussion do |f| + .entry + = f.label :recipient_tokens + = f.collection_select :recipient_tokens, User.all, :id, :name, :prompt => true, :html_options => { :multiple => true } + .entry + = f.fields_for :messages do |j| + = j.label :body + = j.text_area :body, :label => "Сообщение" + + = f.submit 'Отправить'
\ No newline at end of file diff --git a/app/views/discussions/index.html.haml b/app/views/discussions/index.html.haml new file mode 100644 index 0000000..669c01b --- /dev/null +++ b/app/views/discussions/index.html.haml @@ -0,0 +1,5 @@ +%h3 Discussions +- @discussions.each do |discussion| + = discussion.id + += link_to "Create new", new_discussion_path
\ No newline at end of file diff --git a/app/views/discussions/new.html.haml b/app/views/discussions/new.html.haml new file mode 100644 index 0000000..3f68ec9 --- /dev/null +++ b/app/views/discussions/new.html.haml @@ -0,0 +1,2 @@ +%h3 New discussion += render "form"
\ No newline at end of file diff --git a/app/views/discussions/show.html.haml b/app/views/discussions/show.html.haml new file mode 100644 index 0000000..8c13728 --- /dev/null +++ b/app/views/discussions/show.html.haml @@ -0,0 +1,5 @@ += @discussion.users.map { |u| u.name }.join(", ") += debug @discussion.speakers + +- @discussion.messages.each do |message| + = message.body
\ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000..a9bce94 --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,11 @@ +Rails.application.routes.draw do + + resources :discussions, :except => :edit do + resources :messages, :only => [:create, :index] + resources :speakers, :only => [:create, :destroy] + member do + post 'leave' + end + end + +end
\ No newline at end of file diff --git a/inboxes.gemspec b/inboxes.gemspec index 4587ed8..441f0a5 100644 --- a/inboxes.gemspec +++ b/inboxes.gemspec @@ -19,6 +19,6 @@ Gem::Specification.new do |s| s.require_paths = ["lib"] # specify any dependencies here; for example: - # s.add_development_dependency "rspec" + s.add_development_dependency "ruby-debug" # s.add_runtime_dependency "rest-client" end diff --git a/lib/generators/inboxes/install_generator.rb b/lib/generators/inboxes/install_generator.rb new file mode 100644 index 0000000..d5d2a97 --- /dev/null +++ b/lib/generators/inboxes/install_generator.rb @@ -0,0 +1,40 @@ +require 'rails/generators' +require 'rails/generators/migration' + +module Inboxes + module Generators + class InstallGenerator < Rails::Generators::Base + include Rails::Generators::Migration + + source_root File.expand_path("../templates", __FILE__) + + # desc "Generates migration for Discussion, Message, Speaker and DiscussionView models" + + def self.orm + Rails::Generators.options[:rails][:orm] + end + + # def self.source_root + # File.join(File.dirname(__FILE__), 'templates', (orm.to_s unless orm.class.eql?(String)) ) + # end + + def self.orm_has_migration? + [:active_record].include? orm + end + + def self.next_migration_number(dirname) + if ActiveRecord::Base.timestamped_migrations + migration_number = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i + migration_number += 1 + migration_number.to_s + else + "%.3d" % (current_migration_number(dirname) + 1) + end + end + + def copy_migration + migration_template 'install.rb', 'db/migrate/install_inboxes.rb' + end + end + end +end diff --git a/lib/generators/inboxes/templates/install.rb b/lib/generators/inboxes/templates/install.rb new file mode 100644 index 0000000..ff83b40 --- /dev/null +++ b/lib/generators/inboxes/templates/install.rb @@ -0,0 +1,36 @@ +class InstallInboxes < ActiveRecord::Migration + def self.up + create_table :discussion_views do |t| + t.references :user + t.references :discussion + t.timestamps + end + + create_table :discussions do |t| + t.integer :messages_count, :default => 0 # counter cache + t.timestamps + end + + create_table :messages do |t| + t.references :user + t.references :discussion + t.text :body + + t.timestamps + end + + create_table :speakers do |t| + t.references :user + t.references :discussion + + t.timestamps + end + end + + def self.down + drop_table :speakers + drop_table :discussions + drop_table :discussion_views + drop_table :messages + end +end
\ No newline at end of file diff --git a/lib/inboxes.rb b/lib/inboxes.rb index 2720101..29bb078 100644 --- a/lib/inboxes.rb +++ b/lib/inboxes.rb @@ -1,5 +1,7 @@ require "inboxes/version" +require "inboxes/railtie" +require "inboxes/engine" -module Inboxes - # Your code goes here... -end +# module Inboxes +# # Your code goes here... +# end diff --git a/lib/inboxes/engine.rb b/lib/inboxes/engine.rb new file mode 100644 index 0000000..061694e --- /dev/null +++ b/lib/inboxes/engine.rb @@ -0,0 +1,4 @@ +module Inboxes + class Engine < ::Rails::Engine + end +end
\ No newline at end of file diff --git a/lib/inboxes/railtie.rb b/lib/inboxes/railtie.rb new file mode 100644 index 0000000..5be6db5 --- /dev/null +++ b/lib/inboxes/railtie.rb @@ -0,0 +1,16 @@ +require 'rails' + +module Inboxes + class Railtie < ::Rails::Railtie #:nodoc: + initializer 'inboxes' do |app| + ActiveSupport.on_load(:active_record) do + # require 'kaminari/models/active_record_extension' + # ::ActiveRecord::Base.send :include, Kaminari::ActiveRecordExtension + end + + ActiveSupport.on_load(:action_view) do + # ::ActionView::Base.send :include, Kaminari::ActionViewExtension + end + end + end +end
\ No newline at end of file |
