mirror of
https://github.com/TracksApp/tracks.git
synced 2025-12-16 23:30:12 +01:00
get tag view working for updating todos. Refactored update a lot
This commit is contained in:
parent
1cea27ccc9
commit
f923a40a40
15 changed files with 314 additions and 215 deletions
|
|
@ -124,16 +124,9 @@ class ApplicationController < ActionController::Base
|
|||
# config/settings.yml
|
||||
#
|
||||
def format_date(date)
|
||||
if date
|
||||
date_format = prefs.date_format
|
||||
formatted_date = date.in_time_zone(prefs.time_zone).strftime("#{date_format}")
|
||||
else
|
||||
formatted_date = ''
|
||||
end
|
||||
formatted_date
|
||||
return date ? date.in_time_zone(prefs.time_zone).strftime("#{prefs.date_format}") : ''
|
||||
end
|
||||
|
||||
|
||||
def for_autocomplete(coll, substr)
|
||||
filtered = coll.find_all{|item| item.name.downcase.include? substr.downcase}
|
||||
json_elems = "[{" + filtered.map {|item| "\"value\" : \"#{item.name}\", \"id\" : \"#{item.id}\""}.join("},{") + "}]"
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@ class ProjectsController < ApplicationController
|
|||
format.rss &render_rss_feed
|
||||
format.atom &render_atom_feed
|
||||
format.text &render_text_feed
|
||||
format.autocomplete { render :text => for_autocomplete(@projects.reject(&:completed?), params[:q]) }
|
||||
format.autocomplete {
|
||||
uncompleted_projects = current_user.projects.uncompleted(true)
|
||||
render :text => for_autocomplete(uncompleted_projects, params[:term]) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ class TodosController < ApplicationController
|
|||
skip_before_filter :login_required, :only => [:index, :calendar]
|
||||
prepend_before_filter :login_or_feed_token_required, :only => [:index, :calendar]
|
||||
append_before_filter :find_and_activate_ready, :only => [:index, :list_deferred]
|
||||
append_before_filter :init, :except => [ :destroy, :completed,
|
||||
append_before_filter :init, :except => [ :tag, :destroy, :completed,
|
||||
:completed_archive, :check_deferred, :toggle_check, :toggle_star,
|
||||
:edit, :update, :create, :calendar, :auto_complete_for_predecessor, :remove_predecessor, :add_predecessor]
|
||||
append_before_filter :get_todo_from_params, :only => [ :edit, :toggle_check, :toggle_star, :show, :update, :destroy, :remove_predecessor, :show_notes]
|
||||
|
|
@ -80,25 +80,27 @@ class TodosController < ApplicationController
|
|||
|
||||
# Fix for #977 because AASM overrides @state on creation
|
||||
specified_state = @todo.state
|
||||
|
||||
@todo.update_state_from_project
|
||||
@saved = @todo.save
|
||||
|
||||
# Fix for #977 because AASM overrides @state on creation
|
||||
@todo.update_attribute('state', specified_state) unless specified_state == "immediate"
|
||||
@todo.update_state_from_project
|
||||
@saved = @todo.save
|
||||
|
||||
unless (@saved == false) || tag_list.blank?
|
||||
@todo.tag_with(tag_list)
|
||||
@todo.tags.reload
|
||||
end
|
||||
|
||||
unless (@saved == false)
|
||||
if @saved
|
||||
unless @todo.uncompleted_predecessors.empty? || @todo.state == 'project_hidden'
|
||||
@todo.state = 'pending'
|
||||
end
|
||||
@todo.save
|
||||
end
|
||||
|
||||
@todo.reload
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to :action => "index" }
|
||||
format.m do
|
||||
|
|
@ -121,6 +123,7 @@ class TodosController < ApplicationController
|
|||
@status_message = 'Added new next action'
|
||||
@status_message += ' to tickler' if @todo.deferred?
|
||||
@status_message += ' in pending state' if @todo.pending?
|
||||
@status_message += ' in hidden state' if @todo.hidden?
|
||||
@status_message = 'Added new project / ' + status_message if @new_project_created
|
||||
@status_message = 'Added new context / ' + status_message if @new_context_created
|
||||
render :action => 'create'
|
||||
|
|
@ -328,20 +331,20 @@ class TodosController < ApplicationController
|
|||
cache_attributes_from_before_update
|
||||
|
||||
update_tags
|
||||
update_project if params['todo']['project_id'].blank? && !params['project_name'].nil?
|
||||
update_context if params['todo']['context_id'].blank? && !params['context_name'].blank?
|
||||
update_project
|
||||
update_context
|
||||
update_due_and_show_from_dates
|
||||
update_completed_state
|
||||
update_dependencies
|
||||
update_attributes_of_todo
|
||||
|
||||
@todo.attributes = params["todo"]
|
||||
@saved = @todo.save
|
||||
@todo.reload # refresh context and project object too (not only their id's)
|
||||
|
||||
@todo.add_predecessor_list(params[:predecessor_list])
|
||||
update_pending_state if @saved && params[:predecessor_list]
|
||||
update_todo_state_if_project_changed
|
||||
|
||||
determine_changes_by_this_update
|
||||
determine_remaining_in_context_count(@context_changed ? @original_item_context_id : @todo.context_id)
|
||||
update_todo_state_if_project_changed
|
||||
determine_down_count
|
||||
determine_deferred_tag_count(params['_tag_name']) if @source_view == 'tag'
|
||||
|
||||
|
|
@ -479,6 +482,7 @@ class TodosController < ApplicationController
|
|||
|
||||
# /todos/tag/[tag_name] shows all the actions tagged with tag_name
|
||||
def tag
|
||||
init_data_for_sidebar unless mobile?
|
||||
@source_view = params['_source_view'] || 'tag'
|
||||
@tag_name = params[:name]
|
||||
@page_title = t('todos.tagged_page_title', :tag_name => @tag_name)
|
||||
|
|
@ -488,28 +492,22 @@ class TodosController < ApplicationController
|
|||
|
||||
@tag = Tag.find_by_name(@tag_name)
|
||||
@tag = Tag.new(:name => @tag_name) if @tag.nil?
|
||||
tag_collection = @tag.todos
|
||||
|
||||
@not_done_todos = tag_collection.find(:all,
|
||||
:conditions => ['todos.user_id = ? and state = ?', current_user.id, 'active'],
|
||||
:order => 'todos.due IS NULL, todos.due ASC, todos.created_at ASC')
|
||||
@hidden_todos = current_user.todos.find(:all,
|
||||
@not_done_todos = current_user.todos.with_tag(@tag).active.find(:all,
|
||||
:order => 'todos.due IS NULL, todos.due ASC, todos.created_at ASC', :include => [:context])
|
||||
@hidden_todos = current_user.todos.with_tag(@tag).hidden.find(:all,
|
||||
:include => [:taggings, :tags, :context],
|
||||
:conditions => ['tags.name = ? AND (todos.state = ? OR (contexts.hide = ? AND todos.state = ?))', @tag_name, 'project_hidden', true, 'active'],
|
||||
:order => 'todos.completed_at DESC, todos.created_at DESC')
|
||||
@deferred = tag_collection.find(:all,
|
||||
:conditions => ['todos.user_id = ? and state = ?', current_user.id, 'deferred'],
|
||||
:order => 'show_from ASC, todos.created_at DESC')
|
||||
@pending = tag_collection.find(:all,
|
||||
:conditions => ['todos.user_id = ? and state = ?', current_user.id, 'pending'],
|
||||
:order => 'show_from ASC, todos.created_at DESC')
|
||||
@deferred = current_user.todos.with_tag(@tag).deferred.find(:all,
|
||||
:order => 'show_from ASC, todos.created_at DESC', :include => [:context])
|
||||
@pending = current_user.todos.with_tag(@tag).blocked.find(:all,
|
||||
:order => 'show_from ASC, todos.created_at DESC', :include => [:context])
|
||||
|
||||
# If you've set no_completed to zero, the completed items box isn't shown on
|
||||
# the tag page
|
||||
max_completed = current_user.prefs.show_number_completed
|
||||
@done = tag_collection.find(:all,
|
||||
@done = current_user.todos.with_tag(@tag).completed.find(:all,
|
||||
:limit => max_completed,
|
||||
:conditions => ['todos.user_id = ? and state = ?', current_user.id, 'completed'],
|
||||
:order => 'todos.completed_at DESC')
|
||||
|
||||
@projects = current_user.projects
|
||||
|
|
@ -538,7 +536,7 @@ class TodosController < ApplicationController
|
|||
def tags
|
||||
@tags = Tag.all
|
||||
respond_to do |format|
|
||||
format.autocomplete { render :text => for_autocomplete(@tags, params[:q]) }
|
||||
format.autocomplete { render :text => for_autocomplete(@tags, params[:term]) }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -810,9 +808,7 @@ class TodosController < ApplicationController
|
|||
def determine_down_count
|
||||
source_view do |from|
|
||||
from.todo do
|
||||
@down_count = current_user.todos.active.count(
|
||||
:all,
|
||||
:conditions => ['contexts.hide = ? AND (projects.state = ? OR todos.project_id IS NULL)', false, 'active'], :include => [:project,:context])
|
||||
@down_count = current_user.todos.not_hidden.count(:all)
|
||||
end
|
||||
from.context do
|
||||
@down_count = current_user.contexts.find(@todo.context_id).todos.not_completed.count(:all)
|
||||
|
|
@ -820,8 +816,6 @@ class TodosController < ApplicationController
|
|||
from.project do
|
||||
unless @todo.project_id == nil
|
||||
@down_count = current_user.projects.find(@todo.project_id).not_done_todos_including_hidden.count
|
||||
@deferred_count = current_user.projects.find(@todo.project_id).deferred_todos.count
|
||||
@pending_count = current_user.projects.find(@todo.project_id).pending_todos.count
|
||||
end
|
||||
end
|
||||
from.deferred do
|
||||
|
|
@ -833,8 +827,7 @@ class TodosController < ApplicationController
|
|||
if @tag.nil?
|
||||
@tag = Tag.new(:name => @tag_name)
|
||||
end
|
||||
tag_collection = @tag.todos
|
||||
@not_done_todos = tag_collection.find(:all, :conditions => ['todos.user_id = ? and state = ?', current_user.id, 'active'])
|
||||
@not_done_todos = current_user.todos.with_tag(@tag).active.find(:all)
|
||||
@not_done_todos.empty? ? @down_count = 0 : @down_count = @not_done_todos.size
|
||||
end
|
||||
end
|
||||
|
|
@ -844,18 +837,35 @@ class TodosController < ApplicationController
|
|||
source_view do |from|
|
||||
from.deferred {
|
||||
# force reload to todos to get correct count and not a cached one
|
||||
@remaining_in_context = current_user.contexts.find(context_id).todos(true).deferred_or_blocked.count(:all)
|
||||
@target_context_count = current_user.contexts.find(@todo.context_id).todos(true).deferred_or_blocked.count(:all)
|
||||
@remaining_in_context = current_user.contexts.find(context_id).todos(true).deferred_or_blocked.count
|
||||
@target_context_count = current_user.contexts.find(@todo.context_id).todos(true).deferred_or_blocked.count
|
||||
}
|
||||
from.tag {
|
||||
from.tag {
|
||||
tag = Tag.find_by_name(params['_tag_name'])
|
||||
if tag.nil?
|
||||
tag = Tag.new(:name => params['tag'])
|
||||
end
|
||||
@remaining_in_context = current_user.contexts.find(context_id).not_done_todo_count({:tag => tag.id})
|
||||
@remaining_in_context = current_user.contexts.find(context_id).todos.not_hidden.with_tag(tag).count
|
||||
@target_context_count = current_user.contexts.find(@todo.context_id).todos.not_hidden.with_tag(tag).count
|
||||
if !@todo.hidden? && @todo_hidden_state_changed
|
||||
@remaining_hidden_count = current_user.todos.hidden.with_tag(tag).count
|
||||
end
|
||||
}
|
||||
from.project {
|
||||
@remaining_deferred_or_pending_count = current_user.projects.find(@todo.project_id).todos.deferred_or_blocked.count
|
||||
@remaining_in_context = current_user.projects.find(@todo.project_id).todos.active.count
|
||||
@target_context_count = current_user.projects.find(@todo.project_id).todos.active.count
|
||||
}
|
||||
from.calendar {
|
||||
@target_context_count = count_old_due_empty(@new_due_id)
|
||||
}
|
||||
end
|
||||
@remaining_in_context = current_user.contexts.find(context_id).not_done_todo_count if @remaining_in_context.nil?
|
||||
@remaining_in_context = current_user.contexts.find(context_id).not_done_todo_count if !@remaining_in_context
|
||||
@target_context_count = current_user.contexts.find(@todo.context_id).not_done_todo_count if !@target_context_count
|
||||
puts "@remaining_in_context = #{@remaining_in_context}"
|
||||
puts "@target_context_count = #{@target_context_count}"
|
||||
puts "@remaining_hidden_count = #{@remaining_hidden_count}"
|
||||
puts "@remaining_deferred_or_pending_count = #{@remaining_deferred_or_pending_count}"
|
||||
end
|
||||
|
||||
def determine_completed_count
|
||||
|
|
@ -874,16 +884,10 @@ class TodosController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def determine_deferred_tag_count(tag)
|
||||
tags = Tag.find_by_name(tag)
|
||||
if tags.nil?
|
||||
# should normally not happen, but is a workaround for #929
|
||||
@deferred_tag_count = 0
|
||||
else
|
||||
@deferred_tag_count = tags.todos.count(:all,
|
||||
:conditions => ['todos.user_id = ? and state = ?', current_user.id, 'deferred'],
|
||||
:order => 'show_from ASC, todos.created_at DESC')
|
||||
end
|
||||
def determine_deferred_tag_count(tag_name)
|
||||
tag = Tag.find_by_name(tag_name)
|
||||
# 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).count
|
||||
end
|
||||
|
||||
def render_todos_html
|
||||
|
|
@ -1039,25 +1043,29 @@ class TodosController < ApplicationController
|
|||
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 0 == current_user.todos.not_completed.count(:all,
|
||||
return current_user.todos.not_completed.count(:all,
|
||||
:conditions => ['todos.due <= ?', due_today_date])
|
||||
when "due_this_week"
|
||||
return 0 == current_user.todos.not_completed.count(:all,
|
||||
return current_user.todos.not_completed.count(:all,
|
||||
:conditions => ['todos.due > ? AND todos.due <= ?', due_today_date, due_this_week_date])
|
||||
when "due_next_week"
|
||||
return 0 == current_user.todos.not_completed.count(:all,
|
||||
return current_user.todos.not_completed.count(:all,
|
||||
:conditions => ['todos.due > ? AND todos.due <= ?', due_this_week_date, due_next_week_date])
|
||||
when "due_this_month"
|
||||
return 0 == current_user.todos.not_completed.count(:all,
|
||||
return current_user.todos.not_completed.count(:all,
|
||||
:conditions => ['todos.due > ? AND todos.due <= ?', due_next_week_date, due_this_month_date])
|
||||
when "due_after_this_month"
|
||||
return 0 == current_user.todos.not_completed.count(:all,
|
||||
return current_user.todos.not_completed.count(:all,
|
||||
:conditions => ['todos.due > ?', due_this_month_date])
|
||||
else
|
||||
raise Exception.new, "unknown due id for calendar: '#{id}'"
|
||||
|
|
@ -1144,45 +1152,53 @@ class TodosController < ApplicationController
|
|||
@original_item_context_id = @todo.context_id
|
||||
@original_item_project_id = @todo.project_id
|
||||
@original_item_was_deferred = @todo.deferred?
|
||||
@original_item_was_hidden = @todo.hidden?
|
||||
@original_item_due = @todo.due
|
||||
@original_item_due_id = get_due_id_for_calendar(@todo.due)
|
||||
@original_item_predecessor_list = @todo.predecessors.map{|t| t.specification}.join(', ')
|
||||
puts "wh = #{@original_item_was_hidden}"
|
||||
end
|
||||
|
||||
def update_project
|
||||
if params['project_name'] == 'None'
|
||||
project = Project.null_object
|
||||
else
|
||||
project = current_user.projects.find_by_name(params['project_name'].strip)
|
||||
unless project
|
||||
project = current_user.projects.build
|
||||
project.name = params['project_name'].strip
|
||||
project.save
|
||||
@new_project_created = true
|
||||
if params['todo']['project_id'].blank? && !params['project_name'].nil?
|
||||
if params['project_name'] == 'None'
|
||||
project = Project.null_object
|
||||
else
|
||||
project = current_user.projects.find_by_name(params['project_name'].strip)
|
||||
unless project
|
||||
project = current_user.projects.build
|
||||
project.name = params['project_name'].strip
|
||||
project.save
|
||||
@new_project_created = true
|
||||
end
|
||||
end
|
||||
params["todo"]["project_id"] = project.id
|
||||
end
|
||||
params["todo"]["project_id"] = project.id
|
||||
@project_changed = @original_item_project_id != params["todo"]["project_id"] = project.id
|
||||
end
|
||||
|
||||
def update_todo_state_if_project_changed
|
||||
if (@project_changed && !@original_item_project_id.nil?) then
|
||||
if ( @project_changed ) then
|
||||
@todo.update_state_from_project
|
||||
@todo.save!
|
||||
@remaining_undone_in_project = current_user.projects.find(@original_item_project_id).not_done_todos.count
|
||||
@remaining_undone_in_project = current_user.projects.find(@original_item_project_id).todos.active.count if source_view_is :project
|
||||
end
|
||||
end
|
||||
|
||||
def update_context
|
||||
context = current_user.contexts.find_by_name(params['context_name'].strip)
|
||||
unless context
|
||||
@new_context = current_user.contexts.build
|
||||
@new_context.name = params['context_name'].strip
|
||||
@new_context.save
|
||||
@new_context_created = true
|
||||
@not_done_todos = [@todo]
|
||||
context = @new_context
|
||||
if params['todo']['context_id'].blank? && !params['context_name'].blank?
|
||||
context = current_user.contexts.find_by_name(params['context_name'].strip)
|
||||
unless context
|
||||
@new_context = current_user.contexts.build
|
||||
@new_context.name = params['context_name'].strip
|
||||
@new_context.save
|
||||
@new_context_created = true
|
||||
@not_done_todos = [@todo]
|
||||
context = @new_context
|
||||
end
|
||||
params["todo"]["context_id"] = context.id
|
||||
end
|
||||
params["todo"]["context_id"] = context.id
|
||||
@context_changed = @original_item_context_id != params["todo"]["context_id"] = context.id
|
||||
puts "context changed into '#{context.name}'"
|
||||
end
|
||||
|
||||
def update_tags
|
||||
|
|
@ -1220,34 +1236,47 @@ class TodosController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def update_pending_state
|
||||
def update_dependencies
|
||||
@todo.add_predecessor_list(params[:predecessor_list])
|
||||
if @original_item_predecessor_list != params[:predecessor_list]
|
||||
# Possible state change with new dependencies
|
||||
if @todo.uncompleted_predecessors.empty?
|
||||
if @todo.state == 'pending'
|
||||
@todo.activate! # Activate pending if no uncompleted predecessors
|
||||
end
|
||||
if @todo.uncompleted_predecessors.empty?
|
||||
@todo.activate! if @todo.state == 'pending' # Activate pending if no uncompleted predecessors
|
||||
else
|
||||
if @todo.state == 'active'
|
||||
@todo.block! # Block active if we got uncompleted predecessors
|
||||
end
|
||||
@todo.block! if @todo.state == 'active' # Block active if we got uncompleted predecessors
|
||||
end
|
||||
end
|
||||
@todo.save!
|
||||
end
|
||||
|
||||
def update_attributes_of_todo
|
||||
@todo.attributes = params["todo"]
|
||||
end
|
||||
|
||||
def determine_changes_by_this_update
|
||||
@context_changed = @original_item_context_id != @todo.context_id
|
||||
@todo_was_activated_from_deferred_state = @original_item_was_deferred && @todo.active?
|
||||
@todo_was_deferred = !@original_item_was_deferred && @todo.deferred?
|
||||
@todo_was_deferred_from_active_state = @todo.deferred? && !@original_item_was_deferred
|
||||
@todo_deferred_state_changed = @todo_was_deferred_from_active_state || @todo_was_activated_from_deferred_state
|
||||
@due_date_changed = @original_item_due != @todo.due
|
||||
@todo_hidden_state_changed = @original_item_was_hidden != @todo.hidden?
|
||||
|
||||
if source_view_is :calendar
|
||||
@due_date_changed = ((@original_item_due != @todo.due) && @todo.due) || !@todo.due
|
||||
@old_due_empty = is_old_due_empty(@original_item_due_id)
|
||||
@new_due_id = get_due_id_for_calendar(@todo.due) if @due_date_changed
|
||||
puts "@context_changed = #{@context_changed}"
|
||||
puts "@project_changed = #{@project_changed}"
|
||||
puts "@todo_was_activated_from_deferred_state #{@todo_was_activated_from_deferred_state}"
|
||||
puts "@todo_was_deferred_from_active_state = #{@todo_was_deferred_from_active_state}"
|
||||
puts "@due_date_changed = #{@due_date_changed}"
|
||||
puts "@todo_hidden_state_changed = #{@todo_hidden_state_changed} @todo.hidden?=#{@todo.hidden?}"
|
||||
|
||||
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) if @due_date_changed
|
||||
end
|
||||
page.tag do
|
||||
@tag_name = params['_tag_name']
|
||||
@tag_was_removed = !@todo.has_tag?(@tag_name)
|
||||
puts "@tag_was_removed = #{@tag_was_removed}"
|
||||
end
|
||||
end
|
||||
|
||||
@project_changed = @original_item_project_id != @todo.project_id
|
||||
end
|
||||
|
||||
def project_specified_by_name(project_name)
|
||||
|
|
|
|||
|
|
@ -205,22 +205,9 @@ module TodosHelper
|
|||
end
|
||||
end
|
||||
|
||||
def item_container_id (todo)
|
||||
return "c#{todo.context_id}items" if source_view_is :deferred
|
||||
return "tickleritems" if todo.deferred? or todo.pending?
|
||||
return "p#{todo.project_id}items" if source_view_is :project
|
||||
return @new_due_id if source_view_is :calendar
|
||||
return "c#{todo.context_id}items"
|
||||
end
|
||||
|
||||
def should_show_new_item
|
||||
|
||||
unless @todo.project.nil?
|
||||
# do not show new actions that were added to hidden or completed projects
|
||||
# on home page and context page
|
||||
return false if source_view_is(:todo) && (@todo.project.hidden? || @todo.project.completed?)
|
||||
return false if source_view_is(:context) && (@todo.project.hidden? || @todo.project.completed?)
|
||||
end
|
||||
return false if source_view_is(:todo) && @todo.hidden?
|
||||
return false if source_view_is(:context) && @todo.hidden? && !@todo.context.hidden?
|
||||
|
||||
return false if (source_view_is(:tag) && !@todo.tags.include?(@tag_name))
|
||||
return false if (source_view_is(:context) && !(@todo.context_id==@default_context.id) )
|
||||
|
|
@ -228,8 +215,8 @@ module TodosHelper
|
|||
return true if source_view_is(:deferred) && @todo.deferred?
|
||||
return true if source_view_is(:project) && @todo.project.hidden? && @todo.project_hidden?
|
||||
return true if source_view_is(:project) && @todo.deferred?
|
||||
return true if !source_view_is(:deferred) && @todo.active?
|
||||
return true if source_view_is(:project) && @todo.pending?
|
||||
return true if !source_view_is(:deferred) && @todo.active?
|
||||
|
||||
return true if source_view_is(:tag) && @todo.pending?
|
||||
return false
|
||||
|
|
@ -239,20 +226,13 @@ module TodosHelper
|
|||
return 'tickler' if source_view_is :deferred
|
||||
return 'project' if source_view_is :project
|
||||
return 'stats' if source_view_is :stats
|
||||
return 'tag' if source_view_is :tag
|
||||
return 'context'
|
||||
end
|
||||
|
||||
def empty_container_msg_div_id
|
||||
todo = @todo || @successor
|
||||
return "" unless todo # empty id if no todo or successor given
|
||||
return "tickler-empty-nd" if source_view_is_one_of(:project, :tag) && todo.deferred?
|
||||
return "p#{todo.project_id}empty-nd" if source_view_is :project
|
||||
return "c#{todo.context_id}empty-nd"
|
||||
end
|
||||
|
||||
def todo_container_is_empty
|
||||
default_container_empty = ( @down_count == 0 )
|
||||
deferred_container_empty = ( @todo.deferred? && @deferred_count == 0)
|
||||
deferred_container_empty = ( @todo.deferred? && @remaining_deferred_count == 0)
|
||||
return default_container_empty || deferred_container_empty
|
||||
end
|
||||
|
||||
|
|
@ -284,15 +264,18 @@ module TodosHelper
|
|||
end
|
||||
|
||||
def update_needs_to_hide_context
|
||||
return false if source_view_is(:tag) && (@remaining_in_context == 0) && (@todo_hidden_state_changed && !@todo.hidden?)
|
||||
return (@remaining_in_context == 0) && !source_view_is(:context)
|
||||
end
|
||||
|
||||
def update_needs_to_remove_todo_from_container
|
||||
source_view do |page|
|
||||
page.context { return @context_changed || @todo.deferred? || @todo.pending?}
|
||||
page.project { return updated_todo_changed_deferred_state }
|
||||
page.context { return @context_changed || @todo.deferred? || @todo.pending?}
|
||||
page.project { return @todo_deferred_state_changed }
|
||||
page.deferred { return @context_changed || !(@todo.deferred? || @todo.pending?) }
|
||||
page.calendar { return @due_date_changed || !@todo.due }
|
||||
page.stats { return @todo.completed? }
|
||||
page.tag { return (@context_changed && !@todo.hidden?) || @tag_was_removed || @todo_hidden_state_changed || @todo_deferred_state_changed }
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
|
@ -300,35 +283,67 @@ module TodosHelper
|
|||
def replace_with_updated_todo
|
||||
source_view do |page|
|
||||
page.context { return !update_needs_to_remove_todo_from_container }
|
||||
page.project { return !updated_todo_changed_deferred_state}
|
||||
page.project { return !@todo_deferred_state_changed}
|
||||
page.deferred { return !@context_changed && (@todo.deferred? || @todo.pending?) }
|
||||
page.calendar { return !@due_date_changed && @todo.due }
|
||||
end
|
||||
end
|
||||
|
||||
def append_updated_todo
|
||||
source_view do |page|
|
||||
page.context { return false }
|
||||
page.project { return updated_todo_changed_deferred_state }
|
||||
page.deferred { return @context_changed && (@todo.deferred? || @todo.pending?) }
|
||||
page.calendar { return @due_date_changed && @todo.due }
|
||||
page.stats { return !@todo.completed? }
|
||||
page.tag { return !update_needs_to_remove_todo_from_container && !@tag_was_removed }
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
def updated_todo_changed_deferred_state
|
||||
return (@todo.deferred? && !@original_item_was_deferred) || @todo_was_activated_from_deferred_state
|
||||
def append_updated_todo
|
||||
source_view do |page|
|
||||
page.context { return false }
|
||||
page.project { return @todo_deferred_state_changed }
|
||||
page.deferred { return @context_changed && (@todo.deferred? || @todo.pending?) }
|
||||
page.calendar { return @due_date_changed && @todo.due }
|
||||
page.stats { return false }
|
||||
page.tag { return update_needs_to_remove_todo_from_container && !@tag_was_removed}
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
def item_container_id (todo)
|
||||
puts "todo.hidden?=#{todo.hidden?} en source_view=#{@source_view}"
|
||||
return "hiddenitems" if source_view_is(:tag) && todo.hidden?
|
||||
return "c#{todo.context_id}items" if source_view_is :deferred
|
||||
return @new_due_id if source_view_is :calendar
|
||||
return "tickleritems" if todo.deferred? || todo.pending?
|
||||
return "p#{todo.project_id}items" if source_view_is :project
|
||||
return "c#{todo.context_id}items"
|
||||
end
|
||||
|
||||
|
||||
def empty_container_msg_div_id(todo = @todo || @successor)
|
||||
raise Exception.new, "no @todo or @successor set" if !todo
|
||||
|
||||
source_view do |page|
|
||||
page.project {
|
||||
return "tickler-empty-nd" if @todo_was_deferred_from_active_state
|
||||
return "p#{todo.project_id}empty-nd"
|
||||
}
|
||||
page.tag {
|
||||
return "tickler-empty-nd" if @todo_was_deferred_from_active_state
|
||||
return "hidden-empty-nd" if @todo.hidden?
|
||||
return "c#{todo.context_id}empty-nd"
|
||||
}
|
||||
page.calendar {
|
||||
return "empty_#{@new_due_id}"
|
||||
}
|
||||
end
|
||||
|
||||
return "c#{todo.context_id}empty-nd"
|
||||
end
|
||||
|
||||
def render_animation(animation)
|
||||
html = ""
|
||||
animation.each do |step|
|
||||
puts "step='#{step}'"
|
||||
unless step.blank?
|
||||
html += step + "({ go: function() {\r\n"
|
||||
end
|
||||
end
|
||||
html += "}})" * animation.count
|
||||
html += "}}) " * animation.count
|
||||
return html
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
class Project < ActiveRecord::Base
|
||||
has_many :todos, :dependent => :delete_all, :include => [:context,:tags]
|
||||
|
||||
# TODO: remove these scopes. Can be replaced by the named scopes on the todo relation
|
||||
has_many :not_done_todos,
|
||||
:include => [:context,:tags,:project],
|
||||
:class_name => 'Todo',
|
||||
|
|
@ -35,6 +37,7 @@ class Project < ActiveRecord::Base
|
|||
named_scope :active, :conditions => { :state => 'active' }
|
||||
named_scope :hidden, :conditions => { :state => 'hidden' }
|
||||
named_scope :completed, :conditions => { :state => 'completed'}
|
||||
named_scope :uncompleted, :conditions => ["NOT state = ?", 'completed']
|
||||
|
||||
validates_presence_of :name
|
||||
validates_length_of :name, :maximum => 255
|
||||
|
|
|
|||
|
|
@ -23,6 +23,16 @@ class Todo < ActiveRecord::Base
|
|||
named_scope :deferred, :conditions => ["todos.completed_at IS NULL AND NOT todos.show_from IS NULL"]
|
||||
named_scope :blocked, :conditions => ['todos.state = ?', 'pending']
|
||||
named_scope :deferred_or_blocked, :conditions => ["(todos.completed_at IS NULL AND NOT todos.show_from IS NULL) OR (todos.state = ?)", "pending"]
|
||||
named_scope :not_deferred_or_blocked, :conditions => ["todos.completed_at IS NULL AND todos.show_from IS NULL AND NOT todos.state = ?", "pending"]
|
||||
named_scope :with_tag, lambda { |tag| {:joins => :taggings, :conditions => ["taggings.tag_id = ? ", tag.id] } }
|
||||
named_scope :of_user, lambda { |user_id| {:conditions => ["todos.user_id = ? ", user_id] } }
|
||||
named_scope :hidden,
|
||||
:joins => :context,
|
||||
:conditions => ["todos.state = ? OR (contexts.hide = ? AND (todos.state = ? OR todos.state = ?))",
|
||||
'project_hidden', true, 'active', 'deferred']
|
||||
named_scope :not_hidden,
|
||||
:joins => [:context, :project],
|
||||
:conditions => ['contexts.hide = ? AND (projects.state = ? OR todos.project_id IS NULL)', false, 'active']
|
||||
|
||||
STARRED_TAG_NAME = "starred"
|
||||
|
||||
|
|
@ -92,7 +102,6 @@ class Todo < ActiveRecord::Base
|
|||
def no_uncompleted_predecessors?
|
||||
return uncompleted_predecessors.empty?
|
||||
end
|
||||
|
||||
|
||||
# Returns a string with description <context, project>
|
||||
def specification
|
||||
|
|
@ -196,14 +205,24 @@ class Todo < ActiveRecord::Base
|
|||
return false
|
||||
end
|
||||
|
||||
def has_tag?(tag)
|
||||
return self.tags.select{|t| t.name==tag }.size > 0
|
||||
end
|
||||
|
||||
def hidden?
|
||||
puts "hidden => state = #{self.state} context(#{self.context.name}).hidden=#{self.context.hidden?}"
|
||||
return self.state == 'project_hidden' || ( self.context.hidden? && (self.state == 'active' || self.state == 'deferred'))
|
||||
end
|
||||
|
||||
def update_state_from_project
|
||||
if state == 'project_hidden' and !project.hidden?
|
||||
puts "state was #{self.state}; project.hidden?=#{self.project.hidden?}"
|
||||
if state == 'project_hidden' and !self.project.hidden?
|
||||
if self.uncompleted_predecessors.empty?
|
||||
self.state = 'active'
|
||||
else
|
||||
self.state = 'pending'
|
||||
end
|
||||
elsif state == 'active' and project.hidden?
|
||||
elsif self.state == 'active' and self.project.hidden?
|
||||
self.state = 'project_hidden'
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@
|
|||
<%= javascript_tag "var SOURCE_VIEW = '#{@source_view}';" %>
|
||||
<%= javascript_tag "var TAG_NAME = '#{@tag_name}';" if @tag_name %>
|
||||
<script type="text/javascript">
|
||||
var defaultContexts = <%= default_contexts_for_autocomplete rescue '{}' %>;
|
||||
var defaultTags = <%= default_tags_for_autocomplete rescue '{}' %>;
|
||||
var dateFormat = '<%= date_format_for_date_picker %>';
|
||||
var weekStart = '<%= current_user.prefs.week_starts %>';
|
||||
function relative_to_root(path) { return '<%= root_url %>'+path; };
|
||||
|
|
|
|||
|
|
@ -14,29 +14,6 @@
|
|||
animation << (@new_context_created ? "insert_new_context_with_updated_todo" : "add_to_existing_container")
|
||||
end
|
||||
animation << "hide_context" if update_needs_to_hide_context
|
||||
|
||||
# update relevant empty messaged
|
||||
source_view do |page|
|
||||
page.context {
|
||||
animation << "show_empty_message_source_container" if (@remaining_in_context == 0)
|
||||
}
|
||||
page.project {
|
||||
animation << "show_empty_message_project" if (@down_count == 0)
|
||||
if @todo_was_activated_from_deferred_state
|
||||
animation << "show_empty_message_tickler" if @deferred_count == 0
|
||||
animation << "hide_empty_message_project"
|
||||
end
|
||||
animation << "hide_empty_message_tickler" if @todo_was_deferred && (@deferred_count == 1)
|
||||
}
|
||||
page.deferred {
|
||||
animation << "show_empty_message_source_container" if (@remaining_in_context == 0)
|
||||
animation << "hide_empty_message_target_container" if (@context_changed && @target_context_count==1)
|
||||
}
|
||||
page.calendar {
|
||||
animation << "show_empty_message_source_container" if @old_due_empty
|
||||
animation << "hide_empty_message_target_container" if @todo.due
|
||||
}
|
||||
end
|
||||
animation << "highlight_updated_todo"
|
||||
%>
|
||||
|
||||
|
|
@ -48,10 +25,54 @@
|
|||
function remove_todo(next_steps) {
|
||||
$('#<%= dom_id(@todo) %>').fadeOut(400, function() {
|
||||
$('#<%= dom_id(@todo) %>').remove();
|
||||
|
||||
<% # TODO: to helper function: show_empty_message_source_container
|
||||
container_id = ""
|
||||
source_view do |page|
|
||||
page.project {
|
||||
container_id = "p#{@original_item_project_id}empty-nd" if @remaining_in_context == 0
|
||||
container_id = "tickler-empty-nd" if @todo_was_activated_from_deferred_state && @remaining_deferred_or_pending_count == 0
|
||||
}
|
||||
page.deferred { container_id = "c#{@original_item_context_id}empty-nd" if @remaining_in_context == 0 }
|
||||
page.calendar { container_id = "empty_#{@original_item_due_id}" if @old_due_empty }
|
||||
page.tag {
|
||||
container_id = "hidden-empty-nd" if !@todo.hidden? && @todo_hidden_state_changed && @remaining_hidden_count == 0
|
||||
container_id = "tickler-empty-nd" if @todo_was_activated_from_deferred_state && @remaining_deferred_or_pending_count == 0
|
||||
}
|
||||
page.context { container_id = "c#{@original_item_context_id}empty-nd" if @remaining_in_context == 0 }
|
||||
end
|
||||
unless container_id.blank?
|
||||
-%>
|
||||
$("#<%= container_id%>").slideDown(100);
|
||||
<% end -%>
|
||||
next_steps.go();
|
||||
});
|
||||
}
|
||||
|
||||
function add_to_existing_container(next_steps) {
|
||||
$('#<%= item_container_id(@todo) %>').append(html_for_todo());
|
||||
<% if source_view_is_one_of(:project,:calendar) -%>
|
||||
next_steps.go();
|
||||
<% if (@target_context_count==1) || (@todo.deferred? && @remaining_deferred_or_pending_count == 1) -%>
|
||||
$("#<%= empty_container_msg_div_id %>").slideUp(100);
|
||||
<% end -%>
|
||||
<% else -%>
|
||||
<% unless (@todo_hidden_state_changed && @todo.hidden?) || @todo_was_deferred_from_active_state -%>
|
||||
$('#c<%= @todo.context_id %>').fadeIn(500, function() {
|
||||
next_steps.go();
|
||||
<% if @target_context_count==1 -%>
|
||||
$("#<%= empty_container_msg_div_id %>").slideUp(100);
|
||||
<% end -%>
|
||||
});
|
||||
<% else -%>
|
||||
next_steps.go();
|
||||
<% if (@target_context_count==1) || (@todo.deferred? && @remaining_deferred_or_pending_count == 1) -%>
|
||||
$("#<%= empty_container_msg_div_id %>").slideUp(100);
|
||||
<% end -%>
|
||||
<% end -%>
|
||||
<% end -%>
|
||||
}
|
||||
|
||||
function replace_todo(next_steps) {
|
||||
$('#<%= dom_id(@todo) %>').html(html_for_todo());
|
||||
next_steps.go();
|
||||
|
|
@ -66,15 +87,6 @@ function highlight_updated_todo(next_steps) {
|
|||
$('#<%= dom_id(@todo)%>').effect('highlight', {}, 2000, function(){ next_steps.go(); });
|
||||
}
|
||||
|
||||
function add_to_existing_container(next_steps) {
|
||||
$('#<%= item_container_id(@todo) %>').append(html_for_todo());
|
||||
<% if source_view_is_one_of(:project,:calendar) -%>
|
||||
next_steps.go();
|
||||
<% else -%>
|
||||
$('#c<%= @todo.context_id %>').fadeIn(500, function() { next_steps.go(); });
|
||||
<% end -%>
|
||||
}
|
||||
|
||||
function update_badge_count() {
|
||||
<%
|
||||
count = source_view_is(:context) ? @remaining_in_context : @down_count
|
||||
|
|
@ -83,33 +95,6 @@ function update_badge_count() {
|
|||
TracksPages.set_page_badge(<%= count %>);
|
||||
}
|
||||
|
||||
function show_empty_message_source_container(next_steps) {
|
||||
<% container_id = source_view_is(:calendar) ? "empty_#{@original_item_due_id}" : "c#{@original_item_context_id}empty-nd" -%>
|
||||
$("#<%= container_id%>").slideDown(100, function() { next_steps.go(); });
|
||||
}
|
||||
|
||||
function hide_empty_message_target_container(next_steps) {
|
||||
<% container_id = source_view_is(:calendar) ? "empty_#{@new_due_id}" : "c#{@todo.context_id}empty-nd" -%>
|
||||
$("#<%=container_id%>").slideUp(100, function() { next_steps.go(); });
|
||||
}
|
||||
|
||||
function show_empty_message_project(next_steps) {<%
|
||||
id = updated_todo_changed_deferred_state ? "p#{@todo.project_id}empty-nd" : empty_container_msg_div_id %>
|
||||
$('#<%= id %>').slideDown(100, function() { next_steps.go(); });
|
||||
}
|
||||
|
||||
function hide_empty_message_project(next_steps) {
|
||||
$('#<%= empty_container_msg_div_id %>').slideUp(100, function() { next_steps.go(); });
|
||||
}
|
||||
|
||||
function show_empty_message_tickler(next_steps) {
|
||||
$('#tickler-empty-nd').slideDown(100, function() { next_steps.go(); });
|
||||
}
|
||||
|
||||
function hide_empty_message_tickler(next_steps) {
|
||||
$('#tickler-empty-nd').slideUp(100, function(){ next_steps.go(); });
|
||||
}
|
||||
|
||||
function insert_new_context_with_updated_todo(next_steps) {
|
||||
$('#display_box').prepend(html_for_new_context());
|
||||
$('#c<%= @todo.context_id %>').fadeIn(500, function() { next_steps.go(); });
|
||||
|
|
|
|||
|
|
@ -325,8 +325,8 @@ en:
|
|||
months_ago: "Months ago"
|
||||
other_actions_label: "(others)"
|
||||
action_selection_title: "TRACKS::Action selection"
|
||||
actions_selected_from_week: "Actions selected from week"
|
||||
actions_further: "and further"
|
||||
actions_selected_from_week: "Actions selected from week "
|
||||
actions_further: " and further"
|
||||
totals_project_count: "You have {{count}} projects."
|
||||
totals_active_project_count: "Of those {{count}} are active projects"
|
||||
totals_hidden_project_count: "{{count}} are hidden"
|
||||
|
|
|
|||
|
|
@ -27,3 +27,9 @@ Feature: Edit a context
|
|||
|
||||
Scenario: Editing the context of the last todo will remove the todo and show empty message
|
||||
Given this scenario is pending
|
||||
|
||||
Scenario: Adding a todo to a hidden project will not show the todo
|
||||
Given this scenario is pending
|
||||
|
||||
Scenario: Adding a todo to a hidden context will show that todo
|
||||
Given this scenario is pending
|
||||
|
|
|
|||
|
|
@ -62,3 +62,9 @@ Feature: Show statistics
|
|||
And I should see 5 todos
|
||||
And I should see "to return to the statistics page"
|
||||
And I should see "to show the actions from week 0 and further"
|
||||
|
||||
Scenario: I can edit the todos selected from a chart
|
||||
Given this scenario is pending
|
||||
|
||||
Scenario: Marking a todo selected from a chart as complete will remove it from the page
|
||||
Given this scenario is pending
|
||||
|
|
|
|||
40
features/tagging_todos.feature
Normal file
40
features/tagging_todos.feature
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
Feature: Tagging todos
|
||||
In order to organise my todos in various lists
|
||||
As a Tracks user
|
||||
I want to to be able to add or edit one or more tags to todos
|
||||
|
||||
Background:
|
||||
Given the following user record
|
||||
| login | password | is_admin |
|
||||
| testuser | secret | false |
|
||||
And I have logged in as "testuser" with password "secret"
|
||||
|
||||
Scenario: I can edit a todo to add tags to that todo
|
||||
Given this is a pending scenario
|
||||
|
||||
Scenario: I can add a new todo with tags
|
||||
Given this is a pending scenario
|
||||
|
||||
Scenario: I can show all todos tagged with a specific tag
|
||||
Given this is a pending scenario
|
||||
|
||||
Scenario: I can remove a tag from a todo from the tag view and the tag will be removed
|
||||
Given this is a pending scenario
|
||||
|
||||
Scenario: I can add a new todo from tag view with that tag and it will be added to the page
|
||||
Given this is a pending scenario
|
||||
|
||||
Scenario: I can add a new todo from tag view with a different tag and it will not be added to the page
|
||||
Given this is a pending scenario
|
||||
|
||||
Scenario: I can change the context of a tagged todo in tag view and it will move the tag on the page
|
||||
Given this is a pending scenario
|
||||
|
||||
Scenario: I can defer a tagged todo in tag view and it will move the todo on the page to the deferred container
|
||||
Given this is a pending scenario
|
||||
|
||||
Scenario: I can move a tagged todo in tag view to a hidden project and it will move the todo on the page to the hidden container
|
||||
Given this is a pending scenario
|
||||
|
||||
Scenario: I can move a tagged todo in tag view to a hidden context and it will move the todo on the page to the hidden container
|
||||
Given this is a pending scenario
|
||||
|
|
@ -124,7 +124,7 @@ module LoginSystem
|
|||
|
||||
def get_current_user
|
||||
if @user.nil? && session['user_id']
|
||||
@user = User.find session['user_id'], :include => :preference
|
||||
@user = User.find(session['user_id'], :include => [:preference])
|
||||
end
|
||||
@prefs = @user.prefs unless @user.nil?
|
||||
@user
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
module Tracks
|
||||
module TodoList
|
||||
# TODO: this module should be deprecated. This could mostly (all?) be replaced by named scopes)
|
||||
|
||||
def not_done_todos(opts={})
|
||||
@not_done_todos ||= self.find_not_done_todos(opts)
|
||||
|
|
|
|||
|
|
@ -743,7 +743,7 @@ function default_ajax_options_for_submit(ajax_type, element_to_block) {
|
|||
type: ajax_type,
|
||||
async: true,
|
||||
context: element_to_block,
|
||||
data: "_source_view=" + encodeURIComponent( SOURCE_VIEW ),
|
||||
data: "_source_view=" + SOURCE_VIEW,
|
||||
beforeSend: function() {
|
||||
$(this).block({
|
||||
message: null
|
||||
|
|
@ -758,7 +758,7 @@ function default_ajax_options_for_submit(ajax_type, element_to_block) {
|
|||
}
|
||||
}
|
||||
if(typeof(TAG_NAME) !== 'undefined')
|
||||
options.data += "&_tag_name="+ encodeURIComponent (TAG_NAME);
|
||||
options.data += "&_tag_name="+ TAG_NAME;
|
||||
return options;
|
||||
}
|
||||
|
||||
|
|
@ -865,8 +865,8 @@ function enable_rich_interaction(){
|
|||
/* called after completion of all AJAX calls */
|
||||
|
||||
/* fix for #1036 where closing a edit form before the autocomplete was filled
|
||||
* resulted in a dropdown box that could not be removed. We remove all
|
||||
* autocomplete boxes the hard way */
|
||||
* resulted in a dropdown box that could not be removed. We remove all
|
||||
* autocomplete boxes the hard way */
|
||||
$('.ac_results').remove();
|
||||
|
||||
$('input.Date').datepicker({
|
||||
|
|
@ -879,10 +879,12 @@ function enable_rich_interaction(){
|
|||
$('input[name=context_name]').autocomplete({
|
||||
source: relative_to_root('contexts.autocomplete')
|
||||
});
|
||||
$('input[name=project_name]').autocomplete({
|
||||
source: relative_to_root('projects.autocomplete')
|
||||
});
|
||||
|
||||
/* $('input[name=project[default_context_name]]').autocomplete(
|
||||
relative_to_root('contexts.autocomplete'), {matchContains: true});
|
||||
$('input[name=project_name]').autocomplete(
|
||||
relative_to_root('projects.autocomplete'), {matchContains: true});
|
||||
$('input[name=tag_list]:not(.ac_input)').autocomplete(
|
||||
relative_to_root('tags.autocomplete'), {multiple: true,multipleSeparator:',',matchContains:true});
|
||||
$('input[name=predecessor_list]:not(.ac_input)').autocomplete(
|
||||
|
|
@ -971,7 +973,7 @@ function enable_rich_interaction(){
|
|||
field_touched = false;
|
||||
|
||||
/* shrink the notes on the project pages. This is not live(), so this needs
|
||||
* to be run after ajax adding of a new note */
|
||||
* to be run after ajax adding of a new note */
|
||||
$('.note_wrapper').truncate({
|
||||
max_length: 90,
|
||||
more: '',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue