droom 0.0.1

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 (207) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.md +23 -0
  3. data/Rakefile +27 -0
  4. data/app/assets/images/droom/arrows.png +0 -0
  5. data/app/assets/images/droom/big_icons.png +0 -0
  6. data/app/assets/images/droom/blueblob.png +0 -0
  7. data/app/assets/images/droom/close.png +0 -0
  8. data/app/assets/images/droom/cross.png +0 -0
  9. data/app/assets/images/droom/download.png +0 -0
  10. data/app/assets/images/droom/greyblob.png +0 -0
  11. data/app/assets/images/droom/help/goodreader.jpg +0 -0
  12. data/app/assets/images/droom/ical.png +0 -0
  13. data/app/assets/images/droom/icons.png +0 -0
  14. data/app/assets/images/droom/medium_icons.png +0 -0
  15. data/app/assets/images/droom/medium_object_icons.png +0 -0
  16. data/app/assets/images/droom/minimonth.png +0 -0
  17. data/app/assets/images/droom/minisymbols.png +0 -0
  18. data/app/assets/images/droom/object_icons.png +0 -0
  19. data/app/assets/images/droom/pinkblob.png +0 -0
  20. data/app/assets/images/droom/place_busy.png +0 -0
  21. data/app/assets/images/droom/place_quiet.png +0 -0
  22. data/app/assets/images/droom/plus_bullet.png +0 -0
  23. data/app/assets/images/droom/search.png +0 -0
  24. data/app/assets/images/droom/setup.png +0 -0
  25. data/app/assets/images/droom/small_go.png +0 -0
  26. data/app/assets/images/droom/smallbullet.png +0 -0
  27. data/app/assets/images/droom/smallspinner.gif +0 -0
  28. data/app/assets/images/droom/spinner.gif +0 -0
  29. data/app/assets/images/droom/spr_toolbar_icons_r2.png +0 -0
  30. data/app/assets/images/droom/tablesort.png +0 -0
  31. data/app/assets/images/droom/tick.png +0 -0
  32. data/app/assets/images/droom/tinysymbols.png +0 -0
  33. data/app/assets/images/droom/twister.png +0 -0
  34. data/app/assets/images/droom/vcard.png +0 -0
  35. data/app/assets/images/droom/venue_bullet.png +0 -0
  36. data/app/assets/javascripts/droom.js.coffee +88 -0
  37. data/app/assets/javascripts/droom/calendar.js.coffee +121 -0
  38. data/app/assets/javascripts/droom/drag_sort.js.coffee +22 -0
  39. data/app/assets/javascripts/droom/forms.js.coffee +746 -0
  40. data/app/assets/javascripts/droom/lib/extensions.js.coffee +60 -0
  41. data/app/assets/javascripts/droom/lib/jquery.animate-colors.js +109 -0
  42. data/app/assets/javascripts/droom/lib/jquery.cookie.js +71 -0
  43. data/app/assets/javascripts/droom/lib/jquery.sortable.js +97 -0
  44. data/app/assets/javascripts/droom/lib/kalendae.js +1692 -0
  45. data/app/assets/javascripts/droom/lib/modernizr.js +4 -0
  46. data/app/assets/javascripts/droom/lib/parser_rules/advanced.js +553 -0
  47. data/app/assets/javascripts/droom/lib/parser_rules/simple.js +32 -0
  48. data/app/assets/javascripts/droom/lib/wysihtml5.js +9550 -0
  49. data/app/assets/javascripts/droom/map.js.coffee +123 -0
  50. data/app/assets/javascripts/droom/sort.js.coffee +126 -0
  51. data/app/assets/javascripts/droom/suggester.js.coffee +230 -0
  52. data/app/assets/stylesheets/droom.css.sass +1151 -0
  53. data/app/assets/stylesheets/lib/_kalendae.css.sass +142 -0
  54. data/app/assets/stylesheets/lib/_toolbar.css.sass +192 -0
  55. data/app/controllers/droom/dashboard_controller.rb +27 -0
  56. data/app/controllers/droom/document_attachments_controller.rb +19 -0
  57. data/app/controllers/droom/documents_controller.rb +116 -0
  58. data/app/controllers/droom/engine_controller.rb +43 -0
  59. data/app/controllers/droom/events_controller.rb +120 -0
  60. data/app/controllers/droom/group_invitations_controller.rb +47 -0
  61. data/app/controllers/droom/groups_controller.rb +67 -0
  62. data/app/controllers/droom/invitations_controller.rb +43 -0
  63. data/app/controllers/droom/memberships_controller.rb +47 -0
  64. data/app/controllers/droom/pages_controller.rb +61 -0
  65. data/app/controllers/droom/people_controller.rb +92 -0
  66. data/app/controllers/droom/suggestions_controller.rb +58 -0
  67. data/app/controllers/droom/venues_controller.rb +39 -0
  68. data/app/helpers/droom/droom_helper.rb +74 -0
  69. data/app/models/droom/agenda_category.rb +8 -0
  70. data/app/models/droom/category.rb +27 -0
  71. data/app/models/droom/document.rb +110 -0
  72. data/app/models/droom/document_attachment.rb +71 -0
  73. data/app/models/droom/document_link.rb +31 -0
  74. data/app/models/droom/event.rb +409 -0
  75. data/app/models/droom/event_set.rb +6 -0
  76. data/app/models/droom/group.rb +66 -0
  77. data/app/models/droom/group_invitation.rb +23 -0
  78. data/app/models/droom/invitation.rb +30 -0
  79. data/app/models/droom/membership.rb +27 -0
  80. data/app/models/droom/page.rb +26 -0
  81. data/app/models/droom/person.rb +302 -0
  82. data/app/models/droom/personal_document.rb +98 -0
  83. data/app/models/droom/recurrence_rule.rb +82 -0
  84. data/app/models/droom/venue.rb +125 -0
  85. data/app/views/droom/dashboard/_marginalia.html.haml +3 -0
  86. data/app/views/droom/dashboard/_my_future_events.html.haml +9 -0
  87. data/app/views/droom/dashboard/_my_group_documents.html.haml +6 -0
  88. data/app/views/droom/dashboard/_my_past_events.haml +6 -0
  89. data/app/views/droom/dashboard/index.html.haml +5 -0
  90. data/app/views/droom/documents/_created.html.haml +2 -0
  91. data/app/views/droom/documents/_document.html.haml +14 -0
  92. data/app/views/droom/documents/_document_line.html.haml +2 -0
  93. data/app/views/droom/documents/_documents_list.html.haml +31 -0
  94. data/app/views/droom/documents/_documents_table.html.haml +13 -0
  95. data/app/views/droom/documents/_event_document_form.html.haml +15 -0
  96. data/app/views/droom/documents/_form.html.haml +22 -0
  97. data/app/views/droom/documents/_listing.html.haml +8 -0
  98. data/app/views/droom/documents/_suggested.html.haml +9 -0
  99. data/app/views/droom/documents/_table_document.html.haml +31 -0
  100. data/app/views/droom/documents/edit.html.haml +1 -0
  101. data/app/views/droom/documents/index.html.haml +29 -0
  102. data/app/views/droom/documents/new.html.haml +1 -0
  103. data/app/views/droom/errors/bang.html.haml +12 -0
  104. data/app/views/droom/errors/not_allowed.html.haml +12 -0
  105. data/app/views/droom/errors/not_found.html.haml +12 -0
  106. data/app/views/droom/events/_attachment.html.haml +1 -0
  107. data/app/views/droom/events/_attachment_list.html.haml +4 -0
  108. data/app/views/droom/events/_calendar.html.haml +54 -0
  109. data/app/views/droom/events/_created.html.haml +2 -0
  110. data/app/views/droom/events/_event.html.haml +77 -0
  111. data/app/views/droom/events/_event_line.html.haml +13 -0
  112. data/app/views/droom/events/_events.html.haml +2 -0
  113. data/app/views/droom/events/_form.html.haml +35 -0
  114. data/app/views/droom/events/_invitations.html.haml +20 -0
  115. data/app/views/droom/events/_other_page_parts.html.haml +0 -0
  116. data/app/views/droom/events/_popup_event.html.haml +6 -0
  117. data/app/views/droom/events/_suggested.html.haml +12 -0
  118. data/app/views/droom/events/_views.html.haml +7 -0
  119. data/app/views/droom/events/edit.html.haml +1 -0
  120. data/app/views/droom/events/index.html.haml +12 -0
  121. data/app/views/droom/events/index.rss.builder +20 -0
  122. data/app/views/droom/events/new.html.haml +1 -0
  123. data/app/views/droom/events/show.html.haml +4 -0
  124. data/app/views/droom/group_invitations/_attending_groups.html.haml +8 -0
  125. data/app/views/droom/group_invitations/_created.html.haml +3 -0
  126. data/app/views/droom/group_invitations/_form.html.haml +4 -0
  127. data/app/views/droom/group_invitations/new.html.haml +1 -0
  128. data/app/views/droom/groups/_created.html.haml +3 -0
  129. data/app/views/droom/groups/_form.html.haml +12 -0
  130. data/app/views/droom/groups/_group.html.haml +11 -0
  131. data/app/views/droom/groups/_groups.html.haml +3 -0
  132. data/app/views/droom/groups/edit.html.haml +1 -0
  133. data/app/views/droom/groups/index.html.haml +7 -0
  134. data/app/views/droom/groups/show.html.haml +13 -0
  135. data/app/views/droom/invitations/_attending_people.html.haml +8 -0
  136. data/app/views/droom/invitations/_created.html.haml +3 -0
  137. data/app/views/droom/invitations/_form.html.haml +5 -0
  138. data/app/views/droom/invitations/new.html.haml +1 -0
  139. data/app/views/droom/memberships/_button.html.haml +11 -0
  140. data/app/views/droom/memberships/_created.html.haml +4 -0
  141. data/app/views/droom/memberships/_form.html.haml +7 -0
  142. data/app/views/droom/memberships/_member.html.haml +33 -0
  143. data/app/views/droom/memberships/_memberships.html.haml +4 -0
  144. data/app/views/droom/pages/_contents.html.haml +10 -0
  145. data/app/views/droom/pages/_form.html.haml +36 -0
  146. data/app/views/droom/pages/_full_page.html.haml +17 -0
  147. data/app/views/droom/pages/_page.html.haml +5 -0
  148. data/app/views/droom/pages/_pages.html.haml +2 -0
  149. data/app/views/droom/pages/admin.html.haml +24 -0
  150. data/app/views/droom/pages/edit.html.haml +1 -0
  151. data/app/views/droom/pages/index.html.haml +10 -0
  152. data/app/views/droom/pages/new.html.haml +4 -0
  153. data/app/views/droom/pages/show.html.haml +5 -0
  154. data/app/views/droom/people/_created.html.haml +6 -0
  155. data/app/views/droom/people/_form.html.haml +40 -0
  156. data/app/views/droom/people/_people.html.haml +29 -0
  157. data/app/views/droom/people/_person.html.haml +34 -0
  158. data/app/views/droom/people/_suggested.html.haml +9 -0
  159. data/app/views/droom/people/edit.html.haml +1 -0
  160. data/app/views/droom/people/index.html.haml +11 -0
  161. data/app/views/droom/people/new.html.haml +1 -0
  162. data/app/views/droom/people/show.html.haml +1 -0
  163. data/app/views/droom/shared/_calendar_and_search.html.haml +2 -0
  164. data/app/views/droom/shared/_calendar_holder.haml +3 -0
  165. data/app/views/droom/shared/_controls.html.haml +8 -0
  166. data/app/views/droom/shared/_navigation.html.haml +8 -0
  167. data/app/views/droom/shared/_search_form.html.haml +4 -0
  168. data/app/views/droom/shared/_suggestions.html.haml +11 -0
  169. data/app/views/droom/shared/_toolbar.html.haml +17 -0
  170. data/app/views/droom/venues/_suggested.html.haml +6 -0
  171. data/app/views/droom/venues/index.html.haml +6 -0
  172. data/app/views/droom/venues/show.html.haml +21 -0
  173. data/app/views/layouts/droom/application.html.haml +36 -0
  174. data/config/initializers/dav.rb +2 -0
  175. data/config/initializers/paperclip.rb +26 -0
  176. data/config/initializers/snail.rb +2 -0
  177. data/config/locales/en.yml +191 -0
  178. data/config/routes.rb +51 -0
  179. data/db/migrate/20120910075016_create_droom_data.rb +134 -0
  180. data/db/migrate/20120917095804_agenda_sections.rb +13 -0
  181. data/db/migrate/20120918121352_add_postal_address_to_people.rb +10 -0
  182. data/db/migrate/20121009075049_give_groups_descriptions.rb +5 -0
  183. data/db/migrate/20121009105244_more_names.rb +5 -0
  184. data/db/migrate/20121009145944_event_agenda_sections.rb +13 -0
  185. data/db/migrate/20121011091230_create_group_invitations.rb +10 -0
  186. data/db/migrate/20121012144720_give_people_positions.rb +5 -0
  187. data/db/migrate/20121012154558_help_pages.rb +13 -0
  188. data/db/migrate/20121012163201_category_slugs.rb +5 -0
  189. data/db/migrate/20121101160102_document_links.rb +14 -0
  190. data/db/migrate/20121101181247_people_visibility.rb +7 -0
  191. data/db/migrate/20121102094738_shy_people.rb +6 -0
  192. data/db/migrate/20121102095856_visibility_defaults.rb +8 -0
  193. data/lib/droom.rb +73 -0
  194. data/lib/droom/dav_resource.rb +36 -0
  195. data/lib/droom/engine.rb +8 -0
  196. data/lib/droom/helpers.rb +25 -0
  197. data/lib/droom/monkeys.rb +15 -0
  198. data/lib/droom/renderers.rb +13 -0
  199. data/lib/droom/validators.rb +5 -0
  200. data/lib/droom/version.rb +3 -0
  201. data/lib/generators/droom/dashboard/dashboard_generator.rb +16 -0
  202. data/lib/generators/droom/install/USAGE +9 -0
  203. data/lib/generators/droom/install/install_generator.rb +15 -0
  204. data/lib/generators/droom/install/templates/droom_initializer.rb +6 -0
  205. data/lib/generators/droom/views/views_generator.rb +9 -0
  206. data/lib/tasks/droom_tasks.rake +4 -0
  207. metadata +635 -0
