diff --git a/README b/README index 8f080e2c..4f0da9f1 100644 --- a/README +++ b/README @@ -8,7 +8,7 @@ * Mailing list: http://lists.rousette.org.uk/mailman/listinfo/tracks-discuss * Original developer: bsag (http://www.rousette.org.uk/) * Contributors: http://getontracks.org/wiki/Tracks/Contributing/Contributors -* Version: 1.7 +* Version: 1.7RC2 * Copyright: (cc) 2004-2008 rousette.org.uk. * License: GNU GPL diff --git a/app/controllers/application.rb b/app/controllers/application.rb index 4c63b89f..550a8fca 100644 --- a/app/controllers/application.rb +++ b/app/controllers/application.rb @@ -1,268 +1,266 @@ -# The filters added to this controller will be run for all controllers in the -# application. Likewise will all the methods added be available for all -# controllers. - -require_dependency "login_system" -require_dependency "tracks/source_view" -require "redcloth" - -require 'date' -require 'time' - -# Commented the following line because of #744. It prevented rake db:migrate to -# run because this tag went looking for the taggings table that did not exist -# when you feshly create a new database Old comment: We need this in development -# mode, or you get 'method missing' errors -# -# Tag - -class CannotAccessContext < RuntimeError; end - -class ApplicationController < ActionController::Base - +# The filters added to this controller will be run for all controllers in the +# application. Likewise will all the methods added be available for all +# controllers. + +require_dependency "login_system" +require_dependency "tracks/source_view" +require "redcloth" + +require 'date' +require 'time' + +# Commented the following line because of #744. It prevented rake db:migrate to +# run because this tag went looking for the taggings table that did not exist +# when you feshly create a new database Old comment: We need this in development +# mode, or you get 'method missing' errors +# +# Tag + +class CannotAccessContext < RuntimeError; end + +class ApplicationController < ActionController::Base + protect_from_forgery :secret => SITE_CONFIG['salt'] - - helper :application - include LoginSystem - helper_method :current_user, :prefs - - layout proc{ |controller| controller.mobile? ? "mobile" : "standard" } - - before_filter :set_session_expiration - before_filter :set_time_zone - prepend_before_filter :login_required - prepend_before_filter :enable_mobile_content_negotiation - after_filter :set_charset - - - - include ActionView::Helpers::TextHelper - include ActionView::Helpers::SanitizeHelper - extend ActionView::Helpers::SanitizeHelper::ClassMethods - helper_method :format_date, :markdown - - # By default, sets the charset to UTF-8 if it isn't already set - def set_charset - headers["Content-Type"] ||= "text/html; charset=UTF-8" - end - - def set_session_expiration - # http://wiki.rubyonrails.com/rails/show/HowtoChangeSessionOptions - unless session == nil - return if @controller_name == 'feed' or session['noexpiry'] == "on" - # If the method is called by the feed controller (which we don't have - # under session control) or if we checked the box to keep logged in on - # login don't set the session expiry time. - if session - # Get expiry time (allow ten seconds window for the case where we have - # none) - expiry_time = session['expiry_time'] || Time.now + 10 - if expiry_time < Time.now - # Too late, matey... bang goes your session! - reset_session - else - # Okay, you get another hour - session['expiry_time'] = Time.now + (60*60) - end - end - end - end - - def render_failure message, status = 404 - render :text => message, :status => status - end - - # def rescue_action(exception) - # log_error(exception) if logger - # respond_to do |format| - # format.html do - # notify :warning, "An error occurred on the server." - # render :action => "index" - # end - # format.js { render :action => 'error' } - # format.xml { render :text => 'An error occurred on the server.' + $! } - # end - # end - - # Returns a count of next actions in the given context or project The result - # is count and a string descriptor, correctly pluralised if there are no - # actions or multiple actions - # - def count_undone_todos_phrase(todos_parent, string="actions") - count = count_undone_todos(todos_parent) - if count == 1 - word = string.singularize - else - word = string.pluralize - end - return count.to_s + " " + word - end - - def count_undone_todos(todos_parent) - if todos_parent.nil? - count = 0 - elsif (todos_parent.is_a?(Project) && todos_parent.hidden?) - count = eval "@project_project_hidden_todo_counts[#{todos_parent.id}]" - else - count = eval "@#{todos_parent.class.to_s.downcase}_not_done_counts[#{todos_parent.id}]" - end - count || 0 - end - - # Convert a date object to the format specified in the user's preferences in - # config/settings.yml - # - def format_date(date) - if date - date_format = prefs.date_format - formatted_date = date.in_time_zone(prefs.time_zone).strftime("#{date_format}") - else - formatted_date = '' - end - formatted_date - end - - # Uses RedCloth to transform text using either Textile or Markdown Need to - # require redcloth above RedCloth 3.0 or greater is needed to use Markdown, - # otherwise it only handles Textile - # - def markdown(text) - RedCloth.new(text).to_html - end - - def build_default_project_context_name_map(projects) - Hash[*projects.reject{ |p| p.default_context.nil? }.map{ |p| [p.name, p.default_context.name] }.flatten].to_json - end - - # Here's the concept behind this "mobile content negotiation" hack: In - # addition to the main, AJAXy Web UI, Tracks has a lightweight low-feature - # 'mobile' version designed to be suitablef or use from a phone or PDA. It - # makes some sense that tne pages of that mobile version are simply alternate - # representations of the same Todo resources. The implementation goal was to - # treat mobile as another format and be able to use respond_to to render both - # versions. Unfortunately, I ran into a lot of trouble simply registering a - # new mime type 'text/html' with format :m because :html already is linked to - # that mime type and the new registration was forcing all html requests to be - # rendered in the mobile view. The before_filter and after_filter hackery - # below accomplishs that implementation goal by using a 'fake' mime type - # during the processing and then setting it to 'text/html' in an - # 'after_filter' -LKM 2007-04-01 - def mobile? - return params[:format] == 'm' - end - - def enable_mobile_content_negotiation - if mobile? - request.format = :m - end - end - - def create_todo_from_recurring_todo(rt, date=nil) - # create todo and initialize with data from recurring_todo rt - todo = current_user.todos.build( { :description => rt.description, :notes => rt.notes, :project_id => rt.project_id, :context_id => rt.context_id}) - - # set dates - todo.recurring_todo_id = rt.id - todo.due = rt.get_due_date(date) - - show_from_date = rt.get_show_from_date(date) - if show_from_date.nil? - todo.show_from=nil - else - # make sure that show_from is not in the past - todo.show_from = show_from_date < Time.zone.now ? nil : show_from_date - end - - saved = todo.save - if saved - todo.tag_with(rt.tag_list) - todo.tags.reload - end - - # increate number of occurences created from recurring todo - rt.inc_occurences - - # mark recurring todo complete if there are no next actions left - checkdate = todo.due.nil? ? todo.show_from : todo.due - rt.toggle_completion! unless rt.has_next_todo(checkdate) - - return saved ? todo : nil - end - - protected - - def admin_login_required - unless User.find_by_id_and_is_admin(session['user_id'], true) - render :text => "401 Unauthorized: Only admin users are allowed access to this function.", :status => 401 - return false - end - end - - def redirect_back_or_home - respond_to do |format| - format.html { redirect_back_or_default home_url } - format.m { redirect_back_or_default mobile_url } - end - end - - def boolean_param(param_name) - return false if param_name.blank? - s = params[param_name] - return false if s.blank? || s == false || s =~ /^false$/i - return true if s == true || s =~ /^true$/i - raise ArgumentError.new("invalid value for Boolean: \"#{s}\"") - end - - def self.openid_enabled? - Tracks::Config.openid_enabled? - end - - def openid_enabled? - self.class.openid_enabled? - end - - private - - def parse_date_per_user_prefs( s ) - prefs.parse_date(s) - end - - def init_data_for_sidebar - @completed_projects = current_user.projects.completed - @hidden_projects = current_user.projects.hidden - @active_projects = current_user.projects.active - - @active_contexts = current_user.contexts.active - @hidden_contexts = current_user.contexts.hidden - - init_not_done_counts - if prefs.show_hidden_projects_in_sidebar - init_project_hidden_todo_counts(['project']) - end - end - - def init_not_done_counts(parents = ['project','context']) - parents.each do |parent| - eval("@#{parent}_not_done_counts = @#{parent}_not_done_counts || current_user.todos.active.count(:group => :#{parent}_id)") - end - end - - def init_project_hidden_todo_counts(parents = ['project','context']) - parents.each do |parent| - eval("@#{parent}_project_hidden_todo_counts = @#{parent}_project_hidden_todo_counts || current_user.todos.count(:conditions => ['state = ? or state = ?', 'project_hidden', 'active'], :group => :#{parent}_id)") - end - end - - # Set the contents of the flash message from a controller Usage: notify - # :warning, "This is the message" Sets the flash of type 'warning' to "This is - # the message" - def notify(type, message) - flash[type] = message - logger.error("ERROR: #{message}") if type == :error - end - - def set_time_zone - Time.zone = current_user.prefs.time_zone if logged_in? - end - -end + + helper :application + include LoginSystem + helper_method :current_user, :prefs + + layout proc{ |controller| controller.mobile? ? "mobile" : "standard" } + + before_filter :set_session_expiration + before_filter :set_time_zone + prepend_before_filter :login_required + prepend_before_filter :enable_mobile_content_negotiation + after_filter :set_charset + + include ActionView::Helpers::TextHelper + include ActionView::Helpers::SanitizeHelper + extend ActionView::Helpers::SanitizeHelper::ClassMethods + helper_method :format_date, :markdown + + # By default, sets the charset to UTF-8 if it isn't already set + def set_charset + headers["Content-Type"] ||= "text/html; charset=UTF-8" + end + + def set_session_expiration + # http://wiki.rubyonrails.com/rails/show/HowtoChangeSessionOptions + unless session == nil + return if @controller_name == 'feed' or session['noexpiry'] == "on" + # If the method is called by the feed controller (which we don't have + # under session control) or if we checked the box to keep logged in on + # login don't set the session expiry time. + if session + # Get expiry time (allow ten seconds window for the case where we have + # none) + expiry_time = session['expiry_time'] || Time.now + 10 + if expiry_time < Time.now + # Too late, matey... bang goes your session! + reset_session + else + # Okay, you get another hour + session['expiry_time'] = Time.now + (60*60) + end + end + end + end + + def render_failure message, status = 404 + render :text => message, :status => status + end + + # def rescue_action(exception) + # log_error(exception) if logger + # respond_to do |format| + # format.html do + # notify :warning, "An error occurred on the server." + # render :action => "index" + # end + # format.js { render :action => 'error' } + # format.xml { render :text => 'An error occurred on the server.' + $! } + # end + # end + + # Returns a count of next actions in the given context or project The result + # is count and a string descriptor, correctly pluralised if there are no + # actions or multiple actions + # + def count_undone_todos_phrase(todos_parent, string="actions") + count = count_undone_todos(todos_parent) + if count == 1 + word = string.singularize + else + word = string.pluralize + end + return count.to_s + " " + word + end + + def count_undone_todos(todos_parent) + if todos_parent.nil? + count = 0 + elsif (todos_parent.is_a?(Project) && todos_parent.hidden?) + count = eval "@project_project_hidden_todo_counts[#{todos_parent.id}]" + else + count = eval "@#{todos_parent.class.to_s.downcase}_not_done_counts[#{todos_parent.id}]" + end + count || 0 + end + + # Convert a date object to the format specified in the user's preferences in + # config/settings.yml + # + def format_date(date) + if date + date_format = prefs.date_format + formatted_date = date.in_time_zone(prefs.time_zone).strftime("#{date_format}") + else + formatted_date = '' + end + formatted_date + end + + # Uses RedCloth to transform text using either Textile or Markdown Need to + # require redcloth above RedCloth 3.0 or greater is needed to use Markdown, + # otherwise it only handles Textile + # + def markdown(text) + RedCloth.new(text).to_html + end + + def build_default_project_context_name_map(projects) + Hash[*projects.reject{ |p| p.default_context.nil? }.map{ |p| [p.name, p.default_context.name] }.flatten].to_json + end + + # Here's the concept behind this "mobile content negotiation" hack: In + # addition to the main, AJAXy Web UI, Tracks has a lightweight low-feature + # 'mobile' version designed to be suitablef or use from a phone or PDA. It + # makes some sense that tne pages of that mobile version are simply alternate + # representations of the same Todo resources. The implementation goal was to + # treat mobile as another format and be able to use respond_to to render both + # versions. Unfortunately, I ran into a lot of trouble simply registering a + # new mime type 'text/html' with format :m because :html already is linked to + # that mime type and the new registration was forcing all html requests to be + # rendered in the mobile view. The before_filter and after_filter hackery + # below accomplishs that implementation goal by using a 'fake' mime type + # during the processing and then setting it to 'text/html' in an + # 'after_filter' -LKM 2007-04-01 + def mobile? + return params[:format] == 'm' + end + + def enable_mobile_content_negotiation + if mobile? + request.format = :m + end + end + + def create_todo_from_recurring_todo(rt, date=nil) + # create todo and initialize with data from recurring_todo rt + todo = current_user.todos.build( { :description => rt.description, :notes => rt.notes, :project_id => rt.project_id, :context_id => rt.context_id}) + + # set dates + todo.recurring_todo_id = rt.id + todo.due = rt.get_due_date(date) + + show_from_date = rt.get_show_from_date(date) + if show_from_date.nil? + todo.show_from=nil + else + # make sure that show_from is not in the past + todo.show_from = show_from_date < Time.zone.now ? nil : show_from_date + end + + saved = todo.save + if saved + todo.tag_with(rt.tag_list) + todo.tags.reload + end + + # increate number of occurences created from recurring todo + rt.inc_occurences + + # mark recurring todo complete if there are no next actions left + checkdate = todo.due.nil? ? todo.show_from : todo.due + rt.toggle_completion! unless rt.has_next_todo(checkdate) + + return saved ? todo : nil + end + + protected + + def admin_login_required + unless User.find_by_id_and_is_admin(session['user_id'], true) + render :text => "401 Unauthorized: Only admin users are allowed access to this function.", :status => 401 + return false + end + end + + def redirect_back_or_home + respond_to do |format| + format.html { redirect_back_or_default home_url } + format.m { redirect_back_or_default mobile_url } + end + end + + def boolean_param(param_name) + return false if param_name.blank? + s = params[param_name] + return false if s.blank? || s == false || s =~ /^false$/i + return true if s == true || s =~ /^true$/i + raise ArgumentError.new("invalid value for Boolean: \"#{s}\"") + end + + def self.openid_enabled? + Tracks::Config.openid_enabled? + end + + def openid_enabled? + self.class.openid_enabled? + end + + private + + def parse_date_per_user_prefs( s ) + prefs.parse_date(s) + end + + def init_data_for_sidebar + @completed_projects = current_user.projects.completed + @hidden_projects = current_user.projects.hidden + @active_projects = current_user.projects.active + + @active_contexts = current_user.contexts.active + @hidden_contexts = current_user.contexts.hidden + + init_not_done_counts + if prefs.show_hidden_projects_in_sidebar + init_project_hidden_todo_counts(['project']) + end + end + + def init_not_done_counts(parents = ['project','context']) + parents.each do |parent| + eval("@#{parent}_not_done_counts = @#{parent}_not_done_counts || current_user.todos.active.count(:group => :#{parent}_id)") + end + end + + def init_project_hidden_todo_counts(parents = ['project','context']) + parents.each do |parent| + eval("@#{parent}_project_hidden_todo_counts = @#{parent}_project_hidden_todo_counts || current_user.todos.count(:conditions => ['state = ? or state = ?', 'project_hidden', 'active'], :group => :#{parent}_id)") + end + end + + # Set the contents of the flash message from a controller Usage: notify + # :warning, "This is the message" Sets the flash of type 'warning' to "This is + # the message" + def notify(type, message) + flash[type] = message + logger.error("ERROR: #{message}") if type == :error + end + + def set_time_zone + Time.zone = current_user.prefs.time_zone if logged_in? + end + +end diff --git a/app/controllers/data_controller.rb b/app/controllers/data_controller.rb index ba997607..b4b7dd75 100644 --- a/app/controllers/data_controller.rb +++ b/app/controllers/data_controller.rb @@ -21,8 +21,22 @@ class DataController < ApplicationController all_tables['todos'] = current_user.todos.find(:all) all_tables['contexts'] = current_user.contexts.find(:all) all_tables['projects'] = current_user.projects.find(:all) - all_tables['tags'] = current_user.tags.find(:all) - all_tables['taggings'] = current_user.taggings.find(:all) + + tags = Tag.find_by_sql([ + "SELECT tags.* "+ + "FROM tags, taggings, todos "+ + "WHERE todos.user_id=? "+ + "AND tags.id = taggings.tag_id " + + "AND taggings.taggable_id = todos.id ", current_user.id]) + all_tables['tags'] = tags + + taggings = Tagging.find_by_sql([ + "SELECT taggings.* "+ + "FROM taggings, todos "+ + "WHERE todos.user_id=? "+ + "AND taggings.taggable_id = todos.id ", current_user.id]) + all_tables['taggings'] = taggings + all_tables['notes'] = current_user.notes.find(:all) all_tables['recurring_todos'] = current_user.recurring_todos.find(:all) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 4e739fe9..1baca4b6 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -175,7 +175,7 @@ class ProjectsController < ApplicationController def order project_ids = params["list-active-projects"] || params["list-hidden-projects"] || params["list-completed-projects"] - projects = current_user.projects.update_positions( project_ids ) + @projects = current_user.projects.update_positions( project_ids ) render :nothing => true rescue notify :error, $! diff --git a/app/controllers/recurring_todos_controller.rb b/app/controllers/recurring_todos_controller.rb index aaed6a33..e9776006 100644 --- a/app/controllers/recurring_todos_controller.rb +++ b/app/controllers/recurring_todos_controller.rb @@ -1,268 +1,267 @@ -class RecurringTodosController < ApplicationController - - helper :todos, :recurring_todos - - append_before_filter :init, :only => [:index, :new, :edit] - append_before_filter :get_recurring_todo_from_param, :only => [:destroy, :toggle_check, :toggle_star, :edit, :update] - - def index - find_and_inactivate - - @recurring_todos = current_user.recurring_todos.active - @completed_recurring_todos = current_user.recurring_todos.completed - @no_recurring_todos = @recurring_todos.size == 0 - @no_completed_recurring_todos = @completed_recurring_todos.size == 0 - @count = @recurring_todos.size - - @page_title = "TRACKS::Recurring Actions" - end - - def new - end - - def show - end - - def edit - respond_to do |format| - format.js - end - end - - def update - @recurring_todo.tag_with(params[:tag_list]) if params[:tag_list] - @original_item_context_id = @recurring_todo.context_id - @original_item_project_id = @recurring_todo.project_id - - # we needed to rename the recurring_period selector in the edit form because - # the form for a new recurring todo and the edit form are on the same page. - # Same goes for start_from and end_date - params['recurring_todo']['recurring_period']=params['recurring_edit_todo']['recurring_period'] - params['recurring_todo']['end_date']=parse_date_per_user_prefs(params['recurring_todo_edit_end_date']) - params['recurring_todo']['start_from']=parse_date_per_user_prefs(params['recurring_todo_edit_start_from']) - - # update project - if params['recurring_todo']['project_id'].blank? && !params['project_name'].nil? - if params['project_name'] == 'None' - project = Project.null_object - else - project = current_user.projects.find_by_name(params['project_name'].strip) - unless project - project = current_user.projects.build - project.name = params['project_name'].strip - project.save - @new_project_created = true - end - end - params["recurring_todo"]["project_id"] = project.id - end - - # update context - if params['recurring_todo']['context_id'].blank? && !params['context_name'].blank? - context = current_user.contexts.find_by_name(params['context_name'].strip) - unless context - context = current_user.contexts.build - context.name = params['context_name'].strip - context.save - @new_context_created = true - end - params["recurring_todo"]["context_id"] = context.id - end - - params["recurring_todo"]["weekly_return_monday"]=' ' if params["recurring_todo"]["weekly_return_monday"].nil? - params["recurring_todo"]["weekly_return_tuesday"]=' ' if params["recurring_todo"]["weekly_return_tuesday"].nil? - params["recurring_todo"]["weekly_return_wednesday"]=' ' if params["recurring_todo"]["weekly_return_wednesday"].nil? - params["recurring_todo"]["weekly_return_thursday"]=' ' if params["recurring_todo"]["weekly_return_thursday"].nil? - params["recurring_todo"]["weekly_return_friday"]=' ' if params["recurring_todo"]["weekly_return_friday"].nil? - params["recurring_todo"]["weekly_return_saturday"]=' ' if params["recurring_todo"]["weekly_return_saturday"].nil? - params["recurring_todo"]["weekly_return_sunday"]=' ' if params["recurring_todo"]["weekly_return_sunday"].nil? - - @saved = @recurring_todo.update_attributes params["recurring_todo"] - - respond_to do |format| - format.js - end - end - - def create - p = RecurringTodoCreateParamsHelper.new(params) - p.attributes['end_date']=parse_date_per_user_prefs(p.attributes['end_date']) - p.attributes['start_from']=parse_date_per_user_prefs(p.attributes['start_from']) - - @recurring_todo = current_user.recurring_todos.build(p.selector_attributes) - @recurring_todo.update_attributes(p.attributes) - - if p.project_specified_by_name? - project = current_user.projects.find_or_create_by_name(p.project_name) - @new_project_created = project.new_record_before_save? - @recurring_todo.project_id = project.id - end - - if p.context_specified_by_name? - context = current_user.contexts.find_or_create_by_name(p.context_name) - @new_context_created = context.new_record_before_save? - @recurring_todo.context_id = context.id - end - - @recurring_saved = @recurring_todo.save - unless (@recurring_saved == false) || p.tag_list.blank? - @recurring_todo.tag_with(p.tag_list) - @recurring_todo.tags.reload - end - - if @recurring_saved - @message = "The recurring todo was saved" - @todo_saved = create_todo_from_recurring_todo(@recurring_todo).nil? == false - if @todo_saved - @message += " / created a new todo" - else - @message += " / did not create todo" - end - @count = current_user.recurring_todos.active.count - else - @message = "Error saving recurring todo" - end - - respond_to do |format| - format.js - end - end - - def destroy - - # remove all references to this recurring todo - @todos = @recurring_todo.todos - @number_of_todos = @todos.size - @todos.each do |t| - t.recurring_todo_id = nil - t.save - end - - # delete the recurring todo - @saved = @recurring_todo.destroy - @remaining = current_user.recurring_todos.count - - respond_to do |format| - - format.html do - if @saved - notify :notice, "Successfully deleted recurring action", 2.0 - redirect_to :action => 'index' - else - notify :error, "Failed to delete the recurring action", 2.0 - redirect_to :action => 'index' - end - end - - format.js do - render - end - end - end - - def toggle_check - @saved = @recurring_todo.toggle_completion! - - @count = current_user.recurring_todos.active.count - @remaining = @count - - if @recurring_todo.active? - @remaining = current_user.recurring_todos.completed.count - - # from completed back to active -> check if there is an active todo - # current_user.todos.count(:all, {:conditions => ["state = ? AND recurring_todo_id = ?", 'active',params[:id]]}) - @active_todos = @recurring_todo.todos.active.count - # create todo if there is no active todo belonging to the activated - # recurring_todo - @new_recurring_todo = create_todo_from_recurring_todo(@recurring_todo) if @active_todos == 0 - end - - respond_to do |format| - format.js - end - end - - def toggle_star - @recurring_todo.toggle_star! - @saved = @recurring_todo.save! - respond_to do |format| - format.js - end - end - - class RecurringTodoCreateParamsHelper - - def initialize(params) - @params = params['request'] || params - @attributes = params['request'] && params['request']['recurring_todo'] || params['recurring_todo'] - - # make sure all selectors (recurring_period, recurrence_selector, - # daily_selector, monthly_selector and yearly_selector) are first in hash - # so that they are processed first by the model - @selector_attributes = { - 'recurring_period' => @attributes['recurring_period'], - 'daily_selector' => @attributes['daily_selector'], - 'monthly_selector' => @attributes['monthly_selector'], - 'yearly_selector' => @attributes['yearly_selector'] - } - end - - def attributes - @attributes - end - - def selector_attributes - return @selector_attributes - end - - def project_name - @params['project_name'].strip unless @params['project_name'].nil? - end - - def context_name - @params['context_name'].strip unless @params['context_name'].nil? - end - - def tag_list - @params['tag_list'] - end - - def project_specified_by_name? - return false unless @attributes['project_id'].blank? - return false if project_name.blank? - return false if project_name == 'None' - true - end - - def context_specified_by_name? - return false unless @attributes['context_id'].blank? - return false if context_name.blank? - true - end - - end - - private - - def init - @days_of_week = [ ['Sunday',0], ['Monday',1], ['Tuesday', 2], ['Wednesday',3], ['Thursday',4], ['Friday',5], ['Saturday',6]] - @months_of_year = [ - ['January',1], ['Februari',2], ['March', 3], ['April',4], ['May',5], ['June',6], - ['July',7], ['August',8], ['September',9], ['October', 10], ['November', 11], ['December',12]] - @xth_day = [['first',1],['second',2],['third',3],['fourth',4],['last',5]] - @projects = current_user.projects.find(:all, :include => [:default_context]) - @contexts = current_user.contexts.find(:all) - @default_project_context_name_map = build_default_project_context_name_map(@projects).to_json - end - - def get_recurring_todo_from_param - @recurring_todo = current_user.recurring_todos.find(params[:id]) - end - - def find_and_inactivate - # find active recurring todos without active todos and inactivate them - recurring_todos = current_user.recurring_todos.active - recurring_todos.each { |rt| rt.toggle_completion! if rt.todos.not_completed.count == 0} - end - -end +class RecurringTodosController < ApplicationController + + helper :todos, :recurring_todos + + append_before_filter :init, :only => [:index, :new, :edit] + append_before_filter :get_recurring_todo_from_param, :only => [:destroy, :toggle_check, :toggle_star, :edit, :update] + + def index + find_and_inactivate + + @recurring_todos = current_user.recurring_todos.active + @completed_recurring_todos = current_user.recurring_todos.completed + @no_recurring_todos = @recurring_todos.size == 0 + @no_completed_recurring_todos = @completed_recurring_todos.size == 0 + @count = @recurring_todos.size + + @page_title = "TRACKS::Recurring Actions" + end + + def new + end + + def show + end + + def edit + respond_to do |format| + format.js + end + end + + def update + # TODO: write tests for updating + @recurring_todo.tag_with(params[:tag_list]) if params[:tag_list] + @original_item_context_id = @recurring_todo.context_id + @original_item_project_id = @recurring_todo.project_id + + # we needed to rename the recurring_period selector in the edit form because + # the form for a new recurring todo and the edit form are on the same page. + # Same goes for start_from and end_date + params['recurring_todo']['recurring_period']=params['recurring_edit_todo']['recurring_period'] + params['recurring_todo']['end_date']=parse_date_per_user_prefs(params['recurring_todo_edit_end_date']) + params['recurring_todo']['start_from']=parse_date_per_user_prefs(params['recurring_todo_edit_start_from']) + + # update project + if params['recurring_todo']['project_id'].blank? && !params['project_name'].nil? + if params['project_name'] == 'None' + project = Project.null_object + else + project = current_user.projects.find_by_name(params['project_name'].strip) + unless project + project = current_user.projects.build + project.name = params['project_name'].strip + project.save + @new_project_created = true + end + end + params["recurring_todo"]["project_id"] = project.id + end + + # update context + if params['recurring_todo']['context_id'].blank? && !params['context_name'].blank? + context = current_user.contexts.find_by_name(params['context_name'].strip) + unless context + context = current_user.contexts.build + context.name = params['context_name'].strip + context.save + @new_context_created = true + end + params["recurring_todo"]["context_id"] = context.id + end + + # make sure that we set weekly_return_xxx to empty (space) when they are + # not checked (and thus not present in params["recurring_todo"]) + %w{monday tuesday wednesday thursday friday saturday sunday}.each do |day| + params["recurring_todo"]["weekly_return_"+day]=' ' if params["recurring_todo"]["weekly_return_"+day].nil? + end + + @saved = @recurring_todo.update_attributes params["recurring_todo"] + + respond_to do |format| + format.js + end + end + + def create + p = RecurringTodoCreateParamsHelper.new(params) + p.attributes['end_date']=parse_date_per_user_prefs(p.attributes['end_date']) + p.attributes['start_from']=parse_date_per_user_prefs(p.attributes['start_from']) + + @recurring_todo = current_user.recurring_todos.build(p.selector_attributes) + @recurring_todo.update_attributes(p.attributes) + + if p.project_specified_by_name? + project = current_user.projects.find_or_create_by_name(p.project_name) + @new_project_created = project.new_record_before_save? + @recurring_todo.project_id = project.id + end + + if p.context_specified_by_name? + context = current_user.contexts.find_or_create_by_name(p.context_name) + @new_context_created = context.new_record_before_save? + @recurring_todo.context_id = context.id + end + + @recurring_saved = @recurring_todo.save + unless (@recurring_saved == false) || p.tag_list.blank? + @recurring_todo.tag_with(p.tag_list) + @recurring_todo.tags.reload + end + + if @recurring_saved + @message = "The recurring todo was saved" + @todo_saved = create_todo_from_recurring_todo(@recurring_todo).nil? == false + if @todo_saved + @message += " / created a new todo" + else + @message += " / did not create todo" + end + @count = current_user.recurring_todos.active.count + else + @message = "Error saving recurring todo" + end + + respond_to do |format| + format.js + end + end + + def destroy + + # remove all references to this recurring todo + @todos = @recurring_todo.todos + @number_of_todos = @todos.size + @todos.each do |t| + t.recurring_todo_id = nil + t.save + end + + # delete the recurring todo + @saved = @recurring_todo.destroy + @remaining = current_user.recurring_todos.count + + respond_to do |format| + + format.html do + if @saved + notify :notice, "Successfully deleted recurring action", 2.0 + redirect_to :action => 'index' + else + notify :error, "Failed to delete the recurring action", 2.0 + redirect_to :action => 'index' + end + end + + format.js do + render + end + end + end + + def toggle_check + @saved = @recurring_todo.toggle_completion! + + @count = current_user.recurring_todos.active.count + @remaining = @count + + if @recurring_todo.active? + @remaining = current_user.recurring_todos.completed.count + + # from completed back to active -> check if there is an active todo + # current_user.todos.count(:all, {:conditions => ["state = ? AND recurring_todo_id = ?", 'active',params[:id]]}) + @active_todos = @recurring_todo.todos.active.count + # create todo if there is no active todo belonging to the activated + # recurring_todo + @new_recurring_todo = create_todo_from_recurring_todo(@recurring_todo) if @active_todos == 0 + end + + respond_to do |format| + format.js + end + end + + def toggle_star + @recurring_todo.toggle_star! + @saved = @recurring_todo.save! + respond_to do |format| + format.js + end + end + + class RecurringTodoCreateParamsHelper + + def initialize(params) + @params = params['request'] || params + @attributes = params['request'] && params['request']['recurring_todo'] || params['recurring_todo'] + + # make sure all selectors (recurring_period, recurrence_selector, + # daily_selector, monthly_selector and yearly_selector) are first in hash + # so that they are processed first by the model + @selector_attributes = { + 'recurring_period' => @attributes['recurring_period'], + 'daily_selector' => @attributes['daily_selector'], + 'monthly_selector' => @attributes['monthly_selector'], + 'yearly_selector' => @attributes['yearly_selector'] + } + end + + def attributes + @attributes + end + + def selector_attributes + return @selector_attributes + end + + def project_name + @params['project_name'].strip unless @params['project_name'].nil? + end + + def context_name + @params['context_name'].strip unless @params['context_name'].nil? + end + + def tag_list + @params['tag_list'] + end + + def project_specified_by_name? + return false unless @attributes['project_id'].blank? + return false if project_name.blank? + return false if project_name == 'None' + true + end + + def context_specified_by_name? + return false unless @attributes['context_id'].blank? + return false if context_name.blank? + true + end + + end + + private + + def init + @days_of_week = [ ['Sunday',0], ['Monday',1], ['Tuesday', 2], ['Wednesday',3], ['Thursday',4], ['Friday',5], ['Saturday',6]] + @months_of_year = [ + ['January',1], ['Februari',2], ['March', 3], ['April',4], ['May',5], ['June',6], + ['July',7], ['August',8], ['September',9], ['October', 10], ['November', 11], ['December',12]] + @xth_day = [['first',1],['second',2],['third',3],['fourth',4],['last',5]] + @projects = current_user.projects.find(:all, :include => [:default_context]) + @contexts = current_user.contexts.find(:all) + @default_project_context_name_map = build_default_project_context_name_map(@projects).to_json + end + + def get_recurring_todo_from_param + @recurring_todo = current_user.recurring_todos.find(params[:id]) + end + + def find_and_inactivate + # find active recurring todos without active todos and inactivate them + recurring_todos = current_user.recurring_todos.active + recurring_todos.each { |rt| rt.toggle_completion! if rt.todos.not_completed.count == 0} + end + +end \ No newline at end of file diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 0c604533..872b4fdd 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -14,8 +14,8 @@ class SearchController < ApplicationController @found_tags = Tagging.find_by_sql([ "SELECT DISTINCT tags.name as name "+ "FROM tags "+ - "LEFT JOIN taggings ON tags.id = taggings.tag_id "+ - "LEFT JOIN todos ON taggings.taggable_id = todos.id "+ + "LEFT JOIN taggings ON tags.id = taggings.tag_id "+ + "LEFT JOIN todos ON taggings.taggable_id = todos.id "+ "WHERE todos.user_id=? "+ "AND tags.name LIKE ? ", current_user.id, terms]) diff --git a/app/models/todo.rb b/app/models/todo.rb index 5f231221..d608e56f 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -6,8 +6,8 @@ class Todo < ActiveRecord::Base belongs_to :recurring_todo named_scope :active, :conditions => { :state => 'active' } - named_scope :not_completed, :conditions => ['NOT state = ? ', 'completed'] - named_scope :are_due, :conditions => ['NOT todos.due IS NULL'] + named_scope :not_completed, :conditions => ['NOT (state = ? )', 'completed'] + named_scope :are_due, :conditions => ['NOT (todos.due IS NULL)'] STARRED_TAG_NAME = "starred" diff --git a/app/models/user.rb b/app/models/user.rb index f2ecedb8..01f4bf0f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -59,7 +59,7 @@ class User < ActiveRecord::Base "SELECT project.id, count(todo.id) as p_count " + "FROM projects as project " + "LEFT OUTER JOIN todos as todo ON todo.project_id = project.id "+ - "WHERE project.user_id = ? AND NOT todo.state='completed' " + + "WHERE project.user_id = ? AND NOT (todo.state='completed') " + query_state + " GROUP BY project.id ORDER by p_count DESC",user_id]) self.update_positions(projects.map{ |p| p.id }) @@ -91,7 +91,7 @@ class User < ActiveRecord::Base end has_many :completed_todos, :class_name => 'Todo', - :conditions => ['todos.state = ? and todos.completed_at is not null', 'completed'], + :conditions => ['todos.state = ? AND NOT(todos.completed_at IS NULL)', 'completed'], :order => 'todos.completed_at DESC', :include => [ :project, :context ] do def completed_within( date ) diff --git a/db/migrate/043_add_updated_at_to_todos.rb b/db/migrate/043_add_updated_at_to_todos.rb index ec14720a..abf8dc97 100644 --- a/db/migrate/043_add_updated_at_to_todos.rb +++ b/db/migrate/043_add_updated_at_to_todos.rb @@ -2,7 +2,7 @@ class AddUpdatedAtToTodos < ActiveRecord::Migration def self.up add_column :todos, :updated_at, :timestamp execute 'update todos set updated_at = created_at where completed_at IS NULL' - execute 'update todos set updated_at = completed_at where NOT completed_at IS NULL' + execute 'update todos set updated_at = completed_at where NOT (completed_at IS NULL)' end def self.down remove_column :todos, :updated_at diff --git a/db/tracks-17-blank.db b/db/tracks-17-blank.db index 1e6d759a..99987ec9 100644 Binary files a/db/tracks-17-blank.db and b/db/tracks-17-blank.db differ diff --git a/db/tracks-17-example.db b/db/tracks-17-example.db index d70581a9..149781c5 100644 Binary files a/db/tracks-17-example.db and b/db/tracks-17-example.db differ diff --git a/public/stylesheets/print.css b/public/stylesheets/print.css index b99633e8..156069f5 100644 --- a/public/stylesheets/print.css +++ b/public/stylesheets/print.css @@ -6,8 +6,6 @@ body { background: #fff; color: #000; -/* width: 2.5in; - height: 4.7in;*/ font-size: 8.2pt; font-family: "Lucida Grande", "Bitstream Vera Sans", Helvetica, Verdana, Arial, sans-serif; } @@ -18,7 +16,7 @@ img { border:0; } -#navcontainer, #input_box, #footer, .big-box, .refresh, .badge, h1, .icon, #minilinks { +#navcontainer, #input_box, #footer, .big-box, .refresh, .badge, h1, .icon, #minilinks, .defer-container { display:none; } diff --git a/public/stylesheets/standard.css b/public/stylesheets/standard.css index ea8f0dc5..411deda2 100644 --- a/public/stylesheets/standard.css +++ b/public/stylesheets/standard.css @@ -76,31 +76,31 @@ h3 { /* Rules for the icon links */ -img.edit_item {background-image: url(../images/edit_off.png); background-repeat: no-repeat; border: none;} -a:hover img.edit_item {background-image: url(../images/edit_on.png); background-color: transparent; background-repeat: no-repeat; border: none;} +img.edit_item {background-image: url(/images/edit_off.png); background-repeat: no-repeat; border: none;} +a:hover img.edit_item {background-image: url(/images/edit_on.png); background-color: transparent; background-repeat: no-repeat; border: none;} -img.delete_item {background-image: url(../images/delete_off.png); background-repeat: no-repeat; border: none;} -a:hover img.delete_item {background-image: url(../images/delete_on.png);background-color: transparent;background-repeat: no-repeat; border: none;} +img.delete_item {background-image: url(/images/delete_off.png); background-repeat: no-repeat; border: none;} +a:hover img.delete_item {background-image: url(/images/delete_on.png);background-color: transparent;background-repeat: no-repeat; border: none;} -img.starred_todo {background-image: url(../images/staricons.png); background-repeat: no-repeat; border:none; background-position: 0px 0px;} -a:hover img.starred_todo {background-image: url(../images/staricons.png); background-repeat: no-repeat; border:none; background-position: -16px 0px;} -img.unstarred_todo {background-image: url(../images/staricons.png); background-repeat: no-repeat; border:none; background-position: -32px 0px;} -a:hover img.unstarred_todo {background-image: url(../images/staricons.png); background-repeat: no-repeat; border:none; background-position: -48px 0px;} +img.starred_todo {background-image: url(/images/staricons.png); background-repeat: no-repeat; border:none; background-position: 0px 0px;} +a:hover img.starred_todo {background-image: url(/images/staricons.png); background-repeat: no-repeat; border:none; background-position: -16px 0px;} +img.unstarred_todo {background-image: url(/images/staricons.png); background-repeat: no-repeat; border:none; background-position: -32px 0px;} +a:hover img.unstarred_todo {background-image: url(/images/staricons.png); background-repeat: no-repeat; border:none; background-position: -48px 0px;} -a.to_top {background: transparent url(../images/top_off.png) no-repeat;} -a.to_top:hover {background: transparent url(../images/top_on.png) no-repeat;} +a.to_top {background: transparent url(/images/top_off.png) no-repeat;} +a.to_top:hover {background: transparent url(/images/top_on.png) no-repeat;} -a.up {background: transparent url(../images/up_off.png) no-repeat;} -a.up:hover {background: transparent url(../images/up_on.png) no-repeat;} +a.up {background: transparent url(/images/up_off.png) no-repeat;} +a.up:hover {background: transparent url(/images/up_on.png) no-repeat;} -a.down {background: transparent url(../images/down_off.png) no-repeat;} -a.down:hover {background: transparent url(../images/down_on.png) no-repeat;} +a.down {background: transparent url(/images/down_off.png) no-repeat;} +a.down:hover {background: transparent url(/images/down_on.png) no-repeat;} -a.to_bottom {background: transparent url(../images/bottom_off.png) no-repeat;} -a.to_bottom:hover {background: transparent url(../images/bottom_on.png) no-repeat;} +a.to_bottom {background: transparent url(/images/bottom_off.png) no-repeat;} +a.to_bottom:hover {background: transparent url(/images/bottom_on.png) no-repeat;} -a.show_notes, a.link_to_notes {background-image: url(../images/notes_off.png); background-repeat: no-repeat; padding: 1px; background-color: transparent;} -a.show_notes:hover, a.link_to_notes:hover {background-image: url(../images/notes_on.png); background-repeat: no-repeat; padding: 1px; background-color: transparent;} +a.show_notes, a.link_to_notes {background-image: url(/images/notes_off.png); background-repeat: no-repeat; padding: 1px; background-color: transparent;} +a.show_notes:hover, a.link_to_notes:hover {background-image: url(/images/notes_on.png); background-repeat: no-repeat; padding: 1px; background-color: transparent;} /* Structural divs */ @@ -180,7 +180,7 @@ a.show_notes:hover, a.link_to_notes:hover {background-image: url(../images/notes height: 100%; z-index: 102; text-align: center; - background-image:url("../images/trans70.png"); + background-image:url("/images/trans70.png"); } #overlay #new-recurring-todo, #overlay #edit-recurring-todo { @@ -997,7 +997,7 @@ ul#prefs {list-style-type: disc; margin-left: 15px;} font-style:oblique; } input.open_id { - background: url(../images/open-id-login-bg.gif) no-repeat; + background: url(/images/open-id-login-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; color: #000; @@ -1146,28 +1146,28 @@ button.positive, .widgets a.positive{ color:#fff; } .tracks__waiting { - background-image:url('../images/waiting.gif'); + background-image:url('/images/waiting.gif'); background-repeat:no-repeat; background-position:center center; background-color:white; } .bigWaiting { - background-image:url('../images/bigWaiting.gif'); + background-image:url('/images/bigWaiting.gif'); background-repeat:no-repeat; background-position:center 20%; background-color:white; } .blackWaiting { - background-image:url('../images/blackWaiting.gif'); + background-image:url('/images/blackWaiting.gif'); background-repeat:no-repeat; background-position:center center; background-color:black; } .bigBlackWaiting { - background-image:url('../images/bigBlackWaiting.gif'); + background-image:url('/images/bigBlackWaiting.gif'); background-repeat:no-repeat; background-position:center center; background-color:black; diff --git a/test/fixtures/recurring_todos.yml b/test/fixtures/recurring_todos.yml index 81c3e670..a6e80320 100644 --- a/test/fixtures/recurring_todos.yml +++ b/test/fixtures/recurring_todos.yml @@ -1,3 +1,27 @@ +<% + +def today + Time.zone.now.beginning_of_day.to_s(:db) +end + +def next_week + 1.week.from_now.beginning_of_day.to_s(:db) +end + +def last_week + 1.week.ago.beginning_of_day.to_s(:db) +end + +def two_weeks_ago + 2.weeks.ago.beginning_of_day.to_s(:db) +end + +def two_weeks_hence + 2.weeks.from_now.beginning_of_day.to_s(:db) +end + +%> + 1: id: 1 user_id: 1 diff --git a/test/functional/data_controller_test.rb b/test/functional/data_controller_test.rb index 3ac79cec..4084ac59 100644 --- a/test/functional/data_controller_test.rb +++ b/test/functional/data_controller_test.rb @@ -18,4 +18,9 @@ class DataControllerTest < Test::Rails::TestCase login_as :admin_user get :csv_notes end + + def test_yml_export_comleted_without_error + login_as :admin_user + get :yaml_export + end end diff --git a/test/selenium/home/create_first_todo.rsel b/test/selenium/home/create_first_todo.rsel index 53f57824..05b323e1 100644 --- a/test/selenium/home/create_first_todo.rsel +++ b/test/selenium/home/create_first_todo.rsel @@ -5,4 +5,5 @@ assert_context_count_incremented do type "todo_description", "a new action" type "todo_context_name", "Brand new context" click "css=#todo-form-new-action .submit_box button" -end \ No newline at end of file + assert_confirmation "New context \"Brand new context\" will be also created. Are you sure?" +end diff --git a/test/selenium/home/create_new_todo_with_new_action_and_new_context.rsel b/test/selenium/home/create_new_todo_with_new_action_and_new_context.rsel index 63a266d6..97acb99f 100644 --- a/test/selenium/home/create_new_todo_with_new_action_and_new_context.rsel +++ b/test/selenium/home/create_new_todo_with_new_action_and_new_context.rsel @@ -6,4 +6,5 @@ assert_context_count_incremented do type "todo_project_name", "pppp" type "todo_context_name", "cccc" click "css=#todo-form-new-action .submit_box button" -end \ No newline at end of file + assert_confirmation "New context \"cccc\" will be also created. Are you sure?" +end diff --git a/test/selenium/home/create_new_todo_with_new_context.rsel b/test/selenium/home/create_new_todo_with_new_context.rsel index 658922e4..23924577 100644 --- a/test/selenium/home/create_new_todo_with_new_context.rsel +++ b/test/selenium/home/create_new_todo_with_new_context.rsel @@ -5,4 +5,5 @@ assert_context_count_incremented do type "todo_description", "a new action" type "todo_context_name", "Brand new context" click "css=#todo-form-new-action .submit_box button" -end \ No newline at end of file + assert_confirmation "New context \"Brand new context\" will be also created. Are you sure?" +end diff --git a/test/selenium/tickler/create_deferred_todo_with_existing_context.rsel b/test/selenium/tickler/create_deferred_todo_with_existing_context.rsel index 8e7df405..09fb6bc2 100644 --- a/test/selenium/tickler/create_deferred_todo_with_existing_context.rsel +++ b/test/selenium/tickler/create_deferred_todo_with_existing_context.rsel @@ -7,6 +7,7 @@ assert_context_count_incremented do type "todo_project_name", "None" type "todo_show_from", "1/1/2030" click "css=#todo-form-new-action .submit_box button" + assert_confirmation "New context \"errands\" will be also created. Are you sure?" end wait_for_not_visible "tickler-empty-nd" wait_for_element_present "xpath=//div[@class='item-container'] //a[@title='01/01/2030']" diff --git a/test/selenium/tickler/create_deferred_todo_with_new_context.rsel b/test/selenium/tickler/create_deferred_todo_with_new_context.rsel index caecbff0..37cac4f1 100644 --- a/test/selenium/tickler/create_deferred_todo_with_new_context.rsel +++ b/test/selenium/tickler/create_deferred_todo_with_new_context.rsel @@ -6,4 +6,5 @@ assert_context_count_incremented do type "todo_context_name", "Brand new context" type "todo_show_from", "1/1/2030" click "css=#todo-form-new-action .submit_box button" + assert_confirmation "New context \"Brand new context\" will be also created. Are you sure?" end diff --git a/vendor/plugins/bundle-fu/lib/bundle_fu.rb b/vendor/plugins/bundle-fu/lib/bundle_fu.rb index 5991b97a..8cc20b41 100644 --- a/vendor/plugins/bundle-fu/lib/bundle_fu.rb +++ b/vendor/plugins/bundle-fu/lib/bundle_fu.rb @@ -1,146 +1,147 @@ -class BundleFu - - class << self - attr_accessor :content_store - def init - @content_store = {} - end - - def bundle_files(filenames=[]) - output = "" - filenames.each{ |filename| - output << "/* --------- #{filename} --------- */ " - output << "\n" - begin - content = (File.read(File.join(RAILS_ROOT, "public", filename))) - rescue - output << "/* FILE READ ERROR! */" - next - end - - output << (yield(filename, content)||"") - } - output - end - - def bundle_js_files(filenames=[], options={}) - output = - bundle_files(filenames) { |filename, content| - if options[:compress] - if Object.const_defined?("Packr") - content - else - JSMinimizer.minimize_content(content) - end - else - content - end - } - - if Object.const_defined?("Packr") - # use Packr plugin (http://blog.jcoglan.com/packr/) - Packr.new.pack(output, options[:packr_options] || {:shrink_vars => false, :base62 => false}) - else - output - end - - end - - def bundle_css_files(filenames=[], options = {}) - bundle_files(filenames) { |filename, content| - BundleFu::CSSUrlRewriter.rewrite_urls(filename, content) - } - end - end - - self.init - - module InstanceMethods - # valid options: - # :name - The name of the css and js files you wish to output - # returns true if a regen occured. False if not. - def bundle(options={}, &block) - # allow bypassing via the querystring - session[:bundle_fu] = (params[:bundle_fu]=="true") if params.has_key?(:bundle_fu) - - options = { - :css_path => ($bundle_css_path || "/stylesheets/cache"), - :js_path => ($bundle_js_path || "/javascripts/cache"), - :name => ($bundle_default_name || "bundle"), - :compress => true, - :bundle_fu => ( session[:bundle_fu].nil? ? ($bundle_fu.nil? ? true : $bundle_fu) : session[:bundle_fu] ) - }.merge(options) - - # allow them to bypass via parameter - options[:bundle_fu] = false if options[:bypass] - - paths = { :css => options[:css_path], :js => options[:js_path] } - - content = capture(&block) - content_changed = false - - new_files = nil - abs_filelist_paths = [:css, :js].inject({}) { | hash, filetype | hash[filetype] = File.join(RAILS_ROOT, "public", paths[filetype], "#{options[:name]}.#{filetype}.filelist"); hash } - - # only rescan file list if content_changed, or if a filelist cache file is missing - unless content == BundleFu.content_store[options[:name]] && File.exists?(abs_filelist_paths[:css]) && File.exists?(abs_filelist_paths[:js]) - BundleFu.content_store[options[:name]] = content - new_files = {:js => [], :css => []} - - content.scan(/(href|src) *= *["']([^"^'^\?]+)/i).each{ |property, value| - case property - when "src" - new_files[:js] << value - when "href" - new_files[:css] << value - end - } - end - - [:css, :js].each { |filetype| - output_filename = File.join(paths[filetype], "#{options[:name]}.#{filetype}") - abs_path = File.join(RAILS_ROOT, "public", output_filename) - abs_filelist_path = abs_filelist_paths[filetype] - - filelist = FileList.open( abs_filelist_path ) - - # check against newly parsed filelist. If we didn't parse the filelist from the output, then check against the updated mtimes. - new_filelist = new_files ? BundleFu::FileList.new(new_files[filetype]) : filelist.clone.update_mtimes - - unless new_filelist == filelist - FileUtils.mkdir_p(File.join(RAILS_ROOT, "public", paths[filetype])) - # regenerate everything - if new_filelist.filenames.empty? - # delete the javascript/css bundle file if it's empty, but keep the filelist cache - FileUtils.rm_f(abs_path) - else - # call bundle_css_files or bundle_js_files to bundle all files listed. output it's contents to a file - output = BundleFu.send("bundle_#{filetype}_files", new_filelist.filenames, options) - File.open( abs_path, "w") {|f| f.puts output } if output - end - new_filelist.save_as(abs_filelist_path) - end - +class BundleFu + + class << self + attr_accessor :content_store + def init + @content_store = {} + end + + def bundle_files(filenames=[]) + output = "" + filenames.each{ |filename| + filename_no_root = filename.sub(/^#{ActionController::Base.relative_url_root}/, '') + output << "/* --------- #{filename} - #{filename_no_root} --- ------ */ " + output << "\n" + begin + content = (File.read(File.join(RAILS_ROOT, "public", filename_no_root))) + rescue + output << "/* FILE READ ERROR! */" + next + end + + output << (yield(filename, content)||"") + } + output + end + + def bundle_js_files(filenames=[], options={}) + output = + bundle_files(filenames) { |filename, content| + if options[:compress] + if Object.const_defined?("Packr") + content + else + JSMinimizer.minimize_content(content) + end + else + content + end + } + + if Object.const_defined?("Packr") + # use Packr plugin (http://blog.jcoglan.com/packr/) + Packr.new.pack(output, options[:packr_options] || {:shrink_vars => false, :base62 => false}) + else + output + end + + end + + def bundle_css_files(filenames=[], options = {}) + bundle_files(filenames) { |filename, content| + BundleFu::CSSUrlRewriter.rewrite_urls(filename, content) + } + end + end + + self.init + + module InstanceMethods + # valid options: + # :name - The name of the css and js files you wish to output + # returns true if a regen occured. False if not. + def bundle(options={}, &block) + # allow bypassing via the querystring + session[:bundle_fu] = (params[:bundle_fu]=="true") if params.has_key?(:bundle_fu) + + options = { + :css_path => ($bundle_css_path || "/stylesheets/cache"), + :js_path => ($bundle_js_path || "/javascripts/cache"), + :name => ($bundle_default_name || "bundle"), + :compress => true, + :bundle_fu => ( session[:bundle_fu].nil? ? ($bundle_fu.nil? ? true : $bundle_fu) : session[:bundle_fu] ) + }.merge(options) + + # allow them to bypass via parameter + options[:bundle_fu] = false if options[:bypass] + + paths = { :css => options[:css_path], :js => options[:js_path] } + + content = capture(&block) + content_changed = false + + new_files = nil + abs_filelist_paths = [:css, :js].inject({}) { | hash, filetype | hash[filetype] = File.join(RAILS_ROOT, "public", paths[filetype], "#{options[:name]}.#{filetype}.filelist"); hash } + + # only rescan file list if content_changed, or if a filelist cache file is missing + unless content == BundleFu.content_store[options[:name]] && File.exists?(abs_filelist_paths[:css]) && File.exists?(abs_filelist_paths[:js]) + BundleFu.content_store[options[:name]] = content + new_files = {:js => [], :css => []} + + content.scan(/(href|src) *= *["']([^"^'^\?]+)/i).each{ |property, value| + case property + when "src" + new_files[:js] << value + when "href" + new_files[:css] << value + end + } + end + + [:css, :js].each { |filetype| + output_filename = File.join(paths[filetype], "#{options[:name]}.#{filetype}") + abs_path = File.join(RAILS_ROOT, "public", output_filename) + abs_filelist_path = abs_filelist_paths[filetype] + + filelist = FileList.open( abs_filelist_path ) + + # check against newly parsed filelist. If we didn't parse the filelist from the output, then check against the updated mtimes. + new_filelist = new_files ? BundleFu::FileList.new(new_files[filetype]) : filelist.clone.update_mtimes + + unless new_filelist == filelist + FileUtils.mkdir_p(File.join(RAILS_ROOT, "public", paths[filetype])) + # regenerate everything + if new_filelist.filenames.empty? + # delete the javascript/css bundle file if it's empty, but keep the filelist cache + FileUtils.rm_f(abs_path) + else + # call bundle_css_files or bundle_js_files to bundle all files listed. output it's contents to a file + output = BundleFu.send("bundle_#{filetype}_files", new_filelist.filenames, options) + File.open( abs_path, "w") {|f| f.puts output } if output + end + new_filelist.save_as(abs_filelist_path) + end + if File.exists?(abs_path) && options[:bundle_fu] tag = filetype==:css ? stylesheet_link_tag(output_filename) : javascript_include_tag(output_filename) - if Rails::version < "2.2.0" + if Rails::version < "2.2.0" concat( tag , block.binding) else #concat doesn't need block.binding in Rails >= 2.2.0 concat( tag ) - end - - end - } - + end + + end + } + unless options[:bundle_fu] - if Rails::version < "2.2.0" + if Rails::version < "2.2.0" concat( content, block.binding ) else #concat doesn't need block.binding in Rails >= 2.2.0 concat( content ) - end - end - end - end + end + end + end + end end diff --git a/vendor/plugins/bundle-fu/lib/bundle_fu/css_url_rewriter.rb b/vendor/plugins/bundle-fu/lib/bundle_fu/css_url_rewriter.rb index 8df79654..d048a738 100644 --- a/vendor/plugins/bundle-fu/lib/bundle_fu/css_url_rewriter.rb +++ b/vendor/plugins/bundle-fu/lib/bundle_fu/css_url_rewriter.rb @@ -1,12 +1,16 @@ class BundleFu::CSSUrlRewriter class << self # rewrites a relative path to an absolute path, removing excess "../" and "./" - # rewrite_relative_path("stylesheets/default/global.css", "../image.gif") => "/stylesheets/image.gif" + # rewrite_relative_path("stylesheets/default/global.css", "../image.gif") => "#{rails_relative_root}/stylesheets/image.gif" def rewrite_relative_path(source_filename, relative_url) relative_url = relative_url.to_s.strip.gsub(/["']/, "") - return relative_url if relative_url.first == "/" || relative_url.include?("://") - + return relative_url if relative_url.include?("://") + if ( relative_url.first == "/" ) + return relative_url unless ActionController::Base.relative_url_root + return "#{ActionController::Base.relative_url_root}#{relative_url}" + end + elements = File.join("/", File.dirname(source_filename)).gsub(/\/+/, '/').split("/") elements += relative_url.gsub(/\/+/, '/').split("/") @@ -24,7 +28,9 @@ class BundleFu::CSSUrlRewriter end end - elements * "/" + path = elements * "/" + return path unless ActionController::Base.relative_url_root + "#{ActionController::Base.relative_url_root}#{path}" end # rewrite the URL reference paths @@ -39,4 +45,4 @@ class BundleFu::CSSUrlRewriter end end -end \ No newline at end of file +end