spud_photos 0.0.2 → 0.0.3

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/README.markdown CHANGED
@@ -6,14 +6,12 @@ Spud Photos is an engine for creating and managing photo galleries, designed for
6
6
 
7
7
  1. In your Gemfile add the following
8
8
 
9
- gem 'spud_core', :git => "git://github.com/davydotcom/spud_core_admin.git"
10
- gem 'spud_photos', :git => "git://github.com/gregawoods/spud_photos.git"
9
+ gem 'spud_photos'
11
10
 
12
11
  2. Run bundle install
13
12
  3. Copy in database migrations to your new rails project
14
13
 
15
- bundle exec rake spud_core:install:migrations
16
- bundle exec rake spud_photos:install:migrations
14
+ bundle exec rake railties:install:migrations
17
15
  rake db:migrate
18
16
 
19
17
  4. Run a rails server instance and point your browser to /spud/admin
@@ -23,7 +21,7 @@ Spud Photos is an engine for creating and managing photo galleries, designed for
23
21
  Spud Photos accepts the following configuration options:
24
22
 
25
23
  Spud::Photos.configure do |config|
26
- self.base_layout = 'application'
24
+ config.base_layout = 'application'
27
25
  config.galleries_enabled = false
28
26
  config.base_path = 'photos'
29
27
  config.photo_styles = {
@@ -32,6 +30,10 @@ Spud Photos accepts the following configuration options:
32
30
  :large => '400x400#',
33
31
  :huge => '600x600'
34
32
  }
33
+ self.paperclip_storage = :filesystem #use :s3 to use s3 storage (aws gem required)
34
+ config.s3_credentials = "#{Rails.root}/config/s3.yml"
35
+ config.storage_path = ":rails_root/public/system/spud_photos/:id/:style/:basename.:extension"
36
+ config.storage_url = "/system/spud_photos/:id/:style/:basename.:extension"
35
37
  end
36
38
 
37
39
  The `photo_styles` option will be passed to [Paperclip][2], so any valid paperclip styles can be added here.
