diff --git a/app/controllers/galleries_controller.rb b/app/controllers/galleries_controller.rb new file mode 100644 index 0000000..9d7a574 --- /dev/null +++ b/app/controllers/galleries_controller.rb @@ -0,0 +1,78 @@ +class GalleriesController < ApplicationController + append_before_filter :fetch_image, :only => [ :show, :destroy, + :download_original ] + + # GET /images + # GET /images.xml + def index + @content_title = 'Image Gallery' + cond_ary = [] + cond_var = { + :people_id => params[:id] + } + @secondary_title = "Everybody's Images" + if params[:id] + cond_ary << 'images.people_id = :people_id' + @people = People.find(params[:id]) + @secondary_title = "Images from #{@people.title}" + end + cond_ary << '1 = 1' if cond_ary.empty? + @pages, @images = paginate :images, :per_page => per_page, + :order => 'images.created_at DESC', :include => [ 'people' ], + :conditions => [ cond_ary.join(' AND '), cond_var ] + flash.now[:notice] = 'There are no images yet.' if @images.empty? + respond_to do |format| + format.html # index.rhtml + format.xml { render :xml => @images.to_xml } + end + end + + # GET /galleries/1 + # GET /galleries/1.xml + def show + respond_to do |format| + format.html # show.rhtml + format.xml { render :xml => @image.to_xml } + end + end + + # GET /galleries/new + def new + @image = Image.new + end + + # POST /images + # POST /images.xml + def create + @image = Image.new(params[:image]) + if @image.save + flash[:notice] = 'Great success!' + redirect_to gallery_url(@image) + else + render :action => :new + end + end + + # DELETE /galleries/1 + # DELETE /galleries/1.xml + def destroy + @image.destroy + flash[:notice] = 'Destroyed the image.' + redirect_to galleries_url(:id => @image.people_id) + end + + ## + # Sends a copy of the original Image to the People. + # + def download_original + send_file("#{RAILS_ROOT}/public/images/" + + @image.filename_for_version(:original), + :disposition => 'inline', :type => @image.content_type) + end + + protected + + def fetch_image + @image = Image.find(params[:id]) + end +end diff --git a/app/controllers/tag_images_controller.rb b/app/controllers/tag_images_controller.rb new file mode 100644 index 0000000..3e145fc --- /dev/null +++ b/app/controllers/tag_images_controller.rb @@ -0,0 +1,88 @@ +class TagImagesController < ApplicationController + # GET /tag_images + # GET /tag_images.xml + def index + redirect_to images_url + end + + # GET /tag_images/1 + # GET /tag_images/1.xml + def show + @content_title = 'Tag your friends and beers!' + @image = Image.find(params[:id], :include => [ :tag_images ]) + @tag_images = @image.tag_images + respond_to do |format| + format.html # show.rhtml + format.xml { render :xml => @tag_images.to_xml } + end + end + + # POST /tag_images + # POST /tag_images.xml + def create + @tag_image = TagImage.new(params[:tag_image]) + @image = @tag_image.image + if @tag_image.save + @tag_images = @image.tag_images + render :partial => 'tag_images' + else + render :partial => 'tag_image_errors', :status => 500 + end + end + + # DELETE /tag_images/1 + # DELETE /tag_images/1.xml + def destroy + @tag_image = TagImage.find params[:id], :include => [ :image ] + @image = @tag_image.image + @tag_image.destroy + @image.tag_images.reload + @tag_images = @image.tag_images + render :partial => 'tag_images' + end + + ## + # Searches for all known models that support image tagging. Sticks all of + # the matching results into a hash that is indexed by the type. + # + def taggable_search + @results = {} + cond_ary = [ 'title ILIKE :title' ] + cond_var = { :title => "%#{params[:name]}%" } + TagImage.types_for_select.flatten.each do |ctype| + klass = Class.class_eval(ctype) + @results[ctype] = klass.find :all, :order => 'title ASC', + :conditions => [ cond_ary.join(' AND '), cond_var ] + end + render :partial => 'taggable_results' + end + + ## + # Renders an Ajax browser of all tagged Image models for any +:taggable_type+ + # + def tagged_images + images_per_page = 4 + @page_count, @current_page, @tagged_type, @tagged_images = nil, nil, nil, nil + @tagged_type = params[:tagged_type] + if TagImage.types_for_select.flatten.include?(@tagged_type) + cond_ary = [ + 'tagged_type = :tt', + 'tagged_id = :tid' + ] + cond_var = { :tt => @tagged_type, :tid => params[:id] } + conditions = [ cond_ary.join(' AND '), cond_var ] + @current_page = params[:page].to_i + @current_page = 1 if @current_page == 0 + image_count = TagImage.count(conditions) + @page_count = (image_count.to_f / per_page.to_f + 0.5).to_i + @page_count = 1 if @page_count == 0 and image_count >= 0 + @tagged_images = TagImage.find :all, :limit => images_per_page, + :conditions => conditions, :order => 'created_at ASC', + :offset => ((@current_page - 1) * images_per_page), + :include => [ 'image' ] + render :partial => 'tag_images/tagged_images' + else + render :nothing => true, :status => 500 + end + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 5482a33..4a2a213 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -91,4 +91,48 @@ module ApplicationHelper res end end + + ## + # Captures a block output and renders it in a partial as body + # + def block_to_partial(partial_name, options = {}, &block) + options.merge!(:body => capture(&block)) + concat(render(:partial => partial_name, :locals => options), block.binding) + end + + ## + # Helper to build a prototype dialog. + # + def lightbox(options = {}, &block) + options = { + :title => 'DialogTitle', + :window_id => 'DialogId', + :modal => false + }.merge(options) + block_to_partial('shared/lightbox', options, &block) + end + + ## + # Pagination link image browser thingey for the tagged image lightbox. + # + def image_browser_navigation_link(image_name, page_number, total_pages, + tagged_class, tagged_id) + if page_number == 0 or + (page_number == 1 and total_pages == 1) or + (page_number > total_pages) + image_tag(image_name) + else + link_to_remote image_tag(image_name), :update => 'browser_box', + :url => { :controller => 'tag_images', :action => 'tagged_images', + :id => :tagged_id, :tagged_class => tagged_class } + end + end + + ## + # Link to open the dialog box for the tagged image browser. + # + def tagged_image_browser_link + link_to_function 'Tagged Images', + "lightboxes['tagged_image_browser'].open()" + end end diff --git a/app/helpers/galleries_helper.rb b/app/helpers/galleries_helper.rb new file mode 100644 index 0000000..23b68a6 --- /dev/null +++ b/app/helpers/galleries_helper.rb @@ -0,0 +1,5 @@ +module GalleriesHelper + def new_image_link + link_to 'Upload Image', new_gallery_url + end +end diff --git a/app/helpers/tag_images_helper.rb b/app/helpers/tag_images_helper.rb new file mode 100644 index 0000000..5c023de --- /dev/null +++ b/app/helpers/tag_images_helper.rb @@ -0,0 +1,2 @@ +module TagImagesHelper +end diff --git a/app/models/beer.rb b/app/models/beer.rb index 8a9dab5..8ae9f04 100644 --- a/app/models/beer.rb +++ b/app/models/beer.rb @@ -6,6 +6,7 @@ class Beer < ActiveRecord::Base has_one_tuxwiki_page :owner_class => 'Beer' belongs_to :style validates_presence_of :style_id + has_many_tagged_images ## # Returns a list of attributes for the Page partial. diff --git a/app/models/image.rb b/app/models/image.rb new file mode 100644 index 0000000..1d0fed2 --- /dev/null +++ b/app/models/image.rb @@ -0,0 +1,96 @@ +require 'mini_magick' + +class Image < ActiveRecord::Base + attr_accessor :file + belongs_to :people + validates_presence_of :people_id + before_validation_on_create :set_people_id + before_create :validate_image_sanity + after_create :setup_directories + before_destroy :destroy_directories + has_many :tag_images, :dependent => :destroy + has_many :tagged_items, :through => :tag_images + + ## + # Builds the filename for this model for a particular version of the file. + # + def filename_for_version(ver = :screen) + if respond_to?(ver) + "community/#{id}/#{self.send(ver)}" + else + "/images/image-missing.png" + end + end + + protected + + ## + # Determines the base directory for all files in this model. + # + def base_directory + "#{RAILS_ROOT}/public/images/community/#{id}" + end + + ## + # Sets the People marker for ownership on creation. + # + def set_people_id + self[:people_id] = ApplicationController.current_people_id rescue nil + self[:people_id] ||= People.penguincoder.id rescue nil + end + + ## + # Checks to make sure that the file exists and is an image. + # + def validate_image_sanity + if @file.nil? or @file.to_s.empty? + errors.add(:file, 'is not a file') + return false + end + errors.add(:file, 'is too big (3MB max)') if @file.size > 3 * 1048576 + begin + @magick_image = MiniMagick::Image.from_blob(@file.read, + File.extname(@file.original_filename)) + rescue + logger.debug("Caught an exception saving an image:") + logger.debug("* #{$!}") + errors.add(:file, 'is not an image') + end + return false if self.errors.size > 0 + self.content_type = @file.content_type.chomp + true + end + + ## + # Makes the directories and writes the different versions for the uploaded + # files if applicable. + # + def setup_directories + Dir.mkdir(base_directory) unless File.exist?(base_directory) + self.original = File.basename(@file.original_filename).gsub(/[^\w._-]/, '') + @magick_image.write("#{base_directory}/#{self.original}") + @magick_image.thumbnail("600x600>") + self.screen = "screen_#{self.original}" + @magick_image.write("#{base_directory}/#{self.screen}") + if @magick_image.output =~ / (\d+)x(\d+) / + self.screen_width = $1 + self.screen_height = $2 + end + @magick_image.thumbnail("50x50>") + self.thumbnail = "thumbnail_#{self.original}" + @magick_image.write("#{base_directory}/#{self.thumbnail}") + self.save + end + + ## + # Removes the directories and files associated with this model on destroy. + # + def destroy_directories + return unless File.exists?(base_directory) + Dir.foreach(base_directory) do |file| + next if file =~ /^\.\.?$/ + File.delete(base_directory + '/' + file) + end + Dir.delete(base_directory) + end +end diff --git a/app/models/people.rb b/app/models/people.rb index 6840ab2..4069f8f 100644 --- a/app/models/people.rb +++ b/app/models/people.rb @@ -7,6 +7,8 @@ class People < ActiveRecord::Base attr_protected :role_id has_many :created_pages, :class_name => 'Page', :foreign_key => 'created_by' has_many :updated_pages, :class_name => 'Page', :foreign_key => 'updated_by' + has_many :images, :dependent => :destroy + has_many_tagged_images validates_uniqueness_of :title make_authenticatable diff --git a/app/models/tag_image.rb b/app/models/tag_image.rb new file mode 100644 index 0000000..18a39fe --- /dev/null +++ b/app/models/tag_image.rb @@ -0,0 +1,10 @@ +class TagImage < ActiveRecord::Base + belongs_to :image + belongs_to :tagged, :polymorphic => true + validates_presence_of :image_id, :tagged_id, :tagged_type + validates_uniqueness_of :tagged_id, :scope => :tagged_type + + def self.types_for_select + [ 'Beer', 'People', 'Brewery' ].collect { |x| [x] } + end +end diff --git a/app/views/beers/show.rhtml b/app/views/beers/show.rhtml index ecd793d..669aabb 100644 --- a/app/views/beers/show.rhtml +++ b/app/views/beers/show.rhtml @@ -1,7 +1,10 @@ +<%= render :partial => 'shared/tagged_image_browser', :locals => { :obj => @beer } %> + <%= render :partial => 'pages/page' %> <% content_for :sidebar do -%> <%= new_beer_link -%>
<%= edit_beer_link(@beer) -%>
<%= link_to 'Destroy', beer_path(@beer.page.title_for_url), :confirm => 'Are you sure?', :method => :delete %>
+ <% unless @beer.tagged_images.empty? -%><%= tagged_image_browser_link -%>
<% end -%> <% end -%> diff --git a/app/views/breweries/show.rhtml b/app/views/breweries/show.rhtml index 872515a..e40d782 100644 --- a/app/views/breweries/show.rhtml +++ b/app/views/breweries/show.rhtml @@ -4,4 +4,5 @@ <%= new_brewery_link -%>
<%= edit_brewery_link(@brewery) -%>
<%= link_to 'Destroy', brewery_path(@brewery.page.title_for_url), :confirm => 'Are you sure?', :method => :delete %>
+ <%= tagged_image_browser_link -%>
<% end -%> diff --git a/app/views/galleries/_image.rhtml b/app/views/galleries/_image.rhtml new file mode 100644 index 0000000..66afb9f --- /dev/null +++ b/app/views/galleries/_image.rhtml @@ -0,0 +1,6 @@ +
+<% version ||= :screen -%> +
+ <%= link_to_unless_current(image_tag(image.filename_for_version(version), :alt => image.original), gallery_url(image)) %> +

