Extract message system to plugin.

This commit is contained in:
Robert Waltemath 2014-03-07 09:51:24 +01:00 committed by wvengen
parent fe0b17cdb0
commit 7556c753d0
45 changed files with 298 additions and 45 deletions

View file

@ -0,0 +1,15 @@
FoodsoftMessages
================
This plugin adds messages to foodsoft. A new 'Messages' menu entry is added below the 'Foodcoops' menu in the navigation bar.
This plugin is enabled by default in foodsoft, so you don't need to do anything
to install it. If you still want to, for example when it has been disabled,
add the following to foodsoft's Gemfile:
```Gemfile
gem 'foodsoft_messages', path: 'lib/foodsoft_messages'
```
This plugin is part of the foodsoft package and uses the GPL-3 license (see
foodsoft's LICENSE for the full license text).

View file

@ -0,0 +1,40 @@
#!/usr/bin/env rake
begin
require 'bundler/setup'
rescue LoadError
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
end
begin
require 'rdoc/task'
rescue LoadError
require 'rdoc/rdoc'
require 'rake/rdoctask'
RDoc::Task = Rake::RDocTask
end
RDoc::Task.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'FoodsoftMessages'
rdoc.options << '--line-numbers'
rdoc.rdoc_files.include('README.rdoc')
rdoc.rdoc_files.include('lib/**/*.rb')
end
APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
load 'rails/tasks/engine.rake'
Bundler::GemHelper.install_tasks
require 'rake/testtask'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.libs << 'test'
t.pattern = 'test/**/*_test.rb'
t.verbose = false
end
task :default => :test

View file

@ -0,0 +1,34 @@
class MessagesController < ApplicationController
# Renders the "inbox" action.
def index
@messages = Message.public.page(params[:page]).per(@per_page).order('created_at DESC').includes(:sender)
end
# Creates a new message object.
def new
@message = Message.new(params[:message])
if @message.reply_to and not @message.reply_to.is_readable_for?(current_user)
redirect_to new_message_url, alert: 'Nachricht ist privat!'
end
end
# Creates a new message.
def create
@message = @current_user.send_messages.new(params[:message])
if @message.save
Resque.enqueue(MessageNotifier, FoodsoftConfig.scope, 'message_deliver', @message.id)
redirect_to messages_url, :notice => I18n.t('messages.create.notice')
else
render :action => 'new'
end
end
# Shows a single message.
def show
@message = Message.find(params[:id])
unless @message.is_readable_for?(current_user)
redirect_to messages_url, alert: 'Nachricht ist privat!'
end
end
end

View file

@ -0,0 +1,21 @@
module MessagesHelper
def format_subject(message, length)
if message.subject.length > length
subject = truncate(message.subject, :length => length)
body = ""
else
subject = message.subject
body = truncate(message.body, :length => length - subject.length)
end
"<b>#{link_to(h(subject), message)}</b> <span style='color:grey'>#{h(body)}</span>".html_safe
end
def link_to_new_message(options = {})
messages_params = options[:message_params] || nil
link_text = content_tag :id, nil, class: 'icon-envelope'
link_text << " #{options[:text]}" if options[:text].present?
link_to(link_text.html_safe, new_message_path(message: messages_params), class: 'btn',
title: I18n.t('helpers.submit.message.create'))
end
end

View file

@ -0,0 +1,11 @@
class MessagesMailer < Mailer
# Sends an email copy of the given internal foodsoft message.
def foodsoft_message(message, recipient)
set_foodcoop_scope
@message = message
mail subject: "[#{FoodsoftConfig[:name]}] " + message.subject,
to: recipient.email,
from: "#{show_user(message.sender)} <#{message.sender.email}>"
end
end

View file

@ -0,0 +1,92 @@
class Message < ActiveRecord::Base
belongs_to :sender, :class_name => "User", :foreign_key => "sender_id"
serialize :recipients_ids, Array
attr_accessor :sent_to_all, :group_id, :recipient_tokens, :reply_to
scope :pending, -> { where(:email_state => 0) }
scope :sent, -> { where(:email_state => 1) }
scope :public, -> { where(:private => false) }
# Values for the email_state attribute: :none, :pending, :sent, :failed
EMAIL_STATE = {
:pending => 0,
:sent => 1,
:failed => 2
}
validates_presence_of :recipients_ids, :subject, :body
validates_length_of :subject, :in => 1..255
validates_inclusion_of :email_state, :in => EMAIL_STATE.values
before_validation :clean_up_recipient_ids, :on => :create
def self.deliver(message_id)
find(message_id).deliver
end
def clean_up_recipient_ids
self.recipients_ids = recipients_ids.uniq.reject { |id| id.blank? } unless recipients_ids.nil?
self.recipients_ids = User.all.collect(&:id) if sent_to_all == "1"
end
def add_recipients(users)
self.recipients_ids = [] if recipients_ids.blank?
self.recipients_ids += users.collect(&:id) unless users.blank?
end
def group_id=(group_id)
@group_id = group_id
add_recipients Group.find(group_id).users unless group_id.blank?
end
def recipient_tokens=(ids)
@recipient_tokens = ids
add_recipients ids.split(",").collect { |id| User.find(id) }
end
def reply_to=(message_id)
@reply_to = Message.find(message_id)
add_recipients([@reply_to.sender])
self.subject = I18n.t('messages.model.reply_subject', :subject => @reply_to.subject)
self.body = I18n.t('messages.model.reply_header', :user => @reply_to.sender.display, :when => I18n.l(@reply_to.created_at, :format => :short)) + "\n"
@reply_to.body.each_line{ |l| self.body += I18n.t('messages.model.reply_indent', :line => l) }
end
def mail_to=(user_id)
user = User.find(user_id)
add_recipients([user])
end
# Returns true if this message is a system message, i.e. was sent automatically by the FoodSoft itself.
def system_message?
self.sender_id.nil?
end
def sender_name
system_message? ? I18n.t('layouts.foodsoft') : sender.display rescue "?"
end
def recipients
User.find(recipients_ids)
end
def deliver
for user in recipients
if user.receive_email?
begin
MessagesMailer.foodsoft_message(self, user).deliver
rescue
Rails.logger.warn "Deliver failed for user \##{user.id}: #{user.email}"
end
end
end
update_attribute(:email_state, 1)
end
def is_readable_for?(user)
!private || sender == user || recipients_ids.include?(user.id)
end
end

View file

@ -0,0 +1,2 @@
/ insert_after 'erb:contains("delete")'
= link_to t('.send_message'), new_message_path(:message => {:group_id => @ordergroup.id}), class: 'btn'

View file

@ -0,0 +1,2 @@
/ insert_after 'erb:contains("delete")'
= link_to t('.send_message'), new_message_path(:message => {:mail_to => @user.id}), class: 'btn'

View file

@ -0,0 +1,2 @@
/ insert_after 'erb:contains("delete")'
= link_to_new_message(message_params: {group_id: @workgroup.id})

View file

@ -0,0 +1,2 @@
/ insert_bottom 'tbody tr'
%td= link_to_new_message(message_params: {group_id: ordergroup.id})

View file

@ -0,0 +1,2 @@
/ insert_bottom 'tbody tr'
%td= link_to_new_message(message_params: {mail_to: user.id})

View file

@ -0,0 +1,2 @@
/ insert_after 'erb:contains("tasks")'
= link_to_new_message message_params: {group_id: workgroup.id}

View file

@ -0,0 +1,2 @@
/ insert_after 'erb:contains("tasks")'
%li= link_to t('.write_message'), new_message_path

View file

@ -0,0 +1,6 @@
/ insert_after 'erb[silent]:contains("<dashboard_middle_mark>")'
- unless Message.public.empty?
%section#messages
%h2= t '.messages.title'
= render 'messages/messages', messages: Message.public.order('created_at DESC').limit(5), pagination: false
%p= link_to t('.messages.view_all'), messages_path

View file

@ -0,0 +1,14 @@
- if pagination
- if Message.public.count > 20
= items_per_page
= pagination_links_remote messages
- unless messages.empty?
%table.table.table-striped
%tbody
- for message in messages
%tr
%td= format_subject(message, 130)
%td= h(message.sender_name)
%td= format_time(message.created_at)
%td= link_to t('.reply'), new_message_path(:message => {:reply_to => message.id}), class: 'btn'

View file

@ -0,0 +1,6 @@
- title t('.title')
- content_for :actionbar do
= link_to t('.new'), new_message_path, class: 'btn btn-primary'
#messages
= render 'messages', messages: @messages, pagination: true

View file

@ -0,0 +1 @@
$('#messages').html('#{j(render('messages', messages: @messages, pagination: true))}');

View file

@ -0,0 +1,46 @@
- content_for :javascript do
:javascript
$(function() {
$('#message_recipient_tokens').tokenInput("#{users_path(:format => :json)}", {
crossDomain: false,
prePopulate: $('#message_recipient_tokens').data('pre'),
hintText: '#{t '.search_user'}',
noResultText: '#{t '.no_user_found'}',
searchingText: '#{t '.search'}',
theme: 'facebook'
});
$('#message_sent_to_all').on('touchclick', function() {
if ($(this).is(':checked')) {
$('#recipients').slideUp();
} else {
$('#recipients').slideDown();
}
});
});
- title t('.title')
= simple_form_for @message do |f|
- if FoodsoftConfig[:mailing_list].blank?
= f.input :sent_to_all, :as => :boolean
- else
%b= t('.list.desc', list: mail_to(FoodsoftConfig[:mailing_list])).html_safe
%br/
%small{:style => "color:grey"}
= t '.list.subscribe_msg'
%br/
- if FoodsoftConfig[:mailing_list_subscribe].blank?
= t('.list.subscribe', link: link_to(t('.list.wiki'), wiki_page_path('MailingListe'))).html_safe
- else
= t('.list.mail', email: mail_to(FoodsoftConfig[:mailing_list_subscribe])).html_safe
#recipients
= f.input :recipient_tokens, :input_html => { 'data-pre' => User.where(id: @message.recipients_ids).map(&:token_attributes).to_json }
= f.input :group_id, :as => :select, :collection => Group.undeleted.order('type DESC, name ASC').reject { |g| g.memberships.empty? }
= f.input :private
= f.input :subject, input_html: {class: 'input-xxlarge'}
= f.input :body, input_html: {class: 'input-xxlarge'}
.form-actions
= f.submit class: 'btn btn-primary'
= link_to t('ui.or_cancel'), :back

View file

@ -0,0 +1,21 @@
- title t('.title')
%div{:style => "width:40em"}
%table{:style => "width:25em"}
%tr
%td= t '.from'
%td=h @message.sender_name
%tr
%td= t '.subject'
%td
%b=h @message.subject
%tr
%td= t '.sent_on'
%td= format_time(@message.created_at)
%hr/
%p= simple_format(h(@message.body))
%hr/
%p
= link_to t('.reply'), new_message_path(:message => {:reply_to => @message.id}), class: 'btn'
|
= link_to t('.all_messages'), messages_path

View file

@ -0,0 +1,4 @@
= @message.body
======================================================================
\
= t '.footer', reply_url: new_message_url('message[reply_to]' => @message.id), msg_url: message_url(@message), profile_url: my_profile_url

View file

@ -0,0 +1,8 @@
class MessageNotifier < UserNotifier
@queue = :foodsoft_notifier
def self.message_deliver(args)
message_id = args.first
Message.find(message_id).deliver
end
end

View file

@ -0,0 +1,77 @@
en:
activerecord:
attributes:
message:
body: Body
group_id: Group
private: Private
recipient_tokens: Recipients
sent_to_all: Send to all members
subject: Subject
models:
message: Message
admin:
ordergroups:
show:
send_message: Send message
users:
show:
send_message: Send message
helpers:
messages:
write_message: Write message
submit:
message:
create: send message
home:
index:
messages:
title: Newest Messages
view_all: See all messages
start_nav:
write_message: Write message
messages:
create:
notice: Message is saved and will be sent.
index:
new: New message
title: Messages
messages:
reply: Reply
model:
reply_header: ! '%{user} wrote on %{when}:'
reply_indent: ! '> %{line}'
reply_subject: ! 'Re: %{subject}'
new:
list:
desc: ! 'Please send messages to all using the mailing-list: %{list}'
mail: for example with an email to %{email}.
subscribe: You can find more about the mailing-list at %{link}.
subscribe_msg: You may have to subscribe to the mailing-list first.
wiki: Wiki (page Mailing-List)
no_user_found: No user found
search: Search ...
search_user: Search user
title: New message
show:
all_messages: All messages
from: ! 'From:'
reply: Reply
sent_on: ! 'Sent:'
subject: ! 'Subject:'
title: Show message
messages_mailer:
foodsoft_message:
footer: ! 'Reply: %{reply_url}
See message online: %{msg_url}
Messaging options: %{profile_url}
'
navigation:
messages: Messages
simple_form:
hints:
message:
private: Message doesnt show in Foodsoft mail inbox

View file

@ -0,0 +1,5 @@
Rails.application.routes.draw do
scope '/:foodcoop' do
resources :messages, :only => [:index, :show, :new, :create]
end
end

View file

@ -0,0 +1,18 @@
# extracted from 20090120184410_road_to_version_three.rb
class CreateMessages < ActiveRecord::Migration
def self.up
create_table :messages do |t|
t.references :sender
t.text :recipients_ids
t.string :subject, :null => false
t.text :body
t.integer :email_state, :default => 0, :null => false
t.boolean :private, :default => false
t.datetime :created_at
end
end
def self.down
drop_table :messages
end
end

View file

@ -0,0 +1,23 @@
$:.push File.expand_path("../lib", __FILE__)
# Maintain your gem's version:
require "foodsoft_messages/version"
# Describe your gem and declare its dependencies:
Gem::Specification.new do |s|
s.name = "foodsoft_messages"
s.version = FoodsoftMessages::VERSION
s.authors = ["robwa"]
s.email = ["foodsoft-messages@ini.tiative.net"]
s.homepage = "https://github.com/foodcoops/foodsoft"
s.summary = "Messaging plugin for foodsoft."
s.description = "Adds the ability to exchange messages to foodsoft."
s.files = Dir["{app,config,db,lib}/**/*"] + ["Rakefile", "README.md"]
s.test_files = Dir["test/**/*"]
s.add_dependency "rails"
s.add_dependency "deface", "~> 1.0.0"
s.add_development_dependency "sqlite3"
end

View file

@ -0,0 +1,6 @@
require "foodsoft_messages/engine"
require "foodsoft_messages/user_link"
require "deface"
module FoodsoftMessages
end

View file

@ -0,0 +1,11 @@
module FoodsoftMessages
class Engine < ::Rails::Engine
def navigation(primary, context)
item = SimpleNavigation::Item.new(primary, :messages, I18n.t('navigation.messages'), context.messages_path)
sub_nav = primary[:foodcoop].sub_navigation
# display right before tasks item
tasks_index = sub_nav.items.index(sub_nav[:tasks])
sub_nav.items.insert(tasks_index, item)
end
end
end

View file

@ -0,0 +1,26 @@
module FoodsoftMessages
module UserLink
def self.included(base) # :nodoc:
base.class_eval do
# modify user presentation link to writing a message for the user
def show_user_link(user=@current_user)
if user.nil?
show_user user
else
link_to show_user(user), new_message_path('message[mail_to]' => user.id),
:title => I18n.t('helpers.messages.write_message')
end
end
end
end
end
end
# modify existing helper
ActiveSupport.on_load(:after_initialize) do
ApplicationHelper.send :include, FoodsoftMessages::UserLink
end

View file

@ -0,0 +1,3 @@
module FoodsoftMessages
VERSION = "0.0.1"
end