outpost-aggregator 1.0.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md CHANGED
@@ -1,4 +1,22 @@
1
- ### 1.0.0 (2013-06-13)
1
+ ## 1.1.1
2
+ ### Additions
3
+ * Added a `dropLimit` in the view options, for specifying the number of articles allowed in the DropZone. By default, this is "null", which means "no limit".
4
+ * Added notifications for Limit, and a limit counter
5
+ * Specify that only ".sortable" nodes should be sortable
6
+ * Added support for Singular associations. Your `build_association` method should
7
+ just set the association via `self.content = content`.
8
+
9
+ ### Changes
10
+ * Removed old Secretary hooks.
11
+ * All Outpost object now have a "simple_json" method, which just returns a
12
+ hash with the ID. This should be overridden in most cases.
13
+
14
+
15
+ ## 1.1.0
16
+ Yanked
17
+
18
+
19
+ ## 1.0.0
2
20
  * The method to use is now `accepts_json_input_for`, and requires
3
21
  a single argument: the name of the association.
4
22
  * Does not do a `published?` check anymore. That's up to you to check when
@@ -10,7 +28,7 @@
10
28
  on the content - `published?`, `persisted?`, etc.
11
29
 
12
30
 
13
- ### v0.1.1
31
+ ## v0.1.1
14
32
  * Merge in the passed-in params (such as api token) to the params for
15
33
  "by_url" requests. This allows one to use their internal/private API
16
34
  with the by_url requests.
@@ -4,7 +4,7 @@
4
4
  # Hooks into ContentAPI to help YOU, our loyal
5
5
  # customer, aggregate content for various
6
6
  # purposes
7
- #
7
+ #
8
8
  # Made up of basically two parts:
9
9
  # * The "DropZone", where content will be dropped
10
10
  # and sorted and generally managed.
@@ -18,32 +18,32 @@ class outpost.Aggregator
18
18
  @TemplatePath = "outpost/aggregator/templates/"
19
19
 
20
20
  #---------------------
21
-
21
+
22
22
  defaults:
23
23
  apiType: "public"
24
24
  params: {}
25
25
  viewOptions: {}
26
-
26
+
27
27
  constructor: (el, input, json, options={}) ->
28
28
  @options = _.defaults options, @defaults
29
-
29
+
30
30
  @el = $(el)
31
31
  @input = $(input)
32
-
32
+
33
33
  # Set the type of API we're dealing with
34
34
  apiClass = if @options.apiType is "public" then "ContentCollection" else "PrivateContentCollection"
35
-
35
+
36
36
  @baseView = new outpost.Aggregator.Views.Base _.extend options.view || {},
37
- el: @el
38
- collection: new outpost.ContentAPI[apiClass](json)
39
- input: @input
40
- apiClass: apiClass
41
- params: @options.params
42
- viewOptions: @options.viewOptions
43
-
37
+ el : @el
38
+ collection : new outpost.ContentAPI[apiClass](json)
39
+ input : @input
40
+ apiClass : apiClass
41
+ params : @options.params
42
+ viewOptions : @options.viewOptions
43
+
44
44
  @baseView.render()
45
-
46
-
45
+
46
+
47
47
  #----------------------------------
48
48
  # Views!
49
49
  class @Views
@@ -53,49 +53,50 @@ class outpost.Aggregator
53
53
  template: JST[Aggregator.TemplatePath + 'base']
54
54
  defaults:
55
55
  active: "recent"
56
-
56
+ dropLimit: null
57
+
57
58
  #---------------------
58
-
59
+
59
60
  initialize: ->
60
61
  @options = _.defaults @options, @defaults
61
-
62
+
62
63
  # @foundCollection is the collection for all the content
63
64
  # in the RIGHT panel.
64
65
  @foundCollection = new outpost.ContentAPI[@options.apiClass]()
65
-
66
+
66
67
  #---------------------
67
68
  # Import a URL and turn it into content
68
69
  # Let the caller handle what happens after the request
69
70
  # via callbacks
70
71
  importUrl: (url, callbacks={}) ->
71
72
  $.getJSON(
72
- outpost.ContentAPI[@options.apiClass].prototype.url + "/by_url",
73
+ outpost.ContentAPI[@options.apiClass].prototype.url + "/by_url",
73
74
  _.extend @options.params, { url: url })
74
75
  .success((data, textStatus, jqXHR) -> callbacks.success?(data))
75
76
  .error((jqXHR, textStatus, errorThrown) -> callbacks.error?(jqXHR))
76
77
  .complete((jqXHR, status) -> callbacks.complete?(jqXHR))
77
78
 
78
79
  true
79
-
80
+
80
81
  #---------------------
81
-
82
+
82
83
  render: ->
83
84
  # Build the skeleton. We'll fill everything in next.
84
85
  @$el.html @template(active: @options.active)
85
-
86
+
86
87
  # Build each of the tabs
87
88
  @recentContent = new outpost.Aggregator.Views.RecentContent(base: @)
88
89
  @search = new outpost.Aggregator.Views.Search(base: @)
89
90
  @url = new outpost.Aggregator.Views.URL(base: @)
90
-
91
+
91
92
  # Build the Drop Zone section