Uploaded by <%= link_to(image.people.title, galleries_url(:id => image.people_id)) -%>

+
\ No newline at end of file diff --git a/app/views/galleries/_image_form.rhtml b/app/views/galleries/_image_form.rhtml new file mode 100644 index 0000000..f19e0cd --- /dev/null +++ b/app/views/galleries/_image_form.rhtml @@ -0,0 +1,9 @@ +
+

Upload an image

+

+ +

+
diff --git a/app/views/galleries/edit.rhtml b/app/views/galleries/edit.rhtml new file mode 100644 index 0000000..5867c8c --- /dev/null +++ b/app/views/galleries/edit.rhtml @@ -0,0 +1,12 @@ +

Editing images

+ +<%= error_messages_for :images %> + +<% form_for(:images, :url => images_path(@images), :html => { :method => :put }) do |f| %> +

+ <%= submit_tag "Update" %> +

+<% end %> + +<%= link_to 'Show', images_path(@images) %> | +<%= link_to 'Back', images_path %> \ No newline at end of file diff --git a/app/views/galleries/index.rhtml b/app/views/galleries/index.rhtml new file mode 100644 index 0000000..0117257 --- /dev/null +++ b/app/views/galleries/index.rhtml @@ -0,0 +1,7 @@ +<%= render :partial => 'image', :collection => @images, :locals => { :version => :thumbnail } %> + +<%= render :partial => 'shared/pagination_links' %> + +<% content_for :sidebar do -%> + <%= new_image_link -%>
+<% end -%> \ No newline at end of file diff --git a/app/views/galleries/new.rhtml b/app/views/galleries/new.rhtml new file mode 100644 index 0000000..cdbc490 --- /dev/null +++ b/app/views/galleries/new.rhtml @@ -0,0 +1,20 @@ +<%= error_messages_for :image %> + +<% form_for(:images, :url => galleries_path, :html => { :multipart => true, :onsubmit => "$('spinner').style.display = 'inline';" }) do |f| %> +
+

