ari/jobsworth

View on GitHub
app/controllers/tasks_controller.rb

Summary

Maintainability
D
2 days
Test Coverage
# encoding: UTF-8
# Handle tasks for a Company / User

require 'csv'

class TasksController < ApplicationController
  DEFAULT_TASK_COUNT = 5
  DEFAULT_TASK_NAME = 'New Task'

  before_filter :check_if_user_has_projects, :only => [:new, :create]
  before_filter :check_if_user_can_create_task, :only => [:create]
  before_filter :authorize_user_is_admin, :only => [:planning]

  cache_sweeper :tag_sweeper, :only => [:create, :update]

  def index
    @task = TaskRecord.accessed_by(current_user).find_by(:id => session[:last_task_id])
    @tasks = current_task_filter.tasks.includes(:project, :owners, :watchers, :company, :dependencies).paginate(page: params[:page], per_page: 200)
    respond_to do |format|
      format.html
      format.json { render :template => 'tasks/index.json'
      }
    end
  end

  def new
    project = current_user.company.default_project || current_user.company.projects.last
    @task = TaskRecord.new(company: current_user.company,
                           watchers: [current_user],
                           name: DEFAULT_TASK_NAME,
                           project: project,
                           creator_id: current_user.id)
    @task.duration = @task.default_duration
    if @task.save!
      flash[:success] = t('.task_was_created')
      redirect_to edit_task_path(@task.task_num)
    else
      flash[:error] = t('.task_was_not_created')
      redirect_to tasks_path
    end
    
    rescue => e
      flash[:error] = e.message
      redirect_to tasks_path
  end

  def create
    @task.task_due_calculation(params[:task][:due_at], current_user)
    @task.duration = TimeParser.parse_time(params[:task][:duration])
    @task.duration = 0 if @task.duration.nil?
    if @task.service_id == -1
      @task.isQuoted = true
      @task.service_id = nil
    else
      @task.isQuoted = false
    end
    todos_params.collect { |todo| @task.todos.build(todo) } if todos_params

    # One task can have two  worklogs, so following code can raise three exceptions
    # ActiveRecord::RecordInvalid or ActiveRecord::RecordNotSaved
    begin
      ActiveRecord::Base.transaction do
        begin
          @task.save!
        rescue ActiveRecord::RecordNotUnique
          @task.save!
        end
        @task.set_users_dependencies_resources(params, current_user)
        files = @task.create_attachments(params['tmp_files'], current_user)
        create_worklogs_for_tasks_create(files) if @task.is_a?(TaskRecord)
      end
      set_last_task(@task)
      flash[:success] ||= (link_to_task(@task) + " - #{t('flash.notice.model_created', model: TaskRecord.model_name.human)}")
      Trigger.fire(@task, Trigger::Event::CREATED)
      return if request.xhr?
      redirect_to tasks_path
    rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved
      flash[:error] = @task.errors.full_messages.join('. ')
      return if request.xhr?
      render :template => 'tasks/new'
    end
  end

  def calendar
    respond_to do |format|
      format.html
      format.json {
        @tasks = current_task_filter.tasks_for_fullcalendar(params)
      }
    end
  end

  def gantt
    respond_to do |format|
      format.html
      format.json {
        @tasks = current_task_filter.tasks_for_gantt(params)
      }
    end
  end

  def auto_complete_for_dependency_targets
    value = params[:term]
    value.gsub!(/#/, '')
    @keys = [value]
    @tasks = TaskRecord.search(current_user, @keys, status_in: AbstractTask::OPEN)
    render :json => @tasks.collect { |task| {:label => "[##{task.task_num}] #{task.name}", :value => task.name[0..13] + '...', :id => task.task_num} }.to_json
  end

  def auto_complete_for_resource_name
    return unless current_user.use_resources?

    search = params[:term]
    search = search.split(',').last if search

    if !search.blank?
      conds = 'lower(name) like ?'
      cond_params = ["%#{ search.downcase }%"]

      # only return resources related to current selected customer
      params[:customer_ids] ||= []
      params[:customer_ids] = [0] if params[:customer_ids].empty?
      conds += 'and (customer_id in (?))'
      cond_params << params[:customer_ids]

      conds = [conds] + cond_params

      @resources = current_user.company.resources.where(conds)
      render :json => @resources.collect { |resource| {:label => "[##{resource.id}] #{resource.name}", :value => resource.name, :id => resource.id} }.to_json
    else
      render :nothing => true
    end
  end

  def resource
    resource = current_user.company.resources.find(params[:resource_id])
    render(:partial => 'resource', :locals => {:resource => resource})
  end

  def dependency
    dependency = TaskRecord.accessed_by(current_user).find_by(:task_num => params[:dependency_id])
    render(:partial => 'dependency',
           :locals => {:dependency => dependency, :perms => {}})
  end

  def edit
    @task = AbstractTask.accessed_by(current_user).find_by(:task_num => params[:id])

    if @task.nil?
      flash[:error] = t('flash.error.not_exists_or_no_permission', model: TaskRecord.model_name.human)
      redirect_from_last
      return
    end

    set_last_task(@task)
    @task.set_task_read(current_user)
    respond_to do |format|
      format.html { render :template => 'tasks/edit' }
      format.js {
        html = render_to_string(:template => 'tasks/edit', :layout => false)
        render :json => {:html => html, :task_num => @task.task_num, :task_name => @task.name}
      }
    end
  end

  def update
    @task = AbstractTask.accessed_by(current_user).find_by(:id => params[:id])
    if @task.nil?
      flash[:error] = t('flash.error.not_exists_or_no_permission', model: TaskRecord.model_name.human)
      redirect_from_last and return
    end

    # TODO this should be a before_filter
    unless task_edit_permissions?(%w(edit comment milestone))
      flash[:error] = ProjectPermission.message_for('edit')
      redirect_from_last and return
    end

    # if user only have comment rights
    if !task_edit_permissions?(%w(edit milestone)) && task_edit_permissions?(%w(comment))
      params[:task] = {}
    end

    # TODO this should go into Task model
    begin
      ActiveRecord::Base.transaction do
        params[:task] = task_params
        TaskRecord.update(@task, params, current_user)
        @task.owners.last.schedule_tasks if @task.owners.present?
      end

      # TODO this should be an observer
      Trigger.fire(@task, Trigger::Event::UPDATED)
      flash[:success] ||= link_to_task(@task) + " - #{t('flash.notice.model_updated', model: TaskRecord.model_name.human)}"

      respond_to do |format|
        format.html { redirect_to :action => 'edit', :id => @task.task_num }
        format.js {
          render :json => {
              :status => :success,
              :tasknum => @task.task_num,
              :tags => render_to_string(:partial => 'tags/panel_list'),
              :message => render_to_string(:partial => 'layouts/flash', :locals => {:flash => flash}).html_safe}
        }

      end
    rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved
      respond_to do |format|
        format.html {
          flash[:error] = @task.errors.full_messages.join('. ')
          render :template => 'tasks/edit'
        }
        format.js { render :json => {:status => :error, :messages => @task.errors.full_messages}.to_json }
      end
    end
  end


  def get_csv
    filename = 'jobsworth_tasks.csv'
    @tasks= current_task_filter.tasks
    csv_string = CSV.generate(:col_sep => ',') do |csv|
      csv << @tasks.first.csv_header
      @tasks.each do |t|
        csv << t.to_csv
      end

    end
    logger.info("Sending[#{filename}]")

    send_data(csv_string,
              :type => 'text/csv; charset=utf-8; header=present',
              :filename => filename)
  end

  def refresh_service_options
    @task = create_entity
    if params[:taskId]
      @task = AbstractTask.accessed_by(current_user).find(params[:taskId])
    end

    customers = []
    customerIds = []
    customerIds = params[:customerIds].split(',') if params[:customerIds]
    customerIds.each do |cid|
      customers << current_user.company.customers.find(cid)
    end

    render :json => {:success => true, :html => view_context.options_for_task_services(customers, @task)}
  end

  def get_watcher
    @task = create_entity
    unless params[:id].blank?
      @task = AbstractTask.accessed_by(current_user).find_by(:id => params[:id])
    end

    user = current_user.company.users.active.find(params[:user_id])
    @task.task_watchers.build(:user => user)

    render(:partial => 'tasks/notification', :locals => {:notification => user})
  end

  def get_customer
    @task = create_entity
    unless params[:id].blank?
      @task = AbstractTask.accessed_by(current_user).find_by(:id => params[:id])
    end

    customer = current_user.company.customers.find(params[:customer_id])
    @task.task_customers.build(:customer => customer)

    render(:partial => 'tasks/task_customer', :locals => {:task_customer => customer})
  end

  def get_default_customers
    @task = create_entity
    unless params[:id].blank?
      @task = AbstractTask.accessed_by(current_user).find_by(:id => params[:id])
    end

    @project = current_user.projects.find_by(:id => params[:project_id])

    @customers = []
    @customers << @project.customer
    @customers += @task.customers

    render(:partial => 'tasks/task_customer', :collection => @customers, :as => :task_customer)
  end

  def get_default_watchers_for_customer
    @task = create_entity
    unless params[:id].blank?
      @task = AbstractTask.accessed_by(current_user).find_by(:id => params[:id])
    end

    if params[:customer_id].present?
      @customer = current_user.company.customers.find(params[:customer_id])
    end

    users = @customer ? @customer.users.auto_add.all : []
    users.to_a.reject! { |u| @task.users.include?(u) }

    res = render_to_string(:partial => 'tasks/notification', :collection => users)
    render :text => res
  end

  def get_default_watchers_for_project
    @task = create_entity
    unless params[:id].blank?
      @task = AbstractTask.accessed_by(current_user).find_by(:id => params[:id])
    end
    users = []
    if params[:project_id].present?
      users = Project.find(params[:project_id]).default_users
    end

    users = users.name_not_in(params[:users]).task_is_not(@task) if users.present?

    res = render_to_string(:partial => 'tasks/notification', :collection => users)
    render :text => res
  end

  def get_default_watchers
    @task = create_entity
    unless params[:id].blank?
      @task = AbstractTask.accessed_by(current_user).find_by(:id => params[:id])
    end

    @customers = []
    if params[:customer_ids].present?
      @customers = current_user.company.customers.where('customers.id IN (?)', params[:customer_ids])
    end

    if params[:project_id].present?
      @default_users = User.joins('INNER JOIN default_project_users on default_project_users.user_id = users.id').where('default_project_users.project_id = ?', params[:project_id])
      @project = current_user.projects.find_by(:id => params[:project_id])
      @customers << @project.customer if @project.try(:customer)
    end

    @users = [current_user]
    @customers.each { |c| @users += c.users.auto_add.all }
    @users += @task.users
    @users += @default_users
    @users.uniq!

    res = render_to_string(:partial => 'tasks/notification', :collection => @users)
    render :text => res
  end

  # GET /tasks/billable?customer_ids=:customer_ids&project_id=:project_id&service_id=:service_id
  def billable
    @project = current_user.projects.find(params[:project_id]) if params[:project_id]
    return render :json => {:billable => false} if @project and @project.no_billing?
    return render :json => {:billable => false} if params[:service_id].to_i < 0
    return render :json => {:billable => true} if params[:service_id].to_i == 0

    @customer_ids = (params[:customer_ids] || '').split(',')
    slas = []
    @customer_ids.each do |cid|
      customer = current_user.company.customers.find(cid) rescue nil

      if customer
        sla = customer.service_level_agreements.where(:service_id => params[:service_id]).first rescue nil
        slas << sla if sla
      end
    end

    sla = slas.detect { |s| s.billable }
    if sla
      return render :json => {:billable => true}
    else
      return render :json => {:billable => false}
    end
  end

  def set_group
    task = TaskRecord.accessed_by(current_user).find_by(:task_num => params[:id])
    task.update_group(current_user, params[:group], params[:value], params[:icon])

    expire_fragment(%r{tasks\/#{task.id}-.*\/*})
    render :nothing => true
  end

  def users_to_notify_popup
    # anyone already attached to the task should be removed
    excluded_ids = params[:watcher_ids].blank? ? 0 : params[:watcher_ids]
    @users = current_user.customer ? current_user.customer.users.active.where("id NOT IN (#{excluded_ids})").order('name').limit(50) : []
    @task = AbstractTask.accessed_by(current_user).find_by(:id => params[:id])

    @task && @task.customers.each do |customer|
      @users = @users + customer.users.active.where("id NOT IN (#{excluded_ids})").order('name').limit(50)
    end
    @users = @users.uniq.sort_by { |user| user.name }.first(50)

    if @task && current_user.customer != @task.project.customer
      @users = @users + @task.project.customer.users.active.where('id NOT IN (?)', excluded_ids)
      @users = @users.uniq.sort_by { |user| user.name }.first(50)
    end
    render :layout => false
  end

  # The user has dragged a task into a different order and we need to adjust the weight adjustment accordingly
  def change_task_weight
    @user = current_user
    if current_user.admin? and params[:user_id]
      @user = current_user.company.users.find(params[:user_id])
    end

    # Note that we check the user has access to this task before moving it
    moved = TaskRecord.accessed_by(@user).find_by(:id => params[:moved])
    return render :json => {:success => false} if moved.nil?

    # If prev is not passed, then the user wanted to move the task to the top of the list
    if params[:prev]
      prev = TaskRecord.accessed_by(@user).find_by(:id => params[:prev])
    end

    if prev.nil?
      topTask = @user.tasks.open_only.not_snoozed.order('weight DESC').first
      changeRequired = topTask.weight - moved.weight + 1
    else
      changeRequired = prev.weight - moved.weight - 1
    end
    moved.weight_adjustment = moved.weight_adjustment + changeRequired
    moved.weight = moved.weight + changeRequired
    moved.save(:validate => false)
    render :json => {:success => true}
  end

  # GET /tasks/planning
  def planning
    @users = current_user.company.users.active.order('name ASC')
    render :layout => 'layouts/basic'
  end

  def clone
    @template = current_templates.find_by(:task_num => params[:id])
    @task = AbstractTask.new(task_params_for_clone(@template))
    @from_template = 1
    @task.tags = @template.tags
    @template.todos.order('todos.id').select(:name, :position, :creator_id).each { |todo|  @task.todos.build(todo.attributes) } 
    @task.customers = @template.customers
    @task.users = @template.users
    @task.watchers = @template.watchers
    @task.owners = @template.owners
    @task.type = "TaskRecord"
    @template.task_property_values.select(:property_id, :property_value_id).each { |pv| @task.task_property_values.build(pv.attributes) }
    
    if @task.save!
      flash[:success] = t('.task_was_created')
      redirect_to edit_task_path(@task.task_num)
    else
      flash[:error] = t('.task_was_not_created')
      redirect_to tasks_path
    end

    rescue => e
      flash[:error] = e.message
      redirect_to tasks_path
  end

  # GET /tasks/score/:id
  def score
    @task = TaskRecord.accessed_by(current_user).find_by(:task_num => params[:id])
    if @task.nil?
      flash[:error] = t('activerecord.errors.models.task_record.task_number.invalid')
      redirect_to 'index'
    else
      # Force score recalculation
      @task.save(:validation => false)
    end
  end

  # build 'next tasks' panel from an ajax call (click on the more... button)
  def nextTasks
    @user = current_user
    if current_user.admin? and params[:user_id]
      @user = current_user.company.users.find(params[:user_id])
    end

    tasks_count = params[:count] ? params[:count].to_i : DEFAULT_TASK_COUNT

    html = render_to_string :partial => 'tasks/next_tasks_panel', :locals => {:count => tasks_count, :user => @user}
    render :json => {:html => html, :has_more => (@user.tasks.open_only.not_snoozed.count > tasks_count)}
  end

  def task_edit_permissions?(permissions)
    #This method returns true if the user has atleast one of the permissions
    permission = false
    permissions.each do |p|
      permission = true if current_user.can?(@task.project, p)
    end
    permission
  end

  protected

  def check_if_user_can_create_task
    @task = create_entity
    @task.attributes = task_params

    unless current_user.can?(@task.project, 'create')
      flash[:error] = t('flash.alert.unauthorized_operation')
      render :new
    end
  end

  def check_if_user_has_projects
    unless current_user.has_projects?
      redirect_to new_project_path, alert: t('hint.task.project_needed')
    end
  end

  ############### This methods extracted to make Template Method design pattern #############################################3
  def create_entity
    return TaskRecord.new(:company => current_user.company)
  end

  def create_worklogs_for_tasks_create(files)
    # task created
    work_log = WorkLog.create_task_created!(@task, current_user)
    work_log.notify(files)
    work_log = WorkLog.build_work_added_or_comment(@task, current_user, work_log_and_comments_params)
    work_log.save if work_log
  end

  def set_last_task(task)
    session[:last_task_id] = task.id if task.is_a?(TaskRecord)
  end

  private

  def task_params
    params.fetch(:task, {}).permit(*((TaskRecord.new.attributes.keys - ['type']) + [:unknown_emails, :set_tags])).tap do |whitelisted|
      whitelisted[:properties] = params[:task][:properties] || {}
      whitelisted[:customer_attributes] = params[:task][:customer_attributes] || {}
    end
  end

  def todos_params
    params.permit(:todos => [:name, :completed_at, :creator_id, :completed_by_user_id]).fetch :todos, []
  end

  def task_params_for_clone(task)
    task_attributes = task.attributes
    task_attributes.delete('id')
    ActionController::Parameters.new(task_attributes).permit!
  end

  def work_log_and_comments_params
    {
        work_log: params.fetch(:work_log, {}).permit(:started_at, :customer_id, :duration, :body, :access_level_id),
        comment: params[:comment]
    }
  end
end