mirror of
https://github.com/TracksApp/tracks.git
synced 2026-03-03 19:40:15 +01:00
The contexts controller gets more RESTy. It now supports XML, RSS, ATOM, HTML and plain text views of the contexts list.
Changes include:
* Update the URL on the Feeds page to use /contexts.rss or /contexts.txt instead of FeedController? link
* Add created_at and updated_at timestamps to contexts table to support ATOM feeds
Notes:
* This will break previous context listing feed subscriptions.
git-svn-id: http://www.rousette.org.uk/svn/tracks-repos/trunk@423 a4c988fc-2ded-0310-b66e-134b36920a42
This commit is contained in:
parent
6045a7a986
commit
fcab16a5c2
15 changed files with 202 additions and 40 deletions
|
|
@ -4,12 +4,29 @@ class ContextsController < ApplicationController
|
||||||
|
|
||||||
before_filter :init, :except => [:create, :destroy, :order]
|
before_filter :init, :except => [:create, :destroy, :order]
|
||||||
before_filter :init_todos, :only => :show
|
before_filter :init_todos, :only => :show
|
||||||
|
skip_before_filter :login_required, :only => [:index]
|
||||||
|
prepend_before_filter :login_or_feed_token_required, :only => [:index]
|
||||||
|
session :off, :only => :index, :if => Proc.new { |req| ['rss','atom','txt'].include?(req.parameters[:format]) }
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@page_title = "TRACKS::List Contexts"
|
respond_to do |format|
|
||||||
respond_to do |wants|
|
format.html do
|
||||||
wants.html
|
@page_title = "TRACKS::List Contexts"
|
||||||
wants.xml { render :xml => @contexts.to_xml( :except => :user_id ) }
|
render
|
||||||
|
end
|
||||||
|
format.xml { render :xml => @contexts.to_xml( :except => :user_id ) }
|
||||||
|
format.rss do
|
||||||
|
render_rss_feed_for @contexts, :feed => Context.feed_options(@user),
|
||||||
|
:item => { :description => lambda { |c| c.summary(count_undone_todos(c)) } }
|
||||||
|
end
|
||||||
|
format.atom do
|
||||||
|
render_atom_feed_for @contexts, :feed => Context.feed_options(@user),
|
||||||
|
:item => { :description => lambda { |c| c.summary(count_undone_todos(c)) },
|
||||||
|
:author => lambda { |c| nil } }
|
||||||
|
end
|
||||||
|
format.text do
|
||||||
|
render :action => 'index_text', :layout => false, :content_type => Mime::TEXT
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,14 +45,7 @@ class FeedController < ApplicationController
|
||||||
end
|
end
|
||||||
headers["Content-Type"] = "text/calendar"
|
headers["Content-Type"] = "text/calendar"
|
||||||
end
|
end
|
||||||
|
|
||||||
def list_contexts_only
|
|
||||||
init_not_done_counts('context')
|
|
||||||
@contexts = @user.contexts
|
|
||||||
@description = "Lists all the contexts for #{@user.login}."
|
|
||||||
render :action => 'contexts_' + params['feedtype']
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
# Check whether the token in the URL matches the word in the User's table
|
# Check whether the token in the URL matches the word in the User's table
|
||||||
|
|
|
||||||
|
|
@ -30,19 +30,7 @@ module FeedHelper
|
||||||
end
|
end
|
||||||
return result_string
|
return result_string
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_contexts_text_page(contexts)
|
|
||||||
result_string = ""
|
|
||||||
contexts.each do |c|
|
|
||||||
result_string << "\n" + c.name.upcase + "\n"
|
|
||||||
|
|
||||||
result_string << "#{count_undone_todos(c)}. Context is #{c.hidden? ? 'Hidden' : 'Active'}.\n"
|
|
||||||
result_string << "\n"
|
|
||||||
end
|
|
||||||
|
|
||||||
return result_string
|
|
||||||
end
|
|
||||||
|
|
||||||
def format_ical_notes(notes)
|
def format_ical_notes(notes)
|
||||||
split_notes = notes.split(/\n/)
|
split_notes = notes.split(/\n/)
|
||||||
joined_notes = split_notes.join("\\n")
|
joined_notes = split_notes.join("\\n")
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,13 @@ class Context < ActiveRecord::Base
|
||||||
validates_does_not_contain :name, :string => '/', :message => "cannot contain the slash ('/') character"
|
validates_does_not_contain :name, :string => '/', :message => "cannot contain the slash ('/') character"
|
||||||
validates_does_not_contain :name, :string => ',', :message => "cannot contain the comma (',') character"
|
validates_does_not_contain :name, :string => ',', :message => "cannot contain the comma (',') character"
|
||||||
|
|
||||||
|
def self.feed_options(user)
|
||||||
|
{
|
||||||
|
:title => 'Tracks Contexts',
|
||||||
|
:description => "Lists all the contexts for #{user.display_name}."
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def hidden?
|
def hidden?
|
||||||
self.hide == true
|
self.hide == true
|
||||||
end
|
end
|
||||||
|
|
@ -23,5 +30,16 @@ class Context < ActiveRecord::Base
|
||||||
def to_param
|
def to_param
|
||||||
url_friendly_name
|
url_friendly_name
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def title
|
||||||
|
name
|
||||||
|
end
|
||||||
|
|
||||||
|
def summary(undone_todo_count)
|
||||||
|
s = "<p>#{undone_todo_count}. "
|
||||||
|
s += "Context is #{hidden? ? 'Hidden' : 'Active'}. "
|
||||||
|
s += "</p>"
|
||||||
|
s
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
5
tracks/app/views/contexts/index_text.rhtml
Normal file
5
tracks/app/views/contexts/index_text.rhtml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<% @contexts.each do |c| -%>
|
||||||
|
|
||||||
|
<%= c.name.upcase %>
|
||||||
|
<%= count_undone_todos(c)%>. Context is <%= c.hidden? ? "Hidden" : "Active" %>.
|
||||||
|
<% end -%>
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
<% headers["Content-Type"] = "text/plain; charset=utf-8" -%>
|
|
||||||
<%= build_contexts_text_page( @contexts ) -%>
|
|
||||||
|
|
@ -40,8 +40,8 @@
|
||||||
Actions completed in the last 7 days
|
Actions completed in the last 7 days
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<%= rss_feed_link({ :action => 'list_contexts_only', :feedtype => 'rss' }) %>
|
<%= rss_formatted_link({:controller => 'contexts', :action => 'index'}) %>
|
||||||
<%= text_feed_link({ :action => 'list_contexts_only', :feedtype => 'text' }) %>
|
<%= text_formatted_link({:controller => 'contexts', :action => 'index'}) %>
|
||||||
All Contexts
|
All Contexts
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ ActionController::Routing::Routes.draw do |map|
|
||||||
# Context Routes
|
# Context Routes
|
||||||
map.resources :contexts, :collection => {:order => :post}
|
map.resources :contexts, :collection => {:order => :post}
|
||||||
map.connect 'context/:context/feed/:action/:login/:token', :controller => 'feed'
|
map.connect 'context/:context/feed/:action/:login/:token', :controller => 'feed'
|
||||||
map.connect 'contexts/feed/:feedtype/:login/:token', :controller => 'feed', :action => 'list_contexts_only'
|
|
||||||
|
|
||||||
# Projects Routes
|
# Projects Routes
|
||||||
map.resources :projects, :collection => {:order => :post}
|
map.resources :projects, :collection => {:order => :post}
|
||||||
|
|
|
||||||
13
tracks/db/migrate/027_add_context_timestamps.rb
Normal file
13
tracks/db/migrate/027_add_context_timestamps.rb
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
class AddContextTimestamps < ActiveRecord::Migration
|
||||||
|
|
||||||
|
def self.up
|
||||||
|
add_column :contexts, :created_at, :timestamp
|
||||||
|
add_column :contexts, :updated_at, :timestamp
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.down
|
||||||
|
remove_column :contexts, :created_at
|
||||||
|
remove_column :contexts, :updated_at
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
@ -2,13 +2,15 @@
|
||||||
# migrations feature of ActiveRecord to incrementally modify your database, and
|
# migrations feature of ActiveRecord to incrementally modify your database, and
|
||||||
# then regenerate this schema definition.
|
# then regenerate this schema definition.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(:version => 26) do
|
ActiveRecord::Schema.define(:version => 27) do
|
||||||
|
|
||||||
create_table "contexts", :force => true do |t|
|
create_table "contexts", :force => true do |t|
|
||||||
t.column "name", :string, :default => "", :null => false
|
t.column "name", :string, :default => "", :null => false
|
||||||
t.column "hide", :integer, :limit => 4, :default => 0, :null => false
|
t.column "hide", :integer, :limit => 4, :default => 0, :null => false
|
||||||
t.column "position", :integer, :default => 0, :null => false
|
t.column "position", :integer, :default => 0, :null => false
|
||||||
t.column "user_id", :integer, :default => 0, :null => false
|
t.column "user_id", :integer, :default => 0, :null => false
|
||||||
|
t.column "created_at", :datetime
|
||||||
|
t.column "updated_at", :datetime
|
||||||
end
|
end
|
||||||
|
|
||||||
add_index "contexts", ["user_id"], :name => "index_contexts_on_user_id"
|
add_index "contexts", ["user_id"], :name => "index_contexts_on_user_id"
|
||||||
|
|
|
||||||
24
tracks/test/fixtures/contexts.yml
vendored
24
tracks/test/fixtures/contexts.yml
vendored
|
|
@ -1,11 +1,19 @@
|
||||||
# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
|
# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
|
||||||
|
<%
|
||||||
|
|
||||||
|
def today
|
||||||
|
Time.now.utc.to_s(:db)
|
||||||
|
end
|
||||||
|
|
||||||
|
%>
|
||||||
agenda:
|
agenda:
|
||||||
id: 1
|
id: 1
|
||||||
name: agenda
|
name: agenda
|
||||||
position: 1
|
position: 1
|
||||||
hide: false
|
hide: false
|
||||||
user_id: 1
|
user_id: 1
|
||||||
|
created_at: <%= today %>
|
||||||
|
updated_at: <%= today %>
|
||||||
|
|
||||||
call:
|
call:
|
||||||
id: 2
|
id: 2
|
||||||
|
|
@ -13,6 +21,8 @@ call:
|
||||||
position: 2
|
position: 2
|
||||||
hide: false
|
hide: false
|
||||||
user_id: 1
|
user_id: 1
|
||||||
|
created_at: <%= today %>
|
||||||
|
updated_at: <%= today %>
|
||||||
|
|
||||||
email:
|
email:
|
||||||
id: 3
|
id: 3
|
||||||
|
|
@ -20,6 +30,8 @@ email:
|
||||||
position: 3
|
position: 3
|
||||||
hide: false
|
hide: false
|
||||||
user_id: 1
|
user_id: 1
|
||||||
|
created_at: <%= today %>
|
||||||
|
updated_at: <%= today %>
|
||||||
|
|
||||||
errand:
|
errand:
|
||||||
id: 4
|
id: 4
|
||||||
|
|
@ -27,6 +39,8 @@ errand:
|
||||||
position: 4
|
position: 4
|
||||||
hide: false
|
hide: false
|
||||||
user_id: 1
|
user_id: 1
|
||||||
|
created_at: <%= today %>
|
||||||
|
updated_at: <%= today %>
|
||||||
|
|
||||||
lab:
|
lab:
|
||||||
id: 5
|
id: 5
|
||||||
|
|
@ -34,6 +48,8 @@ lab:
|
||||||
position: 5
|
position: 5
|
||||||
hide: false
|
hide: false
|
||||||
user_id: 1
|
user_id: 1
|
||||||
|
created_at: <%= today %>
|
||||||
|
updated_at: <%= today %>
|
||||||
|
|
||||||
library:
|
library:
|
||||||
id: 6
|
id: 6
|
||||||
|
|
@ -41,6 +57,8 @@ library:
|
||||||
position: 6
|
position: 6
|
||||||
hide: false
|
hide: false
|
||||||
user_id: 1
|
user_id: 1
|
||||||
|
created_at: <%= today %>
|
||||||
|
updated_at: <%= today %>
|
||||||
|
|
||||||
freetime:
|
freetime:
|
||||||
id: 7
|
id: 7
|
||||||
|
|
@ -48,6 +66,8 @@ freetime:
|
||||||
position: 7
|
position: 7
|
||||||
hide: false
|
hide: false
|
||||||
user_id: 1
|
user_id: 1
|
||||||
|
created_at: <%= today %>
|
||||||
|
updated_at: <%= today %>
|
||||||
|
|
||||||
office:
|
office:
|
||||||
id: 8
|
id: 8
|
||||||
|
|
@ -55,6 +75,8 @@ office:
|
||||||
position: 8
|
position: 8
|
||||||
hide: false
|
hide: false
|
||||||
user_id: 1
|
user_id: 1
|
||||||
|
created_at: <%= today %>
|
||||||
|
updated_at: <%= today %>
|
||||||
|
|
||||||
waitingfor:
|
waitingfor:
|
||||||
id: 9
|
id: 9
|
||||||
|
|
@ -62,3 +84,5 @@ waitingfor:
|
||||||
position: 9
|
position: 9
|
||||||
hide: false
|
hide: false
|
||||||
user_id: 1
|
user_id: 1
|
||||||
|
created_at: <%= today %>
|
||||||
|
updated_at: <%= today %>
|
||||||
|
|
|
||||||
|
|
@ -39,5 +39,107 @@ class ContextsControllerTest < TodoContainerControllerTestBase
|
||||||
assert_rjs :show, 'status'
|
assert_rjs :show, 'status'
|
||||||
assert_rjs :update, 'status', "<div class=\"ErrorExplanation\" id=\"ErrorExplanation\"><h2>1 error prohibited this record from being saved</h2><p>There were problems with the following fields:</p><ul>Name cannot contain the slash ('/') character</ul></div>"
|
assert_rjs :update, 'status', "<div class=\"ErrorExplanation\" id=\"ErrorExplanation\"><h2>1 error prohibited this record from being saved</h2><p>There were problems with the following fields:</p><ul>Name cannot contain the slash ('/') character</ul></div>"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_rss_feed_content
|
||||||
|
@request.session['user_id'] = users(:admin_user).id
|
||||||
|
get :index, { :format => "rss" }
|
||||||
|
assert_equal 'application/rss+xml; charset=utf-8', @response.headers["Content-Type"]
|
||||||
|
#puts @response.body
|
||||||
|
|
||||||
|
assert_xml_select 'rss[version="2.0"]' do
|
||||||
|
assert_xml_select 'channel' do
|
||||||
|
assert_xml_select '>title', 'Tracks Contexts'
|
||||||
|
assert_xml_select '>description', "Lists all the contexts for #{users(:admin_user).display_name}."
|
||||||
|
assert_xml_select 'language', 'en-us'
|
||||||
|
assert_xml_select 'ttl', '40'
|
||||||
|
end
|
||||||
|
assert_xml_select 'item', 9 do
|
||||||
|
assert_xml_select 'title', /.+/
|
||||||
|
assert_xml_select 'description', /<p>\d+ actions. Context is (active|hidden). <\/p>/
|
||||||
|
%w(guid link).each do |node|
|
||||||
|
assert_xml_select node, /http:\/\/test.host\/contexts\/.+/
|
||||||
|
end
|
||||||
|
assert_xml_select 'pubDate', contexts(:agenda).created_at.to_s(:rfc822)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_rss_feed_not_accessible_to_anonymous_user_without_token
|
||||||
|
@request.session['user_id'] = nil
|
||||||
|
get :index, { :format => "rss" }
|
||||||
|
assert_response 401
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_rss_feed_not_accessible_to_anonymous_user_with_invalid_token
|
||||||
|
@request.session['user_id'] = nil
|
||||||
|
get :index, { :format => "rss", :token => 'foo' }
|
||||||
|
assert_response 401
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_rss_feed_accessible_to_anonymous_user_with_valid_token
|
||||||
|
@request.session['user_id'] = nil
|
||||||
|
get :index, { :format => "rss", :token => users(:admin_user).word }
|
||||||
|
assert_response :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_atom_feed_content
|
||||||
|
@request.session['user_id'] = users(:admin_user).id
|
||||||
|
get :index, { :format => "atom" }
|
||||||
|
assert_equal 'application/atom+xml; charset=utf-8', @response.headers["Content-Type"]
|
||||||
|
#puts @response.body
|
||||||
|
|
||||||
|
assert_xml_select 'feed[xmlns="http://www.w3.org/2005/Atom"]' do
|
||||||
|
assert_xml_select '>title', 'Tracks Contexts'
|
||||||
|
assert_xml_select '>subtitle', "Lists all the contexts for #{users(:admin_user).display_name}."
|
||||||
|
assert_xml_select 'entry', 3 do
|
||||||
|
assert_xml_select 'title', /.+/
|
||||||
|
assert_xml_select 'content[type="html"]', /<p>\d+ actions. Context is (active|hidden). <\/p>/
|
||||||
|
assert_xml_select 'published', contexts(:agenda).created_at.to_s(:rfc822)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_atom_feed_not_accessible_to_anonymous_user_without_token
|
||||||
|
@request.session['user_id'] = nil
|
||||||
|
get :index, { :format => "atom" }
|
||||||
|
assert_response 401
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_atom_feed_not_accessible_to_anonymous_user_with_invalid_token
|
||||||
|
@request.session['user_id'] = nil
|
||||||
|
get :index, { :format => "atom", :token => 'foo' }
|
||||||
|
assert_response 401
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_atom_feed_accessible_to_anonymous_user_with_valid_token
|
||||||
|
@request.session['user_id'] = nil
|
||||||
|
get :index, { :format => "atom", :token => users(:admin_user).word }
|
||||||
|
assert_response :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_text_feed_content
|
||||||
|
@request.session['user_id'] = users(:admin_user).id
|
||||||
|
get :index, { :format => "txt" }
|
||||||
|
assert_equal 'text/plain; charset=utf-8', @response.headers["Content-Type"]
|
||||||
|
#puts @response.body
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_text_feed_not_accessible_to_anonymous_user_without_token
|
||||||
|
@request.session['user_id'] = nil
|
||||||
|
get :index, { :format => "txt" }
|
||||||
|
assert_response 401
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_text_feed_not_accessible_to_anonymous_user_with_invalid_token
|
||||||
|
@request.session['user_id'] = nil
|
||||||
|
get :index, { :format => "txt", :token => 'foo' }
|
||||||
|
assert_response 401
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_text_feed_accessible_to_anonymous_user_with_valid_token
|
||||||
|
@request.session['user_id'] = nil
|
||||||
|
get :index, { :format => "txt", :token => users(:admin_user).word }
|
||||||
|
assert_response :ok
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ require 'contexts_controller'
|
||||||
# Re-raise errors caught by the controller.
|
# Re-raise errors caught by the controller.
|
||||||
class ContextsController; def rescue_action(e) raise e end; end
|
class ContextsController; def rescue_action(e) raise e end; end
|
||||||
|
|
||||||
class ContextsControllerXmlApiTest < ActionController::IntegrationTest
|
class ContextXmlApiTest < ActionController::IntegrationTest
|
||||||
fixtures :users, :contexts
|
fixtures :users, :contexts
|
||||||
|
|
||||||
@@context_name = "@newcontext"
|
@@context_name = "@newcontext"
|
||||||
|
|
@ -51,7 +51,7 @@ class ContextsControllerXmlApiTest < ActionController::IntegrationTest
|
||||||
def test_creates_new_context
|
def test_creates_new_context
|
||||||
initial_count = Context.count
|
initial_count = Context.count
|
||||||
authenticated_post_xml_to_context_create
|
authenticated_post_xml_to_context_create
|
||||||
assert_response_and_body_matches 200, %r|^<\?xml version="1.0" encoding="UTF-8"\?>\n<context>\n <hide type="integer">0</hide>\n <id type="integer">\d+</id>\n <name>#{@@context_name}</name>\n <position type="integer">1</position>\n</context>\n$|
|
assert_response_and_body_matches 200, %r|^<\?xml version="1.0" encoding="UTF-8"\?>\n<context>\n <created-at type=\"datetime\">\d{4}+-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z</created-at>\n <hide type="integer">0</hide>\n <id type="integer">\d+</id>\n <name>#{@@context_name}</name>\n <position type="integer">1</position>\n <updated-at type=\"datetime\">\d{4}+-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z</updated-at>\n</context>\n$|
|
||||||
assert_equal initial_count + 1, Context.count
|
assert_equal initial_count + 1, Context.count
|
||||||
context1 = Context.find_by_name(@@context_name)
|
context1 = Context.find_by_name(@@context_name)
|
||||||
assert_not_nil context1, "expected context '#{@@context_name}' to be created"
|
assert_not_nil context1, "expected context '#{@@context_name}' to be created"
|
||||||
|
|
|
||||||
|
|
@ -72,11 +72,11 @@ class FeedSmokeTest < ActionController::IntegrationTest
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_all_contexts_rss
|
def test_all_contexts_rss
|
||||||
assert_success "/contexts/feed/rss/admin/#{ users(:admin_user).word }"
|
assert_success "/contexts.rss?token=#{ users(:admin_user).word }"
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_all_contexts_txt
|
def test_all_contexts_txt
|
||||||
assert_success "/contexts/feed/text/admin/#{ users(:admin_user).word }"
|
assert_success "/contexts.txt?token=#{ users(:admin_user).word }"
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_all_projects_rss
|
def test_all_projects_rss
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,9 @@ class ContextTest < Test::Unit::TestCase
|
||||||
def test_to_param_returns_url_friendly_name
|
def test_to_param_returns_url_friendly_name
|
||||||
assert_equal 'agenda', @agenda.to_param
|
assert_equal 'agenda', @agenda.to_param
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_title_reader_returns_name
|
||||||
|
assert_equal @agenda.name, @agenda.title
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue