TracksApp/tracks

View on GitHub
app/controllers/todos_controller.rb

Summary

Maintainability
F
4 days
Test Coverage
class TodosController < ApplicationController
  skip_before_action :login_required, :only => [:index, :tag, :list_deferred, :show, :list_hidden, :done]
  prepend_before_action :login_or_feed_token_required, :only => [:index, :tag, :list_deferred, :show, :list_hidden, :done]
  append_before_action :find_and_activate_ready, :only => [:index, :list_deferred]

  protect_from_forgery :except => :check_deferred

  def index
    @source_view = params['_source_view'] || 'todo'

    init_data_for_sidebar unless mobile?

    @todos = current_user.todos.includes(Todo::DEFAULT_INCLUDES)
    if params[:context_id]
      context = current_user.contexts.find(params[:context_id])
      @todos = @todos.where('context_id' => context.id)
    end
    if params[:project_id]
      project = current_user.projects.find(params[:project_id])
      @todos = @todos.where('project_id' => project.id)
    end
    @todos = @todos.limit(sanitize(params[:limit])) if params[:limit]

    @not_done_todos = get_not_done_todos

    @projects = current_user.projects.includes(:default_context)
    @contexts = current_user.contexts
    @contexts_to_show = current_user.contexts.active
    @projects_to_show = current_user.projects.active

    # If you've set no_completed to zero, the completed items box isn't shown
    # on the home page
    max_completed = current_user.prefs.show_number_completed
    @done = current_user.todos.completed.limit(max_completed).includes(Todo::DEFAULT_INCLUDES) unless max_completed == 0

    respond_to do |format|
      format.html do
        @page_title = t('todos.task_list_title')
        # Set count badge to number of not-done, not hidden context items
        @count = current_user.todos.active.not_hidden.count(:all)
        @todos_without_project = @not_done_todos.select { |t| t.project.nil? }
      end
      format.m do
        @page_title = t('todos.mobile_todos_page_title')
        @home = true

        cookies[:mobile_url] = { :value => request.fullpath, :secure => SITE_CONFIG['secure_cookies'] }
        determine_down_count

        render :action => 'index'.freeze
      end
      format.text do
        # somehow passing Mime[:text] using content_type to render does not work
        headers['Content-Type'.freeze] = Mime[:text].to_s
        render :content_type => Mime[:text]
      end
      format.xml do
        @xml_todos = params[:limit_to_active_todos] ? @not_done_todos : @todos
        render :xml => @xml_todos.to_xml(*[todo_xml_params[0].merge({ :root => :todos })])
      end
      format.any(:rss, :atom) do
        @feed_title = 'Tracks Actions'.freeze
        @feed_description = "Actions for #{current_user.display_name}"
      end
      format.ics
    end
  end

  def new
    @projects = current_user.projects.active
    @contexts = current_user.contexts
    respond_to do |format|
      format.m do
        @new_mobile = true
        @return_path = cookies[:mobile_url] ? cookies[:mobile_url] : mobile_path
        @mobile_from_context = current_user.contexts.find(params[:from_context]) if params[:from_context]
        @mobile_from_project = current_user.projects.find(params[:from_project]) if params[:from_project]
        if params[:from_project] && !params[:from_context]
          # we have a project but not a context -> use the default context
          @mobile_from_context = @mobile_from_project.default_context
        end
      end
    end
  end

  def create
    @source_view = params['_source_view'] || 'todo'
    @default_context = current_user.contexts.where(:name => params['default_context_name']).first
    @default_project = current_user.projects.where(:name => params['default_project_name']).first if params['default_project_name'].present?

    @tag_name = params['_tag_name']

    is_multiple = params[:todo] && params[:todo][:multiple_todos] && !params[:todo][:multiple_todos].nil?
    if is_multiple
      create_multiple
    else
      p = Todos::TodoCreateParamsHelper.new(params, current_user)
      p.parse_dates unless mobile?
      tag_list = p.tag_list

      @todo = current_user.todos.build
      @todo.assign_attributes(p.attributes)
      p.add_errors(@todo)

      if @todo.errors.empty?
        @todo.add_predecessor_list(p.predecessor_list)
        @saved = @todo.save
        @todo.tag_with(tag_list) if @saved && tag_list.present?
        @todo.block! if @todo.uncompleted_predecessors?
      else
        @saved = false
      end

      @todo_was_created_deferred = @todo.deferred?
      @todo_was_created_blocked = @todo.pending?
      @not_done_todos = [@todo] if p.new_project_created || p.new_context_created
      @new_project_created = p.new_project_created
      @new_context_created = p.new_context_created

      respond_to do |format|
        format.html do
          redirect_to :action => "index"
        end
        format.m do
          @return_path = cookies[:mobile_url] ? cookies[:mobile_url] : mobile_path
          if @saved
            onsite_redirect_to @return_path
          else
            @projects = current_user.projects
            @contexts = current_user.contexts
            render :action => "new"
          end
        end
        format.js do
          if @saved
            determine_down_count
            @contexts = current_user.contexts
            @projects = current_user.projects
            @context = @todo.context
            @project = @todo.project
            @initial_context_name = params['default_context_name']
            @initial_project_name = params['default_project_name']
            @initial_tags = params['initial_tag_list']
            @status_message = t('todos.added_new_next_action')
            @status_message += ' ' + t('todos.to_tickler') if @todo.deferred?
            @status_message += ' ' + t('todos.in_pending_state') if @todo.pending?
            @status_message += ' ' + t('todos.in_hidden_state') if @todo.hidden?
            @status_message = t('todos.added_new_project') + ' / ' + @status_message if @new_project_created
            @status_message = t('todos.added_new_context') + ' / ' + @status_message if @new_context_created
          end
          render :action => 'create'
        end
        format.xml do
          if @saved
            head :created, :location => todo_url(@todo)
          else
            render_failure @todo.errors.full_messages.to_xml(root: "errors", skip_types: true).html_safe, 409
          end
        end
      end
    end
  end

  def create_multiple
    p = Todos::TodoCreateParamsHelper.new(params, current_user)
    tag_list = p.tag_list

    @not_done_todos = []
    @build_todos = []
    @todos = []
    errors = []
    @predecessor = nil
    validates = true

    # first build all todos and check if they would validate on save
    params[:todo][:multiple_todos].split("\n").map do |line|
      # Ignore blank lines
      if line.present?
        @todo = current_user.todos.build({ :description => line, :context_id => p.context_id, :project_id => p.project_id })
        validates &&= @todo.valid?

        @build_todos << @todo
      end
    end

    # if all todos validate, then save them and add predecessors and tags
    if validates
      @build_todos.each do |todo|
        @saved = todo.save
        validates &&= @saved

        if @predecessor && @saved && p.sequential?
          todo.add_predecessor(@predecessor)
          todo.block!
        end

        todo.tag_with(tag_list) if @saved && tag_list.present?

        @todos << todo
        @not_done_todos << todo if p.new_context_created || p.new_project_created
        @predecessor = todo
      end
    else
      @todos = @build_todos
      @saved = false
    end

    respond_to do |format|
      format.html { redirect_to :action => "index" }
      format.js do
        determine_down_count if @saved
        @contexts = current_user.contexts if p.new_context_created
        @projects = current_user.projects if p.new_project_created
        @new_project_created = p.new_project_created
        @new_context_created = p.new_context_created
        @initial_context_name = params['default_context_name']
        @initial_project_name = params['default_project_name']
        @initial_tags = params['initial_tag_list']
        if @saved && @todos.size > 0
          @default_tags = @todos[0].project.default_tags unless @todos[0].project.nil?
        else
          @multiple_error = @todos.size > 0 ? "" : t('todos.next_action_needed')
          @saved = false
          @default_tags = current_user.projects.where(:name => @initial_project_name).default_tags if @initial_project_name.present?
        end

        @status_message = @todos.size > 1 ? t('todos.added_new_next_action_plural') : t('todos.added_new_next_action_singular')
        @status_message = t('todos.added_new_project') + ' / ' + @status_message if p.new_project_created
        @status_message = t('todos.added_new_context') + ' / ' + @status_message if p.new_context_created

        render :action => 'create_multiple'
      end
      format.xml do
        if @saved
          head :created, :location => context_url(@todos[0].context)
        else
          render :xml => @todos[0].errors.to_xml, :status => 422
        end
      end
    end
  end

  def edit
    @todo = current_user.todos.find(params['id'])
    @source_view = params['_source_view'] || 'todo'
    @tag_name = params['_tag_name']
    respond_to do |format|
      format.js
      format.m do
        @projects = current_user.projects.active
        @contexts = current_user.contexts
        @edit_mobile = true
        @return_path = cookies[:mobile_url] ? cookies[:mobile_url] : mobile_path
      end
    end
  end

  def show
    @todo = current_user.todos.find(params['id'])
    respond_to do |format|
      format.m { render :action => 'show' }
      format.xml { render :xml => @todo.to_xml(*[todo_xml_params[0].merge({ :root => :todo })]) }
    end
  end

  def add_predecessor
    @source_view = params['_source_view'] || 'todo'
    @predecessor = current_user.todos.find(params['predecessor'])
    @predecessors = @predecessor.predecessors
    @todo = current_user.todos.includes(Todo::DEFAULT_INCLUDES).find(params['successor'])
    @original_state = @todo.state
    unless @predecessor.completed?
      begin
        @todo.add_predecessor(@predecessor)
        @todo.block! unless @todo.pending?
        @saved = @todo.save
      rescue ActiveRecord::RecordInvalid
        @saved = false
      end

      @status_message = t('todos.added_dependency', :dependency => @predecessor.description)
      @status_message += ' ' + t('todos.set_to_pending', :task => @todo.description) unless @original_state == 'pending'
    else
      @saved = false
    end
    respond_to do |format|
      format.js
    end
  end

  def remove_predecessor
    @source_view = params['_source_view'] || 'todo'
    @todo = current_user.todos.includes(Todo::DEFAULT_INCLUDES).find(params['id'])
    @predecessor = current_user.todos.find(params['predecessor'])
    @predecessors = @predecessor.predecessors
    @successor = @todo
    @removed = @successor.remove_predecessor(@predecessor)
    determine_remaining_in_container_count(@todo)
    respond_to do |format|
      format.js
    end
  end

  ##
  # Toggles the 'done' status of the action
  def toggle_check
    @todo = current_user.todos.find(params['id'])

    @source_view = params['_source_view'] || 'todo'

    # Create a (unsaved) copy of the original todo, before it was toggled
    @original_item = current_user.todos.build(@todo.attributes)
    @original_completed_period = DoneTodos.completed_period(@todo.completed_at)

    @todo_was_completed_from_deferred_or_blocked_state =
      @original_item.deferred? || @original_item.pending?

    @saved = @todo.toggle_completion!

    # As a result of toggling the done status of the todo, have we shifted from
    # "Done" to "Blocked"?
    @todo_was_blocked_from_completed_state = @todo.pending?

    # Check if this todo has a related recurring_todo. If so, create next todo
    @new_recurring_todo = check_for_next_todo(@todo) if @saved

    @predecessors = @todo.uncompleted_predecessors
    if @saved
      if @todo.completed?
        @pending_to_activate = @todo.activate_pending_todos
      else
        @active_to_block = @todo.block_successors
      end
    end

    respond_to do |format|
      format.js do
        if @saved
          determine_remaining_in_container_count(@todo)
          determine_down_count
          determine_completed_count
          determine_deferred_tag_count(params['_tag_name']) if source_view_is(:tag)
          @wants_redirect_after_complete = @todo.completed? && !@todo.project_id.nil? && current_user.prefs.show_project_on_todo_done && !source_view_is(:project)
          if source_view_is :calendar
            @original_item_due_id = get_due_id_for_calendar(@original_item.due)
            @old_due_empty = is_old_due_empty(@original_item_due_id)
          end
        end
        render
      end
      format.xml { render :xml => @todo.to_xml(*todo_xml_params) }
      format.html do
        if @saved
          # TODO: I think this will work, but can't figure out how to test it
          notify(:notice, t("todos.action_marked_complete", :description => @todo.description, :completed => @todo.completed? ? 'complete' : 'incomplete'))
          redirect_to :action => "index"
        else
          notify(:notice, t("todos.action_marked_complete_error", :description => @todo.description, :completed => @todo.completed? ? 'complete' : 'incomplete'), "index")
          redirect_to :action => "index"
        end
      end
      format.m do
        if @saved
          if cookies[:mobile_url]
            old_path = cookies[:mobile_url]
            cookies[:mobile_url] = { :value => nil, :secure => SITE_CONFIG['secure_cookies'] }
            notify(:notice, t("todos.action_marked_complete", :description => @todo.description, :completed => @todo.completed? ? 'complete' : 'incomplete'))
            onsite_redirect_to old_path
          else
            notify(:notice, t("todos.action_marked_complete", :description => @todo.description, :completed => @todo.completed? ? 'complete' : 'incomplete'))
            onsite_redirect_to todos_path(:format => 'm')
          end
        else
          render :action => "edit", :format => :m
        end
      end
    end
  end

  def toggle_star
    @todo = current_user.todos.find(params['id'])
    @todo.toggle_star!
    @saved = true # cannot determine error
    respond_to do |format|
      format.js
      format.xml { render :xml => @todo.to_xml(*todo_xml_params) }
      format.html { redirect_to request.referrer }
      format.m do
        if cookies[:mobile_url]
          old_path = cookies[:mobile_url]
          cookies[:mobile_url] = { :value => nil, :secure => SITE_CONFIG['secure_cookies'] }
          notify(:notice, "Star toggled")
          onsite_redirect_to old_path
        else
          notify(:notice, "Star toggled")
          onsite_redirect_to todos_path(:format => 'm')
        end
      end
    end
  end

  def change_context
    # change context if you drag a todo to another context
    @todo = current_user.todos.find(params[:id])
    @original_item = current_user.todos.build(@todo.attributes)  # create a (unsaved) copy of the original todo
    @context = current_user.contexts.find(params[:todo][:context_id])
    @todo.context = @context
    @saved = @todo.save

    @context_changed = true
    @status_message = t('todos.context_changed', :name => @context.name)
    determine_down_count
    determine_remaining_in_container_count(@original_item)

    respond_to do |format|
      format.js  { render :action => :update }
      format.xml { render :xml => @todo.to_xml(*todo_xml_params) }
    end
  end

  def update
    @source_view = params['_source_view'] || 'todo'

    @todo = current_user.todos.find(params['id'])
    @original_item = current_user.todos.build(@todo.attributes)  # create a (unsaved) copy of the original todo
    @original_item_due_id = get_due_id_for_calendar(@original_item.due)

    update_tags
    update_project
    update_context
    update_due_and_show_from_dates
    update_completed_state
    update_dependencies
    update_attributes_of_todo

    begin
      @saved = @todo.save!
    rescue ActiveRecord::RecordInvalid => exception
      record = exception.record
      if record.is_a?(Dependency)
        record.errors.each { |key, value| @todo.errors.add(key, value) }
      end
      @saved = false
    end

    provide_project_or_context_for_view

    # this is set after save and cleared after reload, so save it here
    @removed_predecessors = @todo.removed_predecessors

    @todo.reload # refresh context and project object too (not only their id's)

    update_dependency_state
    if @project_changed
      @remaining_undone_in_project = current_user.projects.find(@original_item.project_id).todos.active.count if source_view_is :project
    end

    determine_changes_by_this_update
    determine_remaining_in_container_count((@context_changed || @project_changed) ? @original_item : @todo)
    determine_down_count
    determine_deferred_tag_count(sanitize(params['_tag_name'])) if source_view_is(:tag)

    @todo.touch_predecessors if @original_item.description != @todo.description

    respond_to do |format|
      format.js do
        @status_message = @todo.deferred? ? t('todos.action_saved_to_tickler') : t('todos.action_saved')
        @status_message = t('todos.added_new_project') + ' / ' + @status_message if @new_project_created
        @status_message = t('todos.added_new_context') + ' / ' + @status_message if @new_context_created
      end
      format.xml { render :xml => @todo.to_xml(*todo_xml_params) }
      format.m do
        if @saved
          do_mobile_todo_redirection
        else
          render :action => "edit", :format => :m
        end
      end
    end
  end

  def provide_project_or_context_for_view
    # see application_helper:source_view_key, used in shown partials
    if source_view_is :project
      @project = @todo.project
    elsif source_view_is :context
      @context = @todo.context
    end
  end

  def destroy
    @source_view = params['_source_view'] || 'todo'
    @todo = current_user.todos.find(params['id'])
    # Create a (unsaved) copy of the original todo
    @original_item = current_user.todos.build(@todo.attributes)
    @context_id = @todo.context_id
    @project_id = @todo.project_id
    @todo_was_destroyed = true
    @todo_was_destroyed_from_deferred_state = @todo.deferred?
    @todo_was_destroyed_from_pending_state = @todo.pending?
    @todo_was_destroyed_from_deferred_or_pending_state = @todo_was_destroyed_from_deferred_state || @todo_was_destroyed_from_pending_state

    @uncompleted_predecessors = []
    @todo.uncompleted_predecessors.each do |predecessor|
      @uncompleted_predecessors << predecessor
    end

    activated_successor_count = 0
    @pending_to_activate = []
    @todo.pending_successors.each do |successor|
      if successor.uncompleted_predecessors.size == 1
        @pending_to_activate << successor
        activated_successor_count += 1
      end
    end

    @saved = @todo.destroy

    # check if this todo has a related recurring_todo. If so, create next todo
    @new_recurring_todo = check_for_next_todo(@todo) if @saved

    respond_to do |format|
      format.html do
        if @saved
          message = t('todos.action_deleted_success')
          if activated_successor_count > 0
            message += " activated #{pluralize(activated_successor_count, 'pending action')}"
          end
          notify :notice, message, 2.0
          redirect_to :action => 'index'
        else
          notify :error, t('todos.action_deleted_error'), 2.0
          redirect_to :action => 'index'
        end
      end
      format.js do
        if @saved
          determine_down_count
          if source_view_is_one_of(:todo, :deferred, :project, :context, :tag)
            determine_remaining_in_container_count(@todo)
          elsif source_view_is :calendar
            @original_item_due_id = get_due_id_for_calendar(@original_item.due)
            @old_due_empty = is_old_due_empty(@original_item_due_id)
          end
        end
        render
      end
      format.xml { render :body => '200 OK. Action deleted.', :status => 200 }
    end
  end

  def done
    @source_view = 'done'
    @page_title = t('todos.completed_tasks_title')

    @done_today, @done_rest_of_week, @done_rest_of_month = DoneTodos.done_todos_for_container(current_user.todos)
    @count = @done_today.size + @done_rest_of_week.size + @done_rest_of_month.size

    respond_to do |format|
      format.html
      format.xml do
        completed_todos = current_user.todos.completed
        render :xml => completed_todos.to_xml(*[todo_xml_params[0].merge({ :root => :todos })])
      end
    end
  end

  def all_done
    @source_view = 'done'
    @page_title = t('todos.completed_tasks_title')

    @done = current_user.todos.completed.includes(Todo::DEFAULT_INCLUDES).reorder('completed_at DESC').paginate :page => params[:page], :per_page => 20
    @count = @done.size
  end

  def list_deferred
    @source_view = 'deferred'
    @page_title = t('todos.deferred_tasks_title')

    @contexts_to_show = @contexts = current_user.contexts
    @projects_to_show = @projects = current_user.projects

    includes = params[:format] == 'xml' ? [:context, :project] : Todo::DEFAULT_INCLUDES

    @not_done_todos = current_user.todos.deferred.includes(includes).reorder('show_from') + current_user.todos.pending.includes(includes)
    @todos_without_project = @not_done_todos.select { |t| t.project.nil? }
    @down_count = @count = @not_done_todos.size

    respond_to do |format|
      format.html do
        init_not_done_counts
        init_hidden_todo_counts
        init_data_for_sidebar unless mobile?
      end
      format.m
      format.xml { render :xml => @not_done_todos.to_xml(*[todo_xml_params[0].merge({ :root => :todos })]) }
    end
  end

  # Check for any due tickler items, activate them
  # Called by periodically_call_remote
  def check_deferred
    @due_tickles = current_user.deferred_todos.find_and_activate_ready
    respond_to do |format|
      format.html { redirect_to home_path }
      format.js
    end
  end

  def filter_to_context
    context = current_user.contexts.find(params['context']['id'])
    redirect_to context_todos_path(context, :format => 'm')
  end

  def filter_to_project
    project = current_user.projects.find(params['project']['id'])
    redirect_to project_todos_path(project, :format => 'm')
  end

  # /todos/tag/[tag_name] shows all the actions tagged with tag_name
  def tag
    get_params_for_tag_view
    @page_title = t('todos.tagged_page_title', :tag_name => @tag_title)
    @source_view = params['_source_view'] || 'tag'

    init_data_for_sidebar unless mobile?

    todos_with_tag_ids = find_todos_with_tag_expr(@tag_expr)

    @not_done_todos = todos_with_tag_ids
      .active.not_hidden
      .reorder(Arel.sql('todos.due IS NULL, todos.due ASC, todos.created_at ASC'))
      .includes(Todo::DEFAULT_INCLUDES)
    @hidden_todos = todos_with_tag_ids
      .hidden
      .reorder(Arel.sql('todos.completed_at DESC, todos.created_at DESC'))
      .includes(Todo::DEFAULT_INCLUDES)
    @deferred_todos = todos_with_tag_ids
      .deferred
      .reorder(Arel.sql('todos.show_from ASC, todos.created_at DESC'))
      .includes(Todo::DEFAULT_INCLUDES)
    @pending_todos = todos_with_tag_ids
      .blocked
      .reorder(Arel.sql('todos.show_from ASC, todos.created_at DESC'))
      .includes(Todo::DEFAULT_INCLUDES)
    @todos_without_project = @not_done_todos.select { |t| t.project.nil? }

    # If you've set no_completed to zero, the completed items box isn't shown on
    # the tag page
    @done = todos_with_tag_ids.completed
      .limit(current_user.prefs.show_number_completed)
      .reorder('todos.completed_at DESC')
      .includes(Todo::DEFAULT_INCLUDES)

    @projects = current_user.projects
    @contexts = current_user.contexts
    @contexts_to_show = @contexts.active
    @projects_to_show = @projects.active

    # Set defaults for new_action
    @initial_tags = @tag_name

    # Set count badge to number of items with this tag
    @not_done_todos.empty? ? @count = 0 : @count = @not_done_todos.size
    @down_count = @count

    respond_to do |format|
      format.html
      format.m { cookies[:mobile_url] = { :value => request.fullpath, :secure => SITE_CONFIG['secure_cookies'] } }
      format.text { render :action => 'index', :layout => false, :content_type => Mime[:text] }
    end
  end

  def done_tag
    done_by_tag_setup

    completed_todos = current_user.todos.completed.with_tag(@tag.id)

    @done_today = get_done_today(completed_todos)
    @done_rest_of_week = get_done_rest_of_week(completed_todos)
    @done_rest_of_month = get_done_rest_of_month(completed_todos)
    @count = @done_today.size + @done_rest_of_week.size + @done_rest_of_month.size

    render :template => 'todos/done'
  end

  def all_done_tag
    done_by_tag_setup
    @done = current_user.todos.completed.with_tag(@tag.id).reorder('completed_at DESC').includes(Todo::DEFAULT_INCLUDES).paginate :page => params[:page], :per_page => 20
    @count = @done.size
    render :template => 'todos/all_done'
  end

  def done_by_tag_setup
    @source_view = params['_source_view'] || 'done'
    @tag_name = sanitize(params[:name]) # sanitize to prevent XSS vunerability!
    @page_title = t('todos.all_completed_tagged_page_title', :tag_name => @tag_name)
    @tag = Tag.where(:name => @tag_name).first_or_create
  end

  def tags
    tags_beginning = current_user.tags.where(Tag.arel_table[:name].matches("#{params[:term]}%"))
    tags_all = current_user.tags.where(Tag.arel_table[:name].matches("%#{params[:term]}%"))
    tags_all -= tags_beginning

    respond_to do |format|
      format.autocomplete { render :body => for_autocomplete(tags_beginning + tags_all, params[:term]) }
    end
  end

  def defer
    @source_view = params['_source_view'] || 'todo'
    numdays = params['days'].to_i

    @todo = current_user.todos.find(params[:id])
    # Create a (unsaved) copy of the original todo
    @original_item = current_user.todos.build(@todo.attributes)

    @todo_deferred_state_changed = true
    @new_context_created = false
    @due_date_changed = false
    @tag_was_removed = false
    @todo_hidden_state_changed = false
    @todo_was_deferred_from_active_state = @todo.show_from.nil?

    @todo.show_from = (@todo.show_from || @todo.user.date) + numdays.days
    @saved = @todo.save
    @status_message = t('todos.action_saved_to_tickler')

    determine_down_count
    determine_remaining_in_container_count(@todo)
    source_view do |page|
      page.project do
        @remaining_undone_in_project = current_user.projects.find(@todo.project_id).todos.not_completed.count
      end
      page.tag { determine_deferred_tag_count(params['_tag_name']) }
    end

    respond_to do |format|
      format.html { redirect_to :back }
      format.js { render :action => 'update' }
      format.m do
        notify(:notice, t("todos.action_deferred", :description => @todo.description))
        do_mobile_todo_redirection
      end
    end
  end

  def list_hidden
    @hidden = current_user.todos.hidden
    respond_to do |format|
      format.xml { render :xml => @hidden.to_xml(*[todo_xml_params[0].merge({ :root => :todos })]) }
    end
  end

  def get_not_completed_for_predecessor(relation, todo_id = nil)
    items = relation.todos.not_completed
      .where('(LOWER(todos.description) ' + Common.like_operator + '?)', "%#{params[:term].downcase}%")
    items = items.where("AND NOT(todos.id=?)", todo_id) unless todo_id.nil?

    items
      .includes(:context, :project)
      .reorder('description ASC')
      .limit(10)
  end

  def auto_complete_for_predecessor
    unless params['id'].nil?
      get_todo_from_params
      # Begin matching todos in current project, excluding @todo itself
      @items = get_not_completed_for_predecessor(@todo.project, @todo.id) unless @todo.project.nil?
      # Then look in the current context, excluding @todo itself
      @items = get_not_completed_for_predecessor(@todo.context, @todo.id) unless !@items.empty? || @todo.context.nil?
      # Match todos in other projects, excluding @todo itself
      @items = get_not_completed_for_predecessor(current_user, @todo.id) unless !@items.empty?
    else
      # New todo - TODO: Filter on current project in project view
      @items = get_not_completed_for_predecessor(current_user)
    end
    render :inline => format_dependencies_as_json_for_auto_complete(@items)
  end

  def convert_to_project
    todo = current_user.todos.find(params[:id])
    @project = ProjectFromTodo.new(todo).create

    if @project.valid?
      redirect_to project_url(@project)
    else
      flash[:error] = "Could not create project from todo: #{@project.errors.full_messages[0]}"
      onsite_redirect_to request.env["HTTP_REFERER"] || root_url
    end
  end

  def show_notes
    @todo = current_user.todos.find(params['id'])
    @return_path = cookies[:mobile_url] ? cookies[:mobile_url] : mobile_path
    respond_to do |format|
      format.html { redirect_to home_path, "Viewing note of todo is not implemented" }
      format.m { render :action => "show_notes" }
    end
  end

  def attachment
    id = params[:id]
    filename = params[:filename]
    attachment = current_user.attachments.find(id)

    if attachment
      send_file(attachment.file.path,
        disposition: 'attachment',
        type: 'message/rfc822')
    else
      head :not_found
    end
  end

  private

  def set_group_view_by
    @group_view_by = params['_group_view_by'] || cookies['group_view_by'] || 'context'
  end

  def do_mobile_todo_redirection
    if cookies[:mobile_url]
      old_path = cookies[:mobile_url]
      cookies[:mobile_url] = { :value => nil, :secure => SITE_CONFIG['secure_cookies'] }
      onsite_redirect_to old_path
    else
      onsite_redirect_to todos_path(:format => 'm')
    end
  end

  def get_todo_from_params
    # TODO: this was a :append_before but was removed to tune performance per
    # method. Reconsider re-enabling it
    @todo = current_user.todos.find(params['id'])
  end

  def find_and_activate_ready
    current_user.deferred_todos.find_and_activate_ready
  end

  def tag_title(tag_expr)
    and_list = tag_expr.inject([]) { |s, tag_list| s << tag_list.join(',') }
    return and_list.join(' AND ')
  end

  def get_params_for_tag_view
    filter_format_for_tag_view

    # Don't use sanitize here because these are only used for a DB query.
    @tag_expr = []
    # Tag conditions handled as OR.
    @tag_expr << params[:name].split(',')

    # Additional tag condition(s) handled as AND.
    @tag_expr << params[:and].split(',') if params[:and]
    i = 1
    while params['and' + i.to_s]
      @tag_expr << params['and' + i.to_s].split(',')
      i += 1
    end

    @single_tag = @tag_expr.size == 1 && @tag_expr[0].size == 1

    # These are used in the templates, sanitise to prevent XSS.
    @tag_name = sanitize(@tag_expr[0][0])
    @tag_title = sanitize(@single_tag ? @tag_name : tag_title(@tag_expr))
  end

  def filter_format_for_tag_view
    # routes for tag view do not set :format
    if params[:name] =~ /.*\.m$/
      set_format_for_tag_view(:m)
    elsif params[:name] =~ /.*\.txt$/
      set_format_for_tag_view(:txt)
      # set content-type to text/plain or it remains text/html
      response.headers["Content-Type"] = 'text/plain'
    elsif params[:format].nil?
      # if no format is given, default to html
      # note that if url has ?format=m, we should not overwrite it here
      request.format = :html
      params[:format] = :html
    end
  end

  def set_format_for_tag_view(format)
    # tag name ends with .m, set format to :m en remove .m from name
    request.format = format
    params[:format] = format
    params[:name] = params[:name].chomp(".#{format.to_s}")
