outpost-aggregator 1.0.0 → 1.1.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.
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: