droom 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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