end

  def get_ids_from_tag_expr(tag_expr)
    ids = []
    tag_expr.each do |tag_list|
      id_list = []
      tag_list.each do |tag|
        tag = Tag.where(:name => tag).first
        id_list << tag.id if tag
      end
      ids << id_list
    end
    return ids
  end

  def find_todos_with_tag_expr(tag_expr)
    # Optimize for the common case of selecting only one tag
    if @single_tag
      tag = current_user.tags.where(:name => @tag_name).first
      tag_id = tag.nil? ? -1 : tag.id
      return current_user.todos.with_tag(tag_id)
    end

    tag_ids = get_ids_from_tag_expr(tag_expr)
    todos = current_user.todos
    tag_ids.each do |ids|
      todos = todos.with_tags(ids) unless ids.nil? || ids.empty?
    end
    return todos
  end

  def determine_down_count
    source_view do |from|
      from.todo do
        @down_count = current_user.todos.active.not_hidden.count
      end
      from.context do
        context_id = @original_item ? @original_item.context_id || @todo.context_id : @todo.context_id
        todos = current_user.contexts.find(context_id).todos.not_completed

        if @todo.context.hidden?
          # include hidden todos
          @down_count = todos.count
        else
          # exclude hidden_todos
          @down_count = todos.not_hidden.count
        end
      end
      from.project do
        unless @todo.project_id == nil
          @down_count = current_user.projects.find(@todo.project_id).todos.active_or_hidden.count
        end
      end
      from.deferred do
        @down_count = current_user.todos.deferred_or_blocked.count
      end
      from.tag do
        @tag_name = params['_tag_name']
        @tag = Tag.where(:name => @tag_name).first
        if @tag.nil?
          @tag = Tag.new(:name => @tag_name)
        end
        @down_count = current_user.todos.with_tag(@tag.id).active.not_hidden.count
      end
    end
  end

  def find_todos_in_project_container(todo)
    if todo.project.nil?
      # container with todos without project
      todos_in_container = current_user.todos.where(:project_id => nil)
    else
      todos_in_container = current_user.projects.find(todo.project_id).todos
    end
  end

  def find_todos_in_container_and_target_container(todo, target_todo)
    if @group_view_by == 'context'
      todos_in_container = current_user.contexts.find(todo.context_id).todos
      todos_in_target_container = current_user.contexts.find(@todo.context_id).todos
    else
      todos_in_container = find_todos_in_project_container(todo)
      todos_in_target_container = find_todos_in_project_container(@todo)
    end
    return todos_in_container, todos_in_target_container
  end

  def determine_remaining_in_container_count(todo = @todo)
    source_view do |from|
      from.deferred do
        todos_in_container, todos_in_target_container = find_todos_in_container_and_target_container(todo, @todo)
        @remaining_in_context = todos_in_container.deferred_or_blocked.count
        @target_context_count = todos_in_target_container.deferred_or_blocked.count
      end
      from.todo do
        todos_in_container, todos_in_target_container = find_todos_in_container_and_target_container(todo, @todo)
        @remaining_in_context = todos_in_container.active.not_hidden.count
        @target_context_count = todos_in_target_container.active.not_hidden.count
      end
      from.tag do
        tag = Tag.where(:name => params['_tag_name']).first
        tag = Tag.new(:name => params['tag']) if tag.nil?

        todos_in_container, todos_in_target_container = find_todos_in_container_and_target_container(todo, @todo)

        @remaining_in_context = todos_in_container.active.not_hidden.with_tag(tag.id).count
        @target_context_count = todos_in_target_container.active.not_hidden.with_tag(tag.id).count
        @remaining_hidden_count = current_user.todos.hidden.with_tag(tag.id).count
        @remaining_deferred_or_pending_count = current_user.todos.with_tag(tag.id).deferred_or_blocked.count
      end
      from.project do
        project_id = @project_changed ? @original_item.project_id : @todo.project_id
        @remaining_deferred_or_pending_count = current_user.projects.find(project_id).todos.deferred_or_blocked.count

        if @todo_was_completed_from_deferred_or_blocked_state
          @remaining_in_context = @remaining_deferred_or_pending_count
        else
          @remaining_in_context = current_user.projects.find(project_id).todos.active_or_hidden.count
        end

        @target_context_count = current_user.projects.find(project_id).todos.active.count
      end
      from.calendar do
        @target_context_count = @new_due_id.blank? ? 0 : count_old_due_empty(@new_due_id)
      end
      from.context do
        context = current_user.contexts.find(todo.context_id)
        @remaining_deferred_or_pending_count = context.todos.deferred_or_blocked.count

        remaining_actions_in_context = context.todos.reload.active
        remaining_actions_in_context = remaining_actions_in_context.not_hidden if !context.hidden?
        @remaining_in_context = remaining_actions_in_context.count

        if @todo_was_deferred_or_blocked
          actions_in_target = current_user.contexts.find(@todo.context_id).todos.reload.active
          actions_in_target = actions_in_target.not_hidden if !context.hidden?
        else
          actions_in_target = @todo.context.todos.deferred_or_blocked
        end
        @target_context_count = actions_in_target.count
      end
      from.done do
        @remaining_in_context = DoneTodos.remaining_in_container(current_user.todos, @original_completed_period)
      end
      from.all_done { @remaining_in_context = current_user.todos.completed.count }
    end
  end

  def find_completed(relation, id, include_hidden)
    todos = relation.find(id).todos.completed
    todos = todos.not_hidden if !include_hidden
    return todos
  end

  def determine_completed_count
    todos = nil

    source_view do |from|
      from.todo    { todos = current_user.todos.not_hidden.completed }
      from.context { todos = find_completed(current_user.contexts, @todo.context_id, @todo.context.hidden?) }
      from.project { todos = find_completed(current_user.projects, @todo.project_id, @todo.project.hidden?) unless @todo.project_id.nil? }
      from.tag     { todos = current_user.todos.with_tag(@tag.id).completed }
    end

    @completed_count = todos.nil? ? 0 : todos.count
  end

  def determine_deferred_tag_count(tag_name)
    tag = Tag.where(:name => tag_name).first
    # tag.nil? should normally not happen, but is a workaround for #929
    @remaining_deferred_or_pending_count = tag.nil? ? 0 : current_user.todos.deferred.with_tag(tag.id).count
  end

  def check_for_next_todo(todo)
    # check if this todo has a related recurring_todo. If so, create next todo
    new_recurring_todo = nil
    recurring_todo = nil
    if todo.from_recurring_todo?
      recurring_todo = todo.recurring_todo

      # check if there are active todos belonging to this recurring todo. only
      # add new one if all active todos are completed
      if recurring_todo.todos.active.count == 0

        # check for next todo either from the due date or the show_from date
        date_to_check = todo.due || todo.show_from

        # if both due and show_from are nil, check for a next todo from now
        date_to_check ||= Time.zone.now

        if recurring_todo.active? && recurring_todo.continues_recurring?(date_to_check)
          # shift the reference date to yesterday if date_to_check is furher in
          # the past. This is to make sure we do not get older todos for overdue
          # todos. I.e. checking a daily todo that is overdue with 5 days will
          # create a new todo which is overdue by 4 days if we don't shift the
          # date. Discard the time part in the compare. We pick yesterday so
          # that new todos due for today will be created instead of new todos
          # for tomorrow.
          date = date_to_check.at_midnight >= Time.zone.now.at_midnight ? date_to_check : Time.zone.now - 1.day

          new_recurring_todo = TodoFromRecurringTodo.new(current_user, recurring_todo).create(date)
        end
      end
    end
    return new_recurring_todo
  end

  def get_due_id_for_calendar(due)
    return "" if due.nil?
    due_today_date = Time.zone.now
    due_this_week_date = Time.zone.now.end_of_week
    due_next_week_date = due_this_week_date + 7.days
    due_this_month_date = Time.zone.now.end_of_month
    if due <= due_today_date
      new_due_id = "due_today"
    elsif due <= due_this_week_date
      new_due_id = "due_this_week"
    elsif due <= due_next_week_date
      new_due_id = "due_next_week"
    elsif due <= due_this_month_date
      new_due_id = "due_this_month"
    else
      new_due_id = "due_after_this_month"
    end
    return new_due_id
  end

  def is_old_due_empty(id)
    return 0 == count_old_due_empty(id)
  end

  def count_old_due_empty(id)
    due_today_date = Time.zone.now
    due_this_week_date = Time.zone.now.end_of_week
    due_next_week_date = due_this_week_date + 7.days
    due_this_month_date = Time.zone.now.end_of_month

    case id
    when "due_today"
      return current_user.todos.not_completed.where('todos.due <= ?', due_today_date).count
    when "due_this_week"
      return current_user.todos.not_completed.where('todos.due > ? AND todos.due <= ?', due_today_date, due_this_week_date).count
    when "due_next_week"
      return current_user.todos.not_completed.where('todos.due > ? AND todos.due <= ?', due_this_week_date, due_next_week_date).count
    when "due_this_month"
      return current_user.todos.not_completed.where('todos.due > ? AND todos.due <= ?', due_next_week_date, due_this_month_date).count
    when "due_after_this_month"
      return current_user.todos.not_completed.where('todos.due > ?', due_this_month_date).count
    else
      raise Exception.new, "unknown due id for calendar: '#{id}'"
    end
  end

  def update_project
    @project_changed = false
    if params['todo']['project_id'].blank? && !params['project_name'].nil?
      if params['project_name'].blank?
        project = Project.null_object
      else
        project = current_user.projects.where(:name => params['project_name'].strip).first
        unless project
          project = current_user.projects.build
          project.name = params['project_name'].strip
          project.save
          @new_project_created = true
          @new_container = project
          @not_done_todos = [@todo]
        end
      end
      params["todo"]["project_id"] = project.id
      @project_changed = @original_item.project_id != params["todo"]["project_id"] = project.id
    end
  end

  def update_context
    @context_changed = false
    if params['todo']['context_id'].blank? && params['context_name'].present?
      @context = current_user.contexts.where(:name => params['context_name'].strip).first
      if @context.nil?
        @new_context = current_user.contexts.build
        @new_context.name = params['context_name'].strip
        @new_context.save
        @new_context_created = true
        @new_container = @new_context
        @not_done_todos = [@todo]
        @context = @new_context
      end
      params["todo"]["context_id"] = @context.id
      @context_changed = @original_item.context_id != params["todo"]["context_id"] = @context.id
    end
  end

  def update_tags
    if params[:tag_list]
      @todo.tag_with(params[:tag_list])
      # Force a reload for proper rendering
      @todo.tags.reload
    end
  end

  def parse_date_for_update(date, error_msg)
    begin
      parse_date_per_user_prefs(date)
    rescue
      @todo.errors.add(:base, error_msg)
    end
  end

  def update_date_for_update(key)
    params['todo'][key] = params["todo"].key?(key) ? parse_date_for_update(params["todo"][key], t("todos.error.invalid_#{key}_date")) : ""
  end

  def update_due_and_show_from_dates
    %w{ due show_from }.each { |date| update_date_for_update(date) }
  end

  def update_completed_state
    if params['done'] == '1' && !@todo.completed?
      @todo.complete!
      @todo.activate_pending_todos
    end
    # strange. if checkbox is not checked, there is no 'done' in params.
    # Therefore I've used the negation
    if !(params['done'] == '1') && @todo.completed?
      @todo.activate!
      @todo.block_successors
    end
  end

  def update_dependencies
    @todo.add_predecessor_list(params[:predecessor_list])
  end

  def update_dependency_state
    # assumes @todo.save was called so that the predecessor_list is persistent
    original_item_predecessor_list =
      @original_item.predecessors
        .map { |t| t.specification }
        .join(', ')

    if original_item_predecessor_list != params[:predecessor_list]
      # Possible state change with new dependencies
      if @todo.uncompleted_predecessors.empty?
        @todo.activate! if @todo.state == 'pending' # Activate pending if no uncompleted predecessors
      else
        @todo.block! if @todo.state == 'active' # Block active if we got uncompleted predecessors
      end
    end
  end

  def update_attributes_of_todo
    # TODO: duplication with todo_create_params_helper
    @todo.attributes = params.require(:todo).permit(
        :context_id, :project_id, :description, :notes,
        :due, :show_from, :state)
  end

  def determine_changes_by_this_update
    @todo_was_activated_from_deferred_state = @todo.active? && @original_item.deferred?
    @todo_was_activated_from_pending_state = @todo.active? && @original_item.pending?
    @todo_was_deferred_from_active_state = @todo.deferred? && !@original_item.deferred?
    @todo_was_blocked_from_active_state = @todo.pending? && !@original_item.pending?

    @todo_deferred_state_changed = @original_item.deferred? != @todo.deferred?
    @todo_pending_state_changed = @original_item.pending? != @todo.pending?
    @todo_hidden_state_changed = @original_item.hidden? != @todo.hidden?

    @due_date_changed = @original_item.due != @todo.due

    source_view do |page|
      page.calendar do
        @old_due_empty = is_old_due_empty(@original_item_due_id)
        @new_due_id = get_due_id_for_calendar(@todo.due)
      end
      page.tag do
        @tag_name = params['_tag_name']
        @tag_was_removed = !@todo.has_tag?(@tag_name)
      end
      page.context do
        @todo_should_be_hidden = @todo_hidden_state_changed && @todo.hidden?
      end
    end
  end

  # all completed todos [today@00:00, today@now]
  def get_done_today(completed_todos, includes = { :include => Todo::DEFAULT_INCLUDES })
    start_of_this_day = Time.zone.now.beginning_of_day
    completed_todos.completed_after(start_of_this_day).includes(includes[:include])
  end

  def get_done_in_period(completed_todos, before, after, includes = { :include => Todo::DEFAULT_INCLUDES })
    completed_todos.completed_before(before).completed_after(after).includes(includes[:include])
  end

  # all completed todos [begin_of_week, start_of_today]
  def get_done_rest_of_week(completed_todos, includes = { :include => Todo::DEFAULT_INCLUDES })
    get_done_in_period(completed_todos, Time.zone.now.beginning_of_day, Time.zone.now.beginning_of_week)
  end

  # all completed todos [begin_of_month, begin_of_week]
  def get_done_rest_of_month(completed_todos, includes = { :include => Todo::DEFAULT_INCLUDES })
    get_done_in_period(completed_todos, Time.zone.now.beginning_of_week, Time.zone.now.beginning_of_month)
  end

  def get_not_done_todos
    Todos::UndoneTodosQuery.new(current_user).query(params)
  end

  def onsite_redirect_to(destination)
    uri = URI.parse(destination)

    if uri.query.present?
      redirect_to("#{uri.path}?#{uri.query}")
    else
      redirect_to(uri.path)
    end
  end
end