Skip to content
Browse files

Readme and initial changes

  • Loading branch information...
1 parent a99eda0 commit 7a540a454e4b2c2ccac5f6e542fe5130dbd05c8f @kirs kirs committed
View
1 .gitignore
@@ -2,3 +2,4 @@
.bundle
Gemfile.lock
pkg/*
+.DS_Store
View
12 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.
View
84 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
View
118 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
View
22 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
View
17 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
View
10 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 'Отправить'
View
5 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
View
2 app/views/discussions/new.html.haml
@@ -0,0 +1,2 @@
+%h3 New discussion
+= render "form"
View
5 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
View
11 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
View
2 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
View
40 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
View
36 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
View
8 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
View
4 lib/inboxes/engine.rb
@@ -0,0 +1,4 @@
+module Inboxes
+ class Engine < ::Rails::Engine
+ end
+end
View
16 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

0 comments on commit 7a540a4

Please sign in to comment.
Something went wrong with that request. Please try again.