openproject-meeting 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +15 -0
  2. data/CHANGELOG.md +46 -0
  3. data/README.md +82 -0
  4. data/app/assets/images/meeting/agenda.png +0 -0
  5. data/app/assets/images/meeting/meeting.png +0 -0
  6. data/app/assets/images/meeting/minutes.png +0 -0
  7. data/app/assets/stylesheets/meeting/meeting.css.erb +39 -0
  8. data/app/controllers/meeting_agendas_controller.rb +33 -0
  9. data/app/controllers/meeting_contents_controller.rb +95 -0
  10. data/app/controllers/meeting_minutes_controller.rb +22 -0
  11. data/app/controllers/meetings_controller.rb +150 -0
  12. data/app/helpers/meeting_contents_helper.rb +68 -0
  13. data/app/helpers/meetings_helper.rb +16 -0
  14. data/app/mailers/meeting_mailer.rb +28 -0
  15. data/app/models/meeting.rb +163 -0
  16. data/app/models/meeting_agenda.rb +95 -0
  17. data/app/models/meeting_content.rb +67 -0
  18. data/app/models/meeting_minutes.rb +81 -0
  19. data/app/models/meeting_participant.rb +45 -0
  20. data/app/views/hooks/_activity_index_head.html.erb +13 -0
  21. data/app/views/meeting_contents/_form.html.erb +33 -0
  22. data/app/views/meeting_contents/_show.html.erb +40 -0
  23. data/app/views/meeting_contents/diff.html.erb +33 -0
  24. data/app/views/meeting_contents/history.html.erb +49 -0
  25. data/app/views/meeting_contents/show.html.erb +13 -0
  26. data/app/views/meeting_mailer/content_for_review.html.erb +23 -0
  27. data/app/views/meeting_mailer/content_for_review.text.erb +9 -0
  28. data/app/views/meetings/_form.html.erb +61 -0
  29. data/app/views/meetings/edit.html.erb +17 -0
  30. data/app/views/meetings/index.html.erb +51 -0
  31. data/app/views/meetings/new.html.erb +17 -0
  32. data/app/views/meetings/show.html.erb +48 -0
  33. data/app/views/shared/_meeting_header.html.erb +3 -0
  34. data/config/locales/de.yml +47 -0
  35. data/config/locales/en.yml +47 -0
  36. data/config/routes.rb +53 -0
  37. data/db/migrate/20110106210555_create_meetings.rb +29 -0
  38. data/db/migrate/20110106221214_create_meeting_contents.rb +29 -0
  39. data/db/migrate/20110106221946_create_meeting_content_versions.rb +20 -0
  40. data/db/migrate/20110108230721_create_meeting_participants.rb +30 -0
  41. data/db/migrate/20110224180804_add_lock_to_meeting_content.rb +24 -0
  42. data/db/migrate/20110819162852_create_initial_meeting_journals.rb +49 -0
  43. data/db/migrate/20111605171815_merge_meeting_content_versions_with_journals.rb +65 -0
  44. data/db/migrate/20130731151542_remove_meeting_role_id_from_meeting_participants.rb +20 -0
  45. data/lib/open_project/meeting.rb +16 -0
  46. data/lib/open_project/meeting/engine.rb +101 -0
  47. data/lib/open_project/meeting/hooks.rb +17 -0
  48. data/lib/open_project/meeting/patches/project_patch.rb +24 -0
  49. data/lib/open_project/meeting/version.rb +16 -0
  50. data/lib/openproject-meeting.rb +12 -0
  51. data/spec/controllers/meetings_controller_spec.rb +90 -0
  52. data/spec/factories/meeting_agenda_factory.rb +16 -0
  53. data/spec/factories/meeting_agenda_journal_factory.rb +18 -0
  54. data/spec/factories/meeting_factory.rb +18 -0
  55. data/spec/factories/meeting_minutes_factory.rb +16 -0
  56. data/spec/factories/meeting_minutes_journal_factory.rb +18 -0
  57. data/spec/factories/meeting_participant_factory.rb +17 -0
  58. data/spec/mailers/meeting_mailer_spec.rb +101 -0
  59. data/spec/models/meeting_agenda_journal_spec.rb +21 -0
  60. data/spec/models/meeting_agenda_spec.rb +52 -0
  61. data/spec/models/meeting_minutes_journal_spec.rb +21 -0
  62. data/spec/models/meeting_minutes_spec.rb +44 -0
  63. data/spec/models/meeting_spec.rb +168 -0
  64. data/spec/models/user_deletion_spec.rb +186 -0
  65. data/spec/spec_helper.rb +14 -0
  66. data/spec/support/plugin_spec_helper.rb +47 -0
  67. metadata +158 -0