Upload an image

+

+ +

+
+

+ <%= submit_tag "Create" %> <%= image_tag '/images/spinner.gif', :id => 'spinner', :style => 'display:none' %> +

+<% end %> + +<% content_for :sidebar do -%> + <%= new_image_link -%>
+<% end -%> \ No newline at end of file diff --git a/app/views/galleries/show.rhtml b/app/views/galleries/show.rhtml new file mode 100644 index 0000000..3057b82 --- /dev/null +++ b/app/views/galleries/show.rhtml @@ -0,0 +1,9 @@ +<%= render :partial => 'image', :locals => { :image => @image } %> +
+ +<% content_for :sidebar do -%> + <%= link_to "#{@image.people.title}'s images (#{@image.people.images.size})", galleries_path(:id => @image.people_id) -%>
+ <%= link_to "Download original", :action => 'download_original', :id => @image.id -%>
+ <%= link_to 'Destroy', gallery_path(@image), :confirm => 'Are you sure?', :method => :delete %>
+ <%= link_to 'Tag Image', :controller => :tag_images, :action => :show, :id => @image.id -%>
+<% end -%> \ No newline at end of file diff --git a/app/views/layouts/application.rhtml b/app/views/layouts/application.rhtml index 1c326cf..33e5281 100644 --- a/app/views/layouts/application.rhtml +++ b/app/views/layouts/application.rhtml @@ -5,6 +5,7 @@ <%= stylesheet_link_tag 'application', :media => 'all' %> <%= javascript_include_tag :defaults %> + <%= javascript_include_tag 'control.modal.js' %> @@ -53,4 +54,5 @@ + diff --git a/app/views/shared/_lightbox.rhtml b/app/views/shared/_lightbox.rhtml new file mode 100644 index 0000000..d2dbf55 --- /dev/null +++ b/app/views/shared/_lightbox.rhtml @@ -0,0 +1,30 @@ +
+
+
<%= title -%>
+
+
<%= body -%>
+
+
+<%= link_to(title, "##{window_id}_dialog", { :class => "#{modal ? '' : 'non'}modal_controls", :onclick => "return false;", :id => "#{window_id}_id_key", :style => 'display: none;' }) %> +<% content_for :script do -%> + addLoadEvent(function(){ + if(!window.lightboxes) + lightboxes = {}; + if(!window.after_opens) + after_opens = {}; + if(!window.before_closes) + before_closes = {} + var link = $("<%= window_id -%>_id_key"); + var key = '<%= window_id -%>'; + var ao = after_opens[key]; + var bc = before_closes[key]; + if(ao == undefined) + ao = function(){}; + if(bc == undefined) + bc = function(){}; + lightboxes[key] = new Control.Modal(link, { + afterOpen: ao, beforeClose: bc, + overlayCloseOnClick: <%= modal ? 'false' : 'true' -%> + }); + }); +<% end -%> \ No newline at end of file diff --git a/app/views/shared/_tagged_image_browser.rhtml b/app/views/shared/_tagged_image_browser.rhtml new file mode 100644 index 0000000..115c397 --- /dev/null +++ b/app/views/shared/_tagged_image_browser.rhtml @@ -0,0 +1,46 @@ +<% content_for :stylesheet do -%> +#browser_box { +} + +#browser_box img { + vertical-align: middle; +} + +#browser_controls { + padding: 3px 10px 3px 10px; + text-align: center; +} +<% end -%> + +<% content_for :script do -%> +if(!window.after_opens) + after_opens = {}; +if(!window.before_closes) + before_closes = {} + +after_opens['tagged_image_browser'] = function(){ + $('browser_box').hide(); + new Ajax.Updater('browser_box', + '<%= url_for(:controller => :tag_images, :action => :tagged_images, :id => ((obj ||= nil).nil? ? nil : obj.id), :tagged_type => obj.class) -%>', + { + onFailure: function() { + lightboxes['tagged_image_browser'].close(); + }, + onSuccess: function() { + $('browser_spinner').hide(); + new Effect.Appear('browser_box', { duration: 1.5 }); + } + } + ); +} + +before_closes['tagged_image_browser'] = function(){ + $('browser_box').hide(); + $('browser_spinner').show(); +} +<% end -%> + +<% lightbox :title => 'Tagged Images', :window_id => 'tagged_image_browser' do -%> +
<%= image_tag('spinner.gif') -%>
+
+<% end -%> \ No newline at end of file diff --git a/app/views/tag_images/_tag_image_errors.rhtml b/app/views/tag_images/_tag_image_errors.rhtml new file mode 100644 index 0000000..b7a52c0 --- /dev/null +++ b/app/views/tag_images/_tag_image_errors.rhtml @@ -0,0 +1 @@ +<%= error_messages_for 'tag_image' %> diff --git a/app/views/tag_images/_tag_images.rhtml b/app/views/tag_images/_tag_images.rhtml new file mode 100644 index 0000000..fcb48ab --- /dev/null +++ b/app/views/tag_images/_tag_images.rhtml @@ -0,0 +1,8 @@ +

