diff options
34 files changed, 508 insertions, 156 deletions
| @@ -2,4 +2,5 @@  .bundle  Gemfile.lock  pkg/* -.DS_Store
\ No newline at end of file +.DS_Store +.rvmrc
\ No newline at end of file @@ -6,6 +6,10 @@ Inboxes is a young messaging system for Rails app. It:  - 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 +##Upgrading from 0.1 to current version (0.2) + +**Run `rails generate inboxes:upgrade_discussible` and then roll up the migration. Your DB is upgraded!** +  ##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). Now the gem is tested only with Ruby 1.8.7 and REE. @@ -14,13 +18,15 @@ We recommend to use Inboxes with [Faye](https://github.com/jcoglan/faye), becaus  Remember that unfortunately, Inboxes reserve 3 resources names: Discussion, Message and Speaker. +Since version 0.2.0, it is possible to add `has_inboxes` option to any model. For instance, it can be `Person` or `Teacher`. +  ##Installation  *Make sure that Devise is already installed and configured in your app!* -1. Add `gem "inboxes", "~> 0.1.2"` to the `Gemfile` and run `bundle install` +1. Add `gem "inboxes", "~> 0.2.0"` to the `Gemfile` and run `bundle install`  2. Execute `rails generate inboxes:install`. This command will generate migration for messaging system. Don't forget to run migrations: `rake db:migrate` -3. Add `inboxes` to your User model like [here](https://gist.github.com/1330080) +3. Add `has_inboxes` to your User model like [here](https://gist.github.com/1330080).  4. Now Inboxes are ready to use. Open `http://yoursite.dev/discussions` to see the list of discussions. You can start new one.  Default Inboxes views are ugly, so you can copy into your app and make anything with them: `rails generate inboxes:views` @@ -35,15 +41,17 @@ By default, the gem provides localized phrases for Russian and English languages  You can watch the demo of integration [on YouTube](http://youtu.be/c12gey9DvyU)  1. Add `gem "faye"` to your Gemfile and run `bundle install`. Install Faye by [the screencast](http://railscasts.com/episodes/260-messaging-with-faye) -2. Create `messaging.js` in `app/assets/javascripts/` with these line: `= require inboxes/faye` +2. Create `messaging.js` in `app/assets/javascripts/` with this line: `//= require inboxes/faye`  3. Copy or replace 2 views from Inboxes example app to your application: [app/views/inboxes/messages/_form](https://github.com/kirs/inboxes-app/blob/master/app/views/inboxes/messages/_form.html.haml) and [app/views/inboxes/messages/create](https://github.com/kirs/inboxes-app/blob/master/app/views/inboxes/messages/create.js.erb) -  +  4. Add config parameters to your application config (last 2 are not necessary): -    -`config.inboxes.faye_enabled = true` -`config.inboxes.faye_host = "inboxes-app.dev" # localhost by default` -`config.inboxes.faye_port = 9292 # 9292 by default` + +```ruby +config.inboxes.faye_enabled = true +config.inboxes.faye_host = "inboxes-app.dev" # localhost by default +config.inboxes.faye_port = 9292 # 9292 by default +```  5. Faye installation is finished. If you have any troubles, check the [example app](https://github.com/kirs/inboxes-app/) @@ -57,9 +65,16 @@ You can read more about Faye on it's [official page](http://faye.jcoglan.com/).  ##Todo  - Add RSpec tests +- Add ability to inherit Inboxes controllers +- Add Pusher capability +- Email notifications and the ability to answer received emails like in Github issues (#7) -##Authors +##Contributors  - [Kir Shatrov](https://github.com/kirs/) (Evrone Company) +- [Andrey Ognevsky](https://github.com/ognevsy/) (Evrone Company) +- [Alexander Brodyanoj](https://github.com/dom1nga) +- [Dmitriy Kiriyenko](https://github.com/dmitriy-kiriyenko) +- [Alexey Poimtsev](https://github.com/alec-c4) ([http://progress-engine.ru/](Progress Engine)) -##Feel free for pull requests!
\ No newline at end of file +##Feel free for pull requests! diff --git a/app/controllers/inboxes/base_controller.rb b/app/controllers/inboxes/base_controller.rb index 680e38c..3e3f8dc 100644 --- a/app/controllers/inboxes/base_controller.rb +++ b/app/controllers/inboxes/base_controller.rb @@ -1,13 +1,11 @@  class Inboxes::BaseController < ApplicationController    private -   +    def init_discussion      @discussion = Discussion.find(params[:discussion_id])    end -   -  # Needs to be overriden so that we use Spree's Ability rather than anyone else's. +    def current_ability -    # raise "Loading Ability"      @current_ability ||= Inboxes::Ability.new(current_user)    end  end
\ No newline at end of file diff --git a/app/controllers/inboxes/discussions_controller.rb b/app/controllers/inboxes/discussions_controller.rb index c03a062..52655e2 100644 --- a/app/controllers/inboxes/discussions_controller.rb +++ b/app/controllers/inboxes/discussions_controller.rb @@ -1,57 +1,41 @@  class Inboxes::DiscussionsController < Inboxes::BaseController    load_and_authorize_resource -  # before_filter :authenticate_user! -  # before_filter :init_and_check_permissions, :only => :show    before_filter :load_and_check_discussion_recipient, :only => [:create, :new] -   +    def index      @discussions = current_user.discussions    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)    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.add_recipient_token current_user.id -     +      @discussion.messages.each do |m|        m.discussion = @discussion        m.user = current_user      end -     +      if @discussion.save        redirect_to @discussion, :notice => t("inboxes.discussions.started")      else        render :action => "new"      end    end -   +    private -  # def init_and_check_permissions -  #   @discussion = Discussion.includes(:messages, :speakers).find(params[:id]) -  #   redirect_to discussions_url, :notice => t("inboxes.discussions.can_not_participate") unless @discussion.can_participate?(current_user) -  # end -      def load_and_check_discussion_recipient      # initializing model for new and create actions      @discussion = Discussion.new(params[:discussion].presence || {}) -    # @discussion.recipient_tokens = params[:recipients] if params[:recipients] # pre-population -     +      # checking if discussion with this user already exists      if @discussion.recipient_ids && @discussion.recipient_ids.size == 1        user = User.find(@discussion.recipient_ids.first) @@ -61,8 +45,8 @@ class Inboxes::DiscussionsController < Inboxes::BaseController          @discussion.messages.each do |message|            Message.create(:discussion => discussion, :user => current_user, :body => message.body) if message.body          end -        # перекидываем на нее -        redirect_to discussion_url(discussion), :notice => t("inboxes.discussions.exists", :user => user[Inboxes::config.user_name]) +        # redirecting to this discussion page +        redirect_to discussion_url(discussion), :notice => t("inboxes.discussions.already_exists", :user => user[Inboxes::config.user_name])        end      end    end diff --git a/app/controllers/inboxes/messages_controller.rb b/app/controllers/inboxes/messages_controller.rb index 4089b13..697592a 100644 --- a/app/controllers/inboxes/messages_controller.rb +++ b/app/controllers/inboxes/messages_controller.rb @@ -3,12 +3,12 @@ class Inboxes::MessagesController < Inboxes::BaseController    # load_and_authorize_resource    load_and_authorize_resource :discussion    load_resource :message, :through => :discussion, :shallow => true -   +    def create      @message.user = current_user      @message.discussion = @discussion      @message.save -     +      respond_to do |format|        format.html { redirect_to @message.discussion }        format.js @@ -16,7 +16,7 @@ class Inboxes::MessagesController < Inboxes::BaseController    end    # private -  #  +  #    # def init_and_check_permissions    #   @discussion = Discussion.find(params[:discussion_id])    #   redirect_to discussions_url, :notice => t("inboxes.discussions.can_not_participate") unless @discussion.can_participate?(current_user) diff --git a/app/controllers/inboxes/speakers_controller.rb b/app/controllers/inboxes/speakers_controller.rb index 96a3049..30c0b7f 100644 --- a/app/controllers/inboxes/speakers_controller.rb +++ b/app/controllers/inboxes/speakers_controller.rb @@ -3,14 +3,14 @@ class Inboxes::SpeakersController < Inboxes::BaseController    load_and_authorize_resource :discussion    load_resource :speaker, :through => :discussion, :shallow => true    # load_and_authorize_resource -   +    def create      raise ActiveRecord::RecordNotFound unless params[:speaker] && params[:speaker][:user_id]      @user = User.find(params[:speaker][:user_id])      flash[:notice] = t("inboxes.speakers.added") if @discussion.add_speaker(@user)      redirect_to @discussion    end -   +    def destroy      @speaker = Speaker.find(params[:id])      @speaker.destroy diff --git a/app/models/discussion.rb b/app/models/discussion.rb index aa4ce3e..675547d 100644 --- a/app/models/discussion.rb +++ b/app/models/discussion.rb @@ -1,19 +1,19 @@ -  class Discussion < ActiveRecord::Base    attr_accessor :recipient_tokens, :recipient_ids    attr_reader :recipient_ids    # paginates_per 10 -  # создатель +  # creater    has_many :messages, :dependent => :destroy -  # участники +  belongs_to :discussable, :polymorphic => true + +  # participants of discussion (speakers)    has_many :speakers, :dependent => :destroy    has_many :users, :through => :speakers -  # отметки о прочтении юзеров -   +  # marks about read/unread    scope :unread_for, (lambda do |user_or_user_id|      user = user_or_user_id.is_a?(User) ? user_or_user_id.id : user_or_user_id      joins(:speakers).where("discussions.updated_at >= speakers.updated_at AND speakers.user_id = ?", user) @@ -21,12 +21,10 @@ class Discussion < ActiveRecord::Base    accepts_nested_attributes_for :messages -  validate :check_that_has_at_least_two_users # не даем создать дискуссию, у которой нет получателей +  validate :check_that_has_at_least_two_users # don't allow to create discussion, if there is no creator -  # добавляем записи об указанных собеседников +  # mark as read    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| @@ -55,38 +53,30 @@ class Discussion < ActiveRecord::Base    end    def user_invited_at(user) -    speaker = find_speaker_by_user(user) -    speaker.created_at +    find_speaker_by_user(user).created_at    end    def can_participate?(user) -    speaker = find_speaker_by_user(user) -    speaker ? true : false +    !!find_speaker_by_user(user)    end -  # проверяет, есть ли уже беседа между пользователями -  # TODO вынести в отдельный метод а в этом возращать true/false, а то неправославно как-то +  # don't allow to create discussion with user, if discussion with this user already exists +  # TODO move to separated method and return boolean value    def self.find_between_users(user, user2)      dialog = 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| -      dialog = discussion if discussion.private? && ((discussion.users.first == user && discussion.users.last == user2) || (discussion.users.first == user2 && discussion.users.last == user)) +      dialog = discussion if discussion.private? && ([discussion.users.first, discussion.users.last] - [user, user2]).empty?      end      dialog    end -  # приватная/групповая +  # private/group discussion    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)      speaker = find_speaker_by_user(user)      if speaker @@ -96,21 +86,26 @@ class Discussion < ActiveRecord::Base      end    end -  # пометить как прочитанная +  # return amount of unreaded messages for current discussion +  def unread_messages_count_for(user) +    speaker = find_speaker_by_user(user) +    messages.where("updated_at > ?", speaker.updated_at ).where("user_id != ?", speaker.id ).count +  end +    def mark_as_read_for(user)      speaker = Speaker.find_or_create_by_user_id_and_discussion_id(user.id, self.id)      # flag.update_attributes(:updat => Time.zone.now)      speaker.touch    end -   +    def find_speaker_by_user user      Speaker.find_by_discussion_id_and_user_id(self.id, user.id)    end -   +    private    def check_that_has_at_least_two_users      errors.add :recipient_tokens, t("inboxes.discussions.choose_at_least_one_recipient") if !self.recipient_ids || self.recipient_ids.size < 2    end -end
\ No newline at end of file +end diff --git a/app/models/inboxes/ability.rb b/app/models/inboxes/ability.rb index 4d8408f..deb3d81 100644 --- a/app/models/inboxes/ability.rb +++ b/app/models/inboxes/ability.rb @@ -15,20 +15,20 @@ module Inboxes      # and therefore should be easy to test in isolation.      def self.register_ability(ability)        self.abilities.add(ability) -       +      end      def initialize(user)        # raise "Initializing 3rd patry"        # self.clear_aliased_actions -       +        # can [:index, :create], Discussion        # can :read, Discussion do |discussion|        #   discussion.can_participate?(user)        # end        #include any abilities registered by extensions, etc. -       +        Ability.abilities.each do |clazz|          ability = clazz.send(:new, user)          @rules = rules + ability.send(:rules) diff --git a/app/models/message.rb b/app/models/message.rb index b7f9b12..436775b 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -1,22 +1,21 @@  class Message < ActiveRecord::Base    default_scope order(:created_at) -   -  belongs_to :discussion, :counter_cache => true + +  belongs_to :discussion, :counter_cache => true, :touch => true    belongs_to :user -   +    validates :user, :discussion, :body, :presence => true -   -  after_save :touch_discussion_and_mark_as_read -   + +  after_save :mark_discussion_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 + +  def mark_discussion_as_read      self.discussion.mark_as_read_for(self.user)    end  end diff --git a/app/models/speaker.rb b/app/models/speaker.rb index 0ed9cdd..4d8795b 100644 --- a/app/models/speaker.rb +++ b/app/models/speaker.rb @@ -1,16 +1,16 @@  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 -   +    private -   +    def destroy_discussion      self.discussion.destroy unless self.discussion.speakers.any?    end -   +  end diff --git a/app/views/inboxes/discussions/_form.html.haml b/app/views/inboxes/discussions/_form.html.haml index 303f0bb..800004b 100644 --- a/app/views/inboxes/discussions/_form.html.haml +++ b/app/views/inboxes/discussions/_form.html.haml @@ -8,5 +8,5 @@        = j.label :body        %br        = j.text_area :body -   +    %p= f.submit
\ No newline at end of file diff --git a/app/views/inboxes/discussions/index.html.haml b/app/views/inboxes/discussions/index.html.haml index 86ae297..433e929 100644 --- a/app/views/inboxes/discussions/index.html.haml +++ b/app/views/inboxes/discussions/index.html.haml @@ -11,7 +11,10 @@        %td          = link_to discussion.users.collect{|u| u[Inboxes::config.user_name]}.join(', '), discussion        %td -        = discussion.unread_for?(current_user) ? "Yes" : "No" -   +        - if discussion.unread_for?(current_user) +          = pluralize(discussion.unread_messages_count_for(current_user), "message") +        - else +          No unread messages +  %p -  = link_to "Create new", new_discussion_path
\ No newline at end of file +  = link_to "Create new", new_discussion_path diff --git a/app/views/inboxes/discussions/show.html.haml b/app/views/inboxes/discussions/show.html.haml index a910b51..50f5d19 100644 --- a/app/views/inboxes/discussions/show.html.haml +++ b/app/views/inboxes/discussions/show.html.haml @@ -16,13 +16,13 @@      = f.label :user_id, "Add speaker"      = f.collection_select :user_id, available_users, :id, :name      = f.submit "Add" -     -   + +  %h3 Messages  #messages_box    = render @discussion.messages, :as => :message -   +  %h3 Add message  = render "inboxes/messages/form" diff --git a/config/locales/en.yml b/config/locales/en.yml index 3bcda3c..a9a810d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -9,7 +9,7 @@ en:        removed: "Speaker was removed from discussion"      discussions:        started: "Discussion started" -      leaved: "You leaved the discussion" -      exists: "Discussion between you and %{user} already exists" +      leaved: "You leave the discussion" +      already_exists: "Discussion between you and %{user} already exists"        can_not_participate: "You are not listed in this discussion"        choose_at_least_one_recipient: "You should choose at least one recipient of discussion"
\ No newline at end of file diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 1258f9f..1e311cd 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -9,7 +9,7 @@ ru:      discussions:        started: "Чат начат"        leaved: "Вы покинули дискуссию" -      exists: "Дискуссия между вами и %{user} уже существует" +      already_exists: "Дискуссия между вами и %{user} уже существует"        can_not_participate: "Вы не состоите в этой дискуссии"        choose_at_least_one_recipient: "Укажите хотя бы одного получателя"      speakers: diff --git a/config/routes.rb b/config/routes.rb index 9da2d4a..3ecb989 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,5 @@  Rails.application.routes.draw do -   +    resources :discussions, :except => :edit, :module => :inboxes do      resources :messages, :only => [:create, :index]      resources :speakers, :only => [:create, :destroy] @@ -7,5 +7,5 @@ Rails.application.routes.draw do        post 'leave'      end    end -   +  end
\ No newline at end of file diff --git a/inboxes.gemspec b/inboxes.gemspec index 9f7a2ba..82ec3c3 100644 --- a/inboxes.gemspec +++ b/inboxes.gemspec @@ -21,9 +21,11 @@ Gem::Specification.new do |s|    # specify any dependencies here; for example:    # s.add_development_dependency "ruby-debug"    s.add_runtime_dependency "haml-rails" -  s.add_runtime_dependency "cancan", ['>= 0'] -  # s.add_runtime_dependency "inherited_resources" +  s.add_runtime_dependency "devise" +  s.add_runtime_dependency "rails" +  s.add_runtime_dependency "cancan" +  s.add_development_dependency "sqlite3"    # s.add_development_dependency 'dm-sqlite-adapter', ['>= 1.1.0']    s.add_development_dependency 'rspec', ['>= 0']    s.add_development_dependency 'factory_girl', ['~> 1.2'] diff --git a/lib/generators/inboxes/install_generator.rb b/lib/generators/inboxes/install_generator.rb index d5d2a97..48ac223 100644 --- a/lib/generators/inboxes/install_generator.rb +++ b/lib/generators/inboxes/install_generator.rb @@ -5,19 +5,15 @@ 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" + +      desc "Generates migration for Inboxes"        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 diff --git a/lib/generators/inboxes/templates/install.rb b/lib/generators/inboxes/templates/install.rb index 91fded7..e89c128 100644 --- a/lib/generators/inboxes/templates/install.rb +++ b/lib/generators/inboxes/templates/install.rb @@ -2,6 +2,7 @@ class InstallInboxes < ActiveRecord::Migration    def self.up      create_table :discussions do |t|        t.integer :messages_count, :default => 0 # counter cache +      t.references :discussable        t.timestamps      end @@ -9,14 +10,14 @@ class InstallInboxes < ActiveRecord::Migration        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 @@ -26,4 +27,4 @@ class InstallInboxes < ActiveRecord::Migration      drop_table :discussions      drop_table :messages    end -end
\ No newline at end of file +end diff --git a/lib/generators/inboxes/templates/upgrade_discussible.rb b/lib/generators/inboxes/templates/upgrade_discussible.rb new file mode 100644 index 0000000..dc39dd9 --- /dev/null +++ b/lib/generators/inboxes/templates/upgrade_discussible.rb @@ -0,0 +1,9 @@ +class UpgradeDiscussibleInboxes < ActiveRecord::Migration +  def self.change +    add_column :discussions, :discussible_id, :integer +  end + +  def self.down +    remove_column :discussions, :discussible_id +  end +end diff --git a/lib/generators/inboxes/upgrade_discussible_generator.rb b/lib/generators/inboxes/upgrade_discussible_generator.rb new file mode 100644 index 0000000..475f668 --- /dev/null +++ b/lib/generators/inboxes/upgrade_discussible_generator.rb @@ -0,0 +1,36 @@ +require 'rails/generators' +require 'rails/generators/migration' + +module Inboxes +  module Generators +    class UpgradeDiscussibleGenerator < Rails::Generators::Base +      include Rails::Generators::Migration + +      source_root File.expand_path("../templates", __FILE__) + +      desc "Generates migration for Inboxes 0.2.0 update" + +      def self.orm +        Rails::Generators.options[:rails][:orm] +      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 'upgrade_discussible.rb', 'db/migrate/upgrade_discussible_inboxes.rb' +      end +    end +  end +end diff --git a/lib/generators/inboxes/views_generator.rb b/lib/generators/inboxes/views_generator.rb index 117c05d..e9a8818 100644 --- a/lib/generators/inboxes/views_generator.rb +++ b/lib/generators/inboxes/views_generator.rb @@ -5,7 +5,7 @@ module Inboxes      class ViewsGenerator < Rails::Generators::Base        source_root File.expand_path('../../../../app/views', __FILE__)        #class_option :template_engine, :type => :string, :aliases => '-e', :desc => 'Template engine for the views. Available options are "erb" and "haml".' -       +        # TODO support of both haml and erb        def copy_or_fetch          filename_pattern = File.join self.class.source_root, "*" #/*.html.#{template_engine}" diff --git a/lib/inboxes.rb b/lib/inboxes.rb index f1b9c7a..af88b0c 100644 --- a/lib/inboxes.rb +++ b/lib/inboxes.rb @@ -3,10 +3,9 @@ require "inboxes/railtie"  require "inboxes/ability"  require "inboxes/engine"  require "inboxes/active_record_extension" - +require "cancan"  module Inboxes -      def self.configure(&block)      yield @config ||= Inboxes::Configuration.new    end @@ -25,11 +24,10 @@ module Inboxes      config_accessor :faye_enabled      def param_name -      config.param_name.respond_to?(:call) ? config.param_name.call() : config.param_name  +      config.param_name.respond_to?(:call) ? config.param_name.call() : config.param_name      end    end -   +    # adding method inboxes for models    ActiveRecord::Base.extend(Inboxes::ActiveRecordExtension) -    end diff --git a/lib/inboxes/ability.rb b/lib/inboxes/ability.rb index 3ed216b..c9f8d6b 100644 --- a/lib/inboxes/ability.rb +++ b/lib/inboxes/ability.rb @@ -13,17 +13,17 @@ module Inboxes            discussion.can_participate?(user)          end        end -       +        # Message        # can :create, Message do |message|        #   message.discussion.can_participate?(user)        # end -      #  +      #        # # Speaker        # can [:create, :destroy], Speaker do |speaker|        #   speaker.discussion.can_participate?(user)        # end      end    end -   +  end
\ No newline at end of file diff --git a/lib/inboxes/active_record_extension.rb b/lib/inboxes/active_record_extension.rb index 34fd387..b3c3231 100644 --- a/lib/inboxes/active_record_extension.rb +++ b/lib/inboxes/active_record_extension.rb @@ -3,9 +3,9 @@ module Inboxes      def has_inboxes(options = {})        # field  = options[:as]     || name        # prefix = options[:prefix] || "with" -       +        has_many :speakers, :dependent => :destroy -      has_many :discussions, :through => :speakers +      has_many :discussions, :as => :discussable, :through => :speakers      end    end -end
\ No newline at end of file +end diff --git a/lib/inboxes/engine.rb b/lib/inboxes/engine.rb index 01f63b5..b61eebc 100644 --- a/lib/inboxes/engine.rb +++ b/lib/inboxes/engine.rb @@ -5,7 +5,7 @@ module Inboxes      def self.activate        Ability.register_ability(InboxesAbility)      end -     +      config.to_prepare &method(:activate).to_proc    end  end
\ No newline at end of file diff --git a/lib/inboxes/railtie.rb b/lib/inboxes/railtie.rb index 673784e..1626b20 100644 --- a/lib/inboxes/railtie.rb +++ b/lib/inboxes/railtie.rb @@ -4,7 +4,7 @@ require "inboxes/ability"  module Inboxes    class Railtie < ::Rails::Railtie      config.inboxes = ActiveSupport::OrderedOptions.new -     +      initializer "inboxes.configure" do |app|        Inboxes.configure do |config|          config.user_name = app.config.inboxes[:user_name] || "email" @@ -15,7 +15,7 @@ module Inboxes        # app.config.middleware.insert_before "::Rails::Rack::Logger", "Inboxes::Middleware"      end -     +      # def self.activate      #   Ability.register_ability(InboxesAbility)      # end diff --git a/lib/inboxes/version.rb b/lib/inboxes/version.rb index 5470b38..ee23565 100644 --- a/lib/inboxes/version.rb +++ b/lib/inboxes/version.rb @@ -1,3 +1,3 @@  module Inboxes -  VERSION = "0.1.5" +  VERSION = "0.2.0"  end diff --git a/log/development.log b/log/development.log new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/log/development.log diff --git a/spec/devise_config.rb b/spec/devise_config.rb new file mode 100644 index 0000000..5e86e34 --- /dev/null +++ b/spec/devise_config.rb @@ -0,0 +1,210 @@ +# Use this hook to configure devise mailer, warden hooks and so forth. The first +# four configuration values can also be set straight in your models. +Devise.setup do |config| +  # ==> Mailer Configuration +  # Configure the e-mail address which will be shown in Devise::Mailer, +  # note that it will be overwritten if you use your own mailer class with default "from" parameter. +  config.mailer_sender = "please-change-me-at-config-initializers-devise@example.com" + +  # Configure the class responsible to send e-mails. +  # config.mailer = "Devise::Mailer" + +  # ==> ORM configuration +  # Load and configure the ORM. Supports :active_record (default) and +  # :mongoid (bson_ext recommended) by default. Other ORMs may be +  # available as additional gems. +  require 'devise/orm/active_record' + +  # ==> Configuration for any authentication mechanism +  # Configure which keys are used when authenticating a user. The default is +  # just :email. You can configure it to use [:username, :subdomain], so for +  # authenticating a user, both parameters are required. Remember that those +  # parameters are used only when authenticating and not when retrieving from +  # session. If you need permissions, you should implement that in a before filter. +  # You can also supply a hash where the value is a boolean determining whether +  # or not authentication should be aborted when the value is not present. +  # config.authentication_keys = [ :email ] + +  # Configure parameters from the request object used for authentication. Each entry +  # given should be a request method and it will automatically be passed to the +  # find_for_authentication method and considered in your model lookup. For instance, +  # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. +  # The same considerations mentioned for authentication_keys also apply to request_keys. +  # config.request_keys = [] + +  # Configure which authentication keys should be case-insensitive. +  # These keys will be downcased upon creating or modifying a user and when used +  # to authenticate or find a user. Default is :email. +  config.case_insensitive_keys = [ :email ] + +  # Configure which authentication keys should have whitespace stripped. +  # These keys will have whitespace before and after removed upon creating or +  # modifying a user and when used to authenticate or find a user. Default is :email. +  config.strip_whitespace_keys = [ :email ] + +  # Tell if authentication through request.params is enabled. True by default. +  # config.params_authenticatable = true + +  # Tell if authentication through HTTP Basic Auth is enabled. False by default. +  # config.http_authenticatable = false + +  # If http headers should be returned for AJAX requests. True by default. +  # config.http_authenticatable_on_xhr = true + +  # The realm used in Http Basic Authentication. "Application" by default. +  # config.http_authentication_realm = "Application" + +  # It will change confirmation, password recovery and other workflows +  # to behave the same regardless if the e-mail provided was right or wrong. +  # Does not affect registerable. +  # config.paranoid = true + +  # ==> Configuration for :database_authenticatable +  # For bcrypt, this is the cost for hashing the password and defaults to 10. If +  # using other encryptors, it sets how many times you want the password re-encrypted. +  # +  # Limiting the stretches to just one in testing will increase the performance of +  # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use +  # a value less than 10 in other environments. +  config.stretches = Rails.env.test? ? 1 : 10 + +  # Setup a pepper to generate the encrypted password. +  # config.pepper = "9a86f39fcb72573a367c4fcde09b1ae3fe81c3c2c5858c6024033360b39a0f46ab72c8aade6453e96dfae546262c0336372c19518600367fc8b94e9bb4d5db15" + +  # ==> Configuration for :confirmable +  # A period that the user is allowed to access the website even without +  # confirming his account. For instance, if set to 2.days, the user will be +  # able to access the website for two days without confirming his account, +  # access will be blocked just in the third day. Default is 0.days, meaning +  # the user cannot access the website without confirming his account. +  # config.confirm_within = 2.days + +  # Defines which key will be used when confirming an account +  # config.confirmation_keys = [ :email ] + +  # ==> Configuration for :rememberable +  # The time the user will be remembered without asking for credentials again. +  # config.remember_for = 2.weeks + +  # If true, a valid remember token can be re-used between multiple browsers. +  # config.remember_across_browsers = true + +  # If true, extends the user's remember period when remembered via cookie. +  # config.extend_remember_period = false + +  # If true, uses the password salt as remember token. This should be turned +  # to false if you are not using database authenticatable. +  config.use_salt_as_remember_token = true + +  # Options to be passed to the created cookie. For instance, you can set +  # :secure => true in order to force SSL only cookies. +  # config.cookie_options = {} + +  # ==> Configuration for :validatable +  # Range for password length. Default is 6..128. +  # config.password_length = 6..128 + +  # Email regex used to validate email formats. It simply asserts that +  # an one (and only one) @ exists in the given string. This is mainly +  # to give user feedback and not to assert the e-mail validity. +  # config.email_regexp = /\A[^@]+@[^@]+\z/ + +  # ==> Configuration for :timeoutable +  # The time you want to timeout the user session without activity. After this +  # time the user will be asked for credentials again. Default is 30 minutes. +  # config.timeout_in = 30.minutes + +  # ==> Configuration for :lockable +  # Defines which strategy will be used to lock an account. +  # :failed_attempts = Locks an account after a number of failed attempts to sign in. +  # :none            = No lock strategy. You should handle locking by yourself. +  # config.lock_strategy = :failed_attempts + +  # Defines which key will be used when locking and unlocking an account +  # config.unlock_keys = [ :email ] + +  # Defines which strategy will be used to unlock an account. +  # :email = Sends an unlock link to the user email +  # :time  = Re-enables login after a certain amount of time (see :unlock_in below) +  # :both  = Enables both strategies +  # :none  = No unlock strategy. You should handle unlocking by yourself. +  # config.unlock_strategy = :both + +  # Number of authentication tries before locking an account if lock_strategy +  # is failed attempts. +  # config.maximum_attempts = 20 + +  # Time interval to unlock the account if :time is enabled as unlock_strategy. +  # config.unlock_in = 1.hour + +  # ==> Configuration for :recoverable +  # +  # Defines which key will be used when recovering the password for an account +  # config.reset_password_keys = [ :email ] + +  # Time interval you can reset your password with a reset password key. +  # Don't put a too small interval or your users won't have the time to +  # change their passwords. +  config.reset_password_within = 2.hours + +  # ==> Configuration for :encryptable +  # Allow you to use another encryption algorithm besides bcrypt (default). You can use +  # :sha1, :sha512 or encryptors from others authentication tools as :clearance_sha1, +  # :authlogic_sha512 (then you should set stretches above to 20 for default behavior) +  # and :restful_authentication_sha1 (then you should set stretches to 10, and copy +  # REST_AUTH_SITE_KEY to pepper) +  # config.encryptor = :sha512 + +  # ==> Configuration for :token_authenticatable +  # Defines name of the authentication token params key +  # config.token_authentication_key = :auth_token + +  # If true, authentication through token does not store user in session and needs +  # to be supplied on each request. Useful if you are using the token as API token. +  # config.stateless_token = false + +  # ==> Scopes configuration +  # Turn scoped views on. Before rendering "sessions/new", it will first check for +  # "users/sessions/new". It's turned off by default because it's slower if you +  # are using only default views. +  # config.scoped_views = false + +  # Configure the default scope given to Warden. By default it's the first +  # devise role declared in your routes (usually :user). +  # config.default_scope = :user + +  # Configure sign_out behavior. +  # Sign_out action can be scoped (i.e. /users/sign_out affects only :user scope). +  # The default is true, which means any logout action will sign out all active scopes. +  # config.sign_out_all_scopes = true + +  # ==> Navigation configuration +  # Lists the formats that should be treated as navigational. Formats like +  # :html, should redirect to the sign in page when the user does not have +  # access, but formats like :xml or :json, should return 401. +  # +  # If you have any extra navigational formats, like :iphone or :mobile, you +  # should add them to the navigational formats lists. +  # +  # The :"*/*" and "*/*" formats below is required to match Internet +  # Explorer requests. +  # config.navigational_formats = [:"*/*", "*/*", :html] + +  # The default HTTP method used to sign out a resource. Default is :delete. +  config.sign_out_via = :delete + +  # ==> OmniAuth +  # Add a new OmniAuth provider. Check the wiki for more information on setting +  # up on your models and hooks. +  # config.omniauth :github, 'APP_ID', 'APP_SECRET', :scope => 'user,public_repo' + +  # ==> Warden configuration +  # If you want to use other strategies, that are not supported by Devise, or +  # change the failure app, you can configure them inside the config.warden block. +  # +  # config.warden do |manager| +  #   manager.failure_app   = AnotherApp +  #   manager.intercept_401 = false +  #   manager.default_strategies(:scope => :user).unshift :some_external_strategy +  # end +end diff --git a/spec/fake_app.rb b/spec/fake_app.rb new file mode 100644 index 0000000..f7db07f --- /dev/null +++ b/spec/fake_app.rb @@ -0,0 +1,97 @@ +require 'active_record' +require 'action_controller/railtie' +require 'action_view/railtie' + +require "cancan" +require "cancan/ability" +require "cancan/controller_resource" +require "cancan/controller_additions" + +require 'devise' +require 'devise/orm/active_record' + +# database +ActiveRecord::Base.configurations = {'test' => {:adapter => 'sqlite3', :database => ':memory:'}} +ActiveRecord::Base.establish_connection('test') + +# config +app = Class.new(Rails::Application) +app.config.secret_token = "3b7cd727ee24e8444053437c36cc66c4" +app.config.session_store :cookie_store, :key => "_myapp_session" +app.config.active_support.deprecation = :log +app.initialize! + +require 'devise_config' + +# models +class User < ActiveRecord::Base +  devise :database_authenticatable, :registerable, +         :recoverable, :rememberable, :trackable, :validatable +  attr_accessible :email, :password, :password_confirmation, :remember_me, :name +  validates :name, :presence => true, :uniqueness => true +  has_inboxes +end + +# routes +app.routes.draw do +  devise_for :users +end + +#migrations +ActiveRecord::Base.silence do +  ActiveRecord::Migration.verbose = false +  ActiveRecord::Schema.define :version => 0 do +    create_table "users", :force => true do |t| +      t.string   "email",                                 :default => "", :null => false +      t.string   "encrypted_password",     :limit => 128, :default => "", :null => false +      t.string   "reset_password_token" +      t.datetime "reset_password_sent_at" +      t.datetime "remember_created_at" +      t.integer  "sign_in_count",                         :default => 0 +      t.datetime "current_sign_in_at" +      t.datetime "last_sign_in_at" +      t.string   "current_sign_in_ip" +      t.string   "last_sign_in_ip" +      t.datetime "created_at" +      t.datetime "updated_at" +      t.string   "name" +    end +     +    create_table "discussions", :force => true do |t| +      t.integer  "user_id" +      t.integer  "messages_count", :default => 0 +      t.boolean  "is_private",     :default => true +      t.datetime "created_at" +      t.datetime "updated_at" +    end + +    create_table "messages", :force => true do |t| +      t.integer  "user_id" +      t.integer  "discussion_id" +      t.text     "body" +      t.datetime "created_at" +      t.datetime "updated_at" +    end + +    create_table "speakers", :force => true do |t| +      t.integer  "user_id" +      t.integer  "discussion_id" +      t.datetime "created_at" +      t.datetime "updated_at" +    end +  end +end + +# controllers +class ApplicationController < ActionController::Base +  before_filter :assign_unread_discussions +   +  private +   +  def assign_unread_discussions +    @unread_discussions_count = Discussion.unread_for(current_user).count if user_signed_in? +  end +end + +# helpers +Object.const_set(:ApplicationHelper, Module.new)
\ No newline at end of file diff --git a/spec/fake_gem.rb b/spec/fake_gem.rb new file mode 100644 index 0000000..4435a70 --- /dev/null +++ b/spec/fake_gem.rb @@ -0,0 +1,6 @@ +# Simulate a gem providing a subclass of ActiveRecord::Base before the Railtie is loaded. + +require 'active_record' + +class GemDefinedModel < ActiveRecord::Base +end
\ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 174ab57..5caa0f5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,2 +1,29 @@ +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) +$LOAD_PATH.unshift(File.dirname(__FILE__)) +require 'rails' +require 'active_record'  require 'inboxes' +# require 'database_cleaner' +# Ensure we use 'syck' instead of 'psych' in 1.9.2 +# RubyGems >= 1.5.0 uses 'psych' on 1.9.2, but +# Psych does not yet support YAML 1.1 merge keys. +# Merge keys is often used in mongoid.yml +# See: http://redmine.ruby-lang.org/issues/show/4300 +if RUBY_VERSION >= '1.9.2' +  YAML::ENGINE.yamler = 'syck' +end +# require 'fake_gem' +require 'fake_app' +require 'rspec/rails' +# Requires supporting files with custom matchers and macros, etc, +# in ./support/ and its subdirectories. +Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} + +# RSpec.configure do |config| +#   config.mock_with :rr +#   config.before :all do +# #     ActiveRecord::Base.connection.execute 'CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255))' unless ActiveRecord::Base.connection.table_exists? 'users' +#     # CreateAllTables.up unless ActiveRecord::Base.connection.table_exists? 'users' +#   end +# end
\ No newline at end of file diff --git a/spec/support/factories.rb b/spec/support/factories.rb deleted file mode 100644 index a7eaad6..0000000 --- a/spec/support/factories.rb +++ /dev/null @@ -1,25 +0,0 @@ -FactoryGirl.define do -   -  factory :user do -    email {Factory.next(:email)} -    first_name 'user' -    last_name 'usered' -    username {Factory.next(:login)} -    password "foobar" -    password_confirmation { |u| u.password } -    role 2 -  end - -  factory :discussion do -    recipient_ids {[Factory(:user).id, Factory(:user).id]} -  end - -  factory :message do -    association :user -    association :discussion -    # user {Factory(:user)} -    # discussion {Factory(:discussion)} -  end - - -end
\ No newline at end of file | 