@@ -0,0 +1,68 @@
1
+ #-- copyright
2
+ # OpenProject is a project management system.
3
+ #
4
+ # Copyright (C) 2012-2013 the OpenProject Team
5
+ #
6
+ # This program is free software; you can redistribute it and/or
7
+ # modify it under the terms of the GNU General Public License version 3.
8
+ #
9
+ # See doc/COPYRIGHT.rdoc for more details.
10
+ #++
11
+
12
+ module MeetingContentsHelper
13
+ def can_edit_meeting_content?(content, content_type)
14
+ authorize_for(content_type.pluralize, 'update') && content.editable?
15
+ end
16
+
17
+ def saved_meeting_content_text_present?(content)
18
+ !content.new_record? && content.text.present? && !content.text.empty?
19
+ end
20
+
21
+ def show_meeting_content_editor?(content, content_type)
22
+ can_edit_meeting_content?(content, content_type) && (!saved_meeting_content_text_present?(content) || content.changed?)
23
+ end
24
+
25
+ def meeting_content_context_menu(content, content_type)
26
+ menu = []
27
+ menu << meeting_agenda_toggle_status_link(content, content_type)
28
+ menu << meeting_content_edit_link(content_type) if can_edit_meeting_content?(content, content_type)
29
+ menu << meeting_content_history_link(content_type, content.meeting)
30
+ menu << meeting_content_notify_link(content_type, content.meeting) if saved_meeting_content_text_present?(content)
31
+ menu.join(" ")
32
+ end
33
+
34
+ def meeting_agenda_toggle_status_link(content, content_type)
35
+ content.meeting.agenda.present? && content.meeting.agenda.locked? ?
36
+ open_meeting_agenda_link(content_type, content.meeting) :
37
+ close_meeting_agenda_link(content_type, content.meeting)
38
+ end
39
+
40
+ def close_meeting_agenda_link(content_type, meeting)
41
+ case content_type
42
+ when "meeting_agenda"
43
+ link_to_if_authorized l(:label_meeting_close), {:controller => '/meeting_agendas', :action => 'close', :meeting_id => meeting}, :method => :put, :class => "icon icon-lock show-meeting_agenda"
44
+ when "meeting_minutes"
45
+ link_to_if_authorized l(:label_meeting_agenda_close), {:controller => '/meeting_agendas', :action => 'close', :meeting_id => meeting}, :method => :put, :class => "icon icon-lock show-meeting_minutes"
46
+ end
47
+ end
48
+
49
+ def open_meeting_agenda_link(content_type, meeting)
50
+ case content_type
51
+ when "meeting_agenda"
52
+ link_to_if_authorized l(:label_meeting_open), {:controller => '/meeting_agendas', :action => 'open', :meeting_id => meeting}, :method => :put, :class => 'icon icon-unlock show-meeting_agenda', :confirm => l(:text_meeting_agenda_open_are_you_sure)
53
+ when "meeting_minutes"
54
+ end
55
+ end
56
+
57
+ def meeting_content_edit_link(content_type)
58
+ link_to l(:button_edit), "#", :class => "icon icon-edit show-#{content_type}", :accesskey => accesskey(:edit), :onclick => "$$('.edit-#{content_type}').invoke('show'); $$('.show-#{content_type}').invoke('hide'); return false;"
59
+ end
60
+
61
+ def meeting_content_history_link(content_type, meeting)
62
+ link_to_if_authorized l(:label_history), {:controller => '/' + content_type.pluralize, :action => 'history', :meeting_id => meeting}, :class => "icon icon-history show-#{content_type}"
63
+ end
64
+
65
+ def meeting_content_notify_link(content_type, meeting)
66
+ link_to_if_authorized l(:label_notify), {:controller => '/' + content_type.pluralize, :action => 'notify', :meeting_id => meeting}, :method => :put, :class => "icon icon-notification show-#{content_type}"
67
+ end
68
+ end
@@ -0,0 +1,16 @@
1
+ #-- copyright
2
+ # OpenProject is a project management system.
3
+ #
4
+ # Copyright (C) 2012-2013 the OpenProject Team
5
+ #
6
+ # This program is free software; you can redistribute it and/or
7
+ # modify it under the terms of the GNU General Public License version 3.
8
+ #
9
+ # See doc/COPYRIGHT.rdoc for more details.
10
+ #++
11
+
12
+ module MeetingsHelper
13
+ def format_participant_list(participants)
14
+ participants.sort.collect{|p| link_to_user p.user}.join("; ").html_safe
15
+ end
16
+ end
@@ -0,0 +1,28 @@
1
+ #-- copyright
2
+ # OpenProject is a project management system.
3
+ #
4
+ # Copyright (C) 2012-2013 the OpenProject Team
5
+ #
6
+ # This program is free software; you can redistribute it and/or
7
+ # modify it under the terms of the GNU General Public License version 3.
8
+ #
9
+ # See doc/COPYRIGHT.rdoc for more details.
10
+ #++
11
+
12
+ class MeetingMailer < UserMailer
13
+
14
+ def content_for_review(content, content_type)
15
+ @meeting = content.meeting
16
+ @meeting_url = meeting_url @meeting
17
+ @project_url = project_url @meeting.project
18
+ @content_type = content_type
19
+
20
+ open_project_headers 'Project' => @meeting.project.identifier,
21
+ 'Meeting-Id' => @meeting.id
22
+
23
+ recipients = @meeting.watcher_recipients.reject{|r| r == @meeting.author.mail}
24
+
25
+ subject = "[#{@meeting.project.name}] #{I18n.t(:"label_#{content_type}")}: #{@meeting.title}"
26
+ mail :to => @meeting.author.mail, :cc => recipients, :subject => subject
27
+ end
28
+ end
@@ -0,0 +1,163 @@
1
+ #-- copyright
2
+ # OpenProject is a project management system.
3
+ #
4
+ # Copyright (C) 2012-2013 the OpenProject Team
5
+ #
6
+ # This program is free software; you can redistribute it and/or
7
+ # modify it under the terms of the GNU General Public License version 3.
8
+ #
9
+ # See doc/COPYRIGHT.rdoc for more details.
10
+ #++
11
+
12
+ class Meeting < ActiveRecord::Base
13
+
14
+ self.table_name = 'meetings'
15
+
16
+ belongs_to :project
17
+ belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
18
+ has_one :agenda, :dependent => :destroy, :class_name => 'MeetingAgenda'
19
+ has_one :minutes, :dependent => :destroy, :class_name => 'MeetingMinutes'
20
+ has_many :contents, :class_name => 'MeetingContent', :readonly => true
21
+ has_many :participants, :dependent => :destroy, :class_name => 'MeetingParticipant'
22
+
23
+ default_scope order("#{Meeting.table_name}.start_time DESC")
24
+ scope :from_tomorrow, :conditions => ['start_time >= ?', Date.tomorrow.beginning_of_day]
25
+ scope :with_users_by_date, order("#{Meeting.table_name}.title ASC")
26
+ .includes({:participants => :user}, :author)
27
+
28
+ attr_accessible :title, :location, :start_time, :duration
29
+
30
+ acts_as_watchable
31
+
32
+ acts_as_searchable :columns => ["#{table_name}.title", "#{MeetingContent.table_name}.text"],
33
+ :include => [:contents, :project],
34
+ :date_column => "#{table_name}.created_at"
35
+
36
+ acts_as_journalized :activity_find_options => {:include => [:agenda, :author, :project]},
37
+ :event_title => Proc.new {|o| "#{l :label_meeting}: #{o.title} (#{format_date o.start_time} #{format_time o.start_time, false}-#{format_time o.end_time, false})"},
38
+ :event_url => Proc.new {|o| {:controller => '/meetings', :action => 'show', :id => o.journaled}}
39
+
40
+ register_on_journal_formatter(:plaintext, 'title')
41
+ register_on_journal_formatter(:fraction, 'duration')
42
+ register_on_journal_formatter(:datetime, 'start_time')
43
+ register_on_journal_formatter(:plaintext, 'location')
44
+
45
+ accepts_nested_attributes_for :participants, :allow_destroy => true
46
+
47
+ validates_presence_of :title, :start_time, :duration
48
+
49
+ before_save :add_new_participants_as_watcher
50
+
51
+ after_initialize :set_initial_values
52
+
53
+ User.before_destroy do |user|
54
+ Meeting.update_all ['author_id = ?', DeletedUser.first.id], ['author_id = ?', user.id]
55
+ end
56
+
57
+ def start_date
58
+ # the text_field + calendar_for form helpers expect a Date
59
+ start_time.to_date if start_time.present?
60
+ end
61
+
62
+ def start_month
63
+ start_time.month
64
+ end
65
+
66
+ def start_year
67
+ start_time.year
68
+ end
69
+
70
+ def end_time
71
+ start_time + duration.hours
72
+ end
73
+
74
+ def to_s
75
+ title
76
+ end
77
+
78
+ def text
79
+ agenda.text if agenda.present?
80
+ end
81
+
82
+ def author=(user)
83
+ super
84
+ # Don't add the author as participant if we already have some through nested attributes
85
+ self.participants.build(:user => user, :invited => true) if (self.new_record? && self.participants.empty? && user)
86
+ end
87
+
88
+ # Returns true if usr or current user is allowed to view the meeting
89
+ def visible?(user=nil)
90
+ (user || User.current).allowed_to?(:view_meetings, self.project)
91
+ end
92
+
93
+ def all_possible_participants
94
+ self.project.users.all(:include => { :memberships => [:roles, :project] } ).select{ |u| self.visible?(u) }
95
+ end
96
+
97
+ def copy(attrs)
98
+ copy = self.dup
99
+
100
+ copy.author = attrs.delete(:author)
101
+ copy.attributes = attrs
102
+ copy.send(:set_initial_values)
103
+
104
+ copy.participants.clear
105
+ copy.participants_attributes = self.participants.collect(&:copy_attributes)
106
+
107
+ copy
108
+ end
109
+
110
+ def self.group_by_time(meetings)
111
+ by_start_year_month_date = ActiveSupport::OrderedHash.new do |hy, year|
112
+ hy[year] = ActiveSupport::OrderedHash.new do |hm, month|
113
+ hm[month] = ActiveSupport::OrderedHash.new
114
+ end
115
+ end
116
+
117
+ meetings.group_by(&:start_year).each do |year, objs|
118
+
119
+ objs.group_by(&:start_month).each do |month,objs|
120
+
121
+ objs.group_by(&:start_date).each do |date,objs|
122
+
123
+ by_start_year_month_date[year][month][date] = objs
124
+
125
+ end
126
+
127
+ end
128
+
129
+ end
130
+
131
+ by_start_year_month_date
132
+ end
133
+
134
+ def close_agenda_and_copy_to_minutes!
135
+ self.agenda.lock!
136
+ self.create_minutes(:text => agenda.text)
137
+ end
138
+
139
+ alias :original_participants_attributes= :participants_attributes=
140
+ def participants_attributes=(attrs)
141
+ attrs.each do |participant|
142
+ participant['_destroy'] = true if !(participant['attended'] || participant['invited'])
143
+ end
144
+ self.original_participants_attributes = attrs
145
+ end
146
+
147
+
148
+ protected
149
+
150
+ def set_initial_values
151
+ # set defaults
152
+ self.start_time ||= Date.tomorrow + 10.hours
153
+ self.duration ||= 1
154
+ end
155
+
156
+ private
157
+
158
+ def add_new_participants_as_watcher
159
+ self.participants.select(&:new_record?).each do |p|
160
+ add_watcher(p.user)
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,95 @@
1
+ #-- copyright
2
+ # OpenProject is a project management system.
3
+ #
4
+ # Copyright (C) 2012-2013 the OpenProject Team
5
+ #
6
+ # This program is free software; you can redistribute it and/or
7
+ # modify it under the terms of the GNU General Public License version 3.
8
+ #
9
+ # See doc/COPYRIGHT.rdoc for more details.
10
+ #++
11
+
12
+ class MeetingAgenda < MeetingContent
13
+
14
+ acts_as_journalized :activity_type => 'meetings',
15
+ :activity_permission => :view_meetings,
16
+ :activity_find_options => {:include => {:meeting => :project}},
17
+ :event_title => Proc.new {|o| "#{l :label_meeting_agenda}: #{o.meeting.title}"},
18
+ :event_url => Proc.new {|o| {:controller => '/meetings', :action => 'show', :id => o.meeting}}
19
+
20
+ def activity_type
21
+ 'meetings'
22
+ end
23
+
24
+ # TODO: internationalize the comments
25
+ def lock!(user = User.current)
26
+ self.comment = "Agenda closed"
27
+ self.author = user
28
+ self.locked = true
29
+ self.save
30
+ end
31
+
32
+ def unlock!(user = User.current)
33
+ self.comment = "Agenda opened"
34
+ self.author = user
35
+ self.locked = false
36
+ self.save
37
+ end
38
+
39
+ def editable?
40
+ !locked?
41
+ end
42
+
43
+ MeetingAgendaJournal.class_eval do
44
+ unloadable
45
+
46
+ attr_protected :data
47
+ after_save :compress_version_text
48
+
49
+ # Wiki Content might be large and the data should possibly be compressed
50
+ def compress_version_text
51
+ self.text = changed_data["text"].last if changed_data["text"]
52
+ self.text ||= self.journaled.text if self.journaled.text
53
+ end
54
+
55
+ def text=(plain)
56
+ case Setting.wiki_compression
57
+ when "gzip"
58
+ begin
59
+ text_hash :text => Zlib::Deflate.deflate(plain, Zlib::BEST_COMPRESSION), :compression => Setting.wiki_compression
60
+ rescue
61
+ text_hash :text => plain, :compression => ''
62
+ end
63
+ else
64
+ text_hash :text => plain, :compression => ''
65
+ end
66
+ plain
67
+ end
68
+
69
+ def text_hash(hash)
70
+ changed_data.delete("text")
71
+ changed_data["data"] = hash[:text]
72
+ changed_data["compression"] = hash[:compression]
73
+ update_attribute(:changed_data, changed_data)
74
+ # changed_data = changed_data
75
+ end
76
+
77
+ def text
78
+ @text ||= case changed_data[:compression]
79
+ when 'gzip'
80
+ Zlib::Inflate.inflate(data)
81
+ else
82
+ # uncompressed data
83
+ changed_data["data"]
84
+ end
85
+ end
86
+
87
+ def meeting
88
+ journaled.meeting
89
+ end
90
+
91
+ def editable?
92
+ false
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,67 @@
1
+ #-- copyright
2
+ # OpenProject is a project management system.
3
+ #
4
+ # Copyright (C) 2012-2013 the OpenProject Team
5
+ #
6
+ # This program is free software; you can redistribute it and/or
7
+ # modify it under the terms of the GNU General Public License version 3.
8
+ #
9
+ # See doc/COPYRIGHT.rdoc for more details.
10
+ #++
11
+
12
+ class MeetingContent < ActiveRecord::Base
13
+
14
+ belongs_to :meeting
15
+ belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
16
+
17
+ attr_accessor :comment
18
+
19
+ validates_length_of :comment, :maximum => 255, :allow_nil => true
20
+
21
+ attr_accessible :text, :lock_version, :comment
22
+
23
+ before_save :comment_to_journal_notes
24
+
25
+ User.before_destroy do |user|
26
+ MeetingContent.update_all ['author_id = ?', DeletedUser.first], ['author_id = ?', user.id]
27
+ end
28
+
29
+ def editable?
30
+ true
31
+ end
32
+
33
+ def diff(version_to=nil, version_from=nil)
34
+ version_to = version_to ? version_to.to_i : self.version
35
+ version_from = version_from ? version_from.to_i : version_to - 1
36
+ version_to, version_from = version_from, version_to unless version_from < version_to
37
+
38
+ content_to = self.journals.find_by_version(version_to)
39
+ content_from = self.journals.find_by_version(version_from)
40
+
41
+ (content_to && content_from) ? WikiPage::WikiDiff.new(content_to, content_from) : nil
42
+ end
43
+
44
+ # Compatibility for mailer.rb
45
+ def updated_on
46
+ updated_at
47
+ end
48
+
49
+ # Show the project on activity and search views
50
+ def project
51
+ meeting.project
52
+ end
53
+
54
+ # Provided for compatibility of the old pre-journalized migration
55
+ def self.create_versioned_table
56
+ end
57
+
58
+ # Provided for compatibility of the old pre-journalized migration
59
+ def self.drop_versioned_table
60
+ end
61
+
62
+ private
63
+
64
+ def comment_to_journal_notes
65
+ init_journal(author, comment) unless changes.empty?
66
+ end
67
+ end
@@ -0,0 +1,81 @@
1
+ #-- copyright
2
+ # OpenProject is a project management system.
3
+ #
4
+ # Copyright (C) 2012-2013 the OpenProject Team
5
+ #
6
+ # This program is free software; you can redistribute it and/or
7
+ # modify it under the terms of the GNU General Public License version 3.
8
+ #
9
+ # See doc/COPYRIGHT.rdoc for more details.
10
+ #++
11
+
12
+ class MeetingMinutes < MeetingContent
13
+
14
+ acts_as_journalized :activity_type => 'meetings',
15
+ :activity_permission => :view_meetings,
16
+ :activity_find_options => {:include => {:meeting => :project}},
17
+ :event_title => Proc.new {|o| "#{l :label_meeting_minutes}: #{o.meeting.title}"},
18
+ :event_url => Proc.new {|o| {:controller => '/meetings', :action => 'show', :id => o.meeting}}
19
+
20
+ def activity_type
21
+ 'meetings'
22
+ end
23
+
24
+ def editable?
25
+ meeting.agenda.present? && meeting.agenda.locked?
26
+ end
27
+
28
+ protected
29
+
30
+ MeetingMinutesJournal.class_eval do
31
+ unloadable
32
+
33
+ attr_protected :data
34
+ after_save :compress_version_text
35
+
36
+ # Wiki Content might be large and the data should possibly be compressed
37
+ def compress_version_text
38
+ self.text = changed_data["text"].last if changed_data["text"]
39
+ self.text ||= self.journaled.text if self.journaled.text
40
+ end
41
+
42
+ def text=(plain)
43
+ case Setting.wiki_compression
44
+ when "gzip"
45
+ begin
46
+ text_hash :text => Zlib::Deflate.deflate(plain, Zlib::BEST_COMPRESSION), :compression => Setting.wiki_compression
47
+ rescue
48
+ text_hash :text => plain, :compression => ''
49
+ end
50
+ else
51
+ text_hash :text => plain, :compression => ''
52
+ end
53
+ plain
54
+ end
55
+
56
+ def text_hash(hash)
57
+ changed_data.delete("text")
58
+ changed_data["data"] = hash[:text]
59
+ changed_data["compression"] = hash[:compression]
60
+ update_attribute(:changed_data, changed_data)
61
+ end
62
+
63
+ def text
64
+ @text ||= case changed_data[:compression]
65
+ when 'gzip'
66
+ Zlib::Inflate.inflate(data)
67
+ else
68
+ # uncompressed data
69
+ changed_data["data"]
70
+ end
71
+ end
72
+
73
+ def meeting
74
+ journaled.meeting
75
+ end
76
+
77
+ def editable?
78
+ false
79
+ end
80
+ end
81
+ end