92
93
  @dropZone = new outpost.Aggregator.Views.DropZone
93
94
  collection: @collection # The bootstrapped content
94
95
  base: @
95
-
96
+
96
97
  @
97
-
98
-
98
+
99
+
99
100
  #----------------------------------
100
101
  # The drop-zone!
101
102
  # Gets filled with ContentFull views
@@ -105,71 +106,87 @@ class outpost.Aggregator
105
106
  tagName: 'ul'
106
107
  attributes:
107
108
  class: "drop-zone well"
108
-
109
+
109
110
  # Define alerts as functions
110
111
  @Alerts:
111
112
  success: (el, data) ->
112
- new outpost.Notification(el, "success",
113
+ new outpost.Notification(el, "success",
113
114
  "<strong>Success!</strong> Imported #{data.id}")
114
115
 
115
116
  alreadyExists: (el) ->
116
- new outpost.Notification(el, "warning",
117
+ new outpost.Notification(el, "warning",
117
118
  "That content is already in the drop zone.")
118
119
 
120
+ limitReached: (el) ->
121
+ new outpost.Notification(el, "warning",
122
+ "The limit has been reached. Remove an article first.")
123
+
119
124
  invalidUrl: (el, url) ->
120
- new outpost.Notification(el, "error",
125
+ new outpost.Notification(el, "error",
121
126
  "<strong>Failure.</strong> Invalid URL (#{url})")
122
127
 
123
- error: (el) ->
124
- new outpost.Notification(el, "error",
128
+ error: (el) ->
129
+ new outpost.Notification(el, "error",
125
130
  "<strong>Error.</strong> Try the Search tab.")
126
131
 
127
132
  #---------------------
128
-
133
+
129
134
  initialize: ->
130
135
  @base = @options.base
131
-
136
+
132
137
  # Setup the container, render the template,
133
138
  # and then add in the el (the list)
134
139
  @container = $(@container)
135
140
  @container.html @template
136
141
  @container.append @$el
137
142
  @helper = $("<h1 />").html("Drop Content Here")
138
-
143
+
144
+ # Is there a limit? Add a notification to the top of the
145
+ # drop zone to let them know.
146
+ @limit = @options.limit
147
+
148
+ if @limit
149
+ @limitNotification =
150
+ new outpost.Notification(@$el, "info", "Limit")
151
+
139
152
  @render()
140
-
153
+
141
154
  # Register listeners for URL droppage
142
155
  @dragOver = false
143
156
  @$el.on "dragenter", (event) => @_dragEnter(event)
144
157
  @$el.on "dragleave", (event) => @_dragLeave(event)
145
158
  @$el.on "dragover", (event) => @_dragOver(event)
146
159
  @$el.on "drop", (event) => @importUrl(event)
147
-
160
+
148
161
  # Listeners for @collection events triggered
149
162
  # by Backbone
150
163
  @collection.bind "add remove reorder", =>
151
164
  @checkDropZone()
152
165
  @setPositions()
153
166
  @updateInput()
167
+ @updateLimitNotification()
154
168
 
155
169
  # DropZone callbacks!!
156
170
  sortIn = true
157
171
  dropped = false
158
172
 
159
173
  @$el.sortable
174
+ # Which items are sortable
175
+ items: ".sortable",
176
+
160
177
  # When dragging (sorting) starts
161
178
  start: (event, ui) ->
162
179
  sortIn = true
163
180
  dropped = false
164
181
  ui.item.addClass("dragging")
165
-
182
+
166
183
  # Called whenever an item is moved and is over the
167
184
  # DropZone.
168
185
  over: (event, ui) ->
169
186
  sortIn = true
170
187
  ui.item.addClass("adding")
171
188
  ui.item.removeClass("removing")
172
-
189
+
173
190
  # This one gets called both when the item moves out of
174
191
  # the dropzone, AND when the item is dropped inside of
175
192
  # the dropzone. I don't know why jquery-ui decided to
@@ -182,28 +199,28 @@ class outpost.Aggregator
182
199
  # in the dropzone, then add the "removing" class.
183
200
  # Also stop any animation immediately.
184
201
  #
185
- # If "drop event" is the case but the element came
202
+ # If "drop event" is the case but the element came
186
203
  # from somewhere else, then don't add the "removing"
187
- # class.
204
+ # class.
188
205
  if !dropped && ui.sender[0] == @$el[0]
189
206
  sortIn = false
190
207
  ui.item.stop(false, true)
191
208
  ui.item.addClass("removing")
192
-
209
+
193
210
  ui.item.removeClass("adding")
194
-
211
+
195
212
  # When dragging (sorting) stops, only if the item
196
213
  # being dragged belongs to the original list
197
214
  # Before placeholder disappears
198
215
  beforeStop: (event, ui) =>
199
216
  dropped = true
200
-
217
+
201
218
  # When an item from another list is dropped into this
202
219
  # DropZone
203
220
  # Move it from there to DropZone.
204
221
  receive: (event, ui) =>
205
222
  dropped = true
206
- # If we're able to move it in, Remove the dropped
223
+ # If we're able to move it in, Remove the dropped
207
224
  # element because we're rendering the bigger, better one.
208
225
  # Otherwise, revert the el back to the original element.
209
226
  if @move(ui.item)
@@ -211,7 +228,7 @@ class outpost.Aggregator
211
228
  else
212
229
  $(ui.item).effect 'highlight', color: "#f2dede", 1500
213
230
  $(ui.sender).sortable "cancel"
214
-
231
+
215
232
  # When dragging (sorting) stops, only for items
216
233
  # in the original list.
217
234
  # Update the position attribute for each
@@ -236,13 +253,13 @@ class outpost.Aggregator
236
253
  _stopEvent: (event) ->
237
254
  event.preventDefault()
238
255
  event.stopPropagation()
239
-
256
+
240
257
  #---------------------
241
258
  # When an element enters the zone
242
259
  _dragEnter: (event) ->
243
260
  @_stopEvent event
244
261
  @$el.addClass('dim')
245
-
262
+
246
263
  #---------------------
247
264
  # dragleave has child element problems
248
265
  # When you hover over a child element,
@@ -256,7 +273,7 @@ class outpost.Aggregator
256
273
  , 50
257
274
 
258
275
  @_stopEvent event
259
-
276
+
260
277
  #---------------------
261
278
  # When an element is in the zone and not yet released
262
279
  # Get continuously and rapidly fired when hovering with
@@ -265,7 +282,7 @@ class outpost.Aggregator
265
282
  _dragOver: (event) ->
266
283
  @dragOver = true
267
284
  @_stopEvent event
268
-
285
+
269
286
  #---------------------
270
287
  # Proxy to @base.importUrl
271
288
  # Grabs the dropped-in URL, passes it on
@@ -276,8 +293,8 @@ class outpost.Aggregator
276
293
  @container.spin(zIndex: 1)
277
294
  url = event.originalEvent.dataTransfer.getData('text/uri-list')
278
295
  alert = {}
279
-
280
- @base.importUrl url,
296
+
297
+ @base.importUrl url,
281
298
  success: (data) =>
282
299
  if data
283
300
  if @buildFromData(data)
@@ -286,7 +303,7 @@ class outpost.Aggregator
286
303
  @alert('alreadyExists')
287
304
  else
288
305
  @alert('invalidUrl', url)
289
-
306
+
290
307
  error: (jqXHR) =>
291
308
  @alert('error')
292
309
 
@@ -295,7 +312,7 @@ class outpost.Aggregator
295
312
  complete: (jqXHR) =>
296
313
  @container.spin(false)
297
314
  @$el.removeClass('dim')
298
-
315
+
299
316
  false # prevent default behavior
300
317
 
301
318
  #---------------------
@@ -326,7 +343,7 @@ class outpost.Aggregator
326
343
  alert: (alertKey, args...) ->
327
344
  notification = DropZone.Alerts[alertKey](@$el, args...)
328
345
  notification.prepend()
329
-
346
+
330
347
  setTimeout ->
331
348
  notification.fadeOut -> @remove()
332
349
  , 5000
@@ -335,12 +352,20 @@ class outpost.Aggregator
335
352
  # Moves a model from the "found" section into the drop zone.
336
353
  # Converts its view into a ContentFull view.
337
354
  move: (el) ->
355
+ # If the limit has already been reached.
356
+ # The updateLimitNotification() function should
357
+ # warn the user about this.
358
+ if @limitReached()
359
+ @alert("limitReached")
360
+ return
361
+
338
362
  id = el.attr("data-id")
339
-
363
+
340
364
  # Get the model for this DOM element
341
365
  # and add it to the DropZone
342
366
  # collection
343
367
  model = @base.foundCollection.get id
368
+
344
369
  # If the model is already in @collection, then
345
370
  # let the user know and do not import it
346
371
  # Otherwise, set the position and add it to the collection
@@ -348,7 +373,7 @@ class outpost.Aggregator
348
373
  @collection.add model
349
374
  view = new outpost.Aggregator.Views.ContentFull _.extend @base.options.viewOptions,
350
375
  model: model
351
-
376
+
352
377
  el.replaceWith view.render()
353
378
  @highlightSuccess(view.$el)
354
379
 
@@ -363,13 +388,13 @@ class outpost.Aggregator
363
388
 
364
389
  #---------------------
365
390
  # Remove this el's model from @collection
366
- # This is the only case where we want to
391
+ # This is the only case where we want to
367
392
  # actually remove a view from @base.childViews
368
393
  remove: (el) ->
369
394
  id = el.attr("data-id")
370
395
  model = @collection.get id
371
396
  @collection.remove model
372
-
397
+
373
398
  #---------------------
374
399
  # Render or hide the "Empty message" for the DropZone,
375
400
  # based on if there is content inside or not
@@ -390,12 +415,12 @@ class outpost.Aggregator
390
415
  _disableDropZoneHelper: ->
391
416
  @$el.removeClass('empty')
392
417
  @helper.detach()
393
-
418
+
394
419
  #---------------------
395
420
  # Go through the li's and find the corresponding model.
396
421
  # This is how we're able to save the order based on
397
422
  # the positions in the DropZone.
398
- # Note that this method uses the actual DOM, and
423
+ # Note that this method uses the actual DOM, and
399
424
  # therefore requires that the list has already been
400
425
  # rendered.
401
426
  #
@@ -406,14 +431,37 @@ class outpost.Aggregator
406
431
  id = el.attr("data-id")
407
432
  model = @collection.get id
408
433
  model.set "position", el.index()
409
-
434
+
410
435
  #---------------------
411
436
  # Update the JSON input with current collection
412
437
  updateInput: ->
413
438
  @base.options.input.val(JSON.stringify(@collection.simpleJSON()))
414
439
 
440
+
441
+ # Check if the limit has been reached, only if it exists.
442
+ limitReached: ->
443
+ @limit and @collection.length >= @limit
444
+
445
+
446
+ # Updates the limit notification.
447
+ # Updates the count, and changes the type if necessary.
448
+ updateLimitNotification: ->
449
+ return if not @limitNotification
450
+
451
+ @limitNotification.message =
452
+ "<strong>Limit:</strong> " +
453
+ "#{@collection.length} / #{@limit}"
454
+
455
+ if @limitReached()
456
+ @limitNotification.type = "success"
457
+ else
458
+ @limitNotification.type = "info"
459
+
460
+ @limitNotification.rerender()
461
+
462
+
415
463
  #---------------------
416
-
464
+
417
465
  render: ->
418
466
  @$el.empty()
419
467
  @checkDropZone()
@@ -421,11 +469,16 @@ class outpost.Aggregator
421
469
  # For each model, create a new model view and append it
422
470
  # to the el
423
471
  @collection.each (model) =>
424
- view = new outpost.Aggregator.Views.ContentFull _.extend @base.options.viewOptions,
425
- model: model
426
-
472
+ view = new outpost.Aggregator.Views.ContentFull(
473
+ _.extend @base.options.viewOptions, model: model)
474
+
427
475
  @$el.append view.render()
428
476
 
477
+ # Prepend & Update the limit notification
478
+ if @limitNotification
479
+ @limitNotification.prepend()
480
+ @updateLimitNotification()
481
+
429
482
  # Set positions.
430
483
  # setPositions depends on the DOM, so it has to be called
431
484
  # after the list has been rendered for it to work.
@@ -443,33 +496,33 @@ class outpost.Aggregator
443
496
  class @ContentList extends Backbone.View
444
497
  paginationTemplate: JST[Aggregator.TemplatePath + "_pagination"]
445
498
  errorTemplate: JST[Aggregator.TemplatePath + "error"]
446
- events:
499
+ events:
447
500
  "click .pagination a": "changePage"
448
-
501
+
449
502
  #---------------------
450
503
 
451
504
  initialize: ->
452
505
  @base = @options.base
453
506
  @page = 1
454
507
  @per_page = @base.options.params.limit || 10
455
-
508
+
456
509
  # Grab Recent Content using ContentAPI
457
510
  # Render the list
458
511
  @collection = new outpost.ContentAPI[@base.options.apiClass]()
459
-
512
+
460
513
  # Add just the added model to @base.foundCollection
461
514
  @collection.bind "add", (model, collection, options) =>
462
515
  @base.foundCollection.add model
463
-
516
+
464
517
  # Add the reset collection to @base.foundCollection
465
518
  @collection.bind "reset", (collection, options) =>
466
519
  @base.foundCollection.add collection.models
467
-
520
+
468
521
  @container = $(@container)
469
522
  @container.html @$el
470
-
523
+
471
524
  @render()
472
-
525
+
473
526
  #---------------------
474
527
  # Get the page from the DOM
475
528
  # Proxy to #request to setup params
@@ -490,11 +543,11 @@ class outpost.Aggregator
490
543
  # Also handles transitions
491
544
  _fetch: (params) ->
492
545
  @transitionStart()
493
-
546
+
494
547
  @collection.fetch
495
548
  data: _.defaults params, @base.options.params
496
549
  success: (collection, response, options) =>
497
- # If the collection length is > 0, then
550
+ # If the collection length is > 0, then
498
551
  # call @renderCollection().
499
552
  # Otherwise render a notice that no results
500
553
  # were found.
@@ -502,14 +555,14 @@ class outpost.Aggregator
502
555
  @renderCollection()
503
556
  else
504
557
  @alertNoResults()
505
-
558
+
506
559
  # Set the page and re-render the pagination
507
560
  @renderPagination(params, collection)
508
-
561
+
509
562
  error: (collection, xhr, options) =>
510
563
  @alertError(xhr: xhr)
511
564
  .always => @transitionEnd()
512
-
565
+
513
566
  # Return the collection
514
567
  @collection
515
568
 
@@ -528,7 +581,7 @@ class outpost.Aggregator
528
581
  @$el.spin(false)
529
582
 
530
583
  #---------------------
531
-
584
+
532
585
  _stopEvent: (event) ->
533
586
  event.preventDefault()
534
587
  event.stopPropagation()
@@ -543,16 +596,16 @@ class outpost.Aggregator
543
596
  # Render a notice if the server returned an error
544
597
  alertError: (options={}) ->
545
598
  xhr = options.xhr
546
-
599
+
547
600
  _.defaults options,
548
601
  el: @resultsEl
549
602
  type: "error"
550
603
  message: @errorTemplate(xhr: xhr)
551
604
  method: "replace"
552
-
553
- alert = new outpost.Notification(options.el,
605
+
606
+ alert = new outpost.Notification(options.el,
554
607
  options.type, options.message)
555
-
608
+
556
609
  alert[options.method]()
557
610
 
558
611
  #---------------------
@@ -563,35 +616,35 @@ class outpost.Aggregator
563
616
  type: "notice"
564
617
  message: "No results"
565
618
  method: "replace"
566
-
567
- alert = new outpost.Notification(options.el,
619
+
620
+ alert = new outpost.Notification(options.el,
568
621
  options.type, options.message)
569
-
622
+
570
623
  alert[options.method]()
571
-
624
+
572
625
  #---------------------
573
626
  # Fill in the @resultsEl with the model views
574
627
  renderCollection: ->
575
628
  @resultsEl.empty()
576
-
629
+
577
630
  @collection.each (model) =>
578
631
  view = new outpost.Aggregator.Views.ContentMinimal
579
632
  model: model
580
-
633
+
581
634
  @resultsEl.append view.render()
582
-
635
+
583
636
  @$el
584
637
 
585
638
  #---------------------
586
639
  # Re-render the pagination with new page values,
587
640
  # and set @page.
588
641
  #
589
- # If the passed-in length is less than the requested
590
- # limit, then assume that we reached the end of the
642
+ # If the passed-in length is less than the requested
643
+ # limit, then assume that we reached the end of the
591
644
  # results and disable the "Next" link
592
645
  renderPagination: (params, collection) ->
593
646
  @page = params.page
594
-
647
+
595
648
  # Add in the pagination
596
649
  # Prefer blank classes over "0" for consistency
597
650
  # parseInt(null) and parseInt("") both return null
@@ -603,7 +656,7 @@ class outpost.Aggregator
603
656
  )
604
657
 
605
658
  @$el
606
-
659
+
607
660
  #---------------------
608
661
  # Render the whole section.
609
662
  # This should only be called once per page load.
@@ -629,13 +682,13 @@ class outpost.Aggregator
629
682
  container: "#aggregator-recent-content"
630
683
  resultsId: "#aggregator-recent-content-results"
631
684
  template: JST[Aggregator.TemplatePath + 'recent_content']
632
-
685
+
633
686
  #---------------------
634
687
  # Need to populate right away for Recent Content
635
688
  initialize: ->
636
689
  super
637
690
  @request()
638
-
691
+
639
692
  #---------------------
640
693
  # Sets up default parameters, and then proxies to #_fetch
641
694
  request: (params={}) ->
@@ -643,17 +696,17 @@ class outpost.Aggregator
643
696
  limit: @per_page
644
697
  page: 1
645
698
  query: ""
646
-
699
+
647
700
  @_fetch(params)
648
701
  false # To keep consistent with Search#request
649
702
 
650
-
703
+
651
704
  #----------------------------------
652
705
  # SEARCH?!?!
653
- # This view is the entire Search section. It it made up of
706
+ # This view is the entire Search section. It it made up of
654
707
  # smaller "ContentMinimal" views
655
708
  #
656
- # Note that because of the Input field and pagination,
709
+ # Note that because of the Input field and pagination,
657
710
  # the list of content is actually stored in @resultsEl, not @el
658
711
  #
659
712
  # @render() is for rendering the full section.
@@ -689,10 +742,10 @@ class outpost.Aggregator
689
742
  limit: @per_page
690
743
  page: 1
691
744
  query: $("#aggregator-search-input", @$el).val()
692
-
745
+
693
746
  @_fetch(params)
694
747
  false # to keep the Rails form from submitting
695
-
748
+
696
749
 
697
750
  #----------------------------------
698
751
  # The URL Import view
@@ -731,11 +784,11 @@ class outpost.Aggregator
731
784
  # This overrides the default ContentList#_fetch
732
785
  _fetch: (params={}) ->
733
786
  @transitionStart()
734
-
787
+
735
788
  input = $("#aggregator-url-input", @$el)
736
789
  url = input.val()
737
-
738
- @base.importUrl url,
790
+
791
+ @base.importUrl url,
739
792
  success: (data) =>
740
793
  # Returns null if no record is found
741
794
  # If no data, alert the person
@@ -745,33 +798,34 @@ class outpost.Aggregator
745
798
  @collection.add data
746
799
  @append @collection.get(data.id)
747
800
  input.val("") # Empty the URL input
748
- else
801
+ else
749
802
  @alertNoResults
750
803
  method: "render"
751
804
  message: "Invalid URL"
752
-
805
+
753
806
  error: (jqXHR) => @alertError(xhr: jqXHR)
754
807
  complete: (jqHXR) => @transitionEnd()
755
-
808
+
756
809
  false # Prevent the Rails form from submitting
757
810
 
758
-
811
+
759
812
  #----------------------------------
760
813
  #----------------------------------
761
814
  # An abstract class from which the different
762
815
  # representations of a model should inherit
763
816
  class @ContentView extends Backbone.View
764
817
  tagName: 'li'
765
-
818
+ className: 'sortable'
819
+
766
820
  #---------------------
767
-
821
+
768
822
  initialize: ->
769
823
  # Add the model ID to the DOM
770
824
  # We have to do this so that we can share content
771
825
  # between the lists.
772
826
  @$el.attr("data-id", @model.id)
773
827
  @options = _.defaults @options, { template: @template }
774
-
828
+
775
829
  #---------------------
776
830
 
777
831
  render: ->
@@ -781,16 +835,14 @@ class outpost.Aggregator
781
835
  # A single piece of content in the drop zone!
782
836
  # Full with lots of information
783
837
  class @ContentFull extends @ContentView
784
- attributes:
785
- class: "content-full"
838
+ className: "sortable content-full"
786
839
  template: 'content_full'
787
-
840
+
788
841
  #----------------------------------
789
842
  # A single piece of recent content!
790
843
  # Just the basic info
791
844
  class @ContentMinimal extends @ContentView
792
- attributes:
793
- class: "content-minimal"
845
+ className: "sortable content-minimal"
794
846
  template: 'content_small'
795
847
 
796
848
  #---------------------
@@ -8,15 +8,15 @@
8
8
  <li class="<%= 'active' if @active is 'recent' %>">
9
9
  <a href="#aggregator-recent-content" data-toggle="tab">Recent</a>
10
10
  </li>
11
-
11
+
12
12
  <li class="<%= 'active' if @active is 'search' %>">
13
13
  <a href="#aggregator-search" data-toggle="tab">Search</a>
14
14
  </li>
15
-
15
+
16
16
  <li class"<%= 'active' if @active is 'url' %>">
17
17
  <a href="#aggregator-url" data-toggle="tab">URL</a>
18
18
  </li>
19
-
19
+
20
20
  <li class="<%= 'active' if @active is 'help' %>">
21
21
  <a href="#aggregator-help" data-toggle="tab">Help</a>
22
22
  </li>
@@ -27,7 +27,7 @@
27
27
  <div class="tab-pane<%= ' active' if @active is 'recent' %>" id="aggregator-recent-content">
28
28
  Loading Recent Content...
29
29
  </div>
30
-
30
+
31
31
  <!-- Search Tab -->
32
32
  <div class="tab-pane<%= ' active' if @active is 'search' %>" id="aggregator-search">
33
33
  Loading Search...
@@ -37,7 +37,7 @@
37
37
  <div class="tab-pane<%= ' active' if @active is 'url' %>" id="aggregator-url">
38
38
  Loading URL Import...
39
39
  </div>
40
-
40
+
41
41
  <!-- Help Tab -->
42
42
  <div class="tab-pane<%= ' active' if @active is 'help' %>" id="aggregator-help">
43
43
  <ul class="help-content">
@@ -64,7 +64,7 @@
64
64
  </ul>
65
65
  </li>
66
66
  <hr />
67
-
67
+
68
68
  <h4>Importing content</h4>
69
69
  <li><strong>Recent Tab</strong>: Drag content from the "Recent" tab into the Drop Zone</strong</li>
70
70
  <li>
@@ -9,8 +9,8 @@
9
9
  <% else: %>
10
10
  <span class="text-error"><strong>UNPUBLISHED</strong></span>
11
11
  <% end %><br />
12
-
13
- <% if !_.isEmpty(@content.teaser)?: %>
12
+
13
+ <% if !_.isEmpty(@content.teaser)?: %>
14
14
  <%= @content.teaser %> (<strong><%= @content.teaser?.length %></strong>)<br />
15
15
  <% end %>
16
16
 
@@ -9,7 +9,7 @@
9
9
  <% else: %>
10
10
  <span class="text-error"><strong>UNPUBLISHED</strong></span>
11
11
  <% end %>
12
-
12
+
13
13
  (<a href="<%=@content.edit_url%>" target="_blank"><%= @content.id %></a>)
14
14
  </div>
15
15
  </div>
@@ -4,13 +4,13 @@
4
4
  @include opacity(30);
5
5
  @include transition(opacity 0.1s linear)
6
6
  }
7
-
7
+
8
8
  // Override the Twitter pagination
9
9
  // To reduce the padding
10
10
  .pagination {
11
11
  margin: 0 0 10px 0;
12
12
  }
13
-
13
+
14
14
  input {
15
15
  max-width: 250px;
16
16
  }
@@ -18,13 +18,13 @@
18
18
  .drop-zone.empty {
19
19
  position: relative;
20
20
  color: $grayLight;
21
- h1 {
21
+ h1 {
22
22
  position: absolute;
23
23
  top: 60px;
24
24
  left: 50px;
25
25
  }
26
26
  }
27
-
27
+
28
28
  .drop-zone {
29
29
  max-height: 500px;
30
30
  overflow: auto;
@@ -35,6 +35,12 @@
35
35
  overflow: auto;
36
36
  }
37
37
 
38
+ &.short {
39
+ .content-list, .help-content {
40
+ max-height: 200px;
41
+ }
42
+ }
43
+
38
44
  .help-content {
39
45
  padding-right: 10px;
40
46
  }
@@ -42,36 +48,36 @@
42
48
  .drop-zone, .content-list {
43
49
  min-height: 200px;
44
50
  @extend .unstyled;
45
-
51
+
46
52
  li {
47
53
  @extend .clearfix;
48
-
54
+
49
55
  background: #fff;
50
56
  border: 1px solid $grayLight;
51
57
  margin: 5px 0;
52
58
  padding: 5px;
53
59
  cursor: move;
54
-
60
+
55
61
  @include box-shadow(2px 2px 6px #ccc);
56
62
  @include border-radius(3px);
57
-
63
+
58
64
  // Dragging any content
59
65
  &.dragging {
60
66
  @include box-shadow(2px 2px 10px #ccc);
61
67
  }
62
-
68
+
63
69
  // Adding content from the right panel
64
70
  &.adding {
65
71
  border-color: #0f0;
66
72
  }
67
-
73
+
68
74
  // Removing content from drop zone
69
75
  &.removing {
70
76
  background-color: #f2dede;
71
77
  border-color: #f00;
72
78
  &>div { @include opacity(30); }
73
79
  }
74
-
80
+
75
81
  img {
76
82
  float: left;
77
83
  margin-right: 5px;
@@ -84,7 +90,7 @@
84
90
  max-width: 120px;
85
91
  }
86
92
  }
87
-
93
+
88
94
  .content-minimal {
89
95
  img {
90
96
  max-width: 60px;
@@ -3,6 +3,15 @@ module Outpost
3
3
  module JsonInput
4
4
  extend ActiveSupport::Concern
5
5
 
6
+ # The default simple JSON for all objects.
7
+ # This catches one-to-one and many-to-one associations.
8
+ # It should be overridden for many-to-many associations.
9
+ def simple_json
10
+ @simple_json ||= {
11
+ "id" => self.respond_to?(:obj_key) ? self.obj_key : self.id
12
+ }
13
+ end
14
+
6
15
  module ClassMethods
7
16
  def accepts_json_input_for(name)
8
17
  include InstanceMethodsOnActivation
@@ -33,31 +42,39 @@ module Outpost
33
42
  def process_json_input_for(name, json)
34
43
  return if json.empty?
35
44
  name = name.to_s
36
-
45
+ reflection = self.class.reflect_on_association(name.to_sym)
46
+
37
47
  json = Array(JSON.parse(json)).sort_by { |c| c["position"].to_i }
38
- loaded = []
39
-
40
- json.each do |object_hash|
41
- if object = Outpost.obj_by_key(object_hash["id"])
42
- new_object = build_association_for(name, object_hash, object)
43
- loaded.push(new_object) if new_object
44
- end
45
- end
46
48
 
47
- loaded_json = Aggregator.array_to_simple_json(loaded)
48
- current_json = current_json_for(name)
49
-
50
- if current_json != loaded_json
51
- # Outpost::Version hook
52
- if self.respond_to?(:custom_changes)
53
- self.custom_changes[name] = [current_json, loaded_json]
49
+
50
+ if reflection.collection?
51
+ loaded = []
52
+
53
+ json.each do |object_hash|
54
+ if object = Outpost.obj_by_key(object_hash["id"])
55
+ new_object = build_association_for(name, object_hash, object)
56
+ loaded.push(new_object) if new_object
57
+ end
54
58
  end
55
59
 
56
- self.changed_attributes[name] = current_json
60
+ loaded_json = Aggregator.array_to_simple_json(loaded)
61
+ current_json = current_json_for(name)
57
62
 
58
- # This actually opens a DB transaction and saves stuff.
59
- # This is Rails behavior.
60
- self.send("#{name}=", loaded)
63
+ if current_json != loaded_json
64
+ # This actually opens a DB transaction and saves stuff.
65
+ # This is Rails behavior.
66
+ self.send("#{name}=", loaded)
67
+ end
68
+ else
69
+ object_hash = json.first
70
+
71
+ if object_hash.present?
72
+ if object = Outpost.obj_by_key(object_hash["id"])
73
+ build_association_for(name, object_hash, object)
74
+ end
75
+ else
76
+ self.send("#{name}=", nil)
77
+ end
61
78
  end
62
79
 
63
80
  self.send(name)
@@ -1,5 +1,5 @@
1
1
  module Outpost
2
2
  module Aggregator
3
- VERSION = "1.0.0"
3
+ VERSION = "1.1.1"
4
4
  end
5
5
  end
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
17
17
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
-
20
+
21
21
  spec.add_development_dependency "bundler", "~> 1.3"
22
22
  spec.add_development_dependency "rake"
23
23
  spec.add_development_dependency "rspec-rails"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: outpost-aggregator
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-09-12 00:00:00.000000000 Z
12
+ date: 2013-12-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
16
- requirement: !ruby/object:Gem::Requirement
16
+ requirement: &70360918168500 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,15 +21,10 @@ dependencies:
21
21
  version: '1.3'
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
- requirements:
27
- - - ~>
28
- - !ruby/object:Gem::Version
29
- version: '1.3'
24
+ version_requirements: *70360918168500
30
25
  - !ruby/object:Gem::Dependency
31
26
  name: rake
32
- requirement: !ruby/object:Gem::Requirement
27
+ requirement: &70360918165380 !ruby/object:Gem::Requirement
33
28
  none: false
34
29
  requirements:
35
30
  - - ! '>='
@@ -37,15 +32,10 @@ dependencies:
37
32
  version: '0'
38
33
  type: :development
39
34
  prerelease: false
40
- version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
- requirements:
43
- - - ! '>='
44
- - !ruby/object:Gem::Version
45
- version: '0'
35
+ version_requirements: *70360918165380
46
36
  - !ruby/object:Gem::Dependency
47
37
  name: rspec-rails
48
- requirement: !ruby/object:Gem::Requirement
38
+ requirement: &70360918155860 !ruby/object:Gem::Requirement
49
39
  none: false
50
40
  requirements:
51
41
  - - ! '>='
@@ -53,15 +43,10 @@ dependencies:
53
43
  version: '0'
54
44
  type: :development
55
45
  prerelease: false
56
- version_requirements: !ruby/object:Gem::Requirement
57
- none: false
58
- requirements:
59
- - - ! '>='
60
- - !ruby/object:Gem::Version
61
- version: '0'
46
+ version_requirements: *70360918155860
62
47
  - !ruby/object:Gem::Dependency
63
48
  name: combustion
64
- requirement: !ruby/object:Gem::Requirement
49
+ requirement: &70360918154960 !ruby/object:Gem::Requirement
65
50
  none: false
66
51
  requirements:
67
52
  - - ! '>='
@@ -69,15 +54,10 @@ dependencies:
69
54
  version: '0'
70
55
  type: :development
71
56
  prerelease: false
72
- version_requirements: !ruby/object:Gem::Requirement
73
- none: false
74
- requirements:
75
- - - ! '>='
76
- - !ruby/object:Gem::Version
77
- version: '0'
57
+ version_requirements: *70360918154960
78
58
  - !ruby/object:Gem::Dependency
79
59
  name: activerecord
80
- requirement: !ruby/object:Gem::Requirement
60
+ requirement: &70360918152920 !ruby/object:Gem::Requirement
81
61
  none: false
82
62
  requirements:
83
63
  - - ! '>='
@@ -85,15 +65,10 @@ dependencies:
85
65
  version: '0'
86
66
  type: :development
87
67
  prerelease: false
88
- version_requirements: !ruby/object:Gem::Requirement
89
- none: false
90
- requirements:
91
- - - ! '>='
92
- - !ruby/object:Gem::Version
93
- version: '0'
68
+ version_requirements: *70360918152920
94
69
  - !ruby/object:Gem::Dependency
95
70
  name: sqlite3
96
- requirement: !ruby/object:Gem::Requirement
71
+ requirement: &70360918151420 !ruby/object:Gem::Requirement
97
72
  none: false
98
73
  requirements:
99
74
  - - ~>
@@ -101,15 +76,10 @@ dependencies:
101
76
  version: '1.3'
102
77
  type: :development
103
78
  prerelease: false
104
- version_requirements: !ruby/object:Gem::Requirement
105
- none: false
106
- requirements:
107
- - - ~>
108
- - !ruby/object:Gem::Version
109
- version: '1.3'
79
+ version_requirements: *70360918151420
110
80
  - !ruby/object:Gem::Dependency
111
81
  name: factory_girl
112
- requirement: !ruby/object:Gem::Requirement
82
+ requirement: &70360918149680 !ruby/object:Gem::Requirement
113
83
  none: false
114
84
  requirements:
115
85
  - - ~>
@@ -117,12 +87,7 @@ dependencies:
117
87
  version: '4.2'
118
88
  type: :development
119
89
  prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- none: false
122
- requirements:
123
- - - ~>
124
- - !ruby/object:Gem::Version
125
- version: '4.2'
90
+ version_requirements: *70360918149680
126
91
  description: Content aggregator for Outpost
127
92
  email:
128
93
  - bricker88@gmail.com
@@ -157,7 +122,6 @@ files:
157
122
  - spec/factories.rb
158
123
  - spec/internal/config/database.yml
159
124
  - spec/internal/config/routes.rb
160
- - spec/internal/db/combustion_test.sqlite
161
125
  - spec/internal/db/schema.rb
162
126
  - spec/internal/log/.gitignore
163
127
  - spec/internal/public/favicon.ico
@@ -185,7 +149,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
185
149
  version: '0'
186
150
  requirements: []
187
151
  rubyforge_project:
188
- rubygems_version: 1.8.25
152
+ rubygems_version: 1.8.11
189
153
  signing_key:
190
154
  specification_version: 3
191
155
  summary: A simple UI and server-side integration to help aggregate content using an
@@ -194,11 +158,9 @@ test_files:
194
158
  - spec/factories.rb
195
159
  - spec/internal/config/database.yml
196
160
  - spec/internal/config/routes.rb
197
- - spec/internal/db/combustion_test.sqlite
198
161
  - spec/internal/db/schema.rb
199
162
  - spec/internal/log/.gitignore
200
163
  - spec/internal/public/favicon.ico
201
164
  - spec/lib/outpost/aggregator/json_input_spec.rb
202
165
  - spec/lib/outpost/aggregator/simple_json_spec.rb
203
166
  - spec/spec_helper.rb
204
- has_rdoc: