mirror of
https://github.com/TracksApp/tracks.git
synced 2025-12-24 11:10:12 +01:00
Merged mobile_controller into the todos_controller. The lightweight mobile HTML is
arguably just another representation of the same resources, so it seems to fit the RESTful Rails paradigm to use an extension (.m) to switch on in the respond_to stanza. I needed some hackery to make this work. See my note in todos_controller for a full explanation. I also added a route to get to the mobile view by using 'domain.com/m' Created some selenium tests for the mobile view, too. In optimizing the data access for the mobile view, I ran into "a bug in rails pagination":http://dev.rubyonrails.org/ticket/7885" and integrated a nice pagination plugin from the Err the Blog guys ("will_paginate":http://errtheblog.com/post/929) to work around the issue. NOTE that this changeset includes a new line in environment.rb.tmpl (at the bottom). Be sure to copy this into your environment.rb file. These changes fix #489 (cannot edit action using mobile interface). Thanks for the bug report, lrbalt! In the name of consistency, I made the argument to the block for all respond_to calls 'format' (instead of the formerly cool 'wants'). Lastly, I added a link to the project's new contribute page to the footer of the main web UI. Help us join the Mac on Intel world. :-) git-svn-id: http://www.rousette.org.uk/svn/tracks-repos/trunk@517 a4c988fc-2ded-0310-b66e-134b36920a42
This commit is contained in:
parent
f3f881e47e
commit
ba0b52ff1a
32 changed files with 445 additions and 258 deletions
|
|
@ -28,7 +28,7 @@ class ApplicationController < ActionController::Base
|
|||
def set_charset
|
||||
headers["Content-Type"] ||= "text/html; charset=UTF-8"
|
||||
end
|
||||
|
||||
|
||||
def set_session_expiration
|
||||
# http://wiki.rubyonrails.com/rails/show/HowtoChangeSessionOptions
|
||||
unless session == nil
|
||||
|
|
@ -56,13 +56,13 @@ class ApplicationController < ActionController::Base
|
|||
|
||||
def rescue_action(exception)
|
||||
log_error(exception) if logger
|
||||
respond_to do |wants|
|
||||
wants.html do
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
notify :warning, "An error occurred on the server."
|
||||
render :action => "index"
|
||||
end
|
||||
wants.js { render :action => 'error' }
|
||||
wants.xml { render :text => 'An error occurred on the server.' + $! }
|
||||
format.js { render :action => 'error' }
|
||||
format.xml { render :text => 'An error occurred on the server.' + $! }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -41,9 +41,9 @@ class ContextsController < ApplicationController
|
|||
end
|
||||
@saved = @context.save
|
||||
@context_not_done_counts = { @context.id => 0 }
|
||||
respond_to do |wants|
|
||||
wants.js
|
||||
wants.xml do
|
||||
respond_to do |format|
|
||||
format.js
|
||||
format.xml do
|
||||
if @context.new_record? && params_are_invalid
|
||||
render_failure "Expected post format is valid xml like so: <request><context><name>context name</name></context></request>."
|
||||
elsif @context.new_record?
|
||||
|
|
|
|||
|
|
@ -1,89 +0,0 @@
|
|||
class MobileController < ApplicationController
|
||||
|
||||
layout 'mobile'
|
||||
|
||||
before_filter :init, :except => :update
|
||||
|
||||
# Plain list of all next actions, paginated 6 per page
|
||||
# Sorted by due date, then creation date
|
||||
#
|
||||
def index
|
||||
@page_title = @desc = "All actions"
|
||||
@todos_pages, @todos = paginate( :todos, :order => 'due IS NULL, due ASC, created_at ASC',
|
||||
:conditions => ['user_id = ? and state = ?', @user.id, "active"],
|
||||
:per_page => 6 )
|
||||
@count = @all_todos.reject { |x| !x.active? || x.context.hide? }.size
|
||||
end
|
||||
|
||||
def detail
|
||||
@item = check_user_return_item
|
||||
@place = @item.context.id
|
||||
end
|
||||
|
||||
def update
|
||||
if params[:id]
|
||||
@item = check_user_return_item
|
||||
@item.update_attributes params[:todo]
|
||||
if params[:todo][:state] == "1"
|
||||
@item.state = "completed"
|
||||
else
|
||||
@item.state = "active"
|
||||
end
|
||||
else
|
||||
params[:todo][:user_id] = @user.id
|
||||
@item = Todo.new(params[:todo]) if params[:todo]
|
||||
end
|
||||
|
||||
if @item.save
|
||||
redirect_to :action => 'index'
|
||||
else
|
||||
self.init
|
||||
if params[:id]
|
||||
render :partial => 'mobile_edit'
|
||||
else
|
||||
render :action => 'show_add_form'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def show_add_form
|
||||
# Just render the view
|
||||
end
|
||||
|
||||
def filter
|
||||
@type = params[:type]
|
||||
case params[:type]
|
||||
when 'context'
|
||||
@context = Context.find( params[:context][:id] )
|
||||
@page_title = @desc = "#{@context.name}"
|
||||
@todos = Todo.find( :all, :order => 'due IS NULL, due ASC, created_at ASC',
|
||||
:conditions => ['user_id = ? and state = ? and context_id = ?', @user.id, "active", @context.id] )
|
||||
@count = @all_todos.reject { |x| x.completed? || x.context_id != @context.id }.size
|
||||
when 'project'
|
||||
@project = Project.find( params[:project][:id] )
|
||||
@page_title = @desc = "#{@project.name}"
|
||||
@todos = Todo.find( :all, :order => 'due IS NULL, due ASC, created_at ASC',
|
||||
:conditions => ['user_id = ? and state = ? and project_id = ?', @user.id, "active", @project.id] )
|
||||
@count = @all_todos.reject { |x| x.completed? || x.project_id != @project.id }.size
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def check_user_return_item
|
||||
item = Todo.find( params['id'] )
|
||||
if @user == item.user
|
||||
return item
|
||||
else
|
||||
notify :warning, "Item and session user mis-match: #{item.user.name} and #{@user.name}!"
|
||||
render_text ""
|
||||
end
|
||||
end
|
||||
|
||||
def init
|
||||
@contexts = @user.contexts.find(:all, :order => 'position ASC')
|
||||
@projects = @user.projects.find_in_state(:all, :active, :order => 'position ASC')
|
||||
@all_todos = @user.todos.find(:all, :conditions => ['state = ? or state = ?', "active", "completed"])
|
||||
end
|
||||
|
||||
end
|
||||
|
|
@ -3,9 +3,9 @@ class NotesController < ApplicationController
|
|||
def index
|
||||
@all_notes = @user.notes
|
||||
@page_title = "TRACKS::All notes"
|
||||
respond_to do |wants|
|
||||
wants.html
|
||||
wants.xml { render :xml => @all_notes.to_xml( :except => :user_id ) }
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.xml { render :xml => @all_notes.to_xml( :except => :user_id ) }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -53,9 +53,9 @@ class ProjectsController < ApplicationController
|
|||
@project_not_done_counts = { @project.id => 0 }
|
||||
@active_projects_count = @user.projects.count(:conditions => "state = 'active'")
|
||||
@contexts = @user.contexts
|
||||
respond_to do |wants|
|
||||
wants.js
|
||||
wants.xml do
|
||||
respond_to do |format|
|
||||
format.js
|
||||
format.xml do
|
||||
if @project.new_record? && params_are_invalid
|
||||
render_failure "Expected post format is valid xml like so: <request><project><name>project name</name></project></request>."
|
||||
elsif @project.new_record?
|
||||
|
|
|
|||
|
|
@ -2,13 +2,17 @@ class TodosController < ApplicationController
|
|||
|
||||
helper :todos
|
||||
|
||||
append_before_filter :init, :except => [ :destroy, :completed, :completed_archive, :check_deferred ]
|
||||
append_before_filter :get_todo_from_params, :only => [ :edit, :toggle_check, :show, :update, :destroy ]
|
||||
skip_before_filter :login_required, :only => [:index]
|
||||
prepend_before_filter :login_or_feed_token_required, :only => [:index]
|
||||
append_before_filter :init, :except => [ :destroy, :completed, :completed_archive, :check_deferred ]
|
||||
append_before_filter :get_todo_from_params, :only => [ :edit, :toggle_check, :show, :update, :destroy ]
|
||||
|
||||
prepend_before_filter :enable_mobile_content_negotiation
|
||||
after_filter :restore_content_type_for_mobile
|
||||
|
||||
session :off, :only => :index, :if => Proc.new { |req| is_feed_request(req) }
|
||||
|
||||
layout 'standard'
|
||||
layout proc{ |controller| controller.mobile? ? "mobile" : "standard" }
|
||||
|
||||
def index
|
||||
@projects = @user.projects.find(:all, :include => [ :todos ])
|
||||
|
|
@ -17,20 +21,29 @@ class TodosController < ApplicationController
|
|||
@contexts_to_show = @contexts.reject {|x| x.hide? }
|
||||
|
||||
respond_to do |format|
|
||||
format.html &render_todos_html
|
||||
format.xml { render :action => 'list.rxml', :layout => false }
|
||||
format.html &render_todos_html
|
||||
format.m &render_todos_mobile
|
||||
format.xml { render :action => 'list.rxml', :layout => false }
|
||||
format.rss &render_rss_feed
|
||||
format.atom &render_atom_feed
|
||||
format.text &render_text_feed
|
||||
format.ics &render_ical_feed
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def new
|
||||
@projects = @user.projects.find(:all)
|
||||
@contexts = @user.contexts.find(:all)
|
||||
respond_to do |format|
|
||||
format.m { render :action => "new_mobile" }
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
@todo = @user.todos.build
|
||||
p = params['request'] || params
|
||||
|
||||
if p['todo']['show_from']
|
||||
if p['todo']['show_from'] && !mobile?
|
||||
p['todo']['show_from'] = parse_date_per_user_prefs(p['todo']['show_from'])
|
||||
end
|
||||
|
||||
|
|
@ -60,34 +73,46 @@ class TodosController < ApplicationController
|
|||
end
|
||||
|
||||
if @todo.due?
|
||||
@todo.due = parse_date_per_user_prefs(p['todo']['due'])
|
||||
@todo.due = parse_date_per_user_prefs(p['todo']['due']) unless mobile?
|
||||
else
|
||||
@todo.due = ""
|
||||
end
|
||||
|
||||
@saved = @todo.save
|
||||
if @saved
|
||||
@todo.tag_with(params[:tag_list],@user)
|
||||
@todo.tag_with(params[:tag_list],@user) if params[:tag_list]
|
||||
@todo.reload
|
||||
end
|
||||
|
||||
respond_to do |wants|
|
||||
wants.html { redirect_to :action => "index" }
|
||||
wants.js do
|
||||
respond_to do |format|
|
||||
format.html { redirect_to :action => "index" }
|
||||
format.m do
|
||||
if @saved
|
||||
determine_down_count
|
||||
redirect_to :action => "index", :format => :m
|
||||
else
|
||||
render :action => "new", :format => :m
|
||||
end
|
||||
end
|
||||
format.js do
|
||||
determine_down_count if @saved
|
||||
render :action => 'create'
|
||||
end
|
||||
wants.xml { render :xml => @todo.to_xml( :root => 'todo', :except => :user_id ) }
|
||||
format.xml { render :xml => @todo.to_xml( :root => 'todo', :except => :user_id ) }
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
@projects = @user.projects.find(:all)
|
||||
@contexts = @user.contexts.find(:all)
|
||||
end
|
||||
|
||||
def show
|
||||
respond_to do |format|
|
||||
format.m do
|
||||
@projects = @user.projects.find(:all)
|
||||
@contexts = @user.contexts.find(:all)
|
||||
render :action => 'show_mobile'
|
||||
end
|
||||
format.xml { render :xml => @todo.to_xml( :root => 'todo', :except => :user_id ) }
|
||||
end
|
||||
end
|
||||
|
|
@ -120,7 +145,7 @@ class TodosController < ApplicationController
|
|||
end
|
||||
|
||||
def update
|
||||
@todo.tag_with(params[:tag_list],@user)
|
||||
@todo.tag_with(params[:tag_list],@user) if params[:tag_list]
|
||||
@original_item_context_id = @todo.context_id
|
||||
@original_item_project_id = @todo.project_id
|
||||
@original_item_was_deferred = @todo.deferred?
|
||||
|
|
@ -160,6 +185,10 @@ class TodosController < ApplicationController
|
|||
params['todo']['show_from'] = parse_date_per_user_prefs(params['todo']['show_from'])
|
||||
end
|
||||
|
||||
if params['done'] == '1' && !@todo.completed?
|
||||
@todo.complete!
|
||||
end
|
||||
|
||||
@saved = @todo.update_attributes params["todo"]
|
||||
@context_changed = @original_item_context_id != @todo.context_id
|
||||
@todo_was_activated_from_deferred_state = @original_item_was_deferred && @todo.active?
|
||||
|
|
@ -167,6 +196,16 @@ class TodosController < ApplicationController
|
|||
@project_changed = @original_item_project_id != @todo.project_id
|
||||
if (@project_changed && !@original_item_project_id.nil?) then @remaining_undone_in_project = @user.projects.find(@original_item_project_id).not_done_todo_count; end
|
||||
determine_down_count
|
||||
respond_to do |format|
|
||||
format.js
|
||||
format.m do
|
||||
if @saved
|
||||
redirect_to formatted_todos_path(:m)
|
||||
else
|
||||
render :action => "edit", :format => :m
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
|
|
@ -175,9 +214,9 @@ class TodosController < ApplicationController
|
|||
@project_id = @todo.project_id
|
||||
@saved = @todo.destroy
|
||||
|
||||
respond_to do |wants|
|
||||
respond_to do |format|
|
||||
|
||||
wants.html do
|
||||
format.html do
|
||||
if @saved
|
||||
notify :notice, "Successfully deleted next action", 2.0
|
||||
redirect_to :action => 'index'
|
||||
|
|
@ -185,9 +224,9 @@ class TodosController < ApplicationController
|
|||
notify :error, "Failed to delete the action", 2.0
|
||||
redirect_to :action => 'index'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
wants.js do
|
||||
format.js do
|
||||
if @saved
|
||||
determine_down_count
|
||||
source_view do |from|
|
||||
|
|
@ -199,7 +238,7 @@ class TodosController < ApplicationController
|
|||
render
|
||||
end
|
||||
|
||||
wants.xml { render :text => '200 OK. Action deleted.', :status => 200 }
|
||||
format.xml { render :text => '200 OK. Action deleted.', :status => 200 }
|
||||
|
||||
end
|
||||
end
|
||||
|
|
@ -236,6 +275,16 @@ class TodosController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def filter_to_context
|
||||
context = @user.contexts.find(params['context']['id'])
|
||||
redirect_to formatted_context_todos_path(context, :m)
|
||||
end
|
||||
|
||||
def filter_to_project
|
||||
project = @user.projects.find(params['project']['id'])
|
||||
redirect_to formatted_project_todos_path(project, :m)
|
||||
end
|
||||
|
||||
# /todos/tag/[tag_name] shows all the actions tagged with tag_name
|
||||
#
|
||||
def tag
|
||||
|
|
@ -266,15 +315,47 @@ class TodosController < ApplicationController
|
|||
|
||||
end
|
||||
|
||||
private
|
||||
# Here's the concept behind this "mobile content negotiation" hack:
|
||||
# In addition to the main, AJAXy Web UI, Tracks has a lightweight
|
||||
# low-feature 'mobile' version designed to be suitablef or use
|
||||
# from a phone or PDA. It makes some sense that tne pages of that
|
||||
# mobile version are simply alternate representations of the same
|
||||
# Todo resources. The implementation goal was to treat mobile
|
||||
# as another format and be able to use respond_to to render both
|
||||
# versions. Unfortunately, I ran into a lot of trouble simply
|
||||
# registering a new mime type 'text/html' with format :m because
|
||||
# :html already is linked to that mime type and the new
|
||||
# registration was forcing all html requests to be rendered in
|
||||
# the mobile view. The before_filter and after_filter hackery
|
||||
# below accomplishs that implementation goal by using a 'fake'
|
||||
# mime type during the processing and then setting it to
|
||||
# 'text/html' in an 'after_filter' -LKM 2007-04-01
|
||||
def mobile?
|
||||
return params[:format] == 'm' || response.content_type == MOBILE_CONTENT_TYPE
|
||||
end
|
||||
|
||||
def enable_mobile_content_negotiation
|
||||
if mobile?
|
||||
request.accepts.unshift(Mime::Type::lookup(MOBILE_CONTENT_TYPE))
|
||||
end
|
||||
end
|
||||
|
||||
def restore_content_type_for_mobile
|
||||
if mobile?
|
||||
response.content_type = 'text/html'
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
|
||||
def get_todo_from_params
|
||||
@todo = @user.todos.find(params['id'])
|
||||
end
|
||||
|
||||
def init
|
||||
@source_view = params['_source_view'] || 'todo'
|
||||
init_data_for_sidebar
|
||||
init_data_for_sidebar unless mobile?
|
||||
init_todos
|
||||
end
|
||||
|
||||
|
|
@ -321,13 +402,13 @@ class TodosController < ApplicationController
|
|||
|
||||
def with_parent_resource_scope(&block)
|
||||
if (params[:context_id])
|
||||
context = @user.contexts.find_by_params(params)
|
||||
Todo.with_scope :find => {:conditions => ['todos.context_id = ?', context.id]} do
|
||||
@context = @user.contexts.find_by_params(params)
|
||||
Todo.with_scope :find => {:conditions => ['todos.context_id = ?', @context.id]} do
|
||||
yield
|
||||
end
|
||||
elsif (params[:project_id])
|
||||
project = @user.projects.find_by_params(params)
|
||||
Todo.with_scope :find => {:conditions => ['todos.project_id = ?', project.id]} do
|
||||
@project = @user.projects.find_by_params(params)
|
||||
Todo.with_scope :find => {:conditions => ['todos.project_id = ?', @project.id]} do
|
||||
yield
|
||||
end
|
||||
else
|
||||
|
|
@ -350,12 +431,26 @@ class TodosController < ApplicationController
|
|||
with_feed_query_scope do
|
||||
with_parent_resource_scope do
|
||||
with_limit_scope do
|
||||
|
||||
if mobile?
|
||||
|
||||
@todos, @page = @user.todos.paginate(:all,
|
||||
:conditions => ['state = ?', 'active' ], :include => [:context],
|
||||
:order => 'due IS NULL, due ASC, todos.created_at ASC',
|
||||
:page => params[:page], :per_page => 6)
|
||||
@pagination_params = { :format => :m }
|
||||
@pagination_params[:context_id] = @context.to_param if @context
|
||||
@pagination_params[:project_id] = @project.to_param if @project
|
||||
|
||||
else
|
||||
|
||||
# Exclude hidden projects from count on home page
|
||||
@todos = @user.todos.find(:all, :conditions => ['todos.state = ? or todos.state = ?', 'active', 'complete'], :include => [ :project, :context, :tags ])
|
||||
|
||||
# Exclude hidden projects from count on home page
|
||||
@todos = @user.todos.find(:all, :conditions => ['todos.state = ? or todos.state = ?', 'active', 'complete'], :include => [ :project, :context, :tags ])
|
||||
|
||||
# Exclude hidden projects from the home page
|
||||
@not_done_todos = @user.todos.find(:all, :conditions => ['todos.state = ?', 'active'], :order => "todos.due IS NULL, todos.due ASC, todos.created_at ASC", :include => [ :project, :context, :tags ])
|
||||
# Exclude hidden projects from the home page
|
||||
@not_done_todos = @user.todos.find(:all, :conditions => ['todos.state = ?', 'active'], :order => "todos.due IS NULL, todos.due ASC, todos.created_at ASC", :include => [ :project, :context, :tags ])
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
|
@ -415,6 +510,23 @@ class TodosController < ApplicationController
|
|||
render
|
||||
end
|
||||
end
|
||||
|
||||
def render_todos_mobile
|
||||
lambda do
|
||||
@page_title = "All actions"
|
||||
if @context
|
||||
@page_title += " in context #{@context.name}"
|
||||
@down_count = @context.not_done_todo_count
|
||||
elsif @project
|
||||
@page_title += " in project #{@project.name}"
|
||||
@down_count = @project.not_done_todo_count
|
||||
else
|
||||
determine_down_count
|
||||
end
|
||||
|
||||
render :action => 'index_mobile'
|
||||
end
|
||||
end
|
||||
|
||||
def render_rss_feed
|
||||
lambda do
|
||||
|
|
|
|||
|
|
@ -185,6 +185,11 @@ module TodosHelper
|
|||
split_notes = notes.split(/\n/)
|
||||
joined_notes = split_notes.join("\\n")
|
||||
end
|
||||
|
||||
def formatted_pagination(total, per_page)
|
||||
s = will_paginate(@down_count, 6)
|
||||
(s.gsub /(<\/[^<]+>)/, '\1 ').chomp(' ')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
<%= render_flash %>
|
||||
<% if @todos.length == 0 -%>
|
||||
<p>There are no incomplete actions in this <%= @type %></p>
|
||||
<% else -%>
|
||||
<ul>
|
||||
<% for todo in @todos -%>
|
||||
<li>
|
||||
<%= link_to "»", :controller => 'mobile', :action => 'detail', :id => todo.id %>
|
||||
<% if todo.due? -%>
|
||||
<%= due_date_mobile(todo.due) %>
|
||||
<% end -%>
|
||||
<%= todo.description %>
|
||||
(<em><%= todo.context.name %></em>)
|
||||
<% end -%>
|
||||
</li>
|
||||
<ul>
|
||||
<% if !@todos_pages.nil? -%>
|
||||
<% if @todos_pages.length > 1 -%>
|
||||
<hr />
|
||||
Pages: <%= pagination_links( @todos_pages, :always_show_anchors => true ) %>
|
||||
<% end -%>
|
||||
<% end -%>
|
||||
<% end -%>
|
||||
<hr />
|
||||
<% form_tag( { :action => "filter", :type => "context" } ) do -%>
|
||||
<%= collection_select( "context", "id", @contexts, "id", "name",
|
||||
{ :include_blank => true } ) %>
|
||||
<%= submit_tag( value = "Go" ) %>
|
||||
<% end -%>
|
||||
|
||||
<% form_tag( {:action => "filter", :type => "project" }) do -%>
|
||||
<%= collection_select( "project", "id", @projects, "id", "name",
|
||||
{ :include_blank => true } ) %>
|
||||
<%= submit_tag( value = "Go" ) %>
|
||||
<% end -%>
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
<% form_tag :action => 'update', :id => @item.id do -%>
|
||||
<%= render :partial => 'mobile_edit' %>
|
||||
<% end -%>
|
||||
<%= button_to "Back", :controller => 'mobile', :action => 'index' %>
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
<h1><span class="count"><%= @count.to_s %></span> <%= @desc %>
|
||||
<%= link_to "+", :controller => 'mobile', :action => 'add_action' %></h1>
|
||||
<hr />
|
||||
<%= render :partial => 'mobile_actions' %>
|
||||
|
||||
<%= link_to "View All", :controller => 'mobile', :action => 'index' %>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
<h1><span class="count"><%= @count.to_s %></span> <%= @desc %>
|
||||
<%= link_to "+", :controller => 'mobile', :action => 'add_action' %></h1>
|
||||
<hr />
|
||||
<%= render :partial => 'mobile_actions' %>
|
||||
<%= link_to "Logout", :controller => 'login', :action => 'logout' %>
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
<% form_tag :action => 'update' do %>
|
||||
<%= render :partial => 'mobile_edit' %>
|
||||
<% end -%>
|
||||
<%= button_to "Back", :controller => 'mobile', :action => 'index' %>
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
<div id="footer">
|
||||
<p>Send feedback: <a href="http://dev.rousette.org.uk/report/6">Trac</a> | <a href="http://www.rousette.org.uk/projects/forums/">Forum</a> | <a href="http://www.rousette.org.uk/projects/wiki/index">Wiki</a> | <a href="mailto:butshesagirl@rousette.org.uk?subject=Tracks feedback">Email</a> | <a href="http://www.rousette.org.uk/projects/">Website</a></p>
|
||||
<p>Send feedback: <a href="http://dev.rousette.org.uk/report/6">Trac</a> | <a href="http://www.rousette.org.uk/projects/forums/">Forum</a> | <a href="http://www.rousette.org.uk/projects/wiki/index">Wiki</a> | <a href="mailto:butshesagirl@rousette.org.uk?subject=Tracks feedback">Email</a> | <a href="http://www.rousette.org.uk/projects/">Website</a> | <a href="http://www.rousette.org.uk/projects/tracks/contribute">Contribute</a></p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
<%= error_messages_for("todo") %>
|
||||
</span>
|
||||
<% this_year = user_time.to_date.strftime("%Y").to_i -%>
|
||||
<p><label for="todo_state">Done?</label></p>
|
||||
<p><%= check_box( "todo", "state", "tabindex" => 1) %></p>
|
||||
<p><label for="done">Done?</label></p>
|
||||
<p><%= check_box_tag("done", 1, @todo && @todo.completed?, "tabindex" => 1) %></p>
|
||||
<p><label for="todo_description">Next action</label></p>
|
||||
<p><%= text_field( "todo", "description", "tabindex" => 2) %></p>
|
||||
<p><label for="todo_notes">Notes</label></p>
|
||||
|
|
@ -18,4 +18,3 @@
|
|||
<p><label for="todo_show_from">Show from</label></p>
|
||||
<p><%= date_select("todo", "show_from", :order => [:day, :month, :year],
|
||||
:start_year => this_year, :include_blank => true) %></p>
|
||||
<p><input type="submit" value="Update" tabindex="6" /></p>
|
||||
33
tracks/app/views/todos/_mobile_actions.rhtml
Normal file
33
tracks/app/views/todos/_mobile_actions.rhtml
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<%= render_flash %>
|
||||
<% if @todos.length == 0 -%>
|
||||
<p>There are no incomplete actions in this <%= @type %></p>
|
||||
<% else -%>
|
||||
<ul>
|
||||
<% for todo in @todos -%>
|
||||
<li id="<%= dom_id(todo) %>">
|
||||
<%= link_to "»", formatted_todo_path(todo, :m) %>
|
||||
<% if todo.due? -%>
|
||||
<%= due_date_mobile(todo.due) %>
|
||||
<% end -%>
|
||||
<%= todo.description %>
|
||||
(<em><%= todo.context.name %></em>)
|
||||
</li>
|
||||
<% end -%>
|
||||
</ul>
|
||||
<% if @down_count > 6 -%>
|
||||
<hr />
|
||||
Pages: <%= formatted_pagination(@down_count, 6) %>
|
||||
<% end -%>
|
||||
<% end -%>
|
||||
<hr />
|
||||
<% form_tag( formatted_filter_to_context_todos_path(:m), :method => :post ) do -%>
|
||||
<%= collection_select( "context", "id", @contexts, "id", "name",
|
||||
{ :include_blank => true } ) %>
|
||||
<%= submit_tag( "Go", :id => 'change_context' ) %>
|
||||
<% end -%>
|
||||
|
||||
<% form_tag( formatted_filter_to_project_todos_path(:m), :method => :post ) do -%>
|
||||
<%= collection_select( "project", "id", @projects, "id", "name",
|
||||
{ :include_blank => true } ) %>
|
||||
<%= submit_tag( "Go", :id => 'change_project' ) %>
|
||||
<% end -%>
|
||||
6
tracks/app/views/todos/index_mobile.rhtml
Normal file
6
tracks/app/views/todos/index_mobile.rhtml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<h1><span class="count"><%= @down_count %></span> <%= @page_title %>
|
||||
<%= link_to "+", formatted_new_todo_path(:m) %></h1>
|
||||
<hr />
|
||||
<%= render :partial => 'mobile_actions' %>
|
||||
<hr />
|
||||
<%= link_to "Logout", :controller => 'login', :action => 'logout' %>
|
||||
5
tracks/app/views/todos/new_mobile.rhtml
Normal file
5
tracks/app/views/todos/new_mobile.rhtml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<% form_tag formatted_todos_path(:m), :method => :post do %>
|
||||
<%= render :partial => 'edit_mobile' %>
|
||||
<p><input type="submit" value="Create" tabindex="6" /></p>
|
||||
<% end -%>
|
||||
<%= link_to "Back", formatted_todos_path(:m) %>
|
||||
5
tracks/app/views/todos/show_mobile.rhtml
Normal file
5
tracks/app/views/todos/show_mobile.rhtml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<% form_tag formatted_todo_path(@todo, :m), :method => :put do %>
|
||||
<%= render :partial => 'edit_mobile' %>
|
||||
<p><input type="submit" value="Update" tabindex="6" /></p>
|
||||
<% end -%>
|
||||
<%= link_to "Back", formatted_todos_path(:m) %>
|
||||
|
|
@ -80,3 +80,6 @@ if (AUTHENTICATION_SCHEMES.include? 'open_id')
|
|||
#requires ruby-openid gem to be installed
|
||||
end
|
||||
|
||||
|
||||
MOBILE_CONTENT_TYPE = 'tracks/mobile'
|
||||
Mime::Type.register(MOBILE_CONTENT_TYPE, :m)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
ActionController::Routing::Routes.draw do |map|
|
||||
UJS::routes
|
||||
|
||||
# Mobile/lite version
|
||||
map.connect 'mobile', :controller => 'mobile', :action => 'index'
|
||||
map.connect 'mobile/add_action', :controller => 'mobile', :action => 'show_add_form'
|
||||
|
||||
# Login Routes
|
||||
map.connect 'login', :controller => 'login', :action => 'login'
|
||||
map.connect 'logout', :controller => 'login', :action => 'logout'
|
||||
|
|
@ -13,30 +9,33 @@ ActionController::Routing::Routes.draw do |map|
|
|||
:member => {:change_password => :get, :update_password => :post,
|
||||
:change_auth_type => :get, :update_auth_type => :post, :complete => :get,
|
||||
:refresh_token => :post }
|
||||
map.with_options :controller => "users" do |users|
|
||||
users.signup 'signup', :action => "new"
|
||||
end
|
||||
map.with_options :controller => "users" do |users|
|
||||
users.signup 'signup', :action => "new"
|
||||
end
|
||||
|
||||
# Context Routes
|
||||
map.resources :contexts, :collection => {:order => :post} do |contexts|
|
||||
contexts.resources :todos
|
||||
contexts.resources :todos, :name_prefix => "context_"
|
||||
end
|
||||
|
||||
# Projects Routes
|
||||
map.resources :projects, :collection => {:order => :post} do |projects|
|
||||
projects.resources :todos
|
||||
projects.resources :todos, :name_prefix => "project_"
|
||||
end
|
||||
|
||||
# ToDo Routes
|
||||
map.resources :todos,
|
||||
:member => {:toggle_check => :post},
|
||||
:collection => {:check_deferred => :post}
|
||||
:collection => {:check_deferred => :post, :filter_to_context => :post, :filter_to_project => :post}
|
||||
map.with_options :controller => "todos" do |todos|
|
||||
todos.home '', :action => "index"
|
||||
todos.tickler 'tickler', :action => "list_deferred"
|
||||
todos.done 'done', :action => "completed"
|
||||
todos.done_archive 'done/archive', :action => "completed_archive"
|
||||
todos.tag 'todos/tag/:name', :action => "tag"
|
||||
todos.mobile 'mobile', :action => "index", :format => 'm'
|
||||
todos.mobile_abbrev 'm', :action => "index", :format => 'm'
|
||||
todos.mobile_abbrev_new 'm/new', :action => "new", :format => 'm'
|
||||
end
|
||||
|
||||
# Notes Routes
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ ActiveRecord::Schema.define(:version => 31) do
|
|||
|
||||
create_table "contexts", :force => true do |t|
|
||||
t.column "name", :string, :default => "", :null => false
|
||||
t.column "hide", :boolean, :default => false
|
||||
t.column "hide", :integer, :limit => 4, :default => 0, :null => false
|
||||
t.column "position", :integer, :default => 0, :null => false
|
||||
t.column "user_id", :integer, :default => 0, :null => false
|
||||
t.column "created_at", :datetime
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ module LoginSystem
|
|||
def access_denied
|
||||
respond_to do |format|
|
||||
format.html { redirect_to :controller=>"login", :action =>"login" }
|
||||
format.m { redirect_to :controller=>"login", :action =>"login" }
|
||||
format.js { render :partial => 'login/redirect_to_login' }
|
||||
format.xml { basic_auth_denied }
|
||||
format.rss { basic_auth_denied }
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ module Tracks
|
|||
end
|
||||
|
||||
def source_view
|
||||
responder = Tracks::SourceViewSwitching::Responder.new(params[:_source_view])
|
||||
responder = Tracks::SourceViewSwitching::Responder.new(params[:_source_view] || @source_view)
|
||||
block_given? ? yield(responder) : responder
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
require File.dirname(__FILE__) + '/../test_helper'
|
||||
require 'mobile_controller'
|
||||
|
||||
# Re-raise errors caught by the controller.
|
||||
class MobileController; def rescue_action(e) raise e end; end
|
||||
|
||||
class MobileControllerTest < Test::Unit::TestCase
|
||||
fixtures :users, :preferences, :projects, :contexts, :todos
|
||||
|
||||
def setup
|
||||
@controller = MobileController.new
|
||||
@request = ActionController::TestRequest.new
|
||||
@response = ActionController::TestResponse.new
|
||||
end
|
||||
|
||||
def test_get_index_when_not_logged_in
|
||||
get :index
|
||||
assert_redirected_to :controller => 'login', :action => 'login'
|
||||
end
|
||||
|
||||
def test_create_todo
|
||||
@count = Todo.find(:all)
|
||||
@request.session['user_id'] = users(:admin_user).id
|
||||
xhr :post, :update, "todo"=>{"context_id"=>"1", "project_id"=>"2", "notes"=>"", "description"=>"Invest in spam stock offer", "due"=>"01/01/2007", "show_from"=>"", "state"=>"0"}
|
||||
@todos = Todo.find(:all)
|
||||
assert_equal @count.size+1, @todos.size
|
||||
t = Todo.find(:first, :conditions => ['description = ?', "Invest in spam stock offer"])
|
||||
assert_equal "Invest in spam stock offer", t.description
|
||||
assert_equal Date.parse("01/01/2007"), t.due
|
||||
assert_equal users(:admin_user).id, t.user_id
|
||||
assert_equal 1, t.context_id
|
||||
assert_equal 2, t.project_id
|
||||
assert_equal "active", t.state
|
||||
end
|
||||
|
||||
def test_update_todo
|
||||
t = Todo.find(1)
|
||||
@request.session['user_id'] = users(:admin_user).id
|
||||
xhr :post, :update, :id => 1, :_source_view => 'todo', "todo"=>{"context_id"=>"1", "project_id"=>"2", "id"=>"1", "notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"11/30/2006"}
|
||||
t = Todo.find(1)
|
||||
assert_equal "Call Warren Buffet to find out how much he makes per day", t.description
|
||||
assert_equal Date.parse("11/30/2006"), t.due
|
||||
assert_equal users(:admin_user).id, t.user_id
|
||||
assert_equal "active", t.state
|
||||
end
|
||||
|
||||
def test_complete_todo
|
||||
t = Todo.find(1)
|
||||
@request.session['user_id'] = users(:admin_user).id
|
||||
xhr :post, :update, :id => 1, :_source_view => 'todo', "todo"=>{"context_id"=>"1", "project_id"=>"2", "id"=>"1", "notes"=>"", "description"=>"Call Bill Gates to find out how much he makes per day", "state"=>"1"}
|
||||
t = Todo.find(1)
|
||||
assert_equal "completed", t.state
|
||||
end
|
||||
end
|
||||
|
|
@ -242,5 +242,34 @@ class TodosControllerTest < Test::Unit::TestCase
|
|||
assert !(/ /.match(@response.body))
|
||||
#puts @response.body
|
||||
end
|
||||
|
||||
def test_mobile_index_uses_text_html_content_type
|
||||
@request.session['user_id'] = users(:admin_user).id
|
||||
get :index, { :format => "m" }
|
||||
assert_equal 'text/html; charset=utf-8', @response.headers["Content-Type"]
|
||||
end
|
||||
|
||||
def test_mobile_index_assigns_down_count
|
||||
@request.session['user_id'] = users(:admin_user).id
|
||||
get :index, { :format => "m" }
|
||||
assert_equal 10, assigns['down_count']
|
||||
end
|
||||
|
||||
def test_mobile_create_action
|
||||
@request.session['user_id'] = users(:admin_user).id
|
||||
post :create, {"format"=>"m", "todo"=>{"context_id"=>"2",
|
||||
"due(1i)"=>"2007", "due(2i)"=>"1", "due(3i)"=>"2",
|
||||
"show_from(1i)"=>"", "show_from(2i)"=>"", "show_from(3i)"=>"",
|
||||
"project_id"=>"1",
|
||||
"notes"=>"test notes", "description"=>"test_mobile_create_action", "state"=>"0"}}
|
||||
t = Todo.find_by_description("test_mobile_create_action")
|
||||
assert_not_nil t
|
||||
assert_equal 2, t.context_id
|
||||
assert_equal 1, t.project_id
|
||||
assert t.active?
|
||||
assert_equal 'test notes', t.notes
|
||||
assert_nil t.show_from
|
||||
assert_equal Date.new(2007,1,2).to_s, t.due.to_s
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
|||
18
tracks/test/selenium/mobile/create_new_action.rsel
Normal file
18
tracks/test/selenium/mobile/create_new_action.rsel
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
setup :fixtures => :all
|
||||
login :as => 'admin'
|
||||
|
||||
open '/m'
|
||||
wait_for_text 'css=h1 span.count', '10'
|
||||
|
||||
click_and_wait "link=+"
|
||||
|
||||
type "todo_notes", "test notes"
|
||||
type "todo_description", "test name"
|
||||
select "todo_context_id", "label=call"
|
||||
select "todo_project_id", "label=Make more money than Billy Gates"
|
||||
select "todo_due_3i", "label=1"
|
||||
select "todo_due_2i", "label=January"
|
||||
select "todo_due_1i", "label=2007"
|
||||
click_and_wait "//input[@value='Create']"
|
||||
|
||||
wait_for_text 'css=h1 span.count', '11'
|
||||
11
tracks/test/selenium/mobile/mark_done.rsel
Normal file
11
tracks/test/selenium/mobile/mark_done.rsel
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
setup :fixtures => :all
|
||||
login :as => 'admin'
|
||||
|
||||
open '/m'
|
||||
wait_for_text 'css=h1 span.count', '10'
|
||||
|
||||
open_and_wait '/todos/6.m'
|
||||
click "done"
|
||||
click_and_wait "//input[@value='Update']"
|
||||
|
||||
wait_for_text 'css=h1 span.count', '9'
|
||||
25
tracks/test/selenium/mobile/navigation.rsel
Normal file
25
tracks/test/selenium/mobile/navigation.rsel
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
setup :fixtures => :all
|
||||
login :as => 'admin'
|
||||
|
||||
open '/m'
|
||||
wait_for_title "All actions"
|
||||
wait_for_text 'css=h1 span.count', '10'
|
||||
|
||||
click_and_wait "link=2"
|
||||
verify_title "All actions"
|
||||
wait_for_text 'css=h1 span.count', '10'
|
||||
|
||||
select "context_id", "label=agenda"
|
||||
click_and_wait "change_context"
|
||||
verify_title "All actions in context agenda"
|
||||
wait_for_text 'css=h1 span.count', '5'
|
||||
|
||||
select "context_id", "label=call"
|
||||
click_and_wait "change_context"
|
||||
verify_title "All actions in context call"
|
||||
wait_for_text 'css=h1 span.count', '3'
|
||||
|
||||
select "project_id", "label=Build a working time machine"
|
||||
click_and_wait "change_project"
|
||||
verify_title "All actions in project Build a working time machine"
|
||||
wait_for_text 'css=h1 span.count', '2'
|
||||
58
tracks/vendor/plugins/will_paginate/README
vendored
Normal file
58
tracks/vendor/plugins/will_paginate/README
vendored
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
WillPaginate
|
||||
|
||||
Ruby port by: PJ Hyett
|
||||
Original PHP source: http://www.strangerstudios.com/sandbox/pagination/diggstyle.php
|
||||
Contributors: K. Adam Christensen, Chris Wanstrath, Dr. Nic Williams
|
||||
|
||||
Example usage:
|
||||
|
||||
app/models/post.rb
|
||||
|
||||
class Post < ActiveRecord::Base
|
||||
cattr_reader :per_page
|
||||
@@per_page = 50
|
||||
end
|
||||
|
||||
app/controller/posts_controller.rb
|
||||
|
||||
def index
|
||||
@board = Board.find(params[:id])
|
||||
@posts, @page = Post.paginate_all_by_board_id(@board.id, :page => params[:page])
|
||||
end
|
||||
|
||||
app/views/posts/index.rhtml
|
||||
|
||||
<%= will_paginate(@board.topic_count, Post.per_page) %>
|
||||
|
||||
|
||||
Copy the following css into your stylesheet for a good start:
|
||||
|
||||
.pagination {
|
||||
padding: 3px;
|
||||
margin: 3px;
|
||||
}
|
||||
.pagination a {
|
||||
padding: 2px 5px 2px 5px;
|
||||
margin: 2px;
|
||||
border: 1px solid #aaaadd;
|
||||
text-decoration: none;
|
||||
color: #000099;
|
||||
}
|
||||
.pagination a:hover, .pagination a:active {
|
||||
border: 1px solid #000099;
|
||||
color: #000;
|
||||
}
|
||||
.pagination span.current {
|
||||
padding: 2px 5px 2px 5px;
|
||||
margin: 2px;
|
||||
border: 1px solid #000099;
|
||||
font-weight: bold;
|
||||
background-color: #000099;
|
||||
color: #FFF;
|
||||
}
|
||||
.pagination span.disabled {
|
||||
padding: 2px 5px 2px 5px;
|
||||
margin: 2px;
|
||||
border: 1px solid #eee;
|
||||
color: #ddd;
|
||||
}
|
||||
4
tracks/vendor/plugins/will_paginate/init.rb
vendored
Normal file
4
tracks/vendor/plugins/will_paginate/init.rb
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
require 'will_paginate'
|
||||
require 'finder'
|
||||
ActionView::Base.send(:include, WillPaginate)
|
||||
ActiveRecord::Base.send(:include, WillPaginate::Finder)
|
||||
28
tracks/vendor/plugins/will_paginate/lib/finder.rb
vendored
Normal file
28
tracks/vendor/plugins/will_paginate/lib/finder.rb
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
module WillPaginate
|
||||
module Finder
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
class << base
|
||||
define_method(:per_page) { 30 } unless respond_to? :per_page
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def method_missing_with_will_paginate(method_id, *args, &block)
|
||||
unless match = /^paginate/.match(method_id.to_s)
|
||||
return method_missing_without_will_paginate(method_id, *args, &block)
|
||||
end
|
||||
|
||||
options = args.last.is_a?(Hash) ? args.pop : {}
|
||||
page = (page = options.delete(:page).to_i).zero? ? 1 : page
|
||||
limit_per_page = options.delete(:per_page) || per_page
|
||||
args << options
|
||||
|
||||
with_scope :find => { :offset => (page - 1) * limit_per_page, :limit => limit_per_page } do
|
||||
[send(method_id.to_s.sub(/^paginate/, 'find'), *args), page]
|
||||
end
|
||||
end
|
||||
alias_method_chain :method_missing, :will_paginate
|
||||
end
|
||||
end
|
||||
end
|
||||
43
tracks/vendor/plugins/will_paginate/lib/will_paginate.rb
vendored
Normal file
43
tracks/vendor/plugins/will_paginate/lib/will_paginate.rb
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
module WillPaginate
|
||||
def will_paginate(total_count, per_page, page = @page)
|
||||
adjacents = 2
|
||||
prev_page = page - 1
|
||||
next_page = page + 1
|
||||
last_page = (total_count / per_page.to_f).ceil
|
||||
lpm1 = last_page - 1
|
||||
|
||||
returning '' do |pgn|
|
||||
if last_page > 1
|
||||
pgn << %{<div class="pagination">}
|
||||
|
||||
# not enough pages to bother breaking
|
||||
if last_page < 7 + (adjacents * 2)
|
||||
1.upto(last_page) { |ctr| pgn << (ctr == page ? content_tag(:span, ctr, :class => 'current') : link_to(ctr, params.merge(:page => ctr))) }
|
||||
|
||||
# enough pages to hide some
|
||||
elsif last_page > 5 + (adjacents * 2)
|
||||
|
||||
# close to beginning, only hide later pages
|
||||
if page < 1 + (adjacents * 2)
|
||||
1.upto(3 + (adjacents * 2)) { |ctr| pgn << (ctr == page ? content_tag(:span, ctr, :class => 'current') : link_to(ctr, :page => ctr)) }
|
||||
pgn << "..." + link_to(lpm1, params.merge(:page => lpm1)) + link_to(last_page, params.merge(:page => last_page))
|
||||
|
||||
# in middle, hide some from both sides
|
||||
elsif last_page - (adjacents * 2) > page && page > (adjacents * 2)
|
||||
pgn << link_to('1', params.merge(:page => 1)) + link_to('2', params.merge(:page => 2)) + "..."
|
||||
(page - adjacents).upto(page + adjacents) { |ctr| pgn << (ctr == page ? content_tag(:span, ctr, :class => 'current') : link_to(ctr, params.merge(:page => ctr))) }
|
||||
pgn << "..." + link_to(lpm1, params.merge(:page => lpm1)) + link_to(last_page, params.merge(:page => last_page))
|
||||
|
||||
# close to end, only hide early pages
|
||||
else
|
||||
pgn << link_to('1', params.merge(:page => 1)) + link_to('2', params.merge(:page => 2)) + "..."
|
||||
(last_page - (2 + (adjacents * 2))).upto(last_page) { |ctr| pgn << (ctr == page ? content_tag(:span, ctr, :class => 'current') : link_to(ctr, params.merge(:page => ctr))) }
|
||||
end
|
||||
end
|
||||
pgn << (page > 1 ? link_to("« Previous", params.merge(:page => prev_page)) : content_tag(:span, "« Previous", :class => 'disabled'))
|
||||
pgn << (page < last_page ? link_to("Next »", params.merge(:page => next_page)) : content_tag(:span, "Next »", :class => 'disabled'))
|
||||
pgn << '</div>'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue