device_tracker 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.rubocop.yml +4 -0
- data/Gemfile +4 -0
- data/Rakefile +10 -0
- data/Readme.md +32 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/device_tracker.gemspec +63 -0
- data/exe/device_tracker +91 -0
- data/lib/device_tracker/app.rb +22 -0
- data/lib/device_tracker/config-schema.json +111 -0
- data/lib/device_tracker/config.ru +9 -0
- data/lib/device_tracker/controllers/application_controller.rb +226 -0
- data/lib/device_tracker/controllers/devices_controller.rb +315 -0
- data/lib/device_tracker/controllers/heartbeat_controller.rb +50 -0
- data/lib/device_tracker/controllers/os_controller.rb +38 -0
- data/lib/device_tracker/controllers/transactions_controller.rb +18 -0
- data/lib/device_tracker/controllers/users_controller.rb +131 -0
- data/lib/device_tracker/db/migrate/20150521071815_create_users.rb +14 -0
- data/lib/device_tracker/db/migrate/20150521082155_create_devices.rb +19 -0
- data/lib/device_tracker/db/migrate/20150521120335_create_operating_systems.rb +9 -0
- data/lib/device_tracker/db/migrate/20150527162242_create_transactions.rb +12 -0
- data/lib/device_tracker/db/migrate/20151027073050_create_heartbeat.rb +11 -0
- data/lib/device_tracker/db/migrate/20151028132946_add_user_verification.rb +8 -0
- data/lib/device_tracker/db/migrate/20151028141328_remove_is_active_from_users.rb +6 -0
- data/lib/device_tracker/db/migrate/20151029085629_add_password_reset_code_to_users.rb +8 -0
- data/lib/device_tracker/db/migrate/20151030130341_add_missing_column_to_devices.rb +8 -0
- data/lib/device_tracker/db/migrate/20151102141601_add_serial_number_to_devices.rb +8 -0
- data/lib/device_tracker/db/schema.rb +61 -0
- data/lib/device_tracker/db/seeds.rb +21 -0
- data/lib/device_tracker/dependencies.rb +16 -0
- data/lib/device_tracker/device_tracker.rb +18 -0
- data/lib/device_tracker/helpers/application_helper.rb +116 -0
- data/lib/device_tracker/helpers/user_helper.rb +24 -0
- data/lib/device_tracker/models/device.rb +42 -0
- data/lib/device_tracker/models/heartbeat.rb +7 -0
- data/lib/device_tracker/models/operating_system.rb +7 -0
- data/lib/device_tracker/models/transaction.rb +62 -0
- data/lib/device_tracker/models/user.rb +28 -0
- data/lib/device_tracker/public/css/bootstrap-sortable.css +100 -0
- data/lib/device_tracker/public/css/bootstrap.min.css +5 -0
- data/lib/device_tracker/public/css/custom.css +88 -0
- data/lib/device_tracker/public/favicon/android-chrome-144x144.png +0 -0
- data/lib/device_tracker/public/favicon/android-chrome-192x192.png +0 -0
- data/lib/device_tracker/public/favicon/android-chrome-36x36.png +0 -0
- data/lib/device_tracker/public/favicon/android-chrome-48x48.png +0 -0
- data/lib/device_tracker/public/favicon/android-chrome-72x72.png +0 -0
- data/lib/device_tracker/public/favicon/android-chrome-96x96.png +0 -0
- data/lib/device_tracker/public/favicon/apple-touch-icon-114x114.png +0 -0
- data/lib/device_tracker/public/favicon/apple-touch-icon-120x120.png +0 -0
- data/lib/device_tracker/public/favicon/apple-touch-icon-144x144.png +0 -0
- data/lib/device_tracker/public/favicon/apple-touch-icon-152x152.png +0 -0
- data/lib/device_tracker/public/favicon/apple-touch-icon-180x180.png +0 -0
- data/lib/device_tracker/public/favicon/apple-touch-icon-57x57.png +0 -0
- data/lib/device_tracker/public/favicon/apple-touch-icon-60x60.png +0 -0
- data/lib/device_tracker/public/favicon/apple-touch-icon-72x72.png +0 -0
- data/lib/device_tracker/public/favicon/apple-touch-icon-76x76.png +0 -0
- data/lib/device_tracker/public/favicon/apple-touch-icon-precomposed.png +0 -0
- data/lib/device_tracker/public/favicon/apple-touch-icon.png +0 -0
- data/lib/device_tracker/public/favicon/browserconfig.xml +12 -0
- data/lib/device_tracker/public/favicon/favicon-16x16.png +0 -0
- data/lib/device_tracker/public/favicon/favicon-32x32.png +0 -0
- data/lib/device_tracker/public/favicon/favicon-96x96.png +0 -0
- data/lib/device_tracker/public/favicon/favicon.ico +0 -0
- data/lib/device_tracker/public/favicon/manifest.json +41 -0
- data/lib/device_tracker/public/favicon/mstile-144x144.png +0 -0
- data/lib/device_tracker/public/favicon/mstile-150x150.png +0 -0
- data/lib/device_tracker/public/favicon/mstile-310x150.png +0 -0
- data/lib/device_tracker/public/favicon/mstile-310x310.png +0 -0
- data/lib/device_tracker/public/favicon/mstile-70x70.png +0 -0
- data/lib/device_tracker/public/favicon/safari-pinned-tab.svg +21 -0
- data/lib/device_tracker/public/favicon.png +0 -0
- data/lib/device_tracker/public/fonts/glyphicons-halflings-regular.eot +0 -0
- data/lib/device_tracker/public/fonts/glyphicons-halflings-regular.svg +288 -0
- data/lib/device_tracker/public/fonts/glyphicons-halflings-regular.ttf +0 -0
- data/lib/device_tracker/public/fonts/glyphicons-halflings-regular.woff +0 -0
- data/lib/device_tracker/public/fonts/glyphicons-halflings-regular.woff2 +0 -0
- data/lib/device_tracker/public/js/bootstrap-sortable.js +211 -0
- data/lib/device_tracker/public/js/bootstrap.min.js +7 -0
- data/lib/device_tracker/public/js/jquery-2.1.4.min.js +4 -0
- data/lib/device_tracker/version.rb +4 -0
- data/lib/device_tracker/views/404.erb +25 -0
- data/lib/device_tracker/views/_alert.erb +5 -0
- data/lib/device_tracker/views/_device_form.erb +52 -0
- data/lib/device_tracker/views/_device_list.erb +47 -0
- data/lib/device_tracker/views/_footer.erb +3 -0
- data/lib/device_tracker/views/_header.erb +97 -0
- data/lib/device_tracker/views/_heartbeat_list.erb +25 -0
- data/lib/device_tracker/views/_user_form.erb +30 -0
- data/lib/device_tracker/views/devices/edit.erb +11 -0
- data/lib/device_tracker/views/devices/index.erb +12 -0
- data/lib/device_tracker/views/devices/new.erb +10 -0
- data/lib/device_tracker/views/devices/show.erb +283 -0
- data/lib/device_tracker/views/devices/users.erb +14 -0
- data/lib/device_tracker/views/emails/new_password.erb +17 -0
- data/lib/device_tracker/views/emails/password_reset.erb +18 -0
- data/lib/device_tracker/views/emails/registration.erb +16 -0
- data/lib/device_tracker/views/emails/reminder.erb +15 -0
- data/lib/device_tracker/views/emails/verification.erb +18 -0
- data/lib/device_tracker/views/forgot_password.erb +6 -0
- data/lib/device_tracker/views/index.erb +38 -0
- data/lib/device_tracker/views/layout.erb +8 -0
- data/lib/device_tracker/views/login.erb +14 -0
- data/lib/device_tracker/views/operating_system/operating_systems.json.jbuilder +9 -0
- data/lib/device_tracker/views/os/manage.erb +38 -0
- data/lib/device_tracker/views/transactions/_transactions_list.erb +18 -0
- data/lib/device_tracker/views/transactions/index.erb +3 -0
- data/lib/device_tracker/views/users/edit.erb +9 -0
- data/lib/device_tracker/views/users/manage.erb +31 -0
- data/lib/device_tracker/views/users/new.erb +7 -0
- metadata +505 -0
@@ -0,0 +1,61 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
ActiveRecord::Schema.define(version: 20_151_109_121_036) do
|
3
|
+
create_table 'devices', force: :cascade do |t|
|
4
|
+
t.string 'unid'
|
5
|
+
t.string 'manufacturer'
|
6
|
+
t.string 'device'
|
7
|
+
t.text 'description'
|
8
|
+
t.datetime 'checked_out_since'
|
9
|
+
t.boolean 'available', default: true
|
10
|
+
t.string 'imei'
|
11
|
+
t.integer 'operating_system_id'
|
12
|
+
t.integer 'user_id'
|
13
|
+
t.boolean 'sim_card', default: false
|
14
|
+
t.boolean 'debug_device', default: false
|
15
|
+
t.datetime 'created_at', null: false
|
16
|
+
t.datetime 'updated_at', null: false
|
17
|
+
t.boolean 'missing', default: false
|
18
|
+
t.integer 'checkout_count', default: 0
|
19
|
+
t.string 'serial_number'
|
20
|
+
end
|
21
|
+
|
22
|
+
add_index 'devices', ['unid'], name: 'index_devices_on_unid'
|
23
|
+
|
24
|
+
create_table 'heartbeats', force: :cascade do |t|
|
25
|
+
t.decimal 'longitude', precision: 10, scale: 6
|
26
|
+
t.decimal 'latitude', precision: 10, scale: 6
|
27
|
+
t.integer 'device_id'
|
28
|
+
t.datetime 'created_at', null: false
|
29
|
+
t.datetime 'updated_at', null: false
|
30
|
+
end
|
31
|
+
|
32
|
+
create_table 'operating_systems', force: :cascade do |t|
|
33
|
+
t.string 'name'
|
34
|
+
t.integer 'api_level'
|
35
|
+
end
|
36
|
+
|
37
|
+
create_table 'transactions', force: :cascade do |t|
|
38
|
+
t.string 'transaction_type'
|
39
|
+
t.integer 'user_id'
|
40
|
+
t.integer 'device_id'
|
41
|
+
t.text 'description'
|
42
|
+
t.datetime 'created_at'
|
43
|
+
end
|
44
|
+
|
45
|
+
add_index 'transactions', ['transaction_type'],
|
46
|
+
name: 'index_transactions_on_transaction_type'
|
47
|
+
|
48
|
+
create_table 'users', force: :cascade do |t|
|
49
|
+
t.string 'username'
|
50
|
+
t.string 'name'
|
51
|
+
t.string 'password_digest'
|
52
|
+
t.string 'email'
|
53
|
+
t.boolean 'is_admin', default: false
|
54
|
+
t.datetime 'created_at', null: false
|
55
|
+
t.datetime 'updated_at', null: false
|
56
|
+
t.boolean 'is_verified', default: false
|
57
|
+
t.string 'reset_code'
|
58
|
+
end
|
59
|
+
|
60
|
+
add_index 'users', ['username'], name: 'index_users_on_username'
|
61
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'faker'
|
2
|
+
|
3
|
+
module DeviceTracker
|
4
|
+
# :nodoc:
|
5
|
+
class Seed
|
6
|
+
def self.seed(user)
|
7
|
+
admin = User.where(email: user['email'])
|
8
|
+
|
9
|
+
return if admin.exists?
|
10
|
+
|
11
|
+
admin = User.find_or_create_by(name: user['name'],
|
12
|
+
username: 'admin',
|
13
|
+
email: user['email'],
|
14
|
+
is_admin: true,
|
15
|
+
is_verified: true)
|
16
|
+
admin.password = user['password']
|
17
|
+
admin.save
|
18
|
+
puts "Added #{admin.username} to the database."
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
|
4
|
+
%w(
|
5
|
+
sinatra
|
6
|
+
sinatra/activerecord
|
7
|
+
sinatra/flash
|
8
|
+
sinatra/partial
|
9
|
+
tilt/erb
|
10
|
+
json
|
11
|
+
pony
|
12
|
+
).each { |d| require d }
|
13
|
+
|
14
|
+
Dir.glob(
|
15
|
+
"#{File.dirname(__FILE__)}/{helpers,controllers,models}/*.rb"
|
16
|
+
).each { |file| require file }
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require_relative './version'
|
2
|
+
require_relative './dependencies'
|
3
|
+
require_relative './app'
|
4
|
+
require 'rack'
|
5
|
+
|
6
|
+
# :nodoc:
|
7
|
+
module DeviceTracker
|
8
|
+
def self.start(host, port)
|
9
|
+
options = {
|
10
|
+
Host: host,
|
11
|
+
Port: port
|
12
|
+
}
|
13
|
+
|
14
|
+
Rack::Handler::Thin.run(App.new, options) do |server|
|
15
|
+
[:INT, :TERM].each { |sig| trap(sig) { server.stop } }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module DeviceTracker
|
2
|
+
# :nodoc:
|
3
|
+
module ApplicationHelper
|
4
|
+
def permission?(user_id)
|
5
|
+
if logged_in_user[:is_admin] != true &&
|
6
|
+
logged_in_user[:id] != user_id.to_i
|
7
|
+
return true
|
8
|
+
end
|
9
|
+
false
|
10
|
+
end
|
11
|
+
|
12
|
+
def admin_check
|
13
|
+
if admin?
|
14
|
+
true
|
15
|
+
else
|
16
|
+
flash_and_go_back 'warning',
|
17
|
+
['You don\'t have permission to access this page.']
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def verification_email(user)
|
22
|
+
@user = user
|
23
|
+
@get_started_link = request.base_url + '/devices'
|
24
|
+
body = ERB.new(
|
25
|
+
File.read(settings.email_path + '/verification.erb')
|
26
|
+
).result(binding)
|
27
|
+
|
28
|
+
send_email(
|
29
|
+
to: @user.email,
|
30
|
+
subject: 'Account Verified | Device Tracker',
|
31
|
+
body: body
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
def admin_email_registration(request, user)
|
36
|
+
@user = user
|
37
|
+
@verification_link = "#{request.base_url}/users/#{user.id}/edit"
|
38
|
+
|
39
|
+
body = ERB.new(
|
40
|
+
File.read(settings.email_path + '/registration.erb')
|
41
|
+
).result(binding)
|
42
|
+
|
43
|
+
send_email(
|
44
|
+
to: [User.where(is_admin: true).map(& :email)],
|
45
|
+
subject: 'New Registration | Device Tracker',
|
46
|
+
body: body
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
def send_email(data)
|
51
|
+
Pony.mail(
|
52
|
+
to: data[:to],
|
53
|
+
from: data[:from] || 'no-reply@device-tracker',
|
54
|
+
subject: data[:subject],
|
55
|
+
html_body: data[:body]
|
56
|
+
)
|
57
|
+
rescue Net::OpenTimeout
|
58
|
+
puts ">>> Net::OpenTimeout exception for [#{subject}]"
|
59
|
+
end
|
60
|
+
|
61
|
+
def admin?
|
62
|
+
user = logged_in_user
|
63
|
+
return if user.nil?
|
64
|
+
user[:is_admin]
|
65
|
+
end
|
66
|
+
|
67
|
+
def value_for(name, object)
|
68
|
+
if flash[name]
|
69
|
+
flash[name]
|
70
|
+
elsif object
|
71
|
+
object.send name
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def generate_activation_code(size = 6)
|
76
|
+
charset = %w(0 1 2 3 4 6 7 8 9 A C D E F G H J K M N P Q R T V W X Y Z)
|
77
|
+
(0...size).map { charset.to_a[rand(charset.size)] }.join
|
78
|
+
end
|
79
|
+
|
80
|
+
def protected!
|
81
|
+
if logged_in_user.nil?
|
82
|
+
create_flash 'info', ['You must be logged in to access this page.']
|
83
|
+
redirect '/login'
|
84
|
+
end
|
85
|
+
true
|
86
|
+
end
|
87
|
+
|
88
|
+
def report_transaction(message, type, device = nil)
|
89
|
+
transaction = Transaction.new
|
90
|
+
transaction.user_id = logged_in_user[:id] if session[:user]
|
91
|
+
|
92
|
+
transaction.description = message
|
93
|
+
transaction.transaction_type = type
|
94
|
+
transaction.device_id = device.id unless device.nil?
|
95
|
+
|
96
|
+
transaction.save!
|
97
|
+
end
|
98
|
+
|
99
|
+
def create_flash(type, message)
|
100
|
+
flash[:message] = { css_class: type, message: message }
|
101
|
+
end
|
102
|
+
|
103
|
+
def flash_and_go_back(type, message)
|
104
|
+
create_flash(type, message)
|
105
|
+
redirect back
|
106
|
+
end
|
107
|
+
|
108
|
+
def valid_heartbeat?(data)
|
109
|
+
return false if data['heartbeat'].nil?
|
110
|
+
heartbeat = data['heartbeat']
|
111
|
+
return false if heartbeat['device_id'].nil? ||
|
112
|
+
heartbeat['longitude'].nil? || heartbeat['latitude'].nil?
|
113
|
+
true
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# :nodoc:
|
2
|
+
module DeviceTracker
|
3
|
+
# :nodoc:
|
4
|
+
module UserHelper
|
5
|
+
def logged_in_user
|
6
|
+
session[:user] unless session[:user].nil?
|
7
|
+
end
|
8
|
+
|
9
|
+
def allowed_to_edit_page?(current_user, user_id)
|
10
|
+
# Admins can view any page
|
11
|
+
if current_user[:is_admin] || current_user[:id] == user_id.to_i
|
12
|
+
return true
|
13
|
+
end
|
14
|
+
false
|
15
|
+
end
|
16
|
+
|
17
|
+
def user_params
|
18
|
+
%w(password password_confirmation).each do |key|
|
19
|
+
params[:user].delete key
|
20
|
+
end
|
21
|
+
params[:user]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module DeviceTracker
|
4
|
+
# :nodoc:
|
5
|
+
class Device < ActiveRecord::Base
|
6
|
+
belongs_to :operating_system
|
7
|
+
belongs_to :user
|
8
|
+
|
9
|
+
has_many :transactions
|
10
|
+
has_many :heartbeats
|
11
|
+
|
12
|
+
validates :unid,
|
13
|
+
:manufacturer,
|
14
|
+
:device,
|
15
|
+
:description,
|
16
|
+
:operating_system,
|
17
|
+
presence: true
|
18
|
+
|
19
|
+
validates :unid, uniqueness: true
|
20
|
+
|
21
|
+
def unid=(unid)
|
22
|
+
self[:unid] = unid.upcase
|
23
|
+
end
|
24
|
+
|
25
|
+
def full_name
|
26
|
+
self[:manufacturer] + ' ' + self[:device]
|
27
|
+
end
|
28
|
+
|
29
|
+
def passed_use_by_date?
|
30
|
+
(!self[:available] &&
|
31
|
+
(Time.now - self[:checked_out_since]).to_i / 1.day >= 3)
|
32
|
+
end
|
33
|
+
|
34
|
+
def checked_out_since_formatted
|
35
|
+
self[:checked_out_since].strftime('%A, %d %B %Y at %I:%M%p')
|
36
|
+
end
|
37
|
+
|
38
|
+
def days_checked_out
|
39
|
+
(Time.now.to_date - self[:checked_out_since].to_date).floor
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module DeviceTracker
|
4
|
+
# :nodoc:
|
5
|
+
class Transaction < ActiveRecord::Base
|
6
|
+
belongs_to :user
|
7
|
+
belongs_to :device
|
8
|
+
|
9
|
+
validates :transaction_type, :description, presence: true
|
10
|
+
validates :transaction_type,
|
11
|
+
inclusion: { in: %w(CHECKOUT
|
12
|
+
RETURN
|
13
|
+
REGISTRATION
|
14
|
+
LOGIN
|
15
|
+
ACTIVATE
|
16
|
+
DEACTIVATE
|
17
|
+
DELETION
|
18
|
+
MISSING
|
19
|
+
FOUND),
|
20
|
+
message: '%{value} is not a valid transaction.' }
|
21
|
+
|
22
|
+
def created_at
|
23
|
+
self[:created_at].strftime('%a %d %b %Y at %H:%M:%S')
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.missing
|
27
|
+
'MISSING'
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.found
|
31
|
+
'FOUND'
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.activate
|
35
|
+
'ACTIVATE'
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.deactivate
|
39
|
+
'DEACTIVATE'
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.checkout
|
43
|
+
'CHECKOUT'
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.return
|
47
|
+
'RETURN'
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.registration
|
51
|
+
'REGISTRATION'
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.login
|
55
|
+
'LOGIN'
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.deletion
|
59
|
+
'DELETION'
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'bcrypt'
|
2
|
+
|
3
|
+
module DeviceTracker
|
4
|
+
# :nodoc:
|
5
|
+
class User < ActiveRecord::Base
|
6
|
+
has_secure_password
|
7
|
+
has_many :devices
|
8
|
+
has_many :transactions
|
9
|
+
validates :username, :name, :email, presence: true
|
10
|
+
validates :password, presence: { if: :password_required? }
|
11
|
+
validates_format_of :email,
|
12
|
+
with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i,
|
13
|
+
on: :create
|
14
|
+
validates :username, uniqueness: true
|
15
|
+
validates :email, uniqueness: true
|
16
|
+
validates_confirmation_of :password
|
17
|
+
|
18
|
+
def flashable_attrs
|
19
|
+
[:username, :email, :name]
|
20
|
+
end
|
21
|
+
|
22
|
+
protected
|
23
|
+
|
24
|
+
def password_required?
|
25
|
+
self.new_record?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
table.sortable span.sign {
|
2
|
+
display: block;
|
3
|
+
position: absolute;
|
4
|
+
top: 50%;
|
5
|
+
right: 5px;
|
6
|
+
font-size: 12px;
|
7
|
+
margin-top: -10px;
|
8
|
+
color: #bfbfc1;
|
9
|
+
}
|
10
|
+
|
11
|
+
table.sortable th:after {
|
12
|
+
display: block;
|
13
|
+
position: absolute;
|
14
|
+
top: 50%;
|
15
|
+
right: 5px;
|
16
|
+
font-size: 12px;
|
17
|
+
margin-top: -10px;
|
18
|
+
color: #bfbfc1;
|
19
|
+
}
|
20
|
+
|
21
|
+
table.sortable th.arrow:after {
|
22
|
+
content: '';
|
23
|
+
}
|
24
|
+
|
25
|
+
table.sortable span.arrow, span.reversed, th.arrow.down:after, th.reversedarrow.down:after, th.arrow.up:after, th.reversedarrow.up:after {
|
26
|
+
border-style: solid;
|
27
|
+
border-width: 5px;
|
28
|
+
font-size: 0;
|
29
|
+
border-color: #ccc transparent transparent transparent;
|
30
|
+
line-height: 0;
|
31
|
+
height: 0;
|
32
|
+
width: 0;
|
33
|
+
margin-top: -2px;
|
34
|
+
}
|
35
|
+
|
36
|
+
table.sortable span.arrow.up, th.arrow.up:after {
|
37
|
+
border-color: transparent transparent #ccc transparent;
|
38
|
+
margin-top: -7px;
|
39
|
+
}
|
40
|
+
|
41
|
+
table.sortable span.reversed, th.reversedarrow.down:after {
|
42
|
+
border-color: transparent transparent #ccc transparent;
|
43
|
+
margin-top: -7px;
|
44
|
+
}
|
45
|
+
|
46
|
+
table.sortable span.reversed.up, th.reversedarrow.up:after {
|
47
|
+
border-color: #ccc transparent transparent transparent;
|
48
|
+
margin-top: -2px;
|
49
|
+
}
|
50
|
+
|
51
|
+
table.sortable span.az:before, th.az.down:after {
|
52
|
+
content: "a .. z";
|
53
|
+
}
|
54
|
+
|
55
|
+
table.sortable span.az.up:before, th.az.up:after {
|
56
|
+
content: "z .. a";
|
57
|
+
}
|
58
|
+
|
59
|
+
table.sortable th.az.nosort:after, th.AZ.nosort:after, th._19.nosort:after, th.month.nosort:after {
|
60
|
+
content: "..";
|
61
|
+
}
|
62
|
+
|
63
|
+
table.sortable span.AZ:before, th.AZ.down:after {
|
64
|
+
content: "A .. Z";
|
65
|
+
}
|
66
|
+
|
67
|
+
table.sortable span.AZ.up:before, th.AZ.up:after {
|
68
|
+
content: "Z .. A";
|
69
|
+
}
|
70
|
+
|
71
|
+
table.sortable span._19:before, th._19.down:after {
|
72
|
+
content: "1 .. 9";
|
73
|
+
}
|
74
|
+
|
75
|
+
table.sortable span._19.up:before, th._19.up:after {
|
76
|
+
content: "9 .. 1";
|
77
|
+
}
|
78
|
+
|
79
|
+
table.sortable span.month:before, th.month.down:after {
|
80
|
+
content: "jan .. dec";
|
81
|
+
}
|
82
|
+
|
83
|
+
table.sortable span.month.up:before, th.month.up:after {
|
84
|
+
content: "dec .. jan";
|
85
|
+
}
|
86
|
+
|
87
|
+
table.sortable thead th:not([data-defaultsort=disabled]) {
|
88
|
+
cursor: pointer;
|
89
|
+
position: relative;
|
90
|
+
top: 0;
|
91
|
+
left: 0;
|
92
|
+
}
|
93
|
+
|
94
|
+
table.sortable thead th:hover:not([data-defaultsort=disabled]) {
|
95
|
+
background: #efefef;
|
96
|
+
}
|
97
|
+
|
98
|
+
table.sortable thead th div.mozilla {
|
99
|
+
position: relative;
|
100
|
+
}
|