openproject-meeting 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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