diff --git a/app/controllers/api_v1/activities_controller.rb b/app/controllers/api_v1/activities_controller.rb index ce837ef92b..b17c839001 100644 --- a/app/controllers/api_v1/activities_controller.rb +++ b/app/controllers/api_v1/activities_controller.rb @@ -13,7 +13,7 @@ def index limit(api_limit(:hard => true)) @activities = @activities.threads.except(:order).by_thread if params[:threads] - api_respond @activities, :references => true + api_respond @activities, :references => true, :include => api_include end def show @@ -21,7 +21,7 @@ def show authorize!(:show, @activity) if @activity if @activity - api_respond @activity, :references => true, :include => [:uploads] + api_respond @activity, :references => true, :include => api_include([:uploads]) else api_error :not_found, :type => 'ObjectNotFound', :message => 'Not found' end @@ -47,4 +47,8 @@ def get_target api_error :not_found, :type => 'ObjectNotFound', :message => 'Target not found' end end + + def api_include(default=[]) + [:user, :google_docs, :uploads] & ((params[:include]||[])+default).map(&:to_sym) + end end diff --git a/app/controllers/api_v1/comments_controller.rb b/app/controllers/api_v1/comments_controller.rb index 840de47c8b..5fcc8e7625 100644 --- a/app/controllers/api_v1/comments_controller.rb +++ b/app/controllers/api_v1/comments_controller.rb @@ -6,7 +6,7 @@ def index authorize! :show, @target||current_user context = @target ? @target.comments.where(api_scope) : - Comment.where(:project_id => current_user.project_ids).where(api_scope) + Comment.joins(:project).where(:project_id => current_user.project_ids, :projects => {:archived => false}).where(api_scope) @comments = context.except(:order). where(api_range('comments')). diff --git a/app/controllers/api_v1/conversations_controller.rb b/app/controllers/api_v1/conversations_controller.rb index d476dd1d5e..32a150d093 100644 --- a/app/controllers/api_v1/conversations_controller.rb +++ b/app/controllers/api_v1/conversations_controller.rb @@ -7,7 +7,7 @@ def index context = if @current_project @current_project.conversations.where(api_scope) else - Conversation.where(:project_id => current_user.project_ids).where(api_scope) + Conversation.joins(:project).where(:project_id => current_user.project_ids, :projects => {:archived => false}).where(api_scope) end @conversations = context.except(:order). diff --git a/app/controllers/api_v1/dividers_controller.rb b/app/controllers/api_v1/dividers_controller.rb index ad9a4db8de..8ce1b441c1 100644 --- a/app/controllers/api_v1/dividers_controller.rb +++ b/app/controllers/api_v1/dividers_controller.rb @@ -8,7 +8,7 @@ def index context = if target target.dividers else - Divider.where(:project_id => current_user.project_ids) + Divider.joins(:project).where(:project_id => current_user.project_ids, :projects => {:archived => false}) end.joins(:page) @dividers = context.except(:order). diff --git a/app/controllers/api_v1/notes_controller.rb b/app/controllers/api_v1/notes_controller.rb index 1dccb8c4e1..d9e79c5635 100644 --- a/app/controllers/api_v1/notes_controller.rb +++ b/app/controllers/api_v1/notes_controller.rb @@ -8,7 +8,7 @@ def index context = if target target.notes else - Note.where(:project_id => current_user.project_ids) + Note.joins(:project).where(:project_id => current_user.project_ids, :projects => {:archived => false}) end.joins(:page) @notes = context.except(:order). diff --git a/app/controllers/api_v1/pages_controller.rb b/app/controllers/api_v1/pages_controller.rb index 3d2fecd4bb..57d392309b 100644 --- a/app/controllers/api_v1/pages_controller.rb +++ b/app/controllers/api_v1/pages_controller.rb @@ -7,7 +7,7 @@ def index context = if @current_project @current_project.pages.where(api_scope) else - Page.where(:project_id => current_user.project_ids).where(api_scope) + Page.joins(:project).where(:project_id => current_user.project_ids, :projects => {:archived => false}).where(api_scope) end @pages = context.except(:order). diff --git a/app/controllers/api_v1/task_lists_controller.rb b/app/controllers/api_v1/task_lists_controller.rb index bd1b60ad6e..addaf95c92 100644 --- a/app/controllers/api_v1/task_lists_controller.rb +++ b/app/controllers/api_v1/task_lists_controller.rb @@ -7,7 +7,7 @@ def index context = if @current_project @current_project.task_lists.where(api_scope) else - TaskList.where(:project_id => current_user.project_ids).where(api_scope) + TaskList.joins(:project).where(:project_id => current_user.project_ids, :projects => {:archived => false}).where(api_scope) end @task_lists = context.except(:order). @@ -15,7 +15,7 @@ def index limit(api_limit). order('task_lists.id DESC') - api_respond @task_lists, :references => true + api_respond @task_lists, :references => true, :include => api_include end def show @@ -138,6 +138,6 @@ def api_scope end def api_include - [:tasks, :comments] & (params[:include]||{}).map(&:to_sym) + [:tasks, :unarchived_tasks, :archived_tasks] & (params[:include]||{}).map(&:to_sym) end end \ No newline at end of file diff --git a/app/controllers/api_v1/tasks_controller.rb b/app/controllers/api_v1/tasks_controller.rb index b30b93abfb..f54962858d 100644 --- a/app/controllers/api_v1/tasks_controller.rb +++ b/app/controllers/api_v1/tasks_controller.rb @@ -1,5 +1,6 @@ class ApiV1::TasksController < ApiV1::APIController - before_filter :load_task_list, :only => [:index, :create, :show, :reorder] + before_filter :load_task_list, :only => [:index, :show, :reorder] + before_filter :load_or_create_task_list, :only => [:create] before_filter :load_task, :except => [:index, :create, :reorder] def index @@ -8,7 +9,7 @@ def index context = if @current_project (@task_list || @current_project).tasks.where(api_scope) else - Task.where(:project_id => current_user.project_ids).where(api_scope) + Task.joins(:project).where(:project_id => current_user.project_ids, :projects => {:archived => false}).where(api_scope) end @tasks = context.except(:order). @@ -103,6 +104,15 @@ def load_task api_error :not_found, :type => 'ObjectNotFound', :message => 'Task not found' if @task_list && @task.task_list_id != @task_list.id end + def load_or_create_task_list + if params[:task_list_id] or @current_project.nil? + load_task_list + else + # make or load inbox + @task_list = TaskList.find_or_create_by_name_and_project_id_and_user_id('Inbox', @current_project.id, @current_project.user_id) + end + end + def api_scope conditions = {} unless params[:status].nil? diff --git a/app/controllers/api_v1/uploads_controller.rb b/app/controllers/api_v1/uploads_controller.rb index 5cef20ce8b..c1e4fbd2f9 100644 --- a/app/controllers/api_v1/uploads_controller.rb +++ b/app/controllers/api_v1/uploads_controller.rb @@ -8,7 +8,7 @@ def index context = if target target.uploads.where(api_scope) else - Upload.where(:project_id => current_user.project_ids).where(api_scope) + Upload.joins(:project).where(:project_id => current_user.project_ids, :projects => {:archived => false}).where(api_scope) end @uploads = context.except(:order). diff --git a/app/controllers/tasks_controller.rb b/app/controllers/tasks_controller.rb index 58c3d441a3..d46f34b598 100644 --- a/app/controllers/tasks_controller.rb +++ b/app/controllers/tasks_controller.rb @@ -33,6 +33,7 @@ def create authorize! :make_tasks, @current_project @task = @task_list.tasks.build_by_user(current_user, params[:task]) @task.is_private = (params[:task][:is_private]||false) if params[:task] + @task.updating_user = current_user @task.save respond_to do |f| diff --git a/app/controllers/watchers_controller.rb b/app/controllers/watchers_controller.rb index 1b4da75c09..8801af8118 100644 --- a/app/controllers/watchers_controller.rb +++ b/app/controllers/watchers_controller.rb @@ -2,7 +2,10 @@ class WatchersController < ApplicationController skip_before_filter :load_project def index - @watch_list_by_project = current_user.watchers.includes(:watchable).group_by { |t| t.project.name } + @watch_list_by_project = current_user.watchers.includes(:watchable).reject { |t| + t.project.nil? }.group_by { |t| + t.project.name + } end def unwatch @@ -10,4 +13,4 @@ def unwatch @watch.destroy head :ok end -end \ No newline at end of file +end.nil? \ No newline at end of file diff --git a/app/helpers/tasks_helper.rb b/app/helpers/tasks_helper.rb index 82fbcc90b8..73a280da5e 100644 --- a/app/helpers/tasks_helper.rb +++ b/app/helpers/tasks_helper.rb @@ -9,6 +9,7 @@ def task_classes(task) classes << 'due_3weeks' if task.due_in?(3.weeks) classes << 'due_month' if task.due_in?(1.months) classes << 'overdue' if task.overdue? + classes << 'urgent' if task.urgent? classes << 'unassigned_date' if task.due_on.nil? classes << "status_#{task.status_name}" classes << 'status_notopen' if !task.open? @@ -27,7 +28,11 @@ def sidebar_tasks(tasks) end def render_due_on(task,user) - content_tag(:span, due_on(task), :class => 'due_on') + if task.urgent? + content_tag(:span, "!".html_safe, :class => 'urgent') + else + content_tag(:span, due_on(task), :class => 'due_on') + end end def render_assignment(task,user) @@ -51,17 +56,18 @@ def comment_task_status(comment) end def comment_task_due_on(comment) - if comment.due_on_change? - [].tap { |out| - if comment.due_on_transition? - out << span_for_due_date(comment.previous_due_on) + if comment.urgent_change? || comment.due_on_change? + [].tap do |out| + if comment.due_on_transition? || comment.urgent_transition? + out << (comment.previous_urgent? ? span_for_urgent(comment) : + span_for_due_date(comment.previous_due_on)) out << content_tag(:span, '→'.html_safe, :class => "arr due_on_arr") end - out << span_for_due_date(comment.due_on) - }.join(' ').html_safe + out << (comment.urgent? ? span_for_urgent(comment) : span_for_due_date(comment.due_on)) + end.join(' ').html_safe end end - + def task_status(task,status_type) status_for_column = status_type == :column ? "task_status_#{task.status_name}" : "task_counter" out = %() @@ -128,11 +134,21 @@ def time_tracking_doc def date_picker(f, field, options = {}, html_options = {}) selected_date = f.object.send(field.to_sym) ? localize(f.object.send(field.to_sym), :format => :long) : '' - - content_tag :div, :class => "date_picker", :id => "#{f.object.class.to_s.underscore}_#{f.object.id}_#{field}" do + show_urgent_flag = [Task, Conversation].include?(f.object.class) + datepicker_info = if show_urgent_flag && f.object.urgent? + t('date_picker.urgent.short') + elsif selected_date.blank? + t('date_picker.no_date_assigned') + else + selected_date + end + + classes = ["date_picker", ("show_urgent" if show_urgent_flag)].compact + content_tag :div, :class => classes.join(" "), :id => "#{f.object.class.to_s.underscore}_#{f.object.id}_#{field}" do [ image_tag('/images/calendar_date_select/calendar.gif', :class => :calendar_date_select_popup_icon), - content_tag(:span, selected_date.blank? ? t('date_picker.no_date_assigned') : selected_date, :class => 'localized_date'), - f.hidden_field(field, html_options.reverse_merge!(:class => :datepicker)) + content_tag(:span, datepicker_info, :class => 'datepicker_info'), + f.hidden_field(field, html_options.reverse_merge(:class => :datepicker)), + (f.hidden_field("urgent", html_options.reverse_merge(:class => :urgent)) if show_urgent_flag), ].join.html_safe end end @@ -147,6 +163,10 @@ def span_for_due_date(due_date) content_tag(:span, task_due_on(due_date), :class => "assigned_date") end + + def span_for_urgent(comment) + content_tag(:span, t("tasks.urgent.caption"), :class => 'urgent') + end def span_for_thread_due_date(task) content_tag(:span, due_on(task), diff --git a/app/javascripts/date_picker.js b/app/javascripts/date_picker.js index 6370c9be4c..27ee864f97 100644 --- a/app/javascripts/date_picker.js +++ b/app/javascripts/date_picker.js @@ -2,12 +2,15 @@ document.on('click', 'div.date_picker', function(e, element) { var field = element.down('input') var label = element.down('span') var parentDiv = element.up('div') - DatePicker.initialize(field, label, parentDiv) -}) + var date_picker = DatePicker.initialize(field, label, parentDiv); + if (element.hasClassName("show_urgent")) { + DatePicker.add_urgent_box(date_picker, element, field, label, parentDiv); + } +}); DatePicker = { initialize: function(field, label, parentDiv) { - new CalendarDateSelect(field, { + return(new CalendarDateSelect(field, { buttons: true, time: false, year_range: 10, @@ -26,9 +29,55 @@ DatePicker = { } else { localized_time = this.value } + label.update(localized_time || I18n.translations.date_picker.no_date_assigned); + } + })); + }, + + add_urgent_box: function(date_picker, element, field, label, parentDiv) { + // Render custom urgent box on calendar's top DIV + var urgent_field = field.parentNode.down("input.urgent"); + var html = Mustache.to_html(Templates.tasks.calendar_date_select_urgent_header, { + task_id: element.id.split("_")[1] + }); + date_picker.top_div.update(html); - label.update(localized_time) + // Link toggler for help info + date_picker.top_div.down(".show-help").observe("click", function(event) { + date_picker.top_div.down(".help").toggle(); + event.stop(); + }); + + // On urgent checkbox changes update task[urgent] and show/hide sections accordingly + var update_urgent_box = function (date_picker, input_urgent, user_action) { + urgent_field.value = input_urgent.checked ? "1" : "0"; + urgent_field.removeAttribute("disabled"); + + if (input_urgent.checked) { + label.update(I18n.translations.date_picker.urgent.short); + } else if (user_action) { + date_picker.clearDate(); + date_picker.callback("onchange"); + label.update(I18n.translations.date_picker.no_date_assigned); } - }) - } -} \ No newline at end of file + + if (user_action && input_urgent.checked) { + date_picker.close(); + } else { + date_picker.calendar_div.select("> div").each(function(div) { + if (!div.hasClassName("cds_top")) { + div[input_urgent.checked ? "hide" : "show"](); + } + }); + } + date_picker.positionCalendarDiv(); + } + + var input_urgent = date_picker.top_div.down("input.urgent") + input_urgent.checked = (urgent_field.value == "1"); + update_urgent_box(date_picker, input_urgent, false); + input_urgent.observe("click", function() { + update_urgent_box(date_picker, this, true); + }); + } +} diff --git a/app/javascripts/task.js b/app/javascripts/task.js index ae6c66116d..41a7c9d84d 100644 --- a/app/javascripts/task.js +++ b/app/javascripts/task.js @@ -26,6 +26,7 @@ document.on('ajax:success', '.task_header + form.edit_task', function(e, form) { }) Task = { + sortableChange: function(draggable) { this.currentDraggable = draggable }, @@ -344,3 +345,4 @@ document.on('ajax:success', '.task_list form.new_task', function(e, form) { Form.reset(form).focusFirstElement().up('.task_list').down('.tasks').insert(response) }) + diff --git a/app/javascripts/upload.js b/app/javascripts/upload.js index 527d8df5f8..66dd43dac7 100644 --- a/app/javascripts/upload.js +++ b/app/javascripts/upload.js @@ -62,3 +62,21 @@ document.on('click', '.uploads .upload .header .file a', function(e, el) { document.on('click', '.uploads .upload .header', function(e, el) { toggle_task_row(el); }); + +var toggle_upload_more = function(el) { + var reference = el.up('.upload_file') ? el.up('.upload_file').down('.reference') : el.up('.upload_thumbnail').down('.reference'); + if(reference) { + if (reference.visible()) { + reference.hide(); + } else { + $$('.upload_file .reference').invoke('hide'); + $$('.upload_thumbnail .reference').invoke('hide'); + reference.show(); + } + } + return false; +}; + +document.on('click', '.more span', function(e, el) { + toggle_upload_more(el); +}); diff --git a/app/models/comment.rb b/app/models/comment.rb index 5f9e746a85..02ea6a443f 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -39,7 +39,7 @@ def previous_assigned :reject_if => lambda { |google_docs| google_docs['title'].blank? || google_docs['url'].blank? } attr_accessible :body, :status, :assigned, :hours, :human_hours, :billable, - :upload_ids, :uploads_attributes, :due_on, :google_docs_attributes, :private_ids, :is_private + :upload_ids, :uploads_attributes, :due_on, :urgent, :google_docs_attributes, :private_ids, :is_private attr_accessor :is_importing attr_accessor :private_ids @@ -246,6 +246,7 @@ def cleanup_task if @last_comment_in_task self.target.assigned_id = previous_assigned_id self.target.due_on = previous_due_on + self.target.urgent = previous_urgent self.target.status = previous_status || Task::STATUSES[:open] self.target.save! end diff --git a/app/models/comment/conversions.rb b/app/models/comment/conversions.rb index 24fac910d8..302c7c6f0b 100644 --- a/app/models/comment/conversions.rb +++ b/app/models/comment/conversions.rb @@ -54,6 +54,8 @@ def to_api_hash(options = {}) base[:status] = status base[:due_on] = due_on base[:previous_due_on] = previous_due_on + base[:urgent] = urgent + base[:previous_urgent] = previous_urgent end if Array(options[:include]).include?(:uploads) && uploads.any? diff --git a/app/models/comment/tasks.rb b/app/models/comment/tasks.rb index c7510ff1c9..00c26d18b4 100644 --- a/app/models/comment/tasks.rb +++ b/app/models/comment/tasks.rb @@ -5,7 +5,7 @@ def previously_closed? end def transition? - status_transition? || assigned_transition? || due_on_change? + status_transition? || assigned_transition? || due_on_change? || urgent_change? end def initial_status? @@ -20,6 +20,14 @@ def due_on_change? due_on != previous_due_on end + def urgent_change? + urgent != previous_urgent + end + + def urgent_transition? + urgent? != previous_urgent? and previous_urgent? + end + def assigned_transition? assigned_id != previous_assigned_id end @@ -60,4 +68,4 @@ def previous_status_name Task::STATUS_NAMES[previous_status] end -end \ No newline at end of file +end diff --git a/app/models/conversation/tasks.rb b/app/models/conversation/tasks.rb index 193f059d40..12df99aaf3 100644 --- a/app/models/conversation/tasks.rb +++ b/app/models/conversation/tasks.rb @@ -1,7 +1,11 @@ class Conversation - attr_accessor :due_on, :status, :assigned_id, :task_list_id - attr_accessible :due_on, :status, :assigned_id, :task_list_id + attr_accessor :due_on, :urgent, :status, :assigned_id, :task_list_id + attr_accessible :due_on, :urgent, :status, :assigned_id, :task_list_id + def urgent? + !!urgent + end + def convert_to_task! task_list = task_list_id.blank? ? nil : TaskList.find(task_list_id) task_list ||= TaskList.find_or_create_by_name_and_project_id_and_user_id('Inbox', project.id, user.id) @@ -11,6 +15,8 @@ def convert_to_task! t.name = name t.status = status.blank? ? 0 : status t.due_on = due_on + t.urgent = urgent + t.urgent = t.urgent.nil? ? false : t.urgent t.user = user t.updating_user = updating_user t.task_list = task_list diff --git a/app/models/emailer.rb b/app/models/emailer.rb index 384c41d55e..adc41a8751 100644 --- a/app/models/emailer.rb +++ b/app/models/emailer.rb @@ -240,6 +240,7 @@ def project_digest(user_id, person_id, project_id, target_types_and_ids, comment @person = Person.find(person_id) @project = Project.find(project_id) @targets = target_types_and_ids.map do |target| + target = target.with_indifferent_access target[:target_type].constantize.find_by_id target[:target_id] end.compact.uniq @comments = Comment.where(:id => comment_ids) diff --git a/app/models/organization.rb b/app/models/organization.rb index e31a5354ff..23d31ed91c 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -13,7 +13,7 @@ class Organization < ActiveRecord::Base has_many :task_list_templates - validates_length_of :name, :minimum => 4 + validates_length_of :name, :minimum => 1 validates_presence_of :permalink validates_uniqueness_of :permalink, :case_sensitive => false, :scope => :deleted diff --git a/app/models/project/associations.rb b/app/models/project/associations.rb index 117123252b..4813cee706 100644 --- a/app/models/project/associations.rb +++ b/app/models/project/associations.rb @@ -12,6 +12,7 @@ class Project delete.has_many :folders delete.has_many :notes delete.has_many :dividers + delete.has_many :watcher_tags, :class_name => 'Watcher' delete.with_options :order => 'id DESC' do |ordered| ordered.has_many :conversations diff --git a/app/models/project/validation.rb b/app/models/project/validation.rb index 341eda77e3..fea03a8cb7 100644 --- a/app/models/project/validation.rb +++ b/app/models/project/validation.rb @@ -1,8 +1,7 @@ class Project - validates_length_of :name, :minimum => 5, :on => :create # New projects - validates_length_of :name, :minimum => 5, :on => :update, :if => :name_changed? # Changing the name - validates_length_of :name, :minimum => 3, :on => :update # Legacy validation for existing projects + validates_length_of :name, :minimum => 1, :on => :create + validates_length_of :name, :minimum => 1, :on => :update validates_uniqueness_of :permalink, :case_sensitive => false, :scope => :deleted validates_length_of :permalink, :minimum => 5 validates_format_of :permalink, :with => /^[a-z0-9_\-]{5,}$/, :if => :permalink_length_valid? diff --git a/app/models/task.rb b/app/models/task.rb index a65af2969d..262af41ac3 100644 --- a/app/models/task.rb +++ b/app/models/task.rb @@ -24,7 +24,7 @@ class Task < RoleRecord accepts_nested_attributes_for :comments, :allow_destroy => false, :reject_if => lambda { |comment| %w[is_private body hours human_hours uploads_attributes google_docs_attributes].all? { |k| comment[k].blank? } } - attr_accessible :name, :assigned_id, :status, :due_on, :comments_attributes, :user, :task_list_id + attr_accessible :name, :assigned_id, :status, :due_on, :comments_attributes, :user, :task_list_id, :urgent validates_presence_of :user validates_presence_of :task_list @@ -49,6 +49,7 @@ class Task < RoleRecord before_save :save_completed_at before_validation :remember_comment_created, :on => :update before_save :update_google_calendar_event, :if => lambda {|t| t.assigned.try(:user) || !t.google_calendar_url_token.blank? } + before_validation :nilize_due_on_for_urgent_tasks def assigned @assigned ||= assigned_id ? Person.with_deleted.find_by_id(assigned_id) : nil @@ -56,7 +57,7 @@ def assigned def track_changes? (new_record? and not status_new?) or - (updating_user and (status_changed? or assigned_id_changed? or due_on_changed?)) + (updating_user and (status_changed? or assigned_id_changed? or due_on_changed? or urgent_changed?)) end def archived? @@ -299,7 +300,7 @@ def set_comments_author # before_save end def remember_comment_created # before_update - @comment_created = comments.any?(&:new_record?) || assigned_id_changed? || status_changed? || due_on_changed? + @comment_created = comments.any?(&:new_record?) || assigned_id_changed? || status_changed? || due_on_changed? || urgent_changed? true end @@ -331,6 +332,11 @@ def save_changes_to_comment # before_save comment.previous_due_on = self.due_on_was if due_on_changed? end + if urgent_changed? or self.new_record? + comment.urgent = self.urgent + comment.previous_urgent = self.urgent_was if urgent_changed? + end + @saved_changes_to_comment = true true end @@ -465,4 +471,8 @@ def delete_old_events_if_required self.google_calendar_url_token = nil end end + + def nilize_due_on_for_urgent_tasks + self.due_on = nil if self.urgent? + end end diff --git a/app/models/task/conversions.rb b/app/models/task/conversions.rb index 6db96a86f5..0df2ce7392 100644 --- a/app/models/task/conversions.rb +++ b/app/models/task/conversions.rb @@ -12,6 +12,7 @@ def to_xml(options = {}) xml.tag! 'assigned-id', assigned_id xml.tag! 'status', status xml.tag! 'due-on', due_on.to_s(:db) if due_on + xml.tag! 'urgent', urgent? xml.tag! 'created-at', created_at.to_s(:db) xml.tag! 'updated-at', updated_at.to_s(:db) xml.tag! 'completed-at', completed_at.to_s(:db) if completed_at @@ -41,6 +42,7 @@ def to_api_hash(options = {}) base[:type] = self.class.to_s if options[:emit_type] base[:due_on] = due_on.to_s(:db) if due_on + base[:urgent] = urgent? base[:completed_at] = completed_at.to_s(:db) if completed_at if Array(options[:include]).include? :task_list @@ -70,4 +72,4 @@ def to_api_hash(options = {}) base end -end \ No newline at end of file +end diff --git a/app/models/task_list/conversions.rb b/app/models/task_list/conversions.rb index 4f79afd22d..55629d9603 100644 --- a/app/models/task_list/conversions.rb +++ b/app/models/task_list/conversions.rb @@ -39,6 +39,10 @@ def to_api_hash(options = {}) if Array(options[:include]).include? :tasks base[:tasks] = tasks.map {|t| t.to_api_hash(options)} + elsif Array(options[:include]).include? :unarchived_tasks + base[:tasks] = tasks.unarchived.map {|t| t.to_api_hash(options)} + elsif Array(options[:include]).include? :archived_tasks + base[:tasks] = tasks.archived.map {|t| t.to_api_hash(options)} end base diff --git a/app/models/user.rb b/app/models/user.rb index 61cc6ff3ac..b0882598c0 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -260,7 +260,7 @@ def pending_tasks Rails.cache.fetch("pending_tasks.#{id}") do active_project_ids.empty? ? [] : Task.where(:status => Task::ACTIVE_STATUS_CODES).where(:assigned_id => active_project_ids).order('ID desc').includes(:project). - sort { |a,b| (a.due_on || 1.week.from_now.to_date) <=> (b.due_on || 1.year.from_now.to_date) } + sort { |a,b| [a.urgent? ? 1 : 0, (a.due_on || 1.week.from_now.to_date)] <=> [b.urgent? ? 1 : 0, (b.due_on || 1.year.from_now.to_date)] } end end diff --git a/app/styles/_calendar_date_select.sass b/app/styles/_calendar_date_select.sass index 843efcec1f..b31533492b 100644 --- a/app/styles/_calendar_date_select.sass +++ b/app/styles/_calendar_date_select.sass @@ -7,7 +7,7 @@ -webkit-box-shadow: 0 0 5px rgb(153, 153, 153) background: white display: block - width: 250px + width: 280px th font-size: 11px text-align: center @@ -29,6 +29,17 @@ border: right: solid 1px rgb(140, 140, 140) bottom: solid 1px rgb(140, 140, 140) + .cds_top + .urgent + margin: 10px 0 + text-align: center + .help + margin: 5px 0 + color: #777 + font-size: 12px + .show-help + position: relative + top: 3px .cds_header clear: both text-align: center diff --git a/app/styles/_tasks.sass b/app/styles/_tasks.sass index 81b1876fc0..1127c3596d 100644 --- a/app/styles/_tasks.sass +++ b/app/styles/_tasks.sass @@ -190,7 +190,7 @@ form.new_task, form.edit_task, .task_list_form padding-left: 4px .lock_icon display: none - span.assigned_date, span.assigned_user + span.assigned_date, span.assigned_user, span.urgent display: none color: rgb(100,100,100) padding: 1px 5px @@ -223,6 +223,14 @@ form.new_task, form.edit_task, .task_list_form font-weight: bold span.assigned_date, span.assigned_user color: rgb(200,0,0) + &.urgent + .taskName + a.name + color: rgb(150,0,0) + font-weight: bold + span.urgent + color: rgb(150,0,0) + display: inline-block .task.status_open.mine background: rgb(255,255,210) .taskName @@ -363,7 +371,7 @@ p.assigned_transition text-decoration: line-through color: #999 -.assigned_date +.assigned_date, .comment span.urgent, .taskName span.urgent color: rgb(100,100,100) padding: 0px 5px margin-right: 5px diff --git a/app/styles/_uploads.sass b/app/styles/_uploads.sass index 6588b3e54c..b4ab36d042 100644 --- a/app/styles/_uploads.sass +++ b/app/styles/_uploads.sass @@ -53,26 +53,6 @@ $upload_icon_width: 16px font-size: 10px &:hover .more display: inline-block - .reference - display: inline-block - position: absolute - border: 1px solid #aaa - border-top: 0 - top: 25px - right: -1px - background-color: #ffe - z-index: 100 - a - display: block - padding: 5px 15px - color: black - &:hover - background-color: $navbar - color: black - text-decoration: none - cursor: pointer - a[data-method="delete"] - color: red .uploads_current, .upload_images margin: 5px @@ -95,11 +75,15 @@ $upload_icon_width: 16px img margin-right: 5px padding-top: 2px - span + span, div.more + display: inline-block position: relative top: -2px - .size color: rgb(120, 120, 120) + a.label + color: rgb(120, 120, 120) + text-decoration: underline + cursor: pointer .activity .upload_list @@ -146,4 +130,34 @@ $upload_icon_width: 16px margin: 25px 0 font-size: 12px font-weight: bold - a + +.upload_file, .file_upload + .reference + display: inline-block + position: absolute + min-width: 250px + border: 1px solid #aaa + border-top: 0 + top: 25px + right: -1px + background-color: #ffe + z-index: 100 + a + display: block + padding: 5px 15px + color: black + &:hover + background-color: $navbar + color: black + text-decoration: none + cursor: pointer + a[data-method="delete"] + color: red + +div.more:hover + .reference + display: block !important + top: 14px + right: auto + border: 1px solid #aaa + +box-shadow(#777 1px 2px 10px) diff --git a/app/templates/tasks/calendar_date_select_urgent_header.haml b/app/templates/tasks/calendar_date_select_urgent_header.haml new file mode 100644 index 0000000000..902eedfb7a --- /dev/null +++ b/app/templates/tasks/calendar_date_select_urgent_header.haml @@ -0,0 +1,7 @@ +.urgent + %input.urgent{:type => 'checkbox', :value => '1', :id => 'task_{{task_id}}_urgent'} + %label{:for => 'task_{{task_id}}_urgent'} + =t('date_picker.urgent.long') + %a{:href => '#', :class => 'show-help text_actions'} [?] + .help{:style => 'display: none'} + =t('date_picker.urgent.info') diff --git a/app/views/shared/_navigation.html.haml b/app/views/shared/_navigation.html.haml index b8f5a22030..5d5902e4e9 100644 --- a/app/views/shared/_navigation.html.haml +++ b/app/views/shared/_navigation.html.haml @@ -11,7 +11,7 @@ -# My Tasks - near_tasks = current_user.nearest_pending_tasks - - today_tasks = near_tasks.select { |t| t.due_on && (t.due_on <= Time.now.to_date) } + - today_tasks = near_tasks.select { |t| t.urgent? || (t.due_on && (t.due_on <= Time.now.to_date)) } - late_tasks = near_tasks.select { |t| t.due_on && (t.due_on < Time.now.to_date) } - pending_tasks = near_tasks - today_tasks - if today_tasks.any? diff --git a/app/views/tasks/_task.haml b/app/views/tasks/_task.haml index bd4b1911dd..c69d9659d2 100644 --- a/app/views/tasks/_task.haml +++ b/app/views/tasks/_task.haml @@ -13,6 +13,8 @@ .lock_icon = link_to h(task), [project, task], :class => :name %span.assigned_date= task.due_on ? due_on(task) : '' + - if task.urgent? + %span.urgent= t("tasks.urgent.caption") %span.assigned_user= task.assigned ? link_to(h(task.assigned.short_name), user_path(task.assigned.user)) : '' - unless @current_project .extended diff --git a/app/views/uploads/_file.haml b/app/views/uploads/_file.haml index f817cb1fdb..1d53d9909d 100644 --- a/app/views/uploads/_file.haml +++ b/app/views/uploads/_file.haml @@ -3,4 +3,5 @@ %span.filename = link_to_upload(upload, upload.file_name) %span.size - = '(' + number_to_human_size(upload.size).to_s + ')' \ No newline at end of file + = '(' + number_to_human_size(upload.size).to_s + ')' + = render :partial => "uploads/more", :locals => {:upload => upload} \ No newline at end of file diff --git a/app/views/uploads/_more.haml b/app/views/uploads/_more.haml new file mode 100644 index 0000000000..9941aaccaa --- /dev/null +++ b/app/views/uploads/_more.haml @@ -0,0 +1,5 @@ +.more + %a.label More... + .reference{ :style => 'display: none;' } + = link_to t('uploads.public_download.link.anchor'), public_download_project_upload_path(upload.project, upload), :rel => 'facebox', :title => t('uploads.public_download.link.title') + = link_to_upload upload, t('.download') diff --git a/app/views/uploads/_thumbnail.haml b/app/views/uploads/_thumbnail.haml index 9a14d9bbf7..687fd3f59c 100644 --- a/app/views/uploads/_thumbnail.haml +++ b/app/views/uploads/_thumbnail.haml @@ -1,2 +1,3 @@ .upload_thumbnail{:id => "upload_#{upload.id}"} - = upload_link_with_thumbnail(upload, :small) \ No newline at end of file + = upload_link_with_thumbnail(upload, :small) + = render :partial => "uploads/more", :locals => {:upload => upload} \ No newline at end of file diff --git a/app/views/uploads/_upload.haml b/app/views/uploads/_upload.haml index 733826f4cd..adee4cd308 100644 --- a/app/views/uploads/_upload.haml +++ b/app/views/uploads/_upload.haml @@ -15,8 +15,8 @@ = link_to_upload upload, t('.download'), {:rel => nil} = link_to t('uploads.public_download.link.anchor'), public_download_project_upload_path(upload.project, upload), :rel => 'facebox', :title => t('uploads.public_download.link.title') - if can?(:update, upload) # and request.format != :m - = link_to t('.remove'), [upload.project, upload], :'data-remote' => true, - :'data-method' => 'delete', :'data-confirm' => t('confirm.delete_upload') = link_to t('common.rename'), [:edit, upload.project, upload], :remote => true, :class => "link_rename" + = link_to t('.remove'), [upload.project, upload], :'data-remote' => true, + :'data-method' => 'delete', :'data-confirm' => t('confirm.delete_upload') diff --git a/config/locales/en.yml b/config/locales/en.yml index eb16f47852..21b0bb58b0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -920,6 +920,10 @@ en: due_on_html: Needs to be done before... optional assigned_to: Who is responsible? assigned_to_nobody: Nobody + urgent: + caption: Urgent + set: Urgent flag set + unset: Urgent flag unset assigned: assigned_to: "Assigned to %{user}" myself: "Assign to myself (%{user})" @@ -2033,6 +2037,10 @@ en: date_picker: no_date_assigned: No date assigned + urgent: + long: "Urgent, to be done as soon as possible" + short: "Urgent" + info: Will show up on "tasks due for today" for the assigned user oauth: authentication_failure: Authentication failure, '%{message}' diff --git a/config/routes.rb b/config/routes.rb index 0f5475a9b7..cfddf04fbd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -298,7 +298,7 @@ def self.matches?(request) resources :tasks, :except => [:new, :edit] end - resources :tasks, :except => [:new, :edit, :create] do + resources :tasks, :except => [:new, :edit] do member do put :watch put :unwatch diff --git a/config/teambox.yml b/config/teambox.yml index 3dfd8883fb..184a999a85 100644 --- a/config/teambox.yml +++ b/config/teambox.yml @@ -144,6 +144,7 @@ defaults: &defaults - "*.tasks" - "*.projects" - "*.conversations" + - "*.date_picker" development: <<: *defaults diff --git a/db/migrate/20110818091722_add_urgent_flag_to_tasks_and_comments.rb b/db/migrate/20110818091722_add_urgent_flag_to_tasks_and_comments.rb new file mode 100644 index 0000000000..7818138964 --- /dev/null +++ b/db/migrate/20110818091722_add_urgent_flag_to_tasks_and_comments.rb @@ -0,0 +1,12 @@ +class AddUrgentFlagToTasksAndComments < ActiveRecord::Migration + def self.up + add_column :tasks, :urgent, :boolean, :default => false, :null => false + add_column :comments, :urgent, :boolean, :default => false, :null => false + add_column :comments, :previous_urgent, :boolean, :default => false, :null => false + end + + def self.down + remove_column :tasks, :urgent + remove_column :comments, :urgent, :previous_urgent + end +end diff --git a/db/schema.rb b/db/schema.rb index 40df074787..fe149a3000 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20110812102452) do +ActiveRecord::Schema.define(:version => 20110818091722) do create_table "activities", :force => true do |t| t.integer "user_id" @@ -104,6 +104,8 @@ t.integer "uploads_count", :default => 0 t.boolean "deleted", :default => false, :null => false t.boolean "is_private", :default => false, :null => false + t.boolean "urgent", :default => false, :null => false + t.boolean "previous_urgent", :default => false, :null => false end add_index "comments", ["created_at"], :name => "index_comments_on_created_at" @@ -466,6 +468,7 @@ t.boolean "deleted", :default => false, :null => false t.boolean "is_private", :default => false, :null => false t.string "google_calendar_url_token" + t.boolean "urgent", :default => false, :null => false end add_index "tasks", ["assigned_id"], :name => "index_tasks_on_assigned_id" diff --git a/features/conversation_convert_to_task.feature b/features/conversation_convert_to_task.feature index 45b91bcee5..027323f346 100644 --- a/features/conversation_convert_to_task.feature +++ b/features/conversation_convert_to_task.feature @@ -109,3 +109,18 @@ Feature: Converting a conversation to a task And I wait for 3 seconds And I am going to the bookmarked link Then I should see "An exciting task for you" in the task thread title + + Scenario: I can set the urgent flag for the task being created + Given I started a simple conversation + When I go to the home page + And I click the conversation's comment box + And I follow "Convert to task" + And I wait for 2 seconds + And I fill in "conversation_name" with "Give git course" + And I click the element that contain "No date assigned" within ".date_picker" + And I check "Urgent, to be done as soon as possible" within ".calendar_date_select" + Then I should see "Urgent" within ".fields" + When I press "Convert" + And I wait for 3 seconds + Then I should see "Give git course" in the task thread title + And I should see 'Urgent' diff --git a/features/step_definitions/task_steps.rb b/features/step_definitions/task_steps.rb index 39f516056b..0eb9dbaf7a 100644 --- a/features/step_definitions/task_steps.rb +++ b/features/step_definitions/task_steps.rb @@ -8,6 +8,28 @@ @task = project.create_task(@current_user, task_list, {:name => name}) end +Given /^I have a task called "([^"]*)" with a comment including upload "([^"]*)"$/ do |task_name, file_name| + + Given %(I have a task called "#{task_name}") + @comment = @task.comments.create :body => "Something to say" + + path = File.join(Rails.root, "spec/fixtures/#{file_name}") + if File.exists?(path) + @upload = Factory.create(:upload, { + :asset => open(path), + :asset_file_name => file_name, + :asset_file_size => nil, + :asset_content_type => nil, + :project => @current_project, + :comment => @comment + }) + + else + Factory.create(:upload, :asset_file_name => file_name, :project => @current_project, :comment => @comment) + end + +end + ## FIXME: it's better for 'givens' to set tasks up directly in the db: Given /^the following tasks? with associations exists?:?$/ do |table| @@ -176,6 +198,10 @@ Then %(I should see "#{text}" within ".task_header h2") end +Then /^I should see "([^"]*)" within the task actions$/ do |text| + Then %(I should see "#{text}" within ".task .actions") +end + When /^(?:|I )select "([^\"]*)" in the "([^\"]*)" calendar?$/ do |number, calendar| with_css_scope("div[id$='_#{calender}_on']") do |node| find(:css,"table div[contains(#{number})]").click diff --git a/features/step_definitions/web_steps.rb b/features/step_definitions/web_steps.rb index 271c5c373a..ad5509d373 100644 --- a/features/step_definitions/web_steps.rb +++ b/features/step_definitions/web_steps.rb @@ -97,7 +97,7 @@ def with_css_scope(selector) When /^(?:|I )click the element that contain "([^\"]*)"(?: within "([^\"]*)")?$/ do |text, selector| with_css_scope(selector) do |node| - node.find(:xpath,"//*[.='#{text}']").click + node.find(:xpath,".//*[.='#{text}']").click end end diff --git a/features/task_create.feature b/features/task_create.feature index 3d923bf846..c3f835b948 100644 --- a/features/task_create.feature +++ b/features/task_create.feature @@ -26,6 +26,17 @@ Feature: Creating a task And I wait for 1 second And I should see "Ohhh upload" as a task name + Scenario: Mislav creates a valid task with urgent flag + When I go to the "Awesome Ruby Yahh" task list page of the "Ruby Rockstars" project + When I follow "+ Add Task" + And I fill in "Task title" with "Ohhh upload" + And I click the element that contain "No date assigned" within "#new_task" + And I check "Urgent, to be done as soon as possible" within ".calendar_date_select" + And I press "Add Task" + And I wait for 1 second + And I should see "Ohhh upload" as a task name + And I should see "Urgent" within "span.urgent" + Scenario: Fails to create a task without a title When I go to the "Awesome Ruby Yahh" task list page of the "Ruby Rockstars" project And I follow "+ Add Task" diff --git a/features/task_edit.feature b/features/task_edit.feature index 8890f4ad52..b88a5d2937 100644 --- a/features/task_edit.feature +++ b/features/task_edit.feature @@ -19,3 +19,12 @@ Feature: Editing a task And I press "Update Task" And I wait for 1 second Then I should see "Fix minor bug" within the task header + + Scenario: I set the urgent flag for the task + When I follow "Fix major bug" + And I wait for 1 second + And I click the element that contain "No date assigned" within ".task .date_picker" + And I check "Urgent, to be done as soon as possible" within ".calendar_date_select" + And I press "Save" + Then I should see "Urgent" within the task actions + And I should see "Urgent" within "span.urgent" diff --git a/features/upload_more_options.feature b/features/upload_more_options.feature new file mode 100644 index 0000000000..593b89074c --- /dev/null +++ b/features/upload_more_options.feature @@ -0,0 +1,15 @@ +@uploads +Feature: Uploading a file + + Background: + Given a project exists with name: "Ruby Rockstars" + And a confirmed user exists with login: "mislav", first_name: "Mislav", last_name: "Marohnić" + And I am logged in as @mislav + And I am currently in the project ruby_rockstars + And I have a task called "Railscast Theme" with a comment including upload "tb-space.jpg" + + Scenario: Upload has more options in activity feed + When I go to the project page + And I click the element that contain "More..." + Then I should see "Download" within ".reference" + And I should see "Send this file to somebody..." within ".reference" \ No newline at end of file diff --git a/spec/controllers/api_v1/activities_controller_spec.rb b/spec/controllers/api_v1/activities_controller_spec.rb index 11d9ecafe9..5bb30c8e56 100644 --- a/spec/controllers/api_v1/activities_controller_spec.rb +++ b/spec/controllers/api_v1/activities_controller_spec.rb @@ -17,6 +17,42 @@ JSON.parse(response.body)['objects'].map{|a| a['id'].to_i}.sort.should == (@project.activity_ids+@other_project.activity_ids).sort end + + it "shows uploads in comments when requested" do + login_as @user + + @conversation = Factory.create(:conversation, :project => @project, :body => 'Test conversation') + @task = Factory.create(:conversation, :project => @project, :body => 'Test conversation') + @task.updating_user = @task.user + @task.update_attributes(:comments_attributes => {'0' => {'body' => 'Test'}}) + + @upload = @project.uploads.new({:asset => mock_uploader('semicolons.js', 'application/javascript', "alert('what?!')")}) + @upload.comment = @conversation.comments.first + @upload.user = @user + @upload.save! + + @other_upload = @project.uploads.new({:asset => mock_uploader('jquery.js', 'application/javascript', ";")}) + @other_upload.comment = @task.comments(true).first + @other_upload.user = @user + @other_upload.save! + + get :index, :include => [:uploads, :google_docs] + response.should be_success + + data = JSON.parse(response.body) + references = data['references'].map{|r| "#{r['id']}_#{r['type']}"} + references.include?("#{@upload.id}_Upload").should == true + references.include?("#{@other_upload.id}_Upload").should == true + + comment_ids = [@task.comments(true).first.id, @conversation.comments.first.id] + comments = data['references'].reject{ |o| !(o['type'] == 'Comment' && comment_ids.include?(o['id'])) } + comments.length.should == 2 + + comments.each do |comment| + comment['uploads'].should_not == nil + comment['upload_ids'].should_not == nil + end + end it "shows activities as JSON when requested with :text format" do login_as @user diff --git a/spec/controllers/api_v1/comments_controller_spec.rb b/spec/controllers/api_v1/comments_controller_spec.rb index 34ca38d290..b5207468f5 100644 --- a/spec/controllers/api_v1/comments_controller_spec.rb +++ b/spec/controllers/api_v1/comments_controller_spec.rb @@ -49,6 +49,16 @@ JSON.parse(response.body)['objects'].length.should == 2 end + it "shows no comments for archived projects" do + login_as @user + @project.update_attribute :archived, true + + get :index + response.should be_success + + JSON.parse(response.body)['objects'].length.should == 0 + end + it "shows comments created by a user" do login_as @user diff --git a/spec/controllers/api_v1/conversations_controller_spec.rb b/spec/controllers/api_v1/conversations_controller_spec.rb index 0e0cbc7205..db6affa8ed 100644 --- a/spec/controllers/api_v1/conversations_controller_spec.rb +++ b/spec/controllers/api_v1/conversations_controller_spec.rb @@ -55,6 +55,16 @@ JSON.parse(response.body)['objects'].length.should == 3 end + it "shows no conversations for archived projects" do + login_as @user + @project.update_attribute :archived, true + + get :index + response.should be_success + + JSON.parse(response.body)['objects'].length.should == 0 + end + it "shows conversations created by a user" do login_as @user diff --git a/spec/controllers/api_v1/pages_controller_spec.rb b/spec/controllers/api_v1/pages_controller_spec.rb index 78ef3663e7..160e3bf468 100644 --- a/spec/controllers/api_v1/pages_controller_spec.rb +++ b/spec/controllers/api_v1/pages_controller_spec.rb @@ -48,6 +48,16 @@ JSON.parse(response.body)['objects'].length.should == 2 end + it "shows no pages for archived projects" do + login_as @user + @project.update_attribute :archived, true + + get :index + response.should be_success + + JSON.parse(response.body)['objects'].length.should == 0 + end + it "shows pages created by a user" do login_as @user diff --git a/spec/controllers/api_v1/task_lists_controller_spec.rb b/spec/controllers/api_v1/task_lists_controller_spec.rb index 0a707c507e..2368ee106f 100644 --- a/spec/controllers/api_v1/task_lists_controller_spec.rb +++ b/spec/controllers/api_v1/task_lists_controller_spec.rb @@ -15,6 +15,8 @@ describe "#index" do it "shows task lists in the project" do login_as @user + @project.create_task(@owner,@task_list,{:name => 'Something TODO'}).save! + @project.create_task(@owner,@other_task_list,{:name => 'Something Else TODO'}).save! get :index, :project_id => @project.permalink response.should be_success @@ -22,11 +24,24 @@ data = JSON.parse(response.body) references = data['references'].map{|r| "#{r['id']}_#{r['type']}"} data['objects'].length.should == 2 + data['objects'].each{|o| o['tasks'].should == nil} references.include?("#{@project.id}_Project").should == true references.include?("#{@task_list.user_id}_User").should == true references.include?("#{@other_task_list.user_id}_User").should == true end + + it "shows task lists with tasks with include=tasks" do + login_as @user + @project.create_task(@owner,@task_list,{:name => 'Something TODO'}).save! + @project.create_task(@owner,@other_task_list,{:name => 'Something Else TODO'}).save! + + get :index, :project_id => @project.permalink, :include => 'tasks' + response.should be_success + + data = JSON.parse(response.body) + data['objects'].each{|o| o['tasks'].length.should == 1} + end it "shows task lists as JSON when requested with the :text format" do login_as @user @@ -59,6 +74,16 @@ JSON.parse(response.body)['objects'].length.should == 3 end + it "shows no task lists for archived projects" do + login_as @user + @project.update_attribute :archived, true + + get :index + response.should be_success + + JSON.parse(response.body)['objects'].length.should == 0 + end + it "shows task lists created by a user" do login_as @user diff --git a/spec/controllers/api_v1/tasks_controller_spec.rb b/spec/controllers/api_v1/tasks_controller_spec.rb index 4bbbfd81ab..7a6e563f46 100644 --- a/spec/controllers/api_v1/tasks_controller_spec.rb +++ b/spec/controllers/api_v1/tasks_controller_spec.rb @@ -27,6 +27,16 @@ JSON.parse(response.body)['objects'].length.should == 2 end + it "shows no tasks in archived projects" do + login_as @user + @project.update_attributes :archived => true + + get :index + response.should be_success + + JSON.parse(response.body)['objects'].length.should == 0 + end + it "shows tasks with a JSONP callback" do login_as @user @@ -257,6 +267,25 @@ @task_list.tasks(true).length.should == 2 @task_list.tasks(true).last.name.should == 'Another TODO!' end + + it "should create an inbox in the desired project if no task list is specified" do + login_as @user + + post :create, :project_id => @project.permalink, :id => @task_list.id, :name => 'Another TODO!' + response.should be_success + + data = JSON.parse(response.body) + references = data['references'].map{|r| "#{r['id']}_#{r['type']}"} + + task = Task.find_by_id(data['id']) + task.should_not == nil + references.include?("#{@project.id}_Project").should == true + references.include?("#{task.user_id}_User").should == true + + task_list = TaskList.find_by_id(data['task_list_id']) + task_list.name.should == 'Inbox' + task_list.tasks.first.should == task + end it "should not allow observers to create tasks" do login_as @observer diff --git a/spec/controllers/api_v1/uploads_controller_spec.rb b/spec/controllers/api_v1/uploads_controller_spec.rb index 6d5f560c7a..5b52818f56 100644 --- a/spec/controllers/api_v1/uploads_controller_spec.rb +++ b/spec/controllers/api_v1/uploads_controller_spec.rb @@ -64,6 +64,16 @@ JSON.parse(response.body)['objects'].length.should == 3 end + it "shows no uploads for archived projects" do + login_as @user + @project.update_attribute :archived, true + + get :index + response.should be_success + + JSON.parse(response.body)['objects'].length.should == 0 + end + it "shows uploads created by a user" do login_as @user diff --git a/spec/models/organization_spec.rb b/spec/models/organization_spec.rb index 1df0fe100a..1e43662066 100644 --- a/spec/models/organization_spec.rb +++ b/spec/models/organization_spec.rb @@ -7,7 +7,7 @@ it { should have_many(:task_list_templates) } #it { should validate_presence_of(:permalink) } - it { should validate_length_of(:name, :minimum => 4) } + it { should validate_length_of(:name, :minimum => 1) } it { should validate_length_of(:permalink, :minimum => 4) } describe "permalink" do @@ -22,6 +22,36 @@ end end + describe "validating length of name and permalink" do + it "should fail on create if the name is shorter than 1 chars" do + organization = Factory.build(:organization, :name => "") + organization.should be_invalid + organization.should have(1).error_on(:name) + end + + it "should allow existent organizations to have a name at least 1 chars if they don't change it" do + organization = Factory.build(:organization, :name => "a", :permalink => "abcdefg") + organization.save(:validate => false) + organization.should be_valid + end + + it "should not allow permalinks with less than 4 chars" do + organization = Factory.build(:organization, :name => "a", :permalink => "abcdefg") + organization.save(:validate => false) + organization.should be_valid + organization.permalink = "2" + organization.save + (organization.reload.permalink.length >= 4).should == true + end + + it "should fail if the name is updated and shorter than 1 chars" do + organization = Factory.create(:organization, :name => "abc123") + organization.name = "" + organization.should be_invalid + end + + end + describe "domain" do it "should allow multiple organizations with nil domain" do Factory(:organization, :domain => nil) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index a90da88efc..beee4ef7bb 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -16,7 +16,7 @@ it { should have_many(:activities) } it { should validate_presence_of(:user) } - it { should validate_length_of(:name, :minimum => 3) } + it { should validate_length_of(:name, :minimum => 1) } it { should validate_length_of(:permalink, :minimum => 5) } describe "creating a project" do @@ -36,23 +36,30 @@ @owner = Factory.create(:user) end - it "should fail on create if the name is shorter than 5 chars" do - project = Factory.build(:project, :user => @owner, :name => "abcd") + it "should fail on create if the name is shorter than 1 chars" do + project = Factory.build(:project, :user => @owner, :name => "") project.should be_invalid project.should have(1).error_on(:name) end - it "should allow existent projects to have a name between 3 and 5 chars if they don't change it" do - project = Factory.build(:project, :user => @owner, :name => "abcd", :permalink => "abcdefg") + it "should allow existent projects to have a name at least 1 chars if they don't change it" do + project = Factory.build(:project, :user => @owner, :name => "a", :permalink => "abcdefg") project.save(:validate => false) project.should be_valid - project.permalink = "#{project.permalink}2" + end + + it "should not allow permalinks with less than 5 chars" do + project = Factory.build(:project, :user => @owner, :name => "a", :permalink => "abcdefg") + project.save(:validate => false) project.should be_valid + project.permalink = "2" + project.save + (project.reload.permalink.length >= 5).should == true end - it "should fail if the name is updated and shorter than 5 chars" do - project = Factory.create(:project, :name => "abcde") - project.name = "abc" + it "should fail if the name is updated and shorter than 1 chars" do + project = Factory.create(:project, :name => "abc123") + project.name = "" project.should be_invalid end diff --git a/spec/models/task_spec.rb b/spec/models/task_spec.rb index 8425156c13..cd281e99a0 100644 --- a/spec/models/task_spec.rb +++ b/spec/models/task_spec.rb @@ -57,6 +57,14 @@ task.status_name = 'silly' }.should raise_error(ArgumentError) end + + it "should nilizie due_on only when urgent flag is set" do + task1 = Factory(:task, :due_on => Time.now, :urgent => false) + task1.due_on.should_not be_nil + + task2 = Factory(:task, :due_on => Time.now, :urgent => true) + task2.due_on.should be_nil + end describe "assigning tasks" do before do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index adf3be52d9..931dbf89fa 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -449,6 +449,18 @@ @task.project.update_attribute :archived, true @user.pending_tasks.should be_empty end + it "should list active tasks sorted by (urgent, due_on ASC)" do + @task.assign_to(@user) + @task.update_attribute(:due_on, 1.minute.from_now) + @task2 = Factory.create(:task, :due_on => 10.minutes.from_now) + @task3 = Factory.create(:task, :due_on => 2.days.from_now) + @task4 = Factory.create(:task, :urgent => true) + [@task2, @task3, @task4].each do |task| + task.project.add_user(@user) + task.assign_to(@user) + end + @user.pending_tasks.should == [@task, @task2, @task3, @task4] + end end describe "#assigned_tasks_count" do diff --git a/spec/models/watcher_spec.rb b/spec/models/watcher_spec.rb index 91b26c95c5..c2b09837b6 100644 --- a/spec/models/watcher_spec.rb +++ b/spec/models/watcher_spec.rb @@ -2,6 +2,21 @@ describe Watcher do + describe "in projects" do + before do + @user = Factory.create(:confirmed_user) + @project = Factory.create(:project) + end + + it "should destroy the watcher after project deletion" do + Watcher.create(:user => @user, :project => @project) + + lambda { + @project.destroy + }.should change(Watcher, :count).by(-1) + end + end + describe "in conversations" do before do @user1 = Factory.create(:confirmed_user)