+ Tagged items: + <% if @tag_images.empty? -%> + None. + <% else -%> + <%= @tag_images.collect { |t| "#{t.tagged.title} (#{t.tagged_type} | #{link_to_remote('Remove', :url => tag_image_path(t), :update => 'tag_images', :method => :delete)})" }.join(', ') -%> + <% end -%> +

\ No newline at end of file diff --git a/app/views/tag_images/_taggable_results.rhtml b/app/views/tag_images/_taggable_results.rhtml new file mode 100644 index 0000000..3c96a98 --- /dev/null +++ b/app/views/tag_images/_taggable_results.rhtml @@ -0,0 +1,10 @@ +<% @results.keys.each do |ctype| -%> +<% next if @results[ctype].empty? -%> +

<%= ctype.pluralize -%>

+

+ <%= @results[ctype].collect { |r| link_to_function(r.title, "set_taggable_item(#{r.id}, '#{r.title}', '#{ctype}');") }.join(', ') %> +

+<% end -%> +<% unless @results.detect { |key, val| !val.empty? } -%> + No results found... +<% end -%> diff --git a/app/views/tag_images/_tagged_images.rhtml b/app/views/tag_images/_tagged_images.rhtml new file mode 100644 index 0000000..6cf85fd --- /dev/null +++ b/app/views/tag_images/_tagged_images.rhtml @@ -0,0 +1,11 @@ +
+<% @tagged_images.each do |tag_image| -%> + <%= link_to(image_tag(tag_image.image.filename_for_version(:thumbnail)), gallery_path(tag_image.image), :popup => true) %> +<% end -%> +
+ +
+ +
+ <%= image_browser_navigation_link('go-first.png', 1, @page_count, @tagged_type, params[:id]) -%> <%= image_browser_navigation_link('go-previous.png', @current_page - 1, @page_count, @tagged_type, params[:id]) -%> <%= @current_page -%> / <%= @page_count -%> <%= image_browser_navigation_link('go-next.png', @current_page + 1, @page_count, @tagged_type, params[:id]) -%> <%= image_browser_navigation_link('go-last.png', @page_count, @page_count, @tagged_type, params[:id]) -%> +
diff --git a/app/views/tag_images/show.rhtml b/app/views/tag_images/show.rhtml new file mode 100644 index 0000000..7ff1c4e --- /dev/null +++ b/app/views/tag_images/show.rhtml @@ -0,0 +1,110 @@ +<% content_for :stylesheet do -%> +#image_block { + z-index: 0; + border: 1px solid black; + padding: 0px; + width: <%= @image.screen_width -%>px; + height: <%= @image.screen_height -%>px; + background-image: url('/images/<%= @image.filename_for_version -%>'); + background-repeat: no-repeat; +} +#image_block_container { + margin: 10px <%= (605 - @image.screen_width) / 2 -%>px 20px <%= (605 - @image.screen_width) / 2 -%>px; +} +#image_tag_box { + position: relative; + z-index: 2; + width: 100px; + height: 100px; + border: 5px solid #db3333; + left: 0; + top: 0; + display: none; +} +<% end -%> + +<% content_for :script do -%> +function show_tag_at(xcoord, ycoord) +{ + $('image_tag_box').style.top = (ycoord - 50) + 'px'; + $('image_tag_box').style.left = (xcoord - 50) + 'px'; + $('image_tag_box').style.display = 'block'; +} + +function hide_tag_box() +{ + $('image_tag_box').style.display = 'none'; +} + +function set_coordinates(event) +{ + xcoord = (event.offsetX ? event.offsetX : (event.pageX - $('image_block').offsetLeft)); + ycoord = (event.offsetY ? event.offsetY : (event.pageY - $('image_block').offsetTop)); + show_tag_at(xcoord, ycoord); + lightboxes['taggedContentDialog'].open(); +} + +function set_taggable_item(id, title, type) +{ + $('tag_image_tagged_id').value = id; + $('tag_image_title').innerHTML = title; + $('tag_image_tagged_type').value = type; +} + +if(!window.after_opens) + after_opens = {}; +if(!window.before_closes) + before_closes = {}; +after_opens['taggedContentDialog'] = function(){ + $('tag_image_x').value = xcoord; + $('tag_image_y').value = ycoord; + $('tag_image_image_id').value = <%= params[:id] -%>; + $('search').focus(); +} +before_closes['taggedContentDialog'] = function(){ + hide_tag_box(); +} +<% end -%> + +
+
+ +
+
+
+ <%= render :partial => 'tag_images' %> +
+
+ +<% lightbox :title => 'Search for a taggable item', :window_id => 'taggedContentDialog' do -%> +
+
+ <%= hidden_field 'tag_image', 'x' %> + <%= hidden_field 'tag_image', 'y' %> + <%= hidden_field 'tag_image', 'image_id' %> + <%= hidden_field 'tag_image', 'tagged_id' %> + <%= hidden_field 'tag_image', 'tagged_type' %> +
+ +
+ <%= link_to_function(image_tag('edit-clear.png'), "$('search').value = '';") -%> <%= text_field_tag 'search', '', :size => 30 -%>
+ Selected: None +
+ +
+ + <%= observe_field 'search', + :url => { :action => 'taggable_search' }, + :frequency => 2, + :update => 'taggable_results', + :with => "'name='+escape(value)" %> + +
+ <%= link_to_remote('Save', { :url => tag_images_path, :with => "Form.serialize($('tag_image_fields'))", :success => 'Control.Modal.close()', :update => { :success => 'tag_images', :failure => 'tag_image_errors' } }, { :method => :post }) -%> + <%= link_to_function 'Cancel', "Control.Modal.close()" -%> +
+<% end -%> + +<% content_for :sidebar do -%> + <%= link_to 'Image Details', gallery_path(@image) -%>
+<% end -%> \ No newline at end of file diff --git a/config/environment.rb b/config/environment.rb index 8d3f1c9..4aae99d 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -9,3 +9,4 @@ end require 'redcloth' require 'has_one_page' +require 'has_many_tagged_images' diff --git a/config/routes.rb b/config/routes.rb index f0b9569..fe9a874 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,8 @@ ActionController::Routing::Routes.draw do |map| + map.resources :tag_images + map.resources :beers, :breweries, :pages, :discussions, :peoples, :roles, - :sessions, :styles, :galleries + :sessions, :styles, :galleries, :tag_images map.connect ':controller/:action/:id.:format' map.connect ':controller/:action/:id' diff --git a/db/migrate/011_create_images.rb b/db/migrate/011_create_images.rb new file mode 100644 index 0000000..f365957 --- /dev/null +++ b/db/migrate/011_create_images.rb @@ -0,0 +1,26 @@ +class CreateImages < ActiveRecord::Migration + def self.up + create_table :images do |t| + t.column :people_id, :integer + t.column :created_at, :datetime + t.column :original, :string + t.column :thumbnail, :string + t.column :screen, :string + t.column :screen_width, :integer + t.column :screen_height, :integer + t.column :content_type, :string + end + add_index :images, :people_id + create_table :images_pages, :id => false do |t| + t.column :image_id, :integer + t.column :page_id, :integer + end + add_index :images_pages, :image_id + add_index :images_pages, :page_id + end + + def self.down + drop_table :images + drop_table :images_pages + end +end diff --git a/db/migrate/012_create_tag_images.rb b/db/migrate/012_create_tag_images.rb new file mode 100644 index 0000000..7a6e71d --- /dev/null +++ b/db/migrate/012_create_tag_images.rb @@ -0,0 +1,19 @@ +class CreateTagImages < ActiveRecord::Migration + def self.up + create_table :tag_images do |t| + t.column :image_id, :integer + t.column :tagged_id, :integer + t.column :tagged_type, :string, :limit => 32 + t.column :primary, :boolean + t.column :x, :integer + t.column :y, :integer + end + add_index :tag_images, :image_id + add_index :tag_images, :tagged_id + add_index :tag_images, :tagged_type + end + + def self.down + drop_table :tag_images + end +end diff --git a/generate_permissions b/generate_permissions index af64f71..d2ccdd8 100644 --- a/generate_permissions +++ b/generate_permissions @@ -5,7 +5,7 @@ base_actions = ApplicationController.action_methods # rather than defining them here. controllers = [ PagesController, DiscussionsController, StylesController, PeoplesController, BeersController, BreweriesController, RolesController, - GalleriesController ] + GalleriesController, TagImagesController ] controllers.each do |c| actions = c.action_methods - base_actions cname = c.controller_name @@ -28,6 +28,11 @@ Permission.find(:all, next if [ 'new', 'create', 'edit', 'update', 'destroy' ].include?(p.action) r.permissions << p end +Permission.find(:all, + :conditions => [ 'controller = ?', 'tag_images' ]).each do |p| + next if [ 'show', 'create', 'destroy', 'taggable_search' ].include?(p.action) + r.permissions << p +end r2 = Role.admin_role Permission.find(:all).each do |p| diff --git a/lib/has_many_tagged_images.rb b/lib/has_many_tagged_images.rb new file mode 100644 index 0000000..66f9d71 --- /dev/null +++ b/lib/has_many_tagged_images.rb @@ -0,0 +1,25 @@ +module ActiveRecord # :nodoc: + class Base # :nodoc: + class << self + ## + # This method will add a has_one :page association and a few useful + # callbacks to the requested model. It expects to have a + # :owner_class parameter given so that it knows what the owner class + # name should be. The associated model will automatically be deleted + # when this model is deleted. + # + # The Page will automatically have the title updated from the owner's + # title field and be saved after a successful save. When a Page errors + # on validation, the errors are automatically copied into the owner so + # that the user doesn't even have to know what is going on. + # + def has_many_tagged_images(options = {}) + class_eval do + has_many :tagged_images, :source_type => self.base_class.to_s, + :source => :tagged, :through => :tag_images + has_many :tag_images, :dependent => :destroy, :as => :tagged + end + end + end + end +end diff --git a/public/images/edit-clear.png b/public/images/edit-clear.png new file mode 100644 index 0000000..e6c8e8b Binary files /dev/null and b/public/images/edit-clear.png differ diff --git a/public/images/go-first.png b/public/images/go-first.png new file mode 100644 index 0000000..9c15c09 Binary files /dev/null and b/public/images/go-first.png differ diff --git a/public/images/go-last.png b/public/images/go-last.png new file mode 100644 index 0000000..6e904ef Binary files /dev/null and b/public/images/go-last.png differ diff --git a/public/images/go-next.png b/public/images/go-next.png new file mode 100644 index 0000000..6ef8de7 Binary files /dev/null and b/public/images/go-next.png differ diff --git a/public/images/go-previous.png b/public/images/go-previous.png new file mode 100644 index 0000000..659cd90 Binary files /dev/null and b/public/images/go-previous.png differ diff --git a/public/javascripts/application.js b/public/javascripts/application.js index 867ece8..6241a34 100644 --- a/public/javascripts/application.js +++ b/public/javascripts/application.js @@ -1,3 +1,15 @@ +function addLoadEvent(func) { + var oldonload = window.onload; + if (typeof window.onload != 'function') { + window.onload = func; + } else { + window.onload = function() { + oldonload(); + func(); + } + } +} + function set_all_checkboxes(form_name, field_name, check_value) { if(!document.forms[form_name]) diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index c2338c0..50d5a1e 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -6,6 +6,7 @@ @import 'layout.css'; @import 'content.css'; @import 'syntax.css'; +@import 'lightboxes.css'; @media print { #sidebar { display: none; } #content { float: none; width: 90%; } @@ -22,10 +23,10 @@ textarea { font: normal 12px "bitstream vera sans", verdana, sans-serif; } abbr { border: none; } cite { font-style: normal; } -a img { border: none; padding: 0; margin: 0; } +a img { border: none; padding: 0; margin: 0; vertical-align: middle; text-decoration: none; } -a:link, a:visited { color: #000; } -a:hover, a:active { color: #fff; background: #000; } +a:link, a:visited { color: #000; text-decoration: none; } +a:hover, a:active { text-decoration: underline; } /* http://longren.org/2006/09/27/wrapping-text-inside-pre-tags */ pre { @@ -35,3 +36,5 @@ pre { white-space: -o-pre-wrap; word-wrap: break-word; } + +.centered { text-align: center; } diff --git a/public/stylesheets/content.css b/public/stylesheets/content.css index 4229558..8fa2f80 100644 --- a/public/stylesheets/content.css +++ b/public/stylesheets/content.css @@ -2,12 +2,12 @@ Header --------------------------------------------------------------*/ -#header { +#header { background: url(/images/header_shadow.gif) repeat-x left bottom; } #header a:link, -#header a:visited { +#header a:visited { color: #000; text-decoration: none; } @@ -19,14 +19,14 @@ } #header h1 { - font: bold 410% georgia, serif; - letter-spacing: -1px; + font: bold 410% georgia, serif; + letter-spacing: -1px; margin: 0; - float: left; + float: left; } #header h2 { - font: normal 12px verdana, arial, sans-serif; + font: normal 12px verdana, arial, sans-serif; margin: 2.35em 0.2em 0 0; float: right; } @@ -41,37 +41,37 @@ #content h2, #content h3, #content h4, -#content h5 { +#content h5 { font-family: georgia, times; font-weight: normal; letter-spacing: -1px; } -#content h1 { +#content h1 { font-size: 28px; - margin: 0 0 0.3em; + margin: 0 0 0.3em; } -#content h2 { +#content h2 { font-size: 24px; - margin: 0 0 0.3em; + margin: 0 0 0.3em; } -#content h3 { +#content h3 { font-size: 22px; - margin: 1.2em 0 0.3em; + margin: 1.2em 0 0.3em; } -#content h4 { +#content h4 { font-size: 20px; - margin: 1.2em 0 0.3em; + margin: 1.2em 0 0.3em; border-bottom: 1px dotted #bbb; } #content h5 { font-size: 20px; background: #ffd; - margin: 1.2em 0 0.3em; + margin: 1.2em 0 0.3em; border-bottom: 1px dotted #aaa; } @@ -91,17 +91,17 @@ } #content li { - line-height: 15px; + line-height: 15px; margin: 0 0 0 1em; padding: 0; } #content blockquote { color: #555; - border-left: 5px solid #ccc; + border-left: 5px solid #ccc; margin: 1.3em 1em; padding: 0 1em; } -#content code { +#content code { font: normal 12px "bitstream vera sans mono", monaco "lucida console", "courier new", courier, serif; } @@ -114,25 +114,25 @@ } /* Article Entries - class names based on http://microformats.org/wiki/hatom] */ -#content .hentry { +#content .hentry { margin: 0 0 3em 0; } #content .hentry .entry-title { font-size: 30px; line-height: 99%; - letter-spacing: -1.5px; + letter-spacing: -1.5px; margin: 0; } #content .hentry .entry-title a:link, -#content .hentry .entry-title a:visited { +#content .hentry .entry-title a:visited { color: #111; text-decoration: none; } #content .hentry .entry-title a:hover, -#content .hentry .entry-title a:active { +#content .hentry .entry-title a:active { background: transparent; text-decoration: underline; } @@ -163,7 +163,7 @@ #content .hentry .entry-content { } -#content .hentry ul.meta { +#content .hentry ul.meta { font-size: 10px; background: #eee; margin: 0; padding: 5px; @@ -171,7 +171,7 @@ list-style-type: none; } -#content .hentry ul.meta li { +#content .hentry ul.meta li { line-height: 13px; margin: 0; padding: 0; } @@ -203,7 +203,7 @@ margin: 0 0 1.5em; padding: 1em; } -#content li.discussion.preview { +#content li.discussion.preview { background: #ffc; border: 3px solid #fab444; margin: 0 0 1.5em; padding: 1em; @@ -239,19 +239,19 @@ padding: 1em 0.5em; } -#content form.discussions fieldset { +#content form.discussions fieldset { border: none; } -#content form.discussions legend { +#content form.discussions legend { display: none; } -#content form.discussions label { +#content form.discussions label { font-weight: bold; } -#content form.discussions textarea { +#content form.discussions textarea { width: 90%; height: 150px; padding: 3px; } @@ -263,24 +263,32 @@ } #content .people_image .author { - margin: 0 0 0.5em 0.5em; - width: 60px; height: 60px; - float: right; + margin: 0 2.5em 0.5em 0.5em; } #content .people_image .meta { text-align: center; } +.people_image img { + margin: 10px; +} + /*-------------------------------------------------------------- Sidebar --------------------------------------------------------------*/ #sidebar { - font-size: 11px; + font-size: 11px; } -#sidebar h3 { +#sidebar a:hover, #sidebar a:active { + color: #fff; + background-color: #000; + text-decoration: none; +} + +#sidebar h3 { font: bold 14px "lucidamac bold", "lucida grande", verdana, arial, helvetica, sans-serif; margin: 0 0 0.5em; } @@ -296,11 +304,11 @@ } #sidebar ul { - list-style-type: none; + list-style-type: none; margin: 0 0 2em; padding: 0; } -#sidebar li { +#sidebar li { margin: 0; padding: 1px 0; } @@ -308,7 +316,7 @@ #sidebar em { font-style: normal; } /* Live-search and results */ -#sidebar .search p { +#sidebar .search p { margin: 0; } @@ -322,7 +330,7 @@ height: 15px; } -#sidebar .search .results { +#sidebar .search .results { margin: 0 0 1.25em; } @@ -330,12 +338,12 @@ margin-top: 1em; } -#sidebar .search .results p { +#sidebar .search .results p { font: bold 14px "lucidamac bold", "lucida grande", verdana, arial, helvetica, sans-serif; margin: 0 0 0.5em; } -#sidebar .search .results ul { +#sidebar .search .results ul { margin: 0; padding: 3px; } @@ -346,53 +354,30 @@ color: #222; } -#sidebar .search .results a:hover, +#sidebar .search .results a:hover, #sidebar .search .results a:active { color: #fff; } -/* Flickr sidebar-node */ -#sidebar #flickr { - margin: 0 0 2em; - clear: both; -} - -#sidebar #flickr div { -} - -#sidebar #flickr img { - margin: 0 0 5px; - padding: 5px; - border: 1px solid #ddd; - display: block; -} - -#sidebar #flickr img:hover { - background: #ffc; -} - -#sidebar #flickr a { - border: none; -} - /*-------------------------------------------------------------- Footer --------------------------------------------------------------*/ #footer { border-top: 1px solid #ccc; - font-size: 90%; + font-size: 90%; } #footer a:link, -#footer a:visited { +#footer a:visited { color: #000; } #footer a:hover, -#footer a:active { +#footer a:active { color: #fff; background: #000; + text-decoration: none; } #footer hr { @@ -401,21 +386,21 @@ #footer p { width: 40%; - float: left; + float: left; margin: 0; padding: 0; } #footer ul { width: 40%; margin: 0; padding: 0; - list-style-type: none; - text-align: right; - float: right; + list-style-type: none; + text-align: right; + float: right; } #footer li { margin: 0; padding: 0 0 0 1em; - display: inline; + display: inline; } /* Tag Cloud Styles */ @@ -442,11 +427,11 @@ /* flash hash styles */ #notice { padding-top: 6px; - padding-bottom: 6px; + padding-bottom: 6px; padding-left: 6px; background-color: #F7F4D5; border-top: 1px solid #666; - border-bottom: 1px solid #666; + border-bottom: 1px solid #666; color: #333; font-weight: bold; margin-bottom: 10px; @@ -454,11 +439,11 @@ #error { padding-top: 6px; - padding-bottom: 6px; + padding-bottom: 6px; padding-left: 6px; background-color: #FFCCCC; border-top: 1px solid #666; - border-bottom: 1px solid #666; + border-bottom: 1px solid #666; color: #333; font-weight: bold; margin-bottom: 10px; diff --git a/public/stylesheets/layout.css b/public/stylesheets/layout.css index 89bcd2e..ca315f8 100644 --- a/public/stylesheets/layout.css +++ b/public/stylesheets/layout.css @@ -15,7 +15,7 @@ body { #container { max-width: 795px; - text-align: left; + text-align: left; margin: 0 auto; padding: 10px 0 0 0; } diff --git a/public/stylesheets/lightboxes.css b/public/stylesheets/lightboxes.css new file mode 100644 index 0000000..ac7dd6b --- /dev/null +++ b/public/stylesheets/lightboxes.css @@ -0,0 +1,69 @@ +.dialogBox { + background-color: #eee; + border: 1px solid #444; + -moz-border-radius: 5px; + padding: 4px; + height: auto; + min-height: 75px; + max-height: 450px; + width: auto; + min-width: 275px; + max-width: 550px; + text-align: left; +} + +.dialogHeader { + background: #999; + padding: 2px 5px 2px 5px; + text-align: center; + -moz-border-radius: 5px; + border: 1px solid #444; + color: #FFF; + font-weight: bold; + font-size: 1.2em; +} + +.dialogControls { + height: auto; + text-align: center; + width: auto; + font-size: 1.1em; + margin: 10px; +} +.dialogControls a { + background-color: #CCC; + border: 1px solid black; + padding: 0.25em 1.0em 0.25em 1.0em; + text-decoration: none; + -moz-border-radius: 3px; +} +.dialogControls a:hover, .dialogControls a.over { + text-decoration: none; + border: 1px solid black; + background-color: #999; + color: white; +} + +.dialogContent { + padding: 0px 10px 0px 10px; + overflow: auto; +} + +.dialogSearchResults { + padding: 0.25em; + margin-bottom: 5px; + font-size: 1.1em; + min-height: 50px; + max-height: 300px; + overflow: auto; +} +.dialogSearchResults li { + margin-top: 3px; + margin-bottom: 3px; +} + +#modal_overlay { + /* konqueror doesn't like these styles. omit for now. + background-color: #727272; + */ +} diff --git a/test/fixtures/images.yml b/test/fixtures/images.yml new file mode 100644 index 0000000..b49c4eb --- /dev/null +++ b/test/fixtures/images.yml @@ -0,0 +1,5 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +one: + id: 1 +two: + id: 2 diff --git a/test/fixtures/tag_images.yml b/test/fixtures/tag_images.yml new file mode 100644 index 0000000..b49c4eb --- /dev/null +++ b/test/fixtures/tag_images.yml @@ -0,0 +1,5 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +one: + id: 1 +two: + id: 2 diff --git a/test/functional/galleries_controller_test.rb b/test/functional/galleries_controller_test.rb new file mode 100644 index 0000000..49ddc1f --- /dev/null +++ b/test/functional/galleries_controller_test.rb @@ -0,0 +1,57 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'images_controller' + +# Re-raise errors caught by the controller. +class GalleriesController; def rescue_action(e) raise e end; end + +class GalleriesControllerTest < Test::Unit::TestCase + fixtures :images + + def setup + @controller = GalleriesController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_should_get_index + get :index + assert_response :success + assert assigns(:images) + end + + def test_should_get_new + get :new + assert_response :success + end + + def test_should_create_images + old_count = Images.count + post :create, :images => { } + assert_equal old_count+1, Images.count + + assert_redirected_to images_path(assigns(:images)) + end + + def test_should_show_images + get :show, :id => 1 + assert_response :success + end + + def test_should_get_edit + get :edit, :id => 1 + assert_response :success + end + + def test_should_update_images + put :update, :id => 1, :images => { } + assert_redirected_to images_path(assigns(:images)) + end + + def test_should_destroy_images + old_count = Images.count + delete :destroy, :id => 1 + assert_equal old_count-1, Images.count + + assert_redirected_to images_path + end +end diff --git a/test/functional/tag_images_controller_test.rb b/test/functional/tag_images_controller_test.rb new file mode 100644 index 0000000..6eda0a9 --- /dev/null +++ b/test/functional/tag_images_controller_test.rb @@ -0,0 +1,57 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'tag_images_controller' + +# Re-raise errors caught by the controller. +class TagImagesController; def rescue_action(e) raise e end; end + +class TagImagesControllerTest < Test::Unit::TestCase + fixtures :tag_images + + def setup + @controller = TagImagesController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_should_get_index + get :index + assert_response :success + assert assigns(:tag_images) + end + + def test_should_get_new + get :new + assert_response :success + end + + def test_should_create_tag_image + old_count = TagImage.count + post :create, :tag_image => { } + assert_equal old_count+1, TagImage.count + + assert_redirected_to tag_image_path(assigns(:tag_image)) + end + + def test_should_show_tag_image + get :show, :id => 1 + assert_response :success + end + + def test_should_get_edit + get :edit, :id => 1 + assert_response :success + end + + def test_should_update_tag_image + put :update, :id => 1, :tag_image => { } + assert_redirected_to tag_image_path(assigns(:tag_image)) + end + + def test_should_destroy_tag_image + old_count = TagImage.count + delete :destroy, :id => 1 + assert_equal old_count-1, TagImage.count + + assert_redirected_to tag_images_path + end +end diff --git a/test/unit/image_test.rb b/test/unit/image_test.rb new file mode 100644 index 0000000..499d844 --- /dev/null +++ b/test/unit/image_test.rb @@ -0,0 +1,10 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class ImageTest < Test::Unit::TestCase + fixtures :images + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/test/unit/tag_image_test.rb b/test/unit/tag_image_test.rb new file mode 100644 index 0000000..8f1f3d1 --- /dev/null +++ b/test/unit/tag_image_test.rb @@ -0,0 +1,10 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class TagImageTest < Test::Unit::TestCase + fixtures :tag_images + + # Replace this with your real tests. + def test_truth + assert true + end +end