@@ -0,0 +1,58 @@
1
+ module Droom
2
+ class SuggestionsController < Droom::EngineController
3
+ respond_to :json, :js
4
+ before_filter :authenticate_user!
5
+ before_filter :get_current_person
6
+ before_filter :get_classes
7
+
8
+ def index
9
+ fragment = params[:term]
10
+ max = params[:limit] || 10
11
+
12
+ if @types.include?('event') && span = Chronic.parse(fragment, :guess => false)
13
+ @suggestions = Droom::Event.falling_within(span).visible_to(@current_person)
14
+
15
+ else
16
+ @suggestions = @klasses.collect {|klass|
17
+ klass.constantize.visible_to(@current_person).name_matching(fragment).limit(max)
18
+ }.flatten.sort_by(&:name).slice(0, max)
19
+ end
20
+
21
+ respond_with @suggestions do |format|
22
+ format.json {
23
+ render :json => @suggestions.map { |suggestion| {
24
+ :type => suggestion.identifier,
25
+ :text => suggestion.name,
26
+ :id => suggestion.id
27
+ }}.to_json
28
+ }
29
+ format.js {
30
+ render :partial => "droom/shared/suggestions"
31
+ }
32
+ end
33
+ end
34
+
35
+ protected
36
+
37
+ def get_classes
38
+ if params[:type].blank?
39
+ @types = searchable_classes.keys
40
+ @klasses = searchable_classes.values
41
+ else
42
+ @types = searchable_classes.keys & [params[:type]].flatten
43
+ @klasses = searchable_classes.values_at(*@types)
44
+ end
45
+ end
46
+
47
+ def searchable_classes
48
+ {
49
+ "event" => "Droom::Event",
50
+ "person" => "Droom::Person",
51
+ "document" => "Droom::Document",
52
+ "group" => "Droom::Group",
53
+ "venue" => "Droom::Venue"
54
+ }
55
+ end
56
+
57
+ end
58
+ end
@@ -0,0 +1,39 @@
1
+ module Droom
2
+ class VenuesController < Droom::EngineController
3
+ respond_to :json, :html
4
+
5
+ before_filter :authenticate_user!
6
+ before_filter :get_current_person
7
+ before_filter :get_venues, :only => ["index"]
8
+ before_filter :get_venue, :only => [:show, :update]
9
+
10
+ def index
11
+ respond_with @venues do |format|
12
+ format.json {
13
+ render :json => @venues.to_json(:person => @person)
14
+ }
15
+ end
16
+ end
17
+
18
+ def show
19
+ respond_with @venue
20
+ end
21
+
22
+ def update
23
+ @venue.update_attributes(params[:venue])
24
+ respond_with @venue
25
+ end
26
+
27
+ protected
28
+
29
+ def get_venues
30
+ @venues = Venue.all
31
+ end
32
+
33
+ def get_venue
34
+ @venue = Venue.find(params[:id])
35
+ @events = @venue.events.visible_to(@current_person).future_and_current
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,74 @@
1
+ module Droom
2
+ module DroomHelper
3
+
4
+ def nav_link_to(name, url, options={})
5
+ options[:class] ||= ""
6
+ options[:class] << "here" if (request.path == url) || (request.path =~ /^#{url}/ && url != "/")
7
+ link_to name, url, options
8
+ end
9
+
10
+ def month_header_for(date)
11
+ content_tag('h3', l(date, :format => :month_header))
12
+ end
13
+
14
+ def pagination_summary(collection, options = {})
15
+ entry_name = options[:entry_name] || (collection.empty?? 'entry' : collection.first.class.name.underscore.sub('_', ' '))
16
+ summary = if collection.num_pages < 2
17
+ case collection.total_count
18
+ when 0; "No #{entry_name.pluralize} found"
19
+ when 1; "Displaying <strong>1</strong> #{entry_name}:"
20
+ else; "Displaying <strong>all #{collection.total_count}</strong> #{entry_name.pluralize}:"
21
+ end
22
+ else
23
+ offset = (collection.current_page - 1) * collection.limit_value
24
+ %{Displaying <strong>%d&nbsp;-&nbsp;%d</strong> of <strong>%d</strong> #{entry_name.pluralize}: } % [
25
+ offset + 1,
26
+ offset + collection.length,
27
+ collection.total_count
28
+ ]
29
+ end
30
+ summary.html_safe
31
+ end
32
+
33
+ # This will apply cloud-weighting to any list of items.
34
+ # They must have a 'weight' attribute
35
+ # and be ready to accept a 'cloud_size' attribute.
36
+
37
+ def cloud(these, threshold=0, biggest=3.0, smallest=1.3)
38
+ counts = these.map{|t| t.weight.to_i}.compact
39
+ if counts.any?
40
+ max = counts.max
41
+ min = counts.min
42
+ if max == min
43
+ these.each do |this|
44
+ this.cloud_size = sprintf("%.2f", biggest/2 + smallest/2)
45
+ end
46
+ else
47
+ steepness = Math.log(max - (min-1))/(biggest - smallest)
48
+ these.each do |this|
49
+ offset = Math.log(this.weight.to_i - (min-1))/steepness
50
+ this.cloud_size = sprintf("%.2f", smallest + offset)
51
+ end
52
+ end
53
+ if block_given?
54
+ these.each do |this|
55
+ yield this
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ def url_for_month(date)
62
+ calendar_url(:year => date.year, :month => date.month)
63
+ end
64
+
65
+ def url_for_date(date)
66
+ calendar_url(:year => date.year, :month => date.month, :mday => date.day)
67
+ end
68
+
69
+ def day_names
70
+ ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
71
+ end
72
+
73
+ end
74
+ end
@@ -0,0 +1,8 @@
1
+ module Droom
2
+ class AgendaCategory < ActiveRecord::Base
3
+ attr_accessible :event_id, :category_id
4
+ belongs_to :category
5
+ belongs_to :event
6
+ belongs_to :created_by, :class_name => Droom.user_class
7
+ end
8
+ end
@@ -0,0 +1,27 @@
1
+ module Droom
2
+ class Category < ActiveRecord::Base
3
+ attr_accessible :name, :description, :slug
4
+
5
+ belongs_to :created_by, :class_name => 'User'
6
+ has_many :document_attachments
7
+
8
+ before_validation :check_slug
9
+ validates :slug, :presence => true, :uniqueness => true
10
+
11
+ default_scope order("droom_categories.name ASC")
12
+
13
+ # *for_selection* returns a set of [name, id] pairs suitable for use as select options.
14
+ def self.for_selection
15
+ categories = self.all.map{|c| [c.name, c.id] }
16
+ categories.unshift(['', ''])
17
+ categories
18
+ end
19
+
20
+ protected
21
+
22
+ def check_slug
23
+ ensure_presence_and_uniqueness_of(:slug, name.parameterize)
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,110 @@
1
+ module Droom
2
+ class Document < ActiveRecord::Base
3
+ attr_accessible :name, :file, :description, :attachment_category_id, :event_id
4
+
5
+ # attachment_category and event_id are used on document creation to create an associated attachment
6
+ # this is a temporary shortcut
7
+ attr_accessor :old_version, :attachment_category_id, :event_id
8
+
9
+ belongs_to :created_by, :class_name => 'User'
10
+
11
+ has_many :document_attachments, :dependent => :destroy
12
+ has_many :document_links, :through => :document_attachments
13
+ has_many :people, :through => :document_links
14
+
15
+ has_attached_file :file
16
+
17
+ before_save :set_version
18
+ validates :file, :presence => true
19
+
20
+ scope :all_private, where("secret = 1")
21
+ scope :not_private, where("secret <> 1")
22
+ scope :all_public, where("public = 1 AND secret <> 1")
23
+ scope :not_public, where("public <> 1 OR secret = 1)")
24
+
25
+ scope :visible_to, lambda { |person|
26
+ if person
27
+ select('droom_documents.*')
28
+ .joins('LEFT OUTER JOIN droom_document_attachments ON droom_documents.id = droom_document_attachments.document_id')
29
+ .joins('LEFT OUTER JOIN droom_document_links ON droom_document_attachments.id = droom_document_links.document_attachment_id')
30
+ .where(["(droom_documents.public = 1 OR droom_document_links.person_id = ?)", person.id])
31
+ .group('droom_documents.id')
32
+ else
33
+ all_public
34
+ end
35
+ }
36
+
37
+ scope :name_matching, lambda { |fragment|
38
+ fragment = "%#{fragment}%"
39
+ where('droom_documents.name like ?', fragment)
40
+ }
41
+
42
+ scope :attached_to_these_groups, lambda { |groups|
43
+ placeholders = groups.map{'?'}.join(',')
44
+ select('droom_documents.*')
45
+ .joins('INNER JOIN droom_document_attachments ON droom_documents.id = droom_document_attachments.document_id AND droom_document_attachments.attachee_type = "Droom::Group"')
46
+ .where(["droom_document_attachments.attachee_id IN(#{placeholders})", *groups.map(&:id)])
47
+ }
48
+
49
+ scope :with_latest_event,
50
+ select('droom_documents.*, droom_categories.name AS category_name, droom_events.id AS latest_event_id, droom_events.name AS latest_event_name')
51
+ .joins('LEFT OUTER JOIN droom_document_attachments AS dda ON droom_documents.id = dda.document_id
52
+ LEFT OUTER JOIN droom_categories ON dda.category_id = droom_categories.id
53
+ LEFT OUTER JOIN droom_events ON dda.attachee_id = droom_events.id AND dda.attachee_type = "Droom::Event"')
54
+ .group('droom_documents.id')
55
+
56
+ # so that we can apply the joined finders above to an existing object
57
+ #
58
+ scope :this_document, lambda { |doc|
59
+ where(["droom_documents.id = ?", doc.id])
60
+ }
61
+
62
+ scope :by_date, order("droom_documents.updated_at DESC, droom_documents.created_at DESC")
63
+
64
+ def identifier
65
+ 'document'
66
+ end
67
+
68
+ def file_ok?
69
+ file.exists?
70
+ end
71
+
72
+ def changed_since_creation?
73
+ file_updated_at > created_at
74
+ end
75
+
76
+ def attachment_category_id=(id)
77
+ attach_to(Droom::Event.find(event_id), {:category_id => id})
78
+ end
79
+
80
+ def attach_to(attachee, attributes={})
81
+ save!
82
+ document_attachments.create(attributes.merge(:attachee => attachee))
83
+ end
84
+
85
+ def detach_from(attachee)
86
+ document_attachments.attached_to(attachee).destroy_all
87
+ end
88
+
89
+ def file_extension
90
+ if file_file_name
91
+ File.extname(file_file_name).sub(/^\./, '')
92
+ else
93
+ ""
94
+ end
95
+ end
96
+
97
+ def with_event
98
+ self.class.this_document(self).with_latest_event.first
99
+ end
100
+
101
+ protected
102
+
103
+ def set_version
104
+ if file.dirty?
105
+ self.version = (version || 0) + 1
106
+ end
107
+ end
108
+
109
+ end
110
+ end
@@ -0,0 +1,71 @@
1
+ module Droom
2
+ class DocumentAttachment < ActiveRecord::Base
3
+ attr_accessible :attachee, :document, :agenda_section, :category_id
4
+
5
+ belongs_to :document
6
+ belongs_to :attachee, :polymorphic => true
7
+ belongs_to :category
8
+ belongs_to :created_by, :class_name => "User"
9
+
10
+ has_many :document_links, :dependent => :destroy
11
+ has_many :people, :through => :document_links
12
+
13
+ after_destroy :remove_private_document_if_unattached
14
+ after_create :link_people
15
+
16
+ default_scope order("(CASE WHEN droom_document_attachments.category_id IS NULL THEN 0 ELSE 1 END), droom_documents.updated_at ASC, droom_documents.created_at ASC").includes(:document)
17
+
18
+ scope :unfiled, where("category_id IS NULL")
19
+
20
+ scope :attached_to, lambda { |attachee|
21
+ where(["droom_document_attachments.attachee_type = :class AND droom_document_attachments.attachee_id = :id", :class => attachee.class.to_s, :id => attachee.id])
22
+ }
23
+
24
+ scope :to_event, lambda { |event|
25
+ where(["droom_document_attachments.attachee_type = 'Droom::Event' AND droom_document_attachments.attachee_id = ?", event.id])
26
+ }
27
+
28
+ scope :to_group, lambda { |group|
29
+ where(["droom_document_attachments.attachee_type = 'Droom::Group' AND droom_document_attachments.attachee_id = ?", group.id])
30
+ }
31
+
32
+ scope :to_events, lambda { |events|
33
+ placeholders = events.map{'?'}.join(',')
34
+ ids = events.map(&:id)
35
+ where(["droom_document_attachments.attachee_type = 'Droom::Event' AND droom_document_attachments.attachee_id IN (#{placeholders})", *ids])
36
+ }
37
+
38
+ scope :to_groups, lambda { |groups|
39
+ placeholders = groups.map{'?'}.join(',')
40
+ ids = groups.map(&:id)
41
+ where(["droom_document_attachments.attachee_type = 'Droom::Group' AND droom_document_attachments.attachee_id IN (#{placeholders})", *ids])
42
+ }
43
+
44
+ def slug
45
+ if attachee
46
+ attachee.slug
47
+ else
48
+ 'Unattached'
49
+ end
50
+ end
51
+
52
+ def category_name
53
+ category ? category.name : "uncategorised"
54
+ end
55
+
56
+ protected
57
+
58
+ def remove_private_document_if_unattached
59
+ unless document.public? || document.document_attachments.count > 0
60
+ document.destroy
61
+ end
62
+ end
63
+
64
+ # Upon creation we create document links to all the people who are newly entitled to see the document.
65
+ # Deletion takes care of itself, as document_links are :dependent => :destroy.
66
+ #
67
+ def link_people
68
+ people << attachee.people if attachee
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,31 @@
1
+ # the links between a person and document are too various and too extended to make them
2
+ # easy to retrieve in a single query. Instead we maintain an index of person-document links.
3
+
4
+ module Droom
5
+ class DocumentLink < ActiveRecord::Base
6
+ attr_accessible :person, :document_attachment
7
+ belongs_to :person
8
+ belongs_to :document_attachment
9
+ has_one :personal_document, :dependent => :destroy
10
+
11
+ delegate :slug, :category, :to => :document_attachment
12
+
13
+ def document
14
+ document_attachment.document
15
+ end
16
+
17
+ def document=(doc)
18
+ create_document_attachment(:document => doc)
19
+ end
20
+
21
+ def ensure_personal_document
22
+ create_personal_document unless personal_document
23
+ end
24
+
25
+ def self.repair
26
+ Droom::Person.each do |person|
27
+ person.repair_document_links
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,409 @@
1
+ require 'open-uri'
2
+ require 'zip/zip'
3
+ require 'uuidtools'
4
+ require 'chronic'
5
+ require 'ri_cal'
6
+
7
+ module Droom
8
+ class Event < ActiveRecord::Base
9
+ attr_accessible :start, :finish, :name, :description, :event_set_id, :created_by_id, :uuid, :all_day, :master_id, :url, :start_date, :start_time, :finish_date, :finish_time, :venue, :private, :public, :venue_name
10
+
11
+ belongs_to :created_by, :class_name => 'User'
12
+
13
+ has_many :invitations, :dependent => :destroy
14
+ has_many :people, :through => :invitations
15
+
16
+ has_many :group_invitations, :dependent => :destroy
17
+ has_many :groups, :through => :group_invitations
18
+
19
+ has_many :document_attachments, :as => :attachee, :dependent => :destroy
20
+ has_many :documents, :through => :document_attachments
21
+
22
+ has_many :agenda_categories, :dependent => :destroy
23
+ has_many :categories, :through => :agenda_categories
24
+
25
+ belongs_to :venue
26
+ accepts_nested_attributes_for :venue
27
+
28
+ belongs_to :event_set
29
+ accepts_nested_attributes_for :event_set
30
+
31
+ belongs_to :master, :class_name => 'Event'
32
+ has_many :occurrences, :class_name => 'Event', :foreign_key => 'master_id', :dependent => :destroy
33
+ has_many :recurrence_rules, :dependent => :destroy, :conditions => {:active => true}
34
+ accepts_nested_attributes_for :recurrence_rules, :allow_destroy => true
35
+
36
+ validates :start, :presence => true, :date => true
37
+ validates :finish, :date => {:after => :start, :allow_nil => true}
38
+ validates :uuid, :presence => true, :uniqueness => true
39
+ validates :name, :presence => true
40
+
41
+ before_validation :set_uuid
42
+ before_save :check_slug
43
+ after_save :update_occurrences
44
+
45
+ scope :primary, where("master_id IS NULL")
46
+ scope :recurrent, where(:conditions => "master_id IS NOT NULL")
47
+ default_scope order('start ASC').includes(:venue)
48
+
49
+ ## Event retrieval in various ways
50
+ #
51
+ scope :visible_to, lambda { |person|
52
+ if person
53
+ select('droom_events.*')
54
+ .joins('LEFT OUTER JOIN droom_invitations ON droom_events.id = droom_invitations.event_id')
55
+ .where(["(droom_events.public = 1 OR droom_invitations.person_id = ?)", person.id])
56
+ .group('droom_events.id')
57
+ else
58
+ all_public
59
+ end
60
+ }
61
+
62
+ scope :after, lambda { |datetime| # datetime. eg calendar.occurrences.after(Time.now)
63
+ where(['start > ?', datetime])
64
+ }
65
+
66
+ scope :before, lambda { |datetime| # datetime. eg calendar.occurrences.before(Time.now)
67
+ where(['start < :date AND (finish IS NULL or finish < :date)', :date => datetime])
68
+ }
69
+
70
+ scope :between, lambda { |start, finish| # datetimable objects. eg. Event.between(reader.last_login, Time.now)
71
+ where(['start > :start AND start < :finish AND (finish IS NULL or finish < :finish)', :start => start, :finish => finish])
72
+ }
73
+
74
+ scope :future_and_current, lambda {
75
+ where(['(finish > :now) OR (finish IS NULL AND start > :now)', :now => Time.now])
76
+ }
77
+
78
+ scope :unfinished, lambda { |start| # datetimable object.
79
+ where(['start < :start AND finish > :start', :start => start])
80
+ }
81
+
82
+ scope :by_finish, order("finish ASC")
83
+
84
+ scope :coincident_with, lambda { |start, finish| # datetimable objects.
85
+ where(['(start < :finish AND finish > :start) OR (finish IS NULL AND start > :start AND start < :finish)', {:start => start, :finish => finish}])
86
+ }
87
+
88
+ scope :limited_to, lambda { |limit|
89
+ limit(limit)
90
+ }
91
+
92
+ scope :at_venue, lambda { |venue| # EventVenue object
93
+ where(["venue_id = ?", venue.id])
94
+ }
95
+
96
+ scope :except_these_uuids, lambda { |uuids| # array of uuid strings
97
+ placeholders = uuids.map{'?'}.join(',')
98
+ where(["uuid NOT IN (#{placeholders})", *uuids])
99
+ }
100
+
101
+ scope :without_invitations_to, lambda { |person| # Person object
102
+ select("droom_events.*")
103
+ .joins("LEFT OUTER JOIN droom_invitations ON droom_events.id = droom_invitations.event_id AND droom_invitations.person_id = #{sanitize(person.id)}")
104
+ .group("droom_events.id")
105
+ .having("COUNT(droom_invitations.id) = 0")
106
+ }
107
+
108
+ scope :with_documents,
109
+ select("droom_events.*")
110
+ .joins("INNER JOIN droom_document_attachments ON droom_events.id = droom_document_attachments.attachee_id AND droom_document_attachments.attachee_type = 'Droom::Event'")
111
+ .group("droom_events.id")
112
+
113
+
114
+ scope :all_private, where("private = 1 OR private = 't'")
115
+ scope :not_private, where("private = 0 OR private = 'f'")
116
+ scope :all_public, where("public = 1 OR public = 't'")
117
+ scope :not_public, where("public = 0 OR public = 'f'")
118
+
119
+ scope :name_matching, lambda { |fragment|
120
+ fragment = "%#{fragment}%"
121
+ where('droom_events.name like ?', fragment)
122
+ }
123
+
124
+ # All of these class methods also return scopes.
125
+ #
126
+ def self.in_the_last(period) # seconds. eg calendar.occurrences.in_the_last(1.week)
127
+ finish = Time.now
128
+ start = finish - period
129
+ between(start, finish)
130
+ end
131
+
132
+ def self.in_year(year) # just a number. eg calendar.occurrences.in_year(2010)
133
+ start = DateTime.civil(year)
134
+ finish = start + 1.year
135
+ between(start, finish)
136
+ end
137
+
138
+ def self.in_month(year, month) # numbers. eg calendar.occurrences.in_month(2010, 12)
139
+ start = DateTime.civil(year, month)
140
+ finish = start + 1.month
141
+ between(start, finish)
142
+ end
143
+
144
+ def self.in_week(year, week) # numbers, with a commercial week: eg calendar.occurrences.in_week(2010, 35)
145
+ start = DateTime.commercial(year, week)
146
+ finish = start + 1.week
147
+ between(start, finish)
148
+ end
149
+
150
+ def self.on_day (year, month, day) # numbers: eg calendar.occurrences.on_day(2010, 12, 12)
151
+ start = DateTime.civil(year, month, day)
152
+ finish = start + 1.day
153
+ between(start, finish)
154
+ end
155
+
156
+ def self.in_span(span) # Chronic::Span
157
+ between(span.begin, span.end)
158
+ end
159
+
160
+ def self.falling_within(span) # Chronic::Span
161
+ coincident_with(span.begin, span.end)
162
+ end
163
+
164
+ def self.future
165
+ after(Time.now)
166
+ end
167
+
168
+ def self.past
169
+ before(Time.now)
170
+ end
171
+
172
+ ## Instance methods
173
+ #
174
+ def invite(person)
175
+ self.people << person
176
+ end
177
+
178
+ def attach(doc)
179
+ self.documents << doc
180
+ end
181
+
182
+ def identifier
183
+ 'event'
184
+ end
185
+
186
+ # We store the start and end points of the event as a single DateTime value to make comparison simple.
187
+ # The setters for date and time are overridden to pass strings through chronic's natural language parser
188
+ # and to treat numbers as epoch seconds. These should all work as you'd expect:
189
+ #
190
+ # event.start = "Tuesday at 11pm"
191
+ # event.start = "12/12/1969 at 10pm"
192
+ # event.start = "1347354111"
193
+ # event.start = Time.now + 1.hour
194
+ #
195
+ def start=(value)
196
+ write_attribute :start, parse_date(value)
197
+ end
198
+
199
+ def finish=(value)
200
+ write_attribute :finish, parse_date(value)
201
+ end
202
+
203
+ # For interface purposes we often want to separate date and time parts. These getters will return the
204
+ # corresponding Date or Time object.
205
+ #
206
+ # The `time_of_day` gem makes time handling a bit more intuitive by concealing the date part of a Time object.
207
+ #
208
+ def start_time
209
+ start.time_of_day if start
210
+ end
211
+
212
+ def start_date
213
+ start.to_date if start
214
+ end
215
+
216
+ def finish_time
217
+ finish.time_of_day if finish
218
+ end
219
+
220
+ def finish_date
221
+ finish.to_date if finish
222
+ end
223
+
224
+ # And these setters will adjust the current value so that its date or time part corresponds to the given
225
+ # value. The value is passed through the same parsing mechanism as above, so:
226
+ #
227
+ # event.start = "Tuesday at 11pm" -> next Tuesday at 11pm
228
+ # event.start_time = "8pm" -> next Tuesday at 8pm
229
+ # event.start_date = "Wednesday" -> next Wednesday at 8pm
230
+ # event.start_date = "26 February 2016" -> 26/2/16 at 8pm
231
+ # event.start_time = "18:00" -> 26/2/16 at 6pm
232
+ #
233
+ # If the time is set before the date, we default to that time today. Times default to 00:00 in the usual way.
234
+ #
235
+ def start_time=(value)
236
+ self.start = (start_date || Date.today).to_time + parse_date(value).seconds_since_midnight
237
+ end
238
+
239
+ def start_date=(value)
240
+ self.start = parse_date(value).to_date# + start_time
241
+ end
242
+
243
+ def finish_time=(value)
244
+ self.finish = (finish_date || start_date || Date.today).to_time + parse_date(value).seconds_since_midnight
245
+ end
246
+
247
+ def finish_date=(value)
248
+ self.finish = parse_date(value).to_date# + finish_time
249
+ end
250
+
251
+ def duration
252
+ if finish
253
+ finish - start
254
+ else
255
+ 0
256
+ end
257
+ end
258
+
259
+ def venue_name
260
+ venue.name if venue
261
+ end
262
+
263
+ def venue_name=(name)
264
+ self.venue = Droom::Venue.find_or_create_by_name(name)
265
+ end
266
+
267
+ # Agenda sections are global, and we don't want to have to manage the extra workload of associating a section with an event.
268
+ # This call retrieves all the agenda sections that are associated with any of our document attachments, and is suitable for looping
269
+ # on a template to display attachments per section.
270
+ #
271
+ def agenda_sections
272
+ Droom::AgendaSection.associated_with_event(self)
273
+ end
274
+
275
+ # and this sorts our attachment list into agenda section buckets so that we don't have to go back to the database for each section.
276
+ def attachments_by_category
277
+ cats = {}
278
+ document_attachments.each_with_object({}) do |att, hash|
279
+ key = att.category_name
280
+ cats[key] ||= []
281
+ cats[key].push(att)
282
+ end
283
+ categories.each do |category|
284
+ key = category.name
285
+ cats[key] ||= []
286
+ end
287
+ cats
288
+ end
289
+
290
+ def categories_for_selection
291
+ cats = categories.map{|c| [c.name, c.id] }
292
+ cats.unshift(['', ''])
293
+ cats
294
+ end
295
+
296
+ def visible_to?(user_or_person)
297
+ return true if self.public?
298
+ return false unless user_or_person
299
+ return true if user_or_person.admin?
300
+ return true if user_or_person.person.invited_to?(self)
301
+ return false if self.private?
302
+ return true
303
+ end
304
+
305
+ def one_day?
306
+ all_day? && within_day?
307
+ end
308
+
309
+ def within_day?
310
+ (!finish || start.to.jd == finish.to.jd || finish == start + 1.day)
311
+ end
312
+
313
+ def continuing?
314
+ finish && start < Time.now && finish > Time.now
315
+ end
316
+
317
+ def finished?
318
+ start < Time.now && (!finish || finish < Time.now)
319
+ end
320
+
321
+ def recurs?
322
+ master || occurrences.any?
323
+ end
324
+
325
+ def recurrence
326
+ recurrence_rules.first.to_s
327
+ end
328
+
329
+ def add_recurrence(rule)
330
+ self.recurrence_rules << Droom::RecurrenceRule.from(rule)
331
+ end
332
+
333
+ def documents_zipped
334
+ if self.documents.any?
335
+ tempfile = Tempfile.new("droom-temp-#{slug}-#{Time.now}.zip")
336
+ Zip::ZipOutputStream.open(tempfile.path) do |z|
337
+ self.documents.each do |doc|
338
+ z.add(doc.file_file_name, open(doc.file.url))
339
+ end
340
+ end
341
+ tempfile
342
+ end
343
+ end
344
+
345
+ def to_rical
346
+ RiCal.Event do |cal_event|
347
+ cal_event.uid = uuid
348
+ cal_event.summary = name
349
+ cal_event.description = description if description
350
+ cal_event.dtstart = (all_day? ? start_date : start) if start
351
+ cal_event.dtend = (all_day? ? finish_date : finish) if finish
352
+ cal_event.url = url if url
353
+ cal_event.rrules = recurrence_rules.map(&:to_rical) if recurrence_rules.any?
354
+ cal_event.location = venue.name if venue
355
+ end
356
+ end
357
+
358
+ def to_ics
359
+ to_rical.to_s
360
+ end
361
+
362
+ def as_json(options={})
363
+ json = super
364
+ json[:datestring] = I18n.l start, :format => :natural_with_date
365
+ json
366
+ end
367
+
368
+ protected
369
+
370
+ def check_slug
371
+ ensure_presence_and_uniqueness_of(:slug, "#{start.strftime("%Y %m %d")} #{name}".parameterize)
372
+ end
373
+
374
+ def set_uuid
375
+ self.uuid = UUIDTools::UUID.timestamp_create.to_s if uuid.blank?
376
+ end
377
+
378
+ # doesn't yet observe exceptions
379
+ def update_occurrences
380
+ occurrences.destroy_all
381
+ if recurrence_rules.any?
382
+ recurrence_horizon = Time.now + 10.years
383
+ to_rical.occurrences(:before => recurrence_horizon).each do |occ|
384
+ occurrences.create!({
385
+ :name => self.name,
386
+ :url => self.url,
387
+ :description => self.description,
388
+ :venue => self.venue,
389
+ :start => occ.dtstart,
390
+ :finish => occ.dtend,
391
+ :uuid => nil
392
+ }) unless occ.dtstart == self.start
393
+ end
394
+ end
395
+ end
396
+
397
+ def parse_date(value)
398
+ case value
399
+ when Numeric
400
+ Time.at(value)
401
+ when String
402
+ Chronic.parse(value)
403
+ else
404
+ value
405
+ end
406
+ end
407
+
408
+ end
409
+ end