@@ -6,64 +6,111 @@ Spud.Admin.Photos = new function(){
6
6
  var self = this;
7
7
 
8
8
  this.init = function(){
9
+ // event handlers
9
10
  $('.spud_admin_photo_ui_thumbs_sortable').sortable({
10
11
  connectWith:'.spud_admin_photo_ui_thumbs_sortable'
11
12
  });
12
- $('#spud_admin_photo_album_form, #spud_admin_photo_gallery_form').live('submit', self.submittedPhotoAlbumOrGalleryForm)
13
- $('.spud_admin_photo_mass_destroy').live('click', self.massDestroySelected);
14
- $('#spud_admin_photo_form').live('submit', self.submittedPhotoForm);
15
- $('.spud_admin_photo_ui_thumb_selectable input[type=checkbox]').live('click', self.invertPhotoUiThumbCheckbox);
16
- $('.spud_admin_photo_ui_thumb_selectable').live('click', self.selectedPhotoUiThumb);
17
- self.markSelectedPhotoUiThumbs();
18
- };
19
-
20
- this.submittedPhotoAlbumOrGalleryForm = function(e){
21
- // update photo checkboxes
22
- $('.spud_admin_photo_ui_thumb').each(function(){
23
- var item = $(this);
24
- var checkbox = item.find('input[type=checkbox]');
25
- checkbox.attr('checked', (item.parents('.spud_admin_photos_selection_left').length>0));
13
+ $('body').on('submit', '#spud_admin_photo_album_form', self.submittedPhotoAlbumForm)
14
+ $('body').on('submit', '#spud_admin_photo_form', self.submittedPhotoForm);
15
+ $('body').on('click', '.spud_admin_photos_btn_remove', self.clickedPhotoRemoveFromLibrary)
16
+ $('body').on('click', '.spud_admin_photo_ui_thumbs_selectable .spud_admin_photo_ui_thumb', self.selectedPhotoUiThumb);
17
+ $('body').on('click', '#spud_admin_photo_album_action_library', self.clickedPhotoLibrary);
18
+
19
+ // html5 drag and drop file
20
+ var droparea = document.getElementById('spud_admin_photos_selected');
21
+ droparea.addEventListener('dragenter', self.stopDndPropagation, false);
22
+ droparea.addEventListener('dragexit', self.stopDndPropagation, false);
23
+ droparea.addEventListener('dragover', self.stopDndPropagation, false);
24
+ droparea.addEventListener('drop', self.droppedFile, false);
25
+ };
26
+
27
+ this.submittedPhotoAlbumForm = function(){
28
+ var ids = $('#spud_admin_photos_selected .spud_admin_photo_ui_thumb').map(function(i, el){ return $(el).attr('rel') } );
29
+ $('#spud_photo_album_order').val(ids.toArray().join());
30
+ };
31
+
32
+ this.clickedPhotoRemoveFromLibrary = function(e){
33
+ $(this).parents('.spud_admin_photo_ui_thumb').fadeOut(200, function(){
34
+ $(this).remove();
26
35
  });
27
- }
36
+ };
28
37
 
29
- this.massDestroySelected = function(e){
30
- e.preventDefault();
31
- var ids = $.map($('.spud_admin_photo_ui_thumb_selected'), function(val, i){
32
- return $(val).find('input[type=checkbox]').val()
38
+ /* Handle file uploads passed via iframe (legacy support)
39
+ * -------------------------------------------------------- */
40
+
41
+ this.photoLegacyUploadErrors = function(html){
42
+ $('#spud_admin_photo_form').replaceWith(html);
43
+ };
44
+
45
+ this.photoLegacyUploadComplete = function(id, html){
46
+ var element = $('#spud_admin_photo_' + id);
47
+ if(element.length > 0){
48
+ element.replaceWith(htmlhtml);
49
+ }
50
+ else{
51
+ var target = $('#spud_admin_photos_selected .spud_admin_photo_ui_thumbs, #spud_admin_photos');
52
+ target.prepend(html).fadeIn(200);
53
+ }
54
+ $('#dialog').dialog('close');
55
+ };
56
+
57
+ this.selectedPhotoUiThumb = function(e){
58
+ var thumb = $(this);
59
+ if(thumb.hasClass('spud_admin_photo_ui_thumb_selected')){
60
+ $(this).removeClass('spud_admin_photo_ui_thumb_selected');
61
+ }
62
+ else{
63
+ $(this).addClass('spud_admin_photo_ui_thumb_selected');
64
+ }
65
+ };
66
+
67
+ this.markPhotoAsDeleted = function(photo_id){
68
+ var photo = $('#spud_admin_photo_' + photo_id);
69
+ photo.fadeOut(200, function(){
70
+ photo.remove();
71
+ });
72
+ };
73
+
74
+ this.markPhotoAlbumAsDeleted = function(photo_album_id){
75
+ var photo_album = $('#spud_admin_photo_album_' + photo_album_id);
76
+ photo_album.fadeOut(200, function(){
77
+ photo_album.remove();
78
+ });
79
+ };
80
+
81
+ this.markPhotoGalleryAsDeleted = function(photo_gallery_id){
82
+ var photo_gallery = $('#spud_admin_photo_gallery_' + photo_gallery_id);
83
+ photo_gallery.fadeOut(200, function(){
84
+ photo_gallery.remove();
33
85
  });
34
- $.ajax({
35
- type: 'POST',
36
- url: $(this).attr('href'),
37
- data: {spud_photo_ids:ids},
38
- success: function(data, textStatus, jqXHR){
39
- $('.spud_admin_photo_ui_thumb_selected').fadeOut(200, function(){
40
- $(this).remove();
41
- });
42
- },
43
- error: function(jqXHR, textStatus, errorThrown){
44
- console.log('An error occurred:')
45
- console.log(arguments);
46
- }
47
- })
48
86
  };
49
87
 
88
+ /*
89
+ * Single-Photo Form Upload
90
+ -------------------------------- */
91
+
50
92
  this.submittedPhotoForm = function(e){
51
93
  if(FormData && XMLHttpRequest){
52
94
  // create a FormData object and attach form values
53
95
  var fd = new FormData();
54
96
  var form = $(this);
97
+ var file = form.find('#spud_photo_photo')[0].files[0];
55
98
  fd.append('_method', form.find('[name=_method]').val());
56
99
  fd.append('authenticity_token', form.find('[name=authenticity_token]').val());
57
- fd.append('spud_photo[photo]', form.find('#spud_photo_photo')[0].files[0]);
100
+ fd.append('spud_photo[photo]', file);
58
101
  fd.append('spud_photo[title]', form.find('#spud_photo_title').val());
59
102
  fd.append('spud_photo[caption]', form.find('#spud_photo_caption').val());
60
103
 
104
+ // progress bar to send events to
105
+ var progressBar = self.progressBarForUpload(file.fileName);
106
+ form.find('.form-actions').before(progressBar);
107
+
61
108
  // send FormData object as ajax request
62
109
  var xhr = new XMLHttpRequest();
63
- xhr.upload.addEventListener('progress', self.photoUploadProgress, false);
64
- xhr.addEventListener('load', self.photoUploadComplete, false);
65
- xhr.addEventListener('error', self.photoUploadFailed, false);
66
- xhr.addEventListener('abort', self.photoUploadCanceled, false);
110
+ xhr.upload.addEventListener('progress', function(e){ self.onPhotoUploadProgress(e, progressBar) }, false);
111
+ xhr.addEventListener('load', function(e){ self.onPhotoUploadComplete(e, progressBar); }, false);
112
+ xhr.addEventListener('error', function(e){ self.onPhotoUploadFailure(e, progressBar); }, false);
113
+ xhr.addEventListener('abort', function(e){ self.onPhotoUploadCancel(e, progressBar); }, false);
67
114
  xhr.open('POST', form.attr('action'));
68
115
  xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
69
116
  xhr.send(fd);
@@ -71,23 +118,44 @@ Spud.Admin.Photos = new function(){
71
118
  }
72
119
  };
73
120
 
74
- this.photoUploadProgress = function(e){
121
+ /*
122
+ * Upload Progress Monitoring
123
+ -------------------------------- */
124
+
125
+ this.progressBarForUpload = function(fileName){
126
+ return $('\
127
+ <div class="spud_admin_photo_progress" \
128
+ <h6> \
129
+ <span class="spud_admin_photo_progress_filename">'+fileName+'</span>: \
130
+ <span class="spud_admin_photo_progress_status">Uploading</span> \
131
+ </h6> \
132
+ <div class="progress progress-striped active"> \
133
+ <div class="bar" style="width: 0;"></div> \
134
+ </div> \
135
+ </div>');
136
+ };
137
+
138
+ this.onPhotoUploadProgress = function(e, progressBar){
75
139
  var percent = Math.round(e.loaded * 100 / e.total);
76
- console.log('progress: ' + percent + '%');
77
- $('.progress').show();
78
- $('.progress .bar').css({width: percent + '%'});
140
+ progressBar.find('.bar').css({width: percent + '%'});
141
+ if(percent == 100){
142
+ progressBar.find('.progress').addClass('progress-success');
143
+ progressBar.find('.spud_admin_photo_progress_status').text('Processing');
144
+ }
79
145
  };
80
146
 
81
- this.photoUploadComplete = function(e){
147
+ this.onPhotoUploadComplete = function(e, progressBar){
82
148
  // success
83
149
  var photo = $.parseJSON(e.target.response);
84
150
  if(e.target.status == 200){
151
+ progressBar.find('.spud_admin_photo_progress_status').text('Done!');
152
+ progressBar.find('.progress').removeClass('progress-striped active');
85
153
  var element = $('#spud_admin_photo_' + photo.id);
86
154
  if(element.length > 0){
87
155
  element.replaceWith(photo.html);
88
156
  }
89
157
  else{
90
- var target = $('#spud_admin_photos_selected .spud_admin_photo_ui_thumbs, #spud_admin_photos');
158
+ var target = $('#spud_admin_photos_selected, #spud_admin_photos');
91
159
  target.prepend(photo.html).fadeIn(200);
92
160
  }
93
161
  $('#dialog').dialog('close');
@@ -98,61 +166,150 @@ Spud.Admin.Photos = new function(){
98
166
  }
99
167
  };
100
168
 
101
- this.photoUploadFailed = function(e){
102
- console.log('fail!');
103
- console.log(e);
104
- }
169
+ this.onPhotoUploadCancel = function(e, progressBar){
105
170
 
106
- this.photoUploadCanceled = function(e){
107
- console.log('cancel');
108
- };
171
+ };
109
172
 
110
- // need to invert the checkbox state so that it gets properly checked/uncheckd when `selectedPhotoUiThumb` fires
111
- this.invertPhotoUiThumbCheckbox = function(e){
112
- $(this).attr('checked', !$(this).attr('checked'));
173
+ this.onPhotoUploadCancel = function(e, progressBar){
174
+ progressBar.find('.spud_admin_photo_progress_status').text('Done!');
175
+ progressBar.find('.progress').addClass('progress-danger');
113
176
  };
114
177
 
115
- this.selectedPhotoUiThumb = function(e){
116
- var checkbox = $(this).find('input[type=checkbox]');
117
- if(checkbox){
118
- if(checkbox.attr('checked')){
119
- $(this).removeClass('spud_admin_photo_ui_thumb_selected');
120
- checkbox.attr('checked', false);
121
- }
122
- else{
123
- $(this).addClass('spud_admin_photo_ui_thumb_selected');
124
- checkbox.attr('checked', true);
125
- }
126
- }
178
+ /*
179
+ * Add From Photo Library
180
+ ------------------------------- */
181
+
182
+ this.clickedPhotoLibrary = function(e){
183
+ var url = this.href;
184
+ $.ajax({
185
+ url:url,
186
+ success:self.photoLibraryLoaded
187
+ });
188
+ return false;
127
189
  };
128
190
 
129
- this.markSelectedPhotoUiThumbs = function(){
130
- $('.spud_admin_photo_ui_thumb_selectable').each(function(){
131
- var checkbox = $(this).find('input[type=checkbox]');
132
- if(checkbox && checkbox.attr('checked')){
133
- $(this).addClass('spud_admin_photo_ui_thumb_selected');
191
+ this.photoLibraryLoaded = function(html){
192
+ var dialog = $("#dialog");
193
+ if(dialog.length == 0){
194
+ dialog = $('<div id="dialog" style="display:hidden;"></div>').appendTo('body');
195
+ }
196
+ dialog.html(html);
197
+ $('#spud_admin_photos_selected .spud_admin_photo_ui_thumb').each(function(){
198
+ var id = $(this).attr('id');
199
+ var dupe = dialog.find('#'+id);
200
+ if(dupe){
201
+ dupe.remove();
202
+ }
203
+ });
204
+ dialog.dialog({
205
+ width: 660,
206
+ modal: true,
207
+ height: 450,
208
+ title: 'My Photo Library',
209
+ buttons: {
210
+ 'Add Selected': self.addSelectedPhotosFromLibrary,
211
+ 'Delete Selected': self.deleteSelectedPhotosFromLibrary
134
212
  }
135
213
  });
136
214
  };
137
215
 
138
- this.markPhotoAsDeleted = function(photo_id){
139
- var photo = $('#spud_admin_photo_' + photo_id);
140
- photo.fadeOut(200, function(){
141
- photo.remove();
142
- });
216
+ this.addSelectedPhotosFromLibrary = function(e){
217
+ $('#spud_admin_photo_library .spud_admin_photo_ui_thumb_selected')
218
+ .removeClass('spud_admin_photo_ui_thumb_selected')
219
+ .prependTo('#spud_admin_photos_selected')
220
+ .hide()
221
+ .fadeIn(200);
222
+ $(this).dialog('close');
143
223
  };
144
224
 
145
- this.markPhotoAlbumAsDeleted = function(photo_album_id){
146
- var photo_album = $('#spud_admin_photo_album_' + photo_album_id);
147
- photo_album.fadeOut(200, function(){
148
- photo_album.remove();
225
+ this.deleteSelectedPhotosFromLibrary = function(e){
226
+ var ids = $.map($('.spud_admin_photo_ui_thumb_selected'), function(val, i){
227
+ return $(val).attr('rel');
149
228
  });
229
+ $.ajax({
230
+ type: 'POST',
231
+ url: '/spud/admin/photos/mass_destroy',
232
+ data: {spud_photo_ids:ids},
233
+ success: function(data, textStatus, jqXHR){
234
+ $('.spud_admin_photo_ui_thumb_selected').fadeOut(200, function(){
235
+ $(this).remove();
236
+ });
237
+ },
238
+ error: function(jqXHR, textStatus, errorThrown){
239
+ console.log('An error occurred:')
240
+ console.log(arguments);
241
+ }
242
+ })
243
+
150
244
  };
151
245
 
152
- this.markPhotoGalleryAsDeleted = function(photo_gallery_id){
153
- var photo_gallery = $('#spud_admin_photo_gallery_' + photo_gallery_id);
154
- photo_gallery.fadeOut(200, function(){
155
- photo_gallery.remove();
156
- });
246
+ /*
247
+ * Drag & Drop File Upload Queue
248
+ -------------------------------- */
249
+
250
+ this.fileQueue = [];
251
+ this.fileQueueStarted = false;
252
+
253
+ // prevent default browser behavior of opening the dropped file
254
+ this.stopDndPropagation = function(e){
255
+ e.stopPropagation();
256
+ e.preventDefault();
257
+ }
258
+
259
+ // add files to queue. starts queue if not started already
260
+ this.droppedFile = function(e){
261
+ e.stopPropagation();
262
+ e.preventDefault();
263
+ $('#spud_admin_photo_upload_queue').show();
264
+ var files = e.dataTransfer.files;
265
+ var i = 0;
266
+ while(i < files.length){
267
+ self.fileQueue.push(files[i]);
268
+ i++;
269
+ }
270
+ self.updateQueueCountLabel();
271
+ if(!this.fileQueueStarted){
272
+ self.uploadNextPhoto();
273
+ if(self.fileQueue.length > 0){
274
+ self.uploadNextPhoto();
275
+ }
276
+ }
277
+ };
278
+
279
+ this.updateQueueCountLabel = function(){
280
+ $('#spud_admin_photo_upload_queue_label span').text(self.fileQueue.length);
281
+ };
282
+
283
+ this.uploadNextPhoto = function(){
284
+ if(self.fileQueue.length == 0){
285
+ self.fileQueueStarted = false;
286
+ return;
287
+ }
288
+
289
+ // formdata object
290
+ self.fileQueueStarted = true;
291
+ var file = self.fileQueue.pop();
292
+ var fd = new FormData();
293
+ fd.append('spud_photo[photo]', file);
294
+
295
+ // create a progress bar
296
+ var progressBar = self.progressBarForUpload(file.fileName);
297
+ $('#spud_admin_photo_upload_queue_bars').prepend(progressBar);
298
+
299
+ // send formdata as xhr
300
+ var xhr = new XMLHttpRequest();
301
+ xhr.upload.addEventListener('progress', function(e){ self.onPhotoUploadProgress(e, progressBar); }, false);
302
+ xhr.addEventListener('load', function(e){ self.onQueuedPhotoUploadComplete(e, progressBar) }, false);
303
+ xhr.addEventListener('error', function(e){ self.onPhotoUploadFailure(e, progressBar); }, false);
304
+ xhr.addEventListener('abort', function(e){ self.onPhotoUploadCancel(e, progressBar); }, false);
305
+ xhr.open('POST', '/spud/admin/photos');
306
+ xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
307
+ xhr.send(fd);
308
+ };
309
+
310
+ this.onQueuedPhotoUploadComplete = function(e, progressBar){
311
+ self.onPhotoUploadComplete(e, progressBar);
312
+ self.updateQueueCountLabel();
313
+ self.uploadNextPhoto();
157
314
  };
158
315
  };
@@ -12,7 +12,7 @@
12
12
  margin: 15px 0;
13
13
  }
14
14
  .spud_admin_photo_ui_thumbs{
15
- height: 350px;
15
+ height: 300px;
16
16
  padding: 0;
17
17
  border: 1px solid #cacaca;
18
18
  margin: 15px 0;
@@ -50,16 +50,19 @@
50
50
  .spud_admin_photo_ui_thumbs_selectable .spud_admin_photo_ui_thumb{
51
51
  cursor: pointer;
52
52
  }
53
+ .spud_admin_photo_ui_thumbs_selectable .spud_admin_photo_ui_thumb_controls{
54
+ display: none;
55
+ }
53
56
  .spud_admin_photo_ui_thumb_selected{
54
57
  border: 3px solid #006CCC;
55
- margin: -3px 30px 30px -3px;
58
+ margin: 13px;
59
+ }
60
+ .spud_admin_photo_ui_thumb_small.spud_admin_photo_ui_thumb_selected{
61
+ margin: 9px;
56
62
  }
57
63
  .spud_admin_photo_ui_thumbs_sortable .spud_admin_photo_ui_thumb{
58
64
  cursor: move;
59
65
  }
60
- .spud_admin_photo_ui_thumb_selected:nth-child(6n){
61
- margin-right: -3px;
62
- }
63
66
  .spud_admin_photo_ui_thumb h5{
64
67
  font-size: 12px;
65
68
  color: white;
@@ -86,7 +89,7 @@
86
89
  background: rgba(0,0,0,0.5);
87
90
  border-bottom-right-radius: 15px;
88
91
  }
89
- .spud_admin_photos_btn_delete{
92
+ .spud_admin_photos_btn_remove, .spud_admin_photos_btn_delete{
90
93
  background: url('/assets/spud/photos/buttons/x_16x16.png');
91
94
  height: 16px;
92
95
  width: 16px;
@@ -109,4 +112,28 @@
109
112
  position: absolute;
110
113
  top: 0;
111
114
  right: 0;
115
+ }
116
+
117
+ #spud_admin_photo_album_actions{
118
+
119
+ }
120
+
121
+ /* Upload Queue
122
+ ---------------------------- */
123
+ #spud_admin_photo_album_form{
124
+
125
+ }
126
+ .spud_admin_photo_progress{
127
+ margin: 10px 0;
128
+ }
129
+ #spud_admin_photo_upload_queue{
130
+ display: none;
131
+ background: white;
132
+ padding: 15px;
133
+ margin: 0 0 0 10px;
134
+ width: 250px;
135
+ height: 270px;
136
+ float: right;
137
+ border: 1px solid #cacaca;
138
+ overflow-y: scroll;
112
139
  }
@@ -6,10 +6,11 @@ class PhotoAlbumsController < ApplicationController
6
6
  def index
7
7
  if params[:photo_gallery_id]
8
8
  @photo_gallery = SpudPhotoGallery.find_by_url_name(params[:photo_gallery_id])
9
- if @photo_gallery
10
- @photo_albums = @photo_gallery.albums.order('created_at desc')
9
+ if @photo_gallery.blank?
10
+ flash[:error] = 'Photo gallery could not be found!'
11
+ redirect_to photo_galleries_path and return false
11
12
  else
12
- @photo_albums = []
13
+ @photo_albums = @photo_gallery.albums.order('created_at desc')
13
14
  end
14
15
  else
15
16
  @photo_albums = SpudPhotoAlbum.order('created_at desc')
@@ -19,7 +20,16 @@ class PhotoAlbumsController < ApplicationController
19
20
 
20
21
  def show
21
22
  @photo_album = SpudPhotoAlbum.find_by_url_name(params[:id])
22
- respond_with @photo_album
23
+ if @photo_album.blank?
24
+ flash[:error] = "Post not found!"
25
+ if params[:photo_gallery_id]
26
+ redirect_to photo_gallery_photo_albums_path(params[:photo_gallery_id])
27
+ else
28
+ redirect_to photo_albums_path
29
+ end
30
+ else
31
+ respond_with @photo_album
32
+ end
23
33
  end
24
34
 
25
35
  end
@@ -1,6 +1,6 @@
1
1
  class Spud::Admin::PhotoAlbumsController < Spud::Admin::ApplicationController
2
2
 
3
- before_filter :get_album, :only => [:show, :edit, :update, :destroy]
3
+ before_filter :get_album, :only => [:show, :edit, :update, :destroy, :library]
4
4
  respond_to :html, :json, :xml
5
5
  layout 'spud/admin/spud_photos'
6
6
 
@@ -20,7 +20,10 @@ class Spud::Admin::PhotoAlbumsController < Spud::Admin::ApplicationController
20
20
 
21
21
  def create
22
22
  @photo_album = SpudPhotoAlbum.new(params[:spud_photo_album])
23
- flash[:notice] = 'SpudPhotoAlbum created successfully' if @photo_album.save
23
+ if @photo_album.save
24
+ set_photo_order
25
+ @photo_album.spud_photo_albums_photos = photo_albums_photos
26
+ end
24
27
  respond_with @photo_album, :location => spud_admin_photo_albums_path
25
28
  end
26
29
 
@@ -30,7 +33,10 @@ class Spud::Admin::PhotoAlbumsController < Spud::Admin::ApplicationController
30
33
 
31
34
  def update
32
35
  @photo_album.update_attributes(params[:spud_photo_album])
33
- flash[:notice] = 'SpudPhotoAlbum updated successfully' if @photo_album.save
36
+ if @photo_album.save
37
+ set_photo_order
38
+ flash[:notice] = 'SpudPhotoAlbum updated successfully'
39
+ end
34
40
  respond_with @photo_album, :location => spud_admin_photo_albums_path
35
41
  end
36
42
 
@@ -43,4 +49,15 @@ class Spud::Admin::PhotoAlbumsController < Spud::Admin::ApplicationController
43
49
  @photo_album = SpudPhotoAlbum.find(params[:id])
44
50
  end
45
51
 
52
+ private
53
+
54
+ def set_photo_order
55
+ order_ids = params[:spud_photo_album_order].split(',')
56
+ @photo_album.spud_photo_albums_photos.each do |obj|
57
+ logger.debug "##### ID: #{obj.spud_photo_id.to_s}"
58
+ index = order_ids.index(obj.spud_photo_id.to_s)
59
+ obj.update_attribute(:order, index)
60
+ end
61
+ end
62
+
46
63
  end
@@ -1,11 +1,10 @@
1
1
  class Spud::Admin::PhotosController < Spud::Admin::ApplicationController
2
2
 
3
+ include RespondsToParent
4
+
3
5
  before_filter :get_photo, :only => [:show, :edit, :update, :destroy]
4
6
  respond_to :html, :json, :xml, :js
5
- layout 'spud/admin/spud_photos'
6
-
7
- add_breadcrumb 'Photos', :spud_admin_photos_path
8
- belongs_to_spud_app :photos
7
+ layout false
9
8
 
10
9
  def index
11
10
  @photos = SpudPhoto.all
@@ -32,7 +31,9 @@ class Spud::Admin::PhotosController < Spud::Admin::ApplicationController
32
31
  if request.xhr?
33
32
  render json_for_photo(success)
34
33
  else
35
- respond_with @photo, :location => spud_admin_photos_path
34
+ respond_to_parent do
35
+ render 'show.js'
36
+ end
36
37
  end
37
38
  end
38
39
 
@@ -51,10 +52,12 @@ class Spud::Admin::PhotosController < Spud::Admin::ApplicationController
51
52
  if request.xhr?
52
53
  render json_for_photo(success)
53
54
  else
54
- respond_with @photo, :location => spud_admin_photos_path
55
+ respond_to_parent do
56
+ render 'show.js'
57
+ end
55
58
  end
56
59
  end
57
-
60
+
58
61
  def destroy
59
62
  flash[:notice] = 'SpudPhoto deleted successfully' if @photo.destroy
60
63
  respond_with @photo, :location => spud_admin_photos_path
@@ -1,2 +1,7 @@
1
1
  module Spud::Admin::PhotosHelper
2
- end
2
+
3
+ def photo_is_selected
4
+ return (@photo_album && @photo_album.photo_ids.include?(photo.id))
5
+ end
6
+
7
+ end
@@ -1,7 +1,11 @@
1
1
  class SpudPhoto < ActiveRecord::Base
2
- has_and_belongs_to_many :albums,
3
- :class_name => 'SpudPhotoAlbum',
4
- :join_table => 'spud_photo_albums_photos'
2
+
3
+ attr_accessible :title, :caption, :photo
4
+
5
+ has_many :spud_photo_albums_photos
6
+ has_many :albums,
7
+ :through => :spud_photo_albums_photos,
8
+ :source => :spud_photo_album
5
9
 
6
10
  has_attached_file :photo,
7
11
  :styles => lambda { |attachment| attachment.instance.dynamic_styles },
@@ -1,15 +1,22 @@
1
1
  class SpudPhotoAlbum < ActiveRecord::Base
2
- has_and_belongs_to_many :galleries,
3
- :class_name => 'SpudPhotoGallery',
4
- :join_table => 'spud_photo_galleries_albums'
5
- has_and_belongs_to_many :photos,
6
- :class_name => 'SpudPhoto',
7
- :join_table => 'spud_photo_albums_photos',
8
- :order => 'created_at'
2
+
3
+ attr_accessible :title, :url_name, :photos, :photo_ids
4
+
5
+ has_many :spud_photo_albums_photos
6
+ has_many :photos,
7
+ :through => :spud_photo_albums_photos,
8
+ :source => :spud_photo,
9
+ :order => 'spud_photo_albums_photos.order asc'
10
+
11
+ has_many :spud_photo_galleries_albums
12
+ has_many :galleries,
13
+ :through => :spud_photo_galleries_albums,
14
+ :source => :spud_photo_gallery
15
+
9
16
  validates_presence_of :title, :url_name
10
17
  validates_uniqueness_of :title, :url_name
11
18
  before_validation :set_url_name
12
-
19
+ after_save :update_photo_order
13
20
 
14
21
  def top_photo_url(style)
15
22
  unless photos.empty?
@@ -31,4 +38,12 @@ class SpudPhotoAlbum < ActiveRecord::Base
31
38
  self.url_name = self.title.parameterize
32
39
  end
33
40
 
41
+ def update_photo_order
42
+ # order = 0
43
+ # self.photos.each do |p|
44
+ # p.update_attribute(:order, order)
45
+ # order += 1
46
+ # end
47
+ end
48
+
34
49
  end
@@ -0,0 +1,5 @@
1
+ class SpudPhotoAlbumsPhoto < ActiveRecord::Base
2
+ attr_accessible :spud_photo_id, :spud_photo_album_id, :order
3
+ belongs_to :spud_photo
4
+ belongs_to :spud_photo_album
5
+ end
@@ -0,0 +1,5 @@
1
+ class SpudPhotoGalleriesAlbum < ActiveRecord::Base
2
+ attr_accessible :spud_photo_album_id, :spud_photo_gallery_id, :order
3
+ belongs_to :spud_photo_album
4
+ belongs_to :spud_photo_gallery
5
+ end
@@ -1,7 +1,12 @@
1
1
  class SpudPhotoGallery < ActiveRecord::Base
2
- has_and_belongs_to_many :albums,
3
- :class_name => 'SpudPhotoAlbum',
4
- :join_table => 'spud_photo_galleries_albums'
2
+
3
+ attr_accessible :title, :url_name, :albums, :album_ids
4
+
5
+ has_many :spud_photo_galleries_albums
6
+ has_many :albums,
7
+ :through => :spud_photo_galleries_albums,
8
+ :source => :spud_photo_album
9
+
5
10
  validates_presence_of :title, :url_name
6
11
  validates_uniqueness_of :title, :url_name
7
12
  before_validation :set_url_name
@@ -23,7 +28,9 @@ class SpudPhotoGallery < ActiveRecord::Base
23
28
  private
24
29
 
25
30
  def set_url_name
26
- self.url_name = self.title.parameterize
31
+ if self.title
32
+ self.url_name = self.title.parameterize
33
+ end
27
34
  end
28
35
 
29
36
  end
@@ -2,30 +2,30 @@
2
2
 
3
3
  <%= error_messages_for(f.object) %>
4
4
 
5
+ <%= hidden_field_tag :spud_photo_album_order %>
6
+
5
7
  <fieldset>
6
8
  <legend>Photo Album Info</legend>
7
- <div class="control-group">
8
- <%= f.label :title, :class => "control-label" %>
9
- <div class="controls">
10
- <%= f.text_field :title %>
11
- </div>
9
+ <div class="control-group">
10
+ <%= f.label :title, :class => "control-label" %>
11
+ <div class="controls">
12
+ <%= f.text_field :title %>
12
13
  </div>
14
+ </div>
13
15
  </fieldset>
14
16
 
15
17
  <fieldset class="spud_admin_photos_album_fieldset">
16
- <%= link_to "New Photo", new_spud_admin_photo_path, :class => "spud_admin_photo_create ajax btn btn-primary", :title => "New Photo" %>
17
- <legend>Select Photos</legend>
18
- <div id="spud_admin_photos_selected" class="spud_admin_photos_selection_left">
19
- <h4>Selected:</h4>
20
- <div class="spud_admin_photo_ui_thumbs spud_admin_photo_ui_thumbs_sortable">
21
- <%= render :partial => '/spud/admin/photos/photo', :collection => @photo_album.photos %>
22
- </div>
18
+ <legend>Selected Photos</legend>
19
+ <div id="spud_admin_photo_album_actions" class="control-group">
20
+ <%= link_to "Photo Library", spud_admin_photos_path, :class => "btn btn-primary", :id => 'spud_admin_photo_album_action_library' %>
21
+ <%= link_to "Upload Photo", new_spud_admin_photo_path, :class => "ajax btn btn-primary", :title => "New Photo" %>
23
22
  </div>
24
- <div id="spud_admin_photos_available" class="spud_admin_photos_selection_right">
25
- <h4>Available:</h4>
26
- <div class="spud_admin_photo_ui_thumbs spud_admin_photo_ui_thumbs_sortable">
27
- <%= render :partial => '/spud/admin/photos/photo', :collection => @photo_album.photos_available %>
28
- </div>
23
+ <div id="spud_admin_photo_upload_queue">
24
+ <h5 id="spud_admin_photo_upload_queue_label">Queued Uploads: <span>0</span></h5>
25
+ <div id="spud_admin_photo_upload_queue_bars"></div>
26
+ </div>
27
+ <div id="spud_admin_photos_selected" class="control-group spud_admin_photo_ui_thumbs spud_admin_photo_ui_thumbs_sortable">
28
+ <%= render :partial => '/spud/admin/photos/photo', :collection => @photo_album.photos %>
29
29
  </div>
30
30
  </fieldset>
31
31
 
@@ -1,4 +1,4 @@
1
- <%= form_for @photo, :url => (@photo.new_record? ? spud_admin_photos_path : spud_admin_photo_path(@photo)), :html => {:id => 'spud_admin_photo_form', :class => 'form-horizontal'} do |f| %>
1
+ <%= form_for @photo, :url => (@photo.new_record? ? spud_admin_photos_path : spud_admin_photo_path(@photo)), :html => {:id => 'spud_admin_photo_form', :target => 'spud_admin_photo_form_target', :class => 'form-horizontal'} do |f| %>
2
2
 
3
3
  <%=error_messages_for(f.object)%>
4
4
 
@@ -36,13 +36,11 @@
36
36
  <% end %>
37
37
  </fieldset>
38
38
 
39
- <div class="progress progress-striped active" style="display:none;">
40
- <div class="bar" style="width: 0;"></div>
41
- </div>
42
-
43
39
  <div class="form-actions">
44
40
  <%= f.submit "Save Photo", :class=>"btn btn-primary form-btn", "data-loading-text" => "Saving..." %>
45
41
  <!--or <%=link_to "cancel", request.referer, :class => "btn" %> -->
46
42
  </div>
47
43
 
44
+ <iframe id="spud_admin_photo_form_target" name="spud_admin_photo_form_target" style="display:none;"></iframe>
45
+
48
46
  <% end %>
@@ -1,9 +1,13 @@
1
- <div id="spud_admin_photo_<%= photo.id %>" class="spud_admin_photo_ui_thumb spud_admin_photo_ui_thumb_small" style="background-image:url('<%= photo.photo.url(:spud_admin_small) %>')">
1
+ <%= content_tag :div,
2
+ :rel => photo.id,
3
+ :id => "spud_admin_photo_#{photo.id}",
4
+ :style => "background-image:url('#{photo.photo.url(:spud_admin_small)}')",
5
+ :class => "spud_admin_photo_ui_thumb spud_admin_photo_ui_thumb_small" do %>
2
6
  <div style="display:none;">
3
- <%= check_box_tag 'spud_photo_album[photo_ids][]', photo.id, (@photo_album && @photo_album.photo_ids.include?(photo.id)) %>
7
+ <%= hidden_field_tag 'spud_photo_album[photo_ids][]', photo.id %>
4
8
  </div>
5
9
  <div class="spud_admin_photo_ui_thumb_controls">
6
10
  <%= link_to 'Edit', edit_spud_admin_photo_path(photo), :class => 'ajax spud_admin_photos_btn_edit' %>
7
- <%= link_to 'Delete', spud_admin_photo_path(photo), :method => :delete, :remote => true, :class => 'spud_admin_photos_btn_delete', :confirm => 'Are you sure? This will permanently delete the photo from all albums it currently belongs to.' %>
11
+ <%= link_to 'Delete', '#', :class => 'spud_admin_photos_btn_remove' %>
8
12
  </div>
9
- </div>
13
+ <% end %>
@@ -1,12 +1,3 @@
1
- <%= content_for :data_controls do %>
2
- <!--
3
- <%= link_to 'Delete Selected', mass_destroy_spud_admin_photos_path, :class => 'spud_admin_photo_mass_destroy btn btn-danger', :confirm => 'Are you sure?' %>
4
- -->
5
- <%= link_to "New Photo", new_spud_admin_photo_path, :class => "spud_admin_photo_create ajax btn btn-primary", :title => "New Photo" %>
6
- <% end %>
7
-
8
- <%= content_for :detail do %>
9
- <div id="spud_admin_photos" class="spud_admin_photo_ui_thumbs">
10
- <%= render :partial => 'photo', :collection => @photos %>
11
- <div>
12
- <% end %>
1
+ <div id="spud_admin_photo_library" class="spud_admin_photo_ui_thumbs spud_admin_photo_ui_thumbs_selectable">
2
+ <%= render :partial => 'photo', :collection => @photos %>
3
+ <div>
@@ -0,0 +1,5 @@
1
+ <% if @photo.errors.any? %>
2
+ Spud.Admin.Photos.photoLegacyUploadErrors("<%= escape_javascript(render 'form') %>");
3
+ <% else %>
4
+ Spud.Admin.Photos.photoLegacyUploadComplete(<%= @photo.id %>, "<%= escape_javascript(render :partial => 'photo', :locals => {:photo => @photo}) %>");
5
+ <% end %>
data/config/routes.rb CHANGED
@@ -3,6 +3,7 @@ Rails.application.routes.draw do
3
3
  namespace :spud do
4
4
  namespace :admin do
5
5
  resources :photos do
6
+ get 'library', :on => :collection
6
7
  post 'mass_destroy', :on => :collection
7
8
  end
8
9
  resources :photo_albums
@@ -0,0 +1,8 @@
1
+ class UpgradePhotoRelationships < ActiveRecord::Migration
2
+ def change
3
+ add_column :spud_photo_albums_photos, :id, :primary_key
4
+ add_column :spud_photo_albums_photos, :order, :integer, :default => 0
5
+ add_column :spud_photo_galleries_albums, :id, :primary_key
6
+ add_column :spud_photo_galleries_albums, :order, :integer, :default => 0
7
+ end
8
+ end
@@ -0,0 +1,69 @@
1
+ # Copyright (c) 2006 Sean Treadway
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+
23
+ # Module containing the methods useful for child IFRAME to parent window communication
24
+ module RespondsToParent
25
+
26
+ # Executes the response body as JavaScript in the context of the parent window.
27
+ # Use this method of you are posting a form to a hidden IFRAME or if you would like
28
+ # to use IFRAME base RPC.
29
+ def responds_to_parent(&block)
30
+ yield
31
+
32
+ if performed?
33
+ # We're returning HTML instead of JS or XML now
34
+ response.headers['Content-Type'] = 'text/html; charset=UTF-8'
35
+
36
+ # Either pull out a redirect or the request body
37
+ script = if response.headers['Location']
38
+ #TODO: erase_redirect_results is missing in rails 3.0 has to be implemented
39
+ # erase redirect
40
+ "document.location.href = #{location.to_s.inspect}"
41
+ else
42
+ response.body
43
+ end
44
+
45
+ # Escape quotes, linebreaks and slashes, maintaining previously escaped slashes
46
+ # Suggestions for improvement?
47
+ script = (script || '').
48
+ gsub('\\', '\\\\\\').
49
+ gsub(/\r\n|\r|\n/, '\\n').
50
+ gsub(/['"]/, '\\\\\&').
51
+ gsub('</script>','</scr"+"ipt>')
52
+
53
+ # Clear out the previous render to prevent double render
54
+ response.request.env['action_controller.instance'].instance_variable_set(:@_response_body, nil)
55
+
56
+ # Eval in parent scope and replace document location of this frame
57
+ # so back button doesn't replay action on targeted forms
58
+ # loc = document.location to be set after parent is updated for IE
59
+ # with(window.parent) - pull in variables from parent window
60
+ # setTimeout - scope the execution in the windows parent for safari
61
+ # window.eval - legal eval for Opera
62
+ render :text => "<html><body><script type='text/javascript' charset='utf-8'>
63
+ var loc = document.location;
64
+ with(window.parent) { setTimeout(function() { window.eval('#{script}'); if (typeof(loc) !== 'undefined') loc.replace('about:blank'); }, 1) };
65
+ </script></body></html>".html_safe
66
+ end
67
+ end
68
+ alias respond_to_parent responds_to_parent
69
+ end
data/lib/spud_photos.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  module Spud
2
2
  module Photos
3
+ require 'responds_to_parent.rb'
3
4
  require 'spud_photos/configuration'
4
5
  require 'spud_photos/engine' if defined?(Rails)
5
6
  end
@@ -15,12 +15,12 @@ module Spud
15
15
  :url => '/spud/admin/photo_albums',
16
16
  :retina => true,
17
17
  :order => 82
18
- },{
19
- :name => 'Photos',
20
- :thumbnail => 'spud/photos/photo_albums_thumb.png',
21
- :url => '/spud/admin/photos',
22
- :retina => true,
23
- :order => 83
18
+ # },{
19
+ # :name => 'Photos',
20
+ # :thumbnail => 'spud/photos/photo_albums_thumb.png',
21
+ # :url => '/spud/admin/photos',
22
+ # :retina => true,
23
+ # :order => 83
24
24
  }]
25
25
  if Spud::Photos.config.galleries_enabled
26
26
  Spud::Core.config.admin_applications += [{
@@ -1,5 +1,5 @@
1
1
  module Spud
2
2
  module Photos
3
- VERSION = "0.0.2"
3
+ VERSION = "0.0.3"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spud_photos
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
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: 2012-03-31 00:00:00.000000000 Z
12
+ date: 2012-04-05 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
16
- requirement: &70105838190880 !ruby/object:Gem::Requirement
16
+ requirement: &70189045047420 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 3.2.2
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70105838190880
24
+ version_requirements: *70189045047420
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: spud_core
27
- requirement: &70105838188820 !ruby/object:Gem::Requirement
27
+ requirement: &70189045046840 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -35,10 +35,10 @@ dependencies:
35
35
  version: 0.9.0
36
36
  type: :runtime
37
37
  prerelease: false
38
- version_requirements: *70105838188820
38
+ version_requirements: *70189045046840
39
39
  - !ruby/object:Gem::Dependency
40
40
  name: paperclip
41
- requirement: &70105838186920 !ruby/object:Gem::Requirement
41
+ requirement: &70189045045960 !ruby/object:Gem::Requirement
42
42
  none: false
43
43
  requirements:
44
44
  - - ! '>='
@@ -46,10 +46,10 @@ dependencies:
46
46
  version: '0'
47
47
  type: :runtime
48
48
  prerelease: false
49
- version_requirements: *70105838186920
49
+ version_requirements: *70189045045960
50
50
  - !ruby/object:Gem::Dependency
51
51
  name: mysql2
52
- requirement: &70105838200760 !ruby/object:Gem::Requirement
52
+ requirement: &70189045045160 !ruby/object:Gem::Requirement
53
53
  none: false
54
54
  requirements:
55
55
  - - ! '>='
@@ -57,7 +57,7 @@ dependencies:
57
57
  version: '0'
58
58
  type: :development
59
59
  prerelease: false
60
- version_requirements: *70105838200760
60
+ version_requirements: *70189045045160
61
61
  description: Spud Photos is a feature complete photo management/gallery for the spud
62
62
  engine. Manage multiple galleries, albums, and photos. Use HTML 5 to drag and drop
63
63
  many images at once.
@@ -89,6 +89,8 @@ files:
89
89
  - app/helpers/spud/admin/photos_helper.rb
90
90
  - app/models/spud_photo.rb
91
91
  - app/models/spud_photo_album.rb
92
+ - app/models/spud_photo_albums_photo.rb
93
+ - app/models/spud_photo_galleries_album.rb
92
94
  - app/models/spud_photo_gallery.rb
93
95
  - app/views/layouts/spud/admin/spud_photos.html.erb
94
96
  - app/views/photo_albums/index.html.erb
@@ -111,11 +113,14 @@ files:
111
113
  - app/views/spud/admin/photos/edit.html.erb
112
114
  - app/views/spud/admin/photos/index.html.erb
113
115
  - app/views/spud/admin/photos/new.html.erb
116
+ - app/views/spud/admin/photos/show.js.erb
114
117
  - config/routes.rb
115
118
  - db/migrate/20120228232120_create_spud_photos.rb
116
119
  - db/migrate/20120228232329_create_spud_photo_albums.rb
117
120
  - db/migrate/20120228232344_create_spud_photo_galleries.rb
121
+ - db/migrate/20120405042046_upgrade_photo_relationships.rb
118
122
  - lib/generators/spud/photos/views_generator.rb
123
+ - lib/responds_to_parent.rb
119
124
  - lib/spud_photos/configuration.rb
120
125
  - lib/spud_photos/engine.rb
121
126
  - lib/spud_photos/version.rb
@@ -166,7 +171,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
166
171
  version: '0'
167
172
  segments:
168
173
  - 0
169
- hash: 2069257905353995615
174
+ hash: 4384740055791320075
170
175
  required_rubygems_version: !ruby/object:Gem::Requirement
171
176
  none: false
172
177
  requirements:
@@ -175,10 +180,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
175
180
  version: '0'
176
181
  segments:
177
182
  - 0
178
- hash: 2069257905353995615
183
+ hash: 4384740055791320075
179
184
  requirements: []
180
185
  rubyforge_project:
181
- rubygems_version: 1.8.15
186
+ rubygems_version: 1.8.10
182
187
  signing_key:
183
188
  specification_version: 3
184
189
  summary: Spud Photos Engine