diff --git a/.gitignore b/.gitignore index a609b413..a8f5d2a2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,19 +7,25 @@ *.orig *.tmp *.md5 +*.swp +.fseventsd +db *.*.py .\#* \#*.*\# .hg .hgignore +.rvmrc lighttpd.** development.ini development_*.ini -production.ini +lesswrong.com*.ini r2/r2/public/static/frame.js r2/r2/public/static/reddit.js r2/r2/public/static/vote.js r2/r2/public/static/reddit_rtl.css +r2/r2/public/static/lesswrong.js +public/files/wiki.lesswrong.xml* r2/r2admin r2/reddit_i18n r2/data/* @@ -27,7 +33,18 @@ r2/count.pickle r2/srcount.pickle r2/myproduction.ini .DS_Store +.fseventsd/* r2/r2.egg-info/** r2/lighttpd_access.log r2/lighttpd_error.log -r2/paster.log \ No newline at end of file +r2/paster.log +r2/paster.pid +memcached.*.pid +paster.*.pid +config/deploy/aws-test.rb +exporter/* +notes/* +attic/* +.dir-locals.el +.Trashes/* +spec/selenium-override.rb diff --git a/.gitmodules b/.gitmodules index 85afd7ff..13412252 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "config/cap-tasks"] path = config/cap-tasks url = git@git.trike.com.au:capistrano.git +[submodule "tasks/manual_test_script"] + path = tasks/manual_test_script + url = git://github.com/tricycle/manual_test_script.git diff --git a/.rvmrc.sample b/.rvmrc.sample new file mode 100644 index 00000000..707ae177 --- /dev/null +++ b/.rvmrc.sample @@ -0,0 +1 @@ +rvm --create ree@lesswrong diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..0feb58a9 --- /dev/null +++ b/Gemfile @@ -0,0 +1,13 @@ +source :rubygems + +gem 'capistrano', '~>2.5.19' +gem 'capistrano-ext', '~>1.2.1' +gem 'right_aws', '~>2.0.0' +gem 'erubis', '~>2.6.6' +gem 'activesupport', '~>3.0.0' +gem 'rake', '~>0.8.7' +gem 'treetop', '~>1.4.8' +gem 'capybara' +gem 'selenium-webdriver' +gem 'rspec' + diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..1aac1ca4 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,82 @@ +GEM + remote: http://rubygems.org/ + specs: + abstract (1.0.0) + activesupport (3.0.7) + capistrano (2.5.21) + highline + net-scp (>= 1.0.0) + net-sftp (>= 2.0.0) + net-ssh (>= 2.0.14) + net-ssh-gateway (>= 1.0.0) + capistrano-ext (1.2.1) + capistrano (>= 1.0.0) + capybara (0.4.1.2) + celerity (>= 0.7.9) + culerity (>= 0.2.4) + mime-types (>= 1.16) + nokogiri (>= 1.3.3) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + selenium-webdriver (>= 0.0.27) + xpath (~> 0.1.3) + celerity (0.8.9) + childprocess (0.1.9) + ffi (~> 1.0.6) + culerity (0.2.15) + diff-lcs (1.1.2) + erubis (2.6.6) + abstract (>= 1.0.0) + ffi (1.0.9) + highline (1.6.2) + json_pure (1.5.1) + mime-types (1.16) + net-scp (1.0.4) + net-ssh (>= 1.99.1) + net-sftp (2.0.5) + net-ssh (>= 2.0.9) + net-ssh (2.1.4) + net-ssh-gateway (1.1.0) + net-ssh (>= 1.99.1) + nokogiri (1.4.4) + polyglot (0.3.1) + rack (1.3.0) + rack-test (0.6.0) + rack (>= 1.0) + rake (0.8.7) + right_aws (2.0.0) + right_http_connection (>= 1.2.1) + right_http_connection (1.3.0) + rspec (2.6.0) + rspec-core (~> 2.6.0) + rspec-expectations (~> 2.6.0) + rspec-mocks (~> 2.6.0) + rspec-core (2.6.3) + rspec-expectations (2.6.0) + diff-lcs (~> 1.1.2) + rspec-mocks (2.6.0) + rubyzip (0.9.4) + selenium-webdriver (0.2.0) + childprocess (>= 0.1.7) + ffi (>= 1.0.7) + json_pure + rubyzip + treetop (1.4.9) + polyglot (>= 0.3.1) + xpath (0.1.4) + nokogiri (~> 1.3) + +PLATFORMS + ruby + +DEPENDENCIES + activesupport (~> 3.0.0) + capistrano (~> 2.5.19) + capistrano-ext (~> 1.2.1) + capybara + erubis (~> 2.6.6) + rake (~> 0.8.7) + right_aws (~> 2.0.0) + rspec + selenium-webdriver + treetop (~> 1.4.8) diff --git a/README.textile b/README.textile index 67136b8e..661fd6cf 100644 --- a/README.textile +++ b/README.textile @@ -1,5 +1,11 @@ -The code behind the rationalists community at "lesswrong.com":http://lesswrong.com/.
-Source hosted on "Github":http://github.com/tricycle/lesswrong.
-Issues tracked on "Google Code":http://code.google.com/p/lesswrong/. +h1. Less Wrong -A fork of the "Reddit codebase":http://code.reddit.com/. +The code behind the rationalists community at "lesswrong.com":http://lesswrong.com/. + +h2. Resources + +* The source code is hosted on "Github":http://github.com/tricycle/lesswrong +* Documentation is available via the "GitHub wiki":http://wiki.github.com/tricycle/lesswrong +* Issues are tracked on "Google Code":http://code.google.com/p/lesswrong/ +* Mailing list (lesswrong-dev) on "Google Groups":http://groups.google.com/group/lesswrong-dev +* The code is a fork of the "Reddit":http://reddit.com/ "codebase":http://code.reddit.com/ diff --git a/Rakefile b/Rakefile new file mode 100644 index 00000000..7f056853 --- /dev/null +++ b/Rakefile @@ -0,0 +1,362 @@ +require 'fileutils' +require 'tempfile' +require 'pathname' +require 'shellwords' + +begin + require 'rspec/core/rake_task' +rescue LoadError +end + +namespace :test do + desc "Interactively run through the deployment test script." + task :manual do + # These are in here so they aren't required on the production server + $:.unshift 'tasks/manual_test_script/lib' + require 'active_support' + require 'manual_test_script' + ManualTestScript.run('test/manual.txt') + end +end + +# Borrowed from capistrano +def sudo(command, options = {}) + user = options[:as] && "-u #{options.delete(:as)}" + + sudo_prompt_option = "-p 'sudo password: '" + sudo_command = ["sudo", sudo_prompt_option, user, command].compact.join(" ") + run sudo_command +end + +def run(command) + puts "running `#{command}'" + unless system(command) + raise RuntimeError.new("Error running command: '#{command}'") + end +end + +def basepath + Pathname(__FILE__).dirname.realpath +end + +def shared_path + basepath.parent.parent + 'shared' +end + +def r2_path + basepath + "r2" +end + +def db_dump_path + path = basepath + "db" + "dumps" + path.mkpath unless path.exist? + path +end + +def inifile + ENV['INI'] || (r2_path + "#{environment}.ini") +end + +def user + ENV['APPLICATION_USER'] || raise("APPLICATION_USER environment variable must be set to run this task") +end + +def application + ENV['APPLICATION'] || raise("APPLICATION environment variable variable must be set to run this task") +end + +def environment + ENV['APPLICATION_ENV'] || raise("APPLICATION_ENV environment variable must be set to run this task") +end + +def databases + @databases ||= begin + dbs = ENV['DATABASES'] || raise("DATABASES environment variable must be set to run this task") + dbs.split(/\s*,\s*/) + end +end + +def db_dump_prefix + ENV['DB_DUMP_PREFIX'] || '' +end + +def app_server(action) + return unless [:start, :stop, :restart].include?(action) + sudo "#{action} paster" +end + +# These tasks assume they are running in a capistrano managed directory structure. +namespace :app do + desc "Start the Application" + task :start do + app_server(:start) + end + + desc "Stop the Application" + task :stop do + app_server(:stop) + end + + desc "Restart the Application" + task :restart do + app_server(:restart) + end +end + +namespace :deploy do + desc 'Run Reddit setup routine' + task :setup do + Dir.chdir r2_path + sudo "python setup.py install" + Dir.chdir basepath + sudo "chown -R #{user} #{r2_path}" + end + + desc "Symlink the INI files into the release path" + task :symlink_ini do + Dir["/usr/local/etc/reddit/#{application}.*.ini"].each do |ini| + if File.basename(ini) =~ /#{Regexp.escape(application)}\.([^\.]+)\.ini/ + target = "#{r2_path}/#{$1}.ini" + FileUtils.ln_sf(ini, target, :verbose => true) + end + end + end + + desc 'Compress and concetenate JS and generate MD5 files' + task :process_static_files do + Dir.chdir r2_path + run "./compress_js.sh" + end + + # For compatibilty + desc "Restart the Application" + task :restart do + Rake::Task['app:stop'].invoke + Rake::Task['app:start'].invoke + end + + desc "Copy the lesswrong crontab to /etc/cron.d in production. Requires root permissions" + task :crontab do + crontab = basepath + 'config' + 'crontab' + target = "/etc/cron.d/lesswrong" + if environment == "production" + sudo "/bin/cp #{crontab} #{target}" + else + # Don't want the cron jobs running in non-production environments + sudo "/bin/rm -f #{target}" + end + end + +end + +desc "Hook for tasks that should run after code update" +task :after_update_code => %w[ + deploy:symlink_ini + deploy:setup + deploy:process_static_files + deploy:crontab +] + +def conf + @conf ||= begin + conf = {} + File.open(inifile.to_s) do |ini| + ini.each_line do |line| + next if line =~ /^\s*#/ # skip comments + next if line =~ /^\s*\[[^\]]+\]/ # skip sections + + if line =~ /\s*([^\s=]+)\s*=\s*(.*)$/ + conf[$1] = $2 + end + end + end + conf + end +end + + +# Set the databases variable in your local deploy configuration +# expects an array of PostgreSQL database names +# Example: +# set :databases, %w[reddit change query_queue] + +namespace :postgresql do + + def db_conf(db, var) + key = [db, 'db', var].join('_') + conf[key] + end + + # Common options + def postgresql_opts(database) + opts = [] + opts << "--host=#{db_conf database, 'host'}" + opts << "--username=#{db_conf database, 'user'}" + opts << "--no-password" # Never prompt for password, its read from the file below + opts.join(" ") + end + + def with_pgpass(db) + # Setup the pgpass file + pgpass = Tempfile.new("pgpass") + ENV['PGPASSFILE'] = pgpass.path + pgpass.puts [ + db_conf(db, 'host'), + '*', # port + db_conf(db, 'name'), + db_conf(db, 'user'), + db_conf(db, 'pass') + ].join(':') + pgpass.close + begin + yield + ensure + pgpass.unlink + end + end + + def dump_file_path(db) + (db_dump_path + "#{db_dump_prefix}#{db}.psql").to_s.shellescape + end + + desc 'Dump the database' + task :dump do + databases.each do |db| + with_pgpass(db) do + run "pg_dump #{postgresql_opts(db)} -f #{dump_file_path(db)} -Fc #{db_conf(db, 'name')}" + end + end + end + + desc 'Restore the latest database dump' + task :restore do + databases.each do |db| + with_pgpass(db) do + run "pg_restore #{postgresql_opts(db)} --no-owner --clean -d #{db_conf(db, 'name')} #{dump_file_path(db)} || true" + end + end + end +end + +namespace :memcached do + def memcached_pid_path + basepath + "memcached.#{environment}.pid" + end + + task :start do + port = conf['memcaches'].split(':').last + pid = fork do + exec('memcached','-p',port) + end + File.open(memcached_pid_path, "w") do |f| + f.puts pid + end + end + task :stop do + pid = File.read(memcached_pid_path).to_i + Process.kill("TERM", pid) + File.unlink(memcached_pid_path) + end +end + +namespace :db do + namespace :test do + desc "Clean the test db, and re-populate it" + task :prepare do + ENV['APPLICATION_ENV'] = 'test' + ENV['DATABASES'] = 'main' + ENV['DB_DUMP_PREFIX'] = 'test-' + Rake::Task['db:test:truncate'].invoke + Rake::Task['postgresql:restore'].invoke + end + + task :truncate do + databases.each do |db| + with_pgpass(db) do + tables = `psql -t -A #{postgresql_opts(db)} -d #{db_conf(db, 'name')} -c "select tablename from pg_tables where schemaname='public'"` + tables.lines.each do |table| + run "psql #{postgresql_opts(db)} -d #{db_conf(db, 'name')} -c 'TRUNCATE TABLE #{table.chomp}'" + end + + sequences = `psql -t -A #{postgresql_opts(db)} -d #{db_conf(db, 'name')} -c "select sequence_name from information_schema.sequences where sequence_schema='public'"` + sequences.lines.each do |seq| + run %{psql #{postgresql_opts(db)} -d #{db_conf(db, 'name')} -c "SELECT setval('#{seq.chomp}', 1, false)"} + end + + end + end + end + end +end + +namespace :test do + def paster_pid_path + basepath + "paster.#{environment}.pid" + end + + def paster_log_file_path + basepath + "paster.#{environment}.log" + end + + namespace :paster do + task :start do + ENV['APPLICATION_ENV'] = 'test' + FileUtils.cd r2_path do |d| + system( + 'paster','serve',inifile.to_s, + '--pid-file',paster_pid_path.to_s, + '--log-file',paster_log_file_path.to_s, + '--daemon','--reload' + ) + end + end + task :stop do + ENV['APPLICATION_ENV'] = 'test' + FileUtils.cd r2_path do |d| + system('paster','serve',inifile.to_s,'--pid-file',paster_pid_path.to_s,'--stop-daemon') + end + end + end + + desc "Start the server in test mode for specs" + task :start do + ENV['APPLICATION_ENV'] = 'test' + Rake::Task['db:test:prepare'].invoke + Rake::Task['memcached:start'].invoke + Rake::Task['test:paster:start'].invoke + end + desc "Stop the test server" + task :stop do + ENV['APPLICATION_ENV'] = 'test' + Rake::Task['test:paster:stop'].invoke + Rake::Task['memcached:stop'].invoke + end + + desc "Start+stop test server, and run selenium spec tests" + task :run do + begin + Rake::Task['test:start'].invoke + Rake::Task['spec:setup'].invoke + Rake::Task['spec:test'].invoke + ensure + Rake::Task['test:stop'].invoke + end + end +end + +if defined?(RSpec) + namespace :spec do + desc "Run the setup selenium spec" + RSpec::Core::RakeTask.new(:setup) do |t| + t.rspec_opts = ['--options', "\"#{basepath}/spec/spec.opts\""] + t.pattern = 'spec/selenium-setup.rb' + end + + desc "Run the selenium spec" + RSpec::Core::RakeTask.new(:test) do |t| + t.rspec_opts = ['--options', "\"#{basepath}/spec/spec.opts\""] + t.pattern = 'spec/**/*_spec.rb' + end + end +end + diff --git a/config/cap-tasks b/config/cap-tasks index 899a7f33..49a7311f 160000 --- a/config/cap-tasks +++ b/config/cap-tasks @@ -1 +1 @@ -Subproject commit 899a7f33d6950d65e21675dc6e8f4d22c7bb27d1 +Subproject commit 49a7311f88244b7407937d765c5f9d09412c5144 diff --git a/config/crontab b/config/crontab new file mode 100644 index 00000000..945b474f --- /dev/null +++ b/config/crontab @@ -0,0 +1,5 @@ +# IF YOU STUMBLE UPON THIS FILE YOU MAY NOT HAVE NOTICED THAT IT IS SYMLINKED +# INTO THE APP REPO. MAKE SURE THAT IT GETS COMMITED IF YOU MAKE ANY CHANGES. + +23 * * * * www-data /srv/www/lesswrong.com/current/scripts/sync_wiki_export.sh + diff --git a/config/db.rb b/config/db.rb new file mode 100644 index 00000000..b35db4f3 --- /dev/null +++ b/config/db.rb @@ -0,0 +1,19 @@ +set :db_dump_filename do + "#{stage}.psql.gz" +end + +namespace :db do + + desc 'Fetches the latest PostgeSQL dump from the backup server' + task :fetch_dump, :roles => :backups do + host = roles[:app].servers.first.host + source = fetch(:remote_db_dump_location, File.join(['', 'srv', 'backup', 'sql', host.sub(/\..*$/, ''), 'all_databases.psql.gz'])) + destination = fetch(:db_dump_location, File.join('db', 'dumps', host)) + unless File.directory?(destination) || File.symlink?(destination) + FileUtils.mkdir_p destination + File.chmod 0775, destination # Ensure group has write access + end + get source, File.join(destination, db_dump_filename) + end + +end diff --git a/config/deploy.rb b/config/deploy.rb index fbae92bf..288afcde 100644 --- a/config/deploy.rb +++ b/config/deploy.rb @@ -1,57 +1,68 @@ -stages_glob = File.join(File.dirname(__FILE__), "deploy", "*") -stages = Dir[stages_glob].collect {|f| File.basename(f) }.sort +stages_glob = File.join(File.dirname(__FILE__), "deploy", "*.rb") +stages = Dir[stages_glob].collect { |f| File.basename(f, ".rb") }.sort set :stages, stages require 'capistrano/ext/multistage' -load 'config/cap-tasks/trike-tasks.rb' +load 'config/cap-tasks/common.rb' +load 'config/cap-tasks/test.rb' +load 'config/cap-tasks/console.rb' +load 'config/cap-tasks/console.rb' +load 'config/cap-tasks/rake.rb' +load 'config/cap-tasks/postgresql_dump.rb' +load 'config/db.rb' set :scm, 'git' set :repository, "git@github.com:tricycle/lesswrong.git" set :git_enable_submodules, 1 set :deploy_via, :remote_cache set :repository_cache, 'cached-copy' +set :engine, "paster" # Be sure to change these in your application-specific files -set :branch, 'master' - +set :branch, 'stable' +set :rails_env, nil set :user, "www-data" # defaults to the currently logged in user -default_run_options[:pty] = true +set :public_path, lambda { "#{current_path}/r2/r2/public" } +set :databases, %w[main change email query_queue] namespace :deploy do - task :after_update_code, :roles => [:web, :app] do + after "deploy:update_code", :roles => [:web, :app] do %w[files assets].each {|dir| link_shared_dir(dir) } end + def rake_options + { + 'APPLICATION' => application, + 'APPLICATION_USER' => user, + 'APPLICATION_ENV' => environment + }.map { |k, v| "#{k}=#{v}" }.join(" ") + end + def link_shared_dir(dir) - shared_subdir = "#{deploy_to}/shared/#{dir}" + shared_subdir = "#{shared_path}/#{dir}" public_dir = "#{release_path}/public/#{dir}" run "mkdir -p #{shared_subdir}" # make sure the shared dir exists run "if [ -e #{public_dir} ]; then rm -rf #{public_dir} && echo '***\n*** #{public_dir} removed (in favour of a symlink to the shared version) ***\n***'; fi" run "ln -sv #{shared_subdir} #{public_dir}" end - - desc 'Link to a reddit ini file stored on the server (/usr/local/etc/reddit/#{application}.ini' - task :symlink_remote_reddit_ini, :roles => [:app, :db] do - run "ln -sf /usr/local/etc/reddit/#{application}.ini #{release_path}/r2/#{application}.ini" - end - desc 'Run Reddit setup routine' - task :setup_reddit, :roles => [:app] do - sudo "/bin/bash -c \"cd #{release_path}/r2 && python ./setup.py install\"" - sudo "/bin/bash -c \"cd #{release_path} && chown -R #{user} .\"" + desc 'Symlink all the INI files into the release dir' + task :symlink_remote_reddit_ini, :roles => :app do + # Not using remote rake because need to cd to release path not current + run "cd #{release_path} && rake --trace deploy:symlink_ini #{rake_options}" end desc "Restart the Application" task :restart, :roles => :app do - pid_file = "#{shared_dir}/pids/paster.pid" - run "cd #{deploy_to}/current/r2 && paster serve --stop-daemon --pid-file #{pid_file} #{application}.ini || true" - run "cd #{deploy_to}/current/r2 && paster serve --daemon --pid-file #{pid_file} #{application}.ini" + remote_rake "--trace deploy:restart #{rake_options}" + end + + desc "Run after update code rake task" + task :rake_after_update_code, :roles => :app do + remote_rake "--trace after_update_code #{rake_options}", :path => release_path end end -#before 'deploy:update_code', 'git:ensure_pushed' -#before 'deploy:update_code', 'git:ensure_deploy_branch' -#after "deploy:update_code", "deploy:symlink_remote_db_yaml" -#after "deploy:symlink", "deploy:apache:config" -after "deploy:update_code", "deploy:setup_reddit" -after "deploy:update_code", "deploy:symlink_remote_reddit_ini" +before 'deploy:update_code', 'git:ensure_pushed' +after "deploy:update_code", "deploy:rake_after_update_code" + diff --git a/config/deploy/beta b/config/deploy/beta deleted file mode 100644 index 6770aacc..00000000 --- a/config/deploy/beta +++ /dev/null @@ -1,11 +0,0 @@ -set :application, "moreor.lesswrong.org" -set :domains, %w[ lesswrong.org ] -set :deploy_to, "/srv/www/#{application}" -set :shared_dir, "#{deploy_to}/shared" -set :engine, "paster" -set :rails_env, "NA" - -role :app, "serpent.trike.com.au" -role :web, "serpent.trike.com.au" -role :db, "serpent.trike.com.au", :primary => true - diff --git a/config/deploy/staging b/config/deploy/dev.rb similarity index 50% rename from config/deploy/staging rename to config/deploy/dev.rb index bd543e34..c3334db7 100644 --- a/config/deploy/staging +++ b/config/deploy/dev.rb @@ -1,11 +1,10 @@ set :application, "lesswrong.org" set :domains, %w[ lesswrong.org ] set :deploy_to, "/srv/www/#{application}" -set :shared_dir, "#{deploy_to}/shared" -set :engine, "paster" -set :rails_env, "NA" +set :branch, 'staging-disable-cron' +set :environment, 'dev' -role :app, "polly.trike.com.au" -role :web, "polly.trike.com.au" +role :app, "polly.trike.com.au", :primary => true +role :web, "polly.trike.com.au", :primary => true role :db, "polly.trike.com.au", :primary => true diff --git a/config/deploy/prod b/config/deploy/prod deleted file mode 100644 index e0983bea..00000000 --- a/config/deploy/prod +++ /dev/null @@ -1,11 +0,0 @@ -set :application, "lesswrong.org" -set :domains, %w[ lesswrong.org ] -set :deploy_to, "/srv/www/#{application}" -set :shared_dir, "#{deploy_to}/shared" -set :engine, "paster" -set :rails_env, "NA" - -role :app, "serpent.trike.com.au" -role :web, "serpent.trike.com.au" -role :db, "serpent.trike.com.au", :primary => true - diff --git a/config/deploy/production.rb b/config/deploy/production.rb new file mode 100644 index 00000000..2df31b81 --- /dev/null +++ b/config/deploy/production.rb @@ -0,0 +1,36 @@ +load 'config/cap-tasks/trike-aws.rb' + +set :application, "lesswrong.com" +set :domains, %w[ lesswrong.com ] +set :elb_name, 'python' +set :hosts, lambda { AWS.elb_hosts(elb_name) } +set :environment, 'production' + +set :primary_host, hosts.shift + +role :app, primary_host, :primary => true +role :app, *hosts +role :web, primary_host, :primary => true +role :web, *hosts +role :db, "db.aws.trike.com.au", :primary => true, :no_release => true +role :backups, "backup.trike.com.au", :user => 'backup', :no_release => true + +before "deploy:update_code", "tests_check:manual_tests_executed?" + +after 'multistage:ensure', :check_hostname +after 'deploy:cleanup', :check_hostname + +task :check_hostname, :roles => :app, :only => :primary do + balancers = AWS.elb.describe_load_balancers(elb_name) + unless balancers.empty? + domain = domains.first + balancer = balancers.first + _, _, _, domain_ip = TCPSocket.gethostbyname(domain) + _, _, _, balancer_ip = TCPSocket.gethostbyname(balancer[:dns_name]) + if domain_ip != balancer_ip + warn "\033[31mWARNING:\033[0m #{domain} is not resolving to the IP for the #{elb_name} elb.\n" + + "Do you have an entry in /etc/hosts that needs removing?" + end + end +end + diff --git a/config/deploy/staging.rb b/config/deploy/staging.rb new file mode 100644 index 00000000..f9726fd2 --- /dev/null +++ b/config/deploy/staging.rb @@ -0,0 +1,40 @@ +require 'socket' +load 'config/cap-tasks/trike-aws.rb' + +set :application, "lesswrong.com" +set :domains, %w[ lesswrong.com ] +set :deploy_to, "/srv/www/#{application}" +set :branch, 'staging' +set :environment, 'staging' +set :security_group, 'webserver_python_staging' + +set :instance, lambda { + AWS.find_or_start_host_for_security_group( + security_group, + AWS.auto_scaler_ami('python'), + 120, + File.join('config', "user_data_#{environment}.sh.erb"), + :instance_type => 'c1.medium' + ) +} + +role :app, instance, :primary => true +role :web, instance, :primary => true +role :db, "db.aws.trike.com.au", :primary => true, :no_release => true + +after 'multistage:ensure', :check_hostname +after 'deploy:cleanup', :check_hostname + +task :check_hostname, :roles => :app, :only => :primary do + hosts = AWS.security_group_hosts(security_group) + unless hosts.empty? + _, _, _, domain_ip = TCPSocket.gethostbyname(domains.first) + _, _, _, host_ip = TCPSocket.gethostbyname(hosts.first) + if domain_ip != host_ip + warn "\033[31mWARNING:\033[0m #{domains.first} is not resolving to #{host_ip}. " + + "You should add an entry to /etc/hosts:\n" + + "#{host_ip} #{domains.first}" + end + end +end + diff --git a/config/user_data_staging.sh.erb b/config/user_data_staging.sh.erb new file mode 100644 index 00000000..a218deb1 --- /dev/null +++ b/config/user_data_staging.sh.erb @@ -0,0 +1,25 @@ +#!/bin/bash +/usr/local/sys_scripts/bin/git-clone-update git@git.trike.com.au:sys_scripts.git /usr/local/sys_scripts --branch master +/usr/local/sys_scripts/bin/git-clone-update git@git.trike.com.au:munin.git /etc/munin --branch master + +/usr/local/sys_scripts/bin/aws-update_hosts 75.101.144.164 \ + db.aws.trike.com.au \ + sultana.trike.com.au \ + memcache.aws.trike.com.au + +export APPLICATION=lesswrong.com +export APPLICATION_USER=www-data +export APPLICATION_ENV=staging +APP_REPO='git://github.com/tricycle/lesswrong.git' +APP_DIR=/srv/www/lesswrong.com + +sudo -u $APPLICATION_USER /usr/local/sys_scripts/bin/git-clone-update $APP_REPO $APP_DIR/current --branch staging +cd $APP_DIR/current && sudo -u $APPLICATION_USER env APPLICATION=$APPLICATION APPLICATION_USER=$APPLICATION_USER APPLICATION_ENV=$APPLICATION_ENV rake after_update_code > $APP_DIR/shared/log/after_update_code.log 2>&1 +cd $APP_DIR/current && sudo -u $APPLICATION_USER env APPLICATION=$APPLICATION APPLICATION_USER=$APPLICATION_USER APPLICATION_ENV=$APPLICATION_ENV rake deploy:restart > $APP_DIR/shared/log/deploy-restart.log 2>&1 +sudo -u $APPLICATION_USER $APP_DIR/current/scripts/sync_wiki_export.sh + +<% if time_to_live > 0 %> +# This server will self destruct in <%= time_to_live %> minutes +# run shutdown -c to cancel +shutdown -h +<%= time_to_live %> & +<% end %> diff --git a/db/dumps/test-main.psql b/db/dumps/test-main.psql new file mode 100644 index 00000000..67c2ae36 Binary files /dev/null and b/db/dumps/test-main.psql differ diff --git a/r2/compress_js.sh b/r2/compress_js.sh index 1a7a5282..7929103e 100755 --- a/r2/compress_js.sh +++ b/r2/compress_js.sh @@ -8,64 +8,43 @@ # software over a computer network and provide for limited attribution for the # Original Developer. In addition, Exhibit A has been modified to be consistent # with Exhibit B. -# +# # Software distributed under the License is distributed on an "AS IS" basis, # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for # the specific language governing rights and limitations under the License. -# +# # The Original Code is Reddit. -# +# # The Original Developer is the Initial Developer. The Initial Developer of the # Original Code is CondeNet, Inc. -# +# # All portions of the code written by CondeNet are Copyright (c) 2006-2008 # CondeNet, Inc. All Rights Reserved. ################################################################################ -files=( psrs.js utils.js animate.js link.js comments.js subreddit.js vote_piece.js reddit_piece.js organic.js ) +files=( event.simulate.js utils.js animate.js link.js comments.js subreddit.js vote_piece.js reddit_piece.js organic.js main.js map.js ) wd=`pwd` -redditjs='reddit.js' +redditjs='lesswrong.js' framejs='frame.js' votejs='vote.js' -compressor=" $wd/r2/lib/contrib/jsjam -g -i" +function compressor { + yui-compressor --type js $@ +} -echo "generating rtl style sheet" - -./rtl.sh - - -echo "Generating reddit.js..." - +echo "Generating ${redditjs}..." cd r2/public/static [ -e $redditjs ] && rm $redditjs [ -e $redditjs-big ] && rm $redditjs-big -cat json.js > $redditjs.tmp -for f in "${files[@]}" -do - $compressor $f >> $redditjs.tmp +cat /dev/null > $redditjs.tmp +for f in "${files[@]}"; do + echo "Compressing ${f}" + compressor $f >> $redditjs done; -sed 's/\$/ \$/g' $redditjs.tmp > $redditjs - -echo "Generating vote.js..." -# compress the votes alone (for buttons) -cat psrs.js | $compressor | sed 's/\$/ \$/g' > $votejs -cat utils.js vote_piece.js | $compressor >> $votejs - -echo "Generating frame.js..." -# compress frame alone (for the toolbar) -cat psrs.js > $framejs -cat vote_piece.js utils.js frame_piece.js | $compressor >> $framejs - -echo "droppping md5s..." -for file in *.js -do - cat $file | openssl md5 > $file.md5 -done -for file in *.css -do +echo "Droppping md5s..." +for file in *.{js,css,gif,png,jpg}; do cat $file | openssl md5 > $file.md5 done diff --git a/r2/example.ini b/r2/example.ini index 36ecffbf..47d7abdf 100644 --- a/r2/example.ini +++ b/r2/example.ini @@ -8,6 +8,7 @@ debug = true template_debug = true uncompressedJS = true translator = true +sqlprinting = false proxy_addr = log_path = @@ -64,35 +65,45 @@ enable_doquery = False use_query_cache = False write_query_queue = False -stylesheet = reddit.css +stylesheet = lesswrong.css stylesheet_rtl = reddit_rtl.css allowed_css_linked_domains = my.domain.com, my.otherdomain.com +authorized_cnames = lesswrong.local css_killswitch = False max_sr_images = 20 login_cookie = reddit_session -domain = localhost +domain = lesswrong.local domain_prefix = -authorized_cnames = lesswrong.local default_sr = lesswrong blessed_sr = overcomingbias front_page_title = Less Wrong admins = sponsors = page_cache_time = 30 +wiki_page_cache_time = 86400 static_path = /static/ useragent = Mozilla/5.0 (compatible; bot/1.0; ChangeMe) +feedbox_urls = http://www.overcomingbias.com/feed +recent_edits_feed = http://wiki.lesswrong.com/mediawiki/index.php?title=Special:RecentChanges&feed=atom&hideminor=1 +# Your sitemeter.com username/codename +site_meter_codename = +geoip_db_path = /usr/share/GeoIP/GeoIP.dat +# The radius in km for a meetup to be considered nearby +meetups_radius = 100 + solr_url = +google_api_key = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_bbbbbbbbbbbbbbbbbbbbbbbbb_cccccccccccccccccc-d SECRET = abcdefghijklmnopqrstuvwxyz0123456789 MODSECRET = abcdefghijklmnopqrstuvwxyz0123456789 tracking_secret = abcdefghijklmnopqrstuvwxyz0123456789 ip_hash = S3KEY_ID = ABCDEFGHIJKLMNOP1234 S3SECRET_KEY = aBcDeFgHiJkLmNoPqRsTuVwXyZ1234567890AbCd -s3_thumb_bucket = /your.bucket.here/ +s3_thumb_bucket = /lesswrong.dev/ default_thumb = /static/noimage.png MIN_DOWN_LINK = 0 @@ -110,7 +121,7 @@ media_period = 10 minutes rising_period = 12 hours # time of ratelimit purgatory (min) -RATELIMIT = 0 +RATELIMIT = 1 num_comments = 200 max_comments = 500 @@ -125,6 +136,18 @@ agents = feedback_email = abuse@localhost +about_post_id = 1 +issues_post_id = 2 + +karma_to_post = 40 +post_karma_multiplier = 10 + +side_meetups_max_age = 60 +side_comments_max_age = 60 +side_posts_max_age = 300 +side_tags_max_age = 43200 +side_contributors_max_age = 3600 +article_navigation_max_age = 0 [server:main] use = egg:Paste#http diff --git a/r2/r2/config/databases.py b/r2/r2/config/databases.py index be26f5a0..14d09b01 100644 --- a/r2/r2/config/databases.py +++ b/r2/r2/config/databases.py @@ -74,6 +74,8 @@ dbm.thing('account', main_engine, main_engine) dbm.thing('message', main_engine, main_engine) dbm.thing('tag', main_engine, main_engine) +dbm.thing('edit', main_engine, main_engine) +dbm.thing('meetup', main_engine, main_engine) dbm.relation('savehide', 'account', 'link', main_engine) dbm.relation('click', 'account', 'link', main_engine) diff --git a/r2/r2/config/middleware.py b/r2/r2/config/middleware.py index 46a6767a..127113f8 100644 --- a/r2/r2/config/middleware.py +++ b/r2/r2/config/middleware.py @@ -41,7 +41,7 @@ #middleware stuff from r2.lib.html_source import HTMLValidationParser from cStringIO import StringIO -import sys, tempfile, urllib, re, os, sha +import sys, tempfile, urllib, re, os, hashlib #from pylons.middleware import error_mapper @@ -181,7 +181,7 @@ def __init__(self, app): auth_cnames = [x.strip() for x in auth_cnames.split(',')] # we are going to be matching with endswith, so make sure there # are no empty strings that have snuck in - self.auth_cnames = [x for x in auth_cnames if x] + self.auth_cnames = filter(None, auth_cnames) def is_auth_cname(self, domain): return any((domain == cname or domain.endswith('.' + cname)) @@ -291,8 +291,8 @@ class ExtensionMiddleware(object): extensions = {'rss' : ('xml', 'text/xml; charset=UTF-8'), 'xml' : ('xml', 'text/xml; charset=UTF-8'), 'js' : ('js', 'text/javascript; charset=UTF-8'), - 'png' : ('png', 'image/png'), - 'css' : ('css', 'text/css'), + #'png' : ('png', 'image/png'), + #'css' : ('css', 'text/css'), 'api' : (api_type(), 'application/json; charset=UTF-8'), 'json' : (api_type(), 'application/json; charset=UTF-8'), 'json-html' : (api_type('html'), 'application/json; charset=UTF-8')} @@ -354,7 +354,7 @@ def __init__(self, log_path, process_iden, app): def __call__(self, environ, start_response): request = '\n'.join('%s: %s' % (k,v) for k,v in environ.iteritems() if k.isupper()) - iden = self.process_iden + '-' + sha.new(request).hexdigest() + iden = self.process_iden + '-' + hashlib.sha1(request).hexdigest() fname = os.path.join(self.log_path, iden) f = open(fname, 'w') @@ -371,6 +371,10 @@ def __call__(self, environ, start_response): return r class LimitUploadSize(object): + """ + Middleware for restricting the size of uploaded files (such as + image files for the CSS editing capability). + """ def __init__(self, app, max_size=1024*500): self.app = app self.max_size = max_size @@ -404,7 +408,29 @@ def start_response_wrapper(status, headers, exc_info=None): return self.app(environ, start_response_wrapper) -#god this stuff is disorganized and confusing +class CleanupMiddleware(object): + """ + Put anything here that should be called after every other bit of + middleware. This currently includes the code for removing + duplicate headers (except multiple cookie setting). The behavior + here is to disregard all but the last record. + """ + def __init__(self, app): + self.app = app + + def __call__(self, environ, start_response): + def custom_start_response(status, headers, exc_info = None): + fixed = [] + seen = set() + for head, val in reversed(headers): + head = head.title() + if head == 'Set-Cookie' or head not in seen: + fixed.insert(0, (head, val)) + seen.add(head) + return start_response(status, fixed, exc_info) + return self.app(environ, custom_start_response) + +#god this shit is disorganized and confusing class RedditApp(PylonsBaseWSGIApp): def find_controller(self, controller): if controller in self.controller_classes: @@ -475,14 +501,15 @@ def make_app(global_conf, full_stack=True, **app_conf): # Static files javascripts_app = StaticJavascripts() - static_app = StaticURLParser(config['pylons.paths']['static_files']) + # Set cache headers indicating the client should cache for 7 days + static_app = StaticURLParser(config['pylons.paths']['static_files'], cache_max_age=604800) app = Cascade([static_app, javascripts_app, app]) - app = make_gzip_middleware(app, app_conf) - app = AbsoluteRedirectMiddleware(app) #add the rewrite rules app = RewriteMiddleware(app) + app = CleanupMiddleware(app) + return app diff --git a/r2/r2/config/routing.py b/r2/r2/config/routing.py index 0eea312e..7e6c4279 100644 --- a/r2/r2/config/routing.py +++ b/r2/r2/config/routing.py @@ -6,16 +6,16 @@ # software over a computer network and provide for limited attribution for the # Original Developer. In addition, Exhibit A has been modified to be consistent # with Exhibit B. -# +# # Software distributed under the License is distributed on an "AS IS" basis, # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for # the specific language governing rights and limitations under the License. -# +# # The Original Code is Reddit. -# +# # The Original Developer is the Initial Developer. The Initial Developer of the # Original Code is CondeNet, Inc. -# +# # All portions of the code written by CondeNet are Copyright (c) 2006-2008 # CondeNet, Inc. All Rights Reserved. ################################################################################ @@ -25,13 +25,14 @@ import os from routes import Mapper import admin_routes +from wiki_pages_embed import allWikiPagesCached def make_map(global_conf={}, app_conf={}): map = Mapper() mc = map.connect admin_routes.add(mc) - + mc('/login', controller='front', action='login') mc('/logout', controller='front', action='logout') mc('/adminon', controller='front', action='adminon') @@ -47,7 +48,7 @@ def make_map(global_conf={}, app_conf={}): mc('/search/results', controller='front', action='search_results') - mc('/about/:location', controller='front', + mc('/about/:location', controller='front', action='editreddit', location = 'about') mc('/categories/create', controller='front', action='newreddit') @@ -60,70 +61,89 @@ def make_map(global_conf={}, app_conf={}): requirements=dict(where='subscriber|contributor|moderator')) #mc('/stats', controller='front', action='stats') - + mc('/user/:username/:where', controller='user', action='listing', where='overview') - + mc('/prefs/:location', controller='front', action='prefs', location='options') - + mc('/related/:article/:title', controller='front', action = 'related', title=None) mc('/lw/:article/:title/:comment', controller='front', action= 'comments', title=None, comment = None) mc('/edit/:article', controller='front', action="editarticle") + + + mc('/stylesheet', controller = 'front', action = 'stylesheet') + + mc('/', controller='promoted', action='listing') - mc('/', controller='root', action='listing') - - listing_controllers = "hot|saved|toplinks|new|recommended|randomrising|comments|blessed|recentposts" + for name,page in allWikiPagesCached.items(): + if page.has_key('route'): + mc("/wiki/"+page['route'], controller='wikipage', action='wikipage', name=name) + + mc('/invalidate_cache/:name', controller='wikipage', action='invalidate_cache') + + listing_controllers = "hot|saved|toplinks|topcomments|new|recommended|randomrising|comments|blessed|recentposts|edits|promoted" mc('/:controller', action='listing', requirements=dict(controller=listing_controllers)) - mc('/tag/:tag', controller='tag', action='listing') + # Can't use map.resource because the version of the Routing module we're + # using doesn't support the controller_action kw arg + #map.resource('meetup', 'meetups', collection_actions=['create', 'new']) + mc('/meetups/create', action='create', controller='meetups') + mc('/meetups', action='listing', controller='meetupslisting') + mc('/meetups/new', action='new', controller='meetups') + mc('/meetups/:id/edit', action='edit', controller='meetups') + mc('/meetups/:id/update', action='update', controller='meetups') + mc('/meetups/:id', action='show', controller='meetups') + + mc('/tag/:tag', controller='tag', action='listing', where='tag') mc('/by_id/:names', controller='byId', action='listing') mc('/:sort', controller='browse', sort='top', action = 'listing', requirements = dict(sort = 'top|controversial')) - + mc('/message/compose', controller='message', action='compose') mc('/message/:where', controller='message', action='listing') - + mc('/:action', controller='front', requirements=dict(action="password|random|framebuster")) mc('/:action', controller='embed', requirements=dict(action="help|blog")) - + mc('/:action', controller='toolbar', requirements=dict(action="goto|toolbar")) - + mc('/resetpassword/:key', controller='front', action='resetpassword') mc('/resetpassword', controller='front', action='resetpassword') mc('/post/:action', controller='post', - requirements=dict(action="options|over18|unlogged_options|optout|optin|login|reg")) - + requirements=dict(action="options|over18|optout|optin|login|reg")) + mc('/api/:action', controller='api') - + mc('/captcha/:iden', controller='captcha', action='captchaimg') mc('/doquery', controller='query', action='doquery') mc('/code', controller='redirect', action='redirect', dest='http://code.google.com/p/lesswrong/') - + mc('/about-less-wrong', controller='front', action='about') mc('/issues', controller='front', action='issues') # Google webmaster tools verification page mc('/googlea26ba8329f727095.html', controller='front', action='blank') - # This route handles displaying the error page and + # This route handles displaying the error page and # graphics used in the 404/500 - # error pages. It should likely stay at the top + # error pages. It should likely stay at the top # to ensure that the error page is # displayed properly. mc('/error/document/:id', controller='error', action="document") diff --git a/r2/r2/controllers/__init__.py b/r2/r2/controllers/__init__.py index d1446a3d..bdf83d41 100644 --- a/r2/r2/controllers/__init__.py +++ b/r2/r2/controllers/__init__.py @@ -6,16 +6,16 @@ # software over a computer network and provide for limited attribution for the # Original Developer. In addition, Exhibit A has been modified to be consistent # with Exhibit B. -# +# # Software distributed under the License is distributed on an "AS IS" basis, # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for # the specific language governing rights and limitations under the License. -# +# # The Original Code is Reddit. -# +# # The Original Developer is the Initial Developer. The Initial Developer of the # Original Code is CondeNet, Inc. -# +# # All portions of the code written by CondeNet are Copyright (c) 2006-2008 # CondeNet, Inc. All Rights Reserved. ################################################################################ @@ -23,6 +23,7 @@ from listingcontroller import HotController from listingcontroller import SavedController from listingcontroller import ToplinksController +from listingcontroller import PromotedController from listingcontroller import NewController from listingcontroller import BrowseController from listingcontroller import RecommendedController @@ -32,10 +33,12 @@ from listingcontroller import RandomrisingController from listingcontroller import UserController from listingcontroller import CommentsController -from listingcontroller import RootController +from listingcontroller import TopcommentsController from listingcontroller import BlessedController from listingcontroller import TagController from listingcontroller import RecentpostsController +from listingcontroller import EditsController +from listingcontroller import MeetupslistingController from listingcontroller import MyredditsController @@ -49,6 +52,8 @@ from toolbar import ToolbarController from i18n import I18nController from promotecontroller import PromoteController +from meetupscontroller import MeetupsController +from wikipagecontroller import WikipageController from querycontroller import QueryController diff --git a/r2/r2/controllers/api.py b/r2/r2/controllers/api.py index 40a59d2e..0ed4ebca 100644 --- a/r2/r2/controllers/api.py +++ b/r2/r2/controllers/api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # The contents of this file are subject to the Common Public Attribution # License Version 1.0. (the "License"); you may not use this file except in # compliance with the License. You may obtain a copy of the License at @@ -6,24 +7,26 @@ # software over a computer network and provide for limited attribution for the # Original Developer. In addition, Exhibit A has been modified to be consistent # with Exhibit B. -# +# # Software distributed under the License is distributed on an "AS IS" basis, # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for # the specific language governing rights and limitations under the License. -# +# # The Original Code is Reddit. -# +# # The Original Developer is the Initial Developer. The Initial Developer of the # Original Code is CondeNet, Inc. -# +# # All portions of the code written by CondeNet are Copyright (c) 2006-2008 # CondeNet, Inc. All Rights Reserved. ################################################################################ from reddit_base import RedditController from pylons.i18n import _ -from pylons import c, request +from pylons import c, request, response +from pylons.controllers.util import etag_cache +import hashlib from validator import * from r2.models import * @@ -32,11 +35,16 @@ from r2.controllers import ListingController -from r2.lib.utils import get_title, sanitize_url, timeuntil, set_last_modified +from r2.lib.utils import get_title, sanitize_url, timeuntil, \ + set_last_modified, remote_addr from r2.lib.utils import query_string, to36, timefromnow from r2.lib.wrapped import Wrapped -from r2.lib.pages import FriendList, ContributorList, ModList, \ - BannedList, BoringPage, FormPage, NewLink, CssError, UploadedImage +from r2.lib.pages import FriendList, ContributorList, ModList, EditorList, \ + BannedList, BoringPage, FormPage, NewLink, CssError, UploadedImage, \ + RecentArticles, RecentComments, TagCloud, TopContributors, TopMonthlyContributors, WikiPageList, \ + ArticleNavigation, UpcomingMeetups, RecentPromotedArticles, \ + MeetupsMap + from r2.lib.menus import CommentSortMenu from r2.lib.translation import Translator @@ -55,9 +63,8 @@ from r2.lib.media import force_thumbnail, thumbnail_url from r2.lib.comment_tree import add_comment, delete_comment -from simplejson import dumps - from datetime import datetime, timedelta +from simplejson import dumps from md5 import md5 from r2.lib.promote import promote, unpromote, get_promoted @@ -70,13 +77,13 @@ def link_listing_by_url(url, count = None): links = links[:count] except NotFound: links = () - + names = [l._fullname for l in links] builder = IDBuilder(names, num = 25) listing = LinkListing(builder).listing() return listing - + class ApiController(RedditController): def response_func(self, **kw): return self.sendstring(dumps(kw)) @@ -86,7 +93,7 @@ def ajax_login_redirect(self, res, dest): res._redirect("/login" + query_string(dict(dest=dest))) def link_exists(self, url, sr, message = False): - try: + try: l = Link._by_url(url, sr) if message: return l.already_submitted_link() @@ -162,7 +169,7 @@ def POST_compose(self, res, to, subject, body, ip): spam = (c.user._spam or errors.BANNED_IP in c.errors or errors.BANNED_DOMAIN in c.errors) - + m, inbox_rel = Message._new(c.user, to, subject, body, ip, spam) res._update('success', innerHTML=_("Your message has been delivered")) @@ -175,46 +182,6 @@ def POST_compose(self, res, to, subject, body, ip): else: res._update('success', innerHTML='') - - @validate(VUser(), - VSRSubmitPage(), - url = VRequired('url', None), - title = VRequired('title', None)) - def GET_submit(self, url, title): - if url and not request.get.get('resubmit'): - listing = link_listing_by_url(url) - redirect_link = None - if listing.things: - if len(listing.things) == 1: - redirect_link = listing.things[0] - else: - subscribed = [l for l in listing.things - if c.user_is_loggedin - and l.subreddit.is_subscriber_defaults(c.user)] - - #if there is only 1 link to be displayed, just go there - if len(subscribed) == 1: - redirect_link = subscribed[0] - else: - infotext = strings.multiple_submitted % \ - listing.things[0].resubmit_link() - res = BoringPage(_("Seen it"), - content = listing, - infotext = infotext).render() - return res - - if redirect_link: - return self.redirect(redirect_link.already_submitted_link) - - captcha = Captcha() if c.user.needs_captcha() else None - srs = Subreddit.submit_sr(c.user) if c.default_sr else () - - return FormPage(_("Submit"), - content=NewLink(url=url or '', - title=title or '', - subreddits = srs, - captcha=captcha)).render() - @Json @validate(VAdmin(), link = VByName('id')) @@ -231,6 +198,7 @@ def POST_unbless(self, res, link): @validate(VUser(), VCaptcha(), VRatelimit(rate_user = True, rate_ip = True, prefix='rate_submit_'), + VModhash(), ip = ValidIP(), sr = VSubmitSR('sr'), title = VTitle('title'), @@ -241,8 +209,8 @@ def POST_unbless(self, res, link): tags = VTags('tags')) def POST_submit(self, res, l, new_content, title, save, continue_editing, sr, ip, tags): res._update('status', innerHTML = '') - should_ratelimit = sr.should_ratelimit(c.user, 'link') - + should_ratelimit = sr.should_ratelimit(c.user, 'link') if sr else True + #remove the ratelimit error if the user's karma is high if not should_ratelimit: c.errors.remove(errors.RATELIMIT) @@ -259,6 +227,9 @@ def POST_submit(self, res, l, new_content, title, save, continue_editing, sr, ip res._focus('title') elif res._chk_captcha(errors.BAD_CAPTCHA): pass + elif res._chk_error(errors.SUBREDDIT_FORBIDDEN): + pass + if res.error or not title: return @@ -266,7 +237,7 @@ def POST_submit(self, res, l, new_content, title, save, continue_editing, sr, ip spam = (c.user._spam or errors.BANNED_IP in c.errors or errors.BANNED_DOMAIN in c.errors) - + if not new_content: new_content = '' @@ -275,12 +246,6 @@ def POST_submit(self, res, l, new_content, title, save, continue_editing, sr, ip # print "\n".join(request.post.va) if not l: l = Link._submit(request.post.title, new_content, c.user, sr, ip, tags, spam) - if l.url.lower() == 'self': - l.url = l.make_permalink_slow() - l.is_self = True - l._commit() - l.set_url_cache() - v = Vote.vote(c.user, l, True, ip, spam) if save == 'on': r = l._save(c.user) if g.write_query_queue: @@ -292,8 +257,10 @@ def POST_submit(self, res, l, new_content, title, save, continue_editing, sr, ip #update the queries if g.write_query_queue: queries.new_link(l) - queries.new_vote(v) else: + edit = None + if c.user._id != l.author_id: + edit = Edit._new(l,c.user,new_content) old_url = l.url l.title = request.post.title l.article = new_content @@ -301,14 +268,16 @@ def POST_submit(self, res, l, new_content, title, save, continue_editing, sr, ip l._commit() l.set_tags(tags) l.update_url_cache(old_url) + if edit: + edit._commit() #update the modified flags set_last_modified(c.user, 'overview') set_last_modified(c.user, 'submitted') - + # flag search indexer that something has changed tc.changed(l) - + if continue_editing: path = "/edit/%s" % l._id36 else: @@ -317,7 +286,8 @@ def POST_submit(self, res, l, new_content, title, save, continue_editing, sr, ip # cname. cname = c.cname c.cname = False - path = l.make_permalink_slow() + #path = l.make_permalink_slow() + path = l.make_permalink(sr, sr_path = not sr.name == g.default_sr) c.cname = cname res._redirect(path) @@ -361,6 +331,12 @@ def POST_register(self, res, name, email, password, op, dest, rem, reason): res._update('status_' + op, innerHTML='') if res._chk_error(errors.BAD_USERNAME, op): res._focus('user_reg') + elif res._chk_error(errors.BAD_USERNAME_SHORT, op): + res._focus('user_reg') + elif res._chk_error(errors.BAD_USERNAME_LONG, op): + res._focus('user_reg') + elif res._chk_error(errors.BAD_USERNAME_CHARS, op): + res._focus('user_reg') elif res._chk_error(errors.USERNAME_TAKEN, op): res._focus('user_reg') elif res._chk_error(errors.BAD_PASSWORD, op): @@ -394,7 +370,7 @@ def POST_register(self, res, name, email, password, op, dest, rem, reason): d = c.user._dirties.copy() user._commit() - + c.user = user # Create a drafts subredit for this user @@ -419,7 +395,7 @@ def POST_register(self, res, name, email, password, op, dest, rem, reason): self._subscribe(sr, sub) self._login(res, user, dest, rem) - + @Json @validate(VUser(), @@ -441,7 +417,7 @@ def POST_leave(self, res, container, type): redirect = nop('redirect'), friend = VExistingUname('name'), container = VByName('container'), - type = VOneOf('type', ('friend', 'moderator', 'contributor', 'banned'))) + type = VOneOf('type', ('friend', 'moderator', 'editor', 'contributor', 'banned'))) def POST_friend(self, res, ip, friend, action, redirect, container, type): res._update('status', innerHTML='') @@ -451,6 +427,8 @@ def POST_friend(self, res, ip, friend, action, redirect, container, type): and (type in ('moderator','contributer','banned') and not c.site.is_moderator(c.user))): + abort(403,'forbidden') + elif type == 'editor' and not c.user_is_admin: abort(403,'forbidden') elif action == 'add': if res._chk_errors((errors.USER_DOESNT_EXIST, @@ -460,10 +438,11 @@ def POST_friend(self, res, ip, friend, action, redirect, container, type): new = fn(friend) cls = dict(friend=FriendList, moderator=ModList, + editor=EditorList, contributor=ContributorList, banned=BannedList).get(type) res._update('name', value = '') - + #subscribing doesn't need a response if new and cls: res.object = cls().ajax_user(friend).for_ajax('add') @@ -474,7 +453,7 @@ def POST_friend(self, res, ip, friend, action, redirect, container, type): if msg and subj and friend.name != c.user.name: # fullpath with domain needed or the markdown link # will break - d = dict(url = container.path, + d = dict(url = container.path, title = container.title) msg = msg % d subj = subj % d @@ -497,7 +476,7 @@ def POST_update(self, res, email, curpass, password, newpass, verpass): if res._chk_error(errors.WRONG_PASSWORD): res._focus('curpass') res._update('curpass', value='') - return + return updated = False if res._chk_error(errors.BAD_EMAILS): res._focus('email') @@ -505,10 +484,10 @@ def POST_update(self, res, email, curpass, password, newpass, verpass): or c.user.email != email): c.user.email = email c.user._commit() - res._update('status', + res._update('status', innerHTML=_('Your email has been updated')) updated = True - + if newpass or verpass: if res._chk_error(errors.BAD_PASSWORD): res._focus('newpass') @@ -518,10 +497,10 @@ def POST_update(self, res, email, curpass, password, newpass, verpass): else: change_password(c.user, password) if updated: - res._update('status', + res._update('status', innerHTML=_('Your email and password have been updated')) else: - res._update('status', + res._update('status', innerHTML=_('Your password has been updated')) self.login(c.user) @@ -532,11 +511,11 @@ def POST_update(self, res, email, curpass, password, newpass, verpass): areyousure2 = nop('areyousure2'), areyousure3 = nop('areyousure3')) def POST_delete_user(self, res, areyousure1, areyousure2, areyousure3): - if areyousure1 == areyousure2 == areyousure3 == 'yes': + if areyousure1 == areyousure2 == areyousure3 == 'Yes': c.user.delete() res._redirect('/?deleted=true') else: - res._update('status', + res._update('status', innerHTML = _("See? you don't really want to leave")) @Json @@ -545,6 +524,13 @@ def POST_delete_user(self, res, areyousure1, areyousure2, areyousure3): thing = VByNameIfAuthor('id')) def POST_del(self, res, thing): '''for deleting all sorts of things''' + + # Special check if comment can be deleted + if isinstance(thing, Comment) and (not thing.can_delete()): + c.errors.add(errors.CANNOT_DELETE) + res._chk_error(errors.CANNOT_DELETE) + return + thing._deleted = True thing._commit() @@ -565,6 +551,20 @@ def POST_del(self, res, thing): if g.use_query_cache: queries.new_comment(thing, None) + @Json + @validate(VUser(), + VModhash(), + thing = VByNameIfAuthor('id')) + def POST_retract(self, res, thing): + '''for retracting comments''' + + if isinstance(thing, Comment): + thing.retracted = True + thing._commit() + if g.use_query_cache: + queries.new_comment(thing, None) + + @Json @validate(VUser(), VModhash(), thing = VByName('id')) @@ -610,6 +610,7 @@ def POST_comment(self, res, parent, comment, ip): if isinstance(parent, Message): is_message = True should_ratelimit = False + link = None else: is_message = False is_comment = True @@ -623,7 +624,7 @@ def POST_comment(self, res, parent, comment, ip): if not sr.should_ratelimit(c.user, 'comment'): should_ratelimit = False - if not link.comments_enabled: + if link and not link.comments_enabled: return abort(403,'forbidden') #remove the ratelimit error if the user's karma is high @@ -633,17 +634,17 @@ def POST_comment(self, res, parent, comment, ip): if res._chk_errors((errors.BAD_COMMENT,errors.COMMENT_TOO_LONG, errors.RATELIMIT), parent._fullname): res._focus("comment_reply_" + parent._fullname) - return + return res._show('reply_' + parent._fullname) - res._update("comment_reply_" + parent._fullname, rows = 2) + res._update("comment_reply_" + parent._fullname, rows = '2') spam = (c.user._spam or errors.BANNED_IP in c.errors) - + if is_message: to = Account._byID(parent.author_id) subject = parent.subject - re = "re: " + re = "Re: " if not subject.startswith(re): subject = re + subject item, inbox_rel = Message._new(c.user, to, subject, comment, ip, spam) @@ -652,21 +653,10 @@ def POST_comment(self, res, parent, comment, ip): else: item, inbox_rel = Comment._new(c.user, link, parent_comment, comment, ip, spam) - Vote.vote(c.user, item, True, ip) - res._update("comment_reply_" + parent._fullname, + res._update("comment_reply_" + parent._fullname, innerHTML='', value='') res._send_things(item) res._hide('noresults') - # flag search indexer that something has changed - tc.changed(item) - - #update last modified - set_last_modified(c.user, 'overview') - set_last_modified(c.user, 'commented') - set_last_modified(link, 'comments') - - #update the comment cache - add_comment(item) #update the queries if g.write_query_queue: @@ -688,8 +678,8 @@ def POST_comment(self, res, parent, comment, ip): prefix = "rate_share_"), share_from = VLength('share_from', length = 100), emails = ValidEmails("share_to"), - reply_to = ValidEmails("replyto", num = 1), - message = VLength("message", length = 1000), + reply_to = ValidEmails("replyto", num = 1), + message = VLength("message", length = 1000), thing = VByName('id')) def POST_share(self, res, emails, thing, share_from, reply_to, message): @@ -730,7 +720,7 @@ def POST_share(self, res, emails, thing, share_from, reply_to, innerHTML=_('Shared')) res._update("sharelink_" + thing._fullname, - innerHTML=("

%s

" % + innerHTML=("

%s

" % _("Your link has been shared."))) emailer.share(thing, emails, from_name = share_from or "", @@ -739,9 +729,9 @@ def POST_share(self, res, emails, thing, share_from, reply_to, #set the ratelimiter if should_ratelimit: VRatelimit.ratelimit(rate_user=True, rate_ip = True, prefix = "rate_share_") - - - + + + @Json @validate(VUser(), VModhash(), @@ -755,28 +745,38 @@ def POST_vote(self, res, dir, thing, ip, vote_type): spam = (c.user._spam or errors.BANNED_IP in c.errors or errors.CHEATER in c.errors) + dir = (True if dir > 0 + else False if dir < 0 + else None) - if thing: - dir = (True if dir > 0 - else False if dir < 0 - else None) - organic = vote_type == 'organic' - v = Vote.vote(user, thing, dir, ip, spam, organic) + isRetracted = thing.retracted if thing and isinstance(thing,Comment) else False - #update relevant caches - if isinstance(thing, Link): - sr = thing.subreddit_slow - set_last_modified(c.user, 'liked') - set_last_modified(c.user, 'disliked') + # Ensure authors can't vote on their own posts / comments + # Cannot vote on retracted comments + if thing and thing.author_id != c.user._id and not isRetracted: + try: + organic = vote_type == 'organic' + v = Vote.vote(user, thing, dir, ip, spam, organic) - if v.valid_thing: - expire_hot(sr) + #update relevant caches + if isinstance(thing, Link): + sr = thing.subreddit_slow + set_last_modified(c.user, 'liked') + set_last_modified(c.user, 'disliked') - if g.write_query_queue: - queries.new_vote(v) + if v.valid_thing: + expire_hot(sr) - # flag search indexer that something has changed - tc.changed(thing) + if g.write_query_queue: + queries.new_vote(v) + + # flag search indexer that something has changed + tc.changed(thing) + + except NotEnoughKarma, e: + # User is downvoting and does not have enough karma. + res._update('status_'+thing._fullname, innerHTML = e.message) + res._show('status_'+thing._fullname) @Json @validate(VUser(), @@ -795,7 +795,7 @@ def POST_subreddit_stylesheet(self, res, stylesheet_contents = '', op='save'): if report.errors: error_items = [ CssError(x).render(style='html') for x in sorted(report.errors) ] - + res._update('status', innerHTML = _('Validation errors')) res._update('validation-errors', innerHTML = ''.join(error_items)) res._show('error-header') @@ -893,7 +893,6 @@ def POST_delete_link_img(self, res, link, name): # reset the status res._update('img-status', innerHTML = _("Deleted")) - @Json @validate(VSrModerator(), VModhash()) @@ -916,7 +915,78 @@ def POST_delete_sr_header(self, res): # reset the status boxes res._update('img-status', innerHTML = _("Deleted")) res._update('status', innerHTML = "") - + + def render_cached(self, cache_key, render_cls, max_age, cache_time=0, *args, **kwargs): + """Render content using client caching and server caching.""" + + # Default the cache to be the same as our max age if not + # supplied. + cache_time = cache_time or max_age + + # Postfix the cache key with the subreddit name + # This scopes all the caches by subreddit + cache_key = cache_key + '-' + c.site.name + + # Get the etag and content from the cache. + hit = g.rendercache.get(cache_key) + if hit: + etag, content = hit + else: + # Generate and cache the content along with an etag. + content = render_cls(*args, **kwargs).render() + etag = '"%s"' % datetime.utcnow().isoformat() + g.rendercache.set(cache_key, (etag, content), time=cache_time) + + # Check if the client already has the correct content and + # throw 304 if so. Note that we want to set the max age in the + # 304 response, we can only do this by using the + # pylons.response object just like the etag_cache fn does + # within pylons (it sets the etag header). Setting it on the + # c.response won't work as c.response isn't used when an + # exception is thrown. Note also that setting it on the + # pylons.response will send the max age in the 200 response + # (just like the etag header is sent in the response). + response.headers['Cache-Control'] = 'max-age=%d' % max_age + etag_cache(etag) + + # Return full response using our cached info. + c.response.content = content + return c.response + + TWELVE_HOURS = 3600 * 12 + + def GET_side_posts(self, *a, **kw): + """Return HTML snippet of the recent posts for the side bar.""" + # Server side cache is also invalidated when new article is posted + return self.render_cached('side-posts', RecentArticles, g.side_posts_max_age) + + def GET_side_comments(self, *a, **kw): + """Return HTML snippet of the recent comments for the side bar.""" + # Server side cache is also invalidated when new comment is posted + return self.render_cached('side-comments', RecentComments, g.side_comments_max_age, self.TWELVE_HOURS) + + def GET_side_tags(self, *a, **kw): + """Return HTML snippet of the tags for the side bar.""" + return self.render_cached('side-tags', TagCloud, g.side_tags_max_age) + + def GET_side_contributors(self, *a, **kw): + """Return HTML snippet of the top contributors for the side bar.""" + return self.render_cached('side-contributors', TopContributors, g.side_contributors_max_age) + + def GET_side_meetups(self, *a, **kw): + """Return HTML snippet of the upcoming meetups for the side bar.""" + ip = remote_addr(c.environ) + location = Meetup.geoLocateIp(ip) + # Key to group cached meetup pages with + invalidating_key = g.rendercache.get_key_group_value(Meetup.group_cache_key()) + cache_key = "%s-side-meetups-%s" % (invalidating_key,ip) + return self.render_cached(cache_key, UpcomingMeetups, g.side_meetups_max_age, + cache_time=self.TWELVE_HOURS, location=location, + max_distance=g.meetups_radius) + + def GET_side_monthly_contributors(self, *a, **kw): + """Return HTML snippet of the top monthly contributors for the side bar.""" + return self.render_cached('side-monthly-contributors', TopMonthlyContributors, g.side_contributors_max_age) def GET_upload_sr_img(self, *a, **kw): """ @@ -935,6 +1005,30 @@ def GET_upload_link_img(self, *a, **kw): """ return "nothing to see here." + def GET_front_recent_posts(self, *a, **kw): + """Return HTML snippet of the recent promoted posts for the front page.""" + # Server side cache is also invalidated when new article is posted + return self.render_cached('recent-promoted', RecentPromotedArticles, g.side_posts_max_age) + + def GET_front_meetups_map(self, *a, **kw): + ip = remote_addr(c.environ) + location = Meetup.geoLocateIp(ip) + invalidating_key = g.rendercache.get_key_group_value(Meetup.group_cache_key()) + cache_key = "%s-front-meetups-%s" % (invalidating_key,ip) + return self.render_cached(cache_key, MeetupsMap, g.side_meetups_max_age, + cache_time=self.TWELVE_HOURS, location=location, + max_distance=g.meetups_radius) + + @validate(link = VLink('article_id', redirect=False)) + def GET_article_navigation(self, link, *a, **kw): + """Returns the article navigation fragment for the article specified""" + author = Account._byID(link.author_id, data=True) if link else None + return self.render_cached( + 'article_navigation_%s' % (link._id36 if link else None), + ArticleNavigation, g.article_navigation_max_age, + link=link, author=author + ) + @validate(VModhash(), file = VLength('file', length=1024*500), name = VCssName("name"), @@ -981,7 +1075,7 @@ def POST_upload_sr_img(self, file, header, name): if any(errors.values()): return UploadedImage("", "", "", errors = errors).render() - else: + else: # with the image num, save the image an upload to s3. the # header image will be of the form "${c.site._fullname}.png" # while any other image will be ${c.site._fullname}_${num}.png @@ -989,10 +1083,10 @@ def POST_upload_sr_img(self, file, header, name): if header: c.site.header = new_url c.site._commit() - - return UploadedImage(_('Saved'), new_url, name, + + return UploadedImage(_('Saved'), new_url, name, errors = errors).render() - + @validate(VModhash(), link = VLink('article_id'), @@ -1104,12 +1198,12 @@ def POST_site_admin(self, res, name ='', sr = None, **kw): #editting an existing reddit elif sr.is_moderator(c.user) or c.user_is_admin: #assume sr existed, or was just built - clear_memo('subreddit._by_domain', + clear_memo('subreddit._by_domain', Subreddit, _force_unicode(sr.domain)) for k, v in kw.iteritems(): setattr(sr, k, v) sr._commit() - clear_memo('subreddit._by_domain', + clear_memo('subreddit._by_domain', Subreddit, _force_unicode(sr.domain)) # flag search indexer that something has changed @@ -1205,7 +1299,7 @@ def _children(cur_items): cm.child = None else: items.append(cm.child) - + return items # assumes there is at least one child # a = _children(items[0].child.things) @@ -1216,7 +1310,7 @@ def _children(cur_items): a.extend(_children(item.child.things)) item.child = None - # the result is not always sufficient to replace the + # the result is not always sufficient to replace the # morechildren link if mc_id not in [x._fullname for x in a]: res._hide('thingrow_' + str(mc_id)) @@ -1246,7 +1340,7 @@ def GET_bookmarklet(self, action, uh, links): Subreddit.load_subreddits(links, return_dict = False) user = c.user if c.user_is_loggedin else None links = [l for l in links if l.subreddit_slow.can_view(user)] - + if links: if action in ['like', 'dislike']: #vote up all of the links @@ -1274,7 +1368,7 @@ def POST_password(self, res, user): else: emailer.password_email(user) res._success() - + @Json @validate(user = VCacheKey('reset', ('key', 'name')), key= nop('key'), @@ -1339,7 +1433,7 @@ def POST_deltranslator(self, res, l): sr = VByName('sr')) def POST_subscribe(self, res, action, sr): self._subscribe(sr, action == 'sub') - + def _subscribe(self, sr, sub): Subreddit.subscribe_defaults(c.user) @@ -1359,7 +1453,7 @@ def POST_disable_lang(self, res, lang): if lang and Translator.exists(lang): tr = Translator(locale = lang) tr._is_enabled = False - + @Json @validate(VAdmin(), @@ -1371,7 +1465,7 @@ def POST_enable_lang(self, res, lang): def action_cookie(action): s = action + request.ip + request.user_agent - return sha.new(s).hexdigest() + return hashlib.sha1(s).hexdigest() @Json @@ -1444,7 +1538,7 @@ def POST_edit_promo(self, res, ip, elif (not l or url != l.url) and res._chk_error(errors.ALREADY_SUB): #if url == l.url, we're just editting something else res._focus('url') - elif res._chk_error(errors.SUBREDDIT_NOEXIST): + elif res._chk_error(errors.SUBREDDIT_NOEXIST) or res._chk_error(errors.SUBREDDIT_FORBIDDEN): res._focus('sr') elif expire == 'expirein' and res._chk_error(errors.BAD_NUMBER): res._focus('timelimitlength') @@ -1476,7 +1570,7 @@ def POST_edit_promo(self, res, ip, promote(l, subscribers_only = subscribers_only, promote_until = promote_until, disable_comments = disable_comments) - + res._redirect('/promote/edit_promo/%s' % to36(l._id)) def GET_link_thumb(self, *a, **kw): @@ -1502,7 +1596,7 @@ def POST_link_thumb(self, link=None, file=None): else: return UploadedImage(_('Saved'), thumbnail_url(link), "upload", errors = errors).render() - + @Json @validate(ids = VLinkFullnames('ids')) diff --git a/r2/r2/controllers/captcha.py b/r2/r2/controllers/captcha.py index 20989a7a..b654a546 100644 --- a/r2/r2/controllers/captcha.py +++ b/r2/r2/controllers/captcha.py @@ -22,10 +22,12 @@ from reddit_base import RedditController import StringIO import r2.lib.captcha as captcha +import re from pylons import c class CaptchaController(RedditController): def GET_captchaimg(self, iden): + iden = re.sub("\.png$", "", iden, re.IGNORECASE) image = captcha.get_image(iden) f = StringIO.StringIO() image.save(f, "PNG") diff --git a/r2/r2/controllers/error.py b/r2/r2/controllers/error.py index bd62add9..0dc86339 100644 --- a/r2/r2/controllers/error.py +++ b/r2/r2/controllers/error.py @@ -57,9 +57,7 @@ Error encountered

-

%s -

''' diff --git a/r2/r2/controllers/errors.py b/r2/r2/controllers/errors.py index 993969ae..91bc4567 100644 --- a/r2/r2/controllers/errors.py +++ b/r2/r2/controllers/errors.py @@ -28,9 +28,13 @@ ('BAD_URL', _('You should check that url')), ('NO_TITLE', _('Title required')), ('TITLE_TOO_LONG', _('Title too long')), + ('LOCATION_TOO_LONG', _('Location is too long')), ('COMMENT_TOO_LONG', _('Comment too long')), ('BAD_CAPTCHA', _('Incorrect, try again')), ('BAD_USERNAME', _('Invalid user name')), + ('BAD_USERNAME_SHORT', _('Username is too short')), + ('BAD_USERNAME_LONG', _('Username is too long')), + ('BAD_USERNAME_CHARS', _('Username may not contain special characters')), ('USERNAME_TAKEN', _('That username is already taken')), ('NO_THING_ID', _('Id not specified')), ('NOT_AUTHOR', _("Only the author can do that")), @@ -52,6 +56,7 @@ ('ALREADY_SUB', _("That link has already been submitted")), ('SUBREDDIT_EXISTS', _('That category already exists')), ('SUBREDDIT_NOEXIST', _('That category doesn\'t exist')), + ('SUBREDDIT_FORBIDDEN', _("You don't have permission to submit to that category.")), ('BAD_SR_NAME', _('That name isn\'t going to work')), ('RATELIMIT', _('You are trying to submit too fast. try again in %(time)s.')), ('EXPIRED', _('Your session has expired')), @@ -66,6 +71,11 @@ ('BAD_EMAILS', _('The following emails are invalid: %(emails)s')), ('NO_EMAILS', _('Please enter at least one email address')), ('TOO_MANY_EMAILS', _('Please only share to %(num)s emails at a time.')), + ('NO_LOCATION', _('You must supply a location')), + ('NO_DATE', _('The time and date of the meetup is required')), + ('INVALID_DATE', _('Must be a valid date and time')), + ('NO_DESCRIPTION', _('You must supply a description')), + ('CANNOT_DELETE', _('Cannot delete that comment')), )) errors = Storage([(e, e) for e in error_list.keys()]) diff --git a/r2/r2/controllers/front.py b/r2/r2/controllers/front.py index bd8cd569..878eec38 100644 --- a/r2/r2/controllers/front.py +++ b/r2/r2/controllers/front.py @@ -130,6 +130,11 @@ def GET_comments(self, article, comment, context, sort, num_comments): if not c.default_sr and c.site._id != article.sr_id: return self.abort404() + # moderator is either reddit's moderator or an admin + is_moderator = c.user_is_loggedin and c.site.is_moderator(c.user) or c.user_is_admin + if article._spam and not is_moderator: + return self.abort404() + if not article.subreddit_slow.can_view(c.user): abort(403, 'forbidden') @@ -152,6 +157,10 @@ def GET_comments(self, article, comment, context, sort, num_comments): user_num = c.user.pref_num_comments or g.num_comments num = g.max_comments if num_comments == 'true' else user_num + # Override sort if the link has a default set + if hasattr(article, 'comment_sort_order'): + sort = article.comment_sort_order + builder = CommentBuilder(article, CommentSortMenu.operator(sort), comment, context) listing = NestedListing(builder, num = num, @@ -181,21 +190,34 @@ def GET_comments(self, article, comment, context, sort, num_comments): displayPane.append(listing.listing()) loc = None if c.focal_comment or context is not None else 'comments' - + if article.comments_enabled: + sort_menu = CommentSortMenu(default = sort, type='dropdown2') + if hasattr(article, 'comment_sort_order'): + sort_menu.enabled = False + nav_menus = [sort_menu, + NumCommentsMenu(article.num_comments, + default=num_comments)] + content = CommentListing( content = displayPane, num_comments = article.num_comments, - nav_menus = [CommentSortMenu(default = sort), - NumCommentsMenu(article.num_comments, - default=num_comments)], + nav_menus = nav_menus, ) else: content = PaneStack() + # is_canonical indicates if the page is the canonical location for + # the resource. The canonical location is deemed to be one with + # no query string arguments res = LinkInfoPage(link = article, comment = comment, content = content, - infotext = infotext).render() + infotext = infotext, + is_canonical = bool(not request.GET)).render() + + if c.user_is_loggedin: + article._click(c.user) + return res @validate(VUser(), @@ -251,6 +273,8 @@ def GET_editreddit(self, location, num, after, reverse, count): pane = CreateSubreddit(site = c.site, listings = ListingController.listing_names()) elif location == 'moderators': pane = ModList(editable = is_moderator) + elif location == 'editors': + pane = EditorList(editable = c.user_is_admin) elif is_moderator and location == 'banned': pane = BannedList(editable = is_moderator) elif location == 'contributors' and c.site.type != 'public': @@ -300,9 +324,9 @@ def GET_editreddit(self, location, num, after, reverse, count): return EditReddit(content = pane).render() - def GET_stats(self): - """The stats page.""" - return BoringPage(_("Stats"), content = UserStats()).render() + # def GET_stats(self): + # """The stats page.""" + # return BoringPage(_("Stats"), content = UserStats()).render() # filter for removing punctuation which could be interpreted as lucene syntax related_replace_regex = re.compile('[?\\&|!{}+~^()":*-]+') @@ -428,10 +452,9 @@ def GET_login(self): return LoginPage(dest = dest).render() def GET_logout(self): - """wipe login cookie and redirect to referer.""" + """wipe login cookie and redirect to front page.""" self.logout() - dest = request.referer or '/' - return self.redirect(dest) + return self.redirect('/') @validate(VUser()) @@ -466,12 +489,17 @@ def GET_validuser(self): @validate(VUser(), - VSRSubmitPage(), + can_submit = VSRSubmitPage(), url = VRequired('url', None), title = VRequired('title', None), tags = VTags('tags')) - def GET_submit(self, url, title, tags): + def GET_submit(self, can_submit, url, title, tags): """Submit form.""" + if not can_submit: + return BoringPage(_("Not Enough Karma"), + infotext="You do not have enough karma to post.", + content=NotEnoughKarmaToPost()).render() + if url and not request.get.get('resubmit'): # check to see if the url has already been submitted listing = link_listing_by_url(url) @@ -501,8 +529,8 @@ def GET_submit(self, url, title, tags): if redirect_link: return self.redirect(redirect_link.already_submitted_link) - captcha = Captcha() if c.user.needs_captcha() else None - srs = Subreddit.submit_sr(c.user) if c.default_sr else () + captcha = Captcha(tabular=False) if c.user.needs_captcha() else None + srs = Subreddit.submit_sr(c.user) # Set the default sr to the user's draft when creating a new article try: @@ -510,7 +538,7 @@ def GET_submit(self, url, title, tags): except NotFound: sr = None - return FormPage(_("Submit article"), + return FormPage(_("Submit Article"), content=NewLink(title=title or '', subreddits = srs, tags=tags, @@ -522,12 +550,19 @@ def GET_submit(self, url, title, tags): article = VSubmitLink('article')) def GET_editarticle(self, article): author = Account._byID(article.author_id, data=True) - subreddits = Subreddit.submit_sr(author) if c.default_sr else () + subreddits = Subreddit.submit_sr(author) + article_sr = Subreddit._byID(article.sr_id) if c.user_is_admin: - # Add this admin subreddits to the list - subreddits = list(set(subreddits).union(Subreddit.submit_sr(c.user))) - return FormPage(_("Edit article"), - content=EditLink(article, subreddits=subreddits, tags=article.tag_names(), captcha=None)).render() + # Add this admins subreddits to the list + subreddits = list(set(subreddits).union([article_sr] + Subreddit.submit_sr(c.user))) + elif article_sr.is_editor(c.user) and c.user != author: + # An editor can save to the current subreddit irrspective of the original author's karma + subreddits = [sr for sr in Subreddit.submit_sr(c.user) if sr.is_editor(c.user)] + + captcha = Captcha(tabular=False) if c.user.needs_captcha() else None + + return FormPage(_("Edit article"), + content=EditLink(article, subreddits=subreddits, tags=article.tag_names(), captcha=captcha)).render() def _render_opt_in_out(self, msg_hash, leave): """Generates the form for an optin/optout page""" @@ -604,3 +639,4 @@ def GET_issues(self): def GET_blank(self): return '' + diff --git a/r2/r2/controllers/listingcontroller.py b/r2/r2/controllers/listingcontroller.py index 58289a5a..4af20c70 100644 --- a/r2/r2/controllers/listingcontroller.py +++ b/r2/r2/controllers/listingcontroller.py @@ -6,16 +6,16 @@ # software over a computer network and provide for limited attribution for the # Original Developer. In addition, Exhibit A has been modified to be consistent # with Exhibit B. -# +# # Software distributed under the License is distributed on an "AS IS" basis, # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for # the specific language governing rights and limitations under the License. -# +# # The Original Code is Reddit. -# +# # The Original Developer is the Initial Developer. The Initial Developer of the # Original Code is CondeNet, Inc. -# +# # All portions of the code written by CondeNet are Copyright (c) 2006-2008 # CondeNet, Inc. All Rights Reserved. ################################################################################ @@ -24,7 +24,7 @@ from r2.models import * from r2.lib.pages import * -from r2.lib.menus import NewMenu, TimeMenu, SortMenu, RecSortMenu, ControversyTimeMenu +from r2.lib.menus import NewMenu, TimeMenu, SortMenu, RecSortMenu, TagSortMenu from r2.lib.rising import get_rising from r2.lib.wrapped import Wrapped from r2.lib.normalized_hot import normalized_hot, get_hot @@ -35,6 +35,7 @@ from r2.lib import organic from r2.lib.solrsearch import SearchQuery from r2.lib.utils import iters, check_cheating +from r2.lib.filters import _force_unicode from admin import admin_profile_query @@ -48,7 +49,7 @@ class ListingController(RedditController): # toggle skipping of links based on the users' save/hide/vote preferences skip = True - # toggles showing numbers + # toggles showing numbers show_nums = True # any text that should be shown on the top of the page @@ -72,6 +73,9 @@ class ListingController(RedditController): _link_listings = None + # Robot (search engine) directives + robots = None + @classmethod def link_listings(cls, key = None): # This is done to defer generation of the dictionary until after @@ -96,7 +100,25 @@ def menus(self): """list of menus underneat the header (e.g., sort, time, kind, etc) to be displayed on this listing page""" return [] - + + @property + def top_filter(self): + return None + + @property + def header_sub_nav(self): + buttons = [] + if c.default_sr: + buttons.append(NamedButton("promoted")) + buttons.append(NamedButton("new")) + else: + buttons.append(NamedButton("new", aliases = ["/"])) + + buttons.append(NamedButton('top')) + if c.user_is_loggedin: + buttons.append(NamedButton('saved')) + return buttons + @base_listing def build_listing(self, num, after, reverse, count): """uses the query() method to define the contents of the @@ -107,15 +129,21 @@ def build_listing(self, num, after, reverse, count): self.after = after self.reverse = reverse + if after is not None: + self.robots = "noindex,follow" + self.query_obj = self.query() self.builder_obj = self.builder() self.listing_obj = self.listing() content = self.content() res = self.render_cls(content = content, - show_sidebar = self.show_sidebar, - nav_menus = self.menus, + show_sidebar = self.show_sidebar, + nav_menus = self.menus, title = self.title(), infotext = self.infotext, + robots = self.robots, + top_filter = self.top_filter, + header_sub_nav = self.header_sub_nav, **self.render_params).render() return res @@ -123,7 +151,7 @@ def build_listing(self, num, after, reverse, count): def content(self): """Renderable object which will end up as content of the render_cls""" return self.listing_obj - + def query(self): """Query to execute to generate the listing""" raise NotImplementedError @@ -158,7 +186,7 @@ def listing(self): def title(self): """Page """ - return c.site.title + ": " + _(self.title_text) + return "%s - %s" % (self.title_text, c.site.title) def rightbox(self): """Contents of the right box when rendering""" @@ -199,7 +227,7 @@ def listing(self): if self.after and self.after._hot == 0: self.abort404() - #don't draw next/prev links for + #don't draw next/prev links for if listing.things: if listing.things[-1]._hot == 0: listing.next = None @@ -266,7 +294,7 @@ class SavedController(ListingController): title_text = _('Saved') def query(self): - return queries.get_saved(c.user) + return queries.get_saved(c.user, not c.user_is_admin) @validate(VUser()) def GET_listing(self, **env): @@ -294,35 +322,26 @@ def title(self): def GET_listing(self, **env): return ListingController.GET_listing(self, **env) -# Controller for '/' depending on the subreddit -class RootController(ListingController): - - def __before__(self): - ListingController.__before__(self) - controller = self.link_listings(c.site.default_listing) - self.__class__ = controller +# This used to be RootController, but renamed since there is a new root controller +class PromotedController(ListingController): + def __before__(self): + ListingController.__before__(self) + controller = self.link_listings(c.site.default_listing) + self.__class__ = controller class NewController(ListingController): where = 'new' - title_text = _('Newest submissions') - - @property - def menus(self): - return [NewMenu(default = self.sort)] + title_text = _('Newest Submissions') def query(self): - if self.sort == 'rising': - return get_rising(c.site) - else: - return c.site.get_links('new', 'all') - - @validate(sort = VMenu('controller', NewMenu)) - def GET_listing(self, sort, **env): - self.sort = sort + return c.site.get_links('new', 'all') + + def GET_listing(self, **env): return ListingController.GET_listing(self, **env) class RecentpostsController(NewController): where = 'recentposts' + title_text = _('Recent Posts') @staticmethod def builder_wrapper(thing): @@ -341,41 +360,56 @@ def GET_listing(self, **env): env['limit'] = 250 return NewController.GET_listing(self, **env) - class TagController(ListingController): where = 'tag' - title_text = _('Articles tagged') + title_text = _('Articles Tagged') + + @property + def menus(self): + return [TagSortMenu(default = self.sort)] def query(self): q = LinkTag._query(LinkTag.c._thing2_id == self._tag._id, LinkTag.c._name == 'tag', LinkTag.c._t1_deleted == False, - sort = desc('_date'), + sort = TagSortMenu.operator(self.sort), eager_load = True, thing_data = not g.use_query_cache ) q.prewrap_fn = lambda x: x._thing1 return q - - @validate(tag = VTagByName('tag')) - def GET_listing(self, tag, **env): + + def builder(self): + b = SubredditTagBuilder(self.query_obj, + num = self.num, + skip = self.skip, + after = self.after, + count = self.count, + reverse = self.reverse, + wrap = self.builder_wrapper, + sr_ids = [c.current_or_default_sr._id]) + return b + + @validate(tag = VTagByName('tag'), sort = VMenu('where', TagSortMenu)) + def GET_listing(self, tag, sort, **env): self._tag = tag - TagController.title_text = _('Articles tagged ' + tag.name) + self.sort = sort + TagController.title_text = _('Articles Tagged') + u' \N{LEFT SINGLE QUOTATION MARK}' + unicode(tag.name) + u'\N{RIGHT SINGLE QUOTATION MARK}' return ListingController.GET_listing(self, **env) class BrowseController(ListingController): where = 'browse' @property - def menus(self): - return [ControversyTimeMenu(default = self.time)] - + def top_filter(self): + return TimeMenu(default = self.time, title = _('Filter'), type='dropdown2') + def query(self): return c.site.get_links(self.sort, self.time) # TODO: this is a hack with sort. @validate(sort = VOneOf('sort', ('top', 'controversial')), - time = VMenu('where', ControversyTimeMenu)) + time = VMenu('where', TimeMenu)) def GET_listing(self, sort, time, **env): self.sort = sort if sort == 'top': @@ -400,11 +434,29 @@ def query(self): if isinstance(links, Query): links._limit = 200 links = [x._fullname for x in links] - + random.shuffle(links) return links +class EditsController(ListingController): + title_text = _('Recent Edits') + + def query(self): + return Edit._query(sort = desc('_date')) + + +class MeetupslistingController(ListingController): + title_text = _('Upcoming Meetups') + render_cls = MeetupIndexPage + + @property + def header_sub_nav(self): + return [] + + def query(self): + return Meetup.upcoming_meetups_by_timestamp() + class ByIDController(ListingController): title_text = _('API') @@ -422,14 +474,14 @@ def GET_listing(self, names, **env): class RecommendedController(ListingController): where = 'recommended' title_text = _('Recommended for you') - + @property def menus(self): return [RecSortMenu(default = self.sort)] - + def query(self): return get_recommended(c.user._id, sort = self.sort) - + @validate(VUser(), sort = VMenu("controller", RecSortMenu)) def GET_listing(self, sort, **env): @@ -442,15 +494,15 @@ class UserController(ListingController): show_nums = False def title(self): - titles = {'overview': _("Overview for %(user)s"), - 'comments': _("Comments by %(user)s"), - 'submitted': _("Submitted by %(user)s"), - 'liked': _("Liked by %(user)s"), - 'disliked': _("Disliked by %(user)s"), - 'hidden': _("Hidden by %(user)s"), - 'drafts': _("Drafts for %(user)s")} - title = titles.get(self.where, _('Profile for %(user)s')) \ - % dict(user = self.vuser.name, site = c.site.name) + titles = {'overview': _("Overview for %(user)s - %(site)s"), + 'comments': _("Comments by %(user)s - %(site)s"), + 'submitted': _("Submitted by %(user)s - %(site)s"), + 'liked': _("Liked by %(user)s - %(site)s"), + 'disliked': _("Disliked by %(user)s - %(site)s"), + 'hidden': _("Hidden by %(user)s - %(site)s"), + 'drafts': _("Drafts for %(user)s - %(site)s")} + title = titles.get(self.where, _('Profile for %(user)s - %(site)s')) \ + % dict(user = _force_unicode(self.vuser.name), site = c.site.title) return title def query(self): @@ -470,12 +522,12 @@ def query(self): elif self.where in ('liked', 'disliked'): self.check_modified(self.vuser, self.where) if self.where == 'liked': - q = queries.get_liked(self.vuser) + q = queries.get_liked(self.vuser, not c.user_is_admin) else: - q = queries.get_disliked(self.vuser) + q = queries.get_disliked(self.vuser, not c.user_is_admin) elif self.where == 'hidden': - q = queries.get_hidden(self.vuser) + q = queries.get_hidden(self.vuser, not c.user_is_admin) elif self.where == 'drafts': q = queries.get_drafts(self.vuser) @@ -486,7 +538,7 @@ def query(self): if q is None: return self.abort404() - return q + return q @validate(vuser = VExistingUname('username')) def GET_listing(self, where, vuser, **env): @@ -496,8 +548,12 @@ def GET_listing(self, where, vuser, **env): if not vuser: return self.abort404() + # pretend deleted users don't exist (although they are in the db still) + if vuser._deleted: + return self.abort404() + # hide spammers profile pages - if (not c.user_is_loggedin or + if (not c.user_is_loggedin or (c.user._id != vuser._id and not c.user_is_admin)) \ and vuser._spam: return self.abort404() @@ -507,7 +563,7 @@ def GET_listing(self, where, vuser, **env): return self.abort404() check_cheating('user') - + self.vuser = vuser self.render_params = {'user' : vuser} c.profilepage = True @@ -516,11 +572,13 @@ def GET_listing(self, where, vuser, **env): class MessageController(ListingController): - show_sidebar = False + show_sidebar = True render_cls = MessagePage - def title(self): - return _('Messages') + ': ' + _(self.where) + def title(self, where = None): + if where is None: + where = self.where + return "%s: %s - %s" % (_('Messages'), _(where.title()), c.site.title) @staticmethod def builder_wrapper(thing): @@ -530,7 +588,7 @@ def builder_wrapper(thing): w = Wrapped(thing) w.render_class = Message w.to_id = c.user._id - w.subject = 'comment reply' + w.subject = _('Comment Reply') w.was_comment = True w.permalink, w._fullname = p, f return w @@ -565,11 +623,11 @@ def GET_listing(self, where, **env): def GET_compose(self, to, subject, message, success): captcha = Captcha() if c.user.needs_captcha() else None content = MessageCompose(to = to, subject = subject, - captcha = captcha, + captcha = captcha, message = message, success = success) - return MessagePage(content = content).render() - + return MessagePage(content = content, title = self.title('compose')).render() + class RedditsController(ListingController): render_cls = SubredditsPage @@ -590,7 +648,7 @@ def query(self): reddits._filter(Subreddit.c.lang == c.content_langs) if not c.over18: reddits._filter(Subreddit.c.over_18 == False) - + return reddits def GET_listing(self, where, **env): self.where = where @@ -635,7 +693,7 @@ def content(self): stack.append(InfoBar(message=message)) stack.append(self.listing_obj) - + return stack @validate(VUser()) @@ -645,15 +703,33 @@ def GET_listing(self, where, **env): class CommentsController(ListingController): title_text = _('Comments') + builder_cls = UnbannedCommentBuilder + + @property + def header_sub_nav(self): + return [NamedButton("newcomments", dest="comments"), NamedButton("topcomments")] def query(self): q = Comment._query(Comment.c._spam == (True,False), - sort = desc('_date')) + Comment.c.sr_id == c.current_or_default_sr._id, + sort = desc('_date'), data = True) if not c.user_is_admin: q._filter(Comment.c._spam == False) return q + def builder(self): + b = self.builder_cls(self.query_obj, + num = self.num, + skip = self.skip, + after = self.after, + count = self.count, + reverse = self.reverse, + wrap = self.builder_wrapper, + sr_ids = [c.current_or_default_sr._id]) + return b + + def content(self): ps = PaneStack() ps.append(CommentReplyBox()) @@ -665,3 +741,28 @@ def GET_listing(self, **env): if not env.has_key('limit'): env['limit'] = 2 * c.user.pref_numsites return ListingController.GET_listing(self, **env) + +class TopcommentsController(CommentsController): + title_text = _('Top Comments') + builder_cls = UnbannedCommentBuilder + + def query(self): + q = Comment._query(Comment.c._spam == (True,False), + Comment.c.sr_id == c.current_or_default_sr._id, + sort = desc('_ups'), data = True) + if not c.user_is_admin: + q._filter(Comment.c._spam == False) + + if self.time != 'all': + q._filter(queries.db_times[self.time]) + + return q + + @property + def top_filter(self): + return TimeMenu(default = self.time, title = _('Filter'), type='dropdown2') + + @validate(time = VMenu('where', TimeMenu)) + def GET_listing(self, time, **env): + self.time = time + return CommentsController.GET_listing(self, **env) diff --git a/r2/r2/controllers/meetupscontroller.py b/r2/r2/controllers/meetupscontroller.py new file mode 100644 index 00000000..2d324067 --- /dev/null +++ b/r2/r2/controllers/meetupscontroller.py @@ -0,0 +1,214 @@ +from reddit_base import RedditController +from r2.lib.pages import BoringPage, ShowMeetup, NewMeetup, EditMeetup, PaneStack, CommentListing, LinkInfoPage, CommentReplyBox, NotEnoughKarmaToPost +from validator import validate, VUser, VModhash, VRequired, VMeetup, VEditMeetup, VFloat, ValueOrBlank, ValidIP, VMenu, VCreateMeetup, VTimestamp +from errors import errors +from r2.lib.jsonresponse import Json +from routes.util import url_for +from r2.models import Meetup,Link,Subreddit,CommentBuilder +from r2.models.listing import NestedListing +from r2.lib.menus import CommentSortMenu,NumCommentsMenu +from r2.lib.filters import python_websafe +from mako.template import Template +from pylons.i18n import _ +from pylons import c,g,request +import json + +def meetup_article_text(meetup): + t = Template(filename="r2/templates/showmeetup.html", output_encoding='utf-8', encoding_errors='replace') + res = t.get_def("meetup_info").render_unicode(meetup=meetup) + + url = url_for(controller='meetups',action='show',id=meetup._id36) + title = python_websafe(meetup.title) + hdr = u"<h2>Discussion article for the meetup : <a href='%s'>%s</a></h2>"%(url,title) + return hdr+res+hdr + +def meetup_article_title(meetup): + return "Meetup : %s"%meetup.title + +class MeetupsController(RedditController): + def response_func(self, **kw): + return self.sendstring(json.dumps(kw)) + + @validate(VUser(), + VCreateMeetup(), + title = ValueOrBlank('title'), + description = ValueOrBlank('description'), + location = ValueOrBlank('location'), + latitude = ValueOrBlank('latitude'), + longitude = ValueOrBlank('longitude'), + timestamp = ValueOrBlank('timestamp'), + tzoffset = ValueOrBlank('tzoffset')) + def GET_new(self, *a, **kw): + return BoringPage(pagename = 'New Meetup', content = NewMeetup(*a, **kw)).render() + + @Json + @validate(VUser(), + VCreateMeetup(), + VModhash(), + ip = ValidIP(), + title = VRequired('title', errors.NO_TITLE), + description = VRequired('description', errors.NO_DESCRIPTION), + location = VRequired('location', errors.NO_LOCATION), + latitude = VFloat('latitude', error=errors.NO_LOCATION), + longitude = VFloat('longitude', error=errors.NO_LOCATION), + timestamp = VTimestamp('timestamp'), + tzoffset = VFloat('tzoffset', error=errors.INVALID_DATE)) + def POST_create(self, res, title, description, location, latitude, longitude, timestamp, tzoffset, ip): + if res._chk_error(errors.NO_TITLE): + res._chk_error(errors.TITLE_TOO_LONG) + res._focus('title') + + res._chk_errors((errors.NO_LOCATION, + errors.NO_DESCRIPTION, + errors.INVALID_DATE, + errors.NO_DATE)) + + if res.error: return + + meetup = Meetup( + author_id = c.user._id, + + title = title, + description = description, + + location = location, + latitude = latitude, + longitude = longitude, + + timestamp = timestamp, + tzoffset = tzoffset + ) + + # Expire all meetups in the render cache + g.rendercache.invalidate_key_group(Meetup.group_cache_key()) + + meetup._commit() + + l = Link._submit(meetup_article_title(meetup), meetup_article_text(meetup), + c.user, Subreddit._by_name('discussion'),ip, []) + + l.meetup = meetup._id36 + l._commit() + meetup.assoc_link = l._id + meetup._commit() + + #update the queries + if g.write_query_queue: + queries.new_link(l) + + res._redirect(url_for(action='show', id=meetup._id36)) + + @Json + @validate(VUser(), + VModhash(), + meetup = VEditMeetup('id'), + title = VRequired('title', errors.NO_TITLE), + description = VRequired('description', errors.NO_DESCRIPTION), + location = VRequired('location', errors.NO_LOCATION), + latitude = VFloat('latitude', error=errors.NO_LOCATION), + longitude = VFloat('longitude', error=errors.NO_LOCATION), + timestamp = VTimestamp('timestamp'), + tzoffset = VFloat('tzoffset', error=errors.INVALID_DATE)) + def POST_update(self, res, meetup, title, description, location, latitude, longitude, timestamp, tzoffset): + if res._chk_error(errors.NO_TITLE): + res._chk_error(errors.TITLE_TOO_LONG) + res._focus('title') + + res._chk_errors((errors.NO_LOCATION, + errors.NO_DESCRIPTION, + errors.INVALID_DATE, + errors.NO_DATE)) + + if res.error: return + + meetup.title = title + meetup.description = description + + meetup.location = location + meetup.latitude = latitude + meetup.longitude = longitude + + meetup.timestamp = timestamp + meetup.tzoffset = tzoffset + + # Expire all meetups in the render cache + g.rendercache.invalidate_key_group(Meetup.group_cache_key()) + + meetup._commit() + + # Update the linked article + article = Link._byID(meetup.assoc_link) + article._load() + article_old_url = article.url + article.title = meetup_article_title(meetup) + article.article = meetup_article_text(meetup) + article._commit() + article.update_url_cache(article_old_url) + + + res._redirect(url_for(action='show', id=meetup._id36)) + + @validate(VUser(), + meetup = VEditMeetup('id')) + def GET_edit(self, meetup): + return BoringPage(pagename = 'Edit Meetup', content = EditMeetup(meetup, + title=meetup.title, + description=meetup.description, + location=meetup.location, + latitude=meetup.latitude, + longitude=meetup.longitude, + timestamp=int(meetup.timestamp * 1000), + tzoffset=meetup.tzoffset)).render() + + # Show a meetup. Most of this code was coped from GET_comments in front.py + @validate(meetup = VMeetup('id'), + sort = VMenu('controller', CommentSortMenu), + num_comments = VMenu('controller', NumCommentsMenu)) + def GET_show(self, meetup, sort, num_comments): + article = Link._byID(meetup.assoc_link) + + # figure out number to show based on the menu + user_num = c.user.pref_num_comments or g.num_comments + num = g.max_comments if num_comments == 'true' else user_num + + builder = CommentBuilder(article, CommentSortMenu.operator(sort), None, None) + listing = NestedListing(builder, num=num, parent_name = article._fullname) + displayPane = PaneStack() + + # insert reply box only for logged in user + if c.user_is_loggedin: + displayPane.append(CommentReplyBox()) + displayPane.append(CommentReplyBox(link_name = + article._fullname)) + + # finally add the comment listing + displayPane.append(listing.listing()) + + sort_menu = CommentSortMenu(default = sort, type='dropdown2') + nav_menus = [sort_menu, + NumCommentsMenu(article.num_comments, + default=num_comments)] + + content = CommentListing( + content = displayPane, + num_comments = article.num_comments, + nav_menus = nav_menus, + ) + + + # Update last viewed time, and return the previous last viewed time. Actually tracked on the article + lastViewed = None + if c.user_is_loggedin: + clicked = article._getLastClickTime(c.user) + lastViewed = clicked._date if clicked else None + article._click(c.user) + + res = ShowMeetup(meetup = meetup, content = content, + fullname=article._fullname, + lastViewed = lastViewed) + + return BoringPage(pagename = meetup.title, + content = res, + body_class = 'meetup').render() + + diff --git a/r2/r2/controllers/post.py b/r2/r2/controllers/post.py index 581384f5..629a5a02 100644 --- a/r2/r2/controllers/post.py +++ b/r2/r2/controllers/post.py @@ -26,7 +26,7 @@ from pylons import request, c, g from validator import * from pylons.i18n import _ -import sha +import hashlib def to_referer(func, **params): def _to_referer(self, *a, **kw): @@ -53,8 +53,7 @@ def response_func(self, **kw): def set_options(self, all_langs, pref_lang, **kw): if c.errors.errors: - print "fucker" - raise "broken" + raise "Options are invalid" if all_langs == 'all': langs = 'all' @@ -80,13 +79,15 @@ def set_options(self, all_langs, pref_lang, **kw): c.user._commit() - @validate(pref_lang = VLang('lang'), - all_langs = nop('all-langs', default = 'all')) - def POST_unlogged_options(self, all_langs, pref_lang): - self.set_options( all_langs, pref_lang) - return self.redirect(request.referer) + # @validate(pref_lang = VLang('lang'), + # all_langs = nop('all-langs', default = 'all')) + # def POST_unlogged_options(self, all_langs, pref_lang): + # self.set_options( all_langs, pref_lang) + # return self.redirect(request.referer) - @validate(pref_public_votes = VBoolean('public_votes'), + @validate(VModhash(), + pref_public_votes = VBoolean('public_votes'), + pref_kibitz = VBoolean('kibitz'), pref_hide_ups = VBoolean('hide_ups'), pref_hide_downs = VBoolean('hide_downs'), pref_numsites = VInt('numsites', 1, 100), @@ -95,14 +96,21 @@ def POST_unlogged_options(self, all_langs, pref_lang): pref_min_comment_score = VInt('min_comment_score', -100, 100), pref_num_comments = VInt('num_comments', 1, g.max_comments, default = g.num_comments), + pref_url = VUserWebsiteUrl('url'), + pref_location = VLocation('location'), all_langs = nop('all-langs', default = 'all')) def POST_options(self, all_langs, pref_lang, **kw): + errors = list(c.errors) + if errors: + return PrefsPage(content = PrefOptions(), infotext="Unable to save preferences").render() + self.set_options(all_langs, pref_lang, **kw) - u = UrlParser(c.site.path + "prefs") - u.update_query(done = 'true') - if c.cname: - u.put_in_frame() - return self.redirect(u.unparse()) + # Doesn't work when proxying to AWS + #u = UrlParser(c.site.path + "prefs") + #u.update_query(done = 'true') + #if c.cname: + # u.put_in_frame() + return self.redirect('/prefs?done=true') def GET_over18(self): return BoringPage(_("Over 18?"), @@ -117,7 +125,7 @@ def POST_over18(self, over18, uh, dest): c.user.pref_over_18 = True c.user._commit() else: - ip_hash = sha.new(request.ip).hexdigest() + ip_hash = hashlib.sha1(request.ip).hexdigest() domain = g.domain if not c.frameless_cname else None c.cookies.add('over18', ip_hash, domain = domain) diff --git a/r2/r2/controllers/reddit_base.py b/r2/r2/controllers/reddit_base.py index bb8fd152..4496a6c2 100644 --- a/r2/r2/controllers/reddit_base.py +++ b/r2/r2/controllers/reddit_base.py @@ -24,7 +24,7 @@ from pylons.controllers.util import abort, redirect_to from pylons.i18n import _ from pylons.i18n.translation import LanguageError -from r2.lib.base import BaseController, proxyurl +from r2.lib.base import BaseController, proxyurl, current_login_cookie from r2.lib import pages, utils, filters from r2.lib.utils import http_utils from r2.lib.cache import LocalCache @@ -41,7 +41,7 @@ from copy import copy from Cookie import CookieError from datetime import datetime -import sha, inspect, simplejson +import hashlib, inspect, simplejson from urllib import quote, unquote from r2.lib.tracking import encrypt, decrypt @@ -206,7 +206,7 @@ def over18(): else: if 'over18' in c.cookies: cookie = c.cookies['over18'].value - if cookie == sha.new(request.ip).hexdigest(): + if cookie == hashlib.sha1(request.ip).hexdigest(): return True def set_subreddit(): @@ -248,6 +248,12 @@ def set_subreddit(): if isinstance(c.site, FakeSubreddit): c.default_sr = True + try: + c.current_or_default_sr = Subreddit._by_name(g.default_sr) + except NotFound: + c.current_or_default_sr = None + else: + c.current_or_default_sr = c.site # check that the site is available: if c.site._spam and not c.user_is_admin and not c.error_page: @@ -379,9 +385,13 @@ def base_listing(fn): after = VByName('after'), before = VByName('before'), count = VCount('count')) - def new_fn(self, before, **env): + def new_fn(self, before, num, **env): kw = self.build_arg_list(fn, env) - + + # Multiply the number per page by the per page multiplier for the reddit + if num: + kw['num'] = c.site.posts_per_page_multiplier * num + #turn before into after/reverse kw['reverse'] = False if before: @@ -466,10 +476,7 @@ def pre(self): c.response_wrappers = [] c.errors = ErrorSet() c.firsttime = firsttime() - (c.user, maybe_admin) = \ - valid_cookie(c.cookies[g.login_cookie].value - if g.login_cookie in c.cookies - else '') + (c.user, maybe_admin) = valid_cookie(current_login_cookie()) if c.user: c.user_is_loggedin = True @@ -565,7 +572,7 @@ def post(self): if c.response_access_control: c.response.headers['Access-Control'] = c.response_access_control - if c.user_is_loggedin: + if c.user_is_loggedin and 'Cache-Control' not in response.headers: response.headers['Cache-Control'] = 'no-cache' response.headers['Pragma'] = 'no-cache' diff --git a/r2/r2/controllers/validator/validator.py b/r2/r2/controllers/validator/validator.py index 1039e3fd..2c72ef44 100644 --- a/r2/r2/controllers/validator/validator.py +++ b/r2/r2/controllers/validator/validator.py @@ -23,7 +23,7 @@ from pylons.i18n import _ from pylons.controllers.util import abort from r2.lib import utils, captcha -from r2.lib.filters import unkeep_space, websafe, _force_unicode +from r2.lib.filters import unkeep_space, websafe, _force_utf8, _force_ascii from r2.lib.db.operators import asc, desc from r2.config import cache from r2.lib.template_helpers import add_sr @@ -35,6 +35,7 @@ from copy import copy from datetime import datetime, timedelta +import pytz import re class Validator(object): @@ -115,6 +116,11 @@ def run(self, item): else: return item +class ValueOrBlank(Validator): + def run(self, value): + """Returns the value as is if present, else an empty string""" + return '' if value is None else value + class VLink(Validator): def __init__(self, param, redirect = True, *a, **kw): Validator.__init__(self, param, *a, **kw) @@ -131,16 +137,46 @@ def run(self, link_id): else: return None +class VMeetup(Validator): + def __init__(self, param, redirect = True, *a, **kw): + Validator.__init__(self, param, *a, **kw) + self.redirect = redirect + + def run(self, meetup_id36): + if meetup_id36: + try: + meetup_id = int(meetup_id36, 36) + return Meetup._byID(meetup_id, True) + except (NotFound, ValueError): + if self.redirect: + abort(404, 'page not found') + else: + return None + +class VEditMeetup(VMeetup): + def __init__(self, param, redirect = True, *a, **kw): + VMeetup.__init__(self, param, redirect = redirect, *a, **kw) + + def run(self, param): + meetup = VMeetup.run(self, param) + if meetup and not (c.user_is_loggedin and + meetup.can_edit(c.user, c.user_is_admin)): + abort(403, "forbidden") + return meetup + class VTagByName(Validator): def __init__(self, param, *a, **kw): Validator.__init__(self, param, *a, **kw) def run(self, name): if name: - try: - return Tag._by_name(name) - except NotFound: - abort(404, 'page not found') + cleaned = _force_ascii(name) + if cleaned == name: + try: + return Tag._by_name(cleaned) + except: + pass + abort(404, 'page not found') class VTags(Validator): comma_sep = re.compile('[,\s]+', re.UNICODE) @@ -152,7 +188,7 @@ def run(self, tag_field): tags = [] if tag_field: # Tags are comma delimited - tags = self.comma_sep.split(tag_field) + tags = [x for x in self.comma_sep.split(tag_field) if x==_force_ascii(x)] return tags class VMessage(Validator): @@ -176,16 +212,18 @@ def run(self, cid): class VCount(Validator): def run(self, count): - if count is None: + try: + count = int(count) + except (TypeError, ValueError): count = 0 - return max(int(count), 0) + return max(count, 0) class VLimit(Validator): def run(self, limit): if limit is None: return c.user.pref_numsites - return min(max(int(limit), 1), 100) + return min(max(int(limit), 1), 250) class VCssMeasure(Validator): measure = re.compile(r"^\s*[\d\.]+\w{0,3}\s*$") @@ -204,6 +242,23 @@ def chksrname(x): except UnicodeEncodeError: return None +class VLinkUrls(Validator): + "A comma-separated list of link urls" + splitter = re.compile('[ ,]+') + id_re = re.compile('^/lw/([^/]+)/') + def __init__(self, item, *a, **kw): + self.item = item + Validator.__init__(self, item, *a, **kw) + + def run(self, val): + res=[] + for v in self.splitter.split(val): + link_id = self.id_re.match(v) + if link_id: + l = VLink(None,False).run(link_id.group(1)) + if l: + res.append(l) + return res class VLinkFullnames(Validator): "A space- or comma-separated list of fullnames for Links" @@ -230,7 +285,8 @@ def __init__(self, item, length = 10000, def run(self, title): if not title: - c.errors.add(self.emp_error) + if self.emp_error is not None: + c.errors.add(self.emp_error) elif len(title) > self.length: c.errors.add(self.len_error) else: @@ -344,7 +400,13 @@ def run(self, password = None): class VModhash(Validator): default_param = 'uh' def run(self, uh): - pass + if not c.user_is_loggedin: + raise UserRequiredException + + if not c.user.valid_hash(uh): + g.log.info("Invalid hash on form submission : "+str(c.user)) + raise UserRequiredException + #abort(403, 'forbidden') class VVotehash(Validator): def run(self, vh, thing_name): @@ -396,9 +458,16 @@ def run(self, thing_name): class VSRSubmitPage(Validator): def run(self): - if not (c.default_sr or c.user_is_loggedin and - c.site.can_submit(c.user)): - abort(403, "forbidden") + if not (c.default_sr or c.user_is_loggedin and c.site.can_submit(c.user)): + return False + else: + return True + +class VCreateMeetup(Validator): + def run(self): + if (c.user_is_loggedin and c.user.safe_karma >= g.discussion_karma_to_post): + return True + abort(403, "forbidden") class VSubmitParent(Validator): def run(self, fullname): @@ -432,9 +501,10 @@ def run(self, sr_name): sr = None if sr and not (c.user_is_loggedin and sr.can_submit(c.user)): - abort(403, "forbidden") - else: - return sr + c.errors.add(errors.SUBREDDIT_FORBIDDEN) + sr = None + + return sr pass_rx = re.compile(r".{3,20}") @@ -462,13 +532,23 @@ def chkuser(x): except UnicodeEncodeError: return None +def whyuserbad(x): + if not x: + return errors.BAD_USERNAME_CHARS + if len(x)<3: + return errors.BAD_USERNAME_SHORT + if len(x)>20: + return errors.BAD_USERNAME_LONG + return errors.BAD_USERNAME_CHARS + class VUname(VRequired): def __init__(self, item, *a, **kw): VRequired.__init__(self, item, errors.BAD_USERNAME, *a, **kw) def run(self, user_name): + original_user_name = user_name; user_name = chkuser(user_name) if not user_name: - return self.error() + return self.error(whyuserbad(original_user_name)) else: try: a = Account._by_name(user_name, True) @@ -494,6 +574,14 @@ class VSanitizedUrl(Validator): def run(self, url): return utils.sanitize_url(url) +class VUserWebsiteUrl(VSanitizedUrl): + def run(self, url): + val = VSanitizedUrl.run(self, url) + if val is None: + return '' + else: + return val + class VUrl(VRequired): def __init__(self, item, *a, **kw): VRequired.__init__(self, item, errors.NO_URL, *a, **kw) @@ -528,11 +616,12 @@ class VExistingUname(VRequired): def __init__(self, item, *a, **kw): VRequired.__init__(self, item, errors.NO_USER, *a, **kw) - def run(self, name): - if name: + def run(self, username): + if username: try: + name = _force_utf8(username) return Account._by_name(name) - except NotFound: + except (TypeError, UnicodeEncodeError, NotFound): return self.error(errors.USER_DOESNT_EXIST) self.error() @@ -542,12 +631,37 @@ def run(self, name): if not user or not hasattr(user, 'email') or not user.email: return self.error(errors.NO_EMAIL_FOR_USER) return user - + +class VTimestamp(Validator): + def run(self, val): + if not val: + c.errors.add(errors.INVALID_DATE) + return + + try: + val = float(val) / 1000.0 + datetime.fromtimestamp(val, pytz.utc) # Check it can be converted to a datetime + return val + except ValueError: + c.errors.add(errors.INVALID_DATE) class VBoolean(Validator): def run(self, val): return val != "off" and bool(val) +class VLocation(VLength): + def __init__(self, item, length = 100, **kw): + VLength.__init__(self, item, length = length, + length_error = errors.LOCATION_TOO_LONG, + empty_error = None, **kw) + + def run(self, val): + val = VLength.run(self, val) + if val == None: + return '' + else: + return val + class VInt(Validator): def __init__(self, param, min=None, max=None, *a, **kw): self.min = min @@ -568,6 +682,28 @@ def run(self, val): except ValueError: c.errors.add(errors.BAD_NUMBER) +class VFloat(Validator): + def __init__(self, param, min=None, max=None, error=errors.BAD_NUMBER, *a, **kw): + self.min = min + self.max = max + self.error = error + Validator.__init__(self, param, *a, **kw) + + def run(self, val): + if not val: + c.errors.add(self.error) + return + + try: + val = float(val) + if self.min is not None and val < self.min: + val = self.min + elif self.max is not None and val > self.max: + val = self.max + return val + except ValueError: + c.errors.add(self.error) + class VCssName(Validator): """ returns a name iff it consists of alphanumeric characters and @@ -635,6 +771,10 @@ def run (self): def ratelimit(self, rate_user = False, rate_ip = False, prefix = "rate_"): to_set = {} seconds = g.RATELIMIT*60 + + if seconds <= 0: + return + expire_time = datetime.now(g.tz) + timedelta(seconds = seconds) if rate_user and c.user_is_loggedin: to_set['user' + str(c.user._id36)] = expire_time @@ -646,6 +786,8 @@ def ratelimit(self, rate_user = False, rate_ip = False, prefix = "rate_"): class VCommentIDs(Validator): #id_str is a comma separated list of id36's def run(self, id_str): + if not id_str: + return None cids = [int(i, 36) for i in id_str.split(',')] comments = Comment._byID(cids, data=True, return_dict = False) return comments diff --git a/r2/r2/controllers/wikipagecontroller.py b/r2/r2/controllers/wikipagecontroller.py new file mode 100644 index 00000000..bbb53ed7 --- /dev/null +++ b/r2/r2/controllers/wikipagecontroller.py @@ -0,0 +1,31 @@ +from validator import * +from reddit_base import RedditController +from r2.lib.pages import * +from wiki_pages_embed import allWikiPagesCached + +# Controller for pages pulled from wiki +class WikipageController(RedditController): + + # Get a full page with the wiki page embedded + @validate(skiplayout=VBoolean('skiplayout')) + def GET_wikipage(self,name,skiplayout): + p = allWikiPagesCached[name] + if skiplayout: + # Get just the html of the wiki page + html = WikiPageCached(p).html() + return WikiPageInline(html=html, name=name, skiplayout=skiplayout).render() + else: + return WikiPage(name,p,skiplayout=skiplayout).render() + + @validate(VUser(), + skiplayout=VBoolean('skiplayout')) + def POST_invalidate_cache(self, name, skiplayout): + p = allWikiPagesCached[name] + WikiPageCached(p).invalidate() + if p.has_key('route'): + if skiplayout: + return self.redirect('/wiki/'+p['route']+'?skiplayout=on') + else: + return self.redirect('/wiki/'+p['route']) + else: + return "Done" diff --git a/r2/r2/i18n/en/LC_MESSAGES/r2.mo b/r2/r2/i18n/en/LC_MESSAGES/r2.mo index 42dfbeed..1af88a6d 100644 Binary files a/r2/r2/i18n/en/LC_MESSAGES/r2.mo and b/r2/r2/i18n/en/LC_MESSAGES/r2.mo differ diff --git a/r2/r2/i18n/en/LC_MESSAGES/r2.po b/r2/r2/i18n/en/LC_MESSAGES/r2.po index 13d4c52c..f9abbcfa 100644 --- a/r2/r2/i18n/en/LC_MESSAGES/r2.po +++ b/r2/r2/i18n/en/LC_MESSAGES/r2.po @@ -1035,7 +1035,7 @@ msgstr "on" #: r2/templates/commentreplybox.html:26 r2/templates/frametoolbar.html:68 #: r2/templates/link.html:111 msgid "comment {verb}" -msgstr "comment" +msgstr "Comment" #: r2/templates/commentreplybox.html:33 msgid "cancel" @@ -1883,45 +1883,10 @@ msgstr "" #: randomstring:sadmessages msgid "Funny 500 page message 1" -msgstr "you've done enough damage, please don't hit reload" - -#: randomstring:sadmessages -msgid "Funny 500 page message 2" -msgstr "and i suppose you're perfect?" - -#: randomstring:sadmessages -msgid "Funny 500 page message 3" -msgstr "you know what they say, you get what you pay for..." - -#: randomstring:sadmessages -msgid "Funny 500 page message 4" msgstr "" -"Can you fix this? <a href='mailto:jobs@reddit.com'>jobs@reddit.com</a>." - -#: randomstring:sadmessages -msgid "Funny 500 page message 5" -msgstr "9/11 changed everything." - -#: randomstring:sadmessages -msgid "Funny 500 page message 6" -msgstr "Looks like today just isn't your day." - -#: randomstring:sadmessages -msgid "Funny 500 page message 7" -msgstr "Conde Nast got their money's worth." - -#: randomstring:sadmessages -msgid "Funny 500 page message 8" -msgstr "" -"Quick, <a href=\"http://reddit.com/store \">buy a shirt</a> and make up for this." - -#: randomstring:sadmessages -msgid "Funny 500 page message 9" -msgstr "The features don't write themselves, you know. " - -#: randomstring:sadmessages -msgid "Funny 500 page message 10" -msgstr "It's ok to cry." +"<p>You have encountered an error in the code that runs Less Wrong. The site maintainers have been informed and will get to it is as soon as they can.</p>" +"<p>In the unlikely event that you've bumped into this error before and think that no-one is paying attention, please report the error and how to reproduce it on <a href='http://code.google.com/p/lesswrong/issues/list'>http://code.google.com/p/lesswrong/issues/list'</a></p>" +"<p>If the error is localised you might still find awesome Less Wrong content in the <a href='http://lesswrong.com/promoted/'>Main article area</a> or in the <a href='http://lesswrong.com/r/discussion/'>Discussion area</a>.</p>" #: randomstring:create_reddit msgid "Reason to create a reddit 1" diff --git a/r2/r2/lib/app_globals.py b/r2/r2/lib/app_globals.py index d8306fd4..4b39439a 100644 --- a/r2/r2/lib/app_globals.py +++ b/r2/r2/lib/app_globals.py @@ -45,15 +45,28 @@ class Globals(object): 'num_query_queue_workers', 'max_sr_images', 'karma_to_post', + 'discussion_karma_to_post', + 'side_meetups_max_age', + 'side_comments_max_age', + 'side_posts_max_age', + 'side_tags_max_age', + 'side_contributors_max_age', + 'post_karma_multiplier', + 'article_navigation_max_age', + 'meetups_radius', ] bool_props = ['debug', 'translator', + 'sqlprinting', 'template_debug', 'uncompressedJS', 'enable_doquery', 'use_query_cache', 'write_query_queue', - 'css_killswitch'] + 'css_killswitch', + 'disable_captcha', + 'disable_tracking_js' + ] tuple_props = ['memcaches', 'rec_cache', @@ -144,7 +157,7 @@ def to_iter(name, delim = ','): if os.path.exists(static_files): for f in os.listdir(static_files): if f.endswith('.md5'): - key = f.strip('.md5') + key = f[0:-4] f = os.path.join(static_files, f) with open(f, 'r') as handle: md5 = handle.read().strip('\n') diff --git a/r2/r2/lib/base.py b/r2/r2/lib/base.py index 10bf73bc..0b2a28a2 100644 --- a/r2/r2/lib/base.py +++ b/r2/r2/lib/base.py @@ -25,7 +25,7 @@ from pylons.i18n import N_, _, ungettext, get_lang import r2.lib.helpers as h from r2.lib.utils import to_js -from r2.lib.filters import spaceCompress, _force_unicode +from r2.lib.filters import spaceCompress, _force_unicode, _force_utf8 from utils import storify, string2js, read_http_date import re, md5 @@ -67,7 +67,7 @@ def __call__(self, environ, start_response): request.get = storify(request.GET) request.post = storify(request.POST) request.referer = environ.get('HTTP_REFERER') - request.path = environ.get('PATH_INFO') + request.path = _force_utf8(environ.get('PATH_INFO')) # Enforce only valid utf8 chars in request path request.user_agent = environ.get('HTTP_USER_AGENT') request.fullpath = environ.get('FULLPATH', request.path) request.port = environ.get('request_port') @@ -188,7 +188,10 @@ def proxyurl(url): r = urllib2.Request(url, data, headers) content = embedopen.open(r).read() return content - + +def current_login_cookie(): + return c.cookies[g.login_cookie].value if (g.login_cookie in c.cookies) else '' + __all__ = [__name for __name in locals().keys() if not __name.startswith('_') \ or __name == '_'] diff --git a/r2/r2/lib/c/filters.c b/r2/r2/lib/c/filters.c index 3b59fb4a..525f8661 100644 --- a/r2/r2/lib/c/filters.c +++ b/r2/r2/lib/c/filters.c @@ -235,16 +235,15 @@ filters_uspace_compress(PyObject * self, PyObject *args) { c = command[ic]; if(gobble) { if(Py_UNICODE_ISSPACE(c)) { - while(Py_UNICODE_ISSPACE(command[++ic])); - c = command[ic]; + while(Py_UNICODE_ISSPACE(c)) { c = command[++ic]; } if(c != (Py_UNICODE)('<')) { buffer[ib++] = (Py_UNICODE)(' '); } } if(c == (Py_UNICODE)('>')) { buffer[ib++] = c; - while(Py_UNICODE_ISSPACE(command[++ic])); - c = command[ic]; + c = command[++ic]; + while(Py_UNICODE_ISSPACE(c)) { c = command[++ic]; } } if (len - ic >= MD_START_LEN && memcmp(&command[ic], MD_START_U, diff --git a/r2/r2/lib/cache.py b/r2/r2/lib/cache.py index 4f52e812..98544b84 100644 --- a/r2/r2/lib/cache.py +++ b/r2/r2/lib/cache.py @@ -49,6 +49,17 @@ def get_multi(self, keys, prefix='', partial=True): return dict((key_map[k], r[k]) for k in r.keys()) + def get_key_group_value(self, prefix): + """ This is used to allow the expiring of a group of keys + Use the value returned by "get_key_group_value" in all cache + keys for items you want to expire together """ + return self.get(prefix, 0) + + def invalidate_key_group(self, prefix): + """ Expire a group of keys - use together with get_key_group_value """ + self.add(prefix, 0) + self.incr(prefix) + class Memcache(CacheUtils, memcache.Client): simple_get_multi = memcache.Client.get_multi diff --git a/r2/r2/lib/captcha.py b/r2/r2/lib/captcha.py index 8a36d7b2..86d51f5e 100644 --- a/r2/r2/lib/captcha.py +++ b/r2/r2/lib/captcha.py @@ -24,6 +24,7 @@ from r2.config import cache from Captcha.Base import randomIdentifier from Captcha.Visual import Text, Backgrounds, Distortions, ImageCaptcha +from pylons import g IDEN_LENGTH = 32 SOL_LENGTH = 6 @@ -52,10 +53,12 @@ def get_image(iden): if not solution: solution = make_solution() cache.set(str(iden), solution, time = 300) - g = RandCaptcha(solution=solution) - return g.render() + r = RandCaptcha(solution=solution) + return r.render() def valid_solution(iden, solution): + if getattr(g,'disable_captcha', False): + return True if (not iden or not solution or len(iden) != IDEN_LENGTH diff --git a/r2/r2/lib/comment_tree.py b/r2/r2/lib/comment_tree.py index e85b3e3f..8bdd0c52 100644 --- a/r2/r2/lib/comment_tree.py +++ b/r2/r2/lib/comment_tree.py @@ -42,40 +42,44 @@ def add_comment_nolock(comment): cids, comment_tree, depth, num_children = link_comments(link_id) - #add to comment list - cids.append(comment._id) - - #add to tree - comment_tree.setdefault(p_id, []).append(cm_id) - - #add to depth - depth[cm_id] = depth[p_id] + 1 if p_id else 0 - - #update children - num_children[cm_id] = 0 - - #dfs to find the list of parents for the new comment - def find_parents(): - stack = [cid for cid in comment_tree[None]] - parents = [] - while stack: - cur_cm = stack.pop() - if cur_cm == cm_id: - return parents - elif comment_tree.has_key(cur_cm): - #make cur_cm the end of the parents list - parents = parents[:depth[cur_cm]] + [cur_cm] - for child in comment_tree[cur_cm]: - stack.append(child) - - - #if this comment had a parent, find the parent's parents - if p_id: - for p_id in find_parents(): - num_children[p_id] += 1 - - g.permacache.set(comments_key(link_id), - (cids, comment_tree, depth, num_children)) + # Only add this comment if it is not already present. In + # link_comments a cache miss will rebuild all comments including + # the one we are adding now. + if comment._id not in cids: + #add to comment list + cids.append(comment._id) + + #add to tree + comment_tree.setdefault(p_id, []).append(cm_id) + + #add to depth + depth[cm_id] = depth[p_id] + 1 if p_id else 0 + + #update children + num_children[cm_id] = 0 + + #dfs to find the list of parents for the new comment + def find_parents(): + stack = [cid for cid in comment_tree[None]] + parents = [] + while stack: + cur_cm = stack.pop() + if cur_cm == cm_id: + return parents + elif comment_tree.has_key(cur_cm): + #make cur_cm the end of the parents list + parents = parents[:depth[cur_cm]] + [cur_cm] + for child in comment_tree[cur_cm]: + stack.append(child) + + + #if this comment had a parent, find the parent's parents + if p_id: + for p_id in find_parents(): + num_children[p_id] += 1 + + g.permacache.set(comments_key(link_id), + (cids, comment_tree, depth, num_children)) def delete_comment(comment): #nothing really to do here, atm diff --git a/r2/r2/lib/contrib/jsjam b/r2/r2/lib/contrib/jsjam deleted file mode 100755 index 55a85eff..00000000 --- a/r2/r2/lib/contrib/jsjam +++ /dev/null @@ -1,476 +0,0 @@ -#!/usr/bin/perl -wT -use strict; -# -# Copyright 1998-2005 Eric Hammond <ehammond@thinksome.com> -# - -#---- Setup - -BEGIN { # Set envariables for -T tainting. - $ENV{'PATH'} = '/bin:/usr/bin'; - delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'}; -} -BEGIN { # Extract path and program name. - use vars qw($path $prog); - $0 =~ m%(.*)[/\\]([^/\\]*)%; - ($path, $prog) = ($1 || '.', $2 || $0); -} - -#---- Packages - -use Getopt::Long; -use sigtrap qw(die normal-signals); -use IO::File; -#use POSIX; - -#---- Constants - -# Version is extracted from CVS/RCS revision. -my $REVISION = '$Revision: 1.14 $'; -use vars qw($VERSION); -($VERSION = $REVISION) =~ s%^\$.evision: (.*?) \$$%$1%; - -# Reserved keywords and other identifiers as specified in -# "JavaScript: The Definitive Guide" by David Flanagan -# Plus a few additional things like standard object/method names. -my %RESERVED = map { $_ => 1 } - qw( - anchor area array boolean button checkbox date document element - fileupload form frame function hidden history image javaarray - javaclass javaobject javapackage link location math mimetype - navigator number object option packages password plugin radio - reset select string submit text textarea window abstract alert - assign blur boolean break byte case catch char class cleartimeout - close closed confirm const continue default defaultstatus delete - do document double else escape eval extends false final finally - float focus for frames function getclass goto history if - implements import in instanceof int interface isnan java length - location long name native navigate navigator netscape new null - onblur onerror onfocus onload onunload open opener package parent - parsefloat parseint private prompt protected prototype public ref - return scroll self settimeout short static status sun super - switch synchronized taint this throw throws tostring top - transient true try typeof unescape untaint valueof var void while - window with - - anchor applet area array boolean button checkbox date document e - element embed fileupload form frame function hidden history image - jsobject javaarray javaclass javamethod javaobject javapackage - ln10 ln2 log10e log2e link location max_value min_value math - mimetype negative_infinity nan navigator number object option pi - positive_infinity packages password plugin radio reset sqrt1_2 - sqrt2 select string submit text textarea url utc window abs acos - action alert alinkcolor anchor anchors appcodename appname - appversion applets arguments asin assign atan atan2 back bgcolor - big blink blur bold border call caller ceil charat checked clear - cleartimeout click close closed complete confirm constructor - cookie cos current defaultchecked defaultselected defaultstatus - defaultvalue description document domain elements embeds - enabledplugin encoding escape eval exp fgcolor filename fixed - floor focus fontcolor fontsize form forms forward frames getclass - getdate getday gethours getmember getminutes getmonth getseconds - getslot gettime gettimezoneoffset getwindow getyear go hash - height history host hostname href hspace images index indexof - isnan italics java javaenabled join lastindexof lastmodified - length link linkcolor links location log lowsrc max method - mimetypes min name navigate navigator netscape next onabort - onblur onchange onclick onerror onfocus onload onmouseout - onmouseover onreset onsubmit onunload open opener options parent - parse parsefloat parseint pathname plugins port pow previous - prompt protocol prototype random referrer refresh reload - removemember replace reset reverse round scroll search select - selected selectedindex self setdate sethours setmember setminutes - setmonth setseconds setslot settime settimeout setyear sin small - sort split sqrt src status strike sub submit substring suffixes - sun sup taint taintenabled tan target text title togmtstring - tolocalestring tolowercase tostring touppercase top type unescape - untaint useragent value valueof vlinkcolor vspace width window - write writeln - - fromcharcode all screen classname innertext - ); - -#---- Options - -use vars qw($debug); -$debug = 0; -my $help = 0; -use vars qw($quiet); -$quiet = 0; -my $version = 0; -my $keep_identifiers = 0; -my $keep_globals = 0; -my $keep_whitespace = 0; -my $keep_newlines = 0; -my $keep_comments = 0; -my $add_note = undef; - -Getopt::Long::config('no_ignore_case'); -GetOptions( - 'debug' => \$debug, - 'help' => \$help, - 'quiet' => \$quiet, - 'version' => \$version, - - 'keep-identifiers' => \$keep_identifiers, - 'i' => \$keep_identifiers, - - 'keep-globals' => \$keep_globals, - 'g' => \$keep_globals, - - 'keep-whitespace' => \$keep_whitespace, - 'w' => \$keep_whitespace, - - 'keep-newlines' => \$keep_newlines, - 'n' => \$keep_newlines, - - 'keep-comments' => \$keep_comments, - 'c' => \$keep_comments, - - 'add-note:s' => \$add_note, - 'a:s' => \$add_note, - ) - or die_usage(); - -#---- Initialization - -# Don't buffer output. -STDOUT->autoflush(1); -STDERR->autoflush(1); - -#---- Main - -$quiet or warn "$prog v${VERSION}a\n"; -die_usage() if $help; -exit 0 if $version; - -# Can't keep comments without keeping newlines -$keep_newlines = 1 if $keep_comments; - -# Use stdin if no files specified. -unshift(@ARGV, '-') unless scalar @ARGV; - -# For each file... -my $filename; -while ( $filename = shift ) { - $debug and warn "$main::prog: Processing $filename\n"; - - local($/) = undef; - open(FILE, "< $filename") - or die "$main::prog: Unable to open: $filename: $!"; - my $contents = <FILE>; - close(FILE); - - $contents = jam($contents); - - print $contents; -} - -exit 0; - -#---- Functions - -# -# jam - compress the code. -# -sub jam { - my ($contents) = @_; - - # Identifiers which should not be shortened are indicated in the code using: - # //jsjam-keep:identifier - # where "identifier" is the identifier which should not be shortened. - while ( $contents =~ s%//\s*jsjam-keep\s*:\s*(\w+).*\n%\n% ) { - my $word = "\L$1\E"; - ++ $RESERVED{$word}; - } - - # Regexp for non-greedy stuff, counting quoted strings as opaque chunks. - # Also count /.*/ regular expressions when they follow one of: = ( , - my $stuff_with_strings = <<'EOM'; - (?: - <!-- .*? --> - | - [=(,] \s* / (?: \\/ | [^/] )* / \w* - | - [^"']+? - | - " (?: \\" | [^"] )* " - | - ' (?: \\' | [^'] )* ' - )*? -EOM - - my $starts_with_comment = $1 if $contents =~ s%^(<!--.*?\n)%%; - - if ( not $keep_comments ) { - # Remove comments. - $debug and warn "$main::prog: Removing comments\n"; - # Remove comments as long as the comments are not quoted. - # $source = the remainder of the document to process - my $source=$contents; - my $result=""; - - while(length($source)>1){ - # match \" or \' or "" or '' - # and copy to result - $source =~ s/^(\\"|\\'|""|'')// && do { - $result.=$1; - next; - }; - - # match ".." or '..' - # and copy quoted text to result - $source =~ s/^(".*?[^\\]"|'.*?[^\\]')// && do { - $result.=$1; - next; - }; - - # match // - # remove text to end of line - $source =~ s/^\/\/.*?\n// && do { - next; - }; - - # match /* - # remove text to */ - $source =~ s/^\/\*.*?\*\///s && do { - next; - }; - - # match string before /* or // or \" or \' or " or ' - $source =~ s/^(.+?)(\/\*|\/\/|\\"|\\'|"|')/$2/s && do { - $result.=$1; - next; - }; - - # Copy remainder of input. - $result.=$source; - last; - } - $contents=$result; - } - - if ( not $keep_identifiers ) { - my $new_contents = ''; - - if ( not $keep_globals ) { - # Shorten all identifiers. - $debug and warn "$main::prog: Shortening all identifiers\n"; - while ( $contents =~ s%^($stuff_with_strings)\b([_A-Za-z]\w*)%%sx ) { - $new_contents .= $1.word($2); - } - - } else { - # Shorten var identifiers only (TBD: may conflict with globals). - $debug and warn "$main::prog: Shortening 'var' identifiers\n"; - while ($contents =~ s%^($stuff_with_strings\bvar\s+)([_A-Za-z]\w*)%%sx) { - $new_contents .= $1.word($2); - } - } - $contents = $new_contents . $contents; - } - - if ( not $keep_whitespace ) { - # Remove blank lines. - $debug and warn "$main::prog: Removing blank lines\n"; - $contents =~ s%^(\s*\n)%%gm; - - # Compress whitespace. - my $new_contents = ''; - - if ( not $keep_newlines ) { - $debug and warn "$main::prog: Compressing whitespace\n"; - while ( $contents =~ s%^($stuff_with_strings)\s+%%sx ) { - $new_contents .= $1.' '; - } - - } else { - $debug and warn "$main::prog: Compressing non-newline whitespace\n"; - while ( $contents =~ s%^($stuff_with_strings)([\ \t]|$)+%%sx ) { - $new_contents .= $1.' '; - last if $contents =~ m%^\s*$%; - } - - } - $contents = $new_contents . $contents; - - # Remove whitespace which has punctuation on one side. - $new_contents = ''; - $debug and warn "$main::prog: Removing unneeded whitespace\n"; - - my $re_space = $keep_newlines ? '[ \t]' : '\s'; - while ( $contents =~ s%^($stuff_with_strings)$re_space(\S|$)%$2%sx ) { - my ($stuff, $after) = ($1, $2); - my $before = $1 if $stuff =~ m%(.)$%; - next unless defined $before; - $new_contents .= $stuff; - if ( $before =~ m%[\w'"@]% and - $after =~ m%[\w'"@]% ) { - $new_contents .= ' '; - } - } - $contents = $new_contents . $contents; - - # Remove leading/trailing whitespace. - $debug and warn "$main::prog: Removing leading/trailing whitespace\n"; - $contents =~ s%^\s+%%gm; - $contents =~ s%\s*$%\n%; - - # Fixup HTML comments. - # Unfortunately this breaks the JavaScript: line="<!--"+comment+"-->" -# $contents =~ s%(<!--.*-->)%\n$1\n%g; - - if ( $starts_with_comment ) { - $contents = $starts_with_comment . $contents . "\n// -->\n"; - } - } - - # Add note if desired. - if ( defined $add_note ) { - my $time_string = strftime("%Y/%M/%d %H:%M:%S",localtime); - $contents .= <<"EOM"; -// Compressed by jsjam <www.jsjam.com> $time_string $add_note -EOM - } - - $contents; -} - -# -# word - lookup/create mapping for a potential identifier. -# -BEGIN { - use vars qw(%map $next_short); - %map = (); - - $next_short = 'a'; -} -sub word { - my ($word) = @_; - - return $word if defined $RESERVED{"\L$word\E"}; - - my $short = $map{$word}; - if ( not defined $short ) { - while ( $RESERVED{$next_short} ) { - ++ $next_short; - } - $short = $next_short ++; - $map{$word} = $short; - $debug and warn "$main::prog: Mapping: $word => $short\n"; - } - - $short; -} - -# -# die_usage - Print usage string from manpage at end of file and die -# -sub die_usage { - my $usage; - open(PROG, "< $0") - or die "$prog: Unable to open $0 to print usage"; - local($/) = undef; - $usage = <PROG>; - close(PROG); - $usage =~ s%^.*? - =head1\sSYNOPSIS\s+ - (.*?)\s+ - =head1\sOPTIONS\s*\n - (.*?)\s* - =head1.*$ - %Usage: $1\n$2\n%xs; - die $usage; -} - -=head1 NAME - -jsjam - Compress JavaScript code. - -=head1 SYNOPSIS - - jsjam [opts] file... - -=head1 OPTIONS - - -d --debug Debug mode. - -h --help Print help and exit. - -q --quiet Quiet mode. - -v --version Print version and exit. - - -i --keep-identifiers Do not shorten identifiers. - -g --keep-globals Do not shorten non-"var" identifiers. - -w --keep-whitespace Do not compress whitespace. - -n --keep-newlines Do not remove newlines following stuff. - -c --keep-comments Do not compress comments (implies -n). - - -a --add-note N Adds note "N" to the end of the compressed - output (in a // comment). - - The options --keep-identifiers and --keep-globals are not compatible. - -=head1 ARGUMENTS - - file One or more JavaScript files to compress. If no files - are specified, stdin is used. - -=head1 DOWNLOAD - -The jsjam Perl script is available here: - - http://www.anvilon.com/software/download/jsjam - -=head1 DESCRIPTION - -This program attempts to compress JavaScript so that it downloads -faster to the browser. If the identifier compression is kept on, it -also has a side-effect of making the JavaScript fairly unreadable. - -The compressed output for all input files is sent to stdout. - -Compression methods include: - - - Strip comments. - - - Strip unnecessary whitespace. - - - Shorten identifiers (variable names, function names, field names). - -Identifiers which should not be shortened can be indicated in the -JavaScript code using one line per identifier in the form: - - //jsjam-keep:identifier - -where "identifier" is the identifier which should not be shortened. - -=head1 EXAMPLES - - jsjam --debug mycode.js >mycode-jsjam.js - -=head1 CAVEATS - -This program was originally written to compress one particular set of -JavaScript software, but many others have found it useful for their -situations as well. If this happens to work or almost work for you, -please drop a note to the author. Bug reports are welcomed and may -even get fixed, especially if you can provide sample JavaScript code -that illustrates the problem. - -If you have any short global variable names (1-2 characters), the ---keep-globals option will probably shorten local (var) variables so -that they conflict with the global variables. - -In addition to working well on Linux/Unix, this script has reportedly -been able to run on Perl under Windows, though some Windows users -report that it is better to remove the first line of this script file -(#!/usr/bin/perl -wT) - -=head1 AUTHOR - -Original hack by -Eric Hammond <http://www.anvilon.com> - -Comment and quoted string processing rewritten by -Cameron Shorter <http://cameron.shorter.net> - -=cut diff --git a/r2/r2/lib/contrib/markdown.py b/r2/r2/lib/contrib/markdown.py index 559dbaf2..6ddf5704 100644 --- a/r2/r2/lib/contrib/markdown.py +++ b/r2/r2/lib/contrib/markdown.py @@ -28,9 +28,14 @@ def htmlquote(text): text = text.replace('"', """) return text +def mangle_text(text): + from pylons import g + return md5.new(text + g.SECRET).hexdigest() + def semirandom(seed): + from pylons import g x = 0 - for c in md5.new(seed).digest(): x += ord(c) + for c in md5.new(seed + g.SECRET).digest(): x += ord(c) return x / (255*16.) class _Markdown: @@ -40,7 +45,7 @@ class _Markdown: escapechars = '\\`*_{}[]()>#+-.!' escapetable = {} for char in escapechars: - escapetable[char] = md5.new(char).hexdigest() + escapetable[char] = mangle_text(char) r_multiline = re.compile("\n{2,}") r_stripspace = re.compile(r"^[ \t]+$", re.MULTILINE) @@ -155,7 +160,7 @@ def handler(m): key = key.encode('utf8') except UnicodeDecodeError: key = ''.join(k for k in key if ord(k) < 128) - key = md5.new(key).hexdigest() + key = mangle_text(key) self.html_blocks[key] = m.group(1) return "\n\n%s\n\n" % key @@ -261,6 +266,9 @@ def handler1(m): url = url.replace("_", self.escapetable["_"]) res = '<a href="%s"' % htmlquote(url) + if not re.search('lesswrong|overcomingbias', res): + res += ' rel="nofollow"' + if title: title = title.replace("*", self.escapetable["*"]) title = title.replace("_", self.escapetable["_"]) @@ -279,7 +287,10 @@ def handler2(m): url = url.replace("*", self.escapetable["*"]) url = url.replace("_", self.escapetable["_"]) res = '''<a href="%s"''' % htmlquote(url) - + + if not re.search('lesswrong|overcomingbias', res): + res += ' rel="nofollow"' + if title: title = title.replace('"', '"') title = title.replace("*", self.escapetable["*"]) @@ -288,7 +299,7 @@ def handler2(m): res += ">%s</a>" % htmlquote(link_text) return res - text = self.r_DoAnchors1.sub(handler1, text) + #text = self.r_DoAnchors1.sub(handler1, text) text = self.r_DoAnchors2.sub(handler2, text) return text @@ -597,7 +608,7 @@ def _EncodeBackslashEscapes(self, text): ) >""", re.VERBOSE|re.I) def _DoAutoLinks(self, text): - text = self.r_link.sub(r'<a href="\1">\1</a>', text) + text = self.r_link.sub(r'<a href="\1" rel="nofollow">\1</a>', text) def handler(m): l = m.group(1) diff --git a/r2/r2/lib/contrib/memcache.py b/r2/r2/lib/contrib/memcache.py index 3ae5b960..9aa1bedc 100644 --- a/r2/r2/lib/contrib/memcache.py +++ b/r2/r2/lib/contrib/memcache.py @@ -47,7 +47,7 @@ import socket import time import types -from md5 import md5 +from hashlib import md5 try: import cPickle as pickle except ImportError: diff --git a/r2/r2/lib/db/exporter.py b/r2/r2/lib/db/exporter.py new file mode 100644 index 00000000..1ba0e034 --- /dev/null +++ b/r2/r2/lib/db/exporter.py @@ -0,0 +1,232 @@ +import os +import sys +from datetime import datetime + +from r2.lib.db import tdb_sql as tdb +from r2.lib.db.thing import NotFound, Relation +from r2.models import Link, Comment, Account, Vote, Subreddit +from r2.lib.cache import Memcache, SelfEmptyingCache, CacheChain + +from sqlalchemy import * +import pylons + +class Exporter: + + def __init__(self, output_db): + """Initialise with path to output SQLite DB file""" + # If the output file exists, delete it so that the db is + # created from scratch + if os.path.exists(output_db): + os.unlink(output_db) + self.db = create_engine("sqlite:///%s" % output_db) + + # Python's encoding handling is reallly annoying + # http://stackoverflow.com/questions/3033741/sqlalchemy-automatically-converts-str-to-unicode-on-commit + self.db.raw_connection().connection.text_factory = str + self.init_db() + pylons.g.cache = CacheChain((SelfEmptyingCache(max_size=1000), Memcache(pylons.g.memcaches))) + + def export_db(self): + self.started_at = datetime.now() + self.export_users() + self.export_links() + self.export_comments() + self.export_votes() + self.create_indexes() + print >>sys.stderr, "Finished, total run time %d secs" % ((datetime.now() - self.started_at).seconds,) + + def export_thing(self, thing_class, table, row_extract): + processed = 0 + max_id = self.max_thing_id(thing_class) + print >>sys.stderr, "%d %s to process" % (max_id, table.name) + for thing_id in xrange(max_id): + try: + thing = thing_class._byID(thing_id, data=True) + except NotFound: + continue + + try: + row = row_extract(thing) + except AttributeError: + print >>sys.stderr, " thing with id %d is broken, skipping" % thing_id + continue + + table.insert(values=row).execute() + processed += 1 + self.update_progress(processed) + + def user_row_extract(self, account): + return ( + account._id, + self.utf8(account.name), + account.email if hasattr(account, 'email') else None, + account.link_karma, + account.comment_karma + ) + + def export_users(self): + self.export_thing(Account, self.users, self.user_row_extract) + + def article_row_extract(self, link): + sr = Subreddit._byID(link.sr_id, data=True) + row = ( + link._id, + self.utf8(link.title), + self.utf8(link.article), + link.author_id, + link._date, + sr.name + ) + return row + + def export_links(self): + self.export_thing(Link, self.articles, self.article_row_extract) + + def comment_row_extract(self, comment): + return ( + comment._id, + comment.author_id, + comment.link_id, + comment.body, + comment._date + ) + + def export_comments(self): + self.export_thing(Comment, self.comments, self.comment_row_extract) + + def export_votes(self): + self.export_rel_votes(Link, self.article_votes) + self.export_rel_votes(Comment, self.comment_votes) + + def export_rel_votes(self, votes_on_cls, table): + # Vote.vote(c.user, link, action == 'like', request.ip) + processed = 0 + rel = Vote.rel(Account, votes_on_cls) + max_id = self.max_rel_type_id(rel) + print >>sys.stderr, "%d %s to process" % (max_id, table.name) + for vote_id in xrange(max_id): + try: + vote = rel._byID(vote_id, data=True) + except NotFound: + continue + + try: + row = ( + vote._id, + vote._thing1_id, # Account + vote._thing2_id, # Link/Comment (votes_on_cls) + vote._name, # Vote value + vote._date + ) + except AttributeError: + print >>sys.stderr, " vote with id %d is broken, skipping" % vote_id + continue + + table.insert(values=row).execute() + processed += 1 + self.update_progress(processed) + + def max_rel_type_id(self, rel_thing): + thing_type = tdb.rel_types_id[rel_thing._type_id] + thing_tbl = thing_type.rel_table[0] + rows = select([func.max(thing_tbl.c.rel_id)]).execute().fetchall() + return rows[0][0] + + def max_thing_id(self, thing): + thing_type = tdb.types_id[thing._type_id] + thing_tbl = thing_type.thing_table + rows = select([func.max(thing_tbl.c.thing_id)]).execute().fetchall() + return rows[0][0] + + def utf8(self, text): + if isinstance(text, unicode): + try: + text = text.encode('utf-8') + except UnicodeEncodeError: + print >>sys.stderr, "UnicodeEncodeError, using 'ignore' error mode" % link._id + text = text.encode('utf-8', errors='ignore') + elif isinstance(text, str): + try: + text = text.decode('utf-8').encode('utf-8') + except UnicodeError: + print >>sys.stderr, "UnicodeError, using 'ignore' error mode" % link._id + text = text.decode('utf-8', errors='ignore').encode('utf-8', errors='ignore') + + return text + + def init_db(self): + self.users = Table('users', self.db, + Column('id', Integer, primary_key=True), + Column('name', VARCHAR()), + Column('email', VARCHAR()), + Column('article_karma', Integer), + Column('comment_karma', Integer), + ) + self.users.create() + + self.articles = Table('articles', self.db, + Column('id', Integer, primary_key=True), + Column('title', VARCHAR()), + Column('body', TEXT()), + Column('author_id', Integer, ForeignKey('users.id')), + Column('updated_at', DateTime()), + Column('subreddit', VARCHAR()), + ) + self.articles.create() + + self.comments = Table('comments', self.db, + Column('id', Integer, primary_key=True), + Column('author_id', Integer, ForeignKey('users.id')), + Column('article_id', Integer, ForeignKey('articles.id')), + Column('body', TEXT()), + Column('updated_at', DateTime()), + ) + self.comments.create() + + self.article_votes = Table('article_votes', self.db, + Column('id', Integer, primary_key=True), + Column('user_id', Integer, ForeignKey('users.id')), + Column('article_id', Integer, ForeignKey('articles.id')), + Column('vote', Integer()), + Column('updated_at', DateTime()), + ) + self.article_votes.create() + + self.comment_votes = Table('comment_votes', self.db, + Column('id', Integer, primary_key=True), + Column('user_id', Integer, ForeignKey('users.id')), + Column('comment_id', Integer, ForeignKey('comments.id')), + Column('vote', Integer()), + Column('updated_at', DateTime()), + ) + self.comment_votes.create() + + def create_indexes(self): + #i = Index('someindex', sometable.c.col5) + print >>sys.stderr, "Creating indexes on users table" + Index('ix_users_id', self.users.c.id).create() + Index('ix_users_name', self.users.c.name).create() + Index('ix_users_email', self.users.c.email).create() + print >>sys.stderr, "Creating indexes on articles table" + Index('ix_articles_id', self.articles.c.id).create() + Index('ix_articles_author_id', self.articles.c.author_id).create() + Index('ix_articles_title', self.articles.c.title).create() + print >>sys.stderr, "Creating indexes on comments table" + Index('ix_comments_id', self.comments.c.id).create() + Index('ix_comments_author_id', self.comments.c.author_id).create() + Index('ix_comments_article_id', self.comments.c.article_id).create() + print >>sys.stderr, "Creating indexes on article_votes table" + Index('ix_article_votes_id', self.article_votes.c.id).create() + Index('ix_article_votes_author_id', self.article_votes.c.user_id).create() + Index('ix_article_votes_article_id', self.article_votes.c.article_id).create() + Index('ix_article_votes_vote', self.article_votes.c.vote).create() + print >>sys.stderr, "Creating indexes on comment_votes table" + Index('ix_comment_votes_id', self.comment_votes.c.id).create() + Index('ix_comment_votes_author_id', self.comment_votes.c.user_id).create() + Index('ix_comment_votes_comment_id', self.comment_votes.c.comment_id).create() + Index('ix_comment_votes_vote', self.comment_votes.c.vote).create() + + def update_progress(self, done): + """print a progress message""" + if done % 100 == 0: + print >>sys.stderr, " %d processed, run time %d secs" % (done, (datetime.now() - self.started_at).seconds) diff --git a/r2/r2/lib/db/queries.py b/r2/r2/lib/db/queries.py index 24913620..ac78181b 100644 --- a/r2/r2/lib/db/queries.py +++ b/r2/r2/lib/db/queries.py @@ -213,7 +213,7 @@ def get_overview(user, sort, time): return merge_results(get_comments(user, sort, time), get_submitted(user, sort, time)) -def user_rel_query(rel, user, name): +def user_rel_query(rel, user, name, hide_spam=True): """General user relationship query.""" q = rel._query(rel.c._thing1_id == user._id, rel.c._t2_deleted == False, @@ -222,22 +222,25 @@ def user_rel_query(rel, user, name): eager_load = True, thing_data = not g.use_query_cache ) + + if hide_spam: + q._filter(rel.c._t2_spam == False) return make_results(q, filter_thing2) vote_rel = Vote.rel(Account, Link) -def get_liked(user): - return user_rel_query(vote_rel, user, '1') +def get_liked(user, hide_spam=True): + return user_rel_query(vote_rel, user, '1', hide_spam) -def get_disliked(user): - return user_rel_query(vote_rel, user, '-1') +def get_disliked(user, hide_spam=True): + return user_rel_query(vote_rel, user, '-1', hide_spam) -def get_hidden(user): - return user_rel_query(SaveHide, user, 'hide') +def get_hidden(user, hide_spam=True): + return user_rel_query(SaveHide, user, 'hide', hide_spam) -def get_saved(user): - return user_rel_query(SaveHide, user, 'save') +def get_saved(user, hide_spam=True): + return user_rel_query(SaveHide, user, 'save', hide_spam) def get_drafts(user): draft_sr = Subreddit._by_name(user.draft_sr_name) diff --git a/r2/r2/lib/db/query_queue.py b/r2/r2/lib/db/query_queue.py index 6b61f174..deb5b02e 100644 --- a/r2/r2/lib/db/query_queue.py +++ b/r2/r2/lib/db/query_queue.py @@ -40,7 +40,7 @@ def add_query(cached_results): query_queue_table.insert().execute(d) except SQLError, e: #don't worry about inserting duplicates - if not 'IntegrityError' in e.message: + if not 'IntegrityError' in str(e): raise def remove_query(iden): diff --git a/r2/r2/lib/db/tdb_sql.py b/r2/r2/lib/db/tdb_sql.py index 74e5aa09..54964f71 100644 --- a/r2/r2/lib/db/tdb_sql.py +++ b/r2/r2/lib/db/tdb_sql.py @@ -45,9 +45,15 @@ BigInteger = postgres.PGBigInteger +def alias_generator(): + n = 1 + while True: + yield 'alias_%d' % n + n += 1 + def make_metadata(engine): metadata = sa.BoundMetaData(engine) - metadata.engine.echo = settings.DEBUG + metadata.bind.echo = g.sqlprinting return metadata def create_table(table, index_commands=None): @@ -226,7 +232,7 @@ def build_thing_tables(): index_commands(data_thing_table, 'thing')) else: data_thing_table = get_thing_table(data_metadata, name) - + thing = storage(type_id = type_id, name = name, thing_table = thing_table, @@ -248,7 +254,7 @@ def build_rel_tables(): type2_id = type2_id)) metadata = make_metadata(engine) - + #relation table rel_table = get_rel_table(metadata, name) create_table(rel_table, @@ -557,12 +563,6 @@ def del_rel(rel_type_id, rel_id): table.delete(table.c.rel_id == rel_id).execute() data_table.delete(data_table.c.thing_id == rel_id).execute() -def sa_rval_op(rval): - if isinstance(rval, operators.rval_op): - return getattr(sa.func, rval.__class__.__name__)(rval.rval) - else: - return rval - def sa_op(op): #if BooleanOp if isinstance(op, operators.or_): @@ -700,7 +700,9 @@ def translate_data_value(alias, op): op.lval = lval #convert the rval to db types - op.rval = tuple(py2db(v) for v in tup(op.rval)) + #convert everything to strings for pg8.3 + op.rval = tuple(str(py2db(v)) for v in tup(op.rval)) + #TODO sort by data fields #TODO sort by id wants thing_id @@ -708,11 +710,13 @@ def find_data(type_id, get_cols, sort, limit, constraints): d_table, t_table = types_id[type_id].data_table constraints = deepcopy(constraints) + aliases = alias_generator() + used_first = False s = None need_join = False have_data_rule = False - first_alias = d_table.alias() + first_alias = d_table.alias(aliases.next()) s = sa.select([first_alias.c.thing_id.label('thing_id')])#, distinct=True) for op in operators.op_iter(constraints): @@ -732,7 +736,7 @@ def find_data(type_id, get_cols, sort, limit, constraints): alias = first_alias used_first = True else: - alias = d_table.alias() + alias = d_table.alias(aliases.next()) id_col = first_alias.c.thing_id if id_col: @@ -770,7 +774,8 @@ def find_rels(rel_type_id, get_cols, sort, limit, constraints): r_table, t1_table, t2_table, d_table = rel_types_id[rel_type_id].rel_table constraints = deepcopy(constraints) - t1_table, t2_table = t1_table.alias(), t2_table.alias() + aliases = alias_generator() + t1_table, t2_table = t1_table.alias(aliases.next()), t2_table.alias(aliases.next()) s = sa.select([r_table.c.rel_id.label('rel_id')]) need_join1 = ('thing1_id', t1_table) @@ -803,7 +808,7 @@ def find_rels(rel_type_id, get_cols, sort, limit, constraints): op.lval = r_table.c[key[1:]] else: - alias = d_table.alias() + alias = d_table.alias(aliases.next()) s.append_whereclause(r_table.c.rel_id == alias.c.thing_id) s.append_column(alias.c.value.label(key)) s.append_whereclause(alias.c.key == key) @@ -831,7 +836,7 @@ def find_rels(rel_type_id, get_cols, sort, limit, constraints): if limit: s.limit = limit - + r = s.execute() return Results(r, lambda (row): (row if get_cols else row.rel_id)) diff --git a/r2/r2/lib/db/thing.py b/r2/r2/lib/db/thing.py index 187a5821..99bb398a 100644 --- a/r2/r2/lib/db/thing.py +++ b/r2/r2/lib/db/thing.py @@ -30,7 +30,7 @@ from r2.config.databases import tz from r2.lib.cache import sgm -import new, sys, sha +import new, sys, hashlib from datetime import datetime from copy import copy, deepcopy @@ -592,6 +592,14 @@ def _delete(self): #know it's deleted. save -> unsave, hide -> unhide self._name = 'un' + self._name + @classmethod + def _uncache(cls, thing1, thing2, name): + # Remove a rel from from the fast query cache + prefix = thing_prefix(cls.__name__) + cache.delete(prefix + str((thing1._id, + thing2._id, + name))) + @classmethod def _fast_query(cls, thing1s, thing2s, name, data=True): """looks up all the relationships between thing1_ids and thing2_ids @@ -765,7 +773,7 @@ def _iden(self): rules.sort() for r in rules: i += str(r) - return sha.new(i).hexdigest() + return hashlib.sha1(i).hexdigest() def __iter__(self): used_cache = False diff --git a/r2/r2/lib/filters.py b/r2/r2/lib/filters.py index cf03de1f..e1b9edde 100644 --- a/r2/r2/lib/filters.py +++ b/r2/r2/lib/filters.py @@ -27,7 +27,7 @@ import lxml.html from lxml.html import soupparser -from lxml.html.clean import Cleaner +from lxml.html.clean import Cleaner, autolink_html MD_START = '<div class="md">' MD_END = '</div>' @@ -37,7 +37,8 @@ # embedded: We want to allow flash movies in posts # style: enable removal of style # safe_attrs_only: need to allow strange arguments to <object> -sanitizer = Cleaner(embedded=False,style=True,safe_attrs_only=False) +sanitizer = Cleaner(embedded=False,safe_attrs_only=False) +comment_sanitizer = Cleaner(embedded=False,style=True,safe_attrs_only=False) def python_websafe(text): return text.replace('&', "&").replace("<", "<").replace(">", ">").replace('"', """) @@ -75,7 +76,7 @@ class _Unsafe(unicode): pass def _force_unicode(text): try: - text = unicode(text, 'utf-8') + text = unicode(text, 'utf-8', 'ignore') except TypeError: text = unicode(text) return text @@ -83,9 +84,15 @@ def _force_unicode(text): def _force_utf8(text): return str(_force_unicode(text).encode('utf8')) +def _force_ascii(text): + return _force_unicode(text).encode('ascii', 'ignore') + def unsafe(text=''): return _Unsafe(_force_unicode(text)) +def unsafe_wrap_md(html=''): + return unsafe(MD_START + html + MD_END) + def websafe_json(text=""): return c_websafe_json(_force_unicode(text)) @@ -105,12 +112,38 @@ def edit_comment_filter(text = ''): return url_escape(text) #TODO is this fast? -r_url = re.compile('(?<![\(\[])(http://[^\s\'\"\]\)]+)') +url_re = re.compile(r""" + (\[[^\]]*\]:?)? # optional leading pair of square brackets + \s* # optional whitespace + (\()? # optional open bracket + (?<![<]) # No angle around link already + (http://[^\s\'\"\]\)]+) # a http uri + (?![>]) # No angle around link already + (\))? # optional close bracket + """, re.VERBOSE) jscript_url = re.compile('<a href="(?!http|ftp|mailto|/).*</a>', re.I | re.S) href_re = re.compile('<a href="([^"]+)"', re.I | re.S) code_re = re.compile('<code>([^<]+)</code>') a_re = re.compile('>([^<]+)</a>') +def wrap_urls(text): + #wrap urls in "<>" so that markdown will handle them as urls + matches = url_re.finditer(text) + def check(match): + square_brackets, open_bracket, link, close_bracket = match.groups() + return match if link and not square_brackets else None + + matched = filter(None, [check(match) for match in matches]) + segments = [] + start = 0 + for match in matched: + segments.extend([text[start:match.start(3)], '<', match.group(3), '>']) + start = match.end(3) + + # Tack on any trailing bits + segments.append(text[start:]) + + return ''.join(segments) #TODO markdown should be looked up in batch? #@memoize('markdown') @@ -119,8 +152,8 @@ def safemarkdown(text, div=True): if text: # increase escaping of &, < and > once text = text.replace("&", "&").replace("<", "<").replace(">", ">") - #wrap urls in "<>" so that markdown will handle them as urls - text = r_url.sub(r'<\1>', text) + text = wrap_urls(text) + try: text = markdown(text) except RuntimeError: @@ -146,19 +179,41 @@ def inner_a_handler(m): text = a_re.sub(inner_a_handler, text) return MD_START + text + MD_END if div else text - def keep_space(text): text = websafe(text) for i in " \n\r\t": text=text.replace(i,'&#%02d;' % ord(i)) return unsafe(text) - def unkeep_space(text): return text.replace(' ', ' ').replace(' ', '\n').replace(' ', '\t') -def safehtml(html): - html_doc = soupparser.fromstring(html) - cleaned_html = sanitizer.clean_html(html_doc) - return lxml.html.tostring(cleaned_html) - +whitespace_re = re.compile('^\s*$') +def killhtml(html=''): + html_doc = soupparser.fromstring(remove_control_chars(html)) + text = filter(lambda text: not whitespace_re.match(text), html_doc.itertext()) + cleaned_html = ' '.join([fragment.strip() for fragment in text]) + return cleaned_html + +control_chars = re.compile('[\x00-\x08\x0b\x0c\x0e-\x1f]') # Control characters *except* \t \r \n +def remove_control_chars(text): + return control_chars.sub('',text) + +def cleanhtml(html='', cleaner=None): + html_doc = soupparser.fromstring(remove_control_chars(html)) + if not cleaner: + cleaner = sanitizer + cleaned_html = cleaner.clean_html(html_doc) + return lxml.html.tostring(autolink_html(cleaned_html)) + +def clean_comment_html(html=''): + return cleanhtml(html, comment_sanitizer) + +block_tags = r'h1|h2|h3|h4|h5|h6|table|ol|dl|ul|menu|dir|p|pre|center|form|fieldset|select|blockquote|address|div|hr' +linebreaks_re = re.compile(r'(\n{2}|\r{2}|(?:\r\n){2}|</?(?:%s)[^>]*?>)' % block_tags) +tags_re = re.compile(r'</?(?:%s)' % block_tags) +def format_linebreaks(html=''): + paragraphs = ['<p>%s</p>' % p if not tags_re.match(p) else p + for p in linebreaks_re.split(html.strip()) + if not whitespace_re.match(p)] + return ''.join(paragraphs) diff --git a/r2/r2/lib/importer.py b/r2/r2/lib/importer.py new file mode 100644 index 00000000..cf110f13 --- /dev/null +++ b/r2/r2/lib/importer.py @@ -0,0 +1,291 @@ +import sys +import os +import re +import datetime +import pytz +import yaml +import urlparse + +from random import Random +from r2.models import Link,Comment,Account,Subreddit +from r2.models.account import AccountExists, register +from r2.lib.db.thing import NotFound + +########################### +# Constants +########################### + +MAX_RETRIES = 100 + +# Constants for the characters to compose a password from. +# Easilty confused characters like I and l, 0 and O are omitted +PASSWORD_NUMBERS='123456789' +PASSWORD_LOWER_CHARS='abcdefghjkmnpqrstuwxz' +PASSWORD_UPPER_CHARS='ABCDEFGHJKMNPQRSTUWXZ' +PASSWORD_OTHER_CHARS='@#$%^&*' +ALL_PASSWORD_CHARS = ''.join([PASSWORD_NUMBERS,PASSWORD_LOWER_CHARS,PASSWORD_UPPER_CHARS,PASSWORD_OTHER_CHARS]) + +DATE_FORMAT = '%m/%d/%Y %I:%M:%S %p' +INPUT_TIMEZONE = pytz.timezone('America/New_York') + +rng = Random() +def generate_password(): + password = [] + for i in range(8): + password.append(rng.choice(ALL_PASSWORD_CHARS)) + return ''.join(password) + +class Importer(object): + + def __init__(self, url_handler=None): + """Constructs an importer that takes a data structure based on a yaml file. + + Args: + url_handler: A optional URL transformation function that will be + called with urls detected in post and comment bodies. + """ + + self.url_handler = url_handler if url_handler else self._default_url_handler + + self.username_mapping = {} + + @staticmethod + def _default_url_handler(match): + return match.group() + + def process_comment(self, comment_data, comment, post): + # Prepare data for import + ip = '127.0.0.1' + if comment_data: + naive_date = datetime.datetime.strptime(comment_data['dateCreated'], DATE_FORMAT) + local_date = INPUT_TIMEZONE.localize(naive_date, is_dst=False) # Pick the non daylight savings time + utc_date = local_date.astimezone(pytz.utc) + + # Determine account to use for this comment + account = self._get_or_create_account(comment_data['author'], comment_data['authorEmail']) + + if comment_data and not comment: + # Create new comment + comment, inbox_rel = Comment._new(account, post, None, comment_data['body'], ip, date=utc_date) + comment.is_html = True + comment.ob_imported = True + comment._commit() + elif comment_data and comment: + # Overwrite existing comment + comment.author_id = account._id + comment.body = comment_data['body'] + comment.ip = ip + comment._date = utc_date + comment.is_html = True + comment.ob_imported = True + comment._commit() + elif not comment_data and comment: + # Not enough comment data being imported to overwrite all comments + print 'WARNING: More comments in lesswrong than we are importing, ignoring additional comment in lesswrong' + + kill_tags_re = re.compile(r'</?[iub]>') + transform_categories_re = re.compile(r'[- ]') + + def process_post(self, post_data, sr): + # Prepare data for import + title = self.kill_tags_re.sub('', post_data['title']) + article = u'%s%s' % (post_data['description'], + Link._more_marker + post_data['mt_text_more'] if post_data['mt_text_more'] else u'') + ip = '127.0.0.1' + tags = [self.transform_categories_re.sub('_', tag.lower()) for tag in post_data.get('category', [])] + naive_date = datetime.datetime.strptime(post_data['dateCreated'], DATE_FORMAT) + local_date = INPUT_TIMEZONE.localize(naive_date, is_dst=False) # Pick the non daylight savings time + utc_date = local_date.astimezone(pytz.utc) + + # Determine account to use for this post + account = self._get_or_create_account(post_data['author'], post_data['authorEmail']) + + # Look for an existing post created due to a previous import + post = self._query_post(Link.c.ob_permalink == post_data['permalink']) + + if not post: + # Create new post + post = Link._submit(title, article, account, sr, ip, tags, date=utc_date) + post.blessed = True + post.comment_sort_order = 'old' + post.ob_permalink = post_data['permalink'] + post._commit() + else: + # Update existing post + post.title = title + post.article = article + post.author_id = account._id + post.sr_id = sr._id + post.ip = ip + post.set_tags(tags) + post._date = utc_date + post.blessed = True + post.comment_sort_order = 'old' + post._commit() + + # Process each comment for this post + comments = self._query_comments(Comment.c.link_id == post._id, Comment.c.ob_imported == True) + [self.process_comment(comment_data, comment, post) + for comment_data, comment in map(None, post_data.get('comments', []), comments)] + + def substitute_ob_url(self, url): + try: + url = self.post_mapping[url].url + except KeyError: + pass + return url + + # Borrowed from http://stackoverflow.com/questions/161738/what-is-the-best-regular-expression-to-check-if-a-string-is-a-valid-url/163684#163684 + url_re = re.compile(r"""(?:https?|ftp|file)://[-A-Z0-9+&@#/%?=~_|!:,.;]*[-A-Z0-9+&@#/%=~_|]""", re.IGNORECASE) + def rewrite_ob_urls(self, text): + if text: + if isinstance(text, str): + text = text.decode('utf-8') + + # Double decode needed to handle some wierd characters + text = text.encode('utf-8') + text = self.url_re.sub(lambda match: self.substitute_ob_url(match.group()), text) + + return text + + def post_process_post(self, post): + """Perform post processsing to rewrite URLs and generate mapping + between old and new permalinks""" + post.article = self.rewrite_ob_urls(post.article) + post._commit() + + comments = Comment._query(Comment.c.link_id == post._id, data = True) + for comment in comments: + comment.body = self.rewrite_ob_urls(comment.body) + comment._commit() + + def _post_process(self, rewrite_map_file): + def unicode_safe(text): + if isinstance(text, unicode): + return text.encode('utf-8') + else: + return text + + posts = list(Link._query(Link.c.ob_permalink != None, data = True)) + + # Generate a mapping between ob permalinks and imported posts + self.post_mapping = {} + for post in posts: + self.post_mapping[post.ob_permalink] = post + + # Write out the rewrite map + for old_url, post in self.post_mapping.iteritems(): + ob_url = urlparse.urlparse(old_url) + new_url = post.canonical_url + try: + rewrite_map_file.write("%s %s\n" % (unicode_safe(ob_url.path), unicode_safe(new_url))) + except UnicodeEncodeError, uee: + print "Unable to write to rewrite map file:" + print unicode_safe(ob_url.path) + print unicode_safe(new_url) + + # Update URLs in the posts and comments + print 'Post processing imported content' + for post in posts: + self.post_process_post(post) + + def import_into_subreddit(self, sr, data, rewrite_map_file): + for post_data in data: + try: + print post_data['title'] + self.process_post(post_data, sr) + except Exception, e: + print 'Unable to create post:\n%s\n%s\n%s' % (type(e), e, post_data) + raise + + self._post_process(rewrite_map_file) + + def _query_account(self, *args): + account = None + kwargs = {'data': True} + q = Account._query(*args, **kwargs) + accounts = list(q) + if accounts: + account = accounts[0] + return account + + def _query_post(self, *args): + post = None + kwargs = {'data': True} + q = Link._query(*args, **kwargs) + posts = list(q) + if posts: + post = posts[0] + return post + + def _query_comments(self, *args): + kwargs = {'data': True} + q = Comment._query(*args, **kwargs) + comments = list(q) + return comments + + def _username_from_name(self, name): + """Convert a name into a username""" + return name.replace(' ', '_') + + def _find_account_for(self, name, email): + """Try to find an existing account using derivations of the name""" + + try: + # Look for an account we have cached + account = self.username_mapping[(name, email)] + except KeyError: + # Look for an existing account that was created due to a previous import + account = self._query_account(Account.c.ob_account_name == name, + Account.c.email == email) + if not account: + # Look for an existing account based on derivations of the name + candidates = ( + name, + name.replace(' ', ''), + self._username_from_name(name) + ) + + account = None + for candidate in candidates: + account = self._query_account(Account.c.name == candidate, + Account.c.email == email) + if account: + account.ob_account_name = name + account._commit() + break + + # Cache the result for next time + self.username_mapping[(name, email)] = account + + if not account: + raise NotFound + + return account + + def _get_or_create_account(self, full_name, email): + try: + account = self._find_account_for(full_name, email) + except NotFound: + retry = 2 # First retry will by name2 + name = self._username_from_name(full_name) + username = name + while True: + # Create a new account + try: + account = register(username, generate_password(), email) + account.ob_account_name = full_name + account._commit() + except AccountExists: + # This username is taken, generate another, but first limit the retries + if retry > MAX_RETRIES: + raise StandardError("Unable to create account for '%s' after %d attempts" % (full_name, retry - 1)) + else: + # update cache with the successful account + self.username_mapping[(full_name, email)] = account + break + username = "%s%d" % (name, retry) + retry += 1 + + return account + diff --git a/r2/r2/lib/memoize.py b/r2/r2/lib/memoize.py index 732ee8a0..842e020c 100644 --- a/r2/r2/lib/memoize.py +++ b/r2/r2/lib/memoize.py @@ -20,7 +20,6 @@ # CondeNet, Inc. All Rights Reserved. ################################################################################ from r2.config import cache -import sha class NoneResult(object): pass diff --git a/r2/r2/lib/menus.py b/r2/r2/lib/menus.py index 1d7c0774..267039ed 100644 --- a/r2/r2/lib/menus.py +++ b/r2/r2/lib/menus.py @@ -6,16 +6,16 @@ # software over a computer network and provide for limited attribution for the # Original Developer. In addition, Exhibit A has been modified to be consistent # with Exhibit B. -# +# # Software distributed under the License is distributed on an "AS IS" basis, # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for # the specific language governing rights and limitations under the License. -# +# # The Original Code is Reddit. -# +# # The Original Developer is the Initial Developer. The Initial Developer of the # Original Code is CondeNet, Inc. -# +# # All portions of the code written by CondeNet are Copyright (c) 2006-2008 # CondeNet, Inc. All Rights Reserved. ################################################################################ @@ -63,12 +63,15 @@ def __getattr__(self, attr): controversial = _('Controversial'), saved = _('Saved'), recommended = _('Recommended'), - rising = _('Rising'), + rising = _('Rising'), admin = _('Admin'), drafts = _('Drafts'), blessed = _('Promoted'), comments = _('Comments'), - + posts = _('Posts'), + topcomments = _('Top Comments'), + newcomments = _('New Comments'), + # time sort words hour = _('This hour'), day = _('Today'), @@ -76,32 +79,33 @@ def __getattr__(self, attr): month = _('This month'), year = _('This year'), all = _('All time'), - + # "kind" words spam = _("Spam"), autobanned = _("Autobanned"), # reddit header strings adminon = _("Turn admin on"), - adminoff = _("Turn admin off"), - prefs = _("Preferences"), - stats = _("Stats"), + adminoff = _("Turn admin off"), + prefs = _("Preferences"), + stats = _("Stats"), submit = _("Create new article"), + meetupsnew = _("Add new meetup"), help = _("Help"), blog = _("Blog"), logout = _("Log out"), - + #reddit footer strings feedback = _("Feedback"), bookmarklets = _("Bookmarklets"), socialite = _("Socialite"), buttons = _("Buttons"), - widget = _("Widget"), - code = _("Code"), - mobile = _("Mobile"), - store = _("Store"), + widget = _("Widget"), + code = _("Code"), + mobile = _("Mobile"), + store = _("Store"), ad_inq = _("Advertise"), - + #preferences options = _('Options'), friends = _("Friends"), @@ -118,7 +122,10 @@ def __getattr__(self, attr): details = _("Details"), # reddits - home = _("Home"), + main = _("Main"), + discussion = _("Discussion"), + wiki = _("Wiki"), + sequences = _("Sequences"), about = _("About"), edit = _("Edit"), banned = _("Banned"), @@ -186,17 +193,18 @@ def menu_style(type): tabmenu = ('tabmenu', ''), buttons = ('userlinks', ''), select = ('select', ''), - navlist = ('navlist', '')) + navlist = ('navlist', ''), + dropdown2 = ('dropdown2', '')) return d.get(type, default) - + class NavMenu(Styled): """generates a navigation menu. The intention here is that the 'style' parameter sets what template/layout to use to differentiate, say, a dropdown from a flatlist, while the optional _class, and _id attributes can be used to set individualized CSS.""" - + def __init__(self, options, default = None, title = '', type = "dropdown", base_path = '', separator = '|', **kw): self.options = options @@ -216,6 +224,7 @@ def __init__(self, options, default = None, title = '', type = "dropdown", # (possibly None) self.default = default self.selected = self.find_selected() + self.enabled = True Styled.__init__(self, title = title, **kw) @@ -244,24 +253,24 @@ class NavButton(Styled): must also have its build() method called with the current path to set self.path. This step is done automatically if the button is passed to a NavMenu instance upon its construction.""" - def __init__(self, title, dest, sr_path = True, + def __init__(self, title, dest, sr_path = True, nocname=False, opt = '', aliases = [], target = "", style = "plain", **kw): - + # keep original dest to check against c.location when rendering self.aliases = set(a.rstrip('/') for a in aliases) self.aliases.add(dest.rstrip('/')) self.dest = dest - Styled.__init__(self, style = style, sr_path = sr_path, - nocname = nocname, target = target, + Styled.__init__(self, style = style, sr_path = sr_path, + nocname = nocname, target = target, title = title, opt = opt, **kw) def build(self, base_path = ''): '''Generates the href of the button based on the base_path provided.''' # append to the path or update the get params dependent on presence - # of opt + # of opt if self.opt: p = request.get.copy() p[self.opt] = self.dest @@ -271,16 +280,18 @@ def build(self, base_path = ''): self.bare_path = _force_unicode(base_path.replace('//', '/')).lower() self.bare_path = self.bare_path.rstrip('/') - + # append the query string base_path += query_string(p) - + # since we've been sloppy of keeping track of "//", get rid # of any that may be present self.path = base_path.replace('//', '/') def is_selected(self): """Given the current request path, would the button be selected.""" + if hasattr(self, 'name') and self.name == 'home': + return False if self.opt: return request.params.get(self.opt, '') in self.aliases else: @@ -296,6 +307,18 @@ def selected_title(self): when it is different from self.title)""" return self.title +class AbsButton(NavButton): + """A button for linking to an absolute URL""" + def __init__(self, title, dest): + self.path = dest + NavButton.__init__(self, title, dest, False) + + def build(self, base_path = ''): + pass + + def is_selected(self): + return False + class SubredditButton(NavButton): def __init__(self, sr): self.sr = sr @@ -313,9 +336,9 @@ class NamedButton(NavButton): whereby the 'title' is just the translation of 'name' and the 'dest' defaults to the 'name' as well (unless specified separately).""" - + def __init__(self, name, sr_path = True, nocname=False, dest = None, **kw): - self.name = name.strip('/') + self.name = name.replace('/', '') NavButton.__init__(self, menu[self.name], name if dest is None else dest, sr_path = sr_path, nocname=nocname, **kw) @@ -326,7 +349,18 @@ def selected_title(self): except KeyError: return NavButton.selected_title(self) +class ExpandableButton(NamedButton): + def __init__(self, name, sr_path = True, nocname=False, dest = None, + sub_reddit = "/", sub_menus=[], **kw): + self.sub = sub_menus + self.sub_reddit = sub_reddit + NamedButton.__init__(self,name,sr_path,nocname,dest,**kw) + def sub_menus(self): + return self.sub + + def is_selected(self): + return c.site.path == self.sub_reddit class JsButton(NavButton): """A button which fires a JS event and thus has no path and cannot @@ -360,7 +394,7 @@ class SimpleGetMenu(NavMenu): get_param = '' title = '' default = None - + def __init__(self, type = 'select', **kw): kw['default'] = kw.get('default', self.default) kw['base_path'] = kw.get('base_path') or request.path @@ -369,7 +403,7 @@ def __init__(self, type = 'select', **kw): NavMenu.__init__(self, buttons, type = type, **kw) #if kw.get('default'): # self.selected = kw['default'] - + def make_title(self, attr): return menu[attr] @@ -385,9 +419,9 @@ class SortMenu(SimpleGetMenu): options = ('hot', 'new', 'top', 'old', 'controversial') def __init__(self, **kw): - kw['title'] = _("Sort By") + kw['title'] = _("Sort By") + ':' SimpleGetMenu.__init__(self, **kw) - + @classmethod def operator(self, sort): if sort == 'hot': @@ -438,7 +472,7 @@ def __init__(self, **kw): def operator(self, sort): if sort == 'new': return operators.desc('_date') - + class KindMenu(SimpleGetMenu): get_param = 'kind' @@ -461,7 +495,7 @@ class TimeMenu(SimpleGetMenu): options = ('hour', 'day', 'week', 'month', 'year', 'all') def __init__(self, **kw): - kw['title'] = _("Links from") + kw.setdefault('title', _("Links from")) SimpleGetMenu.__init__(self, **kw) @classmethod @@ -470,10 +504,6 @@ def operator(self, time): if time != 'all': return Link.c._date >= timeago(time) -class ControversyTimeMenu(TimeMenu): - """time interval for controversial sort. Make default time 'day' rather than 'all'""" - default = 'day' - class NumCommentsMenu(SimpleGetMenu): """menu for toggling between the user's preferred number of comments and the max allowed in the display, assuming the number @@ -483,6 +513,7 @@ class NumCommentsMenu(SimpleGetMenu): options = ('true', 'false') def __init__(self, num_comments, **context): + context['title'] = _("Show") + ':' self.num_comments = num_comments SimpleGetMenu.__init__(self, **context) @@ -495,14 +526,14 @@ def make_title(self, attr): elif self.num_comments > g.max_comments: # if the number present is larger than the global max, # label the menu as the user pref and the max number - return dict(true=str(g.max_comments), + return dict(true=str(g.max_comments), false=str(user_num))[attr] else: # if the number is less than the global max, display "all" # instead for the upper bound. return dict(true=_("All"), false=str(user_num))[attr] - + def render(self, **kw): user_num = c.user.pref_num_comments @@ -515,6 +546,25 @@ def find_selected(self): """Always return False so the title is always displayed""" return None +class TagSortMenu(SimpleGetMenu): + """Menu for listings by tag""" + get_param = 'sort' + default = 'old' + options = ('old', 'new', 'top') + + def __init__(self, **kw): + kw['title'] = _("Sort By") + ':' + SimpleGetMenu.__init__(self, **kw) + + @classmethod + def operator(self, sort): + if sort == 'new': + return operators.desc('_t1_date') + elif sort == 'old': + return operators.asc('_t1_date') + elif sort == 'top': + return operators.desc('_t1_score') + # -------------------- # TODO: move to admin area class AdminReporterMenu(SortMenu): diff --git a/r2/r2/lib/pages/pages.py b/r2/r2/lib/pages/pages.py index 40817ca1..3fc597e3 100644 --- a/r2/r2/lib/pages/pages.py +++ b/r2/r2/lib/pages/pages.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # The contents of this file are subject to the Common Public Attribution # License Version 1.0. (the "License"); you may not use this file except in # compliance with the License. You may obtain a copy of the License at @@ -6,21 +7,21 @@ # software over a computer network and provide for limited attribution for the # Original Developer. In addition, Exhibit A has been modified to be consistent # with Exhibit B. -# +# # Software distributed under the License is distributed on an "AS IS" basis, # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for # the specific language governing rights and limitations under the License. -# +# # The Original Code is Reddit. -# +# # The Original Developer is the Initial Developer. The Initial Developer of the # Original Code is CondeNet, Inc. -# +# # All portions of the code written by CondeNet are Copyright (c) 2006-2008 # CondeNet, Inc. All Rights Reserved. ################################################################################ from r2.lib.wrapped import Wrapped, NoTemplateFound -from r2.models import IDBuilder, QueryBuilder, UnbannedCommentBuilder, InlineComment, InlineArticle, LinkListing, Account, Default, FakeSubreddit, Subreddit, Comment, Tag, Link, LinkTag +from r2.models import * from r2.config import cache from r2.lib.jsonresponse import json_respond from r2.lib.jsontemplates import is_api @@ -31,12 +32,14 @@ from r2.lib.captcha import get_iden from r2.lib.filters import spaceCompress, _force_unicode, _force_utf8 from r2.lib.db.queries import db_sort -from r2.lib.menus import NavButton, NamedButton, NavMenu, JsButton +from r2.lib.menus import NavButton, NamedButton, NavMenu, JsButton, ExpandableButton, AbsButton from r2.lib.menus import SubredditButton, SubredditMenu, menu from r2.lib.strings import plurals, rand_strings, strings from r2.lib.utils import title_to_url, query_string, UrlParser from r2.lib.template_helpers import add_sr, get_domain from r2.lib.promote import promote_builder_wrapper +from r2.lib.wikipagecached import WikiPageCached + import sys datefmt = _force_utf8(_('%d %b %Y')) @@ -55,7 +58,7 @@ class Reddit(Wrapped): loginbox -- enable/disable rendering of the small login box in the right margin (only if no user is logged in; login box will be disabled for a logged in user) show_sidebar -- enable/disable content in the right margin - + infotext -- text to display in a <p class="infotext"> above the content nav_menus -- list of Menu objects to be shown in the area below the header content -- renderable object to fill the main content well in the page. @@ -75,8 +78,8 @@ class Reddit(Wrapped): extension_handling = True def __init__(self, space_compress = True, nav_menus = None, loginbox = True, - infotext = '', content = None, title = '', robots = None, - show_sidebar = True, body_class = None, **context): + infotext = '', content = None, title = '', robots = None, + show_sidebar = True, body_class = None, top_filter = None, header_sub_nav = [], **context): Wrapped.__init__(self, **context) self.title = title self.robots = robots @@ -85,6 +88,8 @@ def __init__(self, space_compress = True, nav_menus = None, loginbox = True, self.show_sidebar = show_sidebar self.space_compress = space_compress self.body_class = body_class + self.top_filter = top_filter + self.header_sub_nav = header_sub_nav #put the sort menus at the top self.nav_menu = MenuArea(menus = nav_menus) if nav_menus else None @@ -93,6 +98,8 @@ def __init__(self, space_compress = True, nav_menus = None, loginbox = True, self.infobar = None if c.firsttime and c.site.firsttext and not infotext: infotext = c.site.firsttext + if not infotext and hasattr(c.site, 'infotext'): + infotext = c.site.infotext if infotext: self.infobar = InfoBar(message = infotext) @@ -105,9 +112,12 @@ def __init__(self, space_compress = True, nav_menus = None, loginbox = True, def rightbox(self): """generates content in <div class="rightbox">""" - + ps = PaneStack(css_class='spacer') + if self.searchbox: + ps.append(GoogleSearchForm()) + if not c.user_is_loggedin and self.loginbox: ps.append(LoginFormWide()) else: @@ -123,24 +133,29 @@ def rightbox(self): if not filters_ps.empty: ps.append(SideBox(filters_ps)) - if self.searchbox: - ps.append(GoogleSearchForm()) - #don't show the subreddit info bar on cnames - if not isinstance(c.site, FakeSubreddit) and not c.cname: + if c.user_is_admin and not isinstance(c.site, FakeSubreddit) and not c.cname: ps.append(SubredditInfoBar()) if self.extension_handling: ps.append(FeedLinkBar()) - ps.append(RecentComments()) - ps.append(RecentArticles()) + ps.append(SideBoxPlaceholder('side-meetups', _('Nearest Meetups'), '/meetups', sr_path=False)) + ps.append(SideBoxPlaceholder('side-comments', _('Recent Comments'), '/comments')) + ps.append(SideBoxPlaceholder('side-posts', _('Recent Posts'), '/recentposts')) + + if g.recent_edits_feed: + ps.append(RecentWikiEditsBox(g.recent_edits_feed)) for feed_url in g.feedbox_urls: ps.append(FeedBox(feed_url)) - ps.append(TagCloud()) - ps.append(TopContributors()) + ps.append(SideBoxPlaceholder('side-tags', _('Tags'))) + ps.append(SideBoxPlaceholder('side-monthly-contributors', _('Top Contributors, 30 Days'))) + ps.append(SideBoxPlaceholder('side-contributors', _('Top Contributors, All Time'))) + + if g.site_meter_codename: + ps.append(SiteMeter(g.site_meter_codename)) return ps @@ -167,7 +182,7 @@ def render(self, *a, **kw): else: abort(404, "not found") return c.response - + def corner_buttons(self): """set up for buttons in upper right corner of main page.""" buttons = [] @@ -182,14 +197,17 @@ def corner_buttons(self): nocname=not c.authorized_cname, target = "_self")] - buttons += [NamedButton('submit', False, + buttons += [NamedButton('submit', sr_path = not c.default_sr, nocname=not c.authorized_cname)] + if c.user.safe_karma >= g.discussion_karma_to_post: + buttons += [NamedButton('meetups/new', False, + nocname=not c.authorized_cname)] buttons += [NamedButton("prefs", False, css_class = "pref-lang")] buttons += [NamedButton("logout", False, nocname=not c.authorized_cname, target = "_self")] - + return NavMenu(buttons, base_path = "/", type = "buttons") def footer_nav(self): @@ -212,26 +230,43 @@ def footer_nav(self): def header_nav(self): """Navigation menu for the header""" + + menu_stack = PaneStack() + # Ensure the default button is the first tab - default_button_name = c.site.default_listing - button_names = ['blessed', 'hot', 'new', 'controversial', 'top', 'comments'] - button_names.remove(default_button_name) - button_names.insert(0, default_button_name) - - main_buttons = [] - for name in button_names: - kw = dict(dest='', aliases=['/' + name]) if name == default_button_name else {} - main_buttons.append(NamedButton(name, **kw)) + #default_button_name = c.site.default_listing - if c.user_is_loggedin: - main_buttons.append(NamedButton('saved', False)) + main_buttons = [ + ExpandableButton('main', dest = '/promoted', sr_path = False, sub_menus = + [ NamedButton('posts', dest = '/promoted', sr_path = False), + NamedButton('comments', dest = '/comments', sr_path = False)]), + ExpandableButton('discussion', dest = "/r/discussion/new", sub_reddit = "/r/discussion/", sub_menus = + [ NamedButton('posts', dest = "/r/discussion/new", sr_path = False), + NamedButton('comments', dest = "/r/discussion/comments", sr_path = False)]) + ] - return NavMenu(main_buttons, title = _('Filter by'), _id='nav', type='navlist') + menu_stack.append(NavMenu(main_buttons, title = _('Filter by'), _id='nav', type='navlist')) + + + if self.header_sub_nav: + menu_stack.append(NavMenu(self.header_sub_nav, title = _('Filter by'), _id='filternav', type='navlist')) + + return menu_stack + + def right_menu(self): + """docstring for right_menu""" + buttons = [ + AbsButton('wiki', 'http://wiki.lesswrong.com'), + NamedButton('sequences', sr_path=False), + NamedButton('about', sr_path=False) + ] + + return NavMenu(buttons, title = _('Filter by'), _id='rightnav', type='navlist') def build_toolbars(self): """Additional toolbars/menus""" return [] - + def __repr__(self): return "<Reddit>" @@ -248,7 +283,25 @@ class LoginFormWide(Wrapped): """generates a login form suitable for the 300px rightbox.""" pass -class RecentItems(Wrapped): +class SideBoxPlaceholder(Wrapped): + """A minimal side box with a heading and an anchor. + + If javascript is off the anchor may be followed and if it is on + then javascript will replace the content of the div with the HTML + result of an ajax request. + """ + + def __init__(self, node_id, link_text, link_path=None, sr_path=True): + Wrapped.__init__(self, node_id=node_id, link_text=link_text, link_path=link_path, sr_path=sr_path) + +class SpaceCompressedWrapped(Wrapped): + """Overrides default Wrapped.render to do space compression as well.""" + def render(self, *a, **kw): + res = Wrapped.render(self, *a, **kw) + res = spaceCompress(res) + return res + +class RecentItems(SpaceCompressedWrapped): def __init__(self, *args, **kwargs): self.things = self.init_builder() Wrapped.__init__(self, *args, **kwargs) @@ -262,7 +315,7 @@ def init_builder(self): @staticmethod def wrap_thing(thing): w = Wrapped(thing) - + if isinstance(thing, Link): w.render_class = InlineArticle elif isinstance(thing, Comment): @@ -272,22 +325,20 @@ def wrap_thing(thing): class RecentComments(RecentItems): def query(self): - return c.site.get_comments('new', 'all') + return c.current_or_default_sr.get_comments('new', 'all') def init_builder(self): - user = c.user if c.user_is_loggedin else None - sr_ids = Subreddit.user_subreddits(user) return UnbannedCommentBuilder( self.query(), - sr_ids, num = 5, wrap = RecentItems.wrap_thing, - skip = True + skip = True, + sr_ids = [c.current_or_default_sr._id] ) - + class RecentArticles(RecentItems): def query(self): - q = c.site.get_links('new', 'all') + q = c.current_or_default_sr.get_links('new', 'all') q._limit = 10 return q @@ -296,34 +347,56 @@ class RecentArticlesPage(Wrapped): def __init__(self, content, *a, **kw): Wrapped.__init__(self, content=content, *a, **kw) -class TopContributors(Wrapped): +class RecentPromotedArticles(RecentItems): + def query(self): + sr = DefaultSR() + q = sr.get_links('blessed', 'all') + q._limit = 4 + return q + +class TopContributors(SpaceCompressedWrapped): def __init__(self, *args, **kwargs): from r2.lib.user_stats import top_users uids = top_users() - # Returns a hash keyed in the uid - users = Account._byID(uids, data=True) - # Retrieve the Account objects in this way to preseve the sort order - all_users = (users[u] for u in uids) + users = Account._byID(uids, data=True, return_dict=False) - # Filter out banned and spammy accounts - self.things = filter(lambda user: not c.site.is_banned(user) and user.spammer < 1, all_users) + # Filter out accounts banned from the default subreddit + sr = Subreddit._by_name(g.default_sr) + self.things = filter(lambda user: not sr.is_banned(user), users) Wrapped.__init__(self, *args, **kwargs) -class TagCloud(Wrapped): - +class TopMonthlyContributors(SpaceCompressedWrapped): + def __init__(self, *args, **kwargs): + from r2.lib.user_stats import cached_all_user_change + uids_karma = cached_all_user_change()[1] + uids = map(lambda x: x[0], uids_karma) + users = Account._byID(uids, data=True, return_dict=False) + + # Add the monthly karma to the account objects + karma_lookup = dict(uids_karma) + for u in users: + u.monthly_karma = karma_lookup[u._id] + + # Filter out accounts banned from the default subreddit + sr = Subreddit._by_name(g.default_sr) + self.things = filter(lambda user: not sr.is_banned(user), users) + + Wrapped.__init__(self, *args, **kwargs) + +class TagCloud(SpaceCompressedWrapped): + numbers = ('one','two','three','four','five','six','seven','eight','nine','ten') - + def nav(self): - sr_ids = Subreddit.user_subreddits(c.user) if c.default_sr else [c.site._id] - cloud = Tag.tag_cloud_for_subreddits(sr_ids) + cloud = Tag.tag_cloud_for_subreddits([c.current_or_default_sr._id]) - buttons =[] + buttons = [] for tag, weight in cloud: buttons.append(NavButton(tag.name, tag.name, css_class=self.numbers[weight - 1])) return NavMenu(buttons, type="flatlist", separator=' ', base_path='/tag/') - + class SubredditInfoBar(Wrapped): """When not on Default, renders a sidebox which gives info about the current reddit, including links to the moderator and @@ -351,12 +424,12 @@ def __init__(self, content, _id = None, css_class = ''): class PrefsPage(Reddit): """container for pages accessible via /prefs. No extension handling.""" - + extension_handling = False def __init__(self, show_sidebar = True, *a, **kw): Reddit.__init__(self, show_sidebar = show_sidebar, - title = "%s: %s" %(c.site.title, _("Preferences")), + title = "%s - %s" % (_("Preferences"), c.site.title), *a, **kw) def header_nav(self): @@ -399,24 +472,24 @@ def header_nav(self): class MessageCompose(Wrapped): """Compose message form.""" - def __init__(self,to='', subject='', message='', success='', + def __init__(self,to='', subject='', message='', success='', captcha = None): Wrapped.__init__(self, to = to, subject = subject, - message = message, success = success, + message = message, success = success, captcha = captcha) - + class BoringPage(Reddit): """parent class For rendering all sorts of uninteresting, sortless, navless form-centric pages. The top navmenu is populated only with the text provided with pagename and the page title is 'reddit.com: pagename'""" - + extension_handling= False - + def __init__(self, pagename, **context): self.pagename = pagename - Reddit.__init__(self, title = "%s: %s" % (c.site.title, pagename), + Reddit.__init__(self, title = "%s - %s" % (_force_unicode(pagename), c.site.title), **context) class FormPage(BoringPage): @@ -424,7 +497,7 @@ class FormPage(BoringPage): def __init__(self, pagename, show_sidebar = False, *a, **kw): BoringPage.__init__(self, pagename, show_sidebar = show_sidebar, *a, **kw) - + class LoginPage(BoringPage): """a boring page which provides the Login/register form""" @@ -446,7 +519,7 @@ def __init__(self, user_reg = '', user_login = '', dest=''): Wrapped.__init__(self, user_reg = user_reg, user_login = user_login, dest = dest) - + class SearchPage(BoringPage): """Search results page""" searchbox = False @@ -470,19 +543,29 @@ class LinkInfoPage(Reddit): passed to this class will also be rendered underneath the rendered Link. """ - + create_reddit_box = False - extension_handling = False # No feed until comment feeds are implemented + robots = None + + @staticmethod + def comment_permalink_wrapper(comment, link): + wrapped = Wrapped(link, link_title=comment.make_permalink_title(link), for_comment_permalink=True) + wrapped.render_class = CommentPermalink + return wrapped def __init__(self, link = None, comment = None, - link_title = '', *a, **kw): - # TODO: temp hack until we find place for builder_wrapper - + link_title = '', is_canonical = False, *a, **kw): + link.render_full = True - + + # TODO: temp hack until we find place for builder_wrapper from r2.controllers.listingcontroller import ListingController + if comment: + link_wrapper = lambda link: self.comment_permalink_wrapper(comment, link) + else: + link_wrapper = ListingController.builder_wrapper link_builder = IDBuilder(link._fullname, - wrap = ListingController.builder_wrapper) + wrap = link_wrapper) # link_listing will be the one-element listing at the top self.link_listing = LinkListing(link_builder, nextprev=False).listing() @@ -492,18 +575,22 @@ def __init__(self, link = None, comment = None, link_title = ((self.link.title) if hasattr(self.link, 'title') else '') if comment: - author = Account._byID(comment.author_id, data=True).name - params = {'author' : author, 'title' : _force_unicode(link_title)} - title = strings.permalink_title % params + title = comment.make_permalink_title(link) + + # Comment permalinks should not be indexed, there's too many of them + self.robots = 'noindex' + + if is_canonical == False: + self.canonical_link = comment.make_permalink(link) else: params = {'title':_force_unicode(link_title), 'site' : c.site.title} title = strings.link_info_title % params - if not c.default_sr: - # Not on the main page, so include a pointer to the canonical URL for this link - self.canonical_link = link.canonical_url + if not (c.default_sr and is_canonical): + # Not on the main page, so include a pointer to the canonical URL for this link + self.canonical_link = link.canonical_url - Reddit.__init__(self, title = title, body_class = 'post', *a, **kw) + Reddit.__init__(self, title = title, body_class = 'post', robots = self.robots, *a, **kw) def content(self): return self.content_stack(self.infobar, self.link_listing, self._content) @@ -561,7 +648,7 @@ def rightbox(self): class MySubredditsPage(SubredditsPage): """Same functionality as SubredditsPage, without the search box.""" - + def content(self): return self.content_stack(self.infobar, self._content) @@ -581,21 +668,22 @@ class ProfilePage(Reddit): object of the user must be passed in as the first argument, along with the current sub-page (to determine the title to be rendered on the page)""" - + searchbox = False create_reddit_box = False submit_box = False + def __init__(self, user, *a, **kw): self.user = user - Reddit.__init__(self, *a, **kw) + Reddit.__init__(self, body_class = "profile_page", *a, **kw) def header_nav(self): path = "/user/%s/" % self.user.name main_buttons = [NavButton(menu.overview, '/', aliases = ['/overview']), NavButton(_('Comments'), 'comments'), NamedButton('submitted')] - + if votes_visible(self.user): main_buttons += [NamedButton('liked'), NamedButton('disliked'), @@ -604,9 +692,9 @@ def header_nav(self): if c.user_is_loggedin and self.user._id == c.user._id: # User is looking at their own page main_buttons.append(NamedButton('drafts')) - + return NavMenu(main_buttons, base_path = path, title = _('View'), _id='nav', type='navlist') - + def rightbox(self): rb = Reddit.rightbox(self) @@ -614,7 +702,7 @@ def rightbox(self): rb.push(ProfileBar(self.user)) return rb -class ProfileBar(Wrapped): +class ProfileBar(Wrapped): """Draws a right box for info about the user (karma, etc)""" def __init__(self, user, buttons = None): Wrapped.__init__(self, user = user, buttons = buttons) @@ -641,7 +729,7 @@ class ErrorPage(Wrapped): """Wrapper for an error message""" def __init__(self, message = _("You aren't allowed to do that.")): Wrapped.__init__(self, message = message) - + class Profiling(Wrapped): """Debugging template for code profiling using built in python library (only used in middleware)""" @@ -666,7 +754,7 @@ def __init__(self): return_dict = False) my_reddits.sort(key = lambda sr: sr.name.lower()) - drop_down_buttons = [] + drop_down_buttons = [] for sr in my_reddits: drop_down_buttons.append(SubredditButton(sr)) @@ -681,7 +769,7 @@ def __init__(self): title = _('My categories'), type = 'srdrop') - + pop_reddits = Subreddit.default_srs(c.content_langs, limit = 30) buttons = [] for sr in c.recent_reddits: @@ -692,16 +780,16 @@ def __init__(self): for sr in pop_reddits: if sr not in c.recent_reddits: buttons.append(SubredditButton(sr)) - + self.sr_bar = NavMenu(buttons, type='flatlist', separator = '-', _id = 'sr-bar') - + class SubredditBox(Wrapped): """A content pane that has the lists of subreddits that go in the right pane by default""" def __init__(self): Wrapped.__init__(self) - + self.title = _('Other reddit communities') self.subtitle = 'Visit your subscribed categories (in bold) or explore new ones' self.create_link = ('/categories/', menu.more) @@ -715,7 +803,7 @@ def __init__(self): my_reddits.sort(key = lambda sr: sr._downs, reverse = True) display_reddits = my_reddits[:g.num_side_reddits] - + #remove the current reddit display_reddits = filter(lambda x: x != c.site, display_reddits) @@ -801,10 +889,10 @@ class ResetPassword(Wrapped): class Captcha(Wrapped): """Container for rendering robot detection device.""" - def __init__(self, error=None): + def __init__(self, error=None, tabular=True, label=True): self.error = _('Try entering those letters again') if error else "" self.iden = get_captcha() - Wrapped.__init__(self) + Wrapped.__init__(self, tabular=tabular, label=label) class CommentReplyBox(Wrapped): """Used on LinkInfoPage to render the comment reply form at the @@ -831,7 +919,7 @@ def __init__(self, comments_url, has_more_comments=False): class PaneStack(Wrapped): """Utility class for storing and rendering a list of block elements.""" - + def __init__(self, panes=[], div_id = None, css_class=None, div=False): div = div or div_id or css_class or False self.div_id = div_id @@ -843,7 +931,7 @@ def __init__(self, panes=[], div_id = None, css_class=None, div=False): def append(self, item): """Appends an element to the end of the current stack""" self.stack.append(item) - + def push(self, item): """Prepends an element to the top of the current stack""" self.stack.insert(0, item) @@ -868,6 +956,15 @@ class GoogleSearchForm(Wrapped): def __init__(self): Wrapped.__init__(self) +class WikiPageList(Wrapped): + """Shows Wiki Page List box""" + def __init__(self, link): + if link: + self.articleurl = link.url + else: + self.articleurl = None + Wrapped.__init__(self) + class GoogleSearchResultsFrame(Wrapped): """Shows Google Custom Search box""" def __init__(self): @@ -885,6 +982,11 @@ def content(self): # return self.content_stack(self.infobar, # self.nav_menu, self._content) +class ArticleNavigation(Wrapped): + """Generates article navigation fragment for the supplied link""" + def __init__(self, link, author): + Wrapped.__init__(self, article=link, author=author) + class SearchBar(Wrapped): """More detailed search box for /search and /categories pages. Displays the previous search as well as info of the elapsed_time @@ -920,7 +1022,7 @@ class FrameToolbar(Wrapped): def __init__(self, link = None, **kw): self.title = link.title Wrapped.__init__(self, link = link, *kw) - + class NewLink(Wrapped): @@ -954,26 +1056,26 @@ class OptIn(Wrapped): pass -class UserStats(Wrapped): - """For drawing the stats page, which is fetched from the cache.""" - def __init__(self): - Wrapped.__init__(self) - cache_stats = cache.get('stats') - if cache_stats: - top_users, top_day, top_week = cache_stats - - #lookup user objs - uids = [] - uids.extend(u for u in top_users) - uids.extend(u[0] for u in top_day) - uids.extend(u[0] for u in top_week) - users = Account._byID(uids, data = True) - - self.top_users = (users[u] for u in top_users) - self.top_day = ((users[u[0]], u[1]) for u in top_day) - self.top_week = ((users[u[0]], u[1]) for u in top_week) - else: - self.top_users = self.top_day = self.top_week = () +# class UserStats(Wrapped): +# """For drawing the stats page, which is fetched from the cache.""" +# def __init__(self): +# Wrapped.__init__(self) +# cache_stats = cache.get('stats') +# if cache_stats: +# top_users, top_day, top_week = cache_stats + +# #lookup user objs +# uids = [] +# uids.extend(u for u in top_users) +# uids.extend(u[0] for u in top_day) +# uids.extend(u[0] for u in top_week) +# users = Account._byID(uids, data = True) + +# self.top_users = (users[u] for u in top_users) +# self.top_day = ((users[u[0]], u[1]) for u in top_day) +# self.top_week = ((users[u[0]], u[1]) for u in top_week) +# else: +# self.top_users = self.top_day = self.top_week = () class ButtonEmbed(Wrapped): @@ -981,7 +1083,7 @@ class ButtonEmbed(Wrapped): def __init__(self, button = None, width = 100, height=100, referer = "", url = ""): Wrapped.__init__(self, button = button, width = width, height = height, referer=referer, url = url) - + class ButtonLite(Wrapped): """Generates the JS wrapper around the buttons for embedding.""" def __init__(self, image = None, link = None, url = "", styled = True, target = '_top'): @@ -990,12 +1092,12 @@ def __init__(self, image = None, link = None, url = "", styled = True, target = class Button(Wrapped): """the voting buttons, embedded with the ButtonEmbed wrapper, shown on /buttons""" extension_handling = False - def __init__(self, link = None, button = None, css=None, - url = None, title = '', score_fmt = None, vote = True, target = "_parent", + def __init__(self, link = None, button = None, css=None, + url = None, title = '', score_fmt = None, vote = True, target = "_parent", bgcolor = None, width = 100): Wrapped.__init__(self, link = link, score_fmt = score_fmt, - likes = link.likes if link else None, - button = button, css = css, url = url, title = title, + likes = link.likes if link else None, + button = button, css = css, url = url, title = title, vote = vote, target = target, bgcolor=bgcolor, width=width) class ButtonNoBody(Button): @@ -1040,7 +1142,7 @@ def __init__(self): from r2.lib.translation import list_translations Wrapped.__init__(self) self.translations = list_translations() - + class Embed(Wrapped): """wrapper for embedding /help into reddit as if it were not on a separate wiki.""" @@ -1064,7 +1166,7 @@ class Ajaxed(): 'rendering' dictionary representations of the data which can be passed to the client via JSON over AJAX""" __slots__ = ['kind', 'action', 'data'] - + def __init__(self, kind, action): self._ajax = dict(kind=kind, action = None, @@ -1107,7 +1209,7 @@ def __repr__(self): return '<UserTableItem "%s">' % self.user.name class UserList(Wrapped): - """base class for generating a list of users""" + """base class for generating a list of users""" form_title = '' table_title = '' type = '' @@ -1132,7 +1234,7 @@ def users(self, site = None): objects which should be present in this UserList.""" uids = self.user_ids() if uids: - users = Account._byID(uids, True, return_dict = False) + users = Account._byID(uids, True, return_dict = False) return [self.ajax_user(u) for u in users] else: return () @@ -1195,6 +1297,21 @@ def table_title(self): def user_ids(self): return c.site.moderators +class EditorList(UserList): + """Editor list for a reddit.""" + type = 'editor' + + @property + def form_title(self): + return _('Add editor') + + @property + def table_title(self): + return _("Editors of %(reddit)s") % dict(reddit = c.site.name) + + def user_ids(self): + return c.site.editors + class BannedList(UserList): """List of users banned from a given reddit""" type = 'banned' @@ -1277,3 +1394,71 @@ class FeedBox(Wrapped): def __init__(self, feed_url, *a, **kw): self.feed_url = feed_url Wrapped.__init__(self, *a, **kw) + +class RecentWikiEditsBox(Wrapped): + def __init__(self, feed_url, *a, **kw): + self.feed_url = feed_url + Wrapped.__init__(self, *a, **kw) + +class SiteMeter(Wrapped): + def __init__(self, codename, *a, **kw): + self.codename = codename + Wrapped.__init__(self, *a, **kw) + +class UpcomingMeetups(SpaceCompressedWrapped): + def __init__(self, location, max_distance, *a, **kw): + meetups = Meetup.upcoming_meetups_near(location, max_distance, 2) + Wrapped.__init__(self, meetups=meetups, location=location, *a, **kw) + +class MeetupsMap(Wrapped): + def __init__(self, location, max_distance, *a, **kw): + meetups = Meetup.upcoming_meetups_near(location, max_distance) + Wrapped.__init__(self, meetups=meetups, location=location, *a, **kw) + +class NotEnoughKarmaToPost(Wrapped): + pass + +class ShowMeetup(Wrapped): + """docstring for ShowMeetup""" + def __init__(self, meetup, **kw): + # title_params = {'title':_force_unicode(meetup.title), 'site' : c.site.title} + # title = strings.show_meetup_title % title_params + Wrapped.__init__(self, meetup = meetup, **kw) + +class NewMeetup(Wrapped): + def __init__(self, *a, **kw): + Wrapped.__init__(self, *a, **kw) + +class EditMeetup(Wrapped): + pass + +class MeetupIndexPage(Reddit): + def __init__(self, **context): + self.meetups = context.get("content", None) + Reddit.__init__(self, **context) + + def content(self): + return MeetupIndex(self.meetups) + +class MeetupIndex(Wrapped): + def __init__(self, meetups = [], *a, **kw): + self.meetups = meetups + Wrapped.__init__(self, *a, **kw) + + def meetups(self): + return self.meetups + +class WikiPageInline(Wrapped): pass + +class WikiPage(Reddit): + def __init__(self, name, page, skiplayout, **context): + wikiPage = WikiPageCached(page) + html = wikiPage.html() + self.pagename = wikiPage.title() + Reddit.__init__(self, + content = WikiPageInline(html=html, name=name, + skiplayout=skiplayout,title=self.pagename), + title = self.pagename, + space_compress=False, + **context) + diff --git a/r2/r2/lib/s3cp.py b/r2/r2/lib/s3cp.py index 652734e3..647b6fc9 100644 --- a/r2/r2/lib/s3cp.py +++ b/r2/r2/lib/s3cp.py @@ -22,7 +22,7 @@ # CondeNet, Inc. All Rights Reserved. ################################################################################ -import base64, hmac, sha, os, sys, getopt +import base64, hmac, hashlib, os, sys, getopt from datetime import datetime from pylons import g,config @@ -49,7 +49,7 @@ def make_header(verb, date, amz_headers, resource, content_type): amz_str, resource)) - h = hmac.new(SECRET_KEY, s, sha) + h = hmac.new(SECRET_KEY, s, hashlib.sha1) return base64.encodestring(h.digest()).strip() def send_file(filename, resource, content_type, acl, rate, meter): diff --git a/r2/r2/lib/strings.py b/r2/r2/lib/strings.py index 9c2a90fa..c79ebfad 100644 --- a/r2/r2/lib/strings.py +++ b/r2/r2/lib/strings.py @@ -53,7 +53,7 @@ # this is for Japanese which treats people counds differently person_label = _("%(num)d %(persons)s"), - firsttext = _("Less Wrong is a community blog devoted to refining the art of human rationality. Please visit our About page for more information."), + firsttext = _("Less Wrong is a community blog devoted to refining the art of human rationality. Please visit our [About](/about-less-wrong/) page for more information."), already_submitted = _("That link has already been submitted, but you can try to [submit it again](%s)."), @@ -106,9 +106,10 @@ ), submit_box_text = _('To anything interesting: news article, blog entry, video, picture...'), - permalink_title = _("%(author)s comments on %(title)s"), - link_info_title = _("%(site)s: %(title)s"), - + permalink_title = _("%(author)s comments on %(title)s - %(site)s"), + link_info_title = _("%(title)s - %(site)s"), + show_meetup_title = _("%(title)s - %(site)s"), + not_enough_downvote_karma = _('You do not have enough karma to downvote right now. You need %d more %s.') ) class StringHandler(object): @@ -200,6 +201,10 @@ class Score(object): def number_only(x): return max(x, 0) + @staticmethod + def signed_number(x): + return x + @staticmethod def points(x): return strings.number_label % (x, plurals.N_points(x)) @@ -303,5 +308,5 @@ def __iter__(self): rand_strings = RandomStringManager() -rand_strings.add('sadmessages', "Funny 500 page message", 10) +rand_strings.add('sadmessages', "Funny 500 page message", 1) rand_strings.add('create_reddit', "Reason to create a reddit", 20) diff --git a/r2/r2/lib/tracking.py b/r2/r2/lib/tracking.py index c5c03ed7..f899ca3b 100644 --- a/r2/r2/lib/tracking.py +++ b/r2/r2/lib/tracking.py @@ -25,7 +25,7 @@ from random import choice from pylons import g, c from urllib import quote_plus, unquote_plus -import sha +import hashlib key_len = 16 pad_len = 32 @@ -153,7 +153,7 @@ def init_defaults(self, fullname): @classmethod def make_hash(cls, ip, fullname): - return sha.new("%s%s%s" % (ip, fullname, + return hashlib.sha1("%s%s%s" % (ip, fullname, g.tracking_secret)).hexdigest() def tracking_url(self): diff --git a/r2/r2/lib/translation.py b/r2/r2/lib/translation.py index 374fd110..eec7c8f9 100644 --- a/r2/r2/lib/translation.py +++ b/r2/r2/lib/translation.py @@ -29,7 +29,7 @@ import cPickle as pickle from wrapped import Wrapped from utils import Storage -from md5 import md5 +from hashlib import md5 from logger import WithWriteLock, LoggedSlots diff --git a/r2/r2/lib/user_stats.py b/r2/r2/lib/user_stats.py index 9e6ea766..166e019b 100644 --- a/r2/r2/lib/user_stats.py +++ b/r2/r2/lib/user_stats.py @@ -20,69 +20,180 @@ # CondeNet, Inc. All Rights Reserved. ################################################################################ import sqlalchemy as sa -from r2.models import Account, Vote, Link +from r2.models import Account, Vote, Link, Subreddit, Comment from r2.lib.db import tdb_sql as tdb from r2.lib import utils +import time from pylons import g cache = g.cache +def subreddits_with_custom_karma_multiplier(): + type = tdb.types_id[Subreddit._type_id] + tt, dt = type.thing_table, type.data_table[0] + + aliases = tdb.alias_generator() + karma = dt.alias(aliases.next()) + + q = sa.select( + [tt.c.thing_id], + sa.and_(tt.c.spam == False, + tt.c.deleted == False, + karma.c.thing_id == tt.c.thing_id, + karma.c.key == 'post_karma_multiplier'), + group_by = [tt.c.thing_id], + ) + + sr_ids = [r.thing_id for r in q.execute().fetchall()] + return Subreddit._byID(sr_ids, True, return_dict = False) + def top_users(): type = tdb.types_id[Account._type_id] tt, dt = type.thing_table, type.data_table[0] - karma = dt.alias() + aliases = tdb.alias_generator() + karma = dt.alias(aliases.next()) + + cases = [ + (karma.c.key.like('%_link_karma'), + sa.cast(karma.c.value, sa.Integer) * g.post_karma_multiplier) + ] + + for subreddit in subreddits_with_custom_karma_multiplier(): + key = "%s_link_karma" % subreddit.name + cases.insert(0, (karma.c.key == key, + sa.cast(karma.c.value, sa.Integer) * subreddit.post_karma_multiplier)) - s = sa.select([tt.c.thing_id], - sa.and_(tt.c.spam == False, - tt.c.deleted == False, - karma.c.thing_id == tt.c.thing_id, - karma.c.key.like('%_karma')), - group_by = [tt.c.thing_id], - order_by = sa.desc(sa.func.sum(sa.cast(karma.c.value, sa.Integer))), - limit = 10) + s = sa.select( + [tt.c.thing_id], + sa.and_(tt.c.spam == False, + tt.c.deleted == False, + karma.c.thing_id == tt.c.thing_id, + karma.c.key.like('%_karma')), + group_by = [tt.c.thing_id], + order_by = sa.desc(sa.func.sum( + sa.case(cases, else_ = sa.cast(karma.c.value, sa.Integer)) + )), + limit = 10) # Translation of query: # SELECT - # reddit_thing_account.thing_id, + # reddit_thing_account.thing_id + # FROM + # reddit_thing_account, + # reddit_data_account # WHERE - # (reddit_thing_account.spam = f AND - # reddit_thing_account.deleted = f AND + # (reddit_thing_account.spam = 'f' AND + # reddit_thing_account.deleted = 'f' AND # reddit_thing_account.thing_id = reddit_data_account.thing_id AND - # reddit_data_account.key LIKE '%link_karma') + # reddit_data_account.key LIKE '%_karma') + # GROUP BY + # reddit_thing_account.thing_id # ORDER BY - # sum(CAST(reddit_data_acc_3355.value AS INTEGER)) DESC + # sum( + # CASE + # WHEN reddit_data_account.key = 'lesswrong_link_karma' THEN + # CAST(reddit_data_account.value AS INTEGER) * 10 + # ELSE CAST(reddit_data_account.value AS INTEGER) + # END + # ) DESC # LIMIT 10 rows = s.execute().fetchall() return [r.thing_id for r in rows] -def top_user_change(period = '1 day'): +# Calculate the karma change for the given period for all users +# TODO: handle deleted users, spam articles and deleted articles, (and deleted comments?) +def all_user_change(period = '1 day'): + res={} + + link_karma = user_vote_change_links(period) + for name,val in link_karma: + res[name]=val + + comment_karma = user_vote_change_comments(period) + for name, val in comment_karma: + res[name] = val + res.get(name,0) + + return res + + +def user_vote_change_links(period = '1 day'): rel = Vote.rel(Account, Link) type = tdb.rel_types_id[rel._type_id] # rt = rel table # dt = data table - rt, account, link, dt = type.rel_table + rt, account_tt, link_tt, dt = type.rel_table + + aliases = tdb.alias_generator() + author_dt = dt.alias(aliases.next()) + + link_dt = tdb.types_id[Link._type_id].data_table[0].alias(aliases.next()) + + # Create an SQL CASE statement for the subreddit vote multiplier + cases = [] + for subreddit in subreddits_with_custom_karma_multiplier(): + cases.append( (sa.cast(link_dt.c.value,sa.Integer) == subreddit._id, + subreddit.post_karma_multiplier) ) + cases.append( (True, g.post_karma_multiplier) ) # The default article multiplier - author = dt.alias() date = utils.timeago(period) - s = sa.select([author.c.value, sa.func.sum(sa.cast(rt.c.name, sa.Integer))], - sa.and_(rt.c.date > date, - author.c.thing_id == rt.c.rel_id, - author.c.key == 'author_id'), - group_by = author.c.value, - order_by = sa.desc(sa.func.sum(sa.cast(rt.c.name, sa.Integer))), - limit = 10) + s = sa.select([author_dt.c.value, sa.func.sum(sa.cast(rt.c.name, sa.Integer) * sa.case(cases))], + sa.and_(rt.c.date >= date, + author_dt.c.thing_id == rt.c.rel_id, + author_dt.c.key == 'author_id', + link_tt.c.thing_id == rt.c.thing2_id, + link_tt.c.date >= date, + link_dt.c.key == 'sr_id', + link_dt.c.thing_id == rt.c.thing2_id), + group_by = author_dt.c.value) rows = s.execute().fetchall() + return [(int(r.value), r.sum) for r in rows] + +def user_vote_change_comments(period = '1 day'): + rel = Vote.rel(Account, Comment) + type = tdb.rel_types_id[rel._type_id] + # rt = rel table + # dt = data table + rt, account_tt, comment_tt, dt = type.rel_table + + aliases = tdb.alias_generator() + author_dt = dt.alias(aliases.next()) + + date = utils.timeago(period) + s = sa.select([author_dt.c.value, sa.func.sum(sa.cast(rt.c.name, sa.Integer))], + sa.and_(rt.c.date >= date, + author_dt.c.thing_id == rt.c.rel_id, + author_dt.c.key == 'author_id', + comment_tt.c.thing_id == rt.c.thing2_id, + comment_tt.c.date >= date), + group_by = author_dt.c.value) + + rows = s.execute().fetchall() + return [(int(r.value), r.sum) for r in rows] -def calc_stats(): - top = top_users() - top_day = top_user_change('1 day') - top_week = top_user_change('1 week') - return (top, top_day, top_week) +USER_CHANGE_CACHE_KEY = 'all_user_change' + +def cached_all_user_change(): + r = cache.get(USER_CHANGE_CACHE_KEY) + if not r: + start_time = time.time() + changes = all_user_change('30 days') + s = sorted(changes.iteritems(), key=lambda x: x[1]) + s.reverse() + r = [changes, s[0:5]] + cache.set(USER_CHANGE_CACHE_KEY, r, 3600) + g.log.info("Calculate all users karma change took : %.2fs"%(time.time()-start_time)) + return r + +# def calc_stats(): +# top = top_users() +# top_day = top_user_change('1 day') +# top_week = top_user_change('1 week') +# return (top, top_day, top_week) -def set_stats(): - cache.set('stats', calc_stats()) +# def set_stats(): +# cache.set('stats', calc_stats()) diff --git a/r2/r2/lib/utils/utils.py b/r2/r2/lib/utils/utils.py index a9d5a8f4..7d87b236 100644 --- a/r2/r2/lib/utils/utils.py +++ b/r2/r2/lib/utils/utils.py @@ -25,13 +25,14 @@ import Queue from copy import deepcopy import cPickle as pickle -import re, datetime, math, random, string, sha, os +import re, datetime, math, random, string, os, yaml -from datetime import datetime, timedelta +from datetime import datetime, timedelta, tzinfo from pylons.i18n import ungettext, _ from r2.lib.filters import _force_unicode from mako.filters import url_escape, url_unescape - +from pylons import g + iters = (list, tuple, set) def tup(item, ret_is_single=False): @@ -416,12 +417,27 @@ def timeuntil(d, resultion = 1, bare = True): from pylons import g return timetext(d - datetime.now(g.tz)) +def epochtime(date): + if not date: + return "0" + date = date.astimezone(g.tz) + return date.strftime("%s") + def prettytime(date, seconds = False): + date = date.astimezone(g.tz) return date.strftime('%d %B %Y %I:%M:%S%p' if seconds else '%d %B %Y %I:%M%p') def rfc822format(date): return date.strftime('%a, %d %b %Y %H:%M:%S %z') +def usformat(date): + """ + Format a datetime in US date format + + Makes the date readable by the Protoplasm datetime picker + """ + return date.strftime('%m-%d-%Y %H:%M:%S') + def to_base(q, alphabet): if q < 0: raise ValueError, "must supply a positive integer" l = len(alphabet) @@ -965,11 +981,11 @@ def no_dups(x): return IteratorFilter(iterator, no_dups) -def modhash(user, rand = None, test = False): - return user.name +# def modhash(user, rand = None, test = False): +# return user.name -def valid_hash(user, hash): - return True +# def valid_hash(user, hash): +# return True def check_cheating(loc): pass @@ -1011,3 +1027,38 @@ def new_fn(*a,**kw): % (fn,a,kw,ret)) return ret return new_fn + +def remote_addr(env): + """ + Returns the remote address for the WSGI env passed + + Takes proxies into consideration + """ + # In production the remote address is always the load balancer + # So check X-Forwarded-For first + # E.g. HTTP_X_FORWARDED_FOR: '66.249.72.73, 75.101.144.164' + if env.has_key('HTTP_X_FORWARDED_FOR'): + ips = re.split(r'\s*,\s*', env['HTTP_X_FORWARDED_FOR']) + if len(ips) > 0: + return ips[0] + + return env['REMOTE_ADDR'] + + +# A class building tzinfo objects for a fixed offset. +class FixedOffset(tzinfo): + """Fixed offset in hours east from UTC. name may be None""" + def __init__(self, offset, name): + self.offset = timedelta(hours = offset) + self.name = name + # tzinfo.__init__(self, name) + + def utcoffset(self, dt): + return self.offset + + def tzname(self, dt): + return self.name + + def dst(self, dt): + return timedelta(0) + diff --git a/r2/r2/lib/wiki.py b/r2/r2/lib/wiki.py new file mode 100644 index 00000000..bcc34355 --- /dev/null +++ b/r2/r2/lib/wiki.py @@ -0,0 +1,157 @@ +from r2.lib.utils import UrlParser + +from urllib import urlopen +from r2.lib.filters import _force_ascii +import os, os.path, yaml, re +from lxml import etree + +# Wiki is singleton like +# http://code.activestate.com/recipes/66531-singleton-we-dont-need-no-stinkin-singleton-the-bo/#c22 + +class Wiki(object): + def __new__(cls, *args, **kwargs): + if not '_the_instance' in cls.__dict__: + cls._the_instance = object.__new__(cls) + return cls._the_instance + + def __init__(self): + self.filename = 'wiki.lesswrong.xml' + object.__init__(self) + + @property + def pathname(self): + return os.path.join('..', 'public', 'files', self.filename) + + @property + def cache_key(self): + """Cache key for the wiki data""" + statinfo = os.stat(self.pathname) + return (self.filename + str(statinfo.st_mtime)).encode('ascii', 'ignore') + + @property + def data(self): + """Returns the data extracted from the wiki export XML file""" + from pylons import g + wiki_data = g.permacache.get(self.cache_key) + + if wiki_data is None: + # Parse the XML file + wiki_xml = etree.parse(self.pathname) + wiki_data = self._process_data(wiki_xml) + g.permacache.set(self.cache_key, wiki_data) + + return wiki_data + + def url_for_title(self, title): + """Uses the MediaWiki API to get the URL for a wiki page + with the given title""" + if title is None: + return None + + from pylons import g + cache_key = ('wiki_url_%s' % title).encode('ascii', 'ignore') + wiki_url = g.cache.get(cache_key) + if wiki_url is None: + # http://www.mediawiki.org/wiki/API:Query_-_Properties#info_.2F_in + api = UrlParser(g.wiki_api_url) + api.update_query( + action = 'query', + titles= title, + prop = 'info', + format = 'yaml', + inprop = 'url' + ) + + try: + response = urlopen(api.unparse()).read() + parsed_response = yaml.load(response, Loader=yaml.CLoader) + page = parsed_response['query']['pages'][0] + except: + return None + + wiki_url = page.get('fullurl').strip() + + # Things are created every couple of days so 12 hours seems + # to be a reasonable cache time + g.permacache.set(cache_key, wiki_url, time=3600 * 12) + + return wiki_url + + def sequences_for_article_url(self, url): + """Return the article sequences extracted from the wiki export""" + if url is None: + return {} + + from pylons import g + all_sequences = self.data['sequences'] + url = UrlParser(url) + cache_key = _force_ascii(self.cache_key + url.path) + + sequences = g.permacache.get(cache_key) + + if sequences is None: + sequences = {} + for sequence in all_sequences: + articles = sequence['articles'] + + # Find the index of the given URL in the sequence's articles + try: + article_index = articles.index(url.path) + except ValueError: + article_index = None + + if article_index is not None: + # The url passed is a part of the current sequence + try: + next_in_seq = articles[article_index + 1] + except IndexError: + next_in_seq = None + prev_in_seq = articles[article_index - 1] if article_index > 0 else None + + # Add the result + title = sequence['title'] + sequences[title] = { + 'title': title, + 'next': next_in_seq, + 'prev': prev_in_seq, + 'index': article_index + } + g.permacache.set(cache_key, sequences) + + return sequences + + def _process_data(self, wiki_xml): + """This method processes the wiki data and extracts what is used""" + MEDIAWIKI_NS = 'http://www.mediawiki.org/xml/export-0.3/' + sequences = [] + lw_url_re = re.compile(r'\[(http://lesswrong\.com/lw/[^ ]+) [^\]]+\]') + + for page in wiki_xml.getroot().iterfind('.//{%s}page' % MEDIAWIKI_NS): # TODO: Change to use iterparse + # Get the titles + title = page.findtext('{%s}title' % MEDIAWIKI_NS) + + # See if this page is a sequence page + sequence_elem = page.xpath("mw:revision[1]/mw:text[contains(., '[[Category:Sequences]]')]", namespaces={'mw': MEDIAWIKI_NS}) + + if sequence_elem: + sequence_elem = sequence_elem[0] + articles = [] + + # Find all the lesswrong urls + for match in lw_url_re.finditer(sequence_elem.text): + article_url = UrlParser(match.group(1)) + + # Only store the path to the article + article_path = article_url.path + + # Ensure path ends in slash + if article_path[-1] != '/': + article_path += '/' + + articles.append(article_path) + + sequences.append({ + 'title': title, + 'articles': articles + }) + return {'sequences': sequences} diff --git a/r2/r2/lib/wikipagecached.py b/r2/r2/lib/wikipagecached.py new file mode 100644 index 00000000..0d2fce14 --- /dev/null +++ b/r2/r2/lib/wikipagecached.py @@ -0,0 +1,81 @@ + +from pylons import g +from r2.lib.pages import * +from r2.lib.filters import remove_control_chars +from pylons.i18n import _, ungettext +from urllib2 import Request, HTTPError, URLError, urlopen +from urlparse import urlsplit,urlunsplit +from lxml.html import soupparser +from lxml.etree import tostring + +log = g.log + +def missing_content(): + return "<h2>Unable to fetch wiki page. Try again later</h2>" + +def cache_time(): + return int(g.wiki_page_cache_time) + +def base_url(url): + u = urlsplit(url) + return urlunsplit([u[0],u[1]]+['','','']) + +def fetch(url): + log.debug('fetching: %s' % url) + req = Request(url) + content = urlopen(req).read() + log.debug('fetched %d bytes' % len(content)) + return content + +def getParsedContent(str): + parsed = soupparser.fromstring(remove_control_chars(str)) + try: + elem=parsed.get_element_by_id('content') + elem.set('id','wiki-content') + return elem + except KeyError: + return parsed + +class WikiPageCached: + def __init__(self, page): + self.page = page + self.loaded = False + + def getPage(self): + url=self.page['url'] + contentTitle = g.rendercache.get(url) + [content,title] = contentTitle if contentTitle else [None,None] + + if not content: + try: + str = fetch(url) + elem = getParsedContent(str) + elem.make_links_absolute(base_url(url)) + headlines = elem.cssselect('h1 .mw-headline') + if headlines and len(headlines)>0: + title = headlines[0].text_content() + content = tostring(elem, method='html', encoding='utf8', with_tail=False) + g.rendercache.set(url, [content,title], cache_time()) + except Exception as e: + log.warn("Unable to fetch wiki page: '%s' %s"%(url,e)) + content = missing_content() + + self.titleStr = title + self.content = content + self.loaded = True + + def html(self): + if not self.loaded: + self.getPage() + return self.content + + def title(self): + if not self.loaded: + self.getPage() + return self.titleStr + + def invalidate(self): + g.rendercache.delete(self.page['url']) + log.debug('invalidated: %s' % self.page['url']) + + diff --git a/r2/r2/lib/wrapped.py b/r2/r2/lib/wrapped.py index 6f1013c7..ed037ffd 100644 --- a/r2/r2/lib/wrapped.py +++ b/r2/r2/lib/wrapped.py @@ -48,7 +48,12 @@ def __getattr__(self, attr): found = False for lookup in self.lookups: try: - res = getattr(lookup, attr) + if attr.startswith('_t1'): + res = getattr(lookup, attr[3:]) + elif attr.startswith('_t2'): + res = getattr(lookup, attr[3:]) + else: + res = getattr(lookup, attr) found = True break except AttributeError: diff --git a/r2/r2/models/__init__.py b/r2/r2/models/__init__.py index b93b2f61..9cad0a86 100644 --- a/r2/r2/models/__init__.py +++ b/r2/r2/models/__init__.py @@ -28,5 +28,7 @@ from subreddit import * from mail_queue import Email, has_opted_out, opt_count from admintools import * +from edit import * +from meetup import * import thing_changes diff --git a/r2/r2/models/account.py b/r2/r2/models/account.py index fa336e1a..1f78d5c7 100644 --- a/r2/r2/models/account.py +++ b/r2/r2/models/account.py @@ -19,17 +19,22 @@ # All portions of the code written by CondeNet are Copyright (c) 2006-2008 # CondeNet, Inc. All Rights Reserved. ################################################################################ +from pylons import c from r2.lib.db.thing import Thing, Relation, NotFound from r2.lib.db.operators import lower from r2.lib.db.userrel import UserRel from r2.lib.memoize import memoize, clear_memo -from r2.lib.utils import modhash, valid_hash, randstr +from r2.lib.utils import randstr +from r2.lib.strings import strings, plurals +from r2.lib.base import current_login_cookie from pylons import g -import time, sha +from pylons.i18n import _ +import time, hashlib from copy import copy class AccountExists(Exception): pass +class NotEnoughKarma(Exception): pass class Account(Thing): _data_int_props = Thing._data_int_props + ('link_karma', 'comment_karma', @@ -41,10 +46,11 @@ class Account(Thing): pref_frame = False, pref_newwindow = False, pref_public_votes = False, + pref_kibitz = False, pref_hide_ups = False, pref_hide_downs = True, - pref_min_link_score = -4, - pref_min_comment_score = -4, + pref_min_link_score = -2, + pref_min_comment_score = -2, pref_num_comments = g.num_comments, pref_lang = 'en', pref_content_langs = ('en',), @@ -52,6 +58,8 @@ class Account(Thing): pref_compress = False, pref_organic = True, pref_show_stylesheets = True, + pref_url = '', + pref_location = '', reported = 0, report_made = 0, report_correct = 0, @@ -64,6 +72,7 @@ class Account(Thing): ) def karma(self, kind, sr = None): + from subreddit import Subreddit suffix = '_' + kind + '_karma' #if no sr, return the sum @@ -71,7 +80,16 @@ def karma(self, kind, sr = None): total = 0 for k, v in self._t.iteritems(): if k.endswith(suffix): - total += v + if kind == 'link': + try: + karma_sr_name = k[0:k.rfind(suffix)] + karma_sr = Subreddit._by_name(karma_sr_name) + multiplier = karma_sr.post_karma_multiplier + except NotFound: + multiplier = 1 + else: + multiplier = 1 + total += v * multiplier return total else: try: @@ -103,7 +121,12 @@ def comment_karma(self): @property def safe_karma(self): karma = self.link_karma + self.comment_karma - return max(karma, 1) if karma > -1000 else karma + return max(karma, 0) if karma > -1000 else karma + + @property + def monthly_karma(self): + from r2.lib.user_stats import cached_all_user_change + return cached_all_user_change()[0].get(self._id, 0) def all_karmas(self): """returns a list of tuples in the form (name, link_karma, @@ -132,7 +155,50 @@ def all_karmas(self): self._t.get('comment_karma', 0))) return karmas - + + def vote_cache_key(self, kind): + """kind is 'link' or 'comment'""" + return 'account_%d_%s_downvotes' % (self._id, kind) + + def check_downvote(self, vote_kind): + """Checks whether this account has enough karma to cast a downvote. + + vote_kind is 'link' or 'comment' depending on the type of vote that's + being cast. + + This makes the assumption that the user can't cast a vote for something + on the non-current subreddit. + """ + from r2.models.vote import Vote, Link, Comment + + def get_cached_downvotes(content_cls): + kind = content_cls.__name__.lower() + downvotes = g.cache.get(self.vote_cache_key(kind)) + if downvotes is None: + vote_cls = Vote.rel(Account, content_cls) + downvotes = len(list(vote_cls._query(Vote.c._thing1_id == self._id, + Vote.c._name == str(-1)))) + g.cache.set(self.vote_cache_key(kind), downvotes) + return downvotes + + link_downvote_karma = get_cached_downvotes(Link) * c.current_or_default_sr.post_karma_multiplier + comment_downvote_karma = get_cached_downvotes(Comment) + karma_spent = link_downvote_karma + comment_downvote_karma + + karma_balance = self.safe_karma * 4 + vote_cost = c.current_or_default_sr.post_karma_multiplier if vote_kind == 'link' else 1 + if karma_spent + vote_cost > karma_balance: + points_needed = abs(karma_balance - karma_spent - vote_cost) + msg = strings.not_enough_downvote_karma % (points_needed, plurals.N_points(points_needed)) + raise NotEnoughKarma(msg) + + def incr_downvote(self, delta, kind): + """kind is link or comment""" + try: + g.cache.incr(self.vote_cache_key(kind), delta) + except ValueError, e: + print 'Account.incr_downvote failed with: %s' % e + def make_cookie(self, timestr = None, admin = False): if not self._loaded: self._load() @@ -141,18 +207,17 @@ def make_cookie(self, timestr = None, admin = False): to_hash = ','.join((id_time, self.password, g.SECRET)) if admin: to_hash += 'admin' - return id_time + ',' + sha.new(to_hash).hexdigest() + return id_time + ',' + hashlib.sha1(to_hash).hexdigest() def needs_captcha(self): - # TODO: decide on who/what/why needs a captcha - return False - # return self.link_karma < 1 + return self.safe_karma < 1 - def modhash(self, rand=None, test=False): - return modhash(self, rand = rand, test = test) + def modhash(self): + to_hash = ','.join((current_login_cookie(), g.SECRET)) + return hashlib.sha1(to_hash).hexdigest() def valid_hash(self, hash): - return valid_hash(self, hash) + return hash == self.modhash() @classmethod @memoize('account._by_name') @@ -281,7 +346,7 @@ def passhash(username, password, salt = ''): if isinstance(tohash, unicode): # Force tohash to be a byte string so it can be hashed tohash = tohash.encode('utf8') - return salt + sha.new(tohash).hexdigest() + return salt + hashlib.sha1(tohash).hexdigest() def change_password(user, newpassword): user.password = passhash(user.name, newpassword, True) @@ -289,15 +354,18 @@ def change_password(user, newpassword): return True #TODO reset the cache -def register(name, password): +def register(name, password, email=None): try: a = Account._by_name(name) raise AccountExists except NotFound: a = Account(name = name, password = passhash(name, password, True)) + if email: + a.email = email a._commit() + # Clear memoization of both with and without deleted clear_memo('account._by_name', Account, name.lower(), True) clear_memo('account._by_name', Account, name.lower(), False) diff --git a/r2/r2/models/builder.py b/r2/r2/models/builder.py index 4339726f..6cd18fed 100644 --- a/r2/r2/models/builder.py +++ b/r2/r2/models/builder.py @@ -338,6 +338,18 @@ def keep_item(self, item): return True +class SubredditTagBuilder(QueryBuilder): + + def __init__(self, query, sr_ids, **kw): + self.sr_ids = sr_ids + QueryBuilder.__init__(self, query, **kw) + + def keep_item(self, item): + if item.sr_id not in self.sr_ids: + return False + + return True + class IDBuilder(QueryBuilder): def init_query(self): names = self.names = list(tup(self.query)) @@ -471,6 +483,7 @@ def empty_listing(*things): candidates = [] candidates.extend(self.comment) dont_collapse.extend(cm._id for cm in self.comment) + top = self.comment[0] #assume the comments all have the same parent # TODO: removed by Chris to get rid of parent being sent # when morecomments is used. diff --git a/r2/r2/models/edit.py b/r2/r2/models/edit.py new file mode 100644 index 00000000..db576b0b --- /dev/null +++ b/r2/r2/models/edit.py @@ -0,0 +1,51 @@ +from r2.lib.db.thing import Thing +from account import Account +from link import Link + +import difflib + +class Edit(Thing): + """Used to track edits on Link""" + + @classmethod + def _new(cls, link, user, new_content): + return Edit(link_id = link._id, + author_id = user._id, + diff = Edit.create_diff(link.article, new_content)) + + @staticmethod + def create_diff(old_content, new_content): + return list(difflib.unified_diff(old_content.splitlines(), new_content.splitlines())) + + @classmethod + def add_props(cls, user, wrapped): + for item in wrapped: + item.permalink = item.link.make_permalink_slow() + + @staticmethod + def cache_key(wrapped): + return False + + def keep_item(self, wrapped): + return True + + @property + def link(self): + return Link._byID(self.link_id, data=True) + + @property + def link_author(self): + return Account._byID(self.link.author_id, data=True) + + diff_marker_to_class = { + "+" : "new", + "-" : "del", + "@" : "context" + } + + @staticmethod + def diff_line_style(line): + """Used in the template to find the css class for each diff line""" + if len(line)<=0: return "" + return Edit.diff_marker_to_class.get(line[0],"") + diff --git a/r2/r2/models/link.py b/r2/r2/models/link.py index 206cf484..fe7f5878 100644 --- a/r2/r2/models/link.py +++ b/r2/r2/models/link.py @@ -21,17 +21,22 @@ ################################################################################ from r2.lib.db.thing import Thing, Relation, NotFound, MultiRelation, \ CreationError -from r2.lib.utils import base_url, tup, domain, worker, title_to_url, UrlParser +from r2.lib.utils import base_url, tup, domain, worker, title_to_url, \ + UrlParser, set_last_modified from account import Account from subreddit import Subreddit from printable import Printable +import thing_changes as tc from r2.config import cache from r2.lib.memoize import memoize, clear_memo from r2.lib import utils +from r2.lib.wiki import Wiki from mako.filters import url_escape from r2.lib.strings import strings, Score from r2.lib.db.operators import lower +from r2.lib.db import operators from r2.lib.filters import _force_unicode +from r2.models.subreddit import FakeSubreddit from pylons import c, g, request from pylons.i18n import ungettext @@ -93,19 +98,23 @@ def _by_url(cls, url, sr): return links raise NotFound, 'Link "%s"' % url - + def can_submit(self, user): if c.user_is_admin: return True - elif self.author_id == c.user._id: - # They can submit if they are the author and still have access - # to the subreddit of the article - sr = Subreddit._byID(self.sr_id, data=True) - return sr.can_submit(user) else: - return False - + sr = Subreddit._byID(self.sr_id, data=True) + + if sr.is_editor(c.user): + return True + elif self.author_id == c.user._id: + # They can submit if they are the author and still have access + # to the subreddit of the article + return sr.can_submit(user) + else: + return False + def is_blessed(self): return self.blessed @@ -138,7 +147,8 @@ def resubmit_link(self, sr_url = False): return submit_url @classmethod - def _submit(cls, title, article, author, sr, ip, tags, spam = False): + def _submit(cls, title, article, author, sr, ip, tags, spam = False, date = None): + # Create the Post and commit to db. l = cls(title = title, url = 'self', _spam = spam, @@ -146,13 +156,22 @@ def _submit(cls, title, article, author, sr, ip, tags, spam = False): sr_id = sr._id, lang = sr.lang, ip = ip, - article = article + article = article, + date = date ) l._commit() + + # Now that the post id is known update the Post with the correct permalink. + l.url = l.make_permalink_slow() + l.is_self = True + l._commit() + l.set_url_cache() + # Add tags for tag in tags: l.add_tag(tag) + return l def _summary(self): @@ -200,7 +219,30 @@ def _clicked(cls, user, link): return cls._somethinged(Click, user, link, 'click') def _click(self, user): - return self._something(Click, user, self._clicked, 'click') + try: + saved = Click(user, self, name='click') + saved._commit() + return saved + except CreationError, e: + c = Link._clicked(user,self) + obj = c[(user,self,'click')] + if not obj: + # This is for a possible race. It is possible the row in the db + # has been created but the cache not updated yet. This explicitly + # clears the cache then re-gets from the db + g.log.info("Trying cache clear for lookup : "+str((user,self,'click'))) + Click._uncache(user, self, name='click') + c = Link._clicked(user,self) + obj = c[(user,self,'click')] + if not obj: + raise Exception(user,self,e,c) + obj._date = datetime.now(g.tz) + obj._commit() + return c + + def _getLastClickTime(self, user): + c = Link._clicked(user,self) + return c.get((user, self, 'click')) @classmethod def _hidden(cls, user, link): @@ -267,7 +309,8 @@ def cache_key(wrapped): wrapped.thumbnail, wrapped.moderator_banned, wrapped.render_full, - wrapped.comments_enabled)) + wrapped.comments_enabled, + wrapped.votable)) # htmllite depends on other get params s = ''.join(s) if c.render_style == "htmllite": @@ -278,12 +321,12 @@ def cache_key(wrapped): c.bordercolor])) return s - def make_permalink(self, sr, force_domain = False): + def make_permalink(self, sr, force_domain = False, sr_path = False): from r2.lib.template_helpers import get_domain p = "lw/%s/%s/" % (self._id36, title_to_url(self.title)) - if c.default_sr: + if c.default_sr and not sr_path: res = "/%s" % p - elif not c.cname: + elif sr and not c.cname: res = "/r/%s/%s" % (sr.name, p) elif sr != c.site or force_domain: res = "http://%s/%s" % (get_domain(cname = (c.cname and sr == c.site), @@ -309,8 +352,8 @@ def add_props(cls, user, wrapped): saved = Link._saved(user, wrapped) if user else {} hidden = Link._hidden(user, wrapped) if user else {} - #clicked = Link._clicked(user, wrapped) if user else {} - clicked = {} + clicked = Link._clicked(user, wrapped) if user else {} + #clicked = {} for item in wrapped: show_media = False @@ -332,8 +375,6 @@ def add_props(cls, user, wrapped): else: item.thumbnail = g.default_thumb - item.score = max(0, item.score) - item.domain = (domain(item.url) if not item.is_self else 'self.' + item.subreddit.name) if not hasattr(item,'top_link'): @@ -341,9 +382,9 @@ def add_props(cls, user, wrapped): item.urlprefix = '' item.saved = bool(saved.get((user, item, 'save'))) item.hidden = bool(hidden.get((user, item, 'hide'))) - item.clicked = bool(clicked.get((user, item, 'click'))) + item.clicked = clicked.get((user, item, 'click')) item.num = None - item.score_fmt = Score.number_only + item.score_fmt = Score.signed_number item.permalink = item.make_permalink(item.subreddit) if item.is_self: item.url = item.make_permalink(item.subreddit, force_domain = True) @@ -359,6 +400,12 @@ def add_props(cls, user, wrapped): else: item.hide_score = False + # Don't allow users to vote on their own posts and don't + # allow users to vote on collapsed posts shown when + # viewing comment permalinks. + item.votable = bool(c.user != item.author and + not getattr(item, 'for_comment_permalink', False)) + if c.user_is_loggedin and item.author._id == c.user._id: item.nofollow = False elif item.score <= 1 or item._spam or item.author._spam: @@ -366,6 +413,11 @@ def add_props(cls, user, wrapped): else: item.nofollow = False + if c.user_is_loggedin and item.subreddit.name == c.user.draft_sr_name: + item.draft = True + else: + item.draft = False + if c.user_is_loggedin: incr_counts(wrapped) @@ -382,12 +434,22 @@ def change_subreddit(self, new_sr_id): if self.sr_id != new_sr_id: self.sr_id = new_sr_id self._date = datetime.now(g.tz) + self.url = self.make_permalink_slow() self._commit() + # Comments must be in the same subreddit as the link that + # the comments belong to. This is needed so that if a + # comment is made on a draft link then when the link moves + # to a public subreddit the comments also move and others + # will be able to see and reply to the comment. + for comment in Comment._query(Comment.c.link_id == self._id, data=True): + comment.sr_id = new_sr_id + comment._commit() + def set_blessed(self, is_blessed): if self.blessed != is_blessed: self.blessed = is_blessed - self.date = datetime.now(g.tz) + self._date = datetime.now(g.tz) self._commit() def get_images(self): @@ -523,6 +585,145 @@ def tag_names(self): """Returns just the names of the tags of this article""" return [tag.name for tag in self.get_tags()] + def get_sequence_names(self): + """Returns the names of the sequences""" + return Wiki().sequences_for_article_url(self.url).keys() + + def _next_link_for_tag(self, tag, sort): + """Returns a query navigation by tag using the supplied sort""" + from r2.lib.db import tdb_sql as tdb + import sqlalchemy as sa + + # List of the subreddit ids this user has access to + sr = Subreddit.default() + + # Get a reference to reddit_rel_linktag + linktag_type = tdb.rel_types_id[LinkTag._type_id] + linktag_thing_table = linktag_type.rel_table[0] + + # Get a reference to the reddit_thing_link & reddit_data_link tables + link_type = tdb.types_id[Link._type_id] + link_data_table = link_type.data_table[0] + link_thing_table = link_type.thing_table + + # Subreddit subquery aliased as link_sr + link_sr = sa.select([ + link_data_table.c.thing_id, + sa.cast(link_data_table.c.value, sa.INT).label('sr_id')], + link_data_table.c.key == 'sr_id').alias('link_sr') + + # Determine the date clause based on the sort order requested + if isinstance(sort, operators.desc): + date_clause = link_thing_table.c.date < self._date + sort = sa.desc(link_thing_table.c.date) + else: + date_clause = link_thing_table.c.date > self._date + sort = sa.asc(link_thing_table.c.date) + + query = sa.select([linktag_thing_table.c.thing1_id], + sa.and_(linktag_thing_table.c.thing2_id == tag._id, + linktag_thing_table.c.thing1_id == link_sr.c.thing_id, + linktag_thing_table.c.thing1_id == link_thing_table.c.thing_id, + linktag_thing_table.c.name == 'tag', + link_thing_table.c.spam == False, + link_thing_table.c.deleted == False, + date_clause, + link_sr.c.sr_id == sr._id), + order_by = sort, + limit = 1) + + row = query.execute().fetchone() + return Link._byID(row.thing1_id, data=True) if row else None + + def _link_for_query(self, query): + """Returns a single Link result for the given query""" + results = list(query) + return results[0] if results else None + + # TODO: These navigation methods might be better in their own module + def next_by_tag(self, tag): + return self._next_link_for_tag(tag, operators.asc('_t1_date')) + # TagNamesByTag.append(tag.name) + # IndexesByTag.append(nextIndexByTag); + # nextIndexByTag = nextIndexByTag + 1 + + def prev_by_tag(self, tag): + return self._next_link_for_tag(tag, operators.desc('_t1_date')) + + def next_in_sequence(self, sequence_name): + sequence = Wiki().sequences_for_article_url(self.url).get(sequence_name) + return sequence['next'] if sequence else None + + def prev_in_sequence(self, sequence_name): + sequence = Wiki().sequences_for_article_url(self.url).get(sequence_name) + return sequence['prev'] if sequence else None + + def _nav_query_date_clause(self, sort): + if isinstance(sort, operators.desc): + date_clause = Link.c._date < self._date + else: + date_clause = Link.c._date > self._date + return date_clause + + def _link_nav_query(self, clause = None, sort = None): + sr = Subreddit.default() + + q = Link._query(self._nav_query_date_clause(sort), Link.c._deleted == False, Link.c._spam == False, Link.c.sr_id == sr._id, limit = 1, sort = sort, data = True) + if clause is not None: + q._filter(clause) + return q + + def next_by_author(self): + q = self._link_nav_query(Link.c.author_id == self.author_id, operators.asc('_date')) + return self._link_for_query(q) + + def prev_by_author(self): + q = self._link_nav_query(Link.c.author_id == self.author_id, operators.desc('_date')) + return self._link_for_query(q) + + def next_in_top(self): + q = self._link_nav_query(Link.c.top_link == True, operators.asc('_date')) + return self._link_for_query(q) + + def prev_in_top(self): + q = self._link_nav_query(Link.c.top_link == True, operators.desc('_date')) + return self._link_for_query(q) + + def next_in_promoted(self): + q = self._link_nav_query(Link.c.blessed == True, operators.asc('_date')) + return self._link_for_query(q) + + def prev_in_promoted(self): + q = self._link_nav_query(Link.c.blessed == True, operators.desc('_date')) + return self._link_for_query(q) + + def next_link(self): + q = self._link_nav_query(sort = operators.asc('_date')) + return self._link_for_query(q) + + def prev_link(self): + q = self._link_nav_query(sort = operators.desc('_date')) + return self._link_for_query(q) + + def _commit(self, *a, **kw): + """Detect when we need to invalidate the sidebar recent posts. + + Whenever a post is created we need to invalidate. Also invalidate when + various post attributes are changed (such as moving to a different + subreddit). If the post cache is invalidated the comment one is too. + This is primarily for when a post is banned so that its comments + dissapear from the sidebar too. + """ + + should_invalidate = (not self._created or + frozenset(('title', 'sr_id', '_deleted', '_spam')) & frozenset(self._dirties.keys())) + + Thing._commit(self, *a, **kw) + + if should_invalidate: + g.rendercache.delete('side-posts' + '-' + c.site.name) + g.rendercache.delete('side-comments' + '-' + c.site.name) + # Note that there are no instances of PromotedLink or LinkCompressed, # so overriding their methods here will not change their behaviour # (except for add_props). These classes are used to override the @@ -574,6 +775,9 @@ class InlineArticle(Link): """Exists to gain a different render_class in Wrapped""" _nodb = True +class CommentPermalink(Link): + """Exists to gain a different render_class in Wrapped""" + _nodb = True class TagExists(Exception): pass @@ -686,12 +890,13 @@ def make_cloud(cls, steps, input): class LinkTag(Relation(Link, Tag)): pass - class Comment(Thing, Printable): _data_int_props = Thing._data_int_props + ('reported',) _defaults = dict(reported = 0, moderator_banned = False, - banned_before_moderator = False) + banned_before_moderator = False, + is_html = False, + retracted = False) def _markdown(self): pass @@ -701,20 +906,21 @@ def _delete(self): link._incr('num_comments', -1) @classmethod - def _new(cls, author, link, parent, body, ip, spam = False): - c = Comment(body = body, - link_id = link._id, - sr_id = link.sr_id, - author_id = author._id, - ip = ip) + def _new(cls, author, link, parent, body, ip, spam = False, date = None): + comment = Comment(body = body, + link_id = link._id, + sr_id = link.sr_id, + author_id = author._id, + ip = ip, + date = date) - c._spam = spam + comment._spam = spam #these props aren't relations if parent: - c.parent_id = parent._id + comment.parent_id = parent._id - c._commit() + comment._commit() link._incr('num_comments', 1) @@ -722,13 +928,37 @@ def _new(cls, author, link, parent, body, ip, spam = False): if parent: to = Account._byID(parent.author_id) # only global admins can be message spammed. - if not c._spam or to.name in g.admins: - inbox_rel = Inbox._add(to, c, 'inbox') + if not comment._spam or to.name in g.admins: + inbox_rel = Inbox._add(to, comment, 'inbox') #clear that chache clear_memo('builder.link_comments2', link._id) - return (c, inbox_rel) + # flag search indexer that something has changed + tc.changed(comment) + + #update last modified + set_last_modified(author, 'overview') + set_last_modified(author, 'commented') + set_last_modified(link, 'comments') + + #update the comment cache + from r2.lib.comment_tree import add_comment + add_comment(comment) + + return (comment, inbox_rel) + + def has_children(self): + q = Comment._query(Comment.c.parent_id == self._id, limit=1) + child = list(q) + return len(child)>0 + + def can_delete(self): + if not self._loaded: + self._load() + return (c.user_is_loggedin and self.author_id == c.user._id and \ + self.retracted and not self.has_children()) + @property def subreddit_slow(self): @@ -770,7 +1000,11 @@ def cache_key(wrapped): wrapped.can_ban, wrapped.moderator_banned, wrapped.can_reply, - wrapped.deleted)) + wrapped.deleted, + wrapped.is_html, + wrapped.votable, + wrapped.retracted, + wrapped.can_be_deleted)) s = ''.join(s) return s @@ -789,7 +1023,12 @@ def make_anchored_permalink(self, link=None, sr=None, context=1, anchor=None): def make_permalink_slow(self): l = Link._byID(self.link_id, data=True) return self.make_permalink(l, l.subreddit_slow) - + + def make_permalink_title(self, link): + author = Account._byID(self.author_id, data=True).name + params = {'author' : _force_unicode(author), 'title' : _force_unicode(link.title), 'site' : c.site.title} + return strings.permalink_title % params + @classmethod def add_props(cls, user, wrapped): #fetch parent links @@ -814,8 +1053,8 @@ def add_props(cls, user, wrapped): if not hasattr(item, 'subreddit'): item.subreddit = item.subreddit_slow if hasattr(item, 'parent_id'): - parent = Comment._byID(item.parent_id, True) - parent_author = Account._byID(parent.author_id) + parent = Comment._byID(item.parent_id, data=True) + parent_author = Account._byID(parent.author_id, data=True) item.parent_author = parent_author if not c.full_comment_listing and cids.has_key(item.parent_id): @@ -828,6 +1067,8 @@ def add_props(cls, user, wrapped): item.can_reply = (item.sr_id in can_reply_srs) + # Don't allow users to vote on their own comments + item.votable = bool(c.user != item.author and not item.retracted) # not deleted on profile pages, # deleted if spam and not author or admin @@ -848,7 +1089,23 @@ def add_props(cls, user, wrapped): #will get updated in builder item.num_children = 0 item.score_fmt = Score.points - item.permalink = item.make_anchored_permalink(item.link, item.subreddit, context=None, anchor='comments') + item.permalink = item.make_permalink(item.link, item.subreddit) + item.can_be_deleted = item.can_delete() + + def _commit(self, *a, **kw): + """Detect when we need to invalidate the sidebar recent comments. + + Whenever a comment is created we need to invalidate. Also + invalidate when various comment attributes are changed. + """ + + should_invalidate = (not self._created or + frozenset(('body', '_deleted', '_spam')) & frozenset(self._dirties.keys())) + + Thing._commit(self, *a, **kw) + + if should_invalidate: + g.rendercache.delete('side-comments' + '-' + c.site.name) class InlineComment(Comment): """Exists to gain a different render_class in Wrapped""" diff --git a/r2/r2/models/mail_queue.py b/r2/r2/models/mail_queue.py index f7a4e6f5..df14c037 100644 --- a/r2/r2/models/mail_queue.py +++ b/r2/r2/models/mail_queue.py @@ -29,7 +29,7 @@ from account import Account from r2.lib.db.thing import Thing from email.MIMEText import MIMEText -import sha +import hashlib from r2.lib.memoize import memoize, clear_memo @@ -205,7 +205,7 @@ def add_to_queue(self, user, thing, emails, from_name, fr_addr, date, ip, for email in emails: uid = user._id if user else 0 tid = thing._fullname if thing else "" - key = sha.new(str((email, from_name, uid, tid, ip, kind, body, + key = hashlib.sha1(str((email, from_name, uid, tid, ip, kind, body, datetime.datetime.now()))).hexdigest() s.insert().execute({s.c.to_addr : email, s.c.account_id : uid, diff --git a/r2/r2/models/meetup.py b/r2/r2/models/meetup.py new file mode 100644 index 00000000..c693212e --- /dev/null +++ b/r2/r2/models/meetup.py @@ -0,0 +1,106 @@ +from r2.lib.db.thing import Thing + +import time +from datetime import datetime +from r2.lib.utils import FixedOffset +from r2.lib.db.operators import desc +from geolocator import gislib +# must be here to stop bizarre NotImplementedErrors being raise in the datetime +# method below +import pytz +from r2.models.account import FakeAccount +from r2.models import Subreddit +from account import Account + +from pylons import g +from geolocator.providers import MaxMindCityDataProvider + +class Meetup(Thing): + def datetime(self): + utc_timestamp = datetime.fromtimestamp(self.timestamp, pytz.utc) + tz = FixedOffset(self.tzoffset, None) + return utc_timestamp.astimezone(tz) + + @classmethod + def add_props(cls, user, items): + pass + + @classmethod + def upcoming_meetups_query(cls): + """Return query for all meetups that are in the future""" + # Warning, this timestamp inequality is actually done as a string comparison + # in the db for some reason. BUT, since epoch seconds won't get another digit + # for another 275 years, we're good for now... + return Meetup._query(Meetup.c.timestamp > time.time(), data=True, sort='_date') + + @classmethod + def upcoming_meetups_by_timestamp(cls): + """Return upcoming meetups ordered by soonest first""" + # This doesn't do nice db level paginations, but there should only + # be a smallish number of meetups + query = cls.upcoming_meetups_query() + meetups = list(query) + meetups.sort(key=lambda m: m.timestamp) + return map(lambda m: m._fullname, meetups) + + + @classmethod + def upcoming_meetups_near(cls, location, max_distance, count = 5): + query = cls.upcoming_meetups_query() + meetups = list(query) + + if not location: + meetups.sort(key=lambda m: m.timestamp) + else: + if max_distance: + # Only find nearby ones, sorted by time + meetups = filter(lambda m: m.distance_to(location) <= max_distance, meetups) + meetups.sort(key=lambda m: m.timestamp) + else: + # No max_distance, so just order by distance + meetups.sort(key=lambda m: m.distance_to(location)) + + return meetups[:count] + + def distance_to(self, location): + """ + Returns the distance from this meetup to the passed point. The point is + tuple, (lat, lng) + """ + return gislib.getDistance((self.latitude, self.longitude), location) + + def keep_item(self, item): + return True + + def can_edit(self, user, user_is_admin=False): + """Returns true if the supplied user is allowed to edit this meetup""" + if user is None or isinstance(user, FakeAccount): + return False + elif user_is_admin or self.author_id == user._id: + return True + elif Subreddit._by_name('discussion').is_editor(user): + return True + else: + return False + + @staticmethod + def cache_key(item): + return False + + @staticmethod + def group_cache_key(): + """ Used with CacheUtils.get_key_group_value """ + return "meetup-inc-key" + + def author(self): + return Account._byID(self.author_id, True) + + @staticmethod + def geoLocateIp(ip): + geo = MaxMindCityDataProvider(g.geoip_db_path, "GEOIP_STANDARD") + try: + location = geo.getLocationByIp(ip) + except TypeError: + # geolocate can attempt to index into a None result from GeoIP + location = None + return location diff --git a/r2/r2/models/report.py b/r2/r2/models/report.py index 522d6b96..0da3932b 100644 --- a/r2/r2/models/report.py +++ b/r2/r2/models/report.py @@ -170,7 +170,7 @@ def _by_author_cache(cls, author_id, amount = None): rel_dtable = tdb.rel_types_id[rel._type_id].rel_table[-1] where = [dtable.c.key == 'author_id', - sa.func.substring(dtable.c.value, 1, 1000) == author_id, + sa.func.substring(dtable.c.value, 1, 1000) == str(author_id), dtable.c.thing_id == rel_table.c.thing2_id] if amount is not None: where.extend([rel_table.c.name == str(amount), @@ -455,7 +455,7 @@ def unreport_account(user, correct = True, types = (Link, Comment, Message), by_user_query = sa.and_(table.c.thing_id == dtable.c.thing_id, dtable.c.key == 'author_id', - sa.func.substring(dtable.c.value, 1, 1000) == user._id) + sa.func.substring(dtable.c.value, 1, 1000) == str(user._id)) s = sa.select(["count(*)"], sa.and_(by_user_query, table.c.spam == (not correct))) @@ -480,7 +480,7 @@ def unreport_account(user, correct = True, types = (Link, Comment, Message), u = """UPDATE %(table)s SET spam='%(spam)s' FROM %(dtable)s WHERE %(table)s.thing_id = %(dtable)s.thing_id AND %(dtable)s.key = 'author_id' - AND substring(%(dtable)s.value, 1, 1000) = %(author_id)s""" + AND substring(%(dtable)s.value, 1, 1000) = '%(author_id)s'""" u = u % dict(spam = 't' if correct else 'f', table = table.name, dtable = dtable.name, diff --git a/r2/r2/models/subreddit.py b/r2/r2/models/subreddit.py index 4df70808..76cd08d1 100644 --- a/r2/r2/models/subreddit.py +++ b/r2/r2/models/subreddit.py @@ -6,16 +6,16 @@ # software over a computer network and provide for limited attribution for the # Original Developer. In addition, Exhibit A has been modified to be consistent # with Exhibit B. -# +# # Software distributed under the License is distributed on an "AS IS" basis, # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for # the specific language governing rights and limitations under the License. -# +# # The Original Code is Reddit. -# +# # The Original Developer is the Initial Developer. The Initial Developer of the # Original Code is CondeNet, Inc. -# +# # All portions of the code written by CondeNet are Copyright (c) 2006-2008 # CondeNet, Inc. All Rights Reserved. ################################################################################ @@ -52,7 +52,9 @@ class Subreddit(Thing, Printable): valid_votes = 0, show_media = False, domain = None, - default_listing = 'hot' + default_listing = 'hot', + post_karma_multiplier = g.post_karma_multiplier, + posts_per_page_multiplier = 1 ) sr_limit = 50 @@ -141,6 +143,10 @@ def _by_domain(cls, domain): def moderators(self): return self.moderator_ids() + @property + def editors(self): + return self.editor_ids() + @property def contributors(self): return self.contributor_ids() @@ -171,9 +177,14 @@ def can_submit(self, user): return True elif self.is_banned(user): return False + elif self.is_moderator(user) or self.is_editor(user): + # moderators and editors can always submit + return True + elif self == Subreddit._by_name('discussion') and user.safe_karma < g.discussion_karma_to_post: + return False elif self.type == 'public': return True - elif self.is_moderator(user) or self.is_contributor(user): + elif self.is_contributor(user): #restricted/private require contributorship return True elif self == Subreddit._by_name(g.default_sr) and user.safe_karma >= g.karma_to_post: @@ -210,8 +221,8 @@ def should_ratelimit(self, user, kind): rl_karma = g.MIN_RATE_LIMIT_COMMENT_KARMA else: rl_karma = g.MIN_RATE_LIMIT_KARMA - - return not (self.is_special(user) or + + return not (self.is_special(user) or user.karma(kind, self) >= rl_karma) def can_view(self, user): @@ -253,7 +264,7 @@ def rising_srs(self): def get_links(self, sort, time, link_cls = None): from r2.lib.db import queries from r2.models import Link - + if not link_cls: link_cls = Link return queries.get_links(self, sort, time, link_cls) @@ -317,11 +328,13 @@ def default_srs(cls, lang, ids = False, limit = None): if not c.over18: pop_reddits._filter(Subreddit.c.over_18 == False) + pop_reddits._filter(Subreddit.c.name != 'discussion') + pop_reddits = list(pop_reddits) if not pop_reddits and lang != 'en': pop_reddits = cls.default_srs('en') - + return [s._id for s in pop_reddits] if ids else list(pop_reddits) @classmethod @@ -359,7 +372,16 @@ def submit_sr(cls, user): sub_ids = cls.user_subreddits(user, False) srs = Subreddit._byID(sub_ids, True, return_dict = False) - srs = [s for s in srs if s.can_submit(user)] + srs = [s for s in srs if s.can_submit(user) or s.name == g.default_sr] + + # Add the discussion subreddit manually. Need to do this because users + # are not subscribed to it. + try: + discussion_sr = Subreddit._by_name('discussion') + if discussion_sr._id not in sub_ids and discussion_sr.can_submit(user): + srs.insert(0, discussion_sr) + except NotFound: + pass srs.sort(key=lambda a:a.title) return srs @@ -404,7 +426,7 @@ def add_image(self, name, max_num = None): # copy and blank out the images list to flag as _dirty l = self.images self.images = None - # initialize the /empties/ list + # initialize the /empties/ list l.setdefault('/empties/', []) try: num = l['/empties/'].pop() # grab old number if we can @@ -485,7 +507,7 @@ def get_links(self, sort, time, link_cls = None): if time != 'all': q._filter(queries.db_times[time]) return q - + class AllSR(FakeSubreddit): name = 'all' title = 'All' @@ -493,7 +515,7 @@ class AllSR(FakeSubreddit): def get_links(self, sort, time, link_cls = None): from r2.models import Link from r2.lib.db import queries - + if not link_cls: link_cls = Link q = link_cls._query(sort = queries.db_sort(sort)) @@ -506,7 +528,7 @@ class DefaultSR(FakeSubreddit): #notice the space before reddit.com name = g.default_sr path = '/' - header = 'http://static.reddit.com/reddit.com.header.png' + header = '/static/logo_trans.png' def get_links_sr_ids(self, sr_ids, sort, time, link_cls = None): from r2.lib.db import queries @@ -590,13 +612,13 @@ def path(self): def __init__(self, domain): FakeSubreddit.__init__(self) self.domain = domain - self.name = domain + self.name = domain self.title = domain + ' ' + _('on lesswrong.com') def get_links(self, sort, time, link_cls = None): from r2.lib.db import queries return queries.get_domain_links(self.domain, sort, time) - + Sub = SubSR() Friends = FriendsSR() All = AllSR() @@ -604,6 +626,7 @@ def get_links(self, sort, time, link_cls = None): class SRMember(Relation(Subreddit, Account)): pass Subreddit.__bases__ += (UserRel('moderator', SRMember), + UserRel('editor', SRMember), UserRel('contributor', SRMember), UserRel('subscriber', SRMember), UserRel('banned', SRMember)) diff --git a/r2/r2/models/vote.py b/r2/r2/models/vote.py index 87c37b9f..aea4bf89 100644 --- a/r2/r2/models/vote.py +++ b/r2/r2/models/vote.py @@ -19,6 +19,8 @@ # All portions of the code written by CondeNet are Copyright (c) 2006-2008 # CondeNet, Inc. All Rights Reserved. ################################################################################ +from __future__ import with_statement + from r2.lib.db.thing import MultiRelation, Relation, thing_prefix, cache from r2.lib.utils import tup, timeago from r2.lib.db.operators import ip_network @@ -55,50 +57,65 @@ def vote(cls, sub, obj, dir, ip, spam = False, organic = False): from admintools import valid_user, valid_thing, update_score from r2.lib.count import incr_counts - sr = obj.subreddit_slow - kind = obj.__class__.__name__.lower() - karma = sub.karma(kind, sr) - - #check for old vote - rel = cls.rel(sub, obj) - oldvote = list(rel._query(rel.c._thing1_id == sub._id, - rel.c._thing2_id == obj._id, - data = True)) - - amount = 1 if dir is True else 0 if dir is None else -1 - - is_new = False - #old vote - if len(oldvote): - v = oldvote[0] - oldamount = int(v._name) - v._name = str(amount) - - #these still need to be recalculated - old_valid_thing = v.valid_thing - v.valid_thing = (valid_thing(v, karma) - and v.valid_thing - and not spam) - v.valid_user = (v.valid_user - and v.valid_thing - and valid_user(v, sr, karma)) - #new vote - else: - is_new = True - oldamount = 0 - v = rel(sub, obj, str(amount)) - v.author_id = obj.author_id - v.ip = ip - old_valid_thing = v.valid_thing = (valid_thing(v, karma) and - not spam) - v.valid_user = v.valid_thing and valid_user(v, sr, karma) - if organic: - v.organic = organic - - v._commit() - - up_change, down_change = score_changes(amount, oldamount) - + # An account can only perform 1 voting operation at a time. + with g.make_lock('account_%s_voting' % sub._id): + kind = obj.__class__.__name__.lower() + + # If downvoting ensure that the user has enough karma, it + # will raise an exception if not. + if dir == False: + sub.check_downvote(kind) + + # Do the voting. + sr = obj.subreddit_slow + karma = sub.karma(kind, sr) + + #check for old vote + rel = cls.rel(sub, obj) + oldvote = list(rel._query(rel.c._thing1_id == sub._id, + rel.c._thing2_id == obj._id, + data = True)) + + amount = 1 if dir is True else 0 if dir is None else -1 + + is_new = False + #old vote + if len(oldvote): + v = oldvote[0] + oldamount = int(v._name) + v._name = str(amount) + + #these still need to be recalculated + old_valid_thing = getattr(v, 'valid_thing', True) + v.valid_thing = (valid_thing(v, karma) + and v.valid_thing + and not spam) + v.valid_user = (v.valid_user + and v.valid_thing + and valid_user(v, sr, karma)) + #new vote + else: + is_new = True + oldamount = 0 + v = rel(sub, obj, str(amount)) + v.author_id = obj.author_id + v.ip = ip + old_valid_thing = v.valid_thing = (valid_thing(v, karma) and + not spam) + v.valid_user = v.valid_thing and valid_user(v, sr, karma) + if organic: + v.organic = organic + + v._commit() + + # Record that this account has made a downvote and + # immediately release the lock since both the downvote + # count and the vote have been updated. + up_change, down_change = score_changes(amount, oldamount) + if down_change: + sub.incr_downvote(down_change, kind) + + # Continue by updating karmas. update_score(obj, up_change, down_change, v.valid_thing, old_valid_thing) diff --git a/r2/r2/public/google8aa224485074cec1.html b/r2/r2/public/google8aa224485074cec1.html new file mode 100644 index 00000000..d667f462 --- /dev/null +++ b/r2/r2/public/google8aa224485074cec1.html @@ -0,0 +1 @@ +google-site-verification: google8aa224485074cec1.html \ No newline at end of file diff --git a/r2/r2/public/googlea26ba8329f727095.html b/r2/r2/public/googlea26ba8329f727095.html new file mode 100644 index 00000000..47b21357 --- /dev/null +++ b/r2/r2/public/googlea26ba8329f727095.html @@ -0,0 +1 @@ +google-site-verification: googlea26ba8329f727095.html \ No newline at end of file diff --git a/r2/r2/public/googlefaf120fe6e794df0.html b/r2/r2/public/googlefaf120fe6e794df0.html new file mode 100644 index 00000000..035aa468 --- /dev/null +++ b/r2/r2/public/googlefaf120fe6e794df0.html @@ -0,0 +1 @@ +google-site-verification: googlefaf120fe6e794df0.html diff --git a/r2/r2/public/robots.txt b/r2/r2/public/robots.txt new file mode 100644 index 00000000..e5e0fcf7 --- /dev/null +++ b/r2/r2/public/robots.txt @@ -0,0 +1,8 @@ +User-Agent: * +Disallow: + +Disallow: /framebuster +Disallow: /login +Disallow: /search +Disallow: /r/friends + diff --git a/r2/r2/public/static/FHI_diamond.png b/r2/r2/public/static/FHI_diamond.png index 422d5495..62ccd3db 100644 Binary files a/r2/r2/public/static/FHI_diamond.png and b/r2/r2/public/static/FHI_diamond.png differ diff --git a/r2/r2/public/static/SIAI.png b/r2/r2/public/static/SIAI.png new file mode 100644 index 00000000..134858c7 Binary files /dev/null and b/r2/r2/public/static/SIAI.png differ diff --git a/r2/r2/public/static/accept.png b/r2/r2/public/static/accept.png new file mode 100755 index 00000000..89c8129a Binary files /dev/null and b/r2/r2/public/static/accept.png differ diff --git a/r2/r2/public/static/agree-button.gif b/r2/r2/public/static/agree-button.gif new file mode 100644 index 00000000..e4da00ea Binary files /dev/null and b/r2/r2/public/static/agree-button.gif differ diff --git a/r2/r2/public/static/ajax-loader.gif b/r2/r2/public/static/ajax-loader.gif new file mode 100644 index 00000000..fe03976a Binary files /dev/null and b/r2/r2/public/static/ajax-loader.gif differ diff --git a/r2/r2/public/static/antikibitzer.css b/r2/r2/public/static/antikibitzer.css new file mode 100755 index 00000000..7f884122 --- /dev/null +++ b/r2/r2/public/static/antikibitzer.css @@ -0,0 +1,27 @@ +#side-status div.userinfo span.score { + display:none; +} +div.meta span.votes { + display:none; +} +div.meta span.author a { + display:none; +} +div.comment div.parent span.author a { + display:none; +} +div.comment-meta span.comment-author a { + display:none; +} +div.comment-meta span.votes { + display:none; +} +#side-comments span.tagline a { + display:none; +} +#side-comments span.tagline span{ + display:none; +} +#side-posts span { + display:none; +} diff --git a/r2/r2/public/static/antikibitzer.js b/r2/r2/public/static/antikibitzer.js new file mode 100644 index 00000000..c5042c13 --- /dev/null +++ b/r2/r2/public/static/antikibitzer.js @@ -0,0 +1,46 @@ +// LessWrong anti-kibitzer version 0.5 +// Originally authored by Marcello Herreshoff +// Versions 0.44 and further by Morendil +// Allows the user to toggle whether point values and commenters are shown on LW. + +var kib_hidden = true; + +function toggle_kibitzing(){ + kbutton = document.getElementById("kbutton") + if(kib_hidden){ + kib_hidden = false; + kbutton.value = "Turn Kibitzing Off"; + }else{ + kib_hidden = true; + kbutton.value = "Turn Kibitzing On"; + } + apply_kibitzing() +} + +function apply_kibitzing(){ + function ak_hide(n) { n.style.display = "none"; } + function ak_show(n) { n.style.display = "inline"; } + + // locate the AK stylesheet + var css; + for (var i=0; i < document.styleSheets.length; i++) { + if (document.styleSheets[i].href.indexOf("antikibitzer") > 0) + css = document.styleSheets[i]; + } + + var rules = css.cssRules; + if (!rules) rules = css.rules; // IE compatibility + + var nbRules = rules.length; + for (var i=0; i < nbRules; i++) { + var rule = rules[i]; + if (kib_hidden) ak_hide(rule); else ak_show(rule); + } +} + +var div = document.createElement("div"); +var pos = "fixed"; +if (document.styleSheets[1].rules) pos = "absolute"; // IE compatibility +div.innerHTML = "<div id='kfloat' style='position:"+pos+";top:0px;right:0px'><form><input id='kbutton' type='button' value='Turn Kibitzing On' onclick='toggle_kibitzing()'/></form>"; + +document.body.appendChild(div); diff --git a/r2/r2/public/static/articlenav-next-grey.gif b/r2/r2/public/static/articlenav-next-grey.gif new file mode 100644 index 00000000..42f9df3b Binary files /dev/null and b/r2/r2/public/static/articlenav-next-grey.gif differ diff --git a/r2/r2/public/static/articlenav-next.gif b/r2/r2/public/static/articlenav-next.gif new file mode 100644 index 00000000..f2b49d82 Binary files /dev/null and b/r2/r2/public/static/articlenav-next.gif differ diff --git a/r2/r2/public/static/articlenav-prev-grey.gif b/r2/r2/public/static/articlenav-prev-grey.gif new file mode 100644 index 00000000..4f075b62 Binary files /dev/null and b/r2/r2/public/static/articlenav-prev-grey.gif differ diff --git a/r2/r2/public/static/articlenav-prev.gif b/r2/r2/public/static/articlenav-prev.gif new file mode 100644 index 00000000..793e1f40 Binary files /dev/null and b/r2/r2/public/static/articlenav-prev.gif differ diff --git a/r2/r2/public/static/background-discussion.jpg b/r2/r2/public/static/background-discussion.jpg new file mode 100644 index 00000000..4bb1512c Binary files /dev/null and b/r2/r2/public/static/background-discussion.jpg differ diff --git a/r2/r2/public/static/comments.js b/r2/r2/public/static/comments.js index 8441975a..785449d8 100644 --- a/r2/r2/public/static/comments.js +++ b/r2/r2/public/static/comments.js @@ -1,6 +1,38 @@ +/* Fill in the given help content, and attach a handler for the form invalidate submission */ +function fillInHelpDiv(elem, content) { + elem.innerHTML = content + elem.select('.invalidate').first().observe('submit', function(e) { + e.stop(); + elem.innerHTML = "Loading..." + new Ajax.Request(this.getAttribute('action'), { + method: 'post', + parameters: {'skiplayout': 'on'}, + onSuccess: function(response) { + fillInHelpDiv(elem, response.responseText) + } + }) + }); +} + +/* Perform an ajax get for the help content, and fill in the element */ +function getHelpContent(elem) { + new Ajax.Request("/wiki/Commentmarkuphelp", { + method: 'get', + parameters: {'skiplayout': 'on'}, + onSuccess: function(response) { + fillInHelpDiv(elem, response.responseText) + }}); +} + function helpon(link, what, newlabel) { var id = _id(link); - show(what + id); + show(what+id); + + /* If not loaded help content, load it! */ + if ($(what+id).innerHTML.indexOf('Loading')==0) { + getHelpContent($(what+id)) + } + var oldlabel = link.innerHTML; if(newlabel) { link.innerHTML = newlabel @@ -54,6 +86,34 @@ Comment.prototype = new Thing(); Comment.del = Thing.del; +Comment.prototype.size_textarea = function() { + var textarea = $("comment_reply_" + this._id), + s, parentwidth; + + // if the box-sizing CSS property works, don't bother manually sizing it + if (typeof textarea.style.boxSizing == "string" || + typeof textarea.style.MozBoxSizing == "string" || + typeof textarea.style.WebkitBoxSizing == "string") { + return; + } + + if (window.getComputedStyle) { + s = window.getComputedStyle(textarea,null); + parentwidth = parseInt(window.getComputedStyle(textarea.parentNode,null).width); + } + else { // IE <= 8 + s = textarea.currentStyle; + // padding-left and padding-right are 0, so clientWidth is the inner width + parentwidth = textarea.parentNode.clientWidth; + } + + var padding = parseInt(s.paddingLeft) + parseInt(s.paddingRight) + + parseInt(s.marginLeft) + parseInt(s.marginRight) + + parseInt(s.borderLeftWidth) + parseInt(s.borderRightWidth); + new_width = (parentwidth - padding) + 'px'; + textarea.setStyle({'width': new_width}); +} + Comment.prototype._edit = function(listing, where, text) { var edit_box = comment_reply(this._id); if (edit_box.parentNode != listing.listing) { @@ -71,6 +131,7 @@ Comment.prototype._edit = function(listing, where, text) { clearTitle(box); box.value = text; show(edit_box); + this.size_textarea(); return edit_box; }; @@ -101,7 +162,7 @@ Comment.comment = function(r) { vl[id] = r.vl; }; -// Commenting on a link is handled by the Comment API so defer to it +/* Commenting on a link is handled by the Comment API so defer to it */ Link.comment = Comment.comment; Comment.morechildren = function(r) { @@ -113,6 +174,8 @@ Comment.morechildren = function(r) { var c = new Comment(r.id); c.show(true); vl[r.id] = r.vl; + + highlightNewComments(); }; Comment.editcomment = function(r) { @@ -138,16 +201,55 @@ Comment.prototype.uncollapse = function() { hide(this.get('collapsed')); }; +function all_morechildren(elem) { + $$('.morechildren a').each(function(ahref, i) { + ahref.simulate('click'); + }); + return false; +}; function morechildren(form, link_id, children, depth) { var id = _id(form); + //console.log("id="+id+" form="+form+" link_id="+link_id+" children="+children+" depth="+depth); form.innerHTML = _global_loading_tag; form.style.color="red"; redditRequest('morechildren', {link_id: link_id, children: children, depth: depth, id: id}); return false; +}; + +function getAttrTime(e) { return parseInt(e.readAttribute('time')); } + +function highlightNewComments() { + var lastViewed = $('lastViewed') + if (!lastViewed) + return; + + var last = getAttrTime(lastViewed); + if (last<=0) + return; + $$('div.comment').each(function(div, i) { + var t = getAttrTime(div.select('.comment-date')[0]); + if (last<t) { + div.addClassName('new-comment') + } + }); } +// Display the 'load all comments' if there any to be loaded +document.observe("dom:loaded", function() { + if ($$('.morechildren a').length > 0) + $$('#loadAllComments')[0].show(); + + highlightNewComments(); + + // select the first comment form on the page + var real = $$('form.commentreply:not(#commentform_)'); + if (real.length > 0){ + new Comment(_id(real[0])).size_textarea(); + } +}); + function editcomment(id) { new Comment(id).edit(); @@ -168,14 +270,35 @@ function reply(id) { }; function chkcomment(form) { + if(checkInProgress(form)) { + var r = confirm("Request still in progress\n(click Cancel to attempt to stop the request)"); + if (r==false) + tagInProgress(form, false); + return false; + } + + tagInProgress(form, true); if(form.replace.value) { - return post_form(form, 'editcomment', null, null, true); + return post_form(form, 'editcomment', null, null, true, null, + function() { tagInProgress(form, false)}); } else { - return post_form(form, 'comment', null, null, true); + return post_form(form, 'comment', null, null, true, null, + function() { tagInProgress(form, false)}); } }; +function tagInProgress(form, inProgress) { + if (inProgress) + form.addClassName("inprogress"); + else + form.removeClassName("inprogress"); +} + +function checkInProgress(form) { + return form.hasClassName("inprogress"); +} + function clearTitle(box) { if (box.rows && box.rows < 7 || box.style.color == "gray" || diff --git a/r2/r2/public/static/disagree-button.gif b/r2/r2/public/static/disagree-button.gif new file mode 100644 index 00000000..d7216a4d Binary files /dev/null and b/r2/r2/public/static/disagree-button.gif differ diff --git a/r2/r2/public/static/disclosure-triangle.gif b/r2/r2/public/static/disclosure-triangle.gif new file mode 100644 index 00000000..09540fa0 Binary files /dev/null and b/r2/r2/public/static/disclosure-triangle.gif differ diff --git a/r2/r2/public/static/discussion.css b/r2/r2/public/static/discussion.css new file mode 100644 index 00000000..e0a290e8 --- /dev/null +++ b/r2/r2/public/static/discussion.css @@ -0,0 +1,73 @@ +#content .list { + margin-bottom: 0.5em; + position: relative; + clear: both; +} + +#content .list h2 { + float: left; + margin-left: 35px; + z-index: 1; +} + +#content .list .meta { + float: right; + text-align: right; + margin-bottom: 0; + z-index: 2; +} + +#content .list .meta span { + display: inline-block; + float: none; +} + +#content .list .meta .votes { + position: absolute; + left: 0; + top: 0; +} + +#content .list .meta .votes .votes { + position: relative; +} + +#content .list .content { + display: none; +} + +#content .list .tools { + margin: 0; + padding: 0 0 0 30px; + line-height: 1; + border: none; + background: none; + float: right; +} + +#content .list .tools * { + display: none; +} + +#content .list .tools a.comment { + display: block; + float: right; + line-height: 22px; + margin-right: 15px; +} + +#content .nextprev { + clear: both; +} + +#header.discussion { + background: url(/static/background-discussion.jpg) no-repeat top left; +} + +#header.discussion #header-img { + height: 55px; +} + +#header.discussion #tagline { + display: none; +} \ No newline at end of file diff --git a/r2/r2/public/static/edit.png b/r2/r2/public/static/edit.png new file mode 100644 index 00000000..338c54d8 Binary files /dev/null and b/r2/r2/public/static/edit.png differ diff --git a/r2/r2/public/static/event.simulate.js b/r2/r2/public/static/event.simulate.js new file mode 100644 index 00000000..06a35643 --- /dev/null +++ b/r2/r2/public/static/event.simulate.js @@ -0,0 +1,67 @@ +// Taken from the 'protolicious' library +// https://github.com/kangax/protolicious/blob/master/event.simulate.js + +/** +* Event.simulate(@element, eventName[, options]) -> Element +* +* - @element: element to fire event on +* - eventName: name of event to fire (only MouseEvents and HTMLEvents interfaces are supported) +* - options: optional object to fine-tune event properties - pointerX, pointerY, ctrlKey, etc. +* +* $('foo').simulate('click'); // => fires "click" event on an element with id=foo +* +**/ +(function(){ + + var eventMatchers = { + 'HTMLEvents': /^(?:load|unload|abort|error|select|change|submit|reset|focus|blur|resize|scroll)$/, + 'MouseEvents': /^(?:click|mouse(?:down|up|over|move|out))$/ + } + var defaultOptions = { + pointerX: 0, + pointerY: 0, + button: 0, + ctrlKey: false, + altKey: false, + shiftKey: false, + metaKey: false, + bubbles: true, + cancelable: true + } + + Event.simulate = function(element, eventName) { + var options = Object.extend(Object.clone(defaultOptions), arguments[2] || { }); + var oEvent, eventType = null; + + element = $(element); + + for (var name in eventMatchers) { + if (eventMatchers[name].test(eventName)) { eventType = name; break; } + } + + if (!eventType) + throw new SyntaxError('Only HTMLEvents and MouseEvents interfaces are supported'); + + if (document.createEvent) { + oEvent = document.createEvent(eventType); + if (eventType == 'HTMLEvents') { + oEvent.initEvent(eventName, options.bubbles, options.cancelable); + } + else { + oEvent.initMouseEvent(eventName, options.bubbles, options.cancelable, document.defaultView, + options.button, options.pointerX, options.pointerY, options.pointerX, options.pointerY, + options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, element); + } + element.dispatchEvent(oEvent); + } + else { + options.clientX = options.pointerX; + options.clientY = options.pointerY; + oEvent = Object.extend(document.createEventObject(), options); + element.fireEvent('on' + eventName, oEvent); + } + return element; + } + + Element.addMethods({ simulate: Event.simulate }); +})() diff --git a/r2/r2/public/static/exclamation.png b/r2/r2/public/static/exclamation.png new file mode 100755 index 00000000..c37bd062 Binary files /dev/null and b/r2/r2/public/static/exclamation.png differ diff --git a/r2/r2/public/static/favicon.ico b/r2/r2/public/static/favicon.ico new file mode 100644 index 00000000..bbe695b8 Binary files /dev/null and b/r2/r2/public/static/favicon.ico differ diff --git a/r2/r2/public/static/feed.png b/r2/r2/public/static/feed.png index b38097ca..9dc9c4a5 100644 Binary files a/r2/r2/public/static/feed.png and b/r2/r2/public/static/feed.png differ diff --git a/r2/r2/public/static/footer-bg.gif b/r2/r2/public/static/footer-bg.gif new file mode 100644 index 00000000..9ca313ad Binary files /dev/null and b/r2/r2/public/static/footer-bg.gif differ diff --git a/r2/r2/public/static/header_background.jpg b/r2/r2/public/static/header_background.jpg index 994b3fa1..2194d232 100644 Binary files a/r2/r2/public/static/header_background.jpg and b/r2/r2/public/static/header_background.jpg differ diff --git a/r2/r2/public/static/hide.png b/r2/r2/public/static/hide.png new file mode 100644 index 00000000..fd681440 Binary files /dev/null and b/r2/r2/public/static/hide.png differ diff --git a/r2/r2/public/static/ie6.css b/r2/r2/public/static/ie6.css old mode 100755 new mode 100644 index ac7fc3d4..cb1d2e08 --- a/r2/r2/public/static/ie6.css +++ b/r2/r2/public/static/ie6.css @@ -1,15 +1,19 @@ -/* IE6 PNG Fix - Requires /images/blank.gif - Set correct path in /js/iepngfix.htc +/* IE6 PNG Fix -------------------------------------------------------------------------------------- */ -#sample { - behavior: url("/static/iepngfix.htc"); +#header img, +#side-feed a img, +div.footer div.reddit img { + behavior: url(/iepngfix/iepngfix.htc); } + /* Header -------------------------------------------------------------------------------------- */ -#gradient, a#logo img, a#fhi img { - behavior: url(/iepngfix/iepngfix.htc); +a#logo { + display: inline; } + /* Post -------------------------------------------------------------------------------------- */ div.tools ul { @@ -18,6 +22,26 @@ div.tools ul { } div.tools ul li { display: inline; + float: right; +} +div.tools div.tags { + display: inline; +} +div.tools div.articlenavigation ul { + float: none; +} +div.tools div.articlenavigation ul li { + display: block; + float: none; + height: 15px; + overflow: hidden; +} + + +/* Meetup +-------------------------------------------------------------------------------------- */ +body.meetup-index #map { + display: inline; } @@ -30,6 +54,7 @@ form#comment-listing select { width: 150px; } + /* Sidebar -------------------------------------------------------------------------------------- */ div.sidebox select { @@ -39,14 +64,12 @@ div.sidebox select { width: auto; } #side-status ul.userlinks li a { - width: 90px; + width: 96px; } #side-comments h3 { width: 175px; } - -/* Nav Bar --------------------------------------------------------------------------------------- */ -ul#nav { - behavior: expression(this.firstChild.style.borderLeft = "none"); -} \ No newline at end of file +#side-status .extrainfo dd { + overflow: hidden; + width: 140px; +} diff --git a/r2/r2/public/static/ie7below.css b/r2/r2/public/static/ie7below.css new file mode 100644 index 00000000..782af52a --- /dev/null +++ b/r2/r2/public/static/ie7below.css @@ -0,0 +1,31 @@ +/* Clearing +-------------------------------------------------------------------------------------- */ +#main, +#content, +ul#nav, +ul#filternav, +form div.row, +div.post, +div.post div.content, +div.meta, +div.tools, +div.tools div.boxright, +div.tools div.boxright ul, +#comments, +div.comment, +div.comment div.entry, +div.comment-meta, +div.comment-links, +div.comment-links ul, +ul#rightnav, +div.sidebox, +div.footer, +div.footer ul.footer-links, +.clear { zoom: 1; } + + +/* Everything Else +-------------------------------------------------------------------------------------- */ +div.tools div.boxright { + text-align: right; +} diff --git a/r2/r2/public/static/ie8below.css b/r2/r2/public/static/ie8below.css new file mode 100644 index 00000000..f040d7c4 --- /dev/null +++ b/r2/r2/public/static/ie8below.css @@ -0,0 +1,36 @@ +div.tools ul li.first-child { + border-left: none; +} +ul#nav li.last-child { + border-right: none; +} +ul#filternav li.last-child { + border-right: none; +} +div.comment-links ul li.last-child, div.message-links ul li.last-child { + border-right: none; +} +#sidebar ul li.last-child { + margin: 0; +} +ul#rightnav li.last-child { + border-right: none; + padding-right: 0; +} +#side-comments div.inline-comment.last-child { + margin: 0; +} +#side-posts div.reddit-link.last-child { + margin: 0; +} +#side-contributors div.contributors div.user.last-child { + margin: 0; +} +div.footer ul.footer-links li.last-child { + border-right: none; + margin-right: 0; + padding-right: 0; +} +.ui-tooltip-lesswrong .ui-tooltip-tip { + background: url(/static/qtip-tip-ie.gif) no-repeat top center !important; +} diff --git a/r2/r2/public/static/ie8below.js b/r2/r2/public/static/ie8below.js new file mode 100644 index 00000000..45101f76 --- /dev/null +++ b/r2/r2/public/static/ie8below.js @@ -0,0 +1,11 @@ +(function ($) { + +$(document).ready(function() { + + $('ul li:first-child').addClass('first-child'); + $('ul li:last-child').addClass('last-child'); + $('#side-comments div.inline-comment:last-child, #side-posts div.reddit-link:last-child, #side-contributors div.contributors div.user:last-child').addClass('last-child'); + +}); + +})(jQuery); \ No newline at end of file diff --git a/r2/r2/public/static/imported/2007/08/10/monsterwithgirl_2.jpg b/r2/r2/public/static/imported/2007/08/10/monsterwithgirl_2.jpg new file mode 100644 index 00000000..88c77bcc Binary files /dev/null and b/r2/r2/public/static/imported/2007/08/10/monsterwithgirl_2.jpg differ diff --git a/r2/r2/public/static/imported/2007/08/22/smallerstorm_2.jpg b/r2/r2/public/static/imported/2007/08/22/smallerstorm_2.jpg new file mode 100644 index 00000000..da0dd59a Binary files /dev/null and b/r2/r2/public/static/imported/2007/08/22/smallerstorm_2.jpg differ diff --git a/r2/r2/public/static/imported/2007/09/19/lindacorrelation.png b/r2/r2/public/static/imported/2007/09/19/lindacorrelation.png new file mode 100644 index 00000000..f209da5f Binary files /dev/null and b/r2/r2/public/static/imported/2007/09/19/lindacorrelation.png differ diff --git a/r2/r2/public/static/imported/2007/10/21/keysarselfanchoring_2.png b/r2/r2/public/static/imported/2007/10/21/keysarselfanchoring_2.png new file mode 100644 index 00000000..60aa61ba Binary files /dev/null and b/r2/r2/public/static/imported/2007/10/21/keysarselfanchoring_2.png differ diff --git a/r2/r2/public/static/imported/2007/10/31/scary_3.jpg b/r2/r2/public/static/imported/2007/10/31/scary_3.jpg new file mode 100644 index 00000000..564a0899 Binary files /dev/null and b/r2/r2/public/static/imported/2007/10/31/scary_3.jpg differ diff --git a/r2/r2/public/static/imported/2007/11/16/price.png b/r2/r2/public/static/imported/2007/11/16/price.png new file mode 100644 index 00000000..4e41e0a7 Binary files /dev/null and b/r2/r2/public/static/imported/2007/11/16/price.png differ diff --git a/r2/r2/public/static/imported/2007/11/27/hsee1998.png b/r2/r2/public/static/imported/2007/11/27/hsee1998.png new file mode 100644 index 00000000..aff040ef Binary files /dev/null and b/r2/r2/public/static/imported/2007/11/27/hsee1998.png differ diff --git a/r2/r2/public/static/imported/2007/12/25/asch2.png b/r2/r2/public/static/imported/2007/12/25/asch2.png new file mode 100644 index 00000000..d794aa72 Binary files /dev/null and b/r2/r2/public/static/imported/2007/12/25/asch2.png differ diff --git a/r2/r2/public/static/imported/2008/01/09/squares.png b/r2/r2/public/static/imported/2008/01/09/squares.png new file mode 100644 index 00000000..5669e45a Binary files /dev/null and b/r2/r2/public/static/imported/2008/01/09/squares.png differ diff --git a/r2/r2/public/static/imported/2008/02/09/blegg1_3.png b/r2/r2/public/static/imported/2008/02/09/blegg1_3.png new file mode 100644 index 00000000..98e84a56 Binary files /dev/null and b/r2/r2/public/static/imported/2008/02/09/blegg1_3.png differ diff --git a/r2/r2/public/static/imported/2008/02/09/blegg2.png b/r2/r2/public/static/imported/2008/02/09/blegg2.png new file mode 100644 index 00000000..9163f956 Binary files /dev/null and b/r2/r2/public/static/imported/2008/02/09/blegg2.png differ diff --git a/r2/r2/public/static/imported/2008/02/10/blegg3.png b/r2/r2/public/static/imported/2008/02/10/blegg3.png new file mode 100644 index 00000000..c45072f5 Binary files /dev/null and b/r2/r2/public/static/imported/2008/02/10/blegg3.png differ diff --git a/r2/r2/public/static/imported/2008/02/12/blegg4_4.png b/r2/r2/public/static/imported/2008/02/12/blegg4_4.png new file mode 100644 index 00000000..f66de7df Binary files /dev/null and b/r2/r2/public/static/imported/2008/02/12/blegg4_4.png differ diff --git a/r2/r2/public/static/imported/2008/02/29/blegg2.png b/r2/r2/public/static/imported/2008/02/29/blegg2.png new file mode 100644 index 00000000..9163f956 Binary files /dev/null and b/r2/r2/public/static/imported/2008/02/29/blegg2.png differ diff --git a/r2/r2/public/static/imported/2008/03/13/the_book_2.jpg b/r2/r2/public/static/imported/2008/03/13/the_book_2.jpg new file mode 100644 index 00000000..e36a8bbe Binary files /dev/null and b/r2/r2/public/static/imported/2008/03/13/the_book_2.jpg differ diff --git a/r2/r2/public/static/imported/2008/03/27/elimonk2darker.jpg b/r2/r2/public/static/imported/2008/03/27/elimonk2darker.jpg new file mode 100644 index 00000000..0e1e5486 Binary files /dev/null and b/r2/r2/public/static/imported/2008/03/27/elimonk2darker.jpg differ diff --git a/r2/r2/public/static/imported/2008/04/02/doviende38008649.jpg b/r2/r2/public/static/imported/2008/04/02/doviende38008649.jpg new file mode 100644 index 00000000..7ecdd92e Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/02/doviende38008649.jpg differ diff --git a/r2/r2/public/static/imported/2008/04/07/fig1_4.gif b/r2/r2/public/static/imported/2008/04/07/fig1_4.gif new file mode 100644 index 00000000..6e96b0bb Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/07/fig1_4.gif differ diff --git a/r2/r2/public/static/imported/2008/04/07/fig3.gif b/r2/r2/public/static/imported/2008/04/07/fig3.gif new file mode 100644 index 00000000..f4583a08 Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/07/fig3.gif differ diff --git a/r2/r2/public/static/imported/2008/04/08/fig2.gif b/r2/r2/public/static/imported/2008/04/08/fig2.gif new file mode 100644 index 00000000..fb94d565 Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/08/fig2.gif differ diff --git a/r2/r2/public/static/imported/2008/04/10/fig4_2.gif b/r2/r2/public/static/imported/2008/04/10/fig4_2.gif new file mode 100644 index 00000000..bbdd299e Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/10/fig4_2.gif differ diff --git a/r2/r2/public/static/imported/2008/04/11/fig2_2.gif b/r2/r2/public/static/imported/2008/04/11/fig2_2.gif new file mode 100644 index 00000000..6473a284 Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/11/fig2_2.gif differ diff --git a/r2/r2/public/static/imported/2008/04/11/fig3_2.gif b/r2/r2/public/static/imported/2008/04/11/fig3_2.gif new file mode 100644 index 00000000..e9bbdf89 Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/11/fig3_2.gif differ diff --git a/r2/r2/public/static/imported/2008/04/11/fig5_2.gif b/r2/r2/public/static/imported/2008/04/11/fig5_2.gif new file mode 100644 index 00000000..1915a29e Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/11/fig5_2.gif differ diff --git a/r2/r2/public/static/imported/2008/04/11/fig5_4.gif b/r2/r2/public/static/imported/2008/04/11/fig5_4.gif new file mode 100644 index 00000000..2c53bea6 Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/11/fig5_4.gif differ diff --git a/r2/r2/public/static/imported/2008/04/14/conf1.png b/r2/r2/public/static/imported/2008/04/14/conf1.png new file mode 100644 index 00000000..04eda49f Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/14/conf1.png differ diff --git a/r2/r2/public/static/imported/2008/04/14/conf2.png b/r2/r2/public/static/imported/2008/04/14/conf2.png new file mode 100644 index 00000000..b3cfba1a Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/14/conf2.png differ diff --git a/r2/r2/public/static/imported/2008/04/14/conf3.png b/r2/r2/public/static/imported/2008/04/14/conf3.png new file mode 100644 index 00000000..a15a495c Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/14/conf3.png differ diff --git a/r2/r2/public/static/imported/2008/04/14/conf4.png b/r2/r2/public/static/imported/2008/04/14/conf4.png new file mode 100644 index 00000000..ef09f012 Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/14/conf4.png differ diff --git a/r2/r2/public/static/imported/2008/04/14/conf4_2.png b/r2/r2/public/static/imported/2008/04/14/conf4_2.png new file mode 100644 index 00000000..2b1e18b2 Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/14/conf4_2.png differ diff --git a/r2/r2/public/static/imported/2008/04/14/conf5.png b/r2/r2/public/static/imported/2008/04/14/conf5.png new file mode 100644 index 00000000..91d446dd Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/14/conf5.png differ diff --git a/r2/r2/public/static/imported/2008/04/15/ampl1.png b/r2/r2/public/static/imported/2008/04/15/ampl1.png new file mode 100644 index 00000000..1ff8e16b Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/15/ampl1.png differ diff --git a/r2/r2/public/static/imported/2008/04/15/ampl2.png b/r2/r2/public/static/imported/2008/04/15/ampl2.png new file mode 100644 index 00000000..58af6901 Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/15/ampl2.png differ diff --git a/r2/r2/public/static/imported/2008/04/15/ampl3_3.png b/r2/r2/public/static/imported/2008/04/15/ampl3_3.png new file mode 100644 index 00000000..cda795d7 Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/15/ampl3_3.png differ diff --git a/r2/r2/public/static/imported/2008/04/15/conf6.png b/r2/r2/public/static/imported/2008/04/15/conf6.png new file mode 100644 index 00000000..434f99b3 Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/15/conf6.png differ diff --git a/r2/r2/public/static/imported/2008/04/15/conf6_2.png b/r2/r2/public/static/imported/2008/04/15/conf6_2.png new file mode 100644 index 00000000..f4692401 Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/15/conf6_2.png differ diff --git a/r2/r2/public/static/imported/2008/04/16/feynman1.png b/r2/r2/public/static/imported/2008/04/16/feynman1.png new file mode 100644 index 00000000..275f7344 Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/16/feynman1.png differ diff --git a/r2/r2/public/static/imported/2008/04/19/elizombies.jpg b/r2/r2/public/static/imported/2008/04/19/elizombies.jpg new file mode 100644 index 00000000..9bf4b5b7 Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/19/elizombies.jpg differ diff --git a/r2/r2/public/static/imported/2008/04/21/conf6.png b/r2/r2/public/static/imported/2008/04/21/conf6.png new file mode 100644 index 00000000..f4692401 Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/21/conf6.png differ diff --git a/r2/r2/public/static/imported/2008/04/21/decohered.png b/r2/r2/public/static/imported/2008/04/21/decohered.png new file mode 100644 index 00000000..5ed4bd55 Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/21/decohered.png differ diff --git a/r2/r2/public/static/imported/2008/04/21/entanglecloud.png b/r2/r2/public/static/imported/2008/04/21/entanglecloud.png new file mode 100644 index 00000000..2c34d4ed Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/21/entanglecloud.png differ diff --git a/r2/r2/public/static/imported/2008/04/21/entangler.png b/r2/r2/public/static/imported/2008/04/21/entangler.png new file mode 100644 index 00000000..a5496bca Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/21/entangler.png differ diff --git a/r2/r2/public/static/imported/2008/04/21/multiblobdeco.png b/r2/r2/public/static/imported/2008/04/21/multiblobdeco.png new file mode 100644 index 00000000..b4fd7e40 Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/21/multiblobdeco.png differ diff --git a/r2/r2/public/static/imported/2008/04/21/precohered_2.png b/r2/r2/public/static/imported/2008/04/21/precohered_2.png new file mode 100644 index 00000000..0bfeeae3 Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/21/precohered_2.png differ diff --git a/r2/r2/public/static/imported/2008/04/21/superposition2.png b/r2/r2/public/static/imported/2008/04/21/superposition2.png new file mode 100644 index 00000000..56b9485e Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/21/superposition2.png differ diff --git a/r2/r2/public/static/imported/2008/04/22/ampl1.png b/r2/r2/public/static/imported/2008/04/22/ampl1.png new file mode 100644 index 00000000..fb5326cb Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/22/ampl1.png differ diff --git a/r2/r2/public/static/imported/2008/04/22/heisensplit.png b/r2/r2/public/static/imported/2008/04/22/heisensplit.png new file mode 100644 index 00000000..788cc85c Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/22/heisensplit.png differ diff --git a/r2/r2/public/static/imported/2008/04/22/helix.png b/r2/r2/public/static/imported/2008/04/22/helix.png new file mode 100644 index 00000000..8a26e598 Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/22/helix.png differ diff --git a/r2/r2/public/static/imported/2008/04/22/posmomdual_2.png b/r2/r2/public/static/imported/2008/04/22/posmomdual_2.png new file mode 100644 index 00000000..48f7c585 Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/22/posmomdual_2.png differ diff --git a/r2/r2/public/static/imported/2008/04/22/singleslitheisenberg.png b/r2/r2/public/static/imported/2008/04/22/singleslitheisenberg.png new file mode 100644 index 00000000..2ccec919 Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/22/singleslitheisenberg.png differ diff --git a/r2/r2/public/static/imported/2008/04/25/decohered.png b/r2/r2/public/static/imported/2008/04/25/decohered.png new file mode 100644 index 00000000..bd5e3a61 Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/25/decohered.png differ diff --git a/r2/r2/public/static/imported/2008/04/25/precohered.png b/r2/r2/public/static/imported/2008/04/25/precohered.png new file mode 100644 index 00000000..8da40290 Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/25/precohered.png differ diff --git a/r2/r2/public/static/imported/2008/04/26/ampl1.png b/r2/r2/public/static/imported/2008/04/26/ampl1.png new file mode 100644 index 00000000..572126c7 Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/26/ampl1.png differ diff --git a/r2/r2/public/static/imported/2008/04/26/entanglecloud.png b/r2/r2/public/static/imported/2008/04/26/entanglecloud.png new file mode 100644 index 00000000..be2e4f78 Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/26/entanglecloud.png differ diff --git a/r2/r2/public/static/imported/2008/04/26/superposition2.png b/r2/r2/public/static/imported/2008/04/26/superposition2.png new file mode 100644 index 00000000..6f769392 Binary files /dev/null and b/r2/r2/public/static/imported/2008/04/26/superposition2.png differ diff --git a/r2/r2/public/static/imported/2008/05/01/2polaroids.png b/r2/r2/public/static/imported/2008/05/01/2polaroids.png new file mode 100644 index 00000000..8caf36c5 Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/01/2polaroids.png differ diff --git a/r2/r2/public/static/imported/2008/05/01/3polaroids.png b/r2/r2/public/static/imported/2008/05/01/3polaroids.png new file mode 100644 index 00000000..cba20ed4 Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/01/3polaroids.png differ diff --git a/r2/r2/public/static/imported/2008/05/01/3polaroids_2.png b/r2/r2/public/static/imported/2008/05/01/3polaroids_2.png new file mode 100644 index 00000000..0a7c1a83 Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/01/3polaroids_2.png differ diff --git a/r2/r2/public/static/imported/2008/05/01/heisensplit.png b/r2/r2/public/static/imported/2008/05/01/heisensplit.png new file mode 100644 index 00000000..2ebff2b0 Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/01/heisensplit.png differ diff --git a/r2/r2/public/static/imported/2008/05/01/polar3060.png b/r2/r2/public/static/imported/2008/05/01/polar3060.png new file mode 100644 index 00000000..bcd5c061 Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/01/polar3060.png differ diff --git a/r2/r2/public/static/imported/2008/05/01/polarbreakdown.png b/r2/r2/public/static/imported/2008/05/01/polarbreakdown.png new file mode 100644 index 00000000..359d2fd7 Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/01/polarbreakdown.png differ diff --git a/r2/r2/public/static/imported/2008/05/01/polardecomp.png b/r2/r2/public/static/imported/2008/05/01/polardecomp.png new file mode 100644 index 00000000..8f0781d7 Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/01/polardecomp.png differ diff --git a/r2/r2/public/static/imported/2008/05/01/polarpythagorean.png b/r2/r2/public/static/imported/2008/05/01/polarpythagorean.png new file mode 100644 index 00000000..b2a14130 Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/01/polarpythagorean.png differ diff --git a/r2/r2/public/static/imported/2008/05/02/polar3060.png b/r2/r2/public/static/imported/2008/05/02/polar3060.png new file mode 100644 index 00000000..bcd5c061 Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/02/polar3060.png differ diff --git a/r2/r2/public/static/imported/2008/05/06/bayesodds.png b/r2/r2/public/static/imported/2008/05/06/bayesodds.png new file mode 100644 index 00000000..dad22e57 Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/06/bayesodds.png differ diff --git a/r2/r2/public/static/imported/2008/05/06/bayesodds_2.png b/r2/r2/public/static/imported/2008/05/06/bayesodds_2.png new file mode 100644 index 00000000..f4c1f84e Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/06/bayesodds_2.png differ diff --git a/r2/r2/public/static/imported/2008/05/06/bayestheorem.png b/r2/r2/public/static/imported/2008/05/06/bayestheorem.png new file mode 100644 index 00000000..4715a91f Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/06/bayestheorem.png differ diff --git a/r2/r2/public/static/imported/2008/05/06/bayestheorem_3.png b/r2/r2/public/static/imported/2008/05/06/bayestheorem_3.png new file mode 100644 index 00000000..74eda1b3 Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/06/bayestheorem_3.png differ diff --git a/r2/r2/public/static/imported/2008/05/21/mindscaleacademic.png b/r2/r2/public/static/imported/2008/05/21/mindscaleacademic.png new file mode 100644 index 00000000..36075738 Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/21/mindscaleacademic.png differ diff --git a/r2/r2/public/static/imported/2008/05/21/mindscalereal.png b/r2/r2/public/static/imported/2008/05/21/mindscalereal.png new file mode 100644 index 00000000..e17f990a Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/21/mindscalereal.png differ diff --git a/r2/r2/public/static/imported/2008/05/22/mindscaleparochial.png b/r2/r2/public/static/imported/2008/05/22/mindscaleparochial.png new file mode 100644 index 00000000..946f3cdc Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/22/mindscaleparochial.png differ diff --git a/r2/r2/public/static/imported/2008/05/23/jbarbourconfigurationcube_2.png b/r2/r2/public/static/imported/2008/05/23/jbarbourconfigurationcube_2.png new file mode 100644 index 00000000..3d4253bf Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/23/jbarbourconfigurationcube_2.png differ diff --git a/r2/r2/public/static/imported/2008/05/23/jbarbourrelative.png b/r2/r2/public/static/imported/2008/05/23/jbarbourrelative.png new file mode 100644 index 00000000..62cfc78f Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/23/jbarbourrelative.png differ diff --git a/r2/r2/public/static/imported/2008/05/23/jbarbourshapepath.png b/r2/r2/public/static/imported/2008/05/23/jbarbourshapepath.png new file mode 100644 index 00000000..1dc91bc8 Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/23/jbarbourshapepath.png differ diff --git a/r2/r2/public/static/imported/2008/05/23/jbarbourtriangleland1_2.png b/r2/r2/public/static/imported/2008/05/23/jbarbourtriangleland1_2.png new file mode 100644 index 00000000..5610cfab Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/23/jbarbourtriangleland1_2.png differ diff --git a/r2/r2/public/static/imported/2008/05/23/jbarbourtriangleseries.png b/r2/r2/public/static/imported/2008/05/23/jbarbourtriangleseries.png new file mode 100644 index 00000000..82d1fc6b Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/23/jbarbourtriangleseries.png differ diff --git a/r2/r2/public/static/imported/2008/05/26/jbarbourconfigurationcube_3.png b/r2/r2/public/static/imported/2008/05/26/jbarbourconfigurationcube_3.png new file mode 100644 index 00000000..3d4253bf Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/26/jbarbourconfigurationcube_3.png differ diff --git a/r2/r2/public/static/imported/2008/05/26/jbarbourrelative.png b/r2/r2/public/static/imported/2008/05/26/jbarbourrelative.png new file mode 100644 index 00000000..c92a0e62 Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/26/jbarbourrelative.png differ diff --git a/r2/r2/public/static/imported/2008/05/26/jbarbourtrianglecloud.png b/r2/r2/public/static/imported/2008/05/26/jbarbourtrianglecloud.png new file mode 100644 index 00000000..7035eb02 Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/26/jbarbourtrianglecloud.png differ diff --git a/r2/r2/public/static/imported/2008/05/26/jbarbourtrianglecloud_2.png b/r2/r2/public/static/imported/2008/05/26/jbarbourtrianglecloud_2.png new file mode 100644 index 00000000..7035eb02 Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/26/jbarbourtrianglecloud_2.png differ diff --git a/r2/r2/public/static/imported/2008/05/26/jbarbourtriangleland1.png b/r2/r2/public/static/imported/2008/05/26/jbarbourtriangleland1.png new file mode 100644 index 00000000..5610cfab Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/26/jbarbourtriangleland1.png differ diff --git a/r2/r2/public/static/imported/2008/05/26/jbarbourtriangleland2.png b/r2/r2/public/static/imported/2008/05/26/jbarbourtriangleland2.png new file mode 100644 index 00000000..9cd0744a Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/26/jbarbourtriangleland2.png differ diff --git a/r2/r2/public/static/imported/2008/05/26/schrodinger.gif b/r2/r2/public/static/imported/2008/05/26/schrodinger.gif new file mode 100644 index 00000000..181eb713 Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/26/schrodinger.gif differ diff --git a/r2/r2/public/static/imported/2008/05/27/jbarbourrelative.png b/r2/r2/public/static/imported/2008/05/27/jbarbourrelative.png new file mode 100644 index 00000000..62cfc78f Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/27/jbarbourrelative.png differ diff --git a/r2/r2/public/static/imported/2008/05/27/manybranches4.png b/r2/r2/public/static/imported/2008/05/27/manybranches4.png new file mode 100644 index 00000000..eb19a44e Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/27/manybranches4.png differ diff --git a/r2/r2/public/static/imported/2008/05/28/causeleft_3.png b/r2/r2/public/static/imported/2008/05/28/causeleft_3.png new file mode 100644 index 00000000..e99445db Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/28/causeleft_3.png differ diff --git a/r2/r2/public/static/imported/2008/05/28/causeright.png b/r2/r2/public/static/imported/2008/05/28/causeright.png new file mode 100644 index 00000000..198a982f Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/28/causeright.png differ diff --git a/r2/r2/public/static/imported/2008/05/28/causeright_2.png b/r2/r2/public/static/imported/2008/05/28/causeright_2.png new file mode 100644 index 00000000..3ef1f9f0 Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/28/causeright_2.png differ diff --git a/r2/r2/public/static/imported/2008/05/28/causeright_3.png b/r2/r2/public/static/imported/2008/05/28/causeright_3.png new file mode 100644 index 00000000..3ef1f9f0 Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/28/causeright_3.png differ diff --git a/r2/r2/public/static/imported/2008/05/28/causeundirected2.png b/r2/r2/public/static/imported/2008/05/28/causeundirected2.png new file mode 100644 index 00000000..02a184ef Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/28/causeundirected2.png differ diff --git a/r2/r2/public/static/imported/2008/05/28/causeundirected_2.png b/r2/r2/public/static/imported/2008/05/28/causeundirected_2.png new file mode 100644 index 00000000..923ad9c3 Binary files /dev/null and b/r2/r2/public/static/imported/2008/05/28/causeundirected_2.png differ diff --git a/r2/r2/public/static/imported/2008/06/02/braid_2.png b/r2/r2/public/static/imported/2008/06/02/braid_2.png new file mode 100644 index 00000000..6013bbbc Binary files /dev/null and b/r2/r2/public/static/imported/2008/06/02/braid_2.png differ diff --git a/r2/r2/public/static/imported/2008/06/02/braidslice.png b/r2/r2/public/static/imported/2008/06/02/braidslice.png new file mode 100644 index 00000000..bb0c824d Binary files /dev/null and b/r2/r2/public/static/imported/2008/06/02/braidslice.png differ diff --git a/r2/r2/public/static/imported/2008/06/02/braidtime.png b/r2/r2/public/static/imported/2008/06/02/braidtime.png new file mode 100644 index 00000000..7f26ab7d Binary files /dev/null and b/r2/r2/public/static/imported/2008/06/02/braidtime.png differ diff --git a/r2/r2/public/static/imported/2008/06/02/braidtime_2.png b/r2/r2/public/static/imported/2008/06/02/braidtime_2.png new file mode 100644 index 00000000..d75a4562 Binary files /dev/null and b/r2/r2/public/static/imported/2008/06/02/braidtime_2.png differ diff --git a/r2/r2/public/static/imported/2008/06/02/causeright.png b/r2/r2/public/static/imported/2008/06/02/causeright.png new file mode 100644 index 00000000..3ef1f9f0 Binary files /dev/null and b/r2/r2/public/static/imported/2008/06/02/causeright.png differ diff --git a/r2/r2/public/static/imported/2008/06/02/manybranches4.png b/r2/r2/public/static/imported/2008/06/02/manybranches4.png new file mode 100644 index 00000000..eb19a44e Binary files /dev/null and b/r2/r2/public/static/imported/2008/06/02/manybranches4.png differ diff --git a/r2/r2/public/static/imported/2008/06/05/fwallphysics.png b/r2/r2/public/static/imported/2008/06/05/fwallphysics.png new file mode 100644 index 00000000..a7ac6da3 Binary files /dev/null and b/r2/r2/public/static/imported/2008/06/05/fwallphysics.png differ diff --git a/r2/r2/public/static/imported/2008/06/05/fwmeinphysics.png b/r2/r2/public/static/imported/2008/06/05/fwmeinphysics.png new file mode 100644 index 00000000..24e01741 Binary files /dev/null and b/r2/r2/public/static/imported/2008/06/05/fwmeinphysics.png differ diff --git a/r2/r2/public/static/imported/2008/06/05/fwmevsphysics.png b/r2/r2/public/static/imported/2008/06/05/fwmevsphysics.png new file mode 100644 index 00000000..51b80a03 Binary files /dev/null and b/r2/r2/public/static/imported/2008/06/05/fwmevsphysics.png differ diff --git a/r2/r2/public/static/imported/2008/06/06/fwcausality.png b/r2/r2/public/static/imported/2008/06/06/fwcausality.png new file mode 100644 index 00000000..0301e8c6 Binary files /dev/null and b/r2/r2/public/static/imported/2008/06/06/fwcausality.png differ diff --git a/r2/r2/public/static/imported/2008/06/06/fwdeterminism_2.png b/r2/r2/public/static/imported/2008/06/06/fwdeterminism_2.png new file mode 100644 index 00000000..3d51a7b0 Binary files /dev/null and b/r2/r2/public/static/imported/2008/06/06/fwdeterminism_2.png differ diff --git a/r2/r2/public/static/imported/2008/06/06/fwmarkov.png b/r2/r2/public/static/imported/2008/06/06/fwmarkov.png new file mode 100644 index 00000000..fc780eaa Binary files /dev/null and b/r2/r2/public/static/imported/2008/06/06/fwmarkov.png differ diff --git a/r2/r2/public/static/imported/2008/06/06/fwmarkov_2.png b/r2/r2/public/static/imported/2008/06/06/fwmarkov_2.png new file mode 100644 index 00000000..8b6ea741 Binary files /dev/null and b/r2/r2/public/static/imported/2008/06/06/fwmarkov_2.png differ diff --git a/r2/r2/public/static/imported/2008/06/13/boarddive.png b/r2/r2/public/static/imported/2008/06/13/boarddive.png new file mode 100644 index 00000000..fcb45fcf Binary files /dev/null and b/r2/r2/public/static/imported/2008/06/13/boarddive.png differ diff --git a/r2/r2/public/static/imported/2008/06/13/boards_3.png b/r2/r2/public/static/imported/2008/06/13/boards_3.png new file mode 100644 index 00000000..68f6fdec Binary files /dev/null and b/r2/r2/public/static/imported/2008/06/13/boards_3.png differ diff --git a/r2/r2/public/static/imported/2008/06/13/boardsparity.png b/r2/r2/public/static/imported/2008/06/13/boardsparity.png new file mode 100644 index 00000000..b206c9db Binary files /dev/null and b/r2/r2/public/static/imported/2008/06/13/boardsparity.png differ diff --git a/r2/r2/public/static/imported/2008/06/14/fwmarkov_3.png b/r2/r2/public/static/imported/2008/06/14/fwmarkov_3.png new file mode 100644 index 00000000..0f083d69 Binary files /dev/null and b/r2/r2/public/static/imported/2008/06/14/fwmarkov_3.png differ diff --git a/r2/r2/public/static/imported/2008/06/24/mindspace_2.png b/r2/r2/public/static/imported/2008/06/24/mindspace_2.png new file mode 100644 index 00000000..01b0cba0 Binary files /dev/null and b/r2/r2/public/static/imported/2008/06/24/mindspace_2.png differ diff --git a/r2/r2/public/static/imported/2008/07/30/biggornandkirk_2.jpg b/r2/r2/public/static/imported/2008/07/30/biggornandkirk_2.jpg new file mode 100644 index 00000000..f86e8e52 Binary files /dev/null and b/r2/r2/public/static/imported/2008/07/30/biggornandkirk_2.jpg differ diff --git a/r2/r2/public/static/imported/2008/08/28/boilwater_4.png b/r2/r2/public/static/imported/2008/08/28/boilwater_4.png new file mode 100644 index 00000000..cb2f87ab Binary files /dev/null and b/r2/r2/public/static/imported/2008/08/28/boilwater_4.png differ diff --git a/r2/r2/public/static/imported/2008/08/28/happysmilingai_5.png b/r2/r2/public/static/imported/2008/08/28/happysmilingai_5.png new file mode 100644 index 00000000..68ea3376 Binary files /dev/null and b/r2/r2/public/static/imported/2008/08/28/happysmilingai_5.png differ diff --git a/r2/r2/public/static/imported/2008/08/29/maximumfundevice.png b/r2/r2/public/static/imported/2008/08/29/maximumfundevice.png new file mode 100644 index 00000000..e9a93882 Binary files /dev/null and b/r2/r2/public/static/imported/2008/08/29/maximumfundevice.png differ diff --git a/r2/r2/public/static/imported/2008/08/29/superhappyai.png b/r2/r2/public/static/imported/2008/08/29/superhappyai.png new file mode 100644 index 00000000..199e0828 Binary files /dev/null and b/r2/r2/public/static/imported/2008/08/29/superhappyai.png differ diff --git a/r2/r2/public/static/imported/2008/09/30/zebra_4.jpg b/r2/r2/public/static/imported/2008/09/30/zebra_4.jpg new file mode 100644 index 00000000..bf314ec3 Binary files /dev/null and b/r2/r2/public/static/imported/2008/09/30/zebra_4.jpg differ diff --git a/r2/r2/public/static/imported/6a00d8341c6a2c53ef010536c21d63970b-800wi.jpg b/r2/r2/public/static/imported/6a00d8341c6a2c53ef010536c21d63970b-800wi.jpg new file mode 100644 index 00000000..bc2faa23 Binary files /dev/null and b/r2/r2/public/static/imported/6a00d8341c6a2c53ef010536c21d63970b-800wi.jpg differ diff --git a/r2/r2/public/static/jquery.qtip.css b/r2/r2/public/static/jquery.qtip.css new file mode 100644 index 00000000..858dbfe3 --- /dev/null +++ b/r2/r2/public/static/jquery.qtip.css @@ -0,0 +1,184 @@ +/* +* qTip2 - Pretty powerful tooltips +* http://craigsworks.com/projects/qtip2/ +* +* Version: nightly +* Copyright 2009-2010 Craig Michael Thompson - http://craigsworks.com +* +* Dual licensed under MIT or GPLv2 licenses +* http://en.wikipedia.org/wiki/MIT_License +* http://en.wikipedia.org/wiki/GNU_General_Public_License +* +* Date: Thu Apr 28 05:56:55 PDT 2011 +*/ + +/* Fluid class for determining actual width in IE */ +.ui-tooltip-fluid{ + display: block; + visibility: hidden; + position: static !important; + float: left !important; +} + +.ui-tooltip, .qtip{ + position: absolute; + left: -28000px; + top: -28000px; + display: none; + + max-width: 460px; + min-width: 50px; + + font-size: 12px; +} + + .ui-tooltip-content{ + position: relative; + padding: 5px 9px; + overflow: hidden; + + border-width: 1px; + border-style: solid; + + text-align: left; + word-wrap: break-word; + overflow: hidden; + } + .ui-tooltip-content img { + max-width: 258px !important; + } + + .ui-tooltip-titlebar{ + position: relative; + min-height: 14px; + padding: 5px 35px 5px 10px; + overflow: hidden; + + border-width: 1px 1px 0; + border-style: solid; + + font-weight: bold; + } + + .ui-tooltip-titlebar + .ui-tooltip-content{ border-top-width: 0px !important; } + + /*! Default close button class */ + .ui-tooltip-titlebar .ui-state-default{ + position: absolute; + right: 4px; + top: 50%; + margin-top: -9px; + + cursor: pointer; + outline: medium none; + + border-width: 1px; + border-style: solid; + } + + * html .ui-tooltip-titlebar .ui-state-default{ + top: 16px; + } + + .ui-tooltip-titlebar .ui-icon, + .ui-tooltip-icon .ui-icon{ + display: block; + text-indent: -1000em; + } + + .ui-tooltip-icon, .ui-tooltip-icon .ui-icon{ + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; + } + + .ui-tooltip-icon .ui-icon{ + width: 18px; + height: 14px; + + text-align: center; + text-indent: 0; + font: normal bold 10px/13px Tahoma,sans-serif; + + color: inherit; + background: transparent none no-repeat -100em -100em; + } + + +/* Applied to 'focused' tooltips e.g. most recently displayed/interacted with */ +.ui-tooltip-focus{ + +} + +/* Applied on hover of tooltips i.e. added/removed on mouseenter/mouseleave respectively */ +.ui-tooltip-hover{ + +} + + +/*! Default tooltip style */ +.ui-tooltip-titlebar, +.ui-tooltip-content{ + border-color: #F1D031; + background-color: #FFFFA3; + color: #555; +} + + .ui-tooltip-titlebar{ + background-color: #FFEF93; + } + + .ui-tooltip-titlebar .ui-tooltip-icon{ + border-color: #CCC; + background: #F1F1F1; + color: #777; + } + + .ui-tooltip-titlebar .ui-state-hover{ + border-color: #AAA; + color: #111; + } + + +/* Lesswrong style */ +.ui-tooltip-lesswrong { + opacity: 0.8 !important; +} +.ui-tooltip-lesswrong .ui-tooltip-titlebar, +.ui-tooltip-lesswrong .ui-tooltip-content { + background-color: #000001; /* BUG, specifying black #000 will cause tip to be transparent */ + border: #000001; + color: #fff; +} +.ui-tooltip-lesswrong .ui-tooltip-content { + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + font-size: 11px; + padding: 2px 5px; + text-align: center; +} + + +.ui-tooltip .ui-tooltip-tip{ + margin: 0 auto; + overflow: hidden; + + background: transparent !important; + border: 0px dashed transparent !important; + z-index: 10; +} + + .ui-tooltip .ui-tooltip-tip, + .ui-tooltip .ui-tooltip-tip *{ + position: absolute; + + line-height: 0.1px !important; + font-size: 0.1px !important; + color: #123456; + + background: transparent; + border: 0px dashed transparent; + } + + .ui-tooltip .ui-tooltip-tip canvas{ position: static; } \ No newline at end of file diff --git a/r2/r2/public/static/jquery.qtip.min.js b/r2/r2/public/static/jquery.qtip.min.js new file mode 100644 index 00000000..13436f3a --- /dev/null +++ b/r2/r2/public/static/jquery.qtip.min.js @@ -0,0 +1,13 @@ +/* +* qTip2 - Pretty powerful tooltips +* http://craigsworks.com/projects/qtip2/ +* +* Version: nightly +* Copyright 2009-2010 Craig Michael Thompson - http://craigsworks.com +* +* Dual licensed under MIT or GPLv2 licenses +* http://en.wikipedia.org/wiki/MIT_License +* http://en.wikipedia.org/wiki/GNU_General_Public_License +* +* Date: Thu Apr 28 05:56:55 PDT 2011 +*/"use strict",function(a,b,c){function x(b,g){function v(a){var b=a.precedance==="y",c=n[b?"width":"height"],d=n[b?"height":"width"],e=a.string().indexOf("center")>-1,f=c*(e?.5:1),g=Math.pow,h=Math.round,i,j,k,l=Math.sqrt(g(f,2)+g(d,2)),m=[p/f*l,p/d*l];m[2]=Math.sqrt(g(m[0],2)-g(p,2)),m[3]=Math.sqrt(g(m[1],2)-g(p,2)),i=l+m[2]+m[3]+(e?0:m[0]),j=i/l,k=[h(j*d),h(j*c)];return{height:k[b?0:1],width:k[b?1:0]}}function u(b){var c=k.titlebar&&b.y==="top",d=c?k.titlebar:k.content,e=a.browser.mozilla,f=e?"-moz-":a.browser.webkit?"-webkit-":"",g=b.y+(e?"":"-")+b.x,h=f+(e?"border-radius-"+g:"border-"+g+"-radius");return parseInt(d.css(h),10)||parseInt(l.css(h),10)||0}function t(a,b,c){b=b?b:a[a.precedance];var d=k.titlebar&&a.y==="top",e=d?k.titlebar:k.content,f="border-"+b+"-width",g=parseInt(e.css(f),10);return(c?g||parseInt(l.css(f),10):g)||0}function s(f,g,h,l){if(k.tip){var n=a.extend({},i.corner),o=h.adjusted,p=b.options.position.adjust.method.split(" "),q=p[0],r=p[1]||p[0],s={left:e,top:e,x:0,y:0},t,u={},v;i.corner.fixed!==d&&(q==="shift"&&n.precedance==="x"&&o.left&&n.y!=="center"?n.precedance=n.precedance==="x"?"y":"x":q==="flip"&&o.left&&(n.x=n.x==="center"?o.left>0?"left":"right":n.x==="left"?"right":"left"),r==="shift"&&n.precedance==="y"&&o.top&&n.x!=="center"?n.precedance=n.precedance==="y"?"x":"y":r==="flip"&&o.top&&(n.y=n.y==="center"?o.top>0?"top":"bottom":n.y==="top"?"bottom":"top"),n.string()!==m.corner&&(m.top!==o.top||m.left!==o.left)&&i.update(n,e)),t=i.position(n,o),t.right!==c&&(t.left=-t.right),t.bottom!==c&&(t.top=-t.bottom),t.user=Math.max(0,j.offset);if(s.left=q==="shift"&&!!o.left)n.x==="center"?u["margin-left"]=s.x=t["margin-left"]-o.left:(v=t.right!==c?[o.left,-t.left]:[-o.left,t.left],(s.x=Math.max(v[0],v[1]))>v[0]&&(h.left-=o.left,s.left=e),u[t.right!==c?"right":"left"]=s.x);if(s.top=r==="shift"&&!!o.top)n.y==="center"?u["margin-top"]=s.y=t["margin-top"]-o.top:(v=t.bottom!==c?[o.top,-t.top]:[-o.top,t.top],(s.y=Math.max(v[0],v[1]))>v[0]&&(h.top-=o.top,s.top=e),u[t.bottom!==c?"bottom":"top"]=s.y);k.tip.css(u).toggle(!(s.x&&s.y||n.x==="center"&&s.y||n.y==="center"&&s.x)),h.left-=t.left.charAt?t.user:q!=="shift"||s.top||!s.left&&!s.top?t.left:0,h.top-=t.top.charAt?t.user:r!=="shift"||s.left||!s.left&&!s.top?t.top:0,m.left=o.left,m.top=o.top,m.corner=n.string()}}var i=this,j=b.options.style.tip,k=b.elements,l=k.tooltip,m={top:0,left:0,corner:""},n={width:j.width,height:j.height},o={},p=j.border||0,q=".qtip-tip",r=a("<canvas />")[0].getContext;i.corner=f,i.mimic=f,i.position={},b.checks.tip={"^position.my|style.tip.(corner|mimic|border)$":function(){i.init()||i.destroy(),b.reposition()},"^style.tip.(height|width)$":function(){n={width:j.width,height:j.height},i.create(),i.update(),b.reposition()},"^content.title.text|style.(classes|widget)$":function(){k.tip&&i.update()}},a.extend(i,{init:function(){var b=i.detectCorner()&&(r||a.browser.msie);b&&(i.create(),i.update(),l.unbind(q).bind("tooltipmove"+q,s));return b},detectCorner:function(){var a=j.corner,c=b.options.position,f=c.at,g=c.my.string?c.my.string():c.my;if(a===e||g===e&&f===e)return e;a===d?i.corner=new h.Corner(g):a.string||(i.corner=new h.Corner(a),i.corner.fixed=d);return i.corner.string()!=="centercenter"},detectColours:function(){var c,d,e,f=k.tip.css({backgroundColor:"",border:""}),g=i.corner,h=g[g.precedance],m="border-"+h+"-color",p="border"+h.charAt(0)+h.substr(1)+"Color",q=/rgba?\(0, 0, 0(, 0)?\)|transparent/i,r="background-color",s="transparent",t="ui-tooltip-fluid",u=a(document.body).css("color"),v=b.elements.content.css("color"),w=k.titlebar&&(g.y==="top"||g.y==="center"&&f.position().top+n.height/2+j.offset<k.titlebar.outerHeight(1)),x=w?k.titlebar:k.content;l.addClass(t),d=f.css(r)||s,e=f[0].style[p];if(!d||q.test(d))o.fill=x.css(r),q.test(o.fill)&&(o.fill=l.css(r)||d);if(!e||q.test(e)){o.border=l.css(m);if(q.test(o.border)||o.border===u)o.border=x.css(m),o.border===v&&(o.border=e)}a("*",f).add(f).css(r,s).css("border",""),l.removeClass(t)},create:function(){var b=n.width,c=n.height,d;k.tip&&k.tip.remove(),k.tip=a("<div />",{"class":"ui-tooltip-tip"}).css({width:b,height:c}).prependTo(l),r?a("<canvas />").appendTo(k.tip)[0].getContext("2d").save():(d='<vml:shape coordorigin="0,0" style="display:inline-block; position:absolute; behavior:url(#default#VML);"></vml:shape>',k.tip.html(p?d+=d:d))},update:function(b,c){var g=k.tip,l=g.children(),m=n.width,q=n.height,s="px solid ",u="px dashed transparent",x=j.mimic,y=Math.round,z,A,B,C,D;b||(b=i.corner),x===e?x=b:(x=new h.Corner(x),x.precedance=b.precedance,x.x==="inherit"?x.x=b.x:x.y==="inherit"?x.y=b.y:x.x===x.y&&(x[b.precedance]=b[b.precedance])),z=x.precedance,i.detectColours(),p=o.border==="transparent"||o.border==="#123456"?0:j.border===d?t(b,f,d):j.border,B=w(x,m,q),D=v(b),g.css(D),b.precedance==="y"?C=[y(x.x==="left"?p:x.x==="right"?D.width-m-p:(D.width-m)/2),y(x.y==="top"?D.height-q:0)]:C=[y(x.x==="left"?D.width-m:0),y(x.y==="top"?p:x.y==="bottom"?D.height-q-p:(D.height-q)/2)],r?(l.attr(D),A=l[0].getContext("2d"),A.restore(),A.save(),A.clearRect(0,0,3e3,3e3),A.translate(C[0],C[1]),A.beginPath(),A.moveTo(B[0][0],B[0][1]),A.lineTo(B[1][0],B[1][1]),A.lineTo(B[2][0],B[2][1]),A.closePath(),A.fillStyle=o.fill,A.strokeStyle=o.border,A.lineWidth=p*2,A.lineJoin="miter",A.miterLimit=100,A.stroke(),A.fill()):(B="m"+B[0][0]+","+B[0][1]+" l"+B[1][0]+","+B[1][1]+" "+B[2][0]+","+B[2][1]+" xe",C[2]=p&&/^(r|b)/i.test(b.string())?parseFloat(a.browser.version,10)===8?2:1:0,l.css({antialias:""+(x.string().indexOf("center")>-1),left:C[0]-C[2]*Number(z==="x"),top:C[1]-C[2]*Number(z==="y"),width:m+p,height:q+p}).each(function(b){var c=a(this);c.attr({coordsize:m+p+" "+(q+p),path:B,fillcolor:o.fill,filled:!!b,stroked:!b}).css({display:p||b?"block":"none"}),!b&&p>0&&c.html()===""&&c.html('<vml:stroke weight="'+p*2+'px" color="'+o.border+'" miterlimit="1000" joinstyle="miter" style="behavior:url(#default#VML); display:inline-block;" />')})),c!==e&&i.position(b)},position:function(b){var c=k.tip,f={},g=Math.max(0,j.offset),h,l,m;if(j.corner===e||!c)return e;b=b||i.corner,h=b.precedance,l=v(b),m=[b.x,b.y],h==="x"&&m.reverse(),a.each(m,function(a,c){var e,i;c==="center"?(e=h==="y"?"left":"top",f[e]="50%",f["margin-"+e]=-Math.round(l[h==="y"?"width":"height"]/2)+g):(e=t(b,c,d),i=u(b),f[c]=a?t(b,c):g+(i>e?i:0))}),f[b[h]]-=l[h==="x"?"width":"height"],c.css({top:"",bottom:"",left:"",right:"",margin:""}).css(f);return f},destroy:function(){k.tip&&k.tip.remove(),l.unbind(q)}}),i.init()}function w(a,b,c){var d=Math.ceil(b/2),e=Math.ceil(c/2),f={bottomright:[[0,0],[b,c],[b,0]],bottomleft:[[0,0],[b,0],[0,c]],topright:[[0,c],[b,0],[b,c]],topleft:[[0,0],[0,c],[b,c]],topcenter:[[0,c],[d,0],[b,c]],bottomcenter:[[0,0],[b,0],[d,c]],rightcenter:[[0,0],[b,e],[0,c]],leftcenter:[[b,0],[b,c],[0,e]]};f.lefttop=f.bottomright,f.righttop=f.bottomleft,f.leftbottom=f.topright,f.rightbottom=f.topleft;return f[a.string()]}function v(b,c){var i,j,k,l,m=a(this),n=a(document.body),o=this===document?n:m,p=m.metadata?m.metadata(c.metadata):f,q=c.metadata.type==="html5"&&p?p[c.metadata.name]:f,v=m.data(c.metadata.name||"qtipopts");try{v=typeof v==="string"?(new Function("return "+v))():v}catch(w){s("Unable to parse HTML5 attribute data: "+v)}l=a.extend(d,{},g.defaults,c,typeof v==="object"?t(v):f,t(q||p)),p&&a.removeData(this,"metadata"),j=l.position,l.id=b;if("boolean"===typeof l.content.text){k=m.attr(l.content.attr);if(l.content.attr!==e&&k)l.content.text=k;else return e}j.container===e&&(j.container=n),j.target===e&&(j.target=o),l.show.target===e&&(l.show.target=o),l.show.solo===d&&(l.show.solo=n),l.hide.target===e&&(l.hide.target=o),l.position.viewport===d&&(l.position.viewport=j.container),j.at=new h.Corner(j.at),j.my=new h.Corner(j.my);if(a.data(this,"qtip"))if(l.overwrite)m.qtip("destroy");else if(l.overwrite===e)return e;a.attr(this,"title")&&(a.attr(this,r,a.attr(this,"title")),this.removeAttribute("title")),i=new u(m,l,b,!!k),a.data(this,"qtip",i),m.bind("remove.qtip",function(){i.destroy()});return i}function u(c,p,q,s){function L(c,d,e,f){f=parseInt(f,10)!==0;var g=".qtip-"+q,h={show:c&&p.show.target[0],hide:d&&p.hide.target[0],tooltip:e&&u.rendered&&A.tooltip[0],content:e&&u.rendered&&A.content[0],container:f&&p.position.container[0]===v?document:p.position.container[0],window:f&&b};u.rendered?a([]).pushStack(a.grep([h.show,h.hide,h.tooltip,h.container,h.content,h.window],function(a){return typeof a==="object"})).unbind(g):c&&p.show.target.unbind(g+"-create")}function K(d,f,h,j){function D(a){z.is(":visible")&&u.reposition(a)}function C(a){if(z.hasClass(l))return e;clearTimeout(u.timers.inactive),u.timers.inactive=setTimeout(function(){u.hide(a)},p.hide.inactive)}function y(b){if(z.hasClass(l))return e;var c=a(b.relatedTarget||b.target),d=c.closest(m)[0]===z[0],f=c[0]===r.show[0];clearTimeout(u.timers.show),clearTimeout(u.timers.hide);if(n.target==="mouse"&&d||p.hide.fixed&&(/mouse(out|leave|move)/.test(b.type)&&(d||f))){b.stopPropagation(),b.preventDefault();return e}p.hide.delay>0?u.timers.hide=setTimeout(function(){u.hide(b)},p.hide.delay):u.hide(b)}function x(a){if(z.hasClass(l))return e;r.show.trigger("qtip-"+q+"-inactive"),clearTimeout(u.timers.show),clearTimeout(u.timers.hide);var b=function(){u.show(a)};p.show.delay>0?u.timers.show=setTimeout(b,p.show.delay):b()}var k=".qtip-"+q,n=p.position,r={show:p.show.target,hide:p.hide.target,container:n.container[0]===v?a(document):n.container,doc:a(document)},s={show:a.trim(""+p.show.event).split(" "),hide:a.trim(""+p.hide.event).split(" ")},t=a.browser.msie&&parseInt(a.browser.version,10)===6,w;h&&(p.hide.fixed&&(r.hide=r.hide.add(z),z.bind("mouseover"+k,function(){z.hasClass(l)||clearTimeout(u.timers.hide)})),n.target==="mouse"&&n.adjust.mouse&&p.hide.event&&z.bind("mouseleave"+k,function(a){(a.relatedTarget||a.target)!==r.show[0]&&u.hide(a)}),z.bind("mouseenter"+k,function(a){u[a.type==="mouseenter"?"focus":"blur"](a)}),z.bind("mouseenter"+k+" mouseleave"+k,function(a){z.toggleClass(o,a.type==="mouseenter")})),f&&("number"===typeof p.hide.inactive&&(r.show.bind("qtip-"+q+"-inactive",C),a.each(g.inactiveEvents,function(a,b){r.hide.add(A.tooltip).bind(b+k+"-inactive",C)})),/mouse(over|enter)/i.test(p.show.event)&&!/mouse(out|leave)/i.test(p.hide.event)&&r.hide.bind("mouseleave"+k,function(a){clearTimeout(u.timers.show)}),a.each(s.hide,function(b,c){var d=a.inArray(c,s.show),e=a(r.hide);d>-1&&e.add(r.show).length===e.length||c==="unfocus"?(r.show.bind(c+k,function(a){z.is(":visible")?y(a):x(a)}),delete s.show[d]):r.hide.bind(c+k,y)})),d&&(a.each(s.show,function(a,b){r.show.bind(b+k,x)}),"number"===typeof p.hide.distance&&r.show.bind("mousemove"+k,function(a){var b=B.origin||{},c=p.hide.distance,d=Math.abs;b&&(d(a.pageX-b.pageX)>=c||d(a.pageY-b.pageY)>=c)&&u.hide(a)})),j&&((n.adjust.resize||n.viewport)&&a(a.event.special.resize?n.viewport:b).bind("resize"+k,D),(n.viewport||t&&z.css("position")==="fixed")&&a(n.viewport).bind("scroll"+k,D),/unfocus/i.test(p.hide.event)&&r.doc.bind("mousedown"+k,function(b){var d=a(b.target);d.parents(m).length===0&&d.add(c).length>1&&z.is(":visible")&&!z.hasClass(l)&&u.hide(b)}),p.hide.leave&&/mouseleave|mouseout/i.test(p.hide.event)&&a(b).bind("blur"+k+" mouse"+(p.hide.leave.indexOf("frame")>-1?"out":"leave")+k,function(a){a.relatedTarget||u.hide(a)}),n.target==="mouse"&&r.doc.bind("mousemove"+k,function(a){n.adjust.mouse&&!z.hasClass(l)&&z.is(":visible")&&u.reposition(a||i)}))}function J(b,d){function g(a){function c(c){(b=b.not(this)).length===0&&(u.redraw(),u.reposition(B.event),a())}var b;if((b=f.find("img:not([height]):not([width])")).length===0)return c.call(b);b.each(function(a,b){(function d(){var e=u.timers.img;if(b.height&&b.width){clearTimeout(e[a]);return c.call(b)}e[a]=setTimeout(d,20)})()})}var f=A.content;b=b||p.content.text;if(!u.rendered||!b)return e;a.isFunction(b)&&(b=b.call(c,u)||""),b.jquery&&b.length>0?f.empty().append(b.css({display:"block"})):f.html(b),u.rendered<0?z.queue("fx",g):(y=0,g(a.noop));return u}function I(b){var d=A.title;if(!u.rendered||!b)return e;a.isFunction(b)&&(b=b.call(c,u)||""),b.jquery&&b.length>0?d.empty().append(b.css({display:"block"})):d.html(b),u.redraw(),u.rendered&&z.is(":visible")&&u.reposition(B.event)}function H(a){var b=A.button,c=A.title;if(!u.rendered)return e;a?(c||G(),F()):b.remove()}function G(){var b=w+"-title";A.titlebar&&E(),A.titlebar=a("<div />",{"class":j+"-titlebar "+(p.style.widget?"ui-widget-header":"")}).append(A.title=a("<div />",{id:b,"class":j+"-title","aria-atomic":d})).insertBefore(A.content),p.content.title.button?F():u.rendered&&u.redraw()}function F(){var b=p.content.title.button,c=typeof b==="string",d=c?b:"Close tooltip";A.button&&A.button.remove(),b.jquery?A.button=b:A.button=a("<a />",{"class":"ui-state-default "+(p.style.widget?"":j+"-icon"),title:d,"aria-label":d}).prepend(a("<span />",{"class":"ui-icon ui-icon-close",html:"×"})),A.button.appendTo(A.titlebar).attr("role","button").hover(function(b){a(this).toggleClass("ui-state-hover",b.type==="mouseenter")}).click(function(a){z.hasClass(l)||u.hide(a);return e}).bind("mousedown keydown mouseup keyup mouseout",function(b){a(this).toggleClass("ui-state-active ui-state-focus",b.type.substr(-4)==="down")}),u.redraw()}function E(){A.title&&(A.titlebar.remove(),A.titlebar=A.title=A.button=f,u.reposition())}function D(){var a=p.style.widget;z.toggleClass(k,a),A.content.toggleClass(k+"-content",a),A.titlebar&&A.titlebar.toggleClass(k+"-header",a),A.button&&A.button.toggleClass(j+"-icon",!a)}function C(a){var b=0,c,d=p,e=a.split(".");while(d=d[e[b++]])b<e.length&&(c=d);return[c||p,e.pop()]}var u=this,v=document.body,w=j+"-"+q,x=0,y=0,z=a(),A,B;u.id=q,u.rendered=e,u.elements=A={target:c},u.timers={img:[]},u.options=p,u.checks={},u.plugins={},u.cache=B={event:{},target:f,disabled:e,attr:s},u.checks.builtin={"^id$":function(b,c,f){var h=f===d?g.nextid:f,i=j+"-"+h;h!==e&&h.length>0&&!a("#"+i).length&&(z[0].id=i,A.content[0].id=i+"-content",A.title[0].id=i+"-title")},"^content.text$":function(a,b,c){J(c)},"^content.title.text$":function(a,b,c){if(!c)return E();!A.title&&c&&G(),I(c)},"^content.title.button$":function(a,b,c){H(c)},"^position.(my|at)$":function(a,b,c){"string"===typeof c&&(a[b]=new h.Corner(c))},"^position.container$":function(a,b,c){u.rendered&&z.appendTo(c)},"^(show|hide).(event|target|fixed|delay|inactive)$":function(a,b,c,d,e){var f=[1,0,0];f[e[1]==="show"?"push":"unshift"](0),L.apply(u,f),K.apply(u,[1,1,0,0])},"^show.ready$":function(){u.rendered?u.show():u.render(1)},"^style.classes$":function(b,c,d){a.attr(z[0],"class",j+" qtip ui-helper-reset "+d)},"^style.widget|content.title":D,"^events.(render|show|move|hide|focus|blur)$":function(b,c,d){z[(a.isFunction(d)?"":"un")+"bind"]("tooltip"+c,d)}},a.extend(u,{render:function(b){if(u.rendered)return u;var f=p.content.title.text,g=a.Event("tooltiprender");a.attr(c[0],"aria-describedby",w),z=A.tooltip=a("<div/>",{id:w,"class":j+" qtip ui-helper-reset "+p.style.classes,width:p.style.width||"",role:"alert","aria-live":"polite","aria-atomic":e,"aria-describedby":w+"-content","aria-hidden":d}).toggleClass(l,B.disabled).data("qtip",u).appendTo(p.position.container).append(A.content=a("<div />",{"class":j+"-content",id:w+"-content","aria-atomic":d})),u.rendered=-1,y=1,f&&(G(),I(f)),J(),u.rendered=d,D(),a.each(p.events,function(b,c){a.isFunction(c)&&z.bind(b==="toggle"?"tooltipshow tooltiphide":"tooltip"+b,c)}),a.each(h,function(){this.initialize==="render"&&this(u)}),K(1,1,1,1),z.queue("fx",function(a){g.originalEvent=B.event,z.trigger(g,[u]),y=0,u.redraw(),(p.show.ready||b)&&u.show(B.event),a()});return u},get:function(a){var b,c;switch(a.toLowerCase()){case"dimensions":b={height:z.outerHeight(),width:z.outerWidth()};break;case"offset":b=h.offset(z,p.position.container);break;default:c=C(a.toLowerCase()),b=c[0][c[1]],b=b.precedance?b.string():b}return b},set:function(b,c){function m(a,b){var c,d,e;for(c in k)for(d in k[c])if(e=(new RegExp(d,"i")).exec(a))b.push(e),k[c][d].apply(u,b)}var g=/^position\.(my|at|adjust|target|container)|style|content|show\.ready/i,h=/^content\.(title|attr)|style/i,i=e,j=e,k=u.checks,l;"string"===typeof b?(l=b,b={},b[l]=c):b=a.extend(d,{},b),a.each(b,function(c,d){var e=C(c.toLowerCase()),f;f=e[0][e[1]],e[0][e[1]]="object"===typeof d&&d.nodeType?a(d):d,b[c]=[e[0],e[1],d,f],i=g.test(c)||i,j=h.test(c)||j}),t(p),x=y=1,a.each(b,m),x=y=0,z.is(":visible")&&u.rendered&&(i&&u.reposition(p.position.target==="mouse"?f:B.event),j&&u.redraw());return u},toggle:function(b,c){function l(){b?(a.browser.msie&&z[0].style.removeAttribute("filter"),z.css("overflow","")):z.css({display:"",visibility:"",width:"",opacity:"",left:"",top:""})}if(!u.rendered)if(b)u.render(1);else return u;var d=b?"show":"hide",g=p[d],h=z.is(":visible"),j,k;(typeof b).search("boolean|number")&&(b=!h);if(h===b)return u;if(c){if(/over|enter/.test(c.type)&&/out|leave/.test(B.event.type)&&c.target===p.show.target[0]&&z.has(c.relatedTarget).length)return u;B.event=a.extend({},c)}k=a.Event("tooltip"+d),k.originalEvent=c?B.event:f,z.trigger(k,[u,90]);if(k.isDefaultPrevented())return u;a.attr(z[0],"aria-hidden",!b),b?(B.origin=a.extend({},i),u.focus(c),a.isFunction(p.content.text)&&J(),u.reposition(c),g.solo&&a(m,g.solo).not(z).qtip("hide",k)):(clearTimeout(u.timers.show),delete B.origin,u.blur(c)),z.stop(0,1),a.isFunction(g.effect)?(g.effect.call(z,u),z.queue("fx",function(a){l(),a()})):g.effect===e?(z[d](),l.call(z)):z.fadeTo(90,b?1:0,l),b&&g.target.trigger("qtip-"+q+"-inactive");return u},show:function(a){return u.toggle(d,a)},hide:function(a){return u.toggle(e,a)},focus:function(b){if(!u.rendered)return u;var c=a(m),d=parseInt(z[0].style.zIndex,10),e=g.zindex+c.length,f=a.extend({},b),h,i;z.hasClass(n)||(i=a.Event("tooltipfocus"),i.originalEvent=f,z.trigger(i,[u,e]),i.isDefaultPrevented()||(d!==e&&(c.each(function(){this.style.zIndex>d&&(this.style.zIndex=this.style.zIndex-1)}),c.filter("."+n).qtip("blur",f)),z.addClass(n)[0].style.zIndex=e));return u},blur:function(b){var c=a.extend({},b),d;z.removeClass(n),d=a.Event("tooltipblur"),d.originalEvent=c,z.trigger(d,[u]);return u},reposition:function(c,d){if(!u.rendered||x)return u;x=1;var f=p.position.target,g=p.position,k=g.my,l=g.at,m=g.adjust,n=m.method.split(" "),o=z.outerWidth(),q=z.outerHeight(),r=0,s=0,t=a.Event("tooltipmove"),w=z.css("position")==="fixed",y=g.viewport.jquery?g.viewport:a(b),A={left:0,top:0},C=(u.plugins.tip||{}).corner,D={horizontal:n[0],vertical:n[1]||n[0],tip:p.style.tip||{},left:function(a){var b=D.horizontal==="shift",c=y.offset.left+y.scrollLeft,d=k.x==="left"?o:k.x==="right"?-o:-o/2,e=l.x==="left"?r:l.x==="right"?-r:-r/2,f=D.tip.width+D.tip.border*2||0,g=C&&C.precedance==="x"&&!b?f:0,h=c-a-g,i=a+o-y.width-c+g,j=d-(k.precedance==="x"||k.x===k.y?e:0),n=k.x==="center";b?(g=C&&C.precedance==="y"?f:0,j=(k.x==="left"?1:-1)*d-g,A.left+=h>0?h:i>0?-i:0,A.left=Math.max(y.offset.left+(g&&C.x==="center"?D.tip.offset:0),a-j,Math.min(Math.max(y.offset.left+y.width,a+j),A.left))):(h>0&&(k.x!=="left"||i>0)?A.left-=j+(n?0:2*m.x):i>0&&(k.x!=="right"||h>0)&&(A.left-=n?-j:j+2*m.x),A.left!==a&&n&&(A.left-=m.x),A.left<c&&-A.left>i&&(A.left=a));return A.left-a},top:function(a){var b=D.vertical==="shift",c=y.offset.top+y.scrollTop,d=k.y==="top"?q:k.y==="bottom"?-q:-q/2,e=l.y==="top"?s:l.y==="bottom"?-s:-s/2,f=D.tip.height+D.tip.border*2||0,g=C&&C.precedance==="y"&&!b?f:0,h=c-a-g,i=a+q-y.height-c+g,j=d-(k.precedance==="y"||k.x===k.y?e:0),n=k.y==="center";b?(g=C&&C.precedance==="x"?f:0,j=(k.y==="top"?1:-1)*d-g,A.top+=h>0?h:i>0?-i:0,A.top=Math.max(y.offset.top+(g&&C.x==="center"?D.tip.offset:0),a-j,Math.min(Math.max(y.offset.top+y.height,a+j),A.top))):(h>0&&(k.y!=="top"||i>0)?A.top-=j+(n?0:2*m.y):i>0&&(k.y!=="bottom"||h>0)&&(A.top-=n?-j:j+2*m.y),A.top!==a&&n&&(A.top-=m.y),A.top<0&&-A.top>i&&(A.top=a));return A.top-a}};if(f==="mouse")l={x:"left",y:"top"},c=c&&(c.type==="resize"||c.type==="scroll")?B.event:!m.mouse&&B.origin?B.origin:i&&(m.mouse||!c||!c.pageX)?{pageX:i.pageX,pageY:i.pageY}:c,A={top:c.pageY,left:c.pageX};else{f==="event"&&(c&&c.target&&c.type!=="scroll"&&c.type!=="resize"?f=B.target=a(c.target):f=B.target),f=a(f).eq(0);if(f.length===0)return u;f[0]===document||f[0]===b?(r=f.width(),s=f.height(),f[0]===b&&(A={top:!w||h.iOS?y.scrollTop():0,left:!w||h.iOS?y.scrollLeft():0})):f.is("area")&&h.imagemap?A=h.imagemap(f,l):f[0].namespaceURI==="http://www.w3.org/2000/svg"&&h.svg?A=h.svg(f,l):(r=f.outerWidth(),s=f.outerHeight(),A=h.offset(f,g.container,w)),A.offset&&(r=A.width,s=A.height,A=A.offset),A.left+=l.x==="right"?r:l.x==="center"?r/2:0,A.top+=l.y==="bottom"?s:l.y==="center"?s/2:0}A.left+=m.x+(k.x==="right"?-o:k.x==="center"?-o/2:0),A.top+=m.y+(k.y==="bottom"?-q:k.y==="center"?-q/2:0),y.jquery&&f[0]!==b&&f[0]!==v&&D.vertical+D.horizontal!=="nonenone"?(y={elem:y,height:y[(y[0]===b?"h":"outerH")+"eight"](),width:y[(y[0]===b?"w":"outerW")+"idth"](),scrollLeft:y.scrollLeft(),scrollTop:y.scrollTop(),offset:y.offset()||{left:0,top:0}},A.adjusted={left:D.horizontal!=="none"?D.left(A.left):0,top:D.vertical!=="none"?D.top(A.top):0}):A.adjusted={left:0,top:0},z.attr("class",function(b,c){return a.attr(this,"class").replace(/ui-tooltip-pos-\w+/i,"")}).addClass(j+"-pos-"+k.abbreviation()),t.originalEvent=a.extend({},c),z.trigger(t,[u,A,y.elem||y]);if(t.isDefaultPrevented())return u;delete A.adjusted,d===e||isNaN(A.left)||isNaN(A.top)||!a.isFunction(g.effect)?z.css(A):a.isFunction(g.effect)&&(g.effect.call(z,u,a.extend({},A)),z.queue(function(b){a(this).css({opacity:"",height:""}),a.browser.msie&&this.style.removeAttribute("filter"),b()})),x=0;return u},redraw:function(){if(u.rendered<1||p.style.width||y)return u;var b=j+"-fluid",c=p.position.container,d,e,f,g;y=1,z.css("width","").addClass(b),e=z.width()+(a.browser.mozilla?1:0),f=z.css("max-width")||"",g=z.css("min-width")||"",d=(f+g).indexOf("%")>-1?c.width()/100:0,f=(f.indexOf("%")>-1?d:1)*parseInt(f,10)||e,g=(g.indexOf("%")>-1?d:1)*parseInt(g,10)||0,e=f+g?Math.min(Math.max(e,g),f):e,z.css("width",Math.round(e)).removeClass(b),y=0;return u},disable:function(b){var c=l;"boolean"!==typeof b&&(b=!z.hasClass(c)&&!B.disabled),u.rendered?(z.toggleClass(c,b),a.attr(z[0],"aria-disabled",b)):B.disabled=!!b;return u},enable:function(){return u.disable(e)},destroy:function(){var b=c[0],d=a.attr(b,r);u.rendered&&(z.remove(),a.each(u.plugins,function(){this.destroy&&this.destroy()})),clearTimeout(u.timers.show),clearTimeout(u.timers.hide),L(1,1,1,1),a.removeData(b,"qtip"),d&&(a.attr(b,"title",d),c.removeAttr(r)),c.removeAttr("aria-describedby").unbind(".qtip");return c}})}function t(b){var c;if(!b||"object"!==typeof b)return e;"object"!==typeof b.metadata&&(b.metadata={type:b.metadata});if("content"in b){if("object"!==typeof b.content||b.content.jquery)b.content={text:b.content};c=b.content.text||e,!a.isFunction(c)&&(!c&&!c.attr||c.length<1||"object"===typeof c&&!c.jquery)&&(b.content.text=e),"title"in b.content&&("object"!==typeof b.content.title&&(b.content.title={text:b.content.title}),c=b.content.title.text||e,!a.isFunction(c)&&(!c&&!c.attr||c.length<1||"object"===typeof c&&!c.jquery)&&(b.content.title.text=e))}"position"in b&&("object"!==typeof b.position&&(b.position={my:b.position,at:b.position})),"show"in b&&("object"!==typeof b.show&&(b.show.jquery?b.show={target:b.show}:b.show={event:b.show})),"hide"in b&&("object"!==typeof b.hide&&(b.hide.jquery?b.hide={target:b.hide}:b.hide={event:b.hide})),"style"in b&&("object"!==typeof b.style&&(b.style={classes:b.style})),a.each(h,function(){this.sanitize&&this.sanitize(b)});return b}function s(){var c=b.console;return c&&(c.error||c.log||a.noop).apply(c,arguments)}var d=!0,e=!1,f=null,g,h,i,j="ui-tooltip",k="ui-widget",l="ui-state-disabled",m="div.qtip."+j,n=j+"-focus",o=j+"-hover",p="-31000px",q="_replacedByqTip",r="oldtitle";g=a.fn.qtip=function(b,h,i){var j=(""+b).toLowerCase(),k=f,l=j==="disable"?[d]:a.makeArray(arguments).slice(1,10),m=l[l.length-1],n=this[0]?a.data(this[0],"qtip"):f;if(!arguments.length&&n||j==="api")return n;if("string"===typeof b){this.each(function(){var b=a.data(this,"qtip");if(!b)return d;m&&m.timeStamp&&(b.cache.event=m);if(j!=="option"&&j!=="options"||!h)b[j]&&b[j].apply(b[j],l);else if(a.isPlainObject(h)||i!==c)b.set(h,i);else{k=b.get(h);return e}});return k!==f?k:this}if("object"===typeof b||!arguments.length){n=t(a.extend(d,{},b));return g.bind.call(this,n,m)}},g.bind=function(b,c){return this.each(function(f){function p(b){function c(){o.render(typeof b==="object"||i.show.ready),k.show.unbind(l.show),k.hide.unbind(l.hide)}if(o.cache.disabled)return e;o.cache.event=a.extend({},b),i.show.delay>0?(clearTimeout(o.timers.show),o.timers.show=setTimeout(c,i.show.delay),l.show!==l.hide&&k.hide.bind(l.hide,function(){clearTimeout(o.timers.show)})):c()}var i,k,l,m=!b.id||b.id===e||b.id.length<1||a("#"+j+"-"+b.id).length?g.nextid++:b.id,n=".qtip-"+m+"-create",o=v.call(this,m,b);if(o===e)return d;i=o.options,a.each(h,function(){this.initialize==="initialize"&&this(o)}),k={show:i.show.target,hide:i.hide.target},l={show:a.trim(""+i.show.event).replace(/ /g,n+" ")+n,hide:a.trim(""+i.hide.event).replace(/ /g,n+" ")+n},/mouse(over|enter)/i.test(l.show)&&!/mouse(out|leave)/i.test(l.hide)&&(l.hide+=" mouseleave"+n),k.show.bind(l.show,p),(i.show.ready||i.prerender)&&p(c)})},h=g.plugins={Corner:function(a){a=(""+a).replace(/([A-Z])/," $1").replace(/middle/gi,"center").toLowerCase(),this.x=(a.match(/left|right/i)||a.match(/center/)||["inherit"])[0].toLowerCase(),this.y=(a.match(/top|bottom|center/i)||["inherit"])[0].toLowerCase(),this.precedance=a.charAt(0).search(/^(t|b)/)>-1?"y":"x",this.string=function(){return this.precedance==="y"?this.y+this.x:this.x+this.y},this.abbreviation=function(){var a=this.x.substr(0,1),b=this.y.substr(0,1);return a===b?a:a==="c"||a!=="c"&&b!=="c"?b+a:a+b}},offset:function(c,d,e){function l(a,b){f.left+=b*a.scrollLeft(),f.top+=b*a.scrollTop()}var f=c.offset(),g=d,i=0,j=document.body,k;if(g){do{if(g[0]===j)break;g.css("position")!=="static"&&(k=g.position(),f.left-=k.left+(parseInt(g.css("borderLeftWidth"),10)||0),f.top-=k.top+(parseInt(g.css("borderTopWidth"),10)||0),i++)}while(g=g.offsetParent());(d[0]!==j||i>1)&&l(d,1),(h.iOS<4.1&&h.iOS>3.1||!h.iOS&&e)&&l(a(b),-1)}return f},iOS:parseFloat((""+(/CPU.*OS ([0-9_]{1,3})|(CPU like).*AppleWebKit.*Mobile/i.exec(navigator.userAgent)||[0,""])[1]).replace("undefined","3_2").replace("_","."))||e,fn:{attr:function(b,c){if(this.length){var d=this[0],e="title",f=a.data(d,"qtip");if(b===e){if(arguments.length<2)return a.attr(d,r);if(typeof f==="object"){f&&f.rendered&&f.options.content.attr===e&&f.cache.attr&&f.set("content.text",c),a.fn["attr"+q].apply(this,arguments),a.attr(d,r,a.attr(d,e));return this.removeAttr(e)}}}},clone:function(b){var c=a([]),d="title",e;e=a.fn["clone"+q].apply(this,arguments).filter("[oldtitle]").each(function(){a.attr(this,d,a.attr(this,r)),this.removeAttribute(r)}).end();return e},remove:a.ui?f:function(b,c){a(this).each(function(){c||(!b||a.filter(b,[this]).length)&&a("*",this).add(this).each(function(){a(this).triggerHandler("remove")})})}}},a.each(h.fn,function(b,c){if(!c)return d;var e=a.fn[b+q]=a.fn[b];a.fn[b]=function(){return c.apply(this,arguments)||e.apply(this,arguments)}}),a(document).bind("mousemove.qtip",function(a){i={pageX:a.pageX,pageY:a.pageY,type:"mousemove"}}),g.version="nightly",g.nextid=0,g.inactiveEvents="click dblclick mousedown mouseup mousemove mouseleave mouseenter".split(" "),g.zindex=15e3,g.defaults={prerender:e,id:e,overwrite:d,content:{text:d,attr:"title",title:{text:e,button:e}},position:{my:"top left",at:"bottom right",target:e,container:e,viewport:e,adjust:{x:0,y:0,mouse:d,resize:d,method:"flip flip"},effect:d},show:{target:e,event:"mouseenter",effect:d,delay:90,solo:e,ready:e},hide:{target:e,event:"mouseleave",effect:d,delay:0,fixed:e,inactive:e,leave:"window",distance:e},style:{classes:"",widget:e,width:e},events:{render:f,move:f,show:f,hide:f,toggle:f,focus:f,blur:f}},h.tip=function(a){var b=a.plugins.tip;return"object"===typeof b?b:a.plugins.tip=new x(a)},h.tip.initialize="render",h.tip.sanitize=function(a){var b=a.style,c;b&&"tip"in b&&(c=a.style.tip,typeof c!=="object"&&(a.style.tip={corner:c}),/string|boolean/i.test(typeof c.corner)||(c.corner=d),typeof c.width!=="number"&&delete c.width,typeof c.height!=="number"&&delete c.height,typeof c.border!=="number"&&c.border!==d&&delete c.border,typeof c.offset!=="number"&&delete c.offset)},a.extend(d,g.defaults,{style:{tip:{corner:d,mimic:e,width:6,height:6,border:d,offset:0}}})}(jQuery,window) \ No newline at end of file diff --git a/r2/r2/public/static/json.js b/r2/r2/public/static/json.js deleted file mode 100644 index a4607c91..00000000 --- a/r2/r2/public/static/json.js +++ /dev/null @@ -1,145 +0,0 @@ -if (!Object.prototype.toJSONString) { - Array.prototype.toJSONString = function (w) { - var a = [], // The array holding the partial texts. - i, // Loop counter. - l = this.length, - v; // The value to be stringified. - for (i = 0; i < l; i += 1) { - v = this[i]; - switch (typeof v) { - case 'object': - if (v) { - if (typeof v.toJSONString === 'function') { - a.push(v.toJSONString(w)); - } - } else { - a.push('null'); - } - break; - case 'string': - case 'number': - case 'boolean': - a.push(v.toJSONString()); - } - } - return '[' + a.join(',') + ']'; - }; - Boolean.prototype.toJSONString = function () { - return String(this); - }; - Date.prototype.toJSONString = function () { - function f(n) { - return n < 10 ? '0' + n : n; - } - return '"' + this.getUTCFullYear() + '-' + - f(this.getUTCMonth() + 1) + '-' + - f(this.getUTCDate()) + 'T' + - f(this.getUTCHours()) + ':' + - f(this.getUTCMinutes()) + ':' + - f(this.getUTCSeconds()) + 'Z"'; - }; - Number.prototype.toJSONString = function () { - return isFinite(this) ? String(this) : 'null'; - }; - Object.prototype.toJSONString = function (w) { - var a = [], - k, - i, - v; - if (w) { - for (i = 0; i < w.length; i += 1) { - k = w[i]; - if (typeof k === 'string') { - v = this[k]; - switch (typeof v) { - case 'object': - if (v) { - if (typeof v.toJSONString === 'function') { - a.push(k.toJSONString() + ':' + - v.toJSONString(w)); - } - } else { - a.push(k.toJSONString() + ':null'); - } - break; - case 'string': - case 'number': - case 'boolean': - a.push(k.toJSONString() + ':' + v.toJSONString()); - } - } - } - } else { - for (k in this) { - if (typeof k === 'string' && - Object.prototype.hasOwnProperty.apply(this, [k])) { - v = this[k]; - switch (typeof v) { - case 'object': - if (v) { - if (typeof v.toJSONString === 'function') { - a.push(k.toJSONString() + ':' + - v.toJSONString()); - } - } else { - a.push(k.toJSONString() + ':null'); - } - break; - case 'string': - case 'number': - case 'boolean': - a.push(k.toJSONString() + ':' + v.toJSONString()); - } - } - } - } - return '{' + a.join(',') + '}'; - }; - (function (s) { - var m = { - '\b': '\\b', - '\t': '\\t', - '\n': '\\n', - '\f': '\\f', - '\r': '\\r', - '"' : '\\"', - '\\': '\\\\' - }; - s.parseJSON = function (filter) { - var j; - function walk(k, v) { - var i; - if (v && typeof v === 'object') { - for (i in v) { - if (Object.prototype.hasOwnProperty.apply(v, [i])) { - v[i] = walk(i, v[i]); - } - } - } - return filter(k, v); - } - if (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/.test(this. - replace(/\\./g, '@'). - replace(/"[^"\\\n\r]*"/g, ''))) { - j = eval('(' + this + ')'); - return typeof filter === 'function' ? walk('', j) : j; - } - throw new SyntaxError('parseJSON'); - }; - s.toJSONString = function () { - if (/["\\\x00-\x1f]/.test(this)) { - return '"' + this.replace(/[\x00-\x1f\\"]/g, function (a) { - var c = m[a]; - if (c) { - return c; - } - c = a.charCodeAt(); - return '\\u00' + - Math.floor(c / 16).toString(16) + - (c % 16).toString(16); - }) + '"'; - } - return '"' + this + '"'; - }; - })(String.prototype); -} diff --git a/r2/r2/public/static/lesswrong.css b/r2/r2/public/static/lesswrong.css index f1c36947..6b75236d 100644 --- a/r2/r2/public/static/lesswrong.css +++ b/r2/r2/public/static/lesswrong.css @@ -1,487 +1,517 @@ -#wrapper { - padding: 20px 20px 0 20px; -} -#header { - position: relative; - height: 160px; -} -#gradient { - position: absolute; - top: 0; - left: 0; -} -#fhi { - position: absolute; - bottom: 0; - right: 2px; -} -#content.clear.fullwidth { - padding: 0 15px; - width: 920px; -} - -div.comment-meta span span { - float: none; -} -div.permamessage { - clear: both; -} - -#side-tags { - text-align: justify; - line-height: 1.2; - display: block; -} -#side-tags ul { - margin: 0; -} -#side-tags li.selected { - color: #414143; - font-weight: bold; -} -#side-tags .one { font-size: 0.9em; } -#side-tags .two { font-size: 0.95em; } -#side-tags .three { font-size: 1em; } -#side-tags .four { font-size: 1.125em; } -#side-tags .five { font-size: 1.25em; } -#side-tags .six { font-size: 1.375em; } -#side-tags .seven { font-size: 1.5em; } -#side-tags .eight { font-size: 1.625em; } -#side-tags .nine { font-size: 1.75em; } -#side-tags .ten { font-size: 1.75em; } - -.divide { border-right: 2px solid #D3D3D3; } - -.loginform { - float: left; - width: 45%; - padding-left: 15px; - padding-right: 15px; -} - -.loginform h3 { - margin-bottom: 0; - margin-top: 10px; - font-size: large; - font-weight: bold; - font-variant: small-caps; - color: #404040; -} -.loginform p { - text-align: left; - margin-bottom: 10px; - color: #606060; - margin-bottom: 20px; -} -.loginform label { - display: block; - font-weight: bold; - color: #606060; -} - -.loginform .remember { display:inline; margin-left: 5px; } -.loginform ul { margin: 5px; list-style: none; } -.loginform li { margin-top: 5px; } -.loginform p .btn { margin-top: 5px } -.loginform input.logtxt { width: 125px; } - -.loginform input[type=text], -.loginform input[type=password] { - width: 125px; - border: 1px solid #A0A0A0; - margin-top: 2px; - margin-bottom: 2px; - padding: 1px; -} - -.loginform #captcha { - width: 250px; - } - -/* cover */ -.cover { - position: fixed; - top: 0px; - left: 0px; - height: 100%; - width: 100%; - background-color: gray; - opacity: .7; - filter:alpha(opacity=70); /* IE patch */ - z-index: 1000; -} - -.popup { - position: absolute; - left: 10%; - background-color: white; - top: 100px; - width: 80%; - text-align: left; - z-index: 1001; - padding: 10px; - border-color: #B2B2B2 black black #B2B2B2; - border-style: solid; - border-width: 1px; -} - -.popup h1 { - text-align: center; - font-size: large; - font-weight: normal; - color: orangered; -} - -.popup h2 { - text-align: center; - font-size: small; - margin-top: 0px; - color: black; - font-weight: normal; -} - -/* Recent comments */ -#side-comments h3 { - margin: 0; - font-size: 1em; - font-weight: bold; - margin: 0; - max-width: 175px; - overflow: hidden; - height: 1.4em; -} -#side-comments h2 a, #side-posts h2 a { - color: rgb(65, 65, 67); /* For IE[67] */ -} -#side-comments h2 a:hover, #side-posts h2 a:hover { - text-decoration: underline; -} - -/* Recent articles */ -#side-posts div.reddit-link div, -#side-comments div.inline-comment, -#side-contributors div.user { - display: list-item; - list-style: disc; - margin-left: 15px; -} - -/* Recent articles listing */ -div.inline-listing h3 { - display: inline; - font-size: 1em; - margin: 0; -} -div.inline-listing span { - margin-left: 0.5em; -} -div.inline-listing div.sitetable { - margin-bottom: 1em; -} -div.inline-listing div.sitetable div { - display: list-item; - list-style: disc; - margin-left: 15px; -} -div.inline-listing div.sitetable div.ajaxhook { - display: none; -} -div.inline-listing a { - text-decoration: none; -} -div.inline-listing a:hover { - text-decoration: underline; -} - -.commentreply { - clear: both; - width: 40em; - margin: 10px 10px 10px 0; -} -.commentreply textarea { - width: 100%; -} -.commentreply .buttons { - float: left; -} - -.commentreply table.help { - margin: 5px 0 0 1px; - width: 100%; - border-collapse: collapse; -} -.commentreply .help, -.commentreply .help td, -.commentreply .help tr { - border: 1px solid #C0C0C0; - padding: 4px; - margin: 0px; -} -.commentreply .help-toggle { - float:right; -} - -#comment-controls { - float: right; -} -#comment-controls label { - padding-right: 5px; -} -div.comment .focal { - background-color: #ffc; -} - -/* Markdown */ -.md { font-size: small; } -.md h1, -.md h2, -.md h3, -.md h4, -.md h5, -.md h6 { color: black; float: none; } -#comments .md h2 { color: black; float: none; } -.md h2 { font-size: 16px; } -.md h3 { font-size: 15px; } -.md h4 { font-size: 14px; } -.md strong { font-weight: bold; } -.md em, .help em { font-style: italic; } -.md strong em { font-style: italic; font-weight: bold } -.md ol, .md ul, .help ul { margin: 10px 2em; } -.md ul, .help ul { list-style: disc outside } -.md ol { list-style: decimal outside } -.md pre { margin: 10px; } -.md blockquote, .help blockquote { - border-left: 2px solid #369; - padding-left: 4px; - margin: 5px; - margin-right: 15px; -} - -/* Rules to make the editor styled properly */ -.mceContentBody.md { - padding: 0.5em; -} - -h1 a, h2 a { - color: inherit; - text-decoration: inherit; -} - -a.up, a.down { - cursor: pointer; -} - -/* default form styles */ -.pretty-form { - vertical-align: top; -} - -.pretty-form p {margin: 3px ;} -.pretty-form input[type=checkbox], -.pretty-form input[type=radio] {margin: 2px .5em 0px .5em; } -.pretty-form img { margin: 3px .5em} -.pretty-form table { - width: 100%; -} - -.pretty-form .infobar { - width: 285px; - margin: 5px; -} - -.pretty-form input[type=text], -.pretty-form input[type=file], -.pretty-form input[type=password], -.pretty-form select, -.pretty-form b, -.pretty-form textarea, -.pretty-form button { margin: 3px .5em; } -.pretty-form th { text-align: right } - -/*submit*/ -.pretty-form.long-text input[type=text], -.pretty-form.long-text textarea, -.pretty-form.long-text input[type=password] {margin: 3px; width: 40em } - -/* Image browser */ -#images { - padding: 10px; - width: 100%; -} -.image-upload .new-image { margin-left: 20px } -.image-upload td, -.image-upload th { vertical-align: top; } -.image-upload span { padding-left: 5px; } -.image-upload { display: inline; } - -ul#image-preview-list { - margin: 20px 20px 20px 20px; -} -ul#image-preview-list li { - padding-bottom: 10px; - margin-bottom: 20px; - vertical-align: top; - width: 45%; - height: 100px; - float: left; - position: relative; - display: inline; -} - -ul#image-preview-list .preview { - width: 100px; - float: left; - display: block; - text-align: center; - max-height: 100px; - overflow: hidden; -} -ul#image-preview-list .preview img { - max-width: 100px; - padding: auto; -} -ul#image-preview-list .description { - vertical-align: top; - margin-left: 105px; -} -ul#image-preview-list .description pre { - display: inline; - padding: 5px; -} - -/* pref table - used for preferences and edit subreddit pages */ -.preftable th { - padding-top: 2px; - font-weight: bold; - vertical-align: top; - text-align: right; -} -.preftable td.prefright { padding: 0;} -.preftable .spacer { margin-bottom: 5px; } -.preftable .note { width: 100%; vertical-align: top; padding-top: 2px; } -.save-button { margin-left: 5px; } - -.reported { background-color: #f6e69f } -.suspicious { background-color: #f6e69f } -.spam { background-color: #FA8072 } - -/* For lists of users, such as friends */ -.usertable { margin-left: 10px;} -.usertable td { padding: 0 .7em } -.usertable { white-space: nowrap } -.usertable h1 { padding: 10px 0; margin: 0;} - -div.sidebox select { - width: 126px; -} -div.sidebox label { - width: 64px; -} - -/* Google search */ -#side-search input[type=text] { - width: 130px; -} -#side-search input[type=submit] { - width: 45px; - margin-left: 5px; -} -div#side-search { - padding-bottom: 10px; -} - -/* Message bar at the top of the page */ -.infobar { - background-color: #f7f7f8; - padding: 5px; - margin: 0; - margin-bottom: 10px; - border: 1px solid rgb(83, 141, 77); - font-size: small; -} - -.infobar p { - margin: 0; -} - -.sitetable { - clear: both; -} - -/* Selected up/down votes */ -div.tools div.vote a.mod, body.post div.tools div.vote a.mod { - background-position: 0 -36px; -} -div.comment-links a.mod { - font-weight: bold; -} - -.error { - color: red; - margin: 5px; -} -div.comment div.parent { - padding-left: 5px; -} - -/* Login form in sidebar */ -#remember-me input { - margin-left: 0; - width: auto; - border: none; -} -#side-login #recover { - float: right; - margin: 2px 0; -} -#side-login button { - float: left; -} -#side-login .error, #comments .error { - margin-left: 0; -} - -/* Feed link in sidebar */ -#side-feed a, div.feed-icon { - float: left; -} -#side-feed a { - display: block; - margin-left: 4px; -} -#side-feed div.feed-icon a { - margin: 0; -} -#side-feed div.feed-icon { - margin-top: 2px; -} - -/* About box */ -#side-about a { - font-size: larger; - text-decoration: underline; -} - -/* Footer */ -#reddit-logo { - float: right; -} -.footer { - border-top: 2px solid #d6d5d6; - padding: 5px 0; -} -.footer div.link { - float: left; - margin-left: 15px; - margin-top: 5px; -} - -/* Categories page */ -.sr-toggle-button { - width: 54px; - height: 18px; - margin-bottom: 5px; - cursor: pointer; -} -.sr-toggle-button.add { background-image: url(/static/sr-add-button.png) } -.sr-toggle-button.remove { background-image: url(/static/sr-remove-button.png)} +#content.clear.fullwidth { + padding: 0 15px; + width: 920px; +} + +div.comment-meta span span { + float: none; +} +div.permamessage { + clear: both; +} + +.divide { border-right: 2px solid #D3D3D3; } + +.loginform { + float: left; + width: 45%; + padding-left: 15px; + padding-right: 15px; +} + +.loginform h3 { + margin-bottom: 0; + margin-top: 10px; + font-size: large; + font-weight: bold; + font-variant: small-caps; + color: #404040; +} +.loginform p { + text-align: left; + margin-bottom: 10px; + color: #606060; + margin-bottom: 20px; +} +.loginform label { + display: block; + font-weight: bold; + color: #606060; +} + +.loginform .remember { display:inline; margin-left: 5px; } +.loginform ul { margin: 5px; list-style: none; } +.loginform li { margin-top: 5px; } +.loginform p .btn { margin-top: 5px } +.loginform input.logtxt { width: 125px; } + +.loginform input[type=text], +.loginform input[type=password] { + width: 125px; + border: 1px solid #A0A0A0; + margin-top: 2px; + margin-bottom: 2px; + padding: 1px; +} + +.loginform #captcha { + width: 250px; + } + +/* cover */ +.cover { + position: fixed; + top: 0px; + left: 0px; + height: 100%; + width: 100%; + background-color: gray; + opacity: .7; + filter:alpha(opacity=70); /* IE patch */ + z-index: 1000; +} + +.popup { + position: absolute; + left: 10%; + background-color: white; + top: 100px; + width: 80%; + text-align: left; + z-index: 1001; + padding: 10px; + border-color: #B2B2B2 black black #B2B2B2; + border-style: solid; + border-width: 1px; +} + +.popup h1 { + text-align: center; + font-size: large; + font-weight: normal; + color: orangered; +} + +.popup h2 { + text-align: center; + font-size: small; + margin-top: 0px; + color: black; + font-weight: normal; +} + +/* Recent articles listing */ +div.inline-listing h3 { + display: inline; + font-size: 1em; + margin: 0; +} +div.inline-listing span { + margin-left: 0.5em; +} +div.inline-listing div.sitetable { + margin-bottom: 1em; +} +div.inline-listing div.sitetable div { + display: list-item; + list-style: disc; + margin-left: 15px; +} +div.inline-listing div.sitetable div.ajaxhook { + display: none; +} +div.inline-listing a { + text-decoration: none; +} +div.inline-listing a:hover { + text-decoration: underline; +} + +.commentreply { + clear: both; + margin: 10px 15px 30px 0; +} +.commentreply textarea { + border-color: #e9e9e9; + -webkit-box-shadow: inset 1px 1px 5px #dbdbdb; + -moz-box-shadow: inset 1px 1px 5px #dbdbdb; + box-shadow: inset 1px 1px 5px #dbdbdb; + margin: 0 0 10px; + width: 100%; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +.commentreply .buttons { + float: left; +} +.commentreply .buttons button, .commentreply .help-toggle button { + background: #f0eeee; /* Old browsers */ + background: -moz-linear-gradient(top, #f0eeee 0%, #c1c1c1 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f0eeee), color-stop(100%,#c1c1c1)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #f0eeee 0%,#c1c1c1 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #f0eeee 0%,#c1c1c1 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #f0eeee 0%,#c1c1c1 100%); /* IE10+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f0eeee', endColorstr='#c1c1c1',GradientType=0 ); /* IE6-9 */ + background: linear-gradient(top, #f0eeee 0%,#c1c1c1 100%); /* W3C */ + border-color: #c6c6c6; + color: #666666; + cursor: pointer; + font-weight: bold; + padding: 5px 30px; +} + +.commentreply table.help { + clear: both; + margin: 5px 0 0 1px; + width: 480px; + border-collapse: collapse; +} +.commentreply .help, +.commentreply .help td, +.commentreply .help tr { + border: 1px solid #C0C0C0; + padding: 4px; + margin: 0px; +} +.commentreply .help-toggle { + float: right; +} +.commentreply .help-toggle a { + color: #538D4D; + font-weight: bold; + text-decoration: none; +} + +#comment-controls label { + padding-right: 5px; + margin-left: 1em; +} +div.comment .focal { + background-color: #ffc; + padding-left: 5px; + padding-right: 5px; +} + + + +/* Markdown */ +.md { font-size: small; } +.md h1, +.md h2, +.md h3, +.md h4, +.md h5, +.md h6 { color: black; float: none; } +#comments .md h2 { color: black; float: none; } +.md h2 { font-size: 16px; } +.md h3 { font-size: 15px; } +.md h4 { font-size: 14px; } +.md strong { font-weight: bold; } +.md div { margin-bottom: 1em; } +.md em, .help em { font-style: italic; } +.md strong em { font-style: italic; font-weight: bold } +.md ol, .md ul, .help ul { margin: 10px 2em; } +.md ul, .help ul { list-style: disc outside } +.md ol { list-style: decimal outside } +.md pre { margin: 10px; overflow-x: auto; } +.md blockquote, .help blockquote { + border-left: 2px solid #369; + padding-left: 4px; + margin: 5px; + margin-right: 15px; +} + +/* Rules to make the editor styled properly */ +.mceContentBody.md { + padding: 0.5em; +} + +h1 a, h2 a { + color: inherit; + text-decoration: inherit; +} + +/* default form styles */ +.pretty-form { + margin-top: 10px; + vertical-align: top; +} + +.pretty-form p {margin: 3px ;} +.pretty-form input[type=checkbox], +.pretty-form input[type=radio] {margin: 2px .5em 0px .5em; } +.pretty-form img { margin: 3px .5em} +.pretty-form table { + width: 100%; +} + +.pretty-form .infobar { + width: 285px; + margin: 5px; +} + +.pretty-form input[type=text], +.pretty-form input[type=file], +.pretty-form input[type=password], +.pretty-form select, +.pretty-form b, +.pretty-form textarea, +.pretty-form button { margin: 3px .5em; } +.pretty-form th { text-align: right } + +/*submit*/ +.pretty-form.long-text input[type=text], +.pretty-form.long-text textarea, +.pretty-form.long-text input[type=password] {margin: 3px; width: 40em } + +/* Image browser */ +#images { + padding: 10px; + width: 100%; +} +.image-upload .new-image { margin-left: 20px } +.image-upload td, +.image-upload th { vertical-align: top; } +.image-upload span { padding-left: 5px; } +.image-upload { display: inline; } + +ul#image-preview-list { + margin: 20px 20px 20px 20px; +} +ul#image-preview-list li { + padding-bottom: 10px; + margin-bottom: 20px; + vertical-align: top; + width: 45%; + height: 100px; + float: left; + position: relative; + display: inline; +} + +ul#image-preview-list .preview { + width: 100px; + float: left; + display: block; + text-align: center; + max-height: 100px; + overflow: hidden; +} +ul#image-preview-list .preview img { + max-width: 100px; + padding: auto; +} +ul#image-preview-list .description { + vertical-align: top; + margin-left: 105px; +} +ul#image-preview-list .description pre { + display: inline; + padding: 5px; +} + +/* pref table - used for preferences and edit subreddit pages */ +.preftable th { + padding-top: 2px; + font-weight: bold; + vertical-align: top; + text-align: right; +} +.preftable td.prefright { padding: 0;} +.preftable .spacer { margin-bottom: 5px; } +.preftable .note { width: 100%; vertical-align: top; padding-top: 2px; } +.save-button { margin-left: 5px; } + +.reported { background-color: #f6e69f } +.suspicious { background-color: #f6e69f } +.spam { background-color: #FA8072 } + +/* For lists of users, such as friends */ +.usertable { margin-left: 10px;} +.usertable td { padding: 0 .7em } +.usertable { white-space: nowrap } +.usertable h1 { padding: 10px 0; margin: 0;} + +div.sidebox select { + width: 100px; +} +div.sidebox label { + width: 90px; +} + +/* Wiki links */ +#side-wikilinks select { + width: 136px; +} +#side-wikilinks input[type=submit] { + width: 45px; + margin-left: 2px; +} +div#side-wikilinks { + padding-bottom: 10px; +} + +/* Message bar at the top of the page */ +.infobar { + background-color: #f7f7f8; + padding: 5px; + margin: 10px 0; + border: 1px solid rgb(83, 141, 77); + font-size: small; +} + +.infobar p { + margin: 0; +} + +.sitetable { + clear: both; +} + +.error { + color: red; + margin: 5px; +} + + + +div.comment div.parent { + padding-left: 5px; +} + +/* Login form in sidebar */ +#remember-me input { + margin-left: 0; + width: auto; + border: none; +} +#side-login #recover { + float: right; + margin: 2px 0; +} +#side-login button { + float: left; +} +#side-login .error, #comments .error { + margin-left: 0; +} + + +/* About box */ +#side-about a { + font-size: larger; + text-decoration: underline; +} + +/* Subreddit box */ +#subreddit-info form { + display: inline; +} + +/* Categories page */ +.sr-toggle-button { + width: 54px; + height: 18px; + margin-bottom: 5px; + cursor: pointer; +} +.sr-toggle-button.add { background-image: url(/static/sr-add-button.png) } +.sr-toggle-button.remove { background-image: url(/static/sr-remove-button.png)} + +/* Draft marker on post title */ +#content span.draft { + color: red +} + +/* Submit form */ +.pretty-form.long-text .cap-reply { + display: block; +} +.pretty-form.long-text #captcha { + width: 250px; +} + +/* Edit listing */ +.edit .md { + clear: left; + margin-bottom: 1em; +} +.edit .diff { + padding: 0; + margin: 0 !important; + border: 1px solid #ccc; + clear: left; +} +.edit .diff div { + margin: 0; +} +.edit .diff .new { + background-color: #dfd; +} +.edit .diff .del { + background-color: #fdd; +} +.edit .diff .context { + background-color: #eaf2f5; + color: #999; +} +.edit .editor { + float: left; +} +.edit .editor .author, .edit .orig-author .author { + float: none; + display: inline; +} +.edit .orig-author .author { + margin-right: 0; +} +/* Meetups */ +#map { + margin-top: 1em; + width: 640px; + height: 480px; +} +div.meta.meetup { + color: black; +} +div.meta.meetup strong { + float: left; + width: 4em; +} +.pretty-form.meetup label { + font-weight: bold; + float: left; + width: 100px; +} +.pretty-form.meetup input, +.pretty-form.meetup textarea { + width: 400px; + float: left; +} +.pretty-form.meetup input.date { + width: 142px; + padding-right: 20px; +} +.pretty-form fieldset { + border: none; + margin: 0; + padding: 0; +} +.pretty-form.meetup button.btn { + margin-left: 0; +} +.pretty-form.meetup #NO_DESCRIPTION { + margin: 0 0 5px 106px; + display: block; +} +.pretty-form.meetup #geocoded_location { + margin-left: 106px; + clear: left; +} +.pretty-form.meetup #geocode_status { + display: block; + width: 16px; + height: 16px; + padding: 4px 0; +} diff --git a/r2/r2/public/static/link.js b/r2/r2/public/static/link.js index f13dbb31..542692c6 100644 --- a/r2/r2/public/static/link.js +++ b/r2/r2/public/static/link.js @@ -362,7 +362,7 @@ Listing.exists = function(id) { }; Listing.attach = function(node) { - var id = /siteTable_(.*)/.exec(node.id); + var id = /siteTable_?(.*)/.exec(node.id); if (id) { var listing = new Listing(id[1]); if (listing.listing) { @@ -411,7 +411,28 @@ function _fire_and_hide(type) { Listing.unhide = _fire_and_hide('unhide'); Listing.hide = _fire_and_hide('hide'); Listing.report = _fire_and_hide('report'); -Listing.del = _fire_and_hide('del'); +Listing.retract = function(fullname) { + redditRequest('retract', {id: fullname, uh: modhash}, function(r) { + var res_obj = parse_response(r); + if (res_obj.error) { + alert(res_obj.error.message); + } else { + $('body_'+fullname).addClassName('retracted'); + } + }); +} + + +Listing.del = function(fullname) { + redditRequest('del', {id: fullname, uh: modhash}, function(r) { + var res_obj = parse_response(r); + if (res_obj.error) { + alert(res_obj.error.message); + } else { + new Link(fullname).hide(true); + } + }); +} Listing.parse = function(r) { var links = []; diff --git a/r2/r2/public/static/logo-discussion.png b/r2/r2/public/static/logo-discussion.png new file mode 100644 index 00000000..d3d89c69 Binary files /dev/null and b/r2/r2/public/static/logo-discussion.png differ diff --git a/r2/r2/public/static/logo_trans.png b/r2/r2/public/static/logo_trans.png index 0fba1a09..b5ddccbb 100644 Binary files a/r2/r2/public/static/logo_trans.png and b/r2/r2/public/static/logo_trans.png differ diff --git a/r2/r2/public/static/mail.png b/r2/r2/public/static/mail.png new file mode 100644 index 00000000..48f88e3b Binary files /dev/null and b/r2/r2/public/static/mail.png differ diff --git a/r2/r2/public/static/mailgray.png b/r2/r2/public/static/mailgray.png new file mode 100644 index 00000000..bc3d8005 Binary files /dev/null and b/r2/r2/public/static/mailgray.png differ diff --git a/r2/r2/public/static/main.css b/r2/r2/public/static/main.css old mode 100755 new mode 100644 index 678c48f2..c43dd576 --- a/r2/r2/public/static/main.css +++ b/r2/r2/public/static/main.css @@ -1,26 +1,52 @@ +/* Clearing +-------------------------------------------------------------------------------------- */ +#main:after, +#content:after, +ul#nav:after, +ul#filternav:after, +form div.row:after, +div.post:after, +div.post div.content:after, +div.meta:after, +div.tools:after, +div.tools div.boxright:after, +div.tools div.boxright ul:after, +#comments:after, +div.comment:after, +div.comment div.entry:after, +div.comment-meta:after, +div.comment-links:after, +div.comment-links ul:after, +ul#rightnav:after, +div.sidebox:after, +div.footer:after, +div.footer ul.footer-links:after, +.clear:after { clear: both; content: "."; display: block; height: 0; visibility: hidden; } + + /* General Page Structure -------------------------------------------------------------------------------------- */ body { - background-color: #3d3d3e; + background-color: #d8d8d8; margin: 0; padding: 0; } #wrapper { background-color: #fff; + -webkit-box-shadow: 0px 0px 10px #555; + -moz-box-shadow: 0px 0px 10px #555; + box-shadow: 0px 0px 10px #555; margin: 0 auto; - padding: 35px 20px; - width: 950px; -} -#header { - background: url(/static/header_background.jpg) no-repeat top right; + padding: 0; + width: 990px; } #main { - padding: 15px 0; + padding: 15px 20px; } #content { float: left; - padding: 0 35px 0 15px; - width: 680px; + padding: 0 0 0 15px; + width: 695px; } #sidebar { float: right; @@ -30,10 +56,6 @@ body { /* General Styles -------------------------------------------------------------------------------------- */ -/* IE 6 */ * html .clear, * html form div.row { height: 1%; overflow: visible; } -/* IE 7 */ *+html .clear, *+html form div.row { min-height: 1%; } -/* Other */ .clear:after, form div.row:after { clear: both; content: "."; display: block; height: 0; visibility: hidden; } - a img { border: none; } @@ -53,10 +75,14 @@ h1, h2, h3, h4, h5, h6, p, ol, ul { margin: 0 0 1em; } h1, h2 { - color: #538d4d; + color: #333; font-size: 1.3333em; /* 16px */ margin: 0 0 0.75em; } +h1 { + font-size: 20px; + font-weight: bold; +} h2 a { color: #538d4d; text-decoration: none; @@ -66,12 +92,21 @@ ol, ul { padding: 0; } a { - color: #8a8a8b; + color: #6a8a6b; text-decoration: underline; } -a:hover { +a:visited { + color: #8a8a8b; +} +h2 a:visited:hover, a:hover, div.tools a:hover { color: #3d3d3e; } +h2 a:visited { + color: #5a5a5b; +} +div.tools a { + color: #8a8a8b; +} /* Forms @@ -82,9 +117,9 @@ form { } input, select, textarea, button { border: 1px solid #8a8a8b; - border-radius: 4px; /* CSS3 */ - -moz-border-radius: 4px; /* Mozilla (FF) */ - -webkit-border-radius: 4px; /* Webkit (Safari,Chrome) */ + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + border-radius: 4px; font: 1em Arial, Helvetica, sans-serif; padding: 2px; } @@ -92,56 +127,184 @@ input, select, textarea, button { /* Header -------------------------------------------------------------------------------------- */ +#header { + background: url(/static/header_background.jpg) no-repeat top left; + height: 103px; + text-align: right; +} a#logo { display: block; - position: absolute; - top: 25px; - left: 25px; + float: left; + margin: 35px 10px 0 35px; +} +img#tagline { + display: block; + float: left; + margin: 59px 0 0; } -a#logo img { +a#siai, +a#fhi { display: block; + float: right; + height: 0; + padding-top: 69px; + overflow: hidden; +} +a#siai { + background: url(/static/SIAI.png) no-repeat top left; + background-position: 0 -69px; + width: 121px; + margin: 44px 10px 0 0; +} +a#fhi { + background: url(/static/FHI_diamond.png) no-repeat top left; + background-position: 0 -69px; + width: 108px; + margin: 40px 20px 0 0; } + +a#siai:hover, a#fhi:hover { + background-position: 0 0px; +} + + +/* Nav Menu +-------------------------------------------------------------------------------------- */ ul#nav { - background-color: #f7f7f8; - border-top: 1px solid #d6d5d6; - border-bottom: 2px solid #d6d5d6; + border-bottom: 1px solid #c9c7c7; clear: both; list-style: none; margin: 0; - margin-top: 5px; - padding: 5px 0; + padding: 0 0 10px; } ul#nav li { - border-left: 1px solid #c1c2c4; + border-right: 1px solid #c9c7c7; float: left; - line-height: 1.2; - margin: 1px 0; - padding: 0 15px; + font-size: 15px; + line-height: 1; + margin: 0 30px 0 0; + padding: 0 30px 0 0; + position: relative; text-transform: uppercase; } -ul#nav li:first-child { - border-left: none; +ul#nav li:last-child { + border-right: none; } ul#nav li a { - color: #bbbbbe; + color: #999; text-decoration: none; } ul#nav li.active a { - color: #414143; + color: #666666; font-weight: bold; } ul#nav li a:hover { - color: #414143; + color: #777; } ul#nav li.right { float: right; - border: none; +} +ul#nav li img.dropdown { + cursor: pointer; + vertical-align: middle; +} +ul#nav li ul { + background-color: #fff; + border: 1px solid #c9c7c7; + display: none; + left: 0; + list-style: none; + margin: 0; + padding: 0; + position: absolute; + top: 20px; +} +ul#nav li ul li { + border-right: none; + float: none; + padding: 0; + font-size: 12px; + line-height: 1.5; + position: static; + text-transform: none; +} +ul#nav li ul li a { + display: block; + padding: 2px 10px; +} +ul#nav li ul li a:hover { + background-color: #eee; +} +ul#filternav { + border-bottom: 1px solid #c9c7c7; + list-style: none; + margin: 0 0 10px; + padding: 8px 0; +} +ul#filternav li { + border-right: 1px solid #c9c7c7; + float: left; + line-height: 1; + margin: 0 20px 0 0; + padding: 0 20px 0 0; +} +ul#filternav li a { + color: #9b9a9a; + text-decoration: none; +} +ul#filternav li.active a { + color: #666666; + font-weight: bold; +} +ul#filternav li:last-child { + border-right: none; } +#post-filter { + position: relative; + margin-bottom: 10px; + z-index: 999; +} +#post-filter div.filter-active { + background: url(/static/disclosure-triangle.gif) no-repeat 0 2px; + color: #666666; + cursor: pointer; + font-size: 11px; + padding-left: 18px; +} +#post-filter div.open { + background-position: 0 -16px; +} +#post-filter div.filter-options { + background-color: #fff; + border: 1px solid #c9c7c7; + display: none; + font-size: 11px; + left: 0; + margin: 0; + padding: 0; + position: absolute; + top: 20px; + z-index: 999; +} +#post-filter div.filter-options a { + display: block; + padding: 2px 10px; + text-decoration: none; +} +#post-filter div.filter-options a:hover { + background-color: #eee; +} + + /* Post -------------------------------------------------------------------------------------- */ div.post { - margin: 0 0 30px; + clear: both; + margin: 5px 0 10px; +} +body.post div.post { + padding: 10px 0 0; } div.meta { color: #7f7f83; @@ -153,22 +316,35 @@ div.meta span { float: left; margin-right: 15px; } +div.meta span.date { + color: #999; + font-style: italic; +} div.meta span.votes { - background: url(/static/votes-circle.gif) no-repeat top left; - display: block; - height: 22px; + margin: 0 5px 0 0; +} +div.meta span.votes span.votes { + -webkit-border-radius: 11px; + -moz-border-radius: 11px; + border-radius: 11px; + border: 2px solid #538d4d; + height: 18px; + line-height: 18px; + margin: 0; + padding: 0 7px; font-weight: bold; - line-height: 22px; text-align: center; - width: 22px; } -div.editors-pick div.meta span.votes { - background-position: 0 -22px; +div.editors-pick div.meta span.votes span.votes { color: #fff; - margin-right: 5px; + background-color: #538d4d; + border: none; + height: 22px; + line-height: 22px; + padding: 0 9px; } div.meta span.editors-pick { - color: #538d4d; + color: #538d4d; font-weight: bold; } div.meta span.author a { @@ -183,12 +359,12 @@ div.content a.more { text-align: right; } div.tools { - background-color: #f9f9fa; - border-top: 1px solid #dedede; - border-bottom: 2px solid #dedede; + border-top: 1px solid #c9c7c7; + border-bottom: 1px solid #c9c7c7; line-height: 18px; /* Same height as vote circles */ margin: 12px 0 0; - padding: 5px 10px; + padding: 10px 0; + position: relative; } body.post div.tools a { color: #538d4d; @@ -197,15 +373,19 @@ div.tools div.vote { float: left; } div.tools div.vote a { - background-position: top left; + background-position: 0 0; background-repeat: no-repeat; + cursor: pointer; + display: block; float: left; - margin-right: 20px; - padding: 0 0 0 22px; + height: 0; + margin-right: 5px; + overflow: hidden; + padding: 20px 0 0; + width: 14px; } -body.post div.tools div.vote a { - background-position: 0 -18px; - font-weight: bold; +div.tools div.vote a:hover, div.tools div.vote a.mod { + background-position: 0 -20px; } div.tools div.vote a.up { background-image: url(/static/vote-up.gif); @@ -213,9 +393,11 @@ div.tools div.vote a.up { div.tools div.vote a.down { background-image: url(/static/vote-down.gif); } + div.tools a.comment { float: left; margin-left: 10px; + margin-bottom: 1em; } body.post div.tools a.comment { font-weight: bold; @@ -224,10 +406,11 @@ div.tools div.boxright { float: right; } div.tools ul { + float: right; line-height: 1em; list-style: none; margin: 0; - padding: 3px 0; + padding: 0; } div.tools ul li { border-left: 1px solid #dedede; @@ -241,6 +424,30 @@ body.post div.tools ul li { div.tools ul li:first-child { border-left: none; } +div.tools a.save, div.tools a.edit, div.tools a.hide { + cursor: pointer; + display: block; + height: 0; + overflow: hidden; + padding: 20px 0 0; + width: 16px; +} +div.tools a.save:hover, div.tools a.save.mod, +div.tools a.edit:hover, +div.tools a.hide:hover, div.tools a.hide.mod { + background-position: 0 -20px; +} +div.tools a.save { + background: url(/static/save-button.gif) no-repeat 0 0; +} +div.tools a.edit { + background-image: url(/static/edit.png); +} +div.tools a.hide { + background-image: url(/static/hide.png); + width: 26px; +} + div.tools div.tags { clear: both; padding-left: 20px; @@ -254,18 +461,119 @@ div.tools div.tags a { margin: 0 5px; } div.tools img.landscape { + bottom: 5px; display: block; float: right; margin: 10px 0 0; + position: absolute; + right: 10px; } -/* Comments +/* Meetup +-------------------------------------------------------------------------------------- */ +body.meetup-index div.post, +body.meetup div.post { + padding: 30px 0 0; +} +body.meetup-index h1 strong, +body.meetup h1 strong { + text-transform: uppercase; +} +body.meetup-index #map { + background-color: #eee; + height: 500px; + margin: 0 0 20px; + width: 695px; +} +div.meetup-listing { + margin-top: 20px; +} +div.meetup-listing div#siteTable { + clear: none; +} +div.meetup-listing ul { + color: #6A8A6B; + font-size: 13px; + margin-top: 30px; + margin-left: 20px; +} +div.meetup-listing ul li { + margin: 0 0 5px; +} +div.meetup-content { + float: left; + width: 445px; + text-align: left; +} +div.meetup-meta { + margin: 0 0 20px; +} +div.meetup-meta p { + margin: 0; +} +div.meetup-meta p strong { + text-transform: uppercase; +} +body.meetup div.post div.content { + margin: 0 0 30px; +} +#front-map, +#meetup-map, +body.meetup #map { + background-color: #eee; + float: right; + height: 300px; + margin: 0; + width: 240px; +} +/* Comments and Messages -------------------------------------------------------------------------------------- */ +#comment-controls { + float: right; + position: relative; +} +#comment-controls div.filter-inactive, #comment-controls div.filter-active { + color: #666666; + font-size: 11px; + padding-left: 18px; +} +#comment-controls div.filter-active { + background: url(/static/disclosure-triangle.gif) no-repeat 0 2px; + cursor: pointer; +} +#comment-controls div.open { + background-position: 0 -16px; +} +#comment-controls div.filter-options { + background-color: #fff; + border: 1px solid #c9c7c7; + display: none; + font-size: 11px; + margin: 0; + padding: 0; + position: absolute; + right: 0; + top: 20px; +} +#comment-controls div.filter-options a { + display: block; + padding: 2px 10px; + text-align: right; + text-decoration: none; +} +#comment-controls div.filter-options a:hover { + background-color: #eee; +} + +#comments { + clear: both; +} #comments h2 { - color: #666667; + color: #333; float: left; - padding-right: 10px; + font-weight: bold; + font-size: 16px; } form#comment-listing { float: right; @@ -273,68 +581,242 @@ form#comment-listing { form#comment-listing label { padding-right: 5px; } -div.comment { +div.comment, div.message { background-color: #f7f7f8; border: 1px solid #bbbcbf; clear: both; margin: 0 0 12px; padding: 5px; } +div.comment { + margin: 10px 0px 0px 0px; + padding: 2px 0 2px 15px; +} +div.comment div.entry { + padding-right: 15px; +} +div.comment div.comment { + margin-right: -1px !important; +} div.comment div.comment, div.comment div.comment div.comment div.comment, div.comment div.comment div.comment div.comment div.comment div.comment, -div.comment div.comment div.comment div.comment div.comment div.comment div.comment div.comment { +div.comment div.comment div.comment div.comment div.comment div.comment div.comment div.comment, +div.message div.message, +div.message div.message div.message div.message, +div.message div.message div.message div.message div.message div.message, +div.message div.message div.message div.message div.message div.message div.message div.message { background-color: #fff; - margin: 10px 0 0; + margin: 10px 0 8px; } div.comment div.comment div.comment, div.comment div.comment div.comment div.comment div.comment, div.comment div.comment div.comment div.comment div.comment div.comment div.comment, -div.comment div.comment div.comment div.comment div.comment div.comment div.comment div.comment div.comment { +div.comment div.comment div.comment div.comment div.comment div.comment div.comment div.comment div.comment, +div.message div.message div.message, +div.message div.message div.message div.message div.message, +div.message div.message div.message div.message div.message div.message div.message, +div.message div.message div.message div.message div.message div.message div.message div.message div.message { background-color: #f7f7f8; } +.new-comment, +div.comment div.new-comment { + border: 5px solid #dcf8d9 !important; + /* padding: 10px 0 10px 10px !important; */ + padding: 0 0 0 10px !important; + margin-right: 0 !important; +} +div.retracted { + text-decoration: line-through; +} div.comment-meta { - padding: 0 5px; + } div.comment-meta span { - color: #538d4d; float: left; - margin-right: 30px; + font-size: 13px; + margin: 0 10px 0 0; } div.comment-meta span.comment-author a { color: #538d4d; + text-decoration: none; +} +div.comment-meta span.comment-date { + color: #999; + font-style: italic; + margin: 0 20px 0 0; +} +div.comment-meta span.votes { + color: #538d4d; +} +div.comment-meta a.expand { + float: right; +} +div.message .tagline { + padding: 0 5px; + margin: 0; +} +div.message .head.new { + color: orangered; +} +div.message .subject { + color: #538d4d; + padding: 0 5px; + margin: 0; +} +div.message-content { + clear: both; + padding: 9px 5px 0; } div.comment-content { clear: both; - padding: 12px 5px 0; + padding: 9px 0 0; } -div.comment-links { +div.comment-links, div.message-links { padding: 0 5px 5px; } -div.comment-links ul { +div.comment-links { + padding: 0 0 2px; + margin-top: -10px; +} +div.comment-links ul, div.message-links ul { list-style: none; margin: 0; } -div.comment-links ul li { +div.comment-links ul li, div.message-links ul li { border-right: 1px solid #bbbcbf; float: left; line-height: 1em; margin: 0 10px 0 0; padding: 0 10px 0 0; } -div.comment-links ul li:last-child { +div.comment-links ul li:last-child, div.message-links ul li:last-child { border-right: none; } +div.tools span.error, div.comment-links span.error { + clear: left; + display: block; + padding-top: 5px; + margin: 0; + line-height: 1em; +} +div.comment-links span.error { + padding-top: 10px; +} + +div.comment-links ul { + float: right; +} +div.comment-links ul.votes { + float: left; +} +div.comment-links ul li { + border-right: none; + margin: 0 0 0 5px; + padding: 0; +} +div.comment-links ul.votes li { + margin: 0 5px 0 0; +} +div.comment-links ul li a { + background-position: 0 0; + background-repeat: no-repeat; + cursor: pointer; + display: block; + float: left; + height: 0; + overflow: hidden; + padding: 20px 0 0; +} +div.comment-links ul li a:hover, div.comment-links ul li a.mod { + background-position: 0 -20px; +} +div.comment-links ul li.up a { + background-image: url(/static/vote-up.gif); + width: 14px; +} +div.comment-links ul li.down a { + background-image: url(/static/vote-down.gif); + width: 14px; +} +div.comment-links ul li.reply a { + background-image: url(/static/reply-button.png); + width: 24px; +} +div.comment-links ul li.permalink a { + background-image: url(/static/permalink-button.gif); + width: 25px; +} +div.comment-links ul li.save { + border-left: 1px solid #cbcbcb; + border-right: 1px solid #cbcbcb; + padding: 0 5px; +} +div.comment-links ul li.save a { + background-image: url(/static/save-button.gif); + width: 16px; +} +div.comment-links ul li.agree a { + background-image: url(/static/agree-button.gif); + width: 20px; +} +div.comment-links ul li.disagree a { + background-image: url(/static/disagree-button.gif); + width: 20px; +} +div.comment-links ul li.edit a { + background-image: url(/static/edit.png); + width: 20px; +} +div.comment-links ul li.retract a { + background-image: url(/static/retract.png); + width: 20px; +} +div.comment-links ul li.report a { + background-image: url(/static/report.png); + width: 20px; +} +div.comment-links ul li.parent a { + background-image: url(/static/parent.png); + width: 20px; +} +div.comment-links ul li.hide a { + background-image: url(/static/hide.png); + width: 20px; +} +/* Allow the 'yes/no' popup text to appear */ +div.comment-links ul li.delete a.yes, div.comment-links ul li.delete a.no, +div.comment-links ul li.retract a.yes, div.comment-links ul li.retract a.no { + background-image: none; + width: auto; + display: inline; + float: none; +} + +/* Temporary styling for text buttons until we have icons */ +div.comment-links ul li.delete a, div.comment-links ul li.unban a, +div.comment-links ul li.ban a, div.comment-links ul li.ignore a { + height: 20px; + padding-top: 4px; +} + /* Sidebar -------------------------------------------------------------------------------------- */ #sidebar { - background-color: #f7f7f8; + } #sidebar h2 { - color: #414143; + color: #58585a; font-size: 1em; + text-transform: uppercase; +} +#sidebar h2 a { + color: #58585a; +} +#sidebar h2 a:hover { + text-decoration: underline; } #sidebar h3 { font-size: 1em; @@ -342,12 +824,26 @@ div.comment-links ul li:last-child { #sidebar a { text-decoration: none; } +#sidebar a:hover { + text-decoration: underline; +} #sidebar ul { - margin-left: 15px; + list-style: none; + margin: 0; +} +#sidebar ul li { + margin: 0 0 10px; +} +#sidebar ul li:last-child { + margin: 0; +} +#sidebar div.spacer { + clear: both; } div.sidebox { - border-top: 2px solid #d6d5d6; - padding: 10px 15px; + background-color: #f5f5f5; + border-top: 1px solid #d6d5d6; + padding: 15px; } div.sidebox form { clear: both; @@ -374,60 +870,125 @@ div.sidebox select { #side-login { padding-bottom: 5px; } +#side-search { + background-color: transparent; + border: none; + margin: 0 0 15px; + padding: 0; +} +#side-search input.text { + background-position: 5px center !important; + border: 1px solid #e4e4e4 !important; + -webkit-box-shadow: inset 2px 2px 2px #dbdbdb; + -moz-box-shadow: inset 2px 2px 2px #dbdbdb; + box-shadow: inset 2px 2px 2px #dbdbdb; + font: 12px Arial, Helvetica, sans-serif; + -moz-border-radius: 0; + -webkit-border-radius: 0; + border-radius: 0; + padding: 6px 4px 6px 9px !important; + width: 205px; +} +#side-search input.submit { + display: none; +} #side-status { margin: 0 0 1em; } #side-status div.userinfo { float: left; - width: 98px; + clear: left; + width: 92px; +} +#side-status h2 { + float: left; + text-transform: none; } #side-status div.userinfo span { + color: #538d4d; + clear: both; float: left; display: block; } #side-status div.userinfo span.label { font-size: 0.9090em; /* 10px */ line-height: 1.1em; - width: 32px; -} -#side-status div.userinfo span.score { - background: url(/static/karma-circle.gif) no-repeat top left; + margin: 0 0 3px; +} +#side-status div.userinfo span.score, +#side-status div.userinfo span.monthly-score { + background-color: #538d4d; + -webkit-border-radius: 11px; + -moz-border-radius: 11px; + border-radius: 11px; + color: #fff; height: 22px; line-height: 22px; margin: 0 3px 0 0; + padding: 0 9px; font-weight: bold; text-align: center; - width: 22px; } +#side-status div.userinfo span.monthly-score { + background-color: #8fba75; + margin-top: 3px; +} + #side-status div.userinfo span.role { font-weight: bold; line-height: 22px; } -#side-status div.editor span { - color: #538d4d; -} -#side-status div.editor span.score { - background-position: 0 -22px; - color: #fff; +#side-status div.userinfo span.mail { + margin-top: 8px; + clear: both; } #side-status ul.userlinks { float: right; list-style: none; margin: 0; - width: 92px; + width: 98px; } #side-status ul.userlinks li { margin: 0 0 5px; } #side-status ul.userlinks li a { + background: #ffffff; /* Old browsers */ + background: -moz-linear-gradient(top, #ffffff 0%, #e9e7e7 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(100%,#e9e7e7)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #ffffff 0%,#e9e7e7 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #ffffff 0%,#e9e7e7 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #ffffff 0%,#e9e7e7 100%); /* IE10+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#e9e7e7',GradientType=0 ); /* IE6-9 */ + background: linear-gradient(top, #ffffff 0%,#e9e7e7 100%); /* W3C */ background-color: #fff; border: 1px solid #8a8a8b; - border-radius: 4px; /* CSS3 */ - font-size: 0.9090em; /* 10px */ - -moz-border-radius: 4px; /* Mozilla (FF) */ - -webkit-border-radius: 4px; /* Webkit (Safari,Chrome) */ + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; display: block; + font-size: 0.9090em; /* 10px */ + padding: 2px 0; text-align: center; + text-decoration: none; +} +#side-status dl.extrainfo { + margin: 0; + padding: 8px 0 0 0; + clear: both; +} +#side-status .extrainfo dt, #side-status .extrainfo dd { + margin: 0; + padding: 0; + float: left +} +#side-status .extrainfo dt { + color: #538D4D; + clear: left; + width: 50px; +} +#side-status .extrainfo dd { + overflow: hidden; + max-width: 190px; } #side-login label { width: 84px; @@ -447,3 +1008,275 @@ div.sidebox select { #side-tags li { display: inline; } + +ul#rightnav { + float: right; + margin: 0 0 13px; +} +ul#rightnav li { + border-right: 1px solid #c9c7c7; + float: left; + font-size: 12px; + line-height: 1; + margin: 0; + padding: 0 10px; + text-transform: uppercase; +} +ul#rightnav li:last-child { + border-right: none; + padding-right: 0; +} +ul#rightnav li a { + color: #999999; +} +ul#rightnav li a:hover { + text-decoration: none; +} + +#side-feed a { + font-size: 14px; +} +#side-feed a img { + height: 14px; + margin: 0 3px 0 0; + vertical-align: text-top; + width: 14px; +} + +#side-comments h3 { + font-size: 1em; + font-weight: bold; + height: 1.4em; + margin: 0; + max-width: 100%; + overflow: hidden; +} +#side-comments div.inline-comment { + display: block; + margin: 0 0 10px; +} +#side-comments div.inline-comment:last-child { + margin: 0; +} +#side-comments div.inline-comment span.tagline a { + color: #58585a; +} + +#side-posts div.reddit-link { + display: block; + margin: 0 0 10px; +} +#side-posts div.reddit-link:last-child { + margin: 0; +} +#side-posts div.reddit-link span a { + color: #58585a; +} + +#recent-wiki-edits ul li a { + font-weight: bold; +} +#recent-wiki-edits ul li span a { + color: #58585a; + font-weight: normal; +} + +#side-tags { + display: block; + line-height: 1.2; + text-align: justify; +} +#side-tags ul { + margin: 0; +} +#side-tags li.selected { + color: #414143; + font-weight: bold; +} +#side-tags .one { font-size: 0.9em; } +#side-tags .two { font-size: 0.95em; } +#side-tags .three { font-size: 1em; } +#side-tags .four { font-size: 1.125em; } +#side-tags .five { font-size: 1.25em; } +#side-tags .six { font-size: 1.375em; } +#side-tags .seven { font-size: 1.5em; } +#side-tags .eight { font-size: 1.625em; } +#side-tags .nine { font-size: 1.75em; } +#side-tags .ten { font-size: 1.75em; } + +#side-contributors div.contributors div.user { + margin: 0 0 0; +} +#side-contributors div.contributors div.user:last-child { + margin: 0; +} + + +/* Article Navigation +----------------------------------------------------------------------------------------------------*/ +div.articlenavigation { + clear: both; + font-size: 11px; +} +div.articlenavigation h4 { + font-size: 12px; + margin: 0; +} +div.articlenavigation h4 a { + background: url(/static/disclosure-triangle.gif) top left no-repeat; + padding-left: 18px; + text-decoration: none; +} +div.articlenavigation h4 a.open { + background-position: 0 -16px; +} +div.articlenavigation ul { + border-top: 1px solid #DEDEDE; + clear: both; + float: none; + margin: 5px 0 0; + padding: 10px 0 0; +} +div.articlenavigation ul li { + border: none; + clear: both; + display: block; + line-height: 15px; + margin: 1px 0; + padding: 3px 5px; + width: 650px; +} +div.articlenavigation ul li:hover { + background-color: #eee; +} +div.articlenavigation ul li a.nav { + float: left; + padding: 1px 0 0; + width: 13px; +} +div.articlenavigation ul li a.nav img { + display: block; + float: left; +} +div.articlenavigation ul li img { + display: block; + float: left; +} +div.articlenavigation ul li span.place { + float: left; + display: block; + text-align: center; + width: 70px; +} +div.articlenavigation ul li span.type { + float: left; + margin: 0 0 0 10px; + width: 509px; +} +div.articlenavigation #article_nav_controls .loading { + padding-left: 18px; +} +div.articlenavigation #article_nav_controls .loading img { + padding-right: 5px; +} + +/* Styling for embedded wiki pages */ +#wiki #jump-to-nav, #wiki #siteSub, +.wiki-comment-help div#jump-to-nav, .wiki-comment-help #siteSub, +#wiki h1#firstHeading, .wiki-comment-help h1, +#wiki #toc, +#wiki #catlinks, .wiki-comment-help div#catlinks { + display: none; +} +#wiki .printfooter, .wiki-comment-help .printfooter { + display: none; +} +#wiki h1 { + font-family: Palatino, Palladio, "URW Palladio L", "Book Antiqua", Baskerville, "Bookman Old Style", "Bitstream Charter", "Nimbus Roman No9 L", Garamond, "Apple Garamond", "ITC Garamond Narrow", "New Century Schoolbook", "Century Schoolbook", "Century Schoolbook L", Georgia, serif; + font-style: italic; + color: #538d4d; +} +#wiki .article-list ul { + list-style: none; + margin-left: 0; + font-size: 1.17em; + font-weight: bold; +} +#wiki .article-list li { + border-bottom: 1px solid #D6D5D6; + padding: 10px 0; +} +#wiki .article-list a { + text-decoration: none; +} +#wiki hr { + border: none; + border-bottom: 1px dotted #D6D5D6; +} +form.invalidate { + float: right; + font-size: 6pt; +} + +/* Profile Page +----------------------------------------------------------------------------------------------------*/ + +.profile_page ul#nav li { + margin: 0 14px 0 0; + padding: 0 14px 0 0; +} + +/* Footer +----------------------------------------------------------------------------------------------------*/ +div.footer { + background: #262626 url(/static/footer-bg.gif) repeat-x 0 0; + height: 55px; + padding: 15px 20px 20px 35px; +} +div.footer div.reddit { + color: #b5b5b5; + float: right; + font-size: 10px; +} +div.footer div.reddit img { + margin: 0 0 0 5px; + vertical-align: bottom; +} +div.footer ul.footer-links { + list-style: none; + margin: 10px 0 0; + padding: 0; +} +div.footer ul.footer-links li { + border-right: 1px solid #737373; + float: left; + line-height: 1; + margin-right: 20px; + padding-right: 20px; + text-transform: uppercase; +} +div.footer ul.footer-links li:last-child { + border-right: none; + margin-right: 0; + padding-right: 0; +} +div.footer ul.footer-links li a { + color: #b5b5b5; + font-weight: bold; + text-decoration: none; +} + +.commentreply .loading { + display: none; +} + +.commentreply.inprogress .loading { + display: inline; +} + +/* A hack to fix the layout of "Load more comments" */ +span.deepthread, +span.morecomments { + display: block; + margin: -9px 0 9px; +} diff --git a/r2/r2/public/static/main.js b/r2/r2/public/static/main.js new file mode 100644 index 00000000..142893d8 --- /dev/null +++ b/r2/r2/public/static/main.js @@ -0,0 +1,70 @@ +(function ($) { + +$(document).ready(function() { + + /* Dropdowns in main menu */ + dropdownSel = 'ul#nav li img.dropdown'; + $(dropdownSel).click(function(e) { + var ul = $(this).next('ul'); + var isVisible = $(ul).is(':visible'); + + /* Hide all dropdowns */ + $(dropdownSel).next('ul').hide(); + + /* If it wasn't visible initially, show it */ + if (!isVisible) + ul.show(); + + /* Register for any clicks to close the dropdown */ + $(document).one("click", function() { + $(ul).hide(); + }); + + return false; + }); + + // Post filter control + $('#post-filter div.filter-active').click(function() { + $(this).toggleClass('open'); + $(this).next('div.filter-options').toggle(); + return false; + }); + + // Comment filter control + $('#comment-controls div.filter-active').click(function() { + $(this).toggleClass('open'); + $(this).next('div.filter-options').toggle(); + return false; + }); + + function isiPhone() { + return ((navigator.platform.indexOf("iPhone") != -1) || + (navigator.platform.indexOf("iPod") != -1) || + (navigator.platform.indexOf("iPad") != -1)); + }; + + /* Don't do qtip tooltips with iphones (and related), it seems to interfer with the + normal onclick behaviour */ + if (!isiPhone()) { + // Button tooltips + $('div.tools div.vote a, div.tools div.boxright a.edit, div.tools div.boxright a.save, \ + div.boxright a.hide, div.comment-links ul li a, \ + .userinfo .score, .userinfo .monthly-score').qtip({ + position: { + my: 'bottom center', + at: 'top center' + }, + style: { + classes: 'ui-tooltip-lesswrong', + tip: { + border: 0, + corner: 'bottom center', + height: 7, /* If you adjust this, you must change qtip-tip-ie.gif to the same size */ + width: 10 /* If you adjust this, you must change qtip-tip-ie.gif to the same size */ + } + } + }); + } +}); + +})(jQuery); \ No newline at end of file diff --git a/r2/r2/public/static/maintenance.html b/r2/r2/public/static/maintenance.html index f4abf0f0..f61f5957 100644 --- a/r2/r2/public/static/maintenance.html +++ b/r2/r2/public/static/maintenance.html @@ -3,101 +3,43 @@ <html xmlns="http://www.w3.org/1999/xhtml" lang="en-us" xml:lang="en-us"> <head> <title> - LessWrong: Coming Soon + LessWrong: Back Soon -

- Coming Soon + Back Soon

- For more information see - Overcoming Bias. + Less Wrong is down for maintenance and will be back as soon as possible.

- +
diff --git a/r2/r2/public/static/map.js b/r2/r2/public/static/map.js new file mode 100644 index 00000000..f8d33e2a --- /dev/null +++ b/r2/r2/public/static/map.js @@ -0,0 +1,47 @@ +(function ($) { + /* Show Meetup map */ + window.createMap = function (map) { + if (map) { + loadMaps(function() { + var markers = $('.marker', map); + var myOptions = { + zoom: 12, + mapTypeId: google.maps.MapTypeId.ROADMAP + }; + var gMap = new google.maps.Map(map, myOptions); + + var first; + var bounds = new google.maps.LatLngBounds(); + + markers.each(function(i,m) { + var lat = $(m).attr('data-latitude'); + var lng = $(m).attr('data-longitude'); + latlng = new google.maps.LatLng(lat, lng); + bounds.extend(latlng); + if (!first) + first = latlng; + var marker = new google.maps.Marker({ + map: gMap, + draggable: false, + animation: google.maps.Animation.DROP, + position: latlng, + title: $(m).attr('data-title') + }); + var url = $(m).attr('data-url'); + if (url) { + google.maps.event.addListener(marker, 'click', function() { + window.location.href = url; + }); + } + }); + + /* Show all markers, and center on the first */ + if (bounds.getNorthEast().lat() != bounds.getSouthWest().lat() || + bounds.getNorthEast().lng() != bounds.getSouthWest().lng()) + gMap.fitBounds(bounds); + if (first) + gMap.setCenter(first); + }); + } + }; +})(jQuery); diff --git a/r2/r2/public/static/meetups.js b/r2/r2/public/static/meetups.js new file mode 100644 index 00000000..0c727303 --- /dev/null +++ b/r2/r2/public/static/meetups.js @@ -0,0 +1,73 @@ +(function() { + + document.observe("dom:loaded", function() { + createMap( $('map') ); + }); + + /* Add Meetup */ + function setTimeZone(date) { + $$('input[name="tzoffset"]').each(function(el) { + var tz = (new Date()).getTimezoneOffset() + el.setValue(tz / -60) + }); + } + + var statusIcons = { + "spinner": "/static/spinner.gif", + "ok": "/static/accept.png", + "error": "/static/exclamation.png" + }; + + function updateGeocodeStatus(status, message) { + var el = $('geocode_status'); + el.writeAttribute('src', statusIcons[status]); + el.show(); + + if (message) { + $('geocoded_location').update(message); + } + } + + function geocodeLocation() { + var el = this; + updateGeocodeStatus('spinner'); + + /* Geocode the address with Google */ + var geocoder = new google.maps.Geocoder(); + var request = { + address: el.getValue() + }; + geocoder.geocode(request, function(results, status) { + if (status == google.maps.GeocoderStatus.OK) { + var result = results.first(); + var location = result.geometry.location; + updateGeocodeStatus('ok', result.formatted_address); + $$('input[name="latitude"]').first().setValue(location.lat()); + $$('input[name="longitude"]').first().setValue(location.lng()); + } + else { + updateGeocodeStatus('error'); + } + }); + } + + document.observe("dom:loaded", function() { + var form = $('newmeetup'); + if (form) { + form.focusFirstElement(); + + loadMaps(function() { + $('location').observe('change', geocodeLocation); + }); + + /* Fill in the current time zone */ + setTimeZone(); + + Protoplasm.use('timepicker', function() { /* Used by datepicker below */ + Protoplasm.use('datepicker', function() { + var picker = new Control.DatePicker($$('input.date').first(), {epoch: true, timePicker: true}); + }); + }); + } + }); +})(); diff --git a/r2/r2/public/static/nav-dropdown.gif b/r2/r2/public/static/nav-dropdown.gif new file mode 100644 index 00000000..4844d2ac Binary files /dev/null and b/r2/r2/public/static/nav-dropdown.gif differ diff --git a/r2/r2/public/static/organic.js b/r2/r2/public/static/organic.js index baeb77f0..69b89215 100644 --- a/r2/r2/public/static/organic.js +++ b/r2/r2/public/static/organic.js @@ -66,7 +66,7 @@ function update_organic_pos(new_pos) { var c = readLCookie('reddit_first'); if(c != '') { try { - c = c.parseJSON(); + c = c.evalJSON(); } catch(e) { c = ''; } @@ -82,7 +82,7 @@ function update_organic_pos(new_pos) { c.organic_pos = ['none', new_pos]; } - createLCookie('reddit_first', c.toJSONString()); + createLCookie('reddit_first', Object.toJSON(c)); } OrganicListing.unhide = _fire_and_shift('unhide'); diff --git a/r2/r2/public/static/parent.png b/r2/r2/public/static/parent.png new file mode 100644 index 00000000..4d555cee Binary files /dev/null and b/r2/r2/public/static/parent.png differ diff --git a/r2/r2/public/static/permalink-button.gif b/r2/r2/public/static/permalink-button.gif new file mode 100644 index 00000000..4fc5c83b Binary files /dev/null and b/r2/r2/public/static/permalink-button.gif differ diff --git a/r2/r2/public/static/protoplasm/accordion/accordion.js b/r2/r2/public/static/protoplasm/accordion/accordion.js new file mode 100644 index 00000000..8328888d --- /dev/null +++ b/r2/r2/public/static/protoplasm/accordion/accordion.js @@ -0,0 +1 @@ +if(typeof Protoplasm=="undefined"){throw ("protoplasm.js not loaded, could not intitialize accordion")}if(window.Control==undefined){Control={}}Control.Accordion=Class.create({initialize:function(a,b){this.container=$(a);this.lastExpandedTab=null;this.accordionTabs=new Array();this.setOptions(b);this._attachBehaviors();for(var c=1;c0?(parseInt(this.e1.offsetHeight)-this.start)/this.steps:0;this.resizeBy(b);this.duration-=a;this.steps--;this.timer=setTimeout(this.accordionSize.bind(this),a)},isFinished:function(){return this.steps<=0},resizeBy:function(b){var d=this.e1.offsetHeight;var a=this.e2.offsetHeight;var c=parseInt(b);if(b!=0){this.e1.style.height=(d-c)+"px";this.e2.style.height=(a+c)+"px"}}});Protoplasm.register("accordion",Control.Accordion); \ No newline at end of file diff --git a/r2/r2/public/static/protoplasm/accordion/accordion_src.js b/r2/r2/public/static/protoplasm/accordion/accordion_src.js new file mode 100644 index 00000000..565af1f1 --- /dev/null +++ b/r2/r2/public/static/protoplasm/accordion/accordion_src.js @@ -0,0 +1,217 @@ +if (typeof Protoplasm == 'undefined') + throw('protoplasm.js not loaded, could not intitialize accordion'); +if (window.Control == undefined) Control = {}; + +/** + * class Control.Accordion + * + * Stacked title bars slide up and down to reveal different + * sections of content. +**/ +Control.Accordion = Class.create({ + + initialize: function(container, options) { + this.container = $(container); + this.lastExpandedTab = null; + this.accordionTabs = new Array(); + this.setOptions(options); + this._attachBehaviors(); + + // Set the initial visual state... + for (var i=1; i < this.accordionTabs.length; i++) { + this.accordionTabs[i].collapse(); + this.accordionTabs[i].content.style.display = 'none'; + } + + this.lastExpandedTab = this.accordionTabs[0]; + // TODO: Determine height from parent div? + this.lastExpandedTab.content.style.height = this.options.panelHeight + "px"; + this.lastExpandedTab.showExpanded(); + }, + + setOptions: function(options) { + this.options = Object.extend({ + panelHeight: 200, + onHideTab: null, + onShowTab: null + }, options || {}); + }, + + showTabByIndex: function(anIndex, animate) { + var doAnimate = arguments.length == 1 ? true: animate; + this.showTab(this.accordionTabs[anIndex], doAnimate); + }, + + showTab: function(accordionTab, animate, skipCallback) { + + var doAnimate = arguments.length == 1 ? true: animate; + + if (this.options.onHideTab) + this.options.onHideTab(this.lastExpandedTab); + + this.lastExpandedTab.showCollapsed(); + this.lastExpandedTab.content.style.height = (this.options.panelHeight - 1) + 'px'; + accordionTab.content.style.display = ''; + + var accordion = this; + var lastExpandedTab = this.lastExpandedTab; + + if (doAnimate) { + new Control.Accordion.Animation(this.lastExpandedTab.content, + accordionTab.content, + 1, + this.options.panelHeight, + 100, 10, + { complete: function() { accordion.showTabDone(lastExpandedTab, skipCallback); } } + ); + this.lastExpandedTab = accordionTab; + } else { + this.lastExpandedTab.content.style.height = "1px"; + accordionTab.content.style.height = this.options.panelHeight + "px"; + this.lastExpandedTab = accordionTab; + this.showTabDone(lastExpandedTab); + } + }, + + showTabDone: function(collapsedTab, skipCallback) { + collapsedTab.content.style.display = 'none'; + this.lastExpandedTab.showExpanded(); + if (!skipCallback && this.options.onShowTab) + this.options.onShowTab(this.lastExpandedTab); + }, + + _attachBehaviors: function() { + var panels = this._getDirectChildrenByTag(this.container, 'DIV'); + for (var i = 0; i < panels.length; i++) { + var tabChildren = this._getDirectChildrenByTag(panels[i],'DIV'); + if (tabChildren.length != 2) + continue; // unexpected + var tabTitleBar = tabChildren[0]; + var tabContentBox = tabChildren[1]; + this.accordionTabs.push(new Control.Accordion.Tab(this,tabTitleBar,tabContentBox)); + } + }, + + _getDirectChildrenByTag: function(e, tagName) { + var kids = new Array(); + var allKids = e.childNodes; + for(var i = 0; i < allKids.length; i++) + if (allKids[i] && allKids[i].tagName && allKids[i].tagName == tagName) + kids.push(allKids[i]); + return kids; + } + +}); + +Control.Accordion.Tab = Class.create({ + + initialize: function(accordion, titleBar, content) { + this.accordion = accordion; + this.titleBar = titleBar; + this.content = content; + this._attachBehaviors(); + }, + + collapse: function() { + this.showCollapsed(); + this.content.style.height = "1px"; + }, + + showCollapsed: function() { + this.expanded = false; + if (this.accordion.options.collapsedClass) + this.titleBar.className = this.accordion.options.collapsedClass + this.content.style.overflow = "hidden"; + }, + + showExpanded: function() { + this.expanded = true; + if (this.accordion.options.expandedClass) + this.titleBar.className = this.accordion.options.expandedClass; + this.content.style.overflow = "visible"; + }, + + titleBarClicked: function(e) { + if (this.accordion.lastExpandedTab != this) + this.accordion.showTab(this); + }, + + hover: function(e) { + if (!this.expanded && this.accordion.options.hoverClass) + this.titleBar.className = this.accordion.options.hoverClass; + }, + + unhover: function(e) { + if (this.expanded) { + if (this.accordion.options.expandedClass) + this.titleBar.className = this.accordion.options.expandedClass; + } else { + if (this.accordion.options.collapsedClass) + this.titleBar.className = this.accordion.options.collapsedClass; + } + }, + + _attachBehaviors: function() { + this.titleBar.onclick = this.titleBarClicked.bindAsEventListener(this); + this.titleBar.onmouseover = this.hover.bindAsEventListener(this); + this.titleBar.onmouseout = this.unhover.bindAsEventListener(this); + } +}); + +Control.Accordion.Animation = Class.create({ + + initialize: function(e1, e2, start, end, duration, steps, options) { + this.e1 = $(e1); + this.e2 = $(e2); + this.start = start; + this.end = end; + this.duration = duration; + this.steps = steps; + this.options = arguments[6] || {}; + + this.accordionSize(); + }, + + accordionSize: function() { + + if (this.isFinished()) { + // just in case there are round errors or such... + this.e1.style.height = this.start + "px"; + this.e2.style.height = this.end + "px"; + + if(this.options.complete) + this.options.complete(this); + return; + } + + if (this.timer) + clearTimeout(this.timer); + + var stepDuration = Math.round(this.duration/this.steps) ; + + var diff = this.steps > 0 ? (parseInt(this.e1.offsetHeight) - this.start)/this.steps : 0; + this.resizeBy(diff); + + this.duration -= stepDuration; + this.steps--; + + this.timer = setTimeout(this.accordionSize.bind(this), stepDuration); + }, + + isFinished: function() { + return this.steps <= 0; + }, + + resizeBy: function(diff) { + var h1Height = this.e1.offsetHeight; + var h2Height = this.e2.offsetHeight; + var intDiff = parseInt(diff); + if ( diff != 0 ) { + this.e1.style.height = (h1Height - intDiff) + "px"; + this.e2.style.height = (h2Height + intDiff) + "px"; + } + } + +}); + +Protoplasm.register('accordion', Control.Accordion); diff --git a/r2/r2/public/static/protoplasm/colorpicker/colorpicker.js b/r2/r2/public/static/protoplasm/colorpicker/colorpicker.js new file mode 100644 index 00000000..f91a65ee --- /dev/null +++ b/r2/r2/public/static/protoplasm/colorpicker/colorpicker.js @@ -0,0 +1 @@ +if(typeof Protoplasm=="undefined"){throw ("protoplasm.js not loaded, could not intitialize colorpicker")}if(typeof Control=="undefined"){Control={}}Control.ColorPicker=Class.create({initialize:function(f,c){f=$(f);if(cp=f.retrieve("colorpicker")){cp.destroy()}this.options=Object.extend({},c||{});this.panel=new Control.ColorPicker.Panel({onSelect:this.colorSelected.bind(this)});this.opened=false;this.dialog=new Element("div",{style:"position:absolute;"});var b=new Element("div",{"class":"_pp_frame_small "+this.options.className});b.appendChild(this.panel.element);this.dialog.appendChild(b);wrapper=f.wrap(new Element("div",{"class":"inline-block"}));wrapper.style.position="relative";wrapper.style.zIndex="99";var g=f.getLayout();var e=g.get("height")-2;var a=g.get("padding-top");if(a<1){a=1;e-=(a-g.get("padding-top"))*2}var d=g.get("padding-right");if(d<1){d=1}this.swatch=new Element("div",{"class":"inputExtension",title:"Open color palette",style:"border:1px solid gray;position:absolute;fontSize:1px;display:inline-block;width:"+e+"px;height:"+e+"px;background-color:"+f.value});f.insert({after:this.swatch});console.log(g.get("right"));this.swatch.style.top=(g.get("top")+a+g.get("border-top"))+"px";this.swatch.style.right=(g.get("right")-e-3+g.get("border-right"))+"px";this.oldPadding=f.style.paddingRight;f.style.paddingRight=(e+3+g.get("padding-left")+d)+"px";f.maxLength=7;this.listeners=[f.on("change",this.textChanged.bindAsEventListener(this)),f.on("blur",this.close.bindAsEventListener(this)),this.swatch.on("click",this.toggle.bindAsEventListener(this)),this.swatch.on("selectstart",Event.stop),Event.on(window,"unload",this.destroy.bind(this))];this.clickListener=null;f.store("colorpicker",this);f._show=f.show;f._hide=f.hide;this.element=Protoplasm.extend(f,{show:wrapper.show.bind(wrapper),hide:wrapper.hide.bind(wrapper),open:this.open.bind(this),toggle:this.toggle.bind(this),close:this.close.bind(this),destroy:this.destroy.bind(this)});this.wrapper=wrapper},destroy:function(){Protoplasm.revert(this.element);this.listeners.invoke("stop");if(this.clickListener){this.clickListener.stop()}var a=this.element;this.wrapper.parentNode.replaceChild(a,this.wrapper);a.style.paddingRight=this.oldPadding;a.store("colorpicker",null)},colorSelected:function(a){this.element.value=a;this.swatch.style.backgroundColor=a;this.close()},textChanged:function(a){this.swatch.style.backgroundColor=this.element.value},toggle:function(a){if(this.opened){this.close()}else{this.open()}},open:function(c){if(!this.opened){var b=this.element.getLayout();document.body.appendChild(this.dialog);var a=b.get("border-box-height")-b.get("border-bottom");this.dialog.clonePosition(this.element,{setWidth:false,setHeight:false,offsetTop:a,offsetLeft:-3});this.clickListener=document.on("click",this.clickHandler.bindAsEventListener(this));this.opened=true}},close:function(a){if(this.opened){if(this.clickListener){this.clickListener.stop()}this.dialog.remove();this.opened=false}},clickHandler:function(b){var a=Event.element(b);do{if(a==this.swatch||a==this.dialog){return}}while(a=a.parentNode);this.close()}});Control.ColorPicker.Panel=Class.create({initialize:function(a){this.options=Object.extend({addLabel:"Add",colors:Array("#000000","#993300","#333300","#003300","#003366","#000080","#333399","#333333","#800000","#FF6600","#808000","#008000","#008080","#0000FF","#666699","#808080","#FF0000","#FF9900","#99CC00","#339966","#33CCCC","#3366FF","#800080","#969696","#FF00FF","#FFCC00","#FFFF00","#00FF00","#00FFFF","#00CCFF","#993366","#C0C0C0","#FF99CC","#FFCC99","#FFFF99","#CCFFCC","#CCFFFF","#99CCFF","#CC99FF","#FFFFFF"),onSelect:Prototype.emptyFunction},a||{});this.customSwatches=[];this.activeCustom=null,this.element=this.create()},create:function(){var q=document.createElement("div");var a=this.options.colors;var p,n;var o=new Element("table",{cellPadding:0,cellSpacing:0,border:0});for(var f=0;f<5;++f){p=o.insertRow(f);for(var e=0;e<8;++e){n=p.insertCell(e);Element.setStyle(n,{border:"0px",padding:"0px"});var d=a[(8*f)+e];var m=new Element("div",{style:"width:15px;height:15px;font-size:1px;border:1px solid #EEEEEE;background-color:"+d+";padding:0"});m.on("click",this.clickListener(d));m.on("mouseover",this.hoverListener(d));n.appendChild(m)}}this.addSpacerRow(o,5);p=o.insertRow(6);var k=this.loadSetting("customColors")?this.loadSetting("customColors").split(","):new Array();this.customSwatches=[];for(var f=0;f<8;++f){n=p.insertCell(f);Element.setStyle(n,{border:"0",padding:"0"});var d=k[f]?k[f]:"#000000";var m=new Element("div",{style:"width:15px;height:15px;fontSize:15px;border:1px solid #EEEEEE;background-color:"+d+";padding:0"});n.appendChild(m);m.on("click",this.customClickListener(d,m));m.on("mouseover",this.hoverListener(d));this.customSwatches.push(m)}this.addSpacerRow(o,7);p=o.insertRow(8);n=p.insertCell(0);Element.setStyle(n,{border:"0",padding:"0"});n.colSpan=8;var b=new Element("table",{cellspacing:0,cellpadding:0,border:0,style:"width:136px;"});n.appendChild(b);p=b.insertRow(0);n=p.insertCell(0);Element.setStyle(n,{border:"0",padding:"0","vertical-align":"middle"});var g=new Element("div",{style:"width:15px;height:15px;fontSize:15px;border:1px solid #EEEEEE;background-color:#000000"});n.appendChild(g);this.previewSwatch=g;n=p.insertCell(1);Element.setStyle(n,{border:"0",padding:"0","vertical-align":"middle","text-align":"center"});var l=new Element("input",{type:"text",value:"#000000",style:"width:70px;border:1px solid gray"});l.on("keyup",function(i){this.previewSwatch.style.backgroundColor=l.value}.bindAsEventListener(this));n.appendChild(l);this.customInput=l;n=p.insertCell(2);Element.setStyle(n,{border:"0",padding:"0","vertical-align":"middle","text-align":"right"});var h=new Element("input",{type:"button",value:this.options.addLabel,style:"width:40px;border:1px solid gray"});h.on("click",function(s){var j=0;if(this.activeCustom){for(var r=0;rColor + * Picker demo +**/ +Control.ColorPicker = Class.create({ + +/** + * new Control.ColorPicker(element[, options]) + * - element (String | Element): A `` element (or DOM ID). + * - options (Hash): Additional options for the control. + * + * Create a new color picker from the given `` + * element. + * + * Additional options: + * + * * className: The CSS class to apply to the dialog panel. + * * colors: An array of 40 colors to display in the picker. + * * addLabel: The label for the "Add" button (for internationalization). +**/ + initialize: function (element, options) { + +/** + * Control.ColorPicker#element -> Element + * + * The underlying `` element passed to the constructor. +**/ + element = $(element); + + if (cp = element.retrieve('colorpicker')) + cp.destroy(); + + this.options = Object.extend({ + }, options || {}); + +/** + * Control.ColorPicker#panel -> Control.ColorPicker.Panel + * + * The panel dialog box linked to this control. This may be + * null if the control is not open. +**/ + this.panel = new Control.ColorPicker.Panel({ + onSelect: this.colorSelected.bind(this) + }); + + this.opened = false; + this.dialog = new Element('div', {'style': 'position:absolute;'}); + var cpCont = new Element('div', { 'class': '_pp_frame_small '+this.options.className }); + cpCont.appendChild(this.panel.element); + this.dialog.appendChild(cpCont); + + // Wrap element in a relative div to overlay the clickable swatch + wrapper = element.wrap(new Element('div', {'class': 'inline-block'})); + wrapper.style.position = 'relative'; + wrapper.style.zIndex = '99'; + + // Get layout information for the swatch position + var layout = element.getLayout(); + var size = layout.get('height') - 2; + var topPad = layout.get('padding-top'); + if (topPad < 1) { + topPad = 1; + size -= (topPad - layout.get('padding-top')) * 2; + } + var rightPad = layout.get('padding-right'); + if (rightPad < 1) rightPad = 1; + + // Create the color swatch + this.swatch = new Element('div', { + 'class': 'inputExtension', + 'title': 'Open color palette', + 'style': 'border:1px solid gray;position:absolute;fontSize:1px;display:inline-block;' + +'width:'+size+'px;height:'+size+'px;background-color:'+element.value }); + element.insert({after: this.swatch}); + + // Set the swatch position + //this.swatch.style.left = layout.get('left') + (layout.get('margin-box-width') + 1 + layout.get('border-left')) + 'px'; + console.log(layout.get('right')); + this.swatch.style.top = (layout.get('top') + topPad + layout.get('border-top')) + 'px'; + //this.swatch.style.left = (layout.get('left') + layout.get('margin-box-width')) + 'px'; + this.swatch.style.right = (layout.get('right') - size - 3 + layout.get('border-right')) + 'px'; + this.oldPadding = element.style.paddingRight; + element.style.paddingRight = (size + 3 + layout.get('padding-left') + rightPad) + 'px'; + element.maxLength = 7; + + this.listeners = [ + element.on('change', this.textChanged.bindAsEventListener(this)), + element.on('blur', this.close.bindAsEventListener(this)), + this.swatch.on('click', this.toggle.bindAsEventListener(this)), + this.swatch.on('selectstart', Event.stop), + Event.on(window, 'unload', this.destroy.bind(this)) + ]; + this.clickListener = null; + + element.store('colorpicker', this); + + element._show = element.show; + element._hide = element.hide; + + // Extend element with public API + this.element = Protoplasm.extend(element, { + show: wrapper.show.bind(wrapper), + hide: wrapper.hide.bind(wrapper), + open: this.open.bind(this), + toggle: this.toggle.bind(this), + close: this.close.bind(this), + destroy: this.destroy.bind(this) + }); + + this.wrapper = wrapper; + + }, + +/** + * Control.ColorPicker#destroy() -> null + * + * Destroy this control and return the underlying element to + * its original behavior. +**/ + destroy: function() { + Protoplasm.revert(this.element); + this.listeners.invoke('stop'); + if (this.clickListener) + this.clickListener.stop(); + var e = this.element; + this.wrapper.parentNode.replaceChild(e, this.wrapper); + e.style.paddingRight = this.oldPadding; + e.store('colorpicker', null); + }, + + colorSelected: function(color) { + this.element.value = color; + this.swatch.style.backgroundColor = color; + this.close(); + }, + + textChanged: function(e) { + this.swatch.style.backgroundColor = this.element.value; + }, + +/** + * Control.ColorPicker#toggle() -> null + * + * Toggle the visibility of the picker panel for this control. +**/ + toggle: function(e) { + if (this.opened) this.close(); + else this.open(); + }, + +/** + * Control.ColorPicker#open() -> null + * + * Show the picker panel for this control. +**/ + open: function(e) { + if (!this.opened) { + var layout = this.element.getLayout(); + document.body.appendChild(this.dialog); + var offsetTop = layout.get('border-box-height') - layout.get('border-bottom'); + this.dialog.clonePosition(this.element, { + 'setWidth': false, + 'setHeight': false, + 'offsetTop': offsetTop, + 'offsetLeft': -3}); + this.clickListener = document.on('click', + this.clickHandler.bindAsEventListener(this)); + this.opened = true; + } + }, + +/** + * Control.ColorPicker#close() -> null + * + * Hide the picker panel for this control. +**/ + close: function(e) { + if (this.opened) { + if (this.clickListener) + this.clickListener.stop(); + this.dialog.remove(); + this.opened = false; + } + }, + + clickHandler: function(e) { + var element = Event.element(e); + do { + if (element == this.swatch || element == this.dialog) + return; + } while (element = element.parentNode); + this.close(); + } +}); + +/** + * class Control.ColorPicker.Panel + * + * The dialog panel that is displayed when the color picker is opened. +**/ +Control.ColorPicker.Panel = Class.create({ + +/** + * new Control.ColorPicker.Panel([options]) + * - options (Hash): Additional options for the panel. + * + * Create a new color picker panel. + * + * Additional options: + * + * * colors: An array of 40 colors to display in the picker. + * * addLabel: The label for the "Add" button (for internationalization). +**/ + initialize: function(options) { + this.options = Object.extend({ + addLabel: 'Add', + colors: Array( + '#000000', '#993300', '#333300', '#003300', '#003366', '#000080', '#333399', '#333333', + '#800000', '#FF6600', '#808000', '#008000', '#008080', '#0000FF', '#666699', '#808080', + '#FF0000', '#FF9900', '#99CC00', '#339966', '#33CCCC', '#3366FF', '#800080', '#969696', + '#FF00FF', '#FFCC00', '#FFFF00', '#00FF00', '#00FFFF', '#00CCFF', '#993366', '#C0C0C0', + '#FF99CC', '#FFCC99', '#FFFF99', '#CCFFCC', '#CCFFFF', '#99CCFF', '#CC99FF', '#FFFFFF'), + onSelect: Prototype.emptyFunction + }, options || {}); + this.customSwatches = []; + this.activeCustom = null, + +/** + * Control.ColorPicker.Panel#element -> Element + * + * The root Element of this dialog panel. +**/ + this.element = this.create(); + }, + + create: function() { + var cont = document.createElement('div'); + var colors = this.options.colors; + var row, cell; + + // Create swatch table + var table = new Element('table', {'cellPadding': 0, 'cellSpacing': 0, 'border': 0}); + for (var i = 0; i < 5; ++i) { + row = table.insertRow(i); + for (var j = 0; j < 8; ++j) { + cell = row.insertCell(j); + Element.setStyle(cell, { 'border': '0px', 'padding': '0px' }); + var color = colors[(8 * i) + j]; + var swatch = new Element('div', {'style': 'width:15px;height:15px;font-size:1px;border:1px solid #EEEEEE;background-color:'+color+';padding:0'}); + swatch.on('click', this.clickListener(color)); + swatch.on('mouseover', this.hoverListener(color)); + cell.appendChild(swatch); + } + } + + this.addSpacerRow(table, 5); + + // Add custom color row + row = table.insertRow(6); + var customColors = this.loadSetting('customColors') + ? this.loadSetting('customColors').split(',') + : new Array(); + this.customSwatches = []; + for (var i = 0; i < 8; ++i) { + cell = row.insertCell(i); + Element.setStyle(cell, { 'border': '0', 'padding': '0' }); + var color = customColors[i] ? customColors[i] : '#000000'; + var swatch = new Element('div', {'style': 'width:15px;height:15px;fontSize:15px;border:1px solid #EEEEEE;background-color:'+color+';padding:0'}); + cell.appendChild(swatch); + swatch.on('click', this.customClickListener(color, swatch)); + swatch.on('mouseover', this.hoverListener(color)); + this.customSwatches.push(swatch); + } + + this.addSpacerRow(table, 7); + + // Add custom color entry interface + row = table.insertRow(8); + cell = row.insertCell(0); + Element.setStyle(cell, { 'border': '0', 'padding': '0'}); + cell.colSpan = 8; + var entryTable = new Element('table', {'cellspacing': 0, 'cellpadding': 0, 'border': 0, 'style': 'width:136px;'}); + cell.appendChild(entryTable); + + row = entryTable.insertRow(0); + cell = row.insertCell(0); + Element.setStyle(cell, { 'border': '0', 'padding': '0', 'vertical-align': 'middle'}); + var preview = new Element('div', {'style': 'width:15px;height:15px;fontSize:15px;border:1px solid #EEEEEE;background-color:#000000'}); + cell.appendChild(preview); + this.previewSwatch = preview; + + cell = row.insertCell(1); + Element.setStyle(cell, {'border': '0', 'padding': '0', 'vertical-align': 'middle', 'text-align': 'center'}); + var textbox = new Element('input', {'type': 'text', 'value': '#000000', 'style': 'width:70px;border:1px solid gray' }); + textbox.on('keyup', function(e) { + this.previewSwatch.style.backgroundColor = textbox.value; + }.bindAsEventListener(this)); + cell.appendChild(textbox); + this.customInput = textbox; + + cell = row.insertCell(2); + Element.setStyle(cell, { 'border': '0', 'padding': '0', 'vertical-align': 'middle', 'text-align': 'right'}); + var submit = new Element('input', {'type': 'button', 'value': this.options.addLabel, + 'style': 'width:40px;border:1px solid gray'}); + submit.on('click', function(e) { + var idx = 0; + if (this.activeCustom) { + for (var i = 0; i < this.customSwatches.length; ++i) + if (this.customSwatches[i] == this.activeCustom) { + idx = i; + break; + } + this.activeCustom.style.border = '1px solid #EEEEEE'; + this.activeCustom = null; + } else { + var lastIndex = this.loadSetting('customColorIndex'); + if (lastIndex) idx = (parseInt(lastIndex) + 1) % 8; + } + this.saveSetting('customColorIndex', idx); + customColors[idx] = this.customSwatches[idx].style.backgroundColor = this.customInput.value; + this.customSwatches[idx].onclick = this.customClickListener(customColors[idx], this.customSwatches[idx]); + this.customSwatches[idx].onmouseover = this.hoverListener(customColors[idx]); + this.saveSetting('customColors', customColors.join(',')); + }.bindAsEventListener(this)); + cell.appendChild(submit); + + // Create form + var form = new Element('form', {'style': 'margin:0;padding:0'}); + form.onsubmit = function() { + if (this.activeCustom) this.activeCustom.style.border = '1px solid #EEEEEE'; + this.activeCustom = null; + this.editor.setDialogColor(this.customInput.value); + return false; + }.bindAsEventListener(this); + form.appendChild(table); + + // Add to dialog window + cont.appendChild(form); + return cont; + }, + + addSpacerRow: function(table, idx) { + var row = table.insertRow(idx); + cell = row.insertCell(0); + cell.colSpan = 8; + Element.setStyle(cell, {'border': '0', 'padding': '0'}); + cell.appendChild(new Element('hr', {'style': 'color:gray;background-color:gray;' + +'height:1px;border:0;margin-top:3px;margin-bottom:3px;padding:0'})); + return row; + }, + + clickListener: function(color) { + return function(e) { + if (this.activeCustom) this.activeCustom.style.border = '1px solid #EEEEEE'; + this.activeCustom = null; + this.options.onSelect(color); + }.bindAsEventListener(this); + }, + + customClickListener: function(color, element) { + return function(e) { + if (e.ctrlKey) { + if (this.activeCustom) + this.activeCustom.style.border = '1px solid #EEEEEE'; + this.activeCustom = element; + this.activeCustom.style.border = '1px solid #FF0000'; + } else { + this.activeCustom = null; + this.options.onSelect(color); + } + }.bindAsEventListener(this); + }, + + hoverListener: function(color) { + return function(e) { + this.previewSwatch.style.backgroundColor = color; + this.customInput.value = color; + }.bindAsEventListener(this); + }, + + loadSetting: function(name) { + name = 'colorpicker_' + name; + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for(var i=0;i < ca.length;i++) { + var c = ca[i]; + while (c.charAt(0)==' ') c = c.substring(1,c.length); + if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length); + } + return null; + }, + + saveSetting: function(name, value, days) { + name = 'colorpicker_' + name; + if (!days) days = 180; + var date = new Date(); + date.setTime(date.getTime()+(days*24*60*60*1000)); + var expires = "; expires="+date.toGMTString(); + document.cookie = name+"="+value+expires+"; path=/"; + }, + + clearSetting: function(name) { + this.saveSetting(name, "", -1); + } + +}); + +Protoplasm.register('colorpicker', Control.ColorPicker); diff --git a/r2/r2/public/static/protoplasm/datepicker/calendar.png b/r2/r2/public/static/protoplasm/datepicker/calendar.png new file mode 100644 index 00000000..988116fe Binary files /dev/null and b/r2/r2/public/static/protoplasm/datepicker/calendar.png differ diff --git a/r2/r2/public/static/protoplasm/datepicker/datepicker.css b/r2/r2/public/static/protoplasm/datepicker/datepicker.css new file mode 100644 index 00000000..3d4dce2f --- /dev/null +++ b/r2/r2/public/static/protoplasm/datepicker/datepicker.css @@ -0,0 +1,103 @@ +/** + * Styles for DatePicker + */ + +._pp_datepicker { + line-height: 15px; + display: inline-block; +} + +._pp_datepicker_navigation { + text-align: center; + width: 180px; + margin: auto; +} +._pp_datepicker_today:hover, +._pp_datepicker_next:hover, +._pp_datepicker_previous:hover { + text-decoration: underline; + cursor: pointer; +} +._pp_datepicker_next { + float: right; + width: 25px; +} +._pp_datepicker_previous { + float: left; + width: 25px; +} + +._pp_datepicker_table { + border-collapse: collapse; + width: 180px; +} +._pp_datepicker_table th, +._pp_datepicker_table td { + text-align: center; + padding: 1px; +} + +._pp_datepicker td.day, +._pp_datepicker td.dayothermonth { + cursor: pointer; + background-color: #FFFFFF; + border: 1px solid #EEEEEE; + width: 2em; +} + +._pp_datepicker td.dayothermonth { + color: #999999; + font-style: italic; +} + +._pp_datepicker td.day:hover { + background-color: #EBE4C0; +} + +._pp_datepicker td.weekend { + background-color: #CCCCCC; + font-style: italic; +} + +._pp_datepicker td.today { + font-weight: bold; +} + +._pp_datepicker td.current, +._pp_datepicker td.current:hover { + font-weight: bold; + background-color: #EBC2C0; +} +._pp_datepicker td.rightrange { + background: url(right-range.png) #EBC2C0 right center no-repeat; +} +._pp_datepicker td.leftrange { + background: url(left-range.png) #EBC2C0 left center no-repeat; +} +._pp_datepicker td.singlerange { + background: url(single-range.png) #EBC2C0 center center no-repeat; +} +._pp_datepicker_weekselect { + width: 3px; + background-color: #999; + cursor: pointer; + border-radius: 5px 0 0 5px; + -moz-border-radius: 5px 0 0 5px; + -webkit-border-radius: 5px 0 0 5px; +} + +._pp_datepicker button { + display: block; + width: 174px; + margin: 3px auto; + font-size: 11px; +} +._pp_datepicker td.currenthint { + background-color: #f2e0df; +} +._pp_datepicker td.disabled, +._pp_datepicker td.disabled:hover { + background-color: #EEE; + color: #999999; + cursor: default; +} diff --git a/r2/r2/public/static/protoplasm/datepicker/datepicker.js b/r2/r2/public/static/protoplasm/datepicker/datepicker.js new file mode 100644 index 00000000..dce952d2 --- /dev/null +++ b/r2/r2/public/static/protoplasm/datepicker/datepicker.js @@ -0,0 +1,4 @@ +// This file was adapted to work properly with IE7/IE8. In IE you cannot change the type of an input +// via javascript, which is done when using datepicker with epochtime. The code was removed and instead +// the display style of the input element was set to none. +if(typeof Protoplasm=="undefined"){throw ("protoplasm.js not loaded, could not intitialize datepicker")}if(typeof Control=="undefined"){Control={}}Protoplasm.use("timepicker");Protoplasm.loadStylesheet("datepicker.css","datepicker");Control.DatePicker=Class.create({initialize:function(element,options){element=$(element);if(dp=element.retrieve("datepicker")){dp.destroy()}var wrapper=element.wrap(new Element("span"));wrapper.style.position="relative";options=Object.extend({timePicker:false,manual:true},options||{});if(!options.icon){options.icon=Protoplasm.base("datepicker")+"calendar.png"}this.element=element;this.anchor=element;this.wrapper=wrapper;this.panel=null;this.dialog=null;this.handlers={onClick:options.onClick,onHover:options.onHover,onSelect:options.onSelect};this.options=Object.extend(options||{},{onClick:this.pickerClicked.bind(this),onHover:this.dateHover.bind(this),onSelect:this.datePicked.bind(this)});var locale=options&&options.locale?options.locale:"en_US";try{this.setLocale(new Control.DatePicker.i18n(locale))}catch(e){new Ajax.Request(Protoplasm.base("datepicker")+"locales/"+locale+".js",{onSuccess:function(transport){eval(transport.responseText);this.setLocale(new Control.DatePicker.i18n(locale),true)}.bind(this),onFailure:function(transport){this.setLocale(new Control.DatePicker.i18n("en_US"),true)}.bind(this)})}this.listeners=[document.on("keydown",this.docKeyHandler.bindAsEventListener(this)),Event.on(window,"unload",this.destroy.bind(this))];this.originalRange={start:null,end:null};if(options.range){this.rangeEnd=options.rangeEnd?$(options.rangeEnd):wrapper.next("input[type=text]")}if(options.icon){element.style.background="url("+options.icon+") right center no-repeat #FFF";this.oldPadding=element.style.paddingRight;element.style.paddingRight="20px";if(this.rangeEnd){this.rangeEnd.style.background="url("+options.icon+") right center no-repeat #FFF";this.rangeEnd.style.paddingRight="20px"}}if(options.epoch){this.startLabel=this.makeEpochLabel(element);this.addListener(this.startLabel);this.anchor=this.startLabel;if(this.rangeEnd){this.endLabel=this.makeEpochLabel(this.rangeEnd);this.addListener(this.endLabel)}}else{this.addListener(element);if(this.rangeEnd){this.addListener(this.rangeEnd)}element.readOnly=!this.options.manual}this.hidePickerListener=null;this.pickerActive=false;this.element.store("datepicker",this);this.element=Protoplasm.extend(element,{show:wrapper.show.bind(wrapper),hide:wrapper.hide.bind(wrapper),open:this.open.bind(this),toggle:this.toggle.bind(this),close:this.close.bind(this),destroy:this.destroy.bind(this)})},makeEpochLabel:function(b){var a=b.clone();a.name=null;a.on("change",function(){var c=Control.DatePicker.DateFormat.parseFormat(a.value,this.options.currentFormat);if(c){b.value=c.getTime()}});a.id=null;a.readOnly=!this.options.manual;b.style.display="none";if(b.value){a.value=this.options.currentFormat?Control.DatePicker.DateFormat.format(new Date(parseInt(b.value)),this.options.currentFormat):""}b.insert({after:a});return a},addListener:function(a){this.listeners.push(a.on("click",this.toggle.bindAsEventListener(this)));this.listeners.push(a.on("keydown",this.keyHandler.bindAsEventListener(this)))},setLocale:function(a,c){this.i18n=a;this.options=this.i18n.inheritOptions(this.options);if(!this.options.range&&this.options.timePicker){this.options.currentFormat=this.options.dateTimeFormat}else{this.options.currentFormat=this.options.dateFormat}this.options.date=Control.DatePicker.DateFormat.parseFormat(this.element.value,this.options.currentFormat);if(this.options.range&&this.rangeEnd){this.options.endDate=Control.DatePicker.DateFormat.parseFormat(this.rangeEnd.value,this.options.currentFormat)}if(c){var b=this.getValue();this.setValue(b.start,b.end)}},destroy:function(){Protoplasm.revert(this.element);this.listeners.invoke("stop");if(this.hidePickerListener){this.hidePickerListener.stop()}this.wrapper.parentNode.replaceChild(this.element,this.wrapper);this.element.style.paddingRight=this.oldPadding;this.element.store("datepicker",null)},tr:function(a){return this.i18n.tr(a)},clickHandler:function(b){var a=Event.element(b);do{if(a==document){break}if(a==this.element||a==this.startValue||a==this.endValue||a==this.dialog){return}}while(a=a.parentNode);if(!a){return}this.close()},pickerClicked:function(){if(this.handlers.onClick){this.handlers.onClick()}},datePicked:function(b,a){this.setValue(b,a);this.element.focus();this.close();if(this.handlers.onSelect){this.handlers.onSelect(b,a)}if(this.element.onchange){this.element.onchange()}},dateHover:function(b,a){if(this.pickerActive){this.setValue(b,a);if(this.handlers.onHover){this.handlers.onHover(b,a)}}},toggle:function(a){if(this.pickerActive){this.setValue(this.originalRange.start,this.originalRange.end);this.close()}else{setTimeout(this.open.bind(this))}return false},setValue:function(b,a){startLabel=b?Control.DatePicker.DateFormat.format(b,this.options.currentFormat):null;startValue=b&&this.options.epoch?b.getTime():startLabel;endLabel=a?Control.DatePicker.DateFormat.format(a,this.options.currentFormat):null;endValue=a&&this.options.epoch?a.getTime():endLabel;this.element.value=startValue?startValue:"";if(this.options.epoch){this.startLabel.value=startLabel?startLabel:""}if(this.rangeEnd){this.rangeEnd.value=endValue?endValue:"";if(this.options.epoch){this.endLabel.value=endLabel?endLabel:""}}},getValue:function(){var a={start:null,end:null};if(this.element.value){a.start=this.options.epoch?new Date(parseInt(this.element.value)):Control.DatePicker.DateFormat.parseFormat(this.element.value,this.options.currentFormat)}if(this.rangeEnd&&this.rangeEnd.value){a.end=this.options.epoch?new Date(parseInt(this.rangeEnd.value)):Control.DatePicker.DateFormat.parseFormat(this.rangeEnd.value,this.options.currentFormat)}return a},docKeyHandler:function(a){if(a.keyCode==Event.KEY_ESC){if(this.pickerActive){this.setValue(this.originalRange.start,this.originalRange.end);this.close()}}},keyHandler:function(a){switch(a.keyCode){case Event.KEY_ESC:if(this.pickerActive){this.setValue(this.originalRange.start,this.originalRange.end)}case Event.KEY_TAB:this.close();return;case Event.KEY_DOWN:if(!this.pickerActive){this.open();Event.stop(a)}}if(this.pickerActive){return false}},close:function(){if(this.pickerActive&&!this.element.disabled){this.panel.releaseKeys();this.dialog.remove();if(this.hidePickerListener){this.hidePickerListener.stop();this.hidePickerListener=null}this.pickerActive=false;Control.DatePicker.activePicker=null}},open:function(){if(!this.pickerActive){if(Control.DatePicker.activePicker){Control.DatePicker.activePicker.close()}this.anchor.focus();if(!this.dialog){this.panel=new Control.DatePicker.Panel(this.options);this.dialog=new Element("div",{"class":"_pp_frame_small _pp_datepicker "+this.options.className,style:"position:absolute;"});this.dialog.appendChild(this.panel.element)}this.originalRange=this.getValue();var b=this.anchor.getLayout();var a=b.get("border-box-height")-b.get("border-bottom");document.body.appendChild(this.dialog);this.anchor.style.position="relative";this.dialog.clonePosition(this.anchor,{setWidth:false,setHeight:false,offsetTop:a,offsetLeft:-3});this.dialog.style.zIndex="99";this.panel.selectRange(this.originalRange.start,this.originalRange.end);this.panel.captureKeys();this.hidePickerListener=document.on("click",this.clickHandler.bindAsEventListener(this));this.pickerActive=true;Control.DatePicker.activePicker=this;this.pickerClicked()}}});Control.DatePicker.activePicker=null;Control.DatePicker.create=function(b){b=Object.extend({className:"datepicker",name:"date"},b||{});var a=new Element("input",{"class":b.className,name:name});var c=new Control.DatePicker(a);c.wrapper.store("datepicker",c);return c.wrapper};Control.DatePicker.i18n=Class.create();Object.extend(Control.DatePicker.i18n,{available:["cs_CZ","el_GR","fr_FR","it_IT","lt_LT","nl_NL","pl_PL","pt_BR","ru_RU"],baseLocales:{us:{dateTimeFormat:"MM-dd-yyyy HH:mm:ss",dateFormat:"MM-dd-yyyy",firstWeekDay:0,weekend:[0,6],timeFormat:"HH:mm:ss"},eu:{dateTimeFormat:"dd-MM-yyyy HH:mm:ss",dateFormat:"dd-MM-yyyy",firstWeekDay:1,weekend:[0,6],timeFormat:"HH:mm:ss"},iso8601:{dateTimeFormat:"yyyy-MM-dd HH:mm:ss",dateFormat:"yyyy-MM-dd",firstWeekDay:1,weekend:[0,6],timeFormat:"HH:mm:ss"}},createLocale:function(a,b){return Object.extend(Object.clone(Control.DatePicker.i18n.baseLocales[a]),{language:b})}});Control.DatePicker.i18n.prototype={initialize:function(a){if(a){this.setLocale(a)}},setLocale:function(b){if(!(b in Control.DatePicker.Locale)&&Control.DatePicker.i18n.available.indexOf(b)>-1){throw ("Locale available but not loaded")}var c=b.charAt(2)=="_"?b.substring(0,2):b;var a=(Control.DatePicker.Locale[b]||Control.DatePicker.Locale[c]);this.opts=Object.clone(a||{});var d=a?Control.DatePicker.Language[a.language]:null;if(d){Object.extend(this.opts,d)}},opts:null,inheritOptions:function(a){if(!this.opts){this.setLocale("en_US")}return Object.extend(this.opts,a||{})},tr:function(a){return this.opts&&this.opts.strings?this.opts.strings[a]||a:a}};Control.DatePicker.Locale={};with(Control.DatePicker){Locale.es=i18n.createLocale("eu","es");Locale.en=i18n.createLocale("us","en");Locale.en_GB=i18n.createLocale("eu","en");Locale.en_AU=Locale.en_GB;Locale.de=i18n.createLocale("eu","de");Locale.es_iso8601=i18n.createLocale("iso8601","es");Locale.en_iso8601=i18n.createLocale("iso8601","en");Locale.de_iso8601=i18n.createLocale("iso8601","de")}Control.DatePicker.Language={es:{months:["Enero","Febrero","Marzo","Abril","Mayo","Junio","Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre"],days:["Do","Lu","Ma","Mi","Ju","Vi","Sa"],strings:{Now:"Ahora",Today:"Hoy",Time:"Hora","Exact minutes":"Minuto exacto","Select Date and Time":"Selecciona Dia y Hora","Select Time":"Selecciona Hora","Open calendar":"Abre calendario"}},de:{months:["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],days:["So","Mo","Di","Mi","Do","Fr","Sa"],strings:{Now:"Jetzt",Today:"Heute",Time:"Zeit","Exact minutes":"Exakte minuten","Select Date and Time":"Zeit und Datum Auswählen","Select Time":"Zeit Auswählen","Open calendar":"Kalender öffnen"}}};Control.DatePicker.Panel=Class.create(function(){function pad(x){if(x<10){return"0"+x}return new String(x)}return{initialize:function(options){try{this.i18n=new Control.DatePicker.i18n(options&&options.locale?options.locale:"en_US");options=this.i18n.inheritOptions(options)}catch(e){this.i18n=new Control.DatePicker.i18n()}this.options=Object.extend({className:"",closeOnToday:true,selectToday:true,timePicker:false,use24hrs:false,firstWeekDay:0,weekend:[0,6],months:["January","February","March","April","May","June","July","August","September","October","November","December"],days:["Su","Mo","Tu","We","Th","Fr","Sa"]},options||{});with(this.options){if(isNaN(firstWeekDay*1)){firstWeekDay=0}else{firstWeekDay=firstWeekDay%7}}this.calendarCont=null;this.currentDate=this.options.date?this.options.date:new Date();this.dayOfWeek=0;this.minInterval=5;this.rangeStart=this.currentDate;this.rangeEnd=this.options.dateEnd;this.inRange=false;this.position=this.options.position;this.selectedDays=[];this.currentDays={};this.visibleDays={};this.invisibleDays={};this.element=this.createPicker();this.selectDate(this.currentDate);this.element.on("selectstart",function(e){Event.stop(e)}.bindAsEventListener(this))},createPicker:function(){var elt=new Element("div",{"class":"_pp_datepicker "+this.options.className});this.calendarCont=this.drawCalendar(elt,this.currentDate);Event.observe(elt,"click",this.clickHandler.bindAsEventListener(this));this.keyListener=document.on("keydown",this.keyHandler.bindAsEventListener(this));document.on("click",function(e){if(!e.findElement("._pp_datepicker")){this.keyListener.stop()}}.bindAsEventListener(this));if(!this.options.captureKeys){this.keyListener.stop()}return elt},tr:function(str){return this.i18n.tr(str)},captureKeys:function(){this.keyListener.start()},releaseKeys:function(){this.keyListener.stop()},setDate:function(date,recenter){if(date){this.element.update();var dateKey=this.dateKey(date);if(!(dateKey in this.visibleDays)){if(recenter){this.position=this.options.position}else{if(datethis.currentDate){this.position="right"}else{this.position=this.options.position}}}}this.calendarCont=this.drawCalendar(this.element,date)}},drawCalendar:function(container,date){var calendar=this.createCalendar(date);container.appendChild(calendar);container.style.width=calendar.style.width;if(!this.options.range&&this.options.timePicker){var selectListener=function(e){if(this.options.onSelect){this.options.onSelect(this.currentDate)}}.bindAsEventListener(this);var tp=new Control.TimePicker.Panel({onChange:this.selectTime.bind(this),onSelect:selectListener,use24hrs:this.options.use24hrs});var timewrap=new Element("table",{style:"margin: 3px auto;"});var row=timewrap.insertRow(0);var cell=$(row.insertCell(0));cell.appendChild(tp.element);container.appendChild(timewrap);tp.setTime(this.currentDate);this.timePicker=tp;var select=new Element("button").update(this.tr("Select Date and Time"));select.on("click",selectListener);container.appendChild(select)}return container},createCalendar:function(date){this.currentDays={};this.visibleDays={};this.invisibleDays={};var wrapper=new Element("div");var header=this.createHeader(date);wrapper.appendChild(header);if((months=this.options.monthCount)&&months>1){if(months>3){months=3}wrapper.style.width=(180*months+(months-1)*3)+"px";var vertical=(this.options.layout=="vertical");var row;if(!vertical){var table=new Element("table",{cellPadding:0,cellSpacing:0,border:0});row=table.insertRow(0);wrapper.appendChild(table)}var start=new Date(date.getTime());start.setDate(1);if(this.position&&this.position=="left"){}else{if(this.position&&this.position=="right"){start.setMonth(start.getMonth()-(months-1))}else{start.setMonth(start.getMonth()-Math.floor(months/2))}}for(var i=0;i0){cal.style.marginLeft="3px"}}else{if(i>0){cal.style.marginTop="3px"}}if(!vertical){var cell=$(row.insertCell(row.cells.length));cell.appendChild(cal)}else{wrapper.appendChild(cal)}start.setMonth(start.getMonth()+1)}}else{wrapper.style.width="180px";wrapper.appendChild(this.createMonth(date))}return wrapper},createHeader:function(date){var today=new Date();var previousYear=new Date(date.getFullYear()-1,date.getMonth(),1);var previousMonth=new Date(date.getFullYear(),date.getMonth()-1,1);var nextMonth=new Date(date.getFullYear(),date.getMonth()+1,1);var nextYear=new Date(date.getFullYear()+1,date.getMonth(),1);var nav=new Element("div",{"class":"_pp_datepicker_navigation"});var link=new Element("span",{"class":"_pp_datepicker_previous",title:this.monthName(previousYear.getMonth())+" "+previousYear.getFullYear()}).update("<<");link.on("click",this.movePreviousYearListener());nav.insert(link);link=new Element("span",{"class":"_pp_datepicker_previous",title:this.monthName(previousMonth.getMonth())+" "+previousMonth.getFullYear()}).update("<");link.on("click",this.movePreviousMonthListener());nav.insert(link);link=new Element("span",{"class":"_pp_datepicker_next",title:this.monthName(nextYear.getMonth())+" "+nextYear.getFullYear()}).update(">>");link.on("click",this.moveNextYearListener());nav.insert(link);link=new Element("span",{"class":"_pp_datepicker_next",title:this.monthName(nextMonth.getMonth())+" "+nextMonth.getFullYear()}).update(">");link.on("click",this.moveNextMonthListener());nav.insert(link);link=new Element("div",{"class":"_pp_datepicker_today",title:today.getDate()+" "+this.monthName(today.getMonth())+" "+today.getFullYear()}).update(this.options.timePicker?this.tr("Now"):this.tr("Today"));link.on("click",this.clickedListener(today,true));nav.insert(link);return nav},createMonth:function(date){var table=new Element("table",{cellSpacing:0,cellPadding:0,border:0,"class":"_pp_datepicker_table"});var row=$(table.insertRow(table.rows.length));if(this.options.range){row.insertCell(0)}cell=$(row.insertCell(row.cells.length));cell.className="_pp_title";cell.colSpan=7;cell.update(this.monthName(date.getMonth())+" "+date.getFullYear());row=$(table.insertRow(table.rows.length));if(this.options.range){row.insertCell(0)}for(var i=0;i<7;++i){cell=new Element("th",{width:"14%","class":"_pp_highlight"}).update(this.dayName((this.options.firstWeekDay+i)%7));row.insert(cell)}var workDate=new Date(date.getFullYear(),date.getMonth(),1);var day=workDate.getDay();if(day!=this.options.firstWeekDay){row=$(table.insertRow(table.rows.length));workDate.setDate(workDate.getDate()-((day-this.options.firstWeekDay+7)%7));if(this.options.range){cell=$(row.insertCell(row.cells.length));cell.className="_pp_datepicker_weekselect";cell.on("mousedown",this.weekClicked(workDate))}day=workDate.getDay();while(workDate.getMonth()!=date.getMonth()){cell=new Element("td").update(workDate.getDate());this.assignDayClasses(cell,"dayothermonth",workDate);cell.on("mousedown",this.rangeStartListener(workDate));cell.on("mouseover",this.hoverListener(workDate));cell.on("mouseup",this.rangeEndListener(workDate));var dateKey=this.dateKey(workDate);this.invisibleDays[dateKey]=cell;row.insert(cell);workDate.setDate(workDate.getDate()+1);day=workDate.getDay()}}while(workDate.getMonth()==date.getMonth()){if(day==this.options.firstWeekDay){row=$(table.insertRow(table.rows.length));if(this.options.range){if(this.options.range){cell=new Element("td",{"class":"_pp_datepicker_weekselect"});cell.on("mousedown",this.weekClicked(workDate));row.insert(cell)}}}cell=new Element("td").update(workDate.getDate());this.assignDayClasses(cell,"day",workDate);row.insert(cell);cell.on("mousedown",this.rangeStartListener(workDate));cell.on("mouseover",this.hoverListener(workDate));cell.on("mouseup",this.rangeEndListener(workDate));var dateKey=this.dateKey(workDate);this.visibleDays[dateKey]=cell;if(workDate.getFullYear()==this.currentDate.getFullYear()&&workDate.getMonth()==this.currentDate.getMonth()){this.currentDays[dateKey]=cell}workDate.setDate(workDate.getDate()+1);day=workDate.getDay()}if(day!=this.options.firstWeekDay){do{cell=new Element("td").update(workDate.getDate());this.assignDayClasses(cell,"dayothermonth",workDate);row.insert(cell);var thisDate=new Date(workDate.getTime());cell.on("mousedown",this.rangeStartListener(workDate));cell.on("mouseover",this.hoverListener(workDate));cell.on("mouseup",this.rangeEndListener(workDate));var dateKey=this.dateKey(workDate);this.invisibleDays[dateKey]=cell;workDate.setDate(workDate.getDate()+1);day=workDate.getDay()}while(workDate.getDay()!=this.options.firstWeekDay)}return table},movePreviousMonthListener:function(){return function(e){var d=this.currentDate;var prevMonth=new Date(d.getFullYear(),d.getMonth()-1,d.getDate(),d.getHours(),d.getMinutes());if(prevMonth.getMonth()!=(d.getMonth()+11)%12){prevMonth.setDate(0)}this.selectDate(prevMonth,false,true)}.bindAsEventListener(this)},moveNextMonthListener:function(){return function(e){var d=this.currentDate;var nextMonth=new Date(d.getFullYear(),d.getMonth()+1,d.getDate(),d.getHours(),d.getMinutes());if(nextMonth.getMonth()!=(d.getMonth()+1)%12){nextMonth.setDate(0)}this.selectDate(nextMonth,false,true)}.bindAsEventListener(this)},moveNextYearListener:function(){return function(e){var d=this.currentDate;var nextYear=new Date(d.getFullYear()+1,d.getMonth(),d.getDate(),d.getHours(),d.getMinutes());if(nextYear.getMonth()!=d.getMonth()){nextYear.setDate(0)}this.selectDate(nextYear,false,true)}.bindAsEventListener(this)},movePreviousYearListener:function(){return function(e){var d=this.currentDate;var prevYear=new Date(d.getFullYear()-1,d.getMonth(),d.getDate(),d.getHours(),d.getMinutes());if(prevYear.getMonth()!=d.getMonth()){prevYear.setDate(0)}this.selectDate(prevYear,false,true)}.bindAsEventListener(this)},copyDate:function(d,timeOverride){var d2=new Date(d.getTime());var c=this.currentDate;if(!timeOverride){d2.setHours(c.getHours());d2.setMinutes(c.getMinutes());d2.setSeconds(c.getSeconds());d2.setMilliseconds(c.getMilliseconds())}return d2},rangeStartListener:function(date){var d=this.copyDate(date);return function(e){if(this.options.range){if(this.dragging){return}this.dragging=true;this.dragged=false}this.dateClicked(d)}.bindAsEventListener(this)},rangeEndListener:function(date){var d=this.copyDate(date);return function(e){if(this.options.range){this.dragging=false;if(this.dragged){this.dateClicked(d)}}}.bindAsEventListener(this)},hoverListener:function(date){var d=this.copyDate(date);return function(e){if(this.options.range&&this.dragging){this.dragged=true;this.dateClicked(d,true)}}.bindAsEventListener(this)},moveListener:function(date,timeOverride){var d=this.copyDate(date,timeOverride);return function(e){this.selectDate(d,false,true)}.bindAsEventListener(this)},clickedListener:function(date,timeOverride){var d=this.copyDate(date,timeOverride);return function(e){this.dateClicked(d)}.bindAsEventListener(this)},assignDayClasses:function(cell,baseClass,date){cell=$(cell);var today=new Date();cell.addClassName(baseClass);if(date.getFullYear()==today.getFullYear()&&date.getMonth()==today.getMonth()&&date.getDate()==today.getDate()){cell.addClassName("today")}if(this.options.weekend.include(date.getDay())){cell.addClassName("weekend")}if((this.options.minDate&&datethis.options.maxDate)){cell.addClassName("disabled")}},monthName:function(month){return this.options.months[month]},dayName:function(day){return this.options.days[day]},clickHandler:function(e){this.captureKeys();if(this.options.onClick){this.options.onClick()}},keyHandler:function(e){var days=0;switch(e.keyCode){case Event.KEY_RETURN:if(this.options.onSelect){this.options.onSelect(this.currentDate)}break;case Event.KEY_LEFT:days=-1;break;case Event.KEY_UP:days=-7;break;case Event.KEY_RIGHT:days=1;break;case Event.KEY_DOWN:days=7;break;case 33:var lastMonth=new Date(this.currentDate.getFullYear(),this.currentDate.getMonth()-1,this.currentDate.getDate());days=-this.getDaysOfMonth(lastMonth);break;case 34:days=this.getDaysOfMonth(this.currentDate);break;case 13:this.dateClicked(this.currentDate);break;default:return}if(days!=0){var moveDate=new Date(this.currentDate.getFullYear(),this.currentDate.getMonth(),this.currentDate.getDate()+days);moveDate.setHours(this.currentDate.getHours());moveDate.setMinutes(this.currentDate.getMinutes());moveDate.setSeconds(this.currentDate.getSeconds());moveDate.setMilliseconds(this.currentDate.getMilliseconds());this.selectDate(moveDate)}Event.stop(e);return false},getDaysOfMonth:function(date){var lastDay=new Date(date.getFullYear(),date.getMonth()+1,0);return lastDay.getDate()},getNextMonth:function(month,year,increment){if(p_Month==11){return[0,year+1]}else{return[month+1,year]}},getPrevMonth:function(month,year,increment){if(p_Month==0){return[11,year-1]}else{return[month-1,year]}},dateClicked:function(date,dragging){if(date){var endRange=this.inRange&&!dragging;if(!dragging){this.inRange=false}this.selectDate(date,!endRange);if(this.options.onSelect){if(this.options.range){if(endRange&&this.rangeStart){if(this.rangeStartthis.options.maxDate){date=this.options.maxDate}}this.currentDate=date;var dateKey=this.dateKey(date);if(!(dateKey in this.visibleDays)||(noRange&&!(dateKey in this.currentDays))){this.setDate(date,noRange)}this.selectedDays.invoke("removeClassName","current");if(this.options.range){this.selectedDays.invoke("removeClassName","leftrange");this.selectedDays.invoke("removeClassName","rightrange");this.selectedDays.invoke("removeClassName","currenthint");if(!noRange){if(!this.inRange&&startRange){this.inRange=true;this.rangeStart=date}this.rangeEnd=date}this.selectedDays=[];if(this.rangeStart){var low,high;if(this.rangeEnda){return 1}}return 0},format:function(N,I){var Q=Control.DatePicker.DateFormat.LZ;var n=Control.DatePicker.DateFormat.MONTH_NAMES;var z=Control.DatePicker.DateFormat.DAY_NAMES;I=I+"";var o="";var x=0;var L="";var g="";var l=N.getYear()+"";var i=N.getMonth()+1;var J=N.getDate();var q=N.getDay();var p=N.getHours();var B=N.getMinutes();var t=N.getSeconds();var f=N.getMilliseconds();var v,w,b,u,O,e,G,F,C,r,R,p,P,j,a,D;var A=new Object();if(l.length<4){l=""+(l-0+1900)}A.y=""+l;A.yyyy=l;A.yy=l.substring(2,4);A.M=i;A.MM=Q(i);A.MMM=n[i-1];A.NNN=n[i+11];A.d=J;A.dd=Q(J);A.E=z[q+7];A.EE=z[q];A.H=p;A.HH=Q(p);if(p==0){A.h=12}else{if(p>12){A.h=p-12}else{A.h=p}}A.hh=Q(A.h);if(p>11){A.K=p-12}else{A.K=p}A.k=p+1;A.KK=Q(A.K);A.kk=Q(A.k);if(p>11){A.a="PM"}else{A.a="AM"}A.m=B;A.mm=Q(B);A.s=t;A.ss=Q(t);A.S=f;A.SS=Q(f,2);A.SSS=Q(f,3);while(x=e;a--){var b=f.substring(d,d+a);if(b.length70){l=1900+(l-0)}else{l=2000+(l-0)}}}else{if(f=="MMM"||f=="NNN"){w=0;for(var r=0;r11)){w=r+1;if(w>12){w-=12}B+=e.length;break}}}if((w<1)||(w>12)){return 0}}else{if(f=="EE"||f=="E"){for(var r=0;r12)){return 0}B+=w.length}else{if(f=="dd"||f=="d"){v=z(D,B,f.length,2);if(v==null||(v<1)||(v>31)){return 0}B+=v.length}else{if(f=="hh"||f=="h"){d=z(D,B,f.length,2);if(d==null||(d<1)||(d>12)){return 0}B+=d.length}else{if(f=="HH"||f=="H"){d=z(D,B,f.length,2);if(d==null||(d<0)||(d>23)){return 0}B+=d.length}else{if(f=="KK"||f=="K"){d=z(D,B,f.length,2);if(d==null||(d<0)||(d>11)){return 0}B+=d.length}else{if(f=="kk"||f=="k"){d=z(D,B,f.length,2);if(d==null||(d<1)||(d>24)){return 0}B+=d.length;d--}else{if(f=="mm"||f=="m"){s=z(D,B,f.length,2);if(s==null||(s<0)||(s>59)){return 0}B+=s.length}else{if(f=="ss"||f=="s"){q=z(D,B,f.length,2);if(q==null||(q<0)||(q>59)){return 0}B+=q.length}else{if(f=="SS"||f=="S"||f=="SSS"){k=z(D,B,f.length,3);if(k==null||(k<0)||(k>999)){return 0}B+=k.length}else{if(f=="a"){if(D.substring(B,B+2).toLowerCase()=="am"){m="AM"}else{if(D.substring(B,B+2).toLowerCase()=="pm"){m="PM"}else{return 0}}B+=2}else{if(D.substring(B,B+f.length)!=f){return 0}else{B+=f.length}}}}}}}}}}}}}}}if(B!=D.length){return 0}if(w==2){if(((l%4==0)&&(l%100!=0))||(l%400==0)){if(v>29){return 0}}else{if(v>28){return 0}}}if((w==4)||(w==6)||(w==9)||(w==11)){if(v>30){return 0}}if(d<12&&m=="PM"){d=d-0+12}else{if(d>11&&m=="AM"){d-=12}}var a=new Date(l,w-1,v,d,s,q,k);return a},parse:function(b,k){if(k){return Control.DatePicker.DateFormat.parseFormat(b,k)}else{var m=["y-M-d","MMM d, y","MMM d,y","y-MMM-d","d-MMM-y","MMM d"];var c=["M/d/y","M-d-y","M.d.y","MMM-d","M/d","M-d"];var n=["d/M/y","d-M-y","d.M.y","d-MMM","d/M","d-M"];var a=[m,c,n];var h=null;for(var g=0;g-1){throw ("Locale available but not loaded")}var c=b.charAt(2)=="_"?b.substring(0,2):b;var a=(Control.DatePicker.Locale[b]||Control.DatePicker.Locale[c]);this.opts=Object.clone(a||{});var d=a?Control.DatePicker.Language[a.language]:null;if(d){Object.extend(this.opts,d)}},opts:null,inheritOptions:function(a){if(!this.opts){this.setLocale("en_US")}return Object.extend(this.opts,a||{})},tr:function(a){return this.opts&&this.opts.strings?this.opts.strings[a]||a:a}};Control.DatePicker.Locale={};with(Control.DatePicker){Locale.es=i18n.createLocale("eu","es");Locale.en=i18n.createLocale("us","en");Locale.en_GB=i18n.createLocale("eu","en");Locale.en_AU=Locale.en_GB;Locale.de=i18n.createLocale("eu","de");Locale.es_iso8601=i18n.createLocale("iso8601","es");Locale.en_iso8601=i18n.createLocale("iso8601","en");Locale.de_iso8601=i18n.createLocale("iso8601","de")}Control.DatePicker.Language={es:{months:["Enero","Febrero","Marzo","Abril","Mayo","Junio","Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre"],days:["Do","Lu","Ma","Mi","Ju","Vi","Sa"],strings:{Now:"Ahora",Today:"Hoy",Time:"Hora","Exact minutes":"Minuto exacto","Select Date and Time":"Selecciona Dia y Hora","Select Time":"Selecciona Hora","Open calendar":"Abre calendario"}},de:{months:["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],days:["So","Mo","Di","Mi","Do","Fr","Sa"],strings:{Now:"Jetzt",Today:"Heute",Time:"Zeit","Exact minutes":"Exakte minuten","Select Date and Time":"Zeit und Datum Auswählen","Select Time":"Zeit Auswählen","Open calendar":"Kalender öffnen"}}};Control.DatePicker.Panel=Class.create(function(){function pad(x){if(x<10){return"0"+x}return new String(x)}return{initialize:function(options){try{this.i18n=new Control.DatePicker.i18n(options&&options.locale?options.locale:"en_US");options=this.i18n.inheritOptions(options)}catch(e){this.i18n=new Control.DatePicker.i18n()}this.options=Object.extend({className:"",closeOnToday:true,selectToday:true,timePicker:false,use24hrs:false,firstWeekDay:0,weekend:[0,6],months:["January","February","March","April","May","June","July","August","September","October","November","December"],days:["Su","Mo","Tu","We","Th","Fr","Sa"]},options||{});with(this.options){if(isNaN(firstWeekDay*1)){firstWeekDay=0}else{firstWeekDay=firstWeekDay%7}}this.calendarCont=null;this.currentDate=this.options.date?this.options.date:new Date();this.dayOfWeek=0;this.minInterval=5;this.rangeStart=this.currentDate;this.rangeEnd=this.options.dateEnd;this.inRange=false;this.position=this.options.position;this.selectedDays=[];this.currentDays={};this.visibleDays={};this.invisibleDays={};this.element=this.createPicker();this.selectDate(this.currentDate);this.element.on("selectstart",function(e){Event.stop(e)}.bindAsEventListener(this))},createPicker:function(){var elt=new Element("div",{"class":"_pp_datepicker "+this.options.className});this.calendarCont=this.drawCalendar(elt,this.currentDate);Event.observe(elt,"click",this.clickHandler.bindAsEventListener(this));this.keyListener=document.on("keydown",this.keyHandler.bindAsEventListener(this));document.on("click",function(e){if(!e.findElement("._pp_datepicker")){this.keyListener.stop()}}.bindAsEventListener(this));if(!this.options.captureKeys){this.keyListener.stop()}return elt},tr:function(str){return this.i18n.tr(str)},captureKeys:function(){this.keyListener.start()},releaseKeys:function(){this.keyListener.stop()},setDate:function(date,recenter){if(date){this.element.update();var dateKey=this.dateKey(date);if(!(dateKey in this.visibleDays)){if(recenter){this.position=this.options.position}else{if(datethis.currentDate){this.position="right"}else{this.position=this.options.position}}}}this.calendarCont=this.drawCalendar(this.element,date)}},drawCalendar:function(container,date){var calendar=this.createCalendar(date);container.appendChild(calendar);container.style.width=calendar.style.width;if(!this.options.range&&this.options.timePicker){var selectListener=function(e){if(this.options.onSelect){this.options.onSelect(this.currentDate)}}.bindAsEventListener(this);var tp=new Control.TimePicker.Panel({onChange:this.selectTime.bind(this),onSelect:selectListener,use24hrs:this.options.use24hrs});var timewrap=new Element("table",{style:"margin: 3px auto;"});var row=timewrap.insertRow(0);var cell=$(row.insertCell(0));cell.appendChild(tp.element);container.appendChild(timewrap);tp.setTime(this.currentDate);this.timePicker=tp;var select=new Element("button").update(this.tr("Select Date and Time"));select.on("click",selectListener);container.appendChild(select)}return container},createCalendar:function(date){this.currentDays={};this.visibleDays={};this.invisibleDays={};var wrapper=new Element("div");var header=this.createHeader(date);wrapper.appendChild(header);if((months=this.options.monthCount)&&months>1){if(months>3){months=3}wrapper.style.width=(180*months+(months-1)*3)+"px";var vertical=(this.options.layout=="vertical");var row;if(!vertical){var table=new Element("table",{cellPadding:0,cellSpacing:0,border:0});row=table.insertRow(0);wrapper.appendChild(table)}var start=new Date(date.getTime());start.setDate(1);if(this.position&&this.position=="left"){}else{if(this.position&&this.position=="right"){start.setMonth(start.getMonth()-(months-1))}else{start.setMonth(start.getMonth()-Math.floor(months/2))}}for(var i=0;i0){cal.style.marginLeft="3px"}}else{if(i>0){cal.style.marginTop="3px"}}if(!vertical){var cell=$(row.insertCell(row.cells.length));cell.appendChild(cal)}else{wrapper.appendChild(cal)}start.setMonth(start.getMonth()+1)}}else{wrapper.style.width="180px";wrapper.appendChild(this.createMonth(date))}return wrapper},createHeader:function(date){var today=new Date();var previousYear=new Date(date.getFullYear()-1,date.getMonth(),1);var previousMonth=new Date(date.getFullYear(),date.getMonth()-1,1);var nextMonth=new Date(date.getFullYear(),date.getMonth()+1,1);var nextYear=new Date(date.getFullYear()+1,date.getMonth(),1);var nav=new Element("div",{"class":"_pp_datepicker_navigation"});var link=new Element("span",{"class":"_pp_datepicker_previous",title:this.monthName(previousYear.getMonth())+" "+previousYear.getFullYear()}).update("<<");link.on("click",this.movePreviousYearListener());nav.insert(link);link=new Element("span",{"class":"_pp_datepicker_previous",title:this.monthName(previousMonth.getMonth())+" "+previousMonth.getFullYear()}).update("<");link.on("click",this.movePreviousMonthListener());nav.insert(link);link=new Element("span",{"class":"_pp_datepicker_next",title:this.monthName(nextYear.getMonth())+" "+nextYear.getFullYear()}).update(">>");link.on("click",this.moveNextYearListener());nav.insert(link);link=new Element("span",{"class":"_pp_datepicker_next",title:this.monthName(nextMonth.getMonth())+" "+nextMonth.getFullYear()}).update(">");link.on("click",this.moveNextMonthListener());nav.insert(link);link=new Element("div",{"class":"_pp_datepicker_today",title:today.getDate()+" "+this.monthName(today.getMonth())+" "+today.getFullYear()}).update(this.options.timePicker?this.tr("Now"):this.tr("Today"));link.on("click",this.clickedListener(today,true));nav.insert(link);return nav},createMonth:function(date){var table=new Element("table",{cellSpacing:0,cellPadding:0,border:0,"class":"_pp_datepicker_table"});var row=$(table.insertRow(table.rows.length));if(this.options.range){row.insertCell(0)}cell=$(row.insertCell(row.cells.length));cell.className="_pp_title";cell.colSpan=7;cell.update(this.monthName(date.getMonth())+" "+date.getFullYear());row=$(table.insertRow(table.rows.length));if(this.options.range){row.insertCell(0)}for(var i=0;i<7;++i){cell=new Element("th",{width:"14%","class":"_pp_highlight"}).update(this.dayName((this.options.firstWeekDay+i)%7));row.insert(cell)}var workDate=new Date(date.getFullYear(),date.getMonth(),1);var day=workDate.getDay();if(day!=this.options.firstWeekDay){row=$(table.insertRow(table.rows.length));workDate.setDate(workDate.getDate()-((day-this.options.firstWeekDay+7)%7));if(this.options.range){cell=$(row.insertCell(row.cells.length));cell.className="_pp_datepicker_weekselect";cell.on("mousedown",this.weekClicked(workDate))}day=workDate.getDay();while(workDate.getMonth()!=date.getMonth()){cell=new Element("td").update(workDate.getDate());this.assignDayClasses(cell,"dayothermonth",workDate);cell.on("mousedown",this.rangeStartListener(workDate));cell.on("mouseover",this.hoverListener(workDate));cell.on("mouseup",this.rangeEndListener(workDate));var dateKey=this.dateKey(workDate);this.invisibleDays[dateKey]=cell;row.insert(cell);workDate.setDate(workDate.getDate()+1);day=workDate.getDay()}}while(workDate.getMonth()==date.getMonth()){if(day==this.options.firstWeekDay){row=$(table.insertRow(table.rows.length));if(this.options.range){if(this.options.range){cell=new Element("td",{"class":"_pp_datepicker_weekselect"});cell.on("mousedown",this.weekClicked(workDate));row.insert(cell)}}}cell=new Element("td").update(workDate.getDate());this.assignDayClasses(cell,"day",workDate);row.insert(cell);cell.on("mousedown",this.rangeStartListener(workDate));cell.on("mouseover",this.hoverListener(workDate));cell.on("mouseup",this.rangeEndListener(workDate));var dateKey=this.dateKey(workDate);this.visibleDays[dateKey]=cell;if(workDate.getFullYear()==this.currentDate.getFullYear()&&workDate.getMonth()==this.currentDate.getMonth()){this.currentDays[dateKey]=cell}workDate.setDate(workDate.getDate()+1);day=workDate.getDay()}if(day!=this.options.firstWeekDay){do{cell=new Element("td").update(workDate.getDate());this.assignDayClasses(cell,"dayothermonth",workDate);row.insert(cell);var thisDate=new Date(workDate.getTime());cell.on("mousedown",this.rangeStartListener(workDate));cell.on("mouseover",this.hoverListener(workDate));cell.on("mouseup",this.rangeEndListener(workDate));var dateKey=this.dateKey(workDate);this.invisibleDays[dateKey]=cell;workDate.setDate(workDate.getDate()+1);day=workDate.getDay()}while(workDate.getDay()!=this.options.firstWeekDay)}return table},movePreviousMonthListener:function(){return function(e){var d=this.currentDate;var prevMonth=new Date(d.getFullYear(),d.getMonth()-1,d.getDate(),d.getHours(),d.getMinutes());if(prevMonth.getMonth()!=(d.getMonth()+11)%12){prevMonth.setDate(0)}this.selectDate(prevMonth,false,true)}.bindAsEventListener(this)},moveNextMonthListener:function(){return function(e){var d=this.currentDate;var nextMonth=new Date(d.getFullYear(),d.getMonth()+1,d.getDate(),d.getHours(),d.getMinutes());if(nextMonth.getMonth()!=(d.getMonth()+1)%12){nextMonth.setDate(0)}this.selectDate(nextMonth,false,true)}.bindAsEventListener(this)},moveNextYearListener:function(){return function(e){var d=this.currentDate;var nextYear=new Date(d.getFullYear()+1,d.getMonth(),d.getDate(),d.getHours(),d.getMinutes());if(nextYear.getMonth()!=d.getMonth()){nextYear.setDate(0)}this.selectDate(nextYear,false,true)}.bindAsEventListener(this)},movePreviousYearListener:function(){return function(e){var d=this.currentDate;var prevYear=new Date(d.getFullYear()-1,d.getMonth(),d.getDate(),d.getHours(),d.getMinutes());if(prevYear.getMonth()!=d.getMonth()){prevYear.setDate(0)}this.selectDate(prevYear,false,true)}.bindAsEventListener(this)},copyDate:function(d,timeOverride){var d2=new Date(d.getTime());var c=this.currentDate;if(!timeOverride){d2.setHours(c.getHours());d2.setMinutes(c.getMinutes());d2.setSeconds(c.getSeconds());d2.setMilliseconds(c.getMilliseconds())}return d2},rangeStartListener:function(date){var d=this.copyDate(date);return function(e){if(this.options.range){if(this.dragging){return}this.dragging=true;this.dragged=false}this.dateClicked(d)}.bindAsEventListener(this)},rangeEndListener:function(date){var d=this.copyDate(date);return function(e){if(this.options.range){this.dragging=false;if(this.dragged){this.dateClicked(d)}}}.bindAsEventListener(this)},hoverListener:function(date){var d=this.copyDate(date);return function(e){if(this.options.range&&this.dragging){this.dragged=true;this.dateClicked(d,true)}}.bindAsEventListener(this)},moveListener:function(date,timeOverride){var d=this.copyDate(date,timeOverride);return function(e){this.selectDate(d,false,true)}.bindAsEventListener(this)},clickedListener:function(date,timeOverride){var d=this.copyDate(date,timeOverride);return function(e){this.dateClicked(d)}.bindAsEventListener(this)},assignDayClasses:function(cell,baseClass,date){cell=$(cell);var today=new Date();cell.addClassName(baseClass);if(date.getFullYear()==today.getFullYear()&&date.getMonth()==today.getMonth()&&date.getDate()==today.getDate()){cell.addClassName("today")}if(this.options.weekend.include(date.getDay())){cell.addClassName("weekend")}if((this.options.minDate&&datethis.options.maxDate)){cell.addClassName("disabled")}},monthName:function(month){return this.options.months[month]},dayName:function(day){return this.options.days[day]},clickHandler:function(e){this.captureKeys();if(this.options.onClick){this.options.onClick()}},keyHandler:function(e){var days=0;switch(e.keyCode){case Event.KEY_RETURN:if(this.options.onSelect){this.options.onSelect(this.currentDate)}break;case Event.KEY_LEFT:days=-1;break;case Event.KEY_UP:days=-7;break;case Event.KEY_RIGHT:days=1;break;case Event.KEY_DOWN:days=7;break;case 33:var lastMonth=new Date(this.currentDate.getFullYear(),this.currentDate.getMonth()-1,this.currentDate.getDate());days=-this.getDaysOfMonth(lastMonth);break;case 34:days=this.getDaysOfMonth(this.currentDate);break;case 13:this.dateClicked(this.currentDate);break;default:return}if(days!=0){var moveDate=new Date(this.currentDate.getFullYear(),this.currentDate.getMonth(),this.currentDate.getDate()+days);moveDate.setHours(this.currentDate.getHours());moveDate.setMinutes(this.currentDate.getMinutes());moveDate.setSeconds(this.currentDate.getSeconds());moveDate.setMilliseconds(this.currentDate.getMilliseconds());this.selectDate(moveDate)}Event.stop(e);return false},getDaysOfMonth:function(date){var lastDay=new Date(date.getFullYear(),date.getMonth()+1,0);return lastDay.getDate()},getNextMonth:function(month,year,increment){if(p_Month==11){return[0,year+1]}else{return[month+1,year]}},getPrevMonth:function(month,year,increment){if(p_Month==0){return[11,year-1]}else{return[month-1,year]}},dateClicked:function(date,dragging){if(date){var endRange=this.inRange&&!dragging;if(!dragging){this.inRange=false}this.selectDate(date,!endRange);if(this.options.onSelect){if(this.options.range){if(endRange&&this.rangeStart){if(this.rangeStartthis.options.maxDate){date=this.options.maxDate}}this.currentDate=date;var dateKey=this.dateKey(date);if(!(dateKey in this.visibleDays)||(noRange&&!(dateKey in this.currentDays))){this.setDate(date,noRange)}this.selectedDays.invoke("removeClassName","current");if(this.options.range){this.selectedDays.invoke("removeClassName","leftrange");this.selectedDays.invoke("removeClassName","rightrange");this.selectedDays.invoke("removeClassName","currenthint");if(!noRange){if(!this.inRange&&startRange){this.inRange=true;this.rangeStart=date}this.rangeEnd=date}this.selectedDays=[];if(this.rangeStart){var low,high;if(this.rangeEnda){return 1}}return 0},format:function(N,I){var Q=Control.DatePicker.DateFormat.LZ;var n=Control.DatePicker.DateFormat.MONTH_NAMES;var z=Control.DatePicker.DateFormat.DAY_NAMES;I=I+"";var o="";var x=0;var L="";var g="";var l=N.getYear()+"";var i=N.getMonth()+1;var J=N.getDate();var q=N.getDay();var p=N.getHours();var B=N.getMinutes();var t=N.getSeconds();var f=N.getMilliseconds();var v,w,b,u,O,e,G,F,C,r,R,p,P,j,a,D;var A=new Object();if(l.length<4){l=""+(l-0+1900)}A.y=""+l;A.yyyy=l;A.yy=l.substring(2,4);A.M=i;A.MM=Q(i);A.MMM=n[i-1];A.NNN=n[i+11];A.d=J;A.dd=Q(J);A.E=z[q+7];A.EE=z[q];A.H=p;A.HH=Q(p);if(p==0){A.h=12}else{if(p>12){A.h=p-12}else{A.h=p}}A.hh=Q(A.h);if(p>11){A.K=p-12}else{A.K=p}A.k=p+1;A.KK=Q(A.K);A.kk=Q(A.k);if(p>11){A.a="PM"}else{A.a="AM"}A.m=B;A.mm=Q(B);A.s=t;A.ss=Q(t);A.S=f;A.SS=Q(f,2);A.SSS=Q(f,3);while(x=e;a--){var b=f.substring(d,d+a);if(b.length70){l=1900+(l-0)}else{l=2000+(l-0)}}}else{if(f=="MMM"||f=="NNN"){w=0;for(var r=0;r11)){w=r+1;if(w>12){w-=12}B+=e.length;break}}}if((w<1)||(w>12)){return 0}}else{if(f=="EE"||f=="E"){for(var r=0;r12)){return 0}B+=w.length}else{if(f=="dd"||f=="d"){v=z(D,B,f.length,2);if(v==null||(v<1)||(v>31)){return 0}B+=v.length}else{if(f=="hh"||f=="h"){d=z(D,B,f.length,2);if(d==null||(d<1)||(d>12)){return 0}B+=d.length}else{if(f=="HH"||f=="H"){d=z(D,B,f.length,2);if(d==null||(d<0)||(d>23)){return 0}B+=d.length}else{if(f=="KK"||f=="K"){d=z(D,B,f.length,2);if(d==null||(d<0)||(d>11)){return 0}B+=d.length}else{if(f=="kk"||f=="k"){d=z(D,B,f.length,2);if(d==null||(d<1)||(d>24)){return 0}B+=d.length;d--}else{if(f=="mm"||f=="m"){s=z(D,B,f.length,2);if(s==null||(s<0)||(s>59)){return 0}B+=s.length}else{if(f=="ss"||f=="s"){q=z(D,B,f.length,2);if(q==null||(q<0)||(q>59)){return 0}B+=q.length}else{if(f=="SS"||f=="S"||f=="SSS"){k=z(D,B,f.length,3);if(k==null||(k<0)||(k>999)){return 0}B+=k.length}else{if(f=="a"){if(D.substring(B,B+2).toLowerCase()=="am"){m="AM"}else{if(D.substring(B,B+2).toLowerCase()=="pm"){m="PM"}else{return 0}}B+=2}else{if(D.substring(B,B+f.length)!=f){return 0}else{B+=f.length}}}}}}}}}}}}}}}if(B!=D.length){return 0}if(w==2){if(((l%4==0)&&(l%100!=0))||(l%400==0)){if(v>29){return 0}}else{if(v>28){return 0}}}if((w==4)||(w==6)||(w==9)||(w==11)){if(v>30){return 0}}if(d<12&&m=="PM"){d=d-0+12}else{if(d>11&&m=="AM"){d-=12}}var a=new Date(l,w-1,v,d,s,q,k);return a},parse:function(b,k){if(k){return Control.DatePicker.DateFormat.parseFormat(b,k)}else{var m=["y-M-d","MMM d, y","MMM d,y","y-MMM-d","d-MMM-y","MMM d"];var c=["M/d/y","M-d-y","M.d.y","MMM-d","M/d","M-d"];var n=["d/M/y","d-M-y","d.M.y","d-MMM","d/M","d-M"];var a=[m,c,n];var h=null;for(var g=0;gDate + * Picker demo +**/ +Control.DatePicker = Class.create({ + +/** + * new Control.DatePicker(element[, options]) + * - element (String | Element): A `` element (or DOM ID). + * - options (Hash): Additional options for the control. + * + * Create a new date picker from the given `` + * element. + * + * Additional options: + * + * * icon: The URL of the icon to display on the control + * * monthCount: The number of calendar months to display at one time + * * layout: Layout mode for multiple calendars: 'horizontal' (default) or 'vertical' + * * range: Use date range selection instead of a single date. + * Requires multiple `` elements (see rangeEnd below). + * * rangeEnd: The element for storing a date range's end date in. If a rangeEnd + * element is not specified, it will automatically look for one as a next + * sibling of `element`. + * * minDate: The minimum date that is allowed to be selected + * * maxDate: The maximum date that is allowed to be selected + * * locale: Set the internationalization locale code + * * manual: Allow manual date entry by typing (default true) + * * epoch: The date posted to the server will be as a unix + * timestamp representing the milliseconds since 1-1-1970 + * * timePicker: Display a time picker (default false) + * * use24hrs: Show 24 hours in the time picker instead of + * AM/PM (default false) + * * onSelect: Callback function when a date/time is selected. + * A Date object is passed as the parameter. + * * onHover: Callback function when the active date changes + * via keyboard navigation. A Date object is passed as + * the parameter. +**/ + initialize: function(element, options) { + + element = $(element); + + if (dp = element.retrieve('datepicker')) + dp.destroy(); + + // Wrap to avoid positioning errors from padding/margins + var wrapper = element.wrap(new Element('span')); + wrapper.style.position = 'relative'; + + options = Object.extend({ + timePicker: false, + manual: true + }, options || {}); + + if (!options.icon) + options.icon = Protoplasm.base('datepicker')+'calendar.png'; + +/** + * Control.DatePicker#element -> Element + * + * The underlying `` element passed to the constructor. +**/ + this.element = element; + this.anchor = element; + this.wrapper = wrapper; +/** + * Control.DatePicker#panel -> Control.DatePicker.Panel + * + * The panel dialog box linked to this control. This may be + * null if the control is not open. +**/ + // Lazy load to avoid excessive CPU usage with lots of controls on one page + this.panel = null; + this.dialog = null; + + this.handlers = { onClick: options.onClick, + onHover: options.onHover, + onSelect: options.onSelect }; + + this.options = Object.extend(options || {}, { + onClick: this.pickerClicked.bind(this), + onHover: this.dateHover.bind(this), + onSelect: this.datePicked.bind(this) + }); + + var locale = options && options.locale ? options.locale : 'en_US'; + try { + this.setLocale(new Control.DatePicker.i18n(locale)); + } catch(e) { + // Load available locale on demand + // TODO: fallback to Protoplasm.require() if URL is from different domain + new Ajax.Request(Protoplasm.base('datepicker')+'locales/'+locale+'.js', { + onSuccess: function(transport) { + eval(transport.responseText); + this.setLocale(new Control.DatePicker.i18n(locale), true); + }.bind(this), + onFailure: function(transport) { + this.setLocale(new Control.DatePicker.i18n('en_US'), true); + }.bind(this) + }); + } + + this.listeners = [ + document.on('keydown', this.docKeyHandler.bindAsEventListener(this)), + Event.on(window, 'unload', this.destroy.bind(this)) + ]; + + this.originalRange = { start: null, end: null }; + if (options.range) + this.rangeEnd = options.rangeEnd ? $(options.rangeEnd) : wrapper.next('input[type=text]'); + + if (options.icon) { + element.style.background = 'url('+options.icon+') right center no-repeat #FFF'; + // Prevent text writing over icon + this.oldPadding = element.style.paddingRight; + element.style.paddingRight = '20px'; + if (this.rangeEnd) { + this.rangeEnd.style.background = 'url('+options.icon+') right center no-repeat #FFF'; + this.rangeEnd.style.paddingRight = '20px'; + } + } + + if (options.epoch) { + this.startLabel = this.makeEpochLabel(element); + this.addListener(this.startLabel); + this.anchor = this.startLabel; + if (this.rangeEnd) { + this.endLabel = this.makeEpochLabel(this.rangeEnd); + this.addListener(this.endLabel); + } + } else { + this.addListener(element); + if (this.rangeEnd) + this.addListener(this.rangeEnd); + element.readOnly = !this.options.manual; + } + + this.hidePickerListener = null; + + this.pickerActive = false; + this.element.store('datepicker', this); + + // Extend element with public API + this.element = Protoplasm.extend(element, { + show: wrapper.show.bind(wrapper), + hide: wrapper.hide.bind(wrapper), + open: this.open.bind(this), + toggle: this.toggle.bind(this), + close: this.close.bind(this), + destroy: this.destroy.bind(this) + }); + + }, + + // Submit dates in milliseconds since 1/1/1970 00:00:00 + makeEpochLabel: function(e) { + var r = e.clone(); + r.name = null; + r.on('change', function() { + var d = Control.DatePicker.DateFormat.parseFormat( + r.value, this.options.currentFormat); + if (d) e.value = d.getTime(); + }); + r.id = null; + r.readOnly = !this.options.manual; + e.type = 'hidden'; + if (e.value) + r.value = this.options.currentFormat + ? Control.DatePicker.DateFormat.format( + new Date(parseInt(e.value)), this.options.currentFormat) + : ''; + e.insert({ after: r}); + return r; + }, + + addListener: function(elt) { + this.listeners.push(elt.on('click', this.toggle.bindAsEventListener(this))); + this.listeners.push(elt.on('keydown', this.keyHandler.bindAsEventListener(this))); + }, + + setLocale: function(locale, reset) { + this.i18n = locale; + this.options = this.i18n.inheritOptions(this.options); + if (!this.options.range && this.options.timePicker) + this.options.currentFormat = this.options.dateTimeFormat; + else + this.options.currentFormat = this.options.dateFormat; + this.options.date = Control.DatePicker.DateFormat.parseFormat(this.element.value, this.options.currentFormat); + if (this.options.range && this.rangeEnd) + this.options.endDate = Control.DatePicker.DateFormat.parseFormat(this.rangeEnd.value, this.options.currentFormat); + // Reset field labels + if (reset) { + var original = this.getValue(); + this.setValue(original.start, original.end); + } + }, + +/** + * Control.DatePicker#destroy() -> null + * + * Destroy this control and return the underlying element to + * its original behavior. +**/ + destroy: function() { + Protoplasm.revert(this.element); + this.listeners.invoke('stop'); + if (this.hidePickerListener) + this.hidePickerListener.stop(); + this.wrapper.parentNode.replaceChild(this.element, this.wrapper); + this.element.style.paddingRight = this.oldPadding; + this.element.store('datepicker', null); + }, + + tr: function(str) { + return this.i18n.tr(str); + }, + clickHandler: function(e) { + var element = Event.element(e); + do { + if (element == document) + break; + if (element == this.element + || element == this.startValue + || element == this.endValue + || element == this.dialog) + return; + } while (element = element.parentNode); + // Next/Back buttons remove themselves from the layout before + // we can check the event source + if (!element) + return; + this.close(); + }, + pickerClicked: function() { + if (this.handlers.onClick) + this.handlers.onClick(); + }, + datePicked: function(start, end) { + this.setValue(start, end); + this.element.focus(); + this.close(); + if (this.handlers.onSelect) + this.handlers.onSelect(start, end); + if (this.element.onchange) + this.element.onchange(); + }, + dateHover: function(start, end) { + if (this.pickerActive) { + this.setValue(start, end); + if (this.handlers.onHover) + this.handlers.onHover(start, end); + } + }, +/** + * Control.DatePicker#toggle() -> null + * + * Toggle the visibility of the picker panel for this control. +**/ + toggle: function(e) { + if (this.pickerActive) { + this.setValue(this.originalRange.start, this.originalRange.end); + this.close(); + } else { + setTimeout(this.open.bind(this)); + } + //Event.stop(e); + return false; + }, + setValue: function(start, end) { + startLabel = start ? Control.DatePicker.DateFormat.format(start, this.options.currentFormat) : null; + startValue = start && this.options.epoch ? start.getTime() : startLabel; + endLabel = end ? Control.DatePicker.DateFormat.format(end, this.options.currentFormat) : null; + endValue = end && this.options.epoch ? end.getTime() : endLabel; + + this.element.value = startValue ? startValue : ''; + if (this.options.epoch) + this.startLabel.value = startLabel ? startLabel : ''; + if (this.rangeEnd) { + this.rangeEnd.value = endValue ? endValue : ''; + if (this.options.epoch) + this.endLabel.value = endLabel ? endLabel : ''; + } + }, + getValue: function() { + var range = { start: null, end: null }; + if (this.element.value) + range.start = this.options.epoch + ? new Date(parseInt(this.element.value)) + : Control.DatePicker.DateFormat.parseFormat(this.element.value, this.options.currentFormat); + if (this.rangeEnd && this.rangeEnd.value) + range.end = this.options.epoch + ? new Date(parseInt(this.rangeEnd.value)) + : Control.DatePicker.DateFormat.parseFormat(this.rangeEnd.value, this.options.currentFormat); + return range; + }, + docKeyHandler: function(e) { + if (e.keyCode == Event.KEY_ESC) + if (this.pickerActive) { + this.setValue(this.originalRange.start, this.originalRange.end); + this.close(); + } + + }, + keyHandler: function(e) { + switch (e.keyCode) { + case Event.KEY_ESC: + if (this.pickerActive) + this.setValue(this.originalRange.start, this.originalRange.end); + case Event.KEY_TAB: + this.close(); + return; + case Event.KEY_DOWN: + if (!this.pickerActive) { + this.open(); + Event.stop(e); + } + } + if (this.pickerActive) + return false; + }, +/** + * Control.DatePicker#close() -> null + * + * Hide the picker panel for this control. +**/ + close: function() { + if(this.pickerActive && !this.element.disabled) { + this.panel.releaseKeys(); + this.dialog.remove(); + if (this.hidePickerListener) { + this.hidePickerListener.stop(); + this.hidePickerListener = null; + } + this.pickerActive = false; + Control.DatePicker.activePicker = null; + } + }, +/** + * Control.DatePicker#open() -> null + * + * Show the picker panel for this control. +**/ + open: function () { + if (!this.pickerActive) { + if (Control.DatePicker.activePicker) + Control.DatePicker.activePicker.close(); + this.anchor.focus(); + if (!this.dialog) { + this.panel = new Control.DatePicker.Panel(this.options); + this.dialog = new Element('div', { 'class': '_pp_frame_small _pp_datepicker ' + + this.options.className, 'style': 'position:absolute;' }); + this.dialog.appendChild(this.panel.element); + } + this.originalRange = this.getValue(); + + var layout = this.anchor.getLayout(); + var offsetTop = layout.get('border-box-height') - layout.get('border-bottom'); + document.body.appendChild(this.dialog); + this.anchor.style.position = 'relative'; + this.dialog.clonePosition(this.anchor, { + 'setWidth': false, + 'setHeight': false, + 'offsetTop': offsetTop, + 'offsetLeft': -3 + }); + this.dialog.style.zIndex = '99'; + this.panel.selectRange(this.originalRange.start, this.originalRange.end); + this.panel.captureKeys(); + + this.hidePickerListener = document.on('click', this.clickHandler.bindAsEventListener(this)); + this.pickerActive = true; + Control.DatePicker.activePicker = this; + this.pickerClicked(); + } + } +}); + +/** + * Control.DatePicker.activePicker -> Control.DatePicker + * + * A reference to the last opened date picker. +**/ +Control.DatePicker.activePicker = null; + +/** + * Control.DatePicker.create(options) -> Element + * - options (Hash): Additional options for the control. + * + * Creates a new date picker from scratch instead of transforming + * an existing DOM element. Returns the root element for the + * date picker control, suitable for inserting into your page. + * You can retrieve the Control.DatePicker instance behind the + * returned element with `element.retrieve('datepicker')`. + * + * Options: + * + * * `className`: The CSS class to assign the <input> element + * * `name`: The field name to assign the <input> element + * + * Additional options are passed through to `new Control.DatePicker()`. + * See [[Control.DatePicker]] constructor for available options. + * + * Example: + * + * var dp = Control.DatePicker.create(); + * panel.appendChild(dp); + * dp.open(); +**/ +Control.DatePicker.create = function(options) { + options = Object.extend({ + 'className': 'datepicker', + 'name': 'date' + }, options || {}); + var elt = new Element('input', { 'class': options.className, 'name': name }); + var dp = new Control.DatePicker(elt); + dp.wrapper.store('datepicker', dp); + return dp.wrapper; +}; + +/** + * class Control.DatePicker.i18n + * + * Internationalization settings for a [[Control.DatePicker]] instance. +**/ +Control.DatePicker.i18n = Class.create(); +Object.extend(Control.DatePicker.i18n, { + available: ['cs_CZ', 'el_GR', 'fr_FR', 'it_IT', 'lt_LT', 'nl_NL', 'pl_PL', 'pt_BR', 'ru_RU'], + baseLocales: { + 'us': { + dateTimeFormat: 'MM-dd-yyyy HH:mm:ss', + dateFormat: 'MM-dd-yyyy', + firstWeekDay: 0, + weekend: [0,6], + timeFormat: 'HH:mm:ss' + }, + 'eu': { + dateTimeFormat: 'dd-MM-yyyy HH:mm:ss', + dateFormat: 'dd-MM-yyyy', + firstWeekDay: 1, + weekend: [0,6], + timeFormat: 'HH:mm:ss' + }, + 'iso8601': { + dateTimeFormat: 'yyyy-MM-dd HH:mm:ss', + dateFormat: 'yyyy-MM-dd', + firstWeekDay: 1, + weekend: [0,6], + timeFormat: 'HH:mm:ss' + } + }, + +/** + * Control.DatePicker.i18n.createLocale(base, lang) -> Object + * - base (String): The base locale (one of "us", "eu", "iso8601"). + * - lang (String): The language to use. + * + * Create a new locale combining a standard base locale ("us", "eu", "iso8601") + * and a new language code. +**/ + createLocale: function(base, lang) { + return Object.extend(Object.clone(Control.DatePicker.i18n.baseLocales[base]), {'language': lang}); + } +}); +Control.DatePicker.i18n.prototype = { +/** + * new Control.DatePicker.i18n([code]) + * - code (String): The locale code. This can be a language code ("es") + * or a full locale ("es_AR"). + * + * Create a new internationalization settings based on the + * specified locale code (i.e. "en_US"). + * + * Locales that are supported by default are: + * + * * es + * * en + * * en_US + * * en_GB + * * de + * * es_iso8601 + * * en_iso8601 + * * de_iso8601 + * + * Additionally, there are other locales which are not included + * but will be loaded on demand via an AJAX call if requested: + * + * * cs_CZ + * * el_GR + * * fr_FR + * * it_IT + * * lt_LT + * * nl_NL + * * pl_PL + * * pt_BR + * * ru_RU + * + * For details on creating new locales, see the + * Protoplasm + * web site. +**/ + initialize: function(code) { + if (code) + this.setLocale(code); + }, + setLocale: function(code) { + if (!(code in Control.DatePicker.Locale) + && Control.DatePicker.i18n.available.indexOf(code) > -1) + throw("Locale available but not loaded"); + var lang = code.charAt(2) == '_' ? code.substring(0,2) : code; + var locale = (Control.DatePicker.Locale[code] || Control.DatePicker.Locale[lang]); + this.opts = Object.clone(locale || {}); + var language = locale ? Control.DatePicker.Language[locale.language] : null; + if (language) Object.extend(this.opts, language); + }, + opts: null, + inheritOptions: function(options) { + if (!this.opts) this.setLocale('en_US'); + return Object.extend(this.opts, options || {}); + }, +/** + * Control.DatePicker.i18n#tr(text) -> String + * - text (String): The string to translate. + * + * Translates the given text into the language linked to + * this instance. +**/ + tr: function(str) { + return this.opts && this.opts.strings ? this.opts.strings[str] || str : str; + } +}; + +/** + * Control.DatePicker.Locale -> Hash + * + * Hash object that stores available locale definitions. + * + * Bundled locales: es, en, en\_US, en\_GB, de, es\_iso8601, + * en\_iso8601, de\_iso8601 +**/ +Control.DatePicker.Locale = {}; +with (Control.DatePicker) { + // Full locale definitions not needed if countries use the language default format + // Datepicker will fallback to the language default; i.e. 'es_AR' will use 'es' + Locale['es'] = i18n.createLocale('eu', 'es'); + Locale['en'] = i18n.createLocale('us', 'en'); + Locale['en_GB'] = i18n.createLocale('eu', 'en'); + Locale['en_AU'] = Locale['en_GB']; + Locale['de'] = i18n.createLocale('eu', 'de'); + Locale['es_iso8601'] = i18n.createLocale('iso8601', 'es'); + Locale['en_iso8601'] = i18n.createLocale('iso8601', 'en'); + Locale['de_iso8601'] = i18n.createLocale('iso8601', 'de'); +} + +/** + * Control.DatePicker.Language -> Hash + * + * Hash object that stores available language definitions. + * + * Bundled languages: en, es, de +**/ +Control.DatePicker.Language = { + 'es': { + months: ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'], + days: ['Do', 'Lu', 'Ma', 'Mi', 'Ju', 'Vi', 'Sa'], + strings: { + 'Now': 'Ahora', + 'Today': 'Hoy', + 'Time': 'Hora', + 'Exact minutes': 'Minuto exacto', + 'Select Date and Time': 'Selecciona Dia y Hora', + 'Select Time': 'Selecciona Hora', + 'Open calendar': 'Abre calendario' + } + }, + 'de': { + months: ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'], + days: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'], + strings: { + 'Now': 'Jetzt', + 'Today': 'Heute', + 'Time': 'Zeit', + 'Exact minutes': 'Exakte minuten', + 'Select Date and Time': 'Zeit und Datum Auswählen', + 'Select Time': 'Zeit Auswählen', + 'Open calendar': 'Kalender öffnen' + } + } +}; + +/** + * class Control.DatePicker.Panel + * + * The dialog panel that is displayed when the date picker is opened. +**/ +Control.DatePicker.Panel = Class.create(function() { + + function pad(x) { + if (x < 10) return '0'+x; + return new String(x); + }; + + return { +/** + * new Control.DatePicker.Panel([options]) + * - options (Hash): Additional options for the panel. + * + * Create a new date picker panel. + * + * Additional options: + * + * * className: The CSS class of the panel element + * * monthCount: The number of calendar months to display at one time + * * layout: Layout mode for multiple calendars: 'horizontal' (default) or 'vertical' + * * range: Use date range selection instead of single dates + * * minDate: The minimum date that is allowed to be selected + * * maxDate: The maximum date that is allowed to be selected + * * timePicker: Display a time picker (default false) - not + * compatible with "range" option + * * use24hrs: Show 24 hours in the time picker instead of + * AM/PM (default false) + * * firstWeekDay: The first day of the week (default 0 - Sunday) + * * weekend: An array of day numbers that are considered the + * weekend (default [0,6] - Saturday/Sunday) + * * months: An array of 12 month names + * * days: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'] + * * closeOnToday: Close picker when the "Today" link is clicked + * (default true) + * * selectToday: Automatically select today's date initially +**/ + initialize: function(options) { + try { + this.i18n = new Control.DatePicker.i18n(options && options.locale ? options.locale : 'en_US'); + options = this.i18n.inheritOptions(options); + } catch(e) { + this.i18n = new Control.DatePicker.i18n(); + } + this.options = Object.extend({ + className: '', + closeOnToday: true, + selectToday: true, + timePicker: false, + use24hrs: false, + firstWeekDay: 0, + weekend: [0,6], + months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], + days: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'] + }, options || {}); + + // Make sure first weekday is in the correct range + with (this.options) + if (isNaN(firstWeekDay*1)) firstWeekDay = 0; + else firstWeekDay = firstWeekDay % 7; + + this.calendarCont = null; + this.currentDate = this.options.date ? this.options.date : new Date(); + this.dayOfWeek = 0; + this.minInterval = 5; + + this.rangeStart = this.currentDate; + this.rangeEnd = this.options.dateEnd; + this.inRange = false; + this.position = this.options.position; + this.selectedDays = []; + + this.currentDays = {}; + this.visibleDays = {}; + this.invisibleDays = {}; + +/** + * Control.DatePicker.Panel#element -> Element + * + * The root Element of this dialog panel. +**/ + this.element = this.createPicker(); + this.selectDate(this.currentDate); + + this.element.on('selectstart', function(e) { + Event.stop(e); }.bindAsEventListener(this)); + }, + + createPicker: function() { + var elt = new Element('div', {'class': '_pp_datepicker '+this.options.className}); + this.calendarCont = this.drawCalendar(elt, this.currentDate); + + Event.observe(elt, 'click', this.clickHandler.bindAsEventListener(this)); + this.keyListener = document.on('keydown', this.keyHandler.bindAsEventListener(this)); + document.on('click', function(e) { + if (!e.findElement('._pp_datepicker')) + this.keyListener.stop(); + }.bindAsEventListener(this)); + if (!this.options.captureKeys) + this.keyListener.stop(); + + return elt; + }, + tr: function(str) { + return this.i18n.tr(str); + }, + captureKeys: function() { + this.keyListener.start(); + }, + releaseKeys: function() { + this.keyListener.stop(); + }, + setDate: function(date, recenter) { + if (date) { + // Clear container + this.element.update(); + var dateKey = this.dateKey(date); + if (!(dateKey in this.visibleDays)) { + if (recenter) + this.position = this.options.position; + else if (date < this.currentDate) + this.position = 'left'; + else if (date > this.currentDate) + this.position = 'right'; + else + this.position = this.options.position; + } + this.calendarCont = this.drawCalendar(this.element, date); + } + }, + drawCalendar: function(container, date) { + var calendar = this.createCalendar(date); + container.appendChild(calendar); + container.style.width = calendar.style.width; + if (!this.options.range && this.options.timePicker) { + + var selectListener = function(e) { + if (this.options.onSelect) + this.options.onSelect(this.currentDate); + }.bindAsEventListener(this); + + var tp = new Control.TimePicker.Panel({ + 'onChange': this.selectTime.bind(this), + 'onSelect': selectListener, + 'use24hrs': this.options.use24hrs + }); + + // Fuck this shit... how hard is it to get a centered shrink-to-fit + // block that works in all browsers? + //tp.element.style.display = 'inline-table'; + //tp.element.style.margin = '3px auto'; + var timewrap = new Element('table', {'style': 'margin: 3px auto;'}); + var row = timewrap.insertRow(0); + var cell = $(row.insertCell(0)); + cell.appendChild(tp.element); + container.appendChild(timewrap); + + tp.setTime(this.currentDate); + this.timePicker = tp; + + var select = new Element('button').update(this.tr('Select Date and Time')); + select.on('click', selectListener); + container.appendChild(select); + } + return container; + }, + createCalendar: function(date) { + + this.currentDays = {}; + this.visibleDays = {}; + this.invisibleDays = {}; + + var wrapper = new Element('div'); + var header = this.createHeader(date); + wrapper.appendChild(header); + if ((months = this.options.monthCount) && months > 1) { + if (months > 3) + months = 3; + // IE compatibility + wrapper.style.width = (180*months + (months-1)*3) + 'px'; + var vertical = (this.options.layout == 'vertical'); + var row; + if (!vertical) { + var table = new Element('table', {'cellPadding': 0, 'cellSpacing': 0, 'border': 0}); + row = table.insertRow(0); + wrapper.appendChild(table); + } + var start = new Date(date.getTime()); + start.setDate(1); + if (this.position && this.position == 'left') { + // Nothing, start is already fine + } else if (this.position && this.position == 'right') { + start.setMonth(start.getMonth() - (months - 1)); + } else { + start.setMonth(start.getMonth() - Math.floor(months/2)); + } + for (var i = 0; i < months; i++) { + var cal = this.createMonth(start); + if (!vertical) { + if (i > 0) + cal.style.marginLeft = '3px'; + } else { + if (i > 0) + cal.style.marginTop = '3px'; + } + if (!vertical) { + var cell = $(row.insertCell(row.cells.length)); + cell.appendChild(cal); + } else { + wrapper.appendChild(cal); + } + start.setMonth(start.getMonth() + 1); + } + } else { + // IE compatibility + wrapper.style.width = '180px'; + wrapper.appendChild(this.createMonth(date)); + } + + return wrapper; + }, + + createHeader: function(date) { + + var today = new Date(); + var previousYear = new Date(date.getFullYear() - 1, date.getMonth(), 1) + var previousMonth = new Date(date.getFullYear(), date.getMonth() - 1, 1) + var nextMonth = new Date(date.getFullYear(), date.getMonth() + 1, 1) + var nextYear = new Date(date.getFullYear() + 1, date.getMonth(), 1) + + var nav = new Element('div', {'class': '_pp_datepicker_navigation'}); + var link = new Element('span', {'class': '_pp_datepicker_previous', + 'title': this.monthName(previousYear.getMonth()) + ' ' + + previousYear.getFullYear()}).update('<<'); + link.on('click', this.movePreviousYearListener()); + nav.insert(link); + + link = new Element('span', {'class': '_pp_datepicker_previous', + 'title': this.monthName(previousMonth.getMonth())+' ' + +previousMonth.getFullYear()}).update('<'); + link.on('click', this.movePreviousMonthListener()); + nav.insert(link); + + link = new Element('span', {'class': '_pp_datepicker_next', + 'title': this.monthName(nextYear.getMonth()) + + ' ' + nextYear.getFullYear()}).update('>>'); + link.on('click', this.moveNextYearListener()); + nav.insert(link); + + link = new Element('span', {'class': '_pp_datepicker_next', + 'title': this.monthName(nextMonth.getMonth()) + + ' ' + nextMonth.getFullYear()}).update('>'); + link.on('click', this.moveNextMonthListener()); + nav.insert(link); + + link = new Element('div', {'class': '_pp_datepicker_today', + 'title': today.getDate() + ' ' + + this.monthName(today.getMonth()) + ' ' + today.getFullYear()}).update( + this.options.timePicker ? this.tr('Now') : this.tr('Today')); + link.on('click', this.clickedListener(today, true)); + nav.insert(link); + + return nav; + }, + createMonth: function(date) { + + var table = new Element('table', + {'cellSpacing': 0, 'cellPadding': 0, 'border': 0, 'class': '_pp_datepicker_table'}); + + var row = $(table.insertRow(table.rows.length)); + if (this.options.range) + row.insertCell(0); + + cell = $(row.insertCell(row.cells.length)); + cell.className = '_pp_title'; + cell.colSpan = 7; + cell.update(this.monthName(date.getMonth()) + ' ' + date.getFullYear()); + + row = $(table.insertRow(table.rows.length)); + if (this.options.range) + row.insertCell(0); + for (var i = 0; i < 7; ++i) { + cell = new Element('th', {'width': '14%', 'class': '_pp_highlight'} + ).update(this.dayName((this.options.firstWeekDay + i) % 7)); + row.insert(cell); + } + + var workDate = new Date(date.getFullYear(), date.getMonth(), 1); + var day = workDate.getDay(); + + // Pad with previous month + if (day != this.options.firstWeekDay) { + row = $(table.insertRow(table.rows.length)); + workDate.setDate(workDate.getDate() - ((day - this.options.firstWeekDay + 7) % 7)); + if (this.options.range) { + cell = $(row.insertCell(row.cells.length)); + cell.className = '_pp_datepicker_weekselect'; + cell.on('mousedown', this.weekClicked(workDate)); + } + day = workDate.getDay(); + while (workDate.getMonth() != date.getMonth()) { + cell = new Element('td').update(workDate.getDate()); + this.assignDayClasses(cell, 'dayothermonth', workDate); + cell.on('mousedown', this.rangeStartListener(workDate)); + cell.on('mouseover', this.hoverListener(workDate)); + cell.on('mouseup', this.rangeEndListener(workDate)); + + var dateKey = this.dateKey(workDate); + this.invisibleDays[dateKey] = cell; + + row.insert(cell); + workDate.setDate(workDate.getDate() + 1); + day = workDate.getDay(); + } + } + + // Display days + while (workDate.getMonth() == date.getMonth()) { + if (day == this.options.firstWeekDay) { + row = $(table.insertRow(table.rows.length)); + if (this.options.range) { + if (this.options.range) { + cell = new Element('td', {'class': '_pp_datepicker_weekselect'}); + cell.on('mousedown', this.weekClicked(workDate)); + row.insert(cell); + } + } + } + cell = new Element('td').update(workDate.getDate()); + this.assignDayClasses(cell, 'day', workDate); + row.insert(cell); + cell.on('mousedown', this.rangeStartListener(workDate)); + cell.on('mouseover', this.hoverListener(workDate)); + cell.on('mouseup', this.rangeEndListener(workDate)); + + var dateKey = this.dateKey(workDate); + this.visibleDays[dateKey] = cell; + if (workDate.getFullYear() == this.currentDate.getFullYear() + && workDate.getMonth() == this.currentDate.getMonth()) + this.currentDays[dateKey] = cell; + workDate.setDate(workDate.getDate() + 1); + day = workDate.getDay(); + } + + // Pad with next month + if (day != this.options.firstWeekDay) + do { + cell = new Element('td').update(workDate.getDate()); + this.assignDayClasses(cell, 'dayothermonth', workDate); + row.insert(cell); + var thisDate = new Date(workDate.getTime()); + cell.on('mousedown', this.rangeStartListener(workDate)); + cell.on('mouseover', this.hoverListener(workDate)); + cell.on('mouseup', this.rangeEndListener(workDate)); + + var dateKey = this.dateKey(workDate); + this.invisibleDays[dateKey] = cell; + + workDate.setDate(workDate.getDate() + 1); + day = workDate.getDay(); + } while (workDate.getDay() != this.options.firstWeekDay); + + return table; + }, + movePreviousMonthListener: function() { + return function(e) { + var d = this.currentDate; + var prevMonth = new Date(d.getFullYear(), d.getMonth() - 1, + d.getDate(), d.getHours(), d.getMinutes()); + if (prevMonth.getMonth() != (d.getMonth() + 11) % 12) prevMonth.setDate(0); + this.selectDate(prevMonth, false, true); + }.bindAsEventListener(this); + }, + moveNextMonthListener: function() { + return function(e) { + var d = this.currentDate; + var nextMonth = new Date(d.getFullYear(), d.getMonth() + 1, + d.getDate(), d.getHours(), d.getMinutes()); + if (nextMonth.getMonth() != (d.getMonth() + 1) % 12) nextMonth.setDate(0); + this.selectDate(nextMonth, false, true); + }.bindAsEventListener(this); + }, + moveNextYearListener: function() { + return function(e) { + var d = this.currentDate; + var nextYear = new Date(d.getFullYear() + 1, d.getMonth(), + d.getDate(), d.getHours(), d.getMinutes()); + if (nextYear.getMonth() != d.getMonth()) nextYear.setDate(0); + this.selectDate(nextYear, false, true); + }.bindAsEventListener(this); + }, + movePreviousYearListener: function() { + return function(e) { + var d = this.currentDate; + var prevYear = new Date(d.getFullYear() - 1, d.getMonth(), + d.getDate(), d.getHours(), d.getMinutes()); + if (prevYear.getMonth() != d.getMonth()) prevYear.setDate(0); + this.selectDate(prevYear, false, true); + }.bindAsEventListener(this); + }, + copyDate: function(d, timeOverride) { + var d2 = new Date(d.getTime()); + var c = this.currentDate; + if (!timeOverride) { + d2.setHours(c.getHours()); + d2.setMinutes(c.getMinutes()); + d2.setSeconds(c.getSeconds()); + d2.setMilliseconds(c.getMilliseconds()); + } + return d2; + }, + rangeStartListener: function(date) { + var d = this.copyDate(date); + return function(e) { + if (this.options.range) { + if (this.dragging) return; + this.dragging = true; + this.dragged = false; + } + this.dateClicked(d); + }.bindAsEventListener(this); + }, + rangeEndListener: function(date) { + var d = this.copyDate(date); + return function(e) { + if (this.options.range) { + this.dragging = false; + if (this.dragged) + this.dateClicked(d); + } + }.bindAsEventListener(this); + }, + hoverListener: function(date) { + var d = this.copyDate(date); + return function(e) { + if (this.options.range && this.dragging) { + this.dragged = true; + this.dateClicked(d, true); + } + }.bindAsEventListener(this); + }, + moveListener: function(date, timeOverride) { + var d = this.copyDate(date, timeOverride); + return function(e) { + this.selectDate(d, false, true); + }.bindAsEventListener(this); + }, + clickedListener: function(date, timeOverride) { + var d = this.copyDate(date, timeOverride); + return function(e) { + this.dateClicked(d); + }.bindAsEventListener(this); + }, + assignDayClasses: function(cell, baseClass, date) { + cell = $(cell); + var today = new Date(); + cell.addClassName(baseClass); + if (date.getFullYear() == today.getFullYear() + && date.getMonth() == today.getMonth() + && date.getDate() == today.getDate()) + cell.addClassName('today'); + if (this.options.weekend.include(date.getDay())) + cell.addClassName('weekend'); + if ((this.options.minDate && date < this.options.minDate) + || (this.options.maxDate && date > this.options.maxDate)) + cell.addClassName('disabled'); + }, + monthName: function(month) { + return this.options.months[month]; + }, + dayName: function(day) { + return this.options.days[day]; + }, + clickHandler: function(e) { + this.captureKeys(); + if(this.options.onClick) + this.options.onClick(); + }, + keyHandler: function(e) { + var days = 0; + switch (e.keyCode){ + case Event.KEY_RETURN: + if (this.options.onSelect) this.options.onSelect(this.currentDate); + break; + case Event.KEY_LEFT: + days = -1; + break; + case Event.KEY_UP: + days = -7; + break; + case Event.KEY_RIGHT: + days = 1; + break; + case Event.KEY_DOWN: + days = 7; + break; + case 33: // PgUp + var lastMonth = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth() - 1, this.currentDate.getDate()); + days = -this.getDaysOfMonth(lastMonth); + break; + case 34: // PgDn + days = this.getDaysOfMonth(this.currentDate); + break; + case 13: // enter-key (forms without submit buttons) + this.dateClicked(this.currentDate); + break; + default: + return; + } + if (days != 0) { + var moveDate = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth(), this.currentDate.getDate() + days); + moveDate.setHours(this.currentDate.getHours()); + moveDate.setMinutes(this.currentDate.getMinutes()); + moveDate.setSeconds(this.currentDate.getSeconds()); + moveDate.setMilliseconds(this.currentDate.getMilliseconds()); + this.selectDate(moveDate); + } + Event.stop(e); + return false; + }, + getDaysOfMonth: function(date) { + var lastDay = new Date(date.getFullYear(), date.getMonth() + 1, 0); + return lastDay.getDate(); + }, + getNextMonth: function(month, year, increment) { + if (p_Month == 11) return [0, year + 1]; + else return [month + 1, year]; + }, + getPrevMonth: function(month, year, increment) { + if (p_Month == 0) return [11, year - 1]; + else return [month - 1, year]; + }, + dateClicked: function(date, dragging) { + if (date) { + var endRange = this.inRange && !dragging; + if (!dragging) + this.inRange = false; + this.selectDate(date, !endRange); + if (this.options.onSelect) { + if (this.options.range) { + if (endRange && this.rangeStart) { + if (this.rangeStart < this.rangeEnd) + this.options.onSelect(this.rangeStart, this.rangeEnd); + else + this.options.onSelect(this.rangeEnd, this.rangeStart); + this.dragging = false; + } + } else if (!this.options.timePicker) + this.options.onSelect(date); + } + var dateKey = this.dateKey(date); + if (!(dateKey in this.currentDays) || this.position != this.options.position) { + this.position = this.options.position; + //this.setDate(date); + } + } + }, + dateKey: function(date) { + return date.getFullYear() + pad(date.getMonth()) + pad(date.getDate()); + }, + applyClass: function(date, klass) { + var k = this.dateKey(date); + if (k in this.visibleDays) + this.visibleDays[k].addClassName(klass); + if (k in this.invisibleDays) + this.invisibleDays[k].addClassName(klass); + }, + weekClicked: function(first) { + var start = new Date(first.getTime()); + var end = new Date(first.getTime()); + end.setDate(end.getDate() + 6); + return function(e) { + this.selectRange(start, end); + }.bindAsEventListener(this); + }, + selectRange: function(start, end) { + this.inRange = false; + this.dateClicked(start); + if (this.options.range) + this.dateClicked(end); + }, + selectDate: function(date, startRange, noRange) { + if (date) { + + if (this.options.minDate && date < this.options.minDate) + date = this.options.minDate; + else if (this.options.maxDate && date > this.options.maxDate) + date = this.options.maxDate; + + this.currentDate = date; + + var dateKey = this.dateKey(date); + if (!(dateKey in this.visibleDays) + || (noRange && !(dateKey in this.currentDays))) + this.setDate(date, noRange); + + this.selectedDays.invoke('removeClassName', 'current'); + + if (this.options.range) { + this.selectedDays.invoke('removeClassName', 'leftrange'); + this.selectedDays.invoke('removeClassName', 'rightrange'); + this.selectedDays.invoke('removeClassName', 'currenthint'); + if (!noRange) { + if (!this.inRange && startRange) { + this.inRange = true; + this.rangeStart = date; + } + this.rangeEnd = date; + } + this.selectedDays = []; + if (this.rangeStart) { + var low, high; + if (this.rangeEnd < this.rangeStart) { + low = new Date(this.rangeEnd.getFullYear(), this.rangeEnd.getMonth(), this.rangeEnd.getDate()); + high = new Date(this.rangeStart.getFullYear(), this.rangeStart.getMonth(), this.rangeStart.getDate()); + } else { + low = new Date(this.rangeStart.getFullYear(), this.rangeStart.getMonth(), this.rangeStart.getDate()); + high = new Date(this.rangeEnd.getFullYear(), this.rangeEnd.getMonth(), this.rangeEnd.getDate()); + } + this.applyClass(low, 'leftrange'); + if (low.getTime() != high.getTime()) + this.applyClass(high, 'rightrange'); + while (low.getTime() <= high.getTime()) { + var k = this.dateKey(low); + if (k in this.visibleDays) { + this.visibleDays[k].addClassName('current'); + this.selectedDays.push(this.visibleDays[k]); + } + if (k in this.invisibleDays) { + this.invisibleDays[k].addClassName('currenthint'); + this.selectedDays.push(this.invisibleDays[k]); + } + low.setDate(low.getDate() + 1); + } + } + } else { + this.selectedDays.invoke('removeClassName', 'singlerange'); + this.visibleDays[dateKey].addClassName('singlerange'); + this.selectedDays = [this.visibleDays[dateKey]]; + } + + if (this.options.timePicker) + this.timePicker.setTime(date); + + if (this.options.onHover) { + if (this.options.range) + this.options.onHover(this.rangeStart, this.rangeEnd); + else + this.options.onHover(date); + } + + } + }, + selectTime: function(time) { + this.currentDate.setHours(time.getHours()); + this.currentDate.setMinutes(time.getMinutes()); + this.currentDate.setSeconds(time.getSeconds()); + this.currentDate.setMilliseconds(time.getMilliseconds()); + if (this.options.onHover) + this.options.onHover(this.currentDate); + } + } +}()); + +/** + * class Control.DatePicker.DateFormat + * + * A date formatting utility class. +**/ +Control.DatePicker.DateFormat = Class.create({ + +/** + * new Control.DatePicker.DateFormat(format) + * - format (String): The format string (see [[Control.DatePicker.DateFormat.format]]). + * + * Create a new DateFormat object the uses the specified format string. +**/ + initialize: function(format) { this.format = format; }, + +/** + * Control.DatePicker.DateFormat#parse(text) -> Date + * - text (String): The text to parse into a Date. + * + * Attempt to parse a string into a Date object according to this + * object's formatting rules. +**/ + parse: function(value) { return Control.DatePicker.DateFormat.parseFormat(value, this.format); }, + +/** + * Control.DatePicker.DateFormat#format(date) -> String + * - date (Date): The date to format. + * + * Format a date into a string according to this object's formatting + * rules. +**/ + format: function(value) { return Control.DatePicker.DateFormat.format(value, this.format); } + +}); + +Object.extend(Control.DatePicker.DateFormat, { + MONTH_NAMES: ['January','February','March','April','May','June','July','August','September','October','November','December','Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'], + DAY_NAMES: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sun','Mon','Tue','Wed','Thu','Fri','Sat'], + LZ: function(x,l) {l=l||2;x=''+x;while(x.length d2) return 1; + return 0; + }, +/** + * Control.DatePicker.DateFormat.format(date, format) -> String + * - date (Date): The date to format. + * - format (String): The format definition. + * + * Convert a date to a string representation according to the specified + * format string. + * + * Formatting tokens: + * TODO +**/ + format: function(date,format) { + var LZ = Control.DatePicker.DateFormat.LZ; + var MONTH_NAMES = Control.DatePicker.DateFormat.MONTH_NAMES; + var DAY_NAMES = Control.DatePicker.DateFormat.DAY_NAMES; + format=format+""; + var result=""; + var i_format=0; + var c=""; + var token=""; + var y=date.getYear()+""; + var M=date.getMonth()+1; + var d=date.getDate(); + var E=date.getDay(); + var H=date.getHours(); + var m=date.getMinutes(); + var s=date.getSeconds(); + var S=date.getMilliseconds(); + var yyyy,yy,MMM,MM,dd,hh,h,mm,ss,ampm,HH,H,KK,K,kk,k; + // Convert real date parts into formatted versions + var value=new Object(); + if (y.length < 4) {y=""+(y-0+1900);} + value["y"]=""+y; + value["yyyy"]=y; + value["yy"]=y.substring(2,4); + value["M"]=M; + value["MM"]=LZ(M); + value["MMM"]=MONTH_NAMES[M-1]; + value["NNN"]=MONTH_NAMES[M+11]; + value["d"]=d; + value["dd"]=LZ(d); + value["E"]=DAY_NAMES[E+7]; + value["EE"]=DAY_NAMES[E]; + value["H"]=H; + value["HH"]=LZ(H); + if (H==0){value["h"]=12;} + else if (H>12){value["h"]=H-12;} + else {value["h"]=H;} + value["hh"]=LZ(value["h"]); + if (H>11){value["K"]=H-12;} else {value["K"]=H;} + value["k"]=H+1; + value["KK"]=LZ(value["K"]); + value["kk"]=LZ(value["k"]); + if (H > 11) { value["a"]="PM"; } + else { value["a"]="AM"; } + value["m"]=m; + value["mm"]=LZ(m); + value["s"]=s; + value["ss"]=LZ(s); + value["S"]=S; + value["SS"]=LZ(S,2); + value["SSS"]=LZ(S,3); + while (i_format < format.length) { + c=format.charAt(i_format); + token=""; + while ((format.charAt(i_format)==c) && (i_format < format.length)) + token += format.charAt(i_format++); + if (value[token] != null) result += value[token]; + else result += token; + } + return result; + }, + _isInteger: function(val) { + var digits="1234567890"; + for (var i=0; i < val.length; i++) + if (digits.indexOf(val.charAt(i))==-1) return false; + return true; + }, + _getInt: function(str,i,minlength,maxlength) { + for (var x=maxlength; x>=minlength; x--) { + var token=str.substring(i,i+x); + if (token.length < minlength) return null; + if (Control.DatePicker.DateFormat._isInteger(token)) return token; + } + return null; + }, + parseFormat: function(val,format) { + var LZ = Control.DatePicker.DateFormat.LZ; + var MONTH_NAMES = Control.DatePicker.DateFormat.MONTH_NAMES; + var DAY_NAMES = Control.DatePicker.DateFormat.DAY_NAMES; + var _getInt = Control.DatePicker.DateFormat._getInt; + val=val+""; + format=format+""; + var i_val=0; + var i_format=0; + var c=""; + var token=""; + var token2=""; + var x,y; + var now=new Date(); + var year=now.getYear(); + var month=now.getMonth()+1; + var date=1; + var hh=now.getHours(); + var mm=now.getMinutes(); + var ss=now.getSeconds(); + var SS=now.getMilliseconds(); + var ampm=""; + + while (i_format < format.length) { + // Get next token from format string + c=format.charAt(i_format); + token=""; + while ((format.charAt(i_format)==c) && (i_format < format.length)) + token += format.charAt(i_format++); + // Extract contents of value based on format token + if (token=="yyyy" || token=="yy" || token=="y") { + if (token=="yyyy") x=4;y=4; + if (token=="yy") x=2;y=2; + if (token=="y") x=2;y=4; + year=_getInt(val,i_val,x,y); + if (year==null) return 0; + i_val += year.length; + if (year.length==2) { + if (year > 70) year=1900+(year-0); + else year=2000+(year-0); + } + } else if (token=="MMM"||token=="NNN") { + month=0; + for (var i=0; i11)) { + month=i+1; + if (month>12) month -= 12; + i_val += month_name.length; + break; + } + } + } + if ((month < 1)||(month>12)) return 0; + } else if (token=="EE"||token=="E") { + for (var i=0; i12)) return 0; + i_val+=month.length; + } else if (token=="dd"||token=="d") { + date=_getInt(val,i_val,token.length,2); + if(date==null||(date<1)||(date>31)) return 0; + i_val+=date.length; + } else if (token=="hh"||token=="h") { + hh=_getInt(val,i_val,token.length,2); + if(hh==null||(hh<1)||(hh>12)) return 0; + i_val+=hh.length; + } else if (token=="HH"||token=="H") { + hh=_getInt(val,i_val,token.length,2); + if(hh==null||(hh<0)||(hh>23)) return 0; + i_val+=hh.length; + } else if (token=="KK"||token=="K") { + hh=_getInt(val,i_val,token.length,2); + if(hh==null||(hh<0)||(hh>11)) return 0; + i_val+=hh.length; + } else if (token=="kk"||token=="k") { + hh=_getInt(val,i_val,token.length,2); + if(hh==null||(hh<1)||(hh>24)) return 0; + i_val+=hh.length;hh--; + } else if (token=="mm"||token=="m") { + mm=_getInt(val,i_val,token.length,2); + if(mm==null||(mm<0)||(mm>59)) return 0; + i_val+=mm.length; + } else if (token=="ss"||token=="s") { + ss=_getInt(val,i_val,token.length,2); + if(ss==null||(ss<0)||(ss>59)) return 0; + i_val+=ss.length; + } else if (token=="SS"||token=="S"||token=="SSS") { + SS=_getInt(val,i_val,token.length,3); + if(SS==null||(SS<0)||(SS>999)) return 0; + i_val+=SS.length; + } else if (token=="a") { + if (val.substring(i_val,i_val+2).toLowerCase()=="am") ampm="AM"; + else if (val.substring(i_val,i_val+2).toLowerCase()=="pm") ampm="PM"; + else return 0; + i_val+=2; + } else { + if (val.substring(i_val,i_val+token.length)!=token) return 0; + else i_val+=token.length; + } + } + // If there are any trailing characters left in the value, it doesn't match + if (i_val != val.length) return 0; + // Is date valid for month? + if (month==2) { + // Check for leap year + if (((year%4==0)&&(year%100 != 0)) || (year%400==0)) { // leap year + if (date > 29) return 0; + } else if (date > 28) { + return 0; + } + } + if ((month==4)||(month==6)||(month==9)||(month==11)) + if (date > 30) return 0; + // Correct hours value + if (hh<12 && ampm=="PM") hh=hh-0+12; + else if (hh>11 && ampm=="AM") hh-=12; + var newdate=new Date(year,month-1,date,hh,mm,ss,SS); + return newdate; + }, +/** + * Control.DatePicker.DateFormat.parse(date[, format]) -> Date + * - text (String): The text to parse. + * - format (String): The format definition to match against. + * + * Attempt to parse a string to a Date, given the specified + * format string. If `format` is omitted, DateFormat will + * try some common formats. + * + * See [[Control.DatePicker.DateFormat.format]] for formatting string details. +**/ + parse: function(val, format) { + if (format) { + return Control.DatePicker.DateFormat.parseFormat(val, format); + } else { + var generalFormats=['y-M-d','MMM d, y','MMM d,y','y-MMM-d','d-MMM-y','MMM d']; + var monthFirst=['M/d/y','M-d-y','M.d.y','MMM-d','M/d','M-d']; + var dateFirst =['d/M/y','d-M-y','d.M.y','d-MMM','d/M','d-M']; + var checkList=[generalFormats,monthFirst,dateFirst]; + var d=null; + for (var i=0; i(c.height-100)){var a=d.height-b.height;this.dialog.style.top="50px";this.dialog.style.height=(c.height-100)+"px";this.contents.style.height=(c.height-100-a)+"px";this.contents.style.overflow="auto"}else{this.dialog.style.top=((c.height-d.height)/2)*0.7+"px"}},close:function(a){if(!this.shown){return}if(this.keyListener){this.keyListener.stop();this.keyListener=null}if(Dialog.active==this){Dialog.active=null}if(typeof Effect=="undefined"){this.overlay.hide();this.dialog.hide();if(this.options.onClose){this.options.onClose()}}else{new Effect.Fade(this.overlay,{duration:0.1});new Effect.Fade(this.dialog,{duration:0.1,afterFinish:function(){if(this.options.onClose){this.options.onClose()}}.bind(this)})}if(this.options.onClose){this.options.onClose(this.contents)}if(a){Event.stop(a)}this.shown=false},success:function(a){this.close();if(this.options.onSuccess){this.options.onSuccess(a,this.contents)}},failure:function(a){this.close();if(this.options.onFailure){this.options.onFailure(a,this.contents)}}});Dialog.HTML=Class.create(Dialog.Base,{initialize:function($super,b,a){if(Object.isString(b)){$super(new Element("div").update(b),a)}else{$super(b,a)}}});Dialog.Frame=Class.create(Dialog.Base,{initialize:function($super,e,c,b){var a=Object.isString(c)?new Element("div").update(c):c;var d=new Element("div",{"class":"_pp_dialog _pp_panel"});if(b&&b.height){d.style.height=b.height+"px";delete b.height}d.appendChild(new Element("div",{"class":"_pp_title"}).update(e));d.appendChild(a);$super(d,b)}});Dialog.Ajax=Class.create(Dialog.Base,{initialize:function($super,b,a){$super(new Element("div"),a);this.url=b},show:function(){var a='
Loading...
';if(this.options.loadingIcon){a='

'+a}this.contents.update(a);new Ajax.Updater(this.contents,this.url,{method:"get",onComplete:function(b){}});this.baseShow()}});Protoplasm.register("dialog",Dialog.HTML); \ No newline at end of file diff --git a/r2/r2/public/static/protoplasm/dialog/dialog_src.js b/r2/r2/public/static/protoplasm/dialog/dialog_src.js new file mode 100644 index 00000000..ac0f3ee3 --- /dev/null +++ b/r2/r2/public/static/protoplasm/dialog/dialog_src.js @@ -0,0 +1,435 @@ +if (typeof Protoplasm == 'undefined') + throw('protoplasm.js not loaded, could not intitialize dialog'); +if (typeof Effect == 'undefined') + Protoplasm.useScriptaculous('effects'); + +/** section: Controls + * Dialog + * + * Namespace for all dialog classes. +**/ +var Dialog = { + +/** + * Dialog.active -> Dialog.Base + * + * The currently displayed dialog window. +**/ + active: null, + +/** + * Dialog.close() -> null + * + * Close the currently active dialog window. + * + * Calls `Dialog.active.close()` +**/ + close: function() { + if (Dialog.active) + Dialog.active.close(); + }, + +/** + * Dialog.success(value) -> null + * - value (Object): The success callback value + * + * Close the active dialog window, and call any onSuccess callbacks. + * + * Internally calls `Dialog.active.success(value)` +**/ + success: function() { + if (Dialog.active) + Dialog.active.success(); + }, + +/** + * Dialog.failure(value) -> null + * - value (Object): The failure callback value + * + * Close the active dialog window, and call any onFailure callbacks. + * + * Internally calls `Dialog.active.failure(value)` +**/ + failure: function() { + if (Dialog.active) + Dialog.active.failure(); + } +} + +/** + * class Dialog.Base + * + * Base class for all overlay dialogs. +**/ +Dialog.Base = Class.create({ + +/** + * new Dialog.Base(elt[, options]) + * + * Create a new dialog window, using the given element as its + * contents. + * + * Available options: + * + * * width: The dialog width in pixels + * * height: The dialog width in pixels + * * ignoreClicks: Ignore clicks outside the dialog + * (default: clicks close the dialog) + * * ignoreEsc: Ignore presses of the ESC key + * (default: ESC closes the dialog) + * * dialogClass: The class name to assign the dialog container + * * onOpen: A callback function that is called when the dialog opens + * * onClose: A callback function that is called when the dialog closes + * * onSuccess: A callback function that is called when the dialog succeeds + * * onFailure: A callback function that is called when the dialog fails +**/ + initialize: function(elt, options) { + + elt = $(elt); + + this.options = Object.extend({ + }, options || {}); + + this.createComponents(); + + this.contents = $(elt); + this.dialog.appendChild(this.contents); + + this.keyListener = null; + + // Public API + Object.extend(elt, { + show: this.show.bind(this), + hide: this.close.bind(this), + success: this.success.bind(this), + failure: this.failure.bind(this) + }); + }, + + createComponents: function() { + + if (!this.overlay) { + + this.overlay = new Element('div'); + + this.overlay.setStyle({ + 'position': 'fixed', + 'top': 0, + 'left': 0, + 'zIndex': 90, + 'width': '100%', + 'height': '100%', + 'backgroundColor': '#000', + 'display': 'none' + }); + + document.body.appendChild(this.overlay); + if (!this.options.ignoreClicks) + this.overlay.on('click', this.close.bindAsEventListener(this)); + + } + + if (!this.dialog) { + + this.dialog = new Element('div'); + + this.dialog.setStyle({ + 'position': 'fixed', + 'display': 'none', + 'zIndex': 91 + }); + + console.log(this.options.height); + if (this.options.width || this.options.height) { + this.dialog.style.overflow = 'auto'; + if (this.options.width) + this.dialog.style.width = this.options.width + 'px'; + if (this.options.height) + this.dialog.style.height = this.options.height + 'px'; + } + + if (this.options.dialogClass) + this.dialog.addClassName(this.options.dialogClass); + + document.body.appendChild(this.dialog); + + } + + }, + +/** + * Dialog.Base#baseShow() -> null + * + * Displays the dialog on the screen. Subclasses that need + * to override [[Dialog.Base#show]] should always call this + * method. +**/ + baseShow: function() { + + if (this.shown && Dialog.active == this) + return; + + Dialog.close(); + Dialog.active = this; + + if(typeof Effect == 'undefined') { + this.overlay.show(); + this.dialog.show(); + } else { + new Effect.Appear(this.overlay, { duration: 0.1, from: 0.0, to: 0.3 }); + new Effect.Appear(this.dialog, { duration: 0.1 }); + } + + this.resize(); + + this.keyListener = document.on('keydown', function(e) { + if (e.keyCode == Event.KEY_ESC) { + if (!this.options.ignoreEsc) + this.close(); + Event.stop(e); + } + if (!(Event.findElement(e, '#dialog_box') + || e.shiftKey || e.altKey + || e.metaKey || e.ctrlKey)) + Event.stop(e); + }.bindAsEventListener(this)); + + if (this.options.onOpen) + this.options.onOpen(this.contents); + + this.shown = true; + + }, + +/** alias of: Dialog.Base#baseShow + * Dialog.Base#show() -> null + * + * Display the dialog on the screen. +**/ + show: function() { + this.baseShow(); + }, + + resize: function() { + + var boxDims = Element.getDimensions(this.dialog); + var contDims = Element.getDimensions(this.contents); + var viewDims = document.viewport.getDimensions(); + + this.dialog.style.left = ((viewDims.width - boxDims.width)/2) + 'px'; + + if (boxDims.height > (viewDims.height - 100)) { + // Scroll dialog, too tall + var contDiff = boxDims.height - contDims.height; + this.dialog.style.top = '50px'; + this.dialog.style.height = (viewDims.height - 100) + 'px'; + this.contents.style.height = (viewDims.height - 100 - contDiff) + 'px'; + this.contents.style.overflow = 'auto'; + } else { + // Show dialog slightly higher than centered on the page + this.dialog.style.top = ((viewDims.height - boxDims.height)/2)*.7 + 'px'; + } + + }, + +/** + * Dialog.Base#close() -> null + * + * Close the dialog window, and call any onClose callbacks. +**/ + close: function(e) { + + if (!this.shown) + return; + + if (this.keyListener) { + this.keyListener.stop(); + this.keyListener = null; + } + + if (Dialog.active == this) + Dialog.active = null; + + if(typeof Effect == 'undefined') { + this.overlay.hide(); + this.dialog.hide(); + if (this.options.onClose) + this.options.onClose() + } else { + new Effect.Fade(this.overlay, { duration: 0.1 }); + new Effect.Fade(this.dialog, { + duration: 0.1, + afterFinish: function() { + if (this.options.onClose) + this.options.onClose() + }.bind(this)}); + } + + if (this.options.onClose) + this.options.onClose(this.contents); + + if (e) + Event.stop(e); + + this.shown = false; + + }, + +/** + * Dialog.Base#success(value) -> null + * - value (Object): The success callback value + * + * Close the dialog window, and call any onSuccess callbacks. +**/ + success: function(value) { + this.close(); + if (this.options.onSuccess) + this.options.onSuccess(value, this.contents); + }, + +/** + * Dialog.Base#failure(value) -> null + * - value (Object): The failure callback value + * + * Close the dialog window, and call any onFailure callbacks. +**/ + failure: function(value) { + this.close(); + if (this.options.onFailure) + this.options.onFailure(value, this.contents); + } + +}); + +/** + * class Dialog.HTML < Dialog.Base + * + * An overlay dialog with static HTML content. +**/ +Dialog.HTML = Class.create(Dialog.Base, { + +/** + * new Dialog.HTML(elt[, options]) + * + * Create a new dialog window, using the given element as its + * contents. + * + * Available options: + * + * * width: The dialog width in pixels + * * height: The dialog width in pixels + * * ignoreClicks: Ignore clicks outside the dialog + * (default: clicks close the dialog) + * * ignoreEsc: Ignore presses of the ESC key + * (default: ESC closes the dialog) + * * dialogClass: The class name to assign the dialog container + * * onClose: A callback function that is called when the dialog closes + * * onSuccess: A callback function that is called when the dialog succeeds + * * onFailure: A callback function that is called when the dialog fails +**/ + initialize: function($super, html, options) { + if (Object.isString(html)) + $super(new Element('div').update(html), options); + else + $super(html, options); + } + +}); + +/** + * class Dialog.Frame < Dialog.Base + * + * An overlay dialog with static HTML content, styled using + * the default window frame. +**/ +Dialog.Frame = Class.create(Dialog.Base, { + +/** + * new Dialog.Frame(elt[, options]) + * + * Create a new dialog window, using the given element or HTML + * as its contents. + * + * Available options: + * + * * width: The dialog width in pixels + * * height: The dialog width in pixels + * * ignoreClicks: Ignore clicks outside the dialog + * (default: clicks close the dialog) + * * ignoreEsc: Ignore presses of the ESC key + * (default: ESC closes the dialog) + * * dialogClass: The class name to assign the dialog container + * * onClose: A callback function that is called when the dialog closes + * * onSuccess: A callback function that is called when the dialog succeeds + * * onFailure: A callback function that is called when the dialog fails +**/ + initialize: function($super, title, html, options) { + var elt = Object.isString(html) ? new Element('div').update(html) : html; + var frame = new Element('div', { 'class': '_pp_dialog _pp_panel' }); + if (options && options.height) { + frame.style.height = options.height+'px'; + delete options.height; + } + frame.appendChild(new Element('div', { 'class': '_pp_title' }).update(title)); + frame.appendChild(elt); + $super(frame, options); + } + +}); + +/** + * class Dialog.Ajax < Dialog.Base + * + * An overlay dialog that fetches content from the server + * via AJAX calls. +**/ +Dialog.Ajax = Class.create(Dialog.Base, { + +/** + * new Dialog.Ajax(url[, options]) + * + * Create a new dialog window, loading content from the + * specified URL via XHR. Any forms in the dialog will + * be modified to submit via XHR, and the response will + * be passed back to onSuccess or onFailure callbacks. + * + * Available options: + * + * * width: The dialog width in pixels + * * height: The dialog width in pixels + * * ignoreClicks: Ignore clicks outside the dialog + * (default: clicks close the dialog) + * * ignoreEsc: Ignore presses of the ESC key + * (default: ESC closes the dialog) + * * dialogClass: The class name to assign the dialog container + * * loadingIcon: A loading icon to display while waiting for contents + * * onClose: A callback function that is called when the dialog closes + * * onSuccess: A callback function that is called when the dialog succeeds + * * onFailure: A callback function that is called when the dialog fails +**/ + initialize: function($super, url, options) { + $super(new Element('div'), options); + this.url = url; + }, + + show: function() { + + var loading = '
Loading...
'; + if (this.options.loadingIcon) + loading = '

' + loading; + this.contents.update(loading); + + new Ajax.Updater(this.contents, this.url, { + method: 'get', + onComplete: function(response) { + } + }); + + this.baseShow(); + + } + +}); + +Protoplasm.register('dialog', Dialog.HTML); diff --git a/r2/r2/public/static/protoplasm/expander/expander.js b/r2/r2/public/static/protoplasm/expander/expander.js new file mode 100644 index 00000000..ebd074cd --- /dev/null +++ b/r2/r2/public/static/protoplasm/expander/expander.js @@ -0,0 +1 @@ +if(typeof Protoplasm=="undefined"){throw ("protoplasm.js not loaded, could not intitialize expander")}if(window.Control==undefined){Control={}}Control.Expander=Class.create({initialize:function(a,b){this._initialize(a,b)},_initialize:function(a,b){this.container=$(a);this.options=Object.extend({expandedClass:null,collapsedClass:null,hoverClass:null,triggerElement:null,onexpand:Prototype.emptyFunction,oncollapse:Prototype.emptyFunction},b||{});Element.cleanWhitespace(this.container);if(this.container.childNodes.length==2){this.title=this.container.childNodes[0];this.body=this.container.childNodes[1];this.trigger=this.options.triggerElement||this.title}if(!this.options.expand){this.collapse(true)}this.applyBehavior(this.trigger,this.body)},applyBehavior:function(b,a){b.onclick=function(c){if(this.expanded){this.collapse()}else{this.expand()}}.bindAsEventListener(this);b.onmouseover=this.hover.bindAsEventListener(this);b.onmouseout=this.restore.bindAsEventListener(this);b.onmousedown=function(c){return false}.bindAsEventListener(this);b.onselectstart=function(c){return false}.bindAsEventListener(this)},expand:function(){Element.removeClassName(this.title,this.options.collapsedClass);Element.addClassName(this.title,this.options.expandedClass);this.body.style.display="block";this.expanded=true;if(this.options.onexpand){this.options.onexpand(this)}},collapse:function(a){Element.removeClassName(this.title,this.options.expandedClass);Element.addClassName(this.title,this.options.collapsedClass);this.body.style.display="none";this.expanded=false;if(this.options.oncollapse&&!a){this.options.oncollapse(this)}},hover:function(){Element.addClassName(this.title,this.options.hoverClass)},restore:function(){Element.removeClassName(this.title,this.options.hoverClass)}});Protoplasm.register("expander",Control.Expander); \ No newline at end of file diff --git a/r2/r2/public/static/protoplasm/expander/expander_src.js b/r2/r2/public/static/protoplasm/expander/expander_src.js new file mode 100644 index 00000000..f9bbb07f --- /dev/null +++ b/r2/r2/public/static/protoplasm/expander/expander_src.js @@ -0,0 +1,74 @@ +if (typeof Protoplasm == 'undefined') + throw('protoplasm.js not loaded, could not intitialize expander'); +if (window.Control == undefined) Control = {}; + +/** + * class Control.Expander + * + * Expands / hides a content panel when the header is clicked. +**/ +Control.Expander = Class.create({ + initialize: function(container, options) { + this._initialize(container, options); + }, + _initialize: function(container, options) { + this.container = $(container); + this.options = Object.extend({ + expandedClass: null, + collapsedClass: null, + hoverClass: null, + triggerElement: null, + onexpand: Prototype.emptyFunction, + oncollapse: Prototype.emptyFunction + }, options || {}); + + Element.cleanWhitespace(this.container); + if (this.container.childNodes.length == 2) { + this.title = this.container.childNodes[0]; + this.body = this.container.childNodes[1]; + this.trigger = this.options.triggerElement || this.title; + } + + if (!this.options.expand) + this.collapse(true); + + this.applyBehavior(this.trigger, this.body); + }, + applyBehavior: function(trigger, body) { + trigger.onclick = function(e) { + if(this.expanded) + this.collapse(); + else + this.expand(); + }.bindAsEventListener(this); + trigger.onmouseover = this.hover.bindAsEventListener(this); + trigger.onmouseout = this.restore.bindAsEventListener(this); + // Block text selection + trigger.onmousedown = function(e) { return false; }.bindAsEventListener(this); + trigger.onselectstart = function(e) { return false; }.bindAsEventListener(this); + }, + expand: function() { + Element.removeClassName(this.title, this.options.collapsedClass); + Element.addClassName(this.title, this.options.expandedClass); + this.body.style.display = 'block'; + this.expanded = true; + if (this.options.onexpand) + this.options.onexpand(this); + }, + collapse: function(noEvent) { + Element.removeClassName(this.title, this.options.expandedClass); + Element.addClassName(this.title, this.options.collapsedClass); + this.body.style.display = 'none'; + this.expanded = false; + if (this.options.oncollapse && !noEvent) + this.options.oncollapse(this); + }, + hover: function() { + Element.addClassName(this.title, this.options.hoverClass); + }, + restore: function() { + Element.removeClassName(this.title, this.options.hoverClass); + } +}); + +Protoplasm.register('expander', Control.Expander); diff --git a/r2/r2/public/static/protoplasm/filechooser/directory.gif b/r2/r2/public/static/protoplasm/filechooser/directory.gif new file mode 100644 index 00000000..04bda584 Binary files /dev/null and b/r2/r2/public/static/protoplasm/filechooser/directory.gif differ diff --git a/r2/r2/public/static/protoplasm/filechooser/file.gif b/r2/r2/public/static/protoplasm/filechooser/file.gif new file mode 100644 index 00000000..5a67416b Binary files /dev/null and b/r2/r2/public/static/protoplasm/filechooser/file.gif differ diff --git a/r2/r2/public/static/protoplasm/filechooser/filechooser.css b/r2/r2/public/static/protoplasm/filechooser/filechooser.css new file mode 100644 index 00000000..1f8a5cf3 --- /dev/null +++ b/r2/r2/public/static/protoplasm/filechooser/filechooser.css @@ -0,0 +1,32 @@ +/* + * Styles for FileChooser + */ + +._pp_filechooser { + padding: 3px; +} + +._pp_filechooser, +._pp_filechooser td, +._pp_filechooser input { + font-size: 11px; +} + +._pp_filechooser input[type=text] { + border: 1px solid #DDD; +} + +._pp_filechooser_directoryheader { + border: 1px solid #CCCCCC; + background-color: #FFFFFF; + padding: 2px; +} + +._pp_filechooser_preview { + background-color: #EEEEEE; +} + +._pp_filechooser_filerow { + padding: 3px; + cursor: default; +} diff --git a/r2/r2/public/static/protoplasm/filechooser/filechooser.js b/r2/r2/public/static/protoplasm/filechooser/filechooser.js new file mode 100644 index 00000000..b6365962 --- /dev/null +++ b/r2/r2/public/static/protoplasm/filechooser/filechooser.js @@ -0,0 +1 @@ +if(typeof Protoplasm=="undefined"){throw ("protoplasm.js not loaded, could not intitialize filechooser")}if(typeof Control=="undefined"){Control={}}Protoplasm.use("dialog");Protoplasm.use("upload");Protoplasm.loadStylesheet("filechooser.css","filechooser");Control.FileChooser=Class.create({initialize:function(b,d,a){this.element=$(b);if(fc=this.element.retrieve("filechooser")){fc.destroy()}this.options=a||{};var c=Protoplasm.base("filechooser");if(!this.options.icon){this.options.icon=c+"filechooser.png"}if(!this.options.fileImage){this.options.fileImage=c+"file.gif"}if(!this.options.directoryImage){this.options.directoryImage=c+"directory.gif"}if(!this.options.parentImage){this.options.parentImage=c+"parent.gif"}if(this.options.icon){this.element.style.background="url("+this.options.icon+") right center no-repeat #FFF";this.oldPadding=this.element.style.paddingRight;this.element.style.paddingRight="20px"}this.panel=new Control.FileChooser.Panel(d,Object.extend({openListener:this.fileSelected.bind(this),standalone:true,selectFile:this.element.value},this.options||{}));this.dialogOpen=false;this.dialog=this.panel.getElement();this.listeners=[this.element.on("click",this.toggle.bindAsEventListener(this)),this.element.on("blur",this.delayedHide.bindAsEventListener(this)),this.dialog.on("click",this.cancelHide.bindAsEventListener(this)),this.element.on("keydown",this.keyHandler.bindAsEventListener(this))];this.clickListener=null;this.keyListener=null;this.element.store("filechooser",this);this.destructor=Event.on(window,"unload",this.destroy.bind(this))},destroy:function(){this.hide();for(var a=0;aLoading file list...');this.selectedFile=null;var b=this.fileLister(a,this.populateFileList.bind(this));if(b){this.populateFileList(b)}},populateFileList:function(b){if(b.status=="error"){this.fileList.update('
Could not get directory contents.
');return}this.currentDirectory=b;this.entries=[];this.directoryHeader.update("Folder: "+(b.path||"/"));this.fileList.update();if(b.parent){this.entries[this.entries.length]={image:"parent",type:"directory",name:"Parent folder",path:b.parent}}if(b.files){if(b.files.constructor==Array){b.files.each(function(i){this.entries.push(i)}.bind(this))}else{this.entries.push(b.files)}}if(this.entries.length){var g=new Element("table");var a=null;g.cellSpacing=0;g.cellPadding=0;g.width="100%";g.style.border=3;this.entries.each(function(j){var i=this.createFileRow(j,g);if(this.pendingSelect&&this.pendingSelect==j.url){this.selectRow(j);a=i;this.pendingSelect=null}}.bind(this));this.fileList.appendChild(g);if(a){var f=g.offsetHeight;var c=this.fileList.offsetHeight;var d=a.offsetTop;var e=d+a.offsetHeight;if(e>c){var h=Math.round(d-(c/2));if(h+c>f){this.fileList.scrollTop=f-c}else{this.fileList.scrollTop=h}}}}else{this.fileList.update('
To add items to your folder, please click New Folder or New File below.
')}if(b.fileManager){this.createButton.disabled=false;this.uploadButton.disabled=false}else{this.createButton.disabled=true;this.uploadButton.disabled=true}},showAdvancedUploadDialog:function(a){this.showUploadDialog(a,true)},showUploadDialog:function(j,a){var b=new Element("form",{method:"post",action:this.currentDirectory.fileManager,style:"padding: 12px;width:300px;"});var l=this.currentDirectory.path||"";b.appendChild(new Element("input",{type:"hidden",name:"a",value:"upload"}));b.appendChild(new Element("input",{type:"hidden",name:"p",value:(this.currentDirectory.path||"")}));var i=new Element("div",{style:"float:left;width:60px;padding-top:3px;"}).update("Files:");b.appendChild(i);var d=new Element("input",{type:"file",name:"i",multiple:"multiple"});b.appendChild(d);b.appendChild(new Element("br"));var h=new Element("div",{style:"float:right;"});var k=new Element("input",{type:"button",value:"Close","class":"_pp_button"});k.on("click",function(m){Dialog.close();Event.stop(m)}.bindAsEventListener(this));h.appendChild(k);h.appendChild(new Element("input",{type:"submit",value:"Upload Files","class":"_pp_button"}));b.appendChild(h);b.appendChild(new Element("div",{style:"clear:both;"}));var e=false;var f=new Control.FileUpload(d,{multiple:true,inline:true,batch:true,progress:a,includeFields:["a","p"],prependPath:l,onFailure:function(){e=true},onComplete:function(){if(!e){Dialog.close()}}.bind(this)});var c=new Element("div",{"class":"_pp_dialog _pp_panel"});c.appendChild(new Element("div",{"class":"_pp_title"}).update("Upload Files"));c.appendChild(b);var g=new Dialog.HTML(c,{onClose:function(){this.refresh()}.bind(this)});g.show()},showDeleteDialog:function(){var c=this.selectedFile.type=="directory"?"Are you sure you want to PERMANENTLY delete this folder\nand all files and folders in it?":"Are you sure you want to PERMANENTLY delete this file?";if(this.selectedFile&&confirm(c)){var b=this.currentDirectory.fileManager;var a={parameters:"a=delete&p="+(this.currentDirectory.path||"")+"&f="+this.selectedFile.name,onSuccess:function(d){this.refresh(this.currentDirectory.path)}.bindAsEventListener(this),onFailure:function(d){this.refresh(this.currentDirectory.path);alert(d.responseText)}.bindAsEventListener(this)};this.filePreview.innerHTML="";this.fileList.innerHTML="Loading file list...";new Ajax.Request(b,a)}},showDirectoryCreateDialog:function(){var a=prompt("Enter a folder name:","");if(a){this.createDirectory(a)}},createDirectory:function(c){var b=this.currentDirectory.fileManager;var a={parameters:"a=createdir&p="+(this.currentDirectory.path||"")+"&d="+c,onSuccess:this.createDirectorySuccessful.bind(this),onFailure:this.createDirectoryFailed.bind(this)};this.filePreview.innerHTML="";this.fileList.innerHTML="Loading file list...";new Ajax.Request(b,a)},createDirectorySuccessful:function(a){this.refresh(this.currentDirectory.path)},createDirectoryFailed:function(a){this.refresh(this.currentDirectory.path);alert(a.responseText)},showPreview:function(b){while(this.filePreview.firstChild){Element.remove(this.filePreview.firstChild)}var c=b.substring(b.lastIndexOf(".")+1);if(!c||!$w("jpeg jpg gif png bmp").include(c.toLowerCase())){return}var d=new Element("img");var a=false;d.onload=function(j){if(!a){a=true;var l=d.width;var k=d.height;var i=this.filePreview.offsetWidth-6;var h=this.filePreview.offsetHeight-6;var g=d.width/i;var f=d.height/h;if(g>1&&g>=f){d.width=Math.floor(d.width/g);d.height=Math.floor(d.height/g)}else{if(f>1){d.width=Math.floor(d.width/f);d.height=Math.floor(d.height/f)}}d.setStyle({position:"absolute",top:Math.round((h-d.height)/2)+"px",left:Math.round((i-d.width)/2)+"px",backgroundColor:"#FFFFFF",border:1});this.filePreview.appendChild(d);if(this.options.previewListener){this.options.previewListener(l,k)}}}.bindAsEventListener(this);d.onerror=function(f){while(this.filePreview.firstChild){Element.remove(this.filePreview.firstChild)}if(this.options.previewListener){this.options.previewListener("","")}}.bindAsEventListener(this);d.src=b},createFileRow:function(b,c){var e=c.insertRow(c.rows.length);var a=e.insertCell(0);a.className="_pp_filechooser_filerow";a.width=10;a.appendChild(new Element("img",{src:this.options[(b.image||b.type)+"Image"]}));a=e.insertCell(1);a.className="_pp_filechooser_filerow";a.appendChild(new Element("div",{style:"overflow:hidden;"}).update(b.name));a=e.insertCell(2);a.className="_pp_filechooser_filerow";a.align="right";if(b.size!==undefined){var d;if(b.type=="directory"){d=b.size+" items"}else{d=this.formatFileSize(b.size)}a.innerHTML=""+d+""}else{a.innerHTML=" "}e.onmousedown=this.fileSelectListener(b);if(b.type=="file"){e.ondblclick=this.fileOpenListener(b)}else{e.ondblclick=this.directoryOpenListener(b)}e.onselectstart=function(){return false};b.element=e;return e},formatFileSize:function(a){if(!a){a=0}if(a<1024){return a+" bytes"}else{if(a<1024*1024){return this.twoDecimals(a/1024)+" KB"}else{return this.twoDecimals(a/(1024*1024))+" MB"}}},twoDecimals:function(a){return Math.round(a*100)/100},fileSelectListener:function(a){return function(b){this.selectRow(a);return false}.bindAsEventListener(this)},directoryOpenListener:function(a){return function(b){this.refresh(a.path)}.bindAsEventListener(this)},fileOpenListener:function(a){return function(b){if(this.options.openListener){this.options.openListener(a)}}.bindAsEventListener(this)},keyPressListener:function(){return function(b){if(this.element.parentNode){switch(b.keyCode){case Event.KEY_DELETE:this.showDeleteDialog();break;case Event.KEY_RETURN:if(this.selectedFile){if(this.selectedFile.type=="file"){this.fileOpenListener(this.selectedFile)()}else{this.directoryOpenListener(this.selectedFile)()}}break;case Event.KEY_UP:var a=this.selectedIndex()-1;if(a<0){a=0}this.selectRow(this.entries[a]);break;case Event.KEY_DOWN:var a=this.selectedIndex()+1;if(a>=this.entries.length){a=this.entries.length-1}this.selectRow(this.entries[a]);break;case 33:var c=Math.floor(this.fileList.offsetHeight/this.entries[0].element.offsetHeight);var a=this.selectedIndex()-c;if(a<0){a=0}this.selectRow(this.entries[a]);break;case 34:var c=Math.floor(this.fileList.offsetHeight/this.entries[0].element.offsetHeight);var a=this.selectedIndex()+c;if(a>=this.entries.length){a=this.entries.length-1}this.selectRow(this.entries[a]);break;case 35:if(this.entries){this.selectRow(this.entries[this.entries.length-1])}break;case 36:if(this.entries){this.selectRow(this.entries[0])}break;default:return}Event.stop(b)}}.bindAsEventListener(this)},selectRow:function(a){if(this.selectedFile!=a){if(this.selectedFile){Element.removeClassName(this.selectedFile.element,"_pp_highlight")}this.selectedFile=a;Element.addClassName(a.element,"_pp_highlight");if(a.url){this.showPreview(a.url);if(this.options.standalone){this.fileLocation.value=a.url}if(this.options.selectListener){this.options.selectListener(a.url)}}else{this.showPreview("");if(this.options.selectListener){this.options.selectListener("")}}}if(a.element.offsetTopthis.fileList.scrollTop+this.fileList.offsetHeight){this.fileList.scrollTop=(a.element.offsetTop+a.element.offsetHeight)-this.fileList.offsetHeight}}},selectedIndex:function(){for(index=0;indexFile + * Chooser demo +**/ +Control.FileChooser = Class.create({ + +/** + * new Control.FileChooser(element, fileHandler[, options]) + * - element (String | Element): A `` element (or DOM ID). + * - fileHandler (Function): The file lister callback. + * - options (Hash): Additional options for the control. + * + * Create a new file chooser from the given `` + * element. + * + * For details on the fileHandler specifications, see + * the online + * documentation. + * + * Additional options: + * + * * icon: The icon to display in the input box + * * fileImage: The file icon to use in the chooser + * * directoryImage: The directory icon to use in the chooser + * * parentImage: The parent directory icon to use in the chooser +**/ + initialize: function(element, fileManager, options) { + +/** + * Control.FileChooser#element -> Element + * + * The underlying `` element passed to the constructor. +**/ + this.element = $(element); + + if (fc = this.element.retrieve('filechooser')) + fc.destroy(); + + this.options = options || {}; + + // Load resources from script directory + var base = Protoplasm.base('filechooser'); + if (!this.options.icon) this.options.icon = base + 'filechooser.png'; + if (!this.options.fileImage) this.options.fileImage = base + 'file.gif'; + if (!this.options.directoryImage) this.options.directoryImage = base + 'directory.gif'; + if (!this.options.parentImage) this.options.parentImage = base + 'parent.gif'; + + if (this.options.icon) { + this.element.style.background = 'url('+this.options.icon+') right center no-repeat #FFF'; + // Prevent text writing over icon + this.oldPadding = this.element.style.paddingRight; + this.element.style.paddingRight = '20px'; + } + +/** + * Control.FileChooser#panel -> Control.FileChooser.Panel + * + * The panel dialog box linked to this control. This may be + * null if the control is not open. +**/ + this.panel = new Control.FileChooser.Panel(fileManager, Object.extend({ + openListener: this.fileSelected.bind(this), + standalone: true, + selectFile: this.element.value + }, this.options || {})); + + this.dialogOpen = false; + + this.dialog = this.panel.getElement(); + + this.listeners = [ + this.element.on('click', this.toggle.bindAsEventListener(this)), + this.element.on('blur', this.delayedHide.bindAsEventListener(this)), + this.dialog.on('click', this.cancelHide.bindAsEventListener(this)), + this.element.on('keydown', this.keyHandler.bindAsEventListener(this)) + ]; + + this.clickListener = null; + this.keyListener = null; + + this.element.store('filechooser', this); + this.destructor = Event.on(window, 'unload', this.destroy.bind(this)); + }, + +/** + * Control.FileChooser#destroy() -> null + * + * Destroy this control and return the underlying element to + * its original behavior. +**/ + destroy: function() { + this.hide(); + for (var i = 0; i < this.listeners.length; i++) + this.listeners[i].stop(); + if (this.clickListener) + this.clickListener.stop(); + if (this.keyListener) + this.keyListener.stop(); + //this.wrapper.parentNode.replaceChild(this.element, this.wrapper); + this.element.style.paddingRight = this.oldPadding; + this.element.store('filechooser', null); + this.destructor.stop(); + }, + + fileSelected: function(file) { + if (file) + this.element.value = file.url; + this.hide(); + }, + +/** + * Control.FileChooser#toggle() -> null + * + * Toggle the visibility of the file chooser panel for this control. +**/ + toggle: function() { + if (this.dialogOpen) this.hide(); + else this.show(); + }, + +/** + * Control.FileChooser#show() -> null + * + * Show the file chooser panel for this control. +**/ + show: function() { + if (!this.dialogOpen) { + var dim = Element.getDimensions(this.element); + var position = Position.cumulativeOffset(this.element); + var pickerTop = /MSIE/.test(navigator.userAgent) ? (position[1] + dim.height) + 'px' : (position[1] + dim.height - 1) + 'px'; + this.dialog.style.top = pickerTop; + this.dialog.style.left = position[0] + 'px'; + if (this.element.value) + this.panel.select(this.element.value); + else + this.panel.refresh(); + document.body.appendChild(this.dialog); + this.clickListener = document.on('click', this.documentClickHandler.bindAsEventListener(this)); + this.keyListener = document.on('keydown', this.escHandler.bindAsEventListener(this)); + this.dialogOpen = true; + } + }, + delayedHide: function(e) { + this.hideTimeout = setTimeout(this.hide, 100); + }, + cancelHide: function(e) { + if (this.hideTimeout) { + clearTimeout(this.hideTimeout); + this.hideTimeout = null; + } + }, +/** + * Control.FileChooser#hide() -> null + * + * Hide the file chooser panel for this control. +**/ + hide: function() { + if (this.dialogOpen) { + if (this.clickListener) { + this.clickListener.stop(); + this.clickListener = null; + } + if (this.keyListener) { + this.keyListener.stop(); + this.keyListener = null; + } + if (this.dialog.parentNode) + Element.remove(this.dialog); + this.dialogOpen = false; + } + }, + keyHandler: function(e) { + switch(e.keyCode) { + case Event.KEY_DOWN: + this.show(); + break; + } + }, + escHandler: function(e) { + switch(e.keyCode) { + case Event.KEY_ESC: + this.hide(); + break; + } + }, + documentClickHandler: function(e) { + var element = Event.element(e); + var abort = false; + do { + if (element == this.dialog || element == this.element + || (Dialog.active && (element == Dialog.active.contents + || element == Dialog.active.overlay))) + abort = true; + } while (element = element.parentNode); + if (!abort) + this.hide(); + } +}); + +/** + * class Control.FileChooser.Panel + * + * The dialog panel that is displayed when the file chooser is opened. +**/ +Control.FileChooser.Panel = Class.create({ + +/** + * new Control.FileChooser.Panel([options]) + * - fileHandler (Function): The file lister callback. + * - options (Hash): Additional options for the control. + * + * Create a new file chooser panel. + * + * For details on the fileHandler specifications, see + * the online + * documentation. + * + * Additional options: + * + * * icon: The icon to display in the input box + * * className: The class name for the main panel container + * * width: The panel width + * * width: The panel height + * * fileImage: The file icon to use in the chooser + * * directoryImage: The directory icon to use in the chooser + * * parentImage: The parent directory icon to use in the chooser + * * uploadHandler: The upload handler function, called when "Upload" + * is clicked +**/ + initialize: function(fileLister, options) { + this.fileLister = fileLister || Prototype.emptyFunction; + this.options = Object.extend({ + width: 360, + height: 220, + className: '', + fileImage: '/images/icons/file.gif', + directoryImage: '/images/icons/directory.gif', + parentImage: '/images/icons/parent.gif', + uploadProgress: false + }, options || {}); + + this.uploadHandler = this.options.uploadProgress + ? this.showAdvancedUploadDialog.bind(this) + : this.showUploadDialog.bind(this); +/** + * Control.FileChooser.Panel#element -> Element + * + * The root Element of this dialog panel. +**/ + this.element = this.createFileChooser(); + if (this.options.selectFile) + this.select(this.options.selectFile); + }, + + getElement: function() { + return this.element; + }, + + createFileChooser: function() { + var browser = new Element('div'); + + this.directoryHeader = new Element('div', { 'class': '_pp_filechooser_directoryheader', + 'style': 'margin-bottom:5px;'}).update(' '); + browser.appendChild(this.directoryHeader); + + var table = new Element('table'); + table.cellSpacing = 0; + table.cellPadding = 0; + table.style.border = 0; + + var row = table.insertRow(0); + + var previewHeight = this.options.height - 40; + var previewWidth = Math.round((this.options.width - 6) * 0.3); + var listHeight = this.options.height - 61; + var listWidth = this.options.width - previewWidth - 10; + + var cell = row.insertCell(0); + cell.vAlign = 'top'; + this.fileList = new Element('div', { 'class': '_pp_panel _pp_inset', + 'style': 'height:'+listHeight+'px;width'+listWidth+'px;overflow:auto;margin-right:3px;margin-bottom:5px;'}); + this.fileList.on('mousedown', function() { return false; }); + this.fileList.on('selectstart', function() { return false; }); + cell.appendChild(this.fileList); + + this.createButton = new Element('input', { 'type': 'button', 'value': 'New Folder', 'class': '_pp_button', + 'style': 'margin-right:5px;width:'+Math.round((listWidth - 10) / 3)+'px'}); + this.createButton.on('click', this.showDirectoryCreateDialog.bindAsEventListener(this)); + + this.uploadButton = new Element('input', { 'type': 'button', 'value': 'New File', 'class': '_pp_button', + 'style': 'margin-right:5px;width:'+Math.round((listWidth - 10) / 3)+'px'}); + this.uploadButton.on('click', function(e) { this.uploadHandler(this); }.bindAsEventListener(this)); + + this.deleteButton = new Element('input', { 'type': 'button', 'value': 'Delete', 'class': '_pp_button', + 'style': 'width:'+Math.round((listWidth - 10) / 3)+'px'}); + this.deleteButton.on('click', this.showDeleteDialog.bindAsEventListener(this)); + + var buttons = new Element('div'); + buttons.appendChild(this.createButton); + buttons.appendChild(this.uploadButton); + buttons.appendChild(this.deleteButton); + cell.appendChild(buttons); + + cell = row.insertCell(1); + cell.vAlign = 'top'; + this.filePreview = new Element('div', { 'class': '_pp_filechooser_preview _pp_inset', + 'style': 'height:'+previewHeight+'px;width:'+previewWidth + +'px;margin-left:3px;margin-bottom:5px;overflow:hidden;position:relative'}); + cell.appendChild(this.filePreview); + + browser.appendChild(table); + + document.on('keydown', this.keyPressListener()); + + if (this.options.standalone) { + var form = new Element('form'); + form.style.margin = 0; + + var table = new Element('table'); + table.cellSpacing = 0; + table.cellPadding = 0; + table.border = 0; + + var row = table.insertRow(0); + + var cell = row.insertCell(0); + this.fileLocation = new Element('input', {'type': 'text', 'readOnly': true, + 'style': 'width:245px;margin-right:5px'}); + cell.appendChild(this.fileLocation); + + cell = row.insertCell(1); + cell.style.textAlign = 'right'; + var input = new Element('input', { 'type': 'button', 'value': 'Cancel', 'class': '_pp_button', + 'style': 'width:50px;margin-right:5px;'}); + input.on('click', function(e) { Element.remove(this.getElement()); }.bindAsEventListener(this)); + cell.appendChild(input); + + cell = row.insertCell(2); + cell.style.textAlign = 'right'; + var input = new Element('input', { 'type': 'button', 'value': 'Select', 'class': '_pp_button', + 'style': 'width:50px;' }); + input.on('click', function(e) { + (this.options.openListener || Prototype.emptyFunction)(this.selectedFile); + }.bindAsEventListener(this)); + cell.appendChild(input); + + form.appendChild(table); + + browser.appendChild(form); + + var wrapper = new Element('div'); + wrapper.style.position = 'absolute'; + wrapper.appendChild(browser); + + wrapper.className = '_pp_frame _pp_filechooser '+this.options.className; + return wrapper; + } else { + browser.className = '_pp_frame _pp_filechooser '+this.options.className; + return browser; + } + }, + +/** + * Control.FileChooser.Panel#select(file) -> null + * - file (String): The file path to select (relative to your file manager root) + * + * Navigate to the directory containing the given file and select + * it. +**/ + select: function(path) { + this.filePreview.innerHTML = ''; + this.fileList.innerHTML = '
Loading file list...
'; + this.selectedFile = null; + var response = this.fileLister(null, this.selectByURL(path).bind(this)); + if (response) + this.selectByURL(path)(response); + }, + +/** + * Control.FileChooser.Panel#selectByUrl(url) -> null + * - file (String): The file URL to select + * + * Navigate to the directory containing the file represented by the + * given url and select it. +**/ + selectByURL: function(path) { + return function(directory) { + console.log(directory); + if (directory.status != 'error' && path && path.indexOf(directory.url) == 0) { + var relpath = path.substr(directory.url.length); + var reldir = relpath.substr(0, relpath.lastIndexOf('/')); + this.pendingSelect = path; + if (reldir != directory.path) { + this.refresh(reldir); + return; + } + } + this.populateFileList(directory); + }.bind(this); + }, + +/** + * Control.FileChooser.Panel#refresh() -> null + * + * Refresh the current directory. +**/ + refresh: function(directory) { + if (!directory && this.currentDirectory) directory = this.currentDirectory.path; + Dialog.close(); + this.filePreview.update(); + this.fileList.update('
Loading file list...
'); + this.selectedFile = null; + + var response = this.fileLister(directory, this.populateFileList.bind(this)); + // If it returned no result, it's asynchronous + if (response) + this.populateFileList(response); + }, + + populateFileList: function(directory) { + if (directory.status == 'error') { + this.fileList.update('
Could not get directory contents.
'); + return; + } + + this.currentDirectory = directory; + this.entries = []; + + this.directoryHeader.update('Folder: ' + (directory.path || '/')); + this.fileList.update(); + if (directory.parent) + this.entries[this.entries.length] = { + image: 'parent', + type: 'directory', + name: 'Parent folder', + path: directory.parent + }; + if(directory.files) { + if (directory.files.constructor == Array) + directory.files.each(function(row) { + this.entries.push(row); + }.bind(this)); + else + this.entries.push(directory.files); + } + + if (this.entries.length) { + var table = new Element('table'); + var sRow = null; + table.cellSpacing = 0; + table.cellPadding = 0; + table.width = '100%'; + table.style.border = 3; + this.entries.each(function(row) { + var cRow = this.createFileRow(row, table); + if (this.pendingSelect && this.pendingSelect == row.url) { + this.selectRow(row); + sRow = cRow; + this.pendingSelect = null; + } + }.bind(this)); + this.fileList.appendChild(table); + if (sRow) { + var tHeight = table.offsetHeight; + var cHeight = this.fileList.offsetHeight; + var rTop = sRow.offsetTop; + var rBottom = rTop + sRow.offsetHeight; + if (rBottom > cHeight) { + var idealTop = Math.round(rTop - (cHeight / 2)); + if (idealTop + cHeight > tHeight) + this.fileList.scrollTop = tHeight - cHeight; + else + this.fileList.scrollTop = idealTop; + } + } + } else { + this.fileList.update('
To add items to your folder, please click New Folder or New File below.
'); + } + + if (directory.fileManager) { + this.createButton.disabled = false; + this.uploadButton.disabled = false; + } else { + this.createButton.disabled = true; + this.uploadButton.disabled = true; + } + }, + + showAdvancedUploadDialog: function(chooser) { + this.showUploadDialog(chooser, true); + }, + + showUploadDialog: function(chooser, progress) { + + var form = new Element('form', { 'method': 'post', + 'action': this.currentDirectory.fileManager, + 'style': 'padding: 12px;width:300px;' }); + + var path = this.currentDirectory.path || ''; + form.appendChild(new Element('input', { 'type': 'hidden', 'name': 'a', + 'value': 'upload' })); + form.appendChild(new Element('input', { 'type': 'hidden', 'name': 'p', + 'value': (this.currentDirectory.path || '') })); + + var label = new Element('div', { 'style': 'float:left;width:60px;padding-top:3px;' }).update('Files:'); + form.appendChild(label); + var file = new Element('input', { 'type': 'file', 'name': 'i', 'multiple': 'multiple' }); + form.appendChild(file); + form.appendChild(new Element('br')); + var buttons = new Element('div', { 'style': 'float:right;' }); + var cancel = new Element('input', { 'type': 'button', 'value': 'Close', 'class': '_pp_button' }); + cancel.on('click', function(e) { Dialog.close(); Event.stop(e); }.bindAsEventListener(this)); + buttons.appendChild(cancel); + buttons.appendChild(new Element('input', { 'type': 'submit', 'value': 'Upload Files', 'class': '_pp_button' })); + form.appendChild(buttons); + form.appendChild(new Element('div', {'style': 'clear:both;' })); + + var failed = false; + var uploader = new Control.FileUpload(file, { + multiple: true, + inline: true, + batch: true, + progress: progress, + includeFields: ['a', 'p'], + prependPath: path, + onFailure: function() { + failed = true; + }, + onComplete: function() { + if (!failed) + Dialog.close(); + }.bind(this) + }); + + var frame = new Element('div', { 'class': '_pp_dialog _pp_panel' }); + frame.appendChild(new Element('div', { 'class': '_pp_title' }).update('Upload Files')); + frame.appendChild(form); + + var dialog = new Dialog.HTML(frame, { + onClose: function() { + this.refresh(); + }.bind(this) + }); + dialog.show(); + + }, + + showDeleteDialog: function() { + var message = this.selectedFile.type == 'directory' + ? 'Are you sure you want to PERMANENTLY delete this folder\nand all files and folders in it?' + : 'Are you sure you want to PERMANENTLY delete this file?'; + if (this.selectedFile && confirm(message)) { + var url = this.currentDirectory.fileManager; + var options = { + parameters: 'a=delete&p=' + (this.currentDirectory.path || '') + '&f=' + this.selectedFile.name, + onSuccess: function(transport) { + this.refresh(this.currentDirectory.path); + }.bindAsEventListener(this), + onFailure: function(transport) { + this.refresh(this.currentDirectory.path); + alert(transport.responseText); + }.bindAsEventListener(this) + }; + + this.filePreview.innerHTML = ''; + this.fileList.innerHTML = 'Loading file list...'; + + new Ajax.Request(url, options); + } + }, + + showDirectoryCreateDialog: function() { + var dirname = prompt('Enter a folder name:', ''); + if (dirname) + this.createDirectory(dirname); + }, + + createDirectory: function(dirname) { + var url = this.currentDirectory.fileManager; + var options = { + parameters: 'a=createdir&p=' + (this.currentDirectory.path || '') + '&d=' + dirname, + onSuccess: this.createDirectorySuccessful.bind(this), + onFailure: this.createDirectoryFailed.bind(this) + }; + + this.filePreview.innerHTML = ''; + this.fileList.innerHTML = 'Loading file list...'; + + new Ajax.Request(url, options); + }, + + createDirectorySuccessful: function(transport) { + this.refresh(this.currentDirectory.path); + }, + + createDirectoryFailed: function(transport) { + this.refresh(this.currentDirectory.path); + alert(transport.responseText); + }, + + showPreview: function(url) { + + // Clear preview pane + while(this.filePreview.firstChild) + Element.remove(this.filePreview.firstChild); + + var ext = url.substring(url.lastIndexOf('.')+1); + if (!ext || !$w('jpeg jpg gif png bmp').include(ext.toLowerCase())) + return; + + var image = new Element('img'); + var loaded = false; + + image.onload = function(e) { + // Event fires again when adding to the preview frame + if (!loaded) { + loaded = true; + + var origWidth = image.width; + var origHeight = image.height; + + // Figure out maximum dimensions and current ratios + var maxWidth = this.filePreview.offsetWidth - 6; + var maxHeight = this.filePreview.offsetHeight - 6; + var widthRatio = image.width / maxWidth; + var heightRatio = image.height / maxHeight; + + // Adjust to best fit + if (widthRatio > 1 && widthRatio >= heightRatio) { + image.width = Math.floor(image.width / widthRatio); + image.height = Math.floor(image.height / widthRatio); + } else if (heightRatio > 1) { + image.width = Math.floor(image.width / heightRatio); + image.height = Math.floor(image.height / heightRatio); + } + + // Add to preview pane + image.setStyle({ + 'position': 'absolute', + 'top': Math.round((maxHeight - image.height) / 2) + 'px', + 'left': Math.round((maxWidth - image.width) / 2) + 'px', + 'backgroundColor': '#FFFFFF', + 'border': 1}); + this.filePreview.appendChild(image); + + if (this.options.previewListener) + this.options.previewListener(origWidth, origHeight); + } + }.bindAsEventListener(this); + + image.onerror = function(e) { + // Clear preview pane + while(this.filePreview.firstChild) + Element.remove(this.filePreview.firstChild); + if (this.options.previewListener) + this.options.previewListener('', ''); + }.bindAsEventListener(this); + + image.src = url; + + }, + + createFileRow: function(record, table) { + var row = table.insertRow(table.rows.length); + + var cell = row.insertCell(0); + cell.className = '_pp_filechooser_filerow'; + cell.width = 10; + cell.appendChild(new Element('img', + {'src': this.options[(record.image || record.type) + 'Image']})); + + cell = row.insertCell(1); + cell.className = '_pp_filechooser_filerow'; + cell.appendChild(new Element('div', {'style': 'overflow:hidden;'}) + .update(record.name)); + + cell = row.insertCell(2); + cell.className = '_pp_filechooser_filerow'; + cell.align = 'right'; + + if (record.size !== undefined) { + var sizeDesc; + if (record.type == 'directory') + sizeDesc = record.size + ' items'; + else + sizeDesc= this.formatFileSize(record.size); + cell.innerHTML = '' + sizeDesc + ''; + } else { + cell.innerHTML = ' '; + } + + row.onmousedown = this.fileSelectListener(record); + if (record.type == 'file') + row.ondblclick = this.fileOpenListener(record); + else + row.ondblclick = this.directoryOpenListener(record); + row.onselectstart = function() { return false; }; + + record.element = row; + + return row; + }, + + formatFileSize: function(bytes) { + if (!bytes) bytes = 0; + if (bytes < 1024) { + return bytes + ' bytes'; + } else if (bytes < 1024*1024) { + return this.twoDecimals(bytes / 1024) + ' KB'; + } else { + return this.twoDecimals(bytes / (1024*1024)) + ' MB'; + } + }, + + twoDecimals: function(num) { + return Math.round(num * 100) / 100; + }, + + fileSelectListener: function(record) { + return function(e) { + this.selectRow(record); + return false; + }.bindAsEventListener(this); + }, + + directoryOpenListener: function(record) { + return function(e) { + this.refresh(record.path); + }.bindAsEventListener(this); + }, + + fileOpenListener: function(record) { + return function(e) { + if (this.options.openListener) + this.options.openListener(record); + }.bindAsEventListener(this); + }, + + keyPressListener: function() { + return function(e) { + if (this.element.parentNode) { + switch(e.keyCode) { + case Event.KEY_DELETE: + this.showDeleteDialog(); + break; + case Event.KEY_RETURN: + if (this.selectedFile) { + if (this.selectedFile.type == 'file') + this.fileOpenListener(this.selectedFile)(); + else + this.directoryOpenListener(this.selectedFile)(); + } + break; + case Event.KEY_UP: + var idx = this.selectedIndex() - 1; + if (idx < 0) idx = 0; + this.selectRow(this.entries[idx]); + break; + case Event.KEY_DOWN: + var idx = this.selectedIndex() + 1; + if (idx >= this.entries.length) idx = this.entries.length - 1; + this.selectRow(this.entries[idx]); + break; + case 33: // PgUp + var visibleRows = Math.floor(this.fileList.offsetHeight / this.entries[0].element.offsetHeight); + var idx = this.selectedIndex() - visibleRows; + if (idx < 0) idx = 0; + this.selectRow(this.entries[idx]); + break; + case 34: // PgDn + var visibleRows = Math.floor(this.fileList.offsetHeight / this.entries[0].element.offsetHeight); + var idx = this.selectedIndex() + visibleRows; + if (idx >= this.entries.length) idx = this.entries.length - 1; + this.selectRow(this.entries[idx]); + break; + case 35: // End + if (this.entries) + this.selectRow(this.entries[this.entries.length - 1]); + break; + case 36: // Home + if (this.entries) + this.selectRow(this.entries[0]); + break; + default: + return; + } + Event.stop(e); + } + }.bindAsEventListener(this); + }, + + selectRow: function(record) { + if (this.selectedFile != record) { + if (this.selectedFile) + Element.removeClassName(this.selectedFile.element, '_pp_highlight') + + this.selectedFile = record; + Element.addClassName(record.element, '_pp_highlight') + if (record.url) { + this.showPreview(record.url); + if (this.options.standalone) + this.fileLocation.value = record.url; + if (this.options.selectListener) + this.options.selectListener(record.url); + } else { + this.showPreview(''); + if (this.options.selectListener) + this.options.selectListener(''); + } + } + + if (record.element.offsetTop < this.fileList.scrollTop) + this.fileList.scrollTop = record.element.offsetTop; + else if (record.element.offsetTop + record.element.offsetHeight > this.fileList.scrollTop + this.fileList.offsetHeight) + this.fileList.scrollTop = (record.element.offsetTop + record.element.offsetHeight) - this.fileList.offsetHeight; + }, + + selectedIndex: function() { + for (index = 0; index < this.entries.length; ++index) + if (this.entries[index] == this.selectedFile) + return index; + return -1; + } + +}); + +Protoplasm.register('filechooser', Control.FileChooser); diff --git a/r2/r2/public/static/protoplasm/filechooser/parent.gif b/r2/r2/public/static/protoplasm/filechooser/parent.gif new file mode 100644 index 00000000..11f3f558 Binary files /dev/null and b/r2/r2/public/static/protoplasm/filechooser/parent.gif differ diff --git a/r2/r2/public/static/protoplasm/livegrid/livegrid.css b/r2/r2/public/static/protoplasm/livegrid/livegrid.css new file mode 100644 index 00000000..c7c45861 --- /dev/null +++ b/r2/r2/public/static/protoplasm/livegrid/livegrid.css @@ -0,0 +1,29 @@ +#livegrid_header { + width: 600px; + background-color: #003366; +} +#livegrid_header th { + padding: 2px; + text-align: left; + font-weight: bold; + color: #FFFFFF; +} +#livegrid { + width: 600px; +} +#livegrid tr, #livegrid tr.odd { + background-color: #EEEEEE; +} +#livegrid tr.even { + background-color: #FFFFFF; +} +#livegrid tr.selected { + background-color: #EBC2C0; +} +#livegrid td { + padding: 2px; + border-bottom: 1px solid #CCCCCC; + cursor: default; + overflow: hidden; + white-space: nowrap; +} diff --git a/r2/r2/public/static/protoplasm/livegrid/livegrid.js b/r2/r2/public/static/protoplasm/livegrid/livegrid.js new file mode 100644 index 00000000..979f6008 --- /dev/null +++ b/r2/r2/public/static/protoplasm/livegrid/livegrid.js @@ -0,0 +1 @@ +if(typeof Protoplasm=="undefined"){throw ("protoplasm.js not loaded, could not intitialize livegrid")}if(window.Control==undefined){Control={}}Protoplasm.loadStylesheet("livegrid.css","livegrid");Control.LiveGrid=Class.create({initialize:function(c,f,b,g,a){if(a==null){a={}}this.table=$(c);if(lg=this.table.retrieve("livegrid")){throw ("This table is already a Live Grid")}this.fetchHandler=g;this.sortField=a.sortField?a.sortField:null;this.sortDir=a.sortDir?a.sortDir:null;var d=this.table.rows[0].cells.length;this.metaData=new Control.LiveGrid.MetaData(f,b,d,a);this.buffer=new Control.LiveGrid.Buffer(this.metaData);this.viewPort=new Control.LiveGrid.ViewPort(this.table,this.buffer,this.metaData);this.scroller=new Control.LiveGrid.Scroller(this.viewPort,this.metaData,this.requestContentRefresh.bind(this));if(a.selectable){this.selector=new Control.LiveGrid.Selector(this.table,this.fetchHandler,this.metaData)}if(a.sortHeader){this.sorter=new Control.LiveGrid.Sort(a.sortHeader,this.metaData,this.sort.bind(this))}if(a.captureKeyEvents){this.captureKeys()}this.processingRequest=null;this.unprocessedRequest=null;if(a.prefetchBuffer||a.offset>0){var e=0;if(a.offset){e=a.offset;this.scroller.moveScroll(e);this.viewPort.scrollTo(this.scroller.rowToPixel(e))}this.requestContentRefresh(e)}this.table.store("livegrid",this)},resetContents:function(){this.scroller.unplug();this.scroller.moveScroll(0);this.scroller.plugin();this.buffer.clear();this.viewPort.clearContents()},captureKeys:function(){this.keyObserver=this.keyEvent.bindAsEventListener(this);Event.observe(window,"keypress",this.keyObserver)},releaseKeys:function(){Event.stopObserving(window,"keypress",this.keyObserver)},keyEvent:function(a){if(a.keyCode==Event.KEY_DOWN){this.moveSelection(a,1)}else{if(a.keyCode==Event.KEY_UP){this.moveSelection(a,-1)}else{if(a.keyCode==Event.KEY_RETURN&&a.shiftKey){this.selector.rowdblclickhandler()(a)}else{if(a.keyCode==33){this.moveSelection(a,-this.metaData.pageSize)}else{if(a.keyCode==34){this.moveSelection(a,this.metaData.pageSize)}}}}}},moveSelection:function(f,d){var g=this.metaData.totalRows-this.metaData.pageSize;if(g<0){g=0}if(this.selector){var b=(f.shiftKey&&this.selector.previousSelections!=null?this.selector.lastRangeSelected:this.selector.lastRowSelected);if(b==null){b=-1}if(b+d>=this.metaData.totalRows){d=(this.metaData.totalRows-b)-1}else{if(b+d<0){d=-b}}var c=this.selector.currentOffset;var a=b+d;if(a-c>=this.metaData.getPageSize()||a-c<0){var h=c+d;if(h>g){h=g;a=b+(g-h)}else{if(h<0){a=0;h=0}}this.scroller.moveScroll(h)}this.selector.rowmousedownhandler.bind(this.selector)(a-c)(f)}else{var c=parseInt(this.scroller.scrollerDiv.scrollTop/this.viewPort.rowHeight);var h=c+d;if(h>g){h=g}this.scroller.moveScroll(h)}},sort:function(b,a){this.sortField=b;this.sortDir=a;this.refresh()},refresh:function(){this.resetContents();this.requestContentRefresh(0);if(this.selector){this.selector.deselectAllRows()}},setTotalRows:function(a){this.metaData.setTotalRows(a);this.viewPort.setPageSize(this.metaData.getPageSize());this.scroller.updateSize();if(this.selector){this.selector.applyRowBehavior()}},handleTimedOut:function(){this.processingRequest=null;this.processQueuedRequest()},fetchBuffer:function(c){if(this.buffer.isInRange(c)&&!this.buffer.isNearingLimit(c)){return}if(this.processingRequest){this.unprocessedRequest=new Control.LiveGrid.Request(c);return}var b=this.buffer.getFetchOffset(c);this.processingRequest=new Control.LiveGrid.Request(c);this.processingRequest.bufferOffset=b;var a=this.buffer.getFetchSize(c);var d=false;this.fetchHandler(b,a,this.sortField,this.sortDir,this.updateData.bind(this));this.timeoutHandler=setTimeout(this.handleTimedOut.bind(this),20000)},requestContentRefresh:function(a){this.fetchBuffer(a)},updateData:function(a){try{clearTimeout(this.timeoutHandler);this.buffer.update(a,this.processingRequest.bufferOffset);this.viewPort.bufferChanged()}catch(b){}finally{this.processingRequest=null}this.processQueuedRequest()},processQueuedRequest:function(){if(this.unprocessedRequest!=null){this.requestContentRefresh(this.unprocessedRequest.requestOffset);this.unprocessedRequest=null}}});Control.LiveGrid.staticFetchHandler=function(c,b){var a=Control.LiveGrid.convertToLiveGridRows(c,b);return function(k,d,f,e,l){if(f){var h=-1;for(var g=0;g-1){a=a.sort(function(m,i){var n=m.columns[h];var o=i.columns[h];if(no){return(e.toLowerCase()=="desc"?-1:1)}}}})}}if(k>=a.length){k=a.length-1}if(k+d>a.length){d=a.length-k}l(a.slice(k,k+d))}};Control.LiveGrid.convertToLiveGridRows=function(d,c){var e=Array();if(d.length){for(var b=0;b=0;this.createScrollBar();this.scrollTimeout=null;this.rows=new Array()},isUnPlugged:function(){return this.scrollerDiv.onscroll==null},plugin:function(){this.scrollerDiv.onscroll=this.handleScroll.bindAsEventListener(this)},unplug:function(){this.scrollerDiv.onscroll=null},createScrollBar:function(){var a=this.viewPort.visibleHeight();this.scrollerDiv=document.createElement("div");var b=this.scrollerDiv.style;b.position="relative";b.left=this.isIE?"-6px":"-3px";b.width="19px";b.height=a+"px";b.overflow="auto";this.heightDiv=document.createElement("div");this.heightDiv.style.width="1px";this.heightDiv.style.height=parseInt(a*this.metaData.getTotalRows()/this.metaData.getPageSize())+"px";this.scrollerDiv.appendChild(this.heightDiv);var c=this.viewPort.table;c.parentNode.parentNode.insertBefore(this.scrollerDiv,c.parentNode.nextSibling);setTimeout(this.plugin.bind(this))},updateSize:function(){var b=this.viewPort.table;var a=this.viewPort.visibleHeight();this.scrollerDiv.style.height=a+"px";if(this.metaData.getPageSize()==0){this.heightDiv.style.height=0}else{this.heightDiv.style.height=parseInt(a*this.metaData.getTotalRows()/this.metaData.getPageSize())+"px"}},rowToPixel:function(a){return(a/this.metaData.getTotalRows())*this.heightDiv.offsetHeight},moveScroll:function(a){var b=this.metaData.totalRows==0?0:a;if(this.metaData.options.onbeforescroll){this.metaData.options.onbeforescroll(a,this.metaData.getPageSize(),this.metaData.totalRows)}this.scrollerDiv.scrollTop=this.rowToPixel(a);if(this.metaData.options.onscroll){this.metaData.options.onscroll(a,this.metaData.getPageSize(),this.metaData.totalRows)}},handleScroll:function(){if(this.scrollTimeout){clearTimeout(this.scrollTimeout)}var a=parseInt(this.scrollerDiv.scrollTop/this.viewPort.rowHeight);if(this.metaData.options.onbeforescroll){this.metaData.options.onbeforescroll(a,this.metaData.getPageSize(),this.metaData.totalRows)}this.scrollHandler(a);this.viewPort.scrollTo(this.scrollerDiv.scrollTop);if(this.metaData.options.onscroll){this.metaData.options.onscroll(a,this.metaData.getPageSize(),this.metaData.totalRows)}this.scrollTimeout=setTimeout(this.scrollIdle.bind(this),1200)},scrollIdle:function(){if(this.metaData.options.onscrollidle){this.metaData.options.onscrollidle()}}});Control.LiveGrid.Buffer=Class.create({initialize:function(a,b){this.startPos=0;this.size=0;this.metaData=a;this.rows=new Array();this.updateInProgress=false;this.viewPort=b;this.maxBufferSize=a.getLargeBufferSize()*2;this.maxFetchSize=a.getLargeBufferSize();this.lastOffset=0},getBlankRow:function(){if(!this.blankRow){this.blankRow=new Array();for(var a=0;athis.startPos){if(this.startPos+this.rows.lengththis.maxBufferSize){var c=this.rows.length;this.rows=this.rows.slice(this.rows.length-this.maxBufferSize,this.rows.length);this.startPos=this.startPos+(c-this.rows.length)}}}else{if(d+b.lengththis.maxBufferSize){this.rows=this.rows.slice(0,this.maxBufferSize)}}this.startPos=d}this.size=this.rows.length},clear:function(){this.rows=new Array();this.startPos=0;this.size=0},isOverlapping:function(b,a){return((b=this.startPos)&&(a+this.metaData.getPageSize()<=this.endPos())},isNearingTopLimit:function(a){return a-this.startPos=this.startPos){var d=this.maxFetchSize+a;if(d>this.metaData.totalRows){d=this.metaData.totalRows}b=d-a}else{var b=this.startPos-a;if(b>this.maxFetchSize){b=this.maxFetchSize}}return b},getFetchOffset:function(b){var a=b;if(b>this.startPos){a=(b>this.endPos())?b:this.endPos()}else{if(b+this.maxFetchSize>=this.startPos){var a=this.startPos-this.maxFetchSize;if(a<0){a=0}}}this.lastOffset=a;return a},getRows:function(g,e){var f=g-this.startPos;var b=f+e;if(b>this.size){b=this.size}var d=new Array();var a=0;for(var c=f;c0?this.table.offsetHeight/a:1;this.div.style.height=this.table.offsetHeight+"px";this.div.style.overflow="hidden";this.visibleRows=a>0?a+1:0;this.fillTableRows(this.table,this.visibleRows);this.setOddOrEven(this.table,0)},fillTableRows:function(d,e){if(d.rows.length||this.rowTemplate){if(!this.rowTemplate){this.rowTemplate=d.rows[0];for(var c=0;ce){d.deleteRow(d.rows.length-1)}}},setOddOrEven:function(b,c){for(var a=0;ag;var b=f?this.buffer.startPos:g;var a=(this.buffer.startPos+this.buffer.size0;this.lastRowPos=g},scrollTo:function(a){if(this.lastPixelOffset==a){return}this.refreshContents(parseInt(a/this.rowHeight));this.div.scrollTop=a%this.rowHeight;this.lastPixelOffset=a},visibleHeight:function(){return parseInt(this.div.style.height)}});Control.LiveGrid.Request=Class.create({initialize:function(b,a){this.requestOffset=b}});Control.LiveGrid.Selector=Class.create({initialize:function(b,c,a){this.table=b;this.fetchHandler=c;this.metaData=a;this.lastRowSelected=null;this.lastRangeSelected=null;this.rowIdPrefix=(a.options.rowIdPrefix?a.options.rowIdPrefix:"");this.selectedClass=(a.options.selectedClass?a.options.selectedClass:"selected");this.onrowselect=a.options.onrowselect;this.onrowopen=a.options.onrowopen;this.currentOffset=a.options.offset?a.options.offset:0;a.options.onbeforescroll=this.handleScroll.bind(this);this.selections=Array();this.previousSelections=Array();this.applyRowBehavior()},applyRowBehavior:function(){for(var a=0;ag){for(var c=this.lastRowSelected;c>=g;--c){this.selections[c]=this.table.rows[c-this.currentOffset].id}}}this.lastRangeSelected=g}else{if(f.ctrlKey){this.selections[g]=this.selections[g]?null:d;this.lastRowSelected=g;this.previousSelections=null}else{this.deselectAllRows(true);this.selections[g]=d;this.lastRowSelected=g;this.previousSelections=null}}this.redrawSelections()},copyArray:function(a){var b=new Array(a.length);for(var c=0;c   '}}},isSortable:function(a){return(!this.options.sortFields||this.options.sortFields[a])},onclick:function(b){var a=Event.element(b);while(a.tagName.toUpperCase()!="TD"&&a.tagName.toUpperCase()!="TH"){a=a.parentNode}this.setSortColumn(a);this.executeSort()},setSortColumn:function(a,b){if(a.getAttribute("sortField")==this.sortField){this.sortDirection=(this.sortDirection=="asc"?"desc":"asc")}else{this.sortField=a.getAttribute("sortField");this.sortDirection="asc"}this.refreshColumnDisplay()},refreshColumnDisplay:function(){for(var a=0;a'}else{if(this.sortDirection=="desc"){b.innerHTML='  '}else{b.innerHTML="  "}}}else{b.innerHTML="  "}}}},executeSort:function(){this.sortHandler(this.sortField,this.sortDirection)}});Protoplasm.register("livegrid",Control.LiveGrid); \ No newline at end of file diff --git a/r2/r2/public/static/protoplasm/livegrid/livegrid_src.js b/r2/r2/public/static/protoplasm/livegrid/livegrid_src.js new file mode 100644 index 00000000..390d53f4 --- /dev/null +++ b/r2/r2/public/static/protoplasm/livegrid/livegrid_src.js @@ -0,0 +1,973 @@ +if (typeof Protoplasm == 'undefined') + throw('protoplasm.js not loaded, could not intitialize livegrid'); +if (window.Control == undefined) Control = {}; + +Protoplasm.loadStylesheet('livegrid.css', 'livegrid'); + +/** + * class Control.LiveGrid + * + * Transforms a table into a dynamically updating, scrolling viewport + * that can be populated via AJAX queries. + * + * Example: Live + * Grid demo +**/ +Control.LiveGrid = Class.create({ + + initialize: function(table, visibleRows, totalRows, fetchHandler, options) { + + if (options == null) options = {}; + + this.table = $(table); + + if (lg = this.table.retrieve('livegrid')) + throw('This table is already a Live Grid'); + + this.fetchHandler = fetchHandler; + this.sortField = options.sortField ? options.sortField : null; + this.sortDir = options.sortDir ? options.sortDir : null; + + // Create core components + var columnCount = this.table.rows[0].cells.length + this.metaData = new Control.LiveGrid.MetaData(visibleRows, totalRows, columnCount, options); + this.buffer = new Control.LiveGrid.Buffer(this.metaData); + this.viewPort = new Control.LiveGrid.ViewPort(this.table, this.buffer, this.metaData); + this.scroller = new Control.LiveGrid.Scroller(this.viewPort, this.metaData, this.requestContentRefresh.bind(this)); + + // Create optional components + if (options.selectable) + this.selector = new Control.LiveGrid.Selector(this.table, this.fetchHandler, this.metaData); + if (options.sortHeader) + this.sorter = new Control.LiveGrid.Sort(options.sortHeader, this.metaData, this.sort.bind(this)); + + if (options.captureKeyEvents) this.captureKeys(); + + this.processingRequest = null; + this.unprocessedRequest = null; + + // Pre-fetch data into the buffer + if (options.prefetchBuffer || options.offset > 0) { + var offset = 0; + if (options.offset) { + offset = options.offset; + this.scroller.moveScroll(offset); + this.viewPort.scrollTo(this.scroller.rowToPixel(offset)); + } + //alert('2: requestContentRefresh'); + this.requestContentRefresh(offset); + } + + this.table.store('livegrid', this) + }, + + resetContents: function() { + this.scroller.unplug(); + this.scroller.moveScroll(0); + this.scroller.plugin(); + this.buffer.clear(); + this.viewPort.clearContents(); + }, + + captureKeys: function() { + this.keyObserver = this.keyEvent.bindAsEventListener(this); + Event.observe(window, 'keypress', this.keyObserver); + }, + + releaseKeys: function() { + Event.stopObserving(window, 'keypress', this.keyObserver); + }, + + keyEvent: function(e) { + if (e.keyCode == Event.KEY_DOWN) { + this.moveSelection(e, 1); + } else if (e.keyCode == Event.KEY_UP) { + this.moveSelection(e, -1); + } else if (e.keyCode == Event.KEY_RETURN && e.shiftKey) { + this.selector.rowdblclickhandler()(e); + } else if (e.keyCode == 33) { // PgUp + this.moveSelection(e, -this.metaData.pageSize); + } else if (e.keyCode == 34) { // PgDn + this.moveSelection(e, this.metaData.pageSize); + //} else if (e.keyCode == 36) { // Home + // this.moveSelection(e, -this.metaData.totalRows); + //} else if (e.keyCode == 35) { // End + // this.moveSelection(e, this.metaData.totalRows); + } + }, + + moveSelection: function(e, rows) { + var maxTop = this.metaData.totalRows - this.metaData.pageSize; + if (maxTop < 0) maxTop = 0; + + if (this.selector) { + var lastRow = (e.shiftKey && this.selector.previousSelections != null + ? this.selector.lastRangeSelected + : this.selector.lastRowSelected); + if (lastRow == null) lastRow = -1; + if (lastRow + rows >= this.metaData.totalRows) + rows = (this.metaData.totalRows - lastRow) - 1; + else if (lastRow + rows < 0) + rows = -lastRow; + + var currentOffset = this.selector.currentOffset; + var newRow = lastRow + rows; + + // Scroll view + if (newRow - currentOffset >= this.metaData.getPageSize() || newRow - currentOffset < 0) { + var newOffset = currentOffset + rows; + if (newOffset > maxTop) { + newOffset = maxTop; + newRow = lastRow + (maxTop - newOffset); + } else if (newOffset < 0) { + newRow = 0; + newOffset = 0; + } + this.scroller.moveScroll(newOffset); + } + this.selector.rowmousedownhandler.bind(this.selector)(newRow - currentOffset)(e); + } else { + // Just scroll view + var currentOffset = parseInt(this.scroller.scrollerDiv.scrollTop / this.viewPort.rowHeight); + var newOffset = currentOffset + rows; + if (newOffset > maxTop) newOffset = maxTop; + this.scroller.moveScroll(newOffset); + } + }, + + sort: function(field, direction) { + this.sortField = field; + this.sortDir = direction; + this.refresh(); + }, + + refresh: function() { + this.resetContents(); + //alert('3: requestContentRefresh'); + this.requestContentRefresh(0); + if (this.selector) this.selector.deselectAllRows(); + }, + + setTotalRows: function(newTotalRows) { + this.metaData.setTotalRows(newTotalRows); + this.viewPort.setPageSize(this.metaData.getPageSize()); + this.scroller.updateSize(); + if (this.selector) + this.selector.applyRowBehavior(); + }, + + handleTimedOut: function() { + //server did not respond in 4 seconds... assume that there could have been + //an error or something, and allow requests to be processed again... + this.processingRequest = null; + this.processQueuedRequest(); + }, + + fetchBuffer: function(offset) { + if (this.buffer.isInRange(offset) && + !this.buffer.isNearingLimit(offset)) { + return; + } + + if (this.processingRequest) { + this.unprocessedRequest = new Control.LiveGrid.Request(offset); + return; + } + + var bufferStartPos = this.buffer.getFetchOffset(offset); + this.processingRequest = new Control.LiveGrid.Request(offset); + this.processingRequest.bufferOffset = bufferStartPos; + var fetchSize = this.buffer.getFetchSize(offset); + var partialLoaded = false; + + this.fetchHandler(bufferStartPos, fetchSize, this.sortField, this.sortDir, this.updateData.bind(this)); + + this.timeoutHandler = setTimeout(this.handleTimedOut.bind(this), 20000); //todo: make as option + }, + + requestContentRefresh: function(contentOffset) { + this.fetchBuffer(contentOffset); + }, + + updateData: function(response) { + try { + clearTimeout(this.timeoutHandler); + this.buffer.update(response, this.processingRequest.bufferOffset); + this.viewPort.bufferChanged(); + } + catch(err) {} + finally {this.processingRequest = null; } + this.processQueuedRequest(); + }, + + processQueuedRequest: function() { + if (this.unprocessedRequest != null) { + //alert('1: requestContentRefresh'); + this.requestContentRefresh(this.unprocessedRequest.requestOffset); + this.unprocessedRequest = null; + } + } +}); + +// Helper function to allow using LiveGrid functionality with pre-loaded data +Control.LiveGrid.staticFetchHandler = function(rowdata, columns) { + var sortedRows = Control.LiveGrid.convertToLiveGridRows(rowdata, columns); + return function(offset, limit, sortField, sortDir, callback) { + // Sort is needed + if (sortField) { + var sortIndex = -1; + for (var i = 0; i < columns.length; ++i) { + if (columns[i] == sortField) { + sortIndex = i; + break; + } + } + if (sortIndex > -1) { + sortedRows = sortedRows.sort( + function(a, b) { + var ac = a.columns[sortIndex]; + var bc = b.columns[sortIndex]; + if (ac < bc) return (sortDir.toLowerCase() == 'desc' ? 1 : -1); + else if (ac == bc) return 0; + else if (ac > bc) return (sortDir.toLowerCase() == 'desc' ? -1 : 1); + } + ); + } + } + // Get sub-array + if (offset >= sortedRows.length) offset = sortedRows.length - 1; + if (offset + limit > sortedRows.length) limit = sortedRows.length - offset; + callback(sortedRows.slice(offset, offset + limit)); + }; +}; + +Control.LiveGrid.convertToLiveGridRows = function(rows, tableCols) { + var rowdata = Array(); + if (rows.length) { + for (var i = 0; i < rows.length; ++i) { + rowdata[i] = { id: rows[i].id, columns: new Array() }; + for (var j = 0; j < tableCols.length; ++j) + rowdata[i].columns[j] = rows[i][tableCols[j]]; + } + } else { + rowdata[0] = { id: rows.id, columns: new Array() }; + for (var j = 0; j < tableCols.length; ++j) + rowdata[0].columns[j] = rows[tableCols[j]]; + } + return rowdata; +}; +// Control.LiveGrid.MetaData ----------------------------------------------------- + +Control.LiveGrid.MetaData = Class.create({ + + initialize: function(pageSize, totalRows, columnCount, options) { + this.pageSize = pageSize; + this.totalRows = totalRows; + this.setOptions(options); + this.scrollArrowHeight = 16; + this.columnCount = columnCount; + }, + + setOptions: function(options) { + this.options = Object.extend({ + largeBufferSize: 7.0, // 7 pages + nearLimitFactor: 0.2 // 20% of buffer + }, options || {}); + }, + + getPageSize: function() { + return this.totalRows < this.pageSize ? this.totalRows : this.pageSize; + }, + + getTotalRows: function() { + return this.totalRows; + }, + + setTotalRows: function(n) { + this.totalRows = n; + }, + + getLargeBufferSize: function() { + return parseInt(this.options.largeBufferSize * this.pageSize); + }, + + getLimitTolerance: function() { + return parseInt(this.getLargeBufferSize() * this.options.nearLimitFactor); + } +}); + +// Control.LiveGrid.Scroller ----------------------------------------------------- + +Control.LiveGrid.Scroller = Class.create({ + + initialize: function(viewPort, metaData, scrollHandler) { + this.metaData = metaData; + this.viewPort = viewPort; + this.scrollHandler = scrollHandler; + + this.isIE = navigator.userAgent.toLowerCase().indexOf("msie") >= 0; + this.createScrollBar(); + this.scrollTimeout = null; + this.rows = new Array(); + }, + + isUnPlugged: function() { + return this.scrollerDiv.onscroll == null; + }, + + plugin: function() { + this.scrollerDiv.onscroll = this.handleScroll.bindAsEventListener(this); + }, + + unplug: function() { + this.scrollerDiv.onscroll = null; + }, + + createScrollBar: function() { + var visibleHeight = this.viewPort.visibleHeight(); + // create the outer div... + this.scrollerDiv = document.createElement("div"); + var scrollerStyle = this.scrollerDiv.style; + scrollerStyle.position = "relative"; + scrollerStyle.left = this.isIE ? "-6px" : "-3px"; + scrollerStyle.width = "19px"; + scrollerStyle.height = visibleHeight + "px"; + scrollerStyle.overflow = "auto"; + + // create the inner div... + this.heightDiv = document.createElement("div"); + this.heightDiv.style.width = "1px"; + + this.heightDiv.style.height = parseInt(visibleHeight * + this.metaData.getTotalRows()/this.metaData.getPageSize()) + "px" ; + this.scrollerDiv.appendChild(this.heightDiv); + //this.scrollerDiv.onscroll = this.handleScroll.bindAsEventListener(this); + + var table = this.viewPort.table; + table.parentNode.parentNode.insertBefore(this.scrollerDiv, table.parentNode.nextSibling); + + // Activate scrollbar (needs to be delayed to prevent immediate firing + // of onscroll event - don't ask me why) + setTimeout(this.plugin.bind(this)); + }, + + updateSize: function() { + var table = this.viewPort.table; + var visibleHeight = this.viewPort.visibleHeight(); + this.scrollerDiv.style.height = visibleHeight + 'px'; + if (this.metaData.getPageSize() == 0) + this.heightDiv.style.height = 0; + else + this.heightDiv.style.height = parseInt(visibleHeight * this.metaData.getTotalRows()/this.metaData.getPageSize()) + "px"; + }, + + rowToPixel: function(rowOffset) { + return (rowOffset / this.metaData.getTotalRows()) * this.heightDiv.offsetHeight + }, + + moveScroll: function(rowOffset) { + var offset = this.metaData.totalRows == 0 ? 0 : rowOffset; + if (this.metaData.options.onbeforescroll) + this.metaData.options.onbeforescroll(rowOffset, this.metaData.getPageSize(), this.metaData.totalRows); + this.scrollerDiv.scrollTop = this.rowToPixel(rowOffset); + if (this.metaData.options.onscroll) + this.metaData.options.onscroll(rowOffset, this.metaData.getPageSize(), this.metaData.totalRows); + }, + + handleScroll: function() { + if (this.scrollTimeout) + clearTimeout(this.scrollTimeout); + + var contentOffset = parseInt(this.scrollerDiv.scrollTop / this.viewPort.rowHeight); + + if (this.metaData.options.onbeforescroll) + this.metaData.options.onbeforescroll(contentOffset, this.metaData.getPageSize(), this.metaData.totalRows); + + //alert('4: requestContentRefresh'); + this.scrollHandler(contentOffset); + this.viewPort.scrollTo(this.scrollerDiv.scrollTop); + + if (this.metaData.options.onscroll) + this.metaData.options.onscroll(contentOffset, this.metaData.getPageSize(), this.metaData.totalRows); + + this.scrollTimeout = setTimeout(this.scrollIdle.bind(this), 1200); + }, + + scrollIdle: function() { + if (this.metaData.options.onscrollidle) + this.metaData.options.onscrollidle(); + } +}); + +// Control.LiveGrid.Buffer ----------------------------------------------------- + +Control.LiveGrid.Buffer = Class.create({ + + initialize: function(metaData, viewPort) { + this.startPos = 0; + this.size = 0; + this.metaData = metaData; + this.rows = new Array(); + this.updateInProgress = false; + this.viewPort = viewPort; + this.maxBufferSize = metaData.getLargeBufferSize() * 2; + this.maxFetchSize = metaData.getLargeBufferSize(); + this.lastOffset = 0; + }, + + getBlankRow: function() { + if (!this.blankRow) { + this.blankRow = new Array(); + for (var i=0; i < this.metaData.columnCount ; i++) + this.blankRow[i] = " "; + } + return this.blankRow; + }, + + fixRows: function(response) { + for (var i=0; i < response.length; ++i) + for (var j=0; j < response[i].columns.length; ++j) + if (!response[i].columns[j]) response[i].columns[j] = ' '; + return response; + }, + + update: function(response, start) { + var newRows = this.fixRows(response); + if (this.rows.length == 0) { // initial load + this.rows = newRows; + this.size = this.rows.length; + this.startPos = start; + return; + } + if (start > this.startPos) { //appending + if (this.startPos + this.rows.length < start) { + this.rows = newRows; + this.startPos = start;// + } else { + this.rows = this.rows.concat(newRows.slice(0, newRows.length)); + if (this.rows.length > this.maxBufferSize) { + var fullSize = this.rows.length; + this.rows = this.rows.slice(this.rows.length - this.maxBufferSize, this.rows.length) + this.startPos = this.startPos + (fullSize - this.rows.length); + } + } + } else { //prepending + if (start + newRows.length < this.startPos) { + this.rows = newRows; + } else { + this.rows = newRows.slice(0, this.startPos).concat(this.rows); + if (this.rows.length > this.maxBufferSize) + this.rows = this.rows.slice(0, this.maxBufferSize) + } + this.startPos = start; + } + this.size = this.rows.length; + }, + + clear: function() { + this.rows = new Array(); + this.startPos = 0; + this.size = 0; + }, + + isOverlapping: function(start, size) { + return ((start < this.endPos()) && (this.startPos < start + size)) || (this.endPos() == 0) + }, + + isInRange: function(position) { + return (position >= this.startPos) && (position + this.metaData.getPageSize() <= this.endPos()); + //&& this.size() != 0; + }, + + isNearingTopLimit: function(position) { + return position - this.startPos < this.metaData.getLimitTolerance(); + }, + + endPos: function() { + return this.startPos + this.rows.length; + }, + + isNearingBottomLimit: function(position) { + return this.endPos() - (position + this.metaData.getPageSize()) < this.metaData.getLimitTolerance(); + }, + + isAtTop: function() { + return this.startPos == 0; + }, + + isAtBottom: function() { + return this.endPos() == this.metaData.getTotalRows(); + }, + + isNearingLimit: function(position) { + return (!this.isAtTop() && this.isNearingTopLimit(position)) || + (!this.isAtBottom() && this.isNearingBottomLimit(position)) + }, + + getFetchSize: function(offset) { + var adjustedOffset = this.getFetchOffset(offset); + var adjustedSize = 0; + if (adjustedOffset >= this.startPos) { //apending + var endFetchOffset = this.maxFetchSize + adjustedOffset; + if (endFetchOffset > this.metaData.totalRows) + endFetchOffset = this.metaData.totalRows; + adjustedSize = endFetchOffset - adjustedOffset; + } else {//prepending + var adjustedSize = this.startPos - adjustedOffset; + if (adjustedSize > this.maxFetchSize) + adjustedSize = this.maxFetchSize; + } + return adjustedSize; + }, + + getFetchOffset: function(offset) { + var adjustedOffset = offset; + if (offset > this.startPos) //apending + adjustedOffset = (offset > this.endPos()) ? offset : this.endPos(); + else { //prepending + if (offset + this.maxFetchSize >= this.startPos) { + var adjustedOffset = this.startPos - this.maxFetchSize; + if (adjustedOffset < 0) + adjustedOffset = 0; + } + } + this.lastOffset = adjustedOffset; + return adjustedOffset; + }, + + getRows: function(start, count) { + var begPos = start - this.startPos + var endPos = begPos + count + + // er? need more data... + if (endPos > this.size) + endPos = this.size; + + var results = new Array() + var index = 0; + for (var i=begPos ; i < endPos; i++) + results[index++] = this.rows[i]; + + return results; + }, + + convertSpaces: function(s) { + return s.split(" ").join(" "); + } + +}); + + +//Control.LiveGrid.ViewPort -------------------------------------------------- +Control.LiveGrid.ViewPort = Class.create({ + + initialize: function(table, buffer, metaData) { + + this.metaData = metaData; + this.table = table; + this.buffer = buffer; + + this.setPageSize(metaData.getPageSize()); + + this.rowTemplate = null; + this.lastDisplayedStartPos = 0; + this.lastPixelOffset = 0; + this.startPos = 0; + }, + + setPageSize: function(rows) { + this.fillTableRows(this.table, rows); + + this.div = this.table.parentNode; + this.rowHeight = rows > 0 ? this.table.offsetHeight / rows : 1; + this.div.style.height = this.table.offsetHeight + 'px'; + this.div.style.overflow = "hidden"; + + // Add an extra row to simulate smooth scrolling + this.visibleRows = rows > 0 ? rows + 1 : 0; + this.fillTableRows(this.table, this.visibleRows); + this.setOddOrEven(this.table, 0); + }, + + fillTableRows: function(table, rows) { + // Make sure there's something to clone + if (table.rows.length || this.rowTemplate) { + if (!this.rowTemplate) { + this.rowTemplate = table.rows[0]; + for (var i = 0; i < this.rowTemplate.cells.length; ++i) { + if (!this.rowTemplate.cells[i].innerHTML) + // Put some data in row to force it to expand to actual height + this.rowTemplate.cells[i].innerHTML = ' '; + } + } + // Add more rows if user only provided a single template row + while (table.rows.length < rows) { + var newRow = table.insertRow(table.rows.length); + newRow.className = this.rowTemplate.className; + for (j = 0; j < this.rowTemplate.cells.length; ++j) { + var cell = newRow.insertCell(newRow.cells.length); + if (this.rowTemplate.cells[j].className) + cell.className = this.rowTemplate.cells[j].className; + if (this.rowTemplate.cells[j].width) + cell.width = this.rowTemplate.cells[j].width; + if (this.rowTemplate.cells[j].style.width) + cell.style.width = this.rowTemplate.cells[j].style.width; + // Put some data in row to force it to expand to actual height + cell.innerHTML = ' '; + } + } + while (table.rows.length > rows) { + table.deleteRow(table.rows.length - 1); + } + } + }, + + setOddOrEven: function(table, offset) { + for (var i = 0; i < table.rows.length; ++i) { + if (i % 2 == 1) { + Element.removeClassName(table.rows[i], 'even'); + Element.addClassName(table.rows[i], 'odd'); + } else { + Element.removeClassName(table.rows[i], 'odd'); + Element.addClassName(table.rows[i], 'even'); + } + } + }, + + populateRow: function(htmlRow, row) { + // Avoid overflows from passing in too long of an array + var rowLength = htmlRow.cells.length; + var columns = row.columns ? row.columns : row; + if (row.id) htmlRow.id = this.metaData.options.rowIdPrefix ? this.metaData.options.rowIdPrefix + row.id : row.id; + for (var j=0; j < rowLength; j++) { + // Make up for IE being retarded + //htmlRow.cells[j].innerHTML = '
' + columns[j] + '
'; + htmlRow.cells[j].innerHTML = columns[j]; + } + }, + + bufferChanged: function() { + this.refreshContents(parseInt(this.lastPixelOffset / this.rowHeight)); + }, + + clearRows: function() { + if (!this.isBlank) { + for (var i=0; i < this.visibleRows; i++) + this.populateRow(this.table.rows[i], this.buffer.getBlankRow()); + this.isBlank = true; + } + }, + + clearContents: function() { + this.clearRows(); + this.scrollTo(0); + this.startPos = 0; + this.lastStartPos = -1; + }, + + refreshContents: function(startPos) { + if (startPos == this.lastRowPos && !this.isPartialBlank && !this.isBlank) + return; + + if ((startPos + this.visibleRows < this.buffer.startPos) + || (this.buffer.startPos + this.buffer.size < startPos) + || (this.buffer.size == 0)) { + this.clearRows(); + return; + } + this.isBlank = false; + var viewPrecedesBuffer = this.buffer.startPos > startPos + var contentStartPos = viewPrecedesBuffer ? this.buffer.startPos: startPos; + + var contentEndPos = (this.buffer.startPos + this.buffer.size < startPos + this.visibleRows) + ? this.buffer.startPos + this.buffer.size + : startPos + this.visibleRows; + var rowSize = contentEndPos - contentStartPos; + var rows = this.buffer.getRows(contentStartPos, rowSize); + var blankSize = this.visibleRows - rowSize; + var blankOffset = viewPrecedesBuffer ? 0: rowSize; + var contentOffset = viewPrecedesBuffer ? blankSize: 0; + + // Initialize what we have + for (var i=0; i < rows.length; i++) + this.populateRow(this.table.rows[i + contentOffset], rows[i]); + + // Blank out the rest + for (var i=0; i < blankSize; i++) + this.populateRow(this.table.rows[i + blankOffset], this.buffer.getBlankRow()); + + this.isPartialBlank = blankSize > 0; + this.lastRowPos = startPos; + }, + + scrollTo: function(pixelOffset) { + if (this.lastPixelOffset == pixelOffset) + return; + + this.refreshContents(parseInt(pixelOffset / this.rowHeight)) + this.div.scrollTop = pixelOffset % this.rowHeight + + this.lastPixelOffset = pixelOffset; + }, + + visibleHeight: function() { + return parseInt(this.div.style.height); + } + +}); + +//------------- Control.LiveGrid.Request +Control.LiveGrid.Request = Class.create({ + initialize: function(requestOffset, options) { + this.requestOffset = requestOffset; + } +}); + +//------------- Control.LiveGrid.Selector +Control.LiveGrid.Selector = Class.create({ + initialize: function(table, fetchHandler, metaData) { + this.table = table; + this.fetchHandler = fetchHandler; + this.metaData = metaData; + + this.lastRowSelected = null; + this.lastRangeSelected = null; + + this.rowIdPrefix = (metaData.options.rowIdPrefix ? metaData.options.rowIdPrefix : ''); + this.selectedClass = (metaData.options.selectedClass ? metaData.options.selectedClass : 'selected'); + this.onrowselect = metaData.options.onrowselect; + this.onrowopen = metaData.options.onrowopen; + + // Set scrolling handler and initial offset + this.currentOffset = metaData.options.offset ? metaData.options.offset : 0; + metaData.options.onbeforescroll = this.handleScroll.bind(this); + this.selections = Array(); + this.previousSelections = Array(); + this.applyRowBehavior(); + }, + applyRowBehavior: function() { + // Setup row event handlers + for(var i = 0; i < this.table.rows.length; ++i) { + this.table.rows[i].onmousedown = this.rowmousedownhandler(i); + this.table.rows[i].onmousemove = this.rowmousemovehandler(i); + this.table.rows[i].onmouseup = this.rowmouseuphandler(); + this.table.rows[i].ondblclick = this.rowdblclickhandler(); + // IE text-selection fix + this.table.rows[i].onselectstart = function() { return false; }; + } + }, + /** + * Event handler factories. + */ + rowmousedownhandler: function(index) { + return function(e) { + this.dragging = true; + this.onrowclick(index, e); + if (this.onrowselect) + this.onrowselect(e, this); + // Cancel text selection in Moz + Event.stop(e); + return false; + }.bindAsEventListener(this); + }, + rowmousemovehandler: function(index) { + return function(e) { + if (this.dragging) { + this.onrowclick(index, e, true); + if (this.onrowselect) + this.onrowselect(e, this); + } + return false; + }.bindAsEventListener(this); + }, + rowmouseuphandler: function(index) { + return function(e) { + this.dragging = false; + }.bindAsEventListener(this); + }, + rowdblclickhandler: function() { + return function(e) { + if (this.onrowopen) + this.onrowopen(e, this); + // Cancel text selection in Moz + Event.stop(e); + }.bindAsEventListener(this); + }, + /** + * Event handlers to highlight table rows when clicked by + * setting the className to this.selectedClass. Selecting ranges with + * CTL and SHIFT is also supported. + */ + onrowclick: function(index, e, dragged) { + var rowId = this.table.rows[index].id; + var rowNum = this.currentOffset + index; + if (e.shiftKey || dragged) { + // Save state for reversing range selection + if (!this.previousSelections) + this.previousSelections = this.copyArray(this.selections); + // Restore state for reversing range selection + else + this.selections = this.copyArray(this.previousSelections); + + if (this.lastRowSelected < rowNum) { + for (var i = this.lastRowSelected; i <= rowNum; ++i) + this.selections[i] = this.table.rows[i - this.currentOffset].id; + } else if (this.lastRowSelected > rowNum) { + for (var i = this.lastRowSelected; i >= rowNum; --i) + this.selections[i] = this.table.rows[i - this.currentOffset].id; + } + this.lastRangeSelected = rowNum; + } else if (e.ctrlKey) { + this.selections[rowNum] = this.selections[rowNum] ? null : rowId; + this.lastRowSelected = rowNum; + this.previousSelections = null; + } else { + this.deselectAllRows(true); + this.selections[rowNum] = rowId; + this.lastRowSelected = rowNum; + this.previousSelections = null; + } + this.redrawSelections(); + }, + copyArray: function(ar) { + var ret = new Array(ar.length); + for (var i = 0; i < ar.length; ++i) + ret[i] = ar[i]; + return ret; + }, + /** + * Return the CSS ID of all rows marked by onrowclick() in the given table. + */ + selectedRows: function() { + var selected = new Array(); + var offset = (this.rowIdPrefix ? this.rowIdPrefix.length : 0); + for (var i = 0; i < this.selections.length; ++i) { + if (this.selections[i]) + selected[selected.length] = this.selections[i].substring(offset); + } + return selected; + }, + handleScroll: function(newOffset, pageSize, totalRows) { + this.currentOffset = newOffset; + this.redrawSelections(); + }, + selectAllRows: function() { + // TODO: What do we do here? Don't really want to load the whole dataset, + // but can't really rely on something like a wildcard either. + alert('Not implemented'); + if (this.onrowselect) + this.onrowselect(null, this); + }, + deselectAllRows: function(skipEvent) { + this.selections = new Array(); + this.redrawSelections(); + this.lastRowSelected = null; + this.previousSelections = null; + if (this.onrowselect) + this.onrowselect(null, this); + }, + redrawSelections: function(offset) { + offset = offset ? offset : this.currentOffset; + for (var i = 0; i < this.table.rows.length; ++i) { + if (offset + i < this.metaData.totalRows) { + if (this.selections[offset + i]) + Element.addClassName(this.table.rows[i], this.selectedClass); + else + Element.removeClassName(this.table.rows[i], this.selectedClass); + } + } + } +}); + +//-------- Control.LiveGrid.Sort +Control.LiveGrid.Sort = Class.create({ + initialize: function(table, metaData, sortHandler) { + this.table = $(table); + this.sortHandler = sortHandler; + this.setOptions(metaData.options); + this.columns = this.table.rows[0].cells; + + this.applySortBehavior(this.columns); + this.initSortColumn(); + }, + initSortColumn: function() { + var sortField = this.options.sortField; + if (sortField) { + var sortDirection = this.options.sortDirection ? this.options.sortDirection : 'asc'; + for (var i = 0; i < this.columns.length; ++i) + if (this.columns[i].getAttribute('sortField') == sortField) + this.setSortColumn(this.columns[i], sortDirection); + } + }, + setOptions: function(options) { + var base = Protoplasm.base('livegrid'); + var defaults = { + sortAscendImg: base+'sort_asc.png', + sortDescendImg: base+'sort_desc.png', + imageWidth: 9, + imageHeight: 5}; + this.options = Object.extend(defaults, options); + + // preload the images... + new Image().src = this.options.sortAscendImg; + new Image().src = this.options.sortDescendImg; + }, + applySortBehavior: function(cells) { + for (var i = 0; i < cells.length; ++i) { + if (this.isSortable(i)) { + var cellName = this.options.sortFields ? this.options.sortFields[i] : cells[i].id; + cells[i].setAttribute('sortField', cellName); + cells[i].style.cursor = 'pointer'; + cells[i].onclick = this.onclick.bindAsEventListener(this); + cells[i].innerHTML += '' + '   '; + } + } + }, + isSortable: function(column) { + return (!this.options.sortFields || this.options.sortFields[column]) + }, + onclick: function(e) { + var cell = Event.element(e); + // Find real cell when click is fired by children + while (cell.tagName.toUpperCase() != 'TD' && cell.tagName.toUpperCase() != 'TH') + cell = cell.parentNode; + this.setSortColumn(cell); + this.executeSort(); + }, + setSortColumn: function(cell, direction) { + if (cell.getAttribute('sortField') == this.sortField) { + this.sortDirection = (this.sortDirection == 'asc' ? 'desc' : 'asc'); + } else { + this.sortField = cell.getAttribute('sortField'); + this.sortDirection = 'asc'; + } + this.refreshColumnDisplay(); + }, + refreshColumnDisplay: function() { + for (var i = 0; i < this.columns.length; ++i) { + if (this.columns[i].onclick) { + var image = $(this.columns[i].getAttribute('sortField') + '_img'); + if (this.columns[i].getAttribute('sortField') == this.sortField) { + if (this.sortDirection == 'asc') { + image.innerHTML = '  '; + } else if (this.sortDirection == 'desc') { + image.innerHTML = '  '; + } else { + image.innerHTML = '  '; + } + } else { + image.innerHTML = '  '; + } + } + } + }, + executeSort: function() { + this.sortHandler(this.sortField, this.sortDirection); + } +}); + +Protoplasm.register('livegrid', Control.LiveGrid); diff --git a/r2/r2/public/static/protoplasm/livegrid/sort_asc.png b/r2/r2/public/static/protoplasm/livegrid/sort_asc.png new file mode 100644 index 00000000..cbe948b6 Binary files /dev/null and b/r2/r2/public/static/protoplasm/livegrid/sort_asc.png differ diff --git a/r2/r2/public/static/protoplasm/livegrid/sort_desc.png b/r2/r2/public/static/protoplasm/livegrid/sort_desc.png new file mode 100644 index 00000000..f9087f28 Binary files /dev/null and b/r2/r2/public/static/protoplasm/livegrid/sort_desc.png differ diff --git a/r2/r2/public/static/protoplasm/protoplasm.css b/r2/r2/public/static/protoplasm/protoplasm.css new file mode 100644 index 00000000..fb4c0c96 --- /dev/null +++ b/r2/r2/public/static/protoplasm/protoplasm.css @@ -0,0 +1,122 @@ +._pp_frame { + border: 1px solid #999; + background-color: #EEE; + padding: 3px; +} + +._pp_frame_small { + border: 1px solid #999; + background-color: #EEE; + padding: 2px; + font-size: 11px; + border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; +} +._pp_frame_small input, +._pp_frame_small button, +._pp_frame_small select, +._pp_frame_small td, { + font-size: 11px; +} + +._pp_flush { + padding: 0; +} + +._pp_dialog { + border: 2px solid #DDD; + background-color: #EEE; +} + +._pp_title { + background-color: #4E799B; + color: #FFF; + padding: 3px; + font-weight: bold; +} + +._pp_heading { + background-color: #FFF; + padding: 3px; + font-weight: bold; + border: 1px solid #999; +} + +._pp_panel { + background-color: #FFF; +} + +._pp_button { + border: 1px solid #999; + border-radius: 4px; + padding: 2px 8px; + background-color: #DDD; + cursor: pointer; + text-decoration: none; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; +} +._pp_button:hover { + background-color: #EEE; +} + +._pp_disabled { + color: #999; +} + +hr._pp_line { + height: 1px; + background-color: #999; + border: 0; + margin: 0; + padding: 0; +} + +._pp_highlight { + background-color: #B4CEFF; +} + +._pp_small_text, +._pp_small_text input, +._pp_small_text select { + font-size: 10px; +} + +._pp_inset { + border-top: 1px solid #CCC; + border-left: 1px solid #CCC; + border-bottom: 1px solid #FFF; + border-right: 1px solid #FFF; +} + +._pp_outset { + border-top: 1px solid #FFF; + border-left: 1px solid #FFF; + border-bottom: 1px solid #CCC; + border-right: 1px solid #CCC; +} + +._pp_focused { + border: 1px dotted #999; +} + +input[type=text]._pp_input, +textarea._pp_input { +} + +table._pp_table { + border-collapse: collapse; +} + +table._pp_table th { +} + +table._pp_table td { +} + +table._pp_table td.odd { +} + +table._pp_table td.even { +} diff --git a/r2/r2/public/static/protoplasm/protoplasm.js b/r2/r2/public/static/protoplasm/protoplasm.js new file mode 100644 index 00000000..59db58bf --- /dev/null +++ b/r2/r2/public/static/protoplasm/protoplasm.js @@ -0,0 +1 @@ +if(typeof Protoplasm=="undefined"){var Protoplasm=function(){var b="1.7.0.0";var g="1.8.3";var h="https://ajax.googleapis.com/ajax/libs/prototype/"+b+"/prototype.js";var d="https://ajax.googleapis.com/ajax/libs/scriptaculous/"+g+"/";var e=source=loaded=failed=used=false;var i;var c=[];var a=[];var f={};return{Version:"0.1",base:function(j){return i+j+"/"},load:function(){function j(l){var m=l.replace(/_.*|\./g,"");m=parseInt(m+"0".times(4-m.length));return l.indexOf("_")>-1?m-1:m}failed=false;if(typeof Prototype=="undefined"){Protoplasm.require(h,Protoplasm.load);return}else{if(j(Prototype.Version)= "+b)}}var k=/protoplasm(_[a-z]*)?\.js(\?.*)?$/;$$("head script[src]").findAll(function(l){return l.src.match(k)}).each(function(m){var n=m.src.match(k),l=m.src.match(/\?.*load=([a-z,]*)/);i=m.src.replace(k,"");loaded=true;if(n[1]=="_full"){e=true;Protoplasm.loadStylesheet(i+"protoplasm_full.css");return}if(n[1]=="_src"){source=true}var o=(l?l[1].split(","):a);if(o){o.each(Protoplasm.use)}})},loadStylesheet:function(k,l){if(l){k=e?i+"/protoplasm_full.css":i+l+"/"+k}if(!$$("head link[rel=stylesheet]").find(function(m){return m.href==k})){var j=new Element("link",{rel:"stylesheet",type:"text/css",href:k});$$("head")[0].appendChild(j)}},register:function(k,j){f[k]=j},require:function(m,o){if(c.indexOf(m)>-1){if(o){o()}return}c.push(m);try{if(document.loaded){throw ("Already loaded")}document.write(' + %if label: + + %endif %if tabular: diff --git a/r2/r2/templates/comment.html b/r2/r2/templates/comment.html index 4feadbc5..206d0099 100644 --- a/r2/r2/templates/comment.html +++ b/r2/r2/templates/comment.html @@ -6,23 +6,23 @@ ## software over a computer network and provide for limited attribution for the ## Original Developer. In addition, Exhibit A has been modified to be consistent ## with Exhibit B. -## +## ## Software distributed under the License is distributed on an "AS IS" basis, ## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for ## the specific language governing rights and limitations under the License. -## +## ## The Original Code is Reddit. -## +## ## The Original Developer is the Initial Developer. The Initial Developer of ## the Original Code is CondeNet, Inc. -## +## ## All portions of the code written by CondeNet are Copyright (c) 2006-2008 ## CondeNet, Inc. All Rights Reserved. ################################################################################ <%! - from r2.lib.filters import edit_comment_filter, unsafe, safemarkdown - from r2.lib.utils import to36, prettytime + from r2.lib.filters import edit_comment_filter, unsafe, safemarkdown, clean_comment_html, unsafe_wrap_md, format_linebreaks + from r2.lib.utils import to36, prettytime, epochtime %> <%inherit file="comment_skeleton.html"/> @@ -38,7 +38,7 @@ ${parent.ParentDiv()} %if c.profilepage or c.full_comment_listing: - %if thing.link: + %if thing.link: %if thing.link.title: ${_('In response to')} %if thing.parent_permalink and thing.parent_author: @@ -73,7 +73,7 @@ ${_("comment score below threshold")} %else: ##${thing.timesince} ${_("ago")} - + ${prettytime(thing._date, seconds = True)} %if thing.editted: *  @@ -91,7 +91,7 @@ %endif [${_("+") if collapse else _("-")}] %if collapse: - (${thing.num_children} + (${thing.num_children} ${ungettext("child", "children", thing.num_children)}) %endif @@ -104,7 +104,11 @@ <%def name="commentBody()"> %if not thing.deleted: -${unsafe(safemarkdown(thing.body))} + %if thing.is_html: + ${unsafe_wrap_md(clean_comment_html(format_linebreaks(thing.body)))} + %else: + ${unsafe(safemarkdown(thing.body))} + %endif %endif @@ -121,42 +125,50 @@ <%def name="midcol()"> -<%def name="buttons()"> +<%def name="buttons(parent_link = False)"> %if not thing.deleted: -
    -
  • ${self.arrow(thing, 1, thing.likes)}
  • -
  • ${self.arrow(thing, 0, thing.likes == False)}
  • + %if thing.votable and (not c.profilepage): +
      +
    • ${self.arrow(thing, 1, thing.likes)}
    • +
    • ${self.arrow(thing, 0, thing.likes == False)}
    • +
    + %endif <% fullname = thing._fullname %> -
  • - ${parent.comment_button("permalink", fullname, _("Permalink"), 0, - thing.permalink)} -
  • - %if not c.profilepage: - %if thing.parent_permalink: -
  • - ${bylink_button("parent", _("Parent"), thing.parent_permalink)} -
  • - %endif - %if c.user_is_loggedin and thing.author.name == c.user.name and not c.full_comment_listing: -
  • - ${parent.simple_button("edit", fullname, _("Edit"), "editcomment")} +
      + %if not c.profilepage and thing.can_reply: + %if thing.parent_permalink and parent_link: +
    • + ${bylink_button("parent", _("Parent"), thing.parent_permalink, tool_tip=_("Parent"))} +
    • + %endif +
    • + ${parent.simple_button("reply", fullname, _("Reply"), "reply", tool_tip=_("Reply"))}
    • %endif - %endif - ${parent.delete_or_report_buttons()} - ${parent.buttons()} - %if not c.profilepage and thing.can_reply: -
    • - ${parent.simple_button("reply", fullname, _("Reply"), "reply")} +
    • - %endif -
    + %if not c.profilepage: + %if c.user_is_loggedin and thing.author.name == c.user.name: +
  • + ${parent.simple_button("edit", fullname, _("Edit"), "editcomment", tool_tip=_("Edit"))} +
  • + %endif + %endif + <% allow_delete = thing.can_be_deleted %> + ${parent.delete_or_report_buttons(allow_delete,False,True)} + ${parent.buttons()} +
+ ## Each comment has a hidden status span that is used to indicate voting errors. + %endif ${self.admintagline()} -<%def name="bylink_button(name, title, link)"> - ${title} +<%def name="bylink_button(name, title, link, tool_tip=None)"> + ${title} diff --git a/r2/r2/templates/comment.xml b/r2/r2/templates/comment.xml index be765884..29d84231 100644 --- a/r2/r2/templates/comment.xml +++ b/r2/r2/templates/comment.xml @@ -21,14 +21,25 @@ ################################################################################ <%! - from r2.lib.filters import safemarkdown + from r2.lib.filters import safemarkdown, clean_comment_html, format_linebreaks + from r2.lib.template_helpers import add_sr +%> +<% + url = add_sr(thing.permalink, force_hostname = True) %> ${thing.author.name} ${_("on")} ${thing.link.title} - ${thing.permalink} + ${url} + ${url} ${thing._date.isoformat()} - ${safemarkdown(thing.body)} + + %if thing.is_html: + ${clean_comment_html(format_linebreaks(thing.body))} + %else: + ${safemarkdown(thing.body)} + %endif + ${hasattr(thing, "child") and thing.child.render() or ''} diff --git a/r2/r2/templates/comment_skeleton.html b/r2/r2/templates/comment_skeleton.html index ef20dced..63b80a07 100644 --- a/r2/r2/templates/comment_skeleton.html +++ b/r2/r2/templates/comment_skeleton.html @@ -6,16 +6,16 @@ ## software over a computer network and provide for limited attribution for the ## Original Developer. In addition, Exhibit A has been modified to be consistent ## with Exhibit B. -## +## ## Software distributed under the License is distributed on an "AS IS" basis, ## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for ## the specific language governing rights and limitations under the License. -## +## ## The Original Code is Reddit. -## +## ## The Original Developer is the Initial Developer. The Initial Developer of ## the Original Code is CondeNet, Inc. -## +## ## All portions of the code written by CondeNet are Copyright (c) 2006-2008 ## CondeNet, Inc. All Rights Reserved. ################################################################################ @@ -47,7 +47,9 @@ <%def name="entry()"> <% fullname = thing._fullname %> <% collapse = thing.collapsed %> - <% focal = (c.focal_comment and c.focal_comment == thing._id36) and 'focal' or '' %> + <% retractedClass = "retracted" if getattr(thing, "retracted", False) else "" %> + <% isFocal = (c.focal_comment and c.focal_comment == thing._id36) %> + <% focal = isFocal and 'focal' or '' %> ${thing.content.render()} \ No newline at end of file diff --git a/r2/r2/templates/commentlisting.xml b/r2/r2/templates/commentlisting.xml index e69de29b..d4fdd229 100644 --- a/r2/r2/templates/commentlisting.xml +++ b/r2/r2/templates/commentlisting.xml @@ -0,0 +1 @@ +${thing.content.render()} diff --git a/r2/r2/templates/commentpermalink.html b/r2/r2/templates/commentpermalink.html new file mode 100644 index 00000000..16ac8149 --- /dev/null +++ b/r2/r2/templates/commentpermalink.html @@ -0,0 +1,22 @@ +<%inherit file="link.html"/> + +<%def name="article()"> + ${self.placeholder()} + + +<%def name="summary()"> + ${self.placeholder()} + + +<%def name="placeholder()"> +

You are viewing a comment permalink. View the original post to see all + comments and the full post content.

+ + +<%def name="title()"> + %if hasattr(thing, 'link_title'): + ${thing.link_title} + %else: + ${parent.title()} + %endif + \ No newline at end of file diff --git a/r2/r2/templates/commentreplybox.html b/r2/r2/templates/commentreplybox.html index ec964fe5..584adadd 100644 --- a/r2/r2/templates/commentreplybox.html +++ b/r2/r2/templates/commentreplybox.html @@ -19,10 +19,15 @@ ## All portions of the code written by CondeNet are Copyright (c) 2006-2008 ## CondeNet, Inc. All Rights Reserved. ################################################################################ +<%! + from r2.lib.template_helpers import get_domain, static +%> <%namespace file="utils.html" import="error_field"/> %if not thing.link_name: -%endif diff --git a/r2/r2/templates/edit.html b/r2/r2/templates/edit.html new file mode 100644 index 00000000..a4319604 --- /dev/null +++ b/r2/r2/templates/edit.html @@ -0,0 +1,35 @@ + +<%! +from r2.lib.utils import prettytime +from r2.models import Edit + %> + +<%namespace file="utils.html" import="separator,plain_link"/> +<%inherit file="printable.html"/> + + + +<%def name="entry()"> +<% fullname = thing._fullname %> +
+
+

${thing.link.title}

+
+ Edited by ${unsafe(self.author())} + (${unsafe(self.author(author=thing.link_author))}) + ${prettytime(thing._date)} +
+
+
+%for l in thing.diff[2:]:
+<% style = Edit.diff_line_style(l) %>
${l}
+%endfor +
+
+
+
+ + +## disable voting arrows +<%def name="midcol()"> + diff --git a/r2/r2/templates/edit.xml b/r2/r2/templates/edit.xml new file mode 100644 index 00000000..d29e5235 --- /dev/null +++ b/r2/r2/templates/edit.xml @@ -0,0 +1,21 @@ +<%! + from r2.lib.template_helpers import add_sr, get_domain + from r2.lib.utils import rfc822format + from xml.sax import saxutils +%> +<% url = add_sr(thing.permalink, force_hostname = True) %> + + ${thing.link.title} + ${url} + ${thing._fullname} + ${rfc822format(thing._date)} + + <% domain = get_domain(cname = c.cname, subreddit = False) %> + Edited by <a href="http://${domain}/user/${thing.link_author.name}">${thing.author.name}</a> + Original by <a href="http://${domain}/user/${thing.link_author.name}">${thing.link_author.name}</a> + <% entities = {"\n": " "} %> + <pre> + ${saxutils.escape("\n".join(thing.diff[2:]), entities)} + </pre> + + diff --git a/r2/r2/templates/editmeetup.html b/r2/r2/templates/editmeetup.html new file mode 100644 index 00000000..d8ad62f5 --- /dev/null +++ b/r2/r2/templates/editmeetup.html @@ -0,0 +1,15 @@ +<%! + from r2.lib.template_helpers import static + from r2.lib.utils import usformat + from routes.util import url_for +%> +<%namespace name="utils" file="utils.html" import="error_field, submit_form"/> +<%namespace name="form" file="newmeetup.html" import="form_fields"/> + +

Edit Meetup

+ +<%utils:submit_form action="/meetups/${thing._id36}/update" onsubmit="return post_form(this, 'meetups/${thing._id36}/update', null, null, true, '/')" _id="newmeetup" _class="meetup" tzoffset="${thing.tzoffset}" latitude="${thing.latitude}" longitude="${thing.longitude}"> + ${form_fields()} + + + diff --git a/r2/r2/templates/featuredarticles.html b/r2/r2/templates/featuredarticles.html new file mode 100644 index 00000000..ec96a6f0 --- /dev/null +++ b/r2/r2/templates/featuredarticles.html @@ -0,0 +1,13 @@ +<%namespace file="utils.html" import="plain_link"/> +<% + article_generator = thing.things +%> +%for a in article_generator: + +%endfor: diff --git a/r2/r2/templates/feedlinkbar.html b/r2/r2/templates/feedlinkbar.html index 31528b1d..2397de98 100644 --- a/r2/r2/templates/feedlinkbar.html +++ b/r2/r2/templates/feedlinkbar.html @@ -2,10 +2,13 @@ from r2.lib.template_helpers import static, add_sr, join_urls %> diff --git a/r2/r2/templates/googlesearchform.html b/r2/r2/templates/googlesearchform.html index 56729275..49a1b633 100644 --- a/r2/r2/templates/googlesearchform.html +++ b/r2/r2/templates/googlesearchform.html @@ -4,8 +4,8 @@ - - + + diff --git a/r2/r2/templates/imagebrowser.html b/r2/r2/templates/imagebrowser.html index 6fd2f86e..a8e36bcd 100644 --- a/r2/r2/templates/imagebrowser.html +++ b/r2/r2/templates/imagebrowser.html @@ -25,18 +25,19 @@ <%def name="javascript()"> <% from r2.lib.template_helpers import static %> + %if g.uncompressedJS: - + %else: - + %endif @@ -244,4 +245,4 @@

Article not saved

You must save your article before you can upload an image for it.

%endif - \ No newline at end of file + diff --git a/r2/r2/templates/infobar.html b/r2/r2/templates/infobar.html index 1ea5244d..c01a00ff 100644 --- a/r2/r2/templates/infobar.html +++ b/r2/r2/templates/infobar.html @@ -21,6 +21,6 @@ ################################################################################ <%! - from r2.lib.filters import safemarkdown + from r2.lib.filters import safemarkdown, _force_unicode %> -
${unsafe(safemarkdown(_(thing.message)))}
+
${unsafe(safemarkdown(_(_force_unicode(thing.message))))}
diff --git a/r2/r2/templates/inlinearticle.html b/r2/r2/templates/inlinearticle.html index fd3f9c62..fd99bbb2 100644 --- a/r2/r2/templates/inlinearticle.html +++ b/r2/r2/templates/inlinearticle.html @@ -48,7 +48,8 @@

by - ${thing.author.name} | - ${thing.score} + ${thing.author.name} | + ${thing.score}v (${thing.num_comments}c) +
diff --git a/r2/r2/templates/inlinearticle.xml b/r2/r2/templates/inlinearticle.xml index bd420776..7f5e0c3a 100644 --- a/r2/r2/templates/inlinearticle.xml +++ b/r2/r2/templates/inlinearticle.xml @@ -24,7 +24,6 @@ from pylons.i18n import _, ungettext from r2.lib.template_helpers import add_sr, get_domain from r2.models import FakeSubreddit - from r2.lib.filters import safehtml from r2.lib.utils import rfc822format %> <% diff --git a/r2/r2/templates/inlinecomment.html b/r2/r2/templates/inlinecomment.html index 1a352898..53c5bd15 100644 --- a/r2/r2/templates/inlinecomment.html +++ b/r2/r2/templates/inlinecomment.html @@ -1,10 +1,10 @@ <%! from pylons.i18n import _, ungettext - from r2.lib.filters import safemarkdown + from r2.lib.filters import killhtml from r2.lib.template_helpers import get_domain %> -

${thing.body}

+

${killhtml(thing.body)}

by ${thing.author.name} diff --git a/r2/r2/templates/invalidate_wikipagecache.html b/r2/r2/templates/invalidate_wikipagecache.html new file mode 100644 index 00000000..2a2f2a47 --- /dev/null +++ b/r2/r2/templates/invalidate_wikipagecache.html @@ -0,0 +1,6 @@ +<%def name="invalidate_link(name,skiplayout)"> +
+ + +
+ \ No newline at end of file diff --git a/r2/r2/templates/link.html b/r2/r2/templates/link.html index 0b5f8492..4162f920 100644 --- a/r2/r2/templates/link.html +++ b/r2/r2/templates/link.html @@ -23,8 +23,8 @@ <%! from r2.models.subreddit import Default from r2.lib.template_helpers import get_domain, static - from r2.lib.filters import safemarkdown, safehtml - from r2.lib.utils import prettytime + from r2.lib.filters import safemarkdown, cleanhtml + from r2.lib.utils import prettytime, epochtime from r2.lib.strings import strings %> @@ -43,11 +43,13 @@ else: full_article = False heading_size = 2 + css_class += ' list' fullname = thing._fullname %> +
- ${thing.title} + ${self.title()}${unsafe(self.draft())}
%if thing.hide_score: @@ -58,6 +60,15 @@ ${unsafe(self.author(friend = thing.friend))} ${prettytime(thing._date)}
+##{_RL + +##}_RL
## The div below is a hack to get the space compressor to leave whitespace in articles alone
@@ -68,8 +79,20 @@ %endif
+ %if full_article: + + %endif
- ${parent.midcol()} + ${parent.midcol(thing.votable)} + %if c.user_is_loggedin and (not c.profilepage): + + %endif %if thing.comments_enabled: <% num_comments = 0 if not thing.num_comments else thing.num_comments @@ -78,7 +101,7 @@ %endif
    - ${self.buttons(comments = False)} + ${self.buttons(comments = False, report=False)}
%if full_article: <% tags = thing.get_tags() %> @@ -90,25 +113,31 @@ %endfor
%endif - %endif
+ ## Each link has a hidden status span that is used to indicate voting errors. +
+ + + +<%def name="title()"> + ${thing.title} + + +<%def name="draft()"> + %if thing.draft: + Draft + %endif <%def name="article()"> - ${unsafe(safehtml(thing.article))} - + ${unsafe(cleanhtml(thing.article))} <%def name="summary()"> - ${unsafe(safehtml(thing._summary()))} + ${unsafe(cleanhtml(thing._summary()))} % if thing._has_more(): <%call expr="make_link('read_more', 'more', 'more')"> continue reading » @@ -166,7 +195,7 @@ % if thing.render_full:
- ${unsafe(safehtml(thing.article))} + ${unsafe(cleanhtml(thing.article))}
<% tags = thing.get_tags() %> %if len(tags): @@ -178,16 +207,10 @@ %endif - %else: % if hasattr(thing, '_summary'):
- ${unsafe(safehtml(thing._summary()))} + ${unsafe(cleanhtml(thing._summary()))}
% if thing._has_more(): <%call expr="make_link('read_more', 'md', 'more')"> @@ -265,7 +288,8 @@ %if c.user_is_loggedin: %if thing.can_submit(c.user):
  • - ${plain_link("Edit", "/edit/%s" % thing._id36)} + <% kw = {"class": "edit", "title": "Edit"} %> + ${plain_link("Edit", "/edit/%s" % thing._id36, **kw)}
  • %endif %if c.user_is_admin: @@ -280,22 +304,19 @@ %endif
  • - %if thing.saved: - ${parent.state_button("unsave", fullname, _("Unsave"), \ - "return change_state(this, 'unsave');", _("Unsaved"))} - %else: - ${parent.state_button("save", fullname, _("Save"), \ - "return change_state(this, 'save');", _("Saved"))} - %endif + ${parent.state_button("save", fullname, _("Save"), \ + "return change_state_by_class(this, '%s', 'mod')"%("unsave" if thing.saved else "save"), \ + _("Saved"), a_class="save"+(" mod" if thing.saved else ""), \ + tool_tip = "Unsave" if thing.saved else "Save")}
  • %if not thing.render_full:
  • %if thing.hidden: ${parent.state_button("unhide", fullname, _("Unhide"), \ - "return change_w_callback(this, $ListClass.unhide);", _("Unhidden"))} + "return change_w_callback(this, $ListClass.unhide);", _("Unhidden"), a_class="hide mod", tool_tip="Unhide")} %else: ${parent.state_button("hide", fullname, _("Hide"), \ - "return change_w_callback(this, $ListClass.hide);", _("Hidden"))} + "return change_w_callback(this, $ListClass.hide);", _("Hidden"), a_class="hide", tool_tip="Hide")} %endif
  • %endif diff --git a/r2/r2/templates/link.xml b/r2/r2/templates/link.xml index 0e4c9537..276e330e 100644 --- a/r2/r2/templates/link.xml +++ b/r2/r2/templates/link.xml @@ -24,7 +24,7 @@ from pylons.i18n import _, ungettext from r2.lib.template_helpers import add_sr, get_domain from r2.models import FakeSubreddit - from r2.lib.filters import safehtml + from r2.lib.filters import cleanhtml from r2.lib.utils import rfc822format %> <% @@ -42,7 +42,7 @@ Submitted by <a href="http://${domain}/user/${thing.author.name}">${thing.author.name}</a> <a href="${url}#comments">${thing.num_comments} ${com_label}</a> %if hasattr(thing, 'article'): - ${safehtml(thing.article)} + ${cleanhtml(thing.article)} %endif %if use_thumbs: diff --git a/r2/r2/templates/login.html b/r2/r2/templates/login.html index 7bdb926e..6e272e51 100644 --- a/r2/r2/templates/login.html +++ b/r2/r2/templates/login.html @@ -63,6 +63,9 @@ type="text" maxlength="20"/> %if register: ${error_field("BAD_USERNAME_" + op, kind="span")} + ${error_field("BAD_USERNAME_CHARS_" + op, kind="span")} + ${error_field("BAD_USERNAME_SHORT_" + op, kind="span")} + ${error_field("BAD_USERNAME_LONG_" + op, kind="span")} ${error_field("USERNAME_TAKEN_" + op, kind="span")} %endif diff --git a/r2/r2/templates/meetup.html b/r2/r2/templates/meetup.html new file mode 100644 index 00000000..00649fe3 --- /dev/null +++ b/r2/r2/templates/meetup.html @@ -0,0 +1,12 @@ +<%! + from routes.util import url_for + from r2.lib.utils import prettytime +%> +<%namespace file="utils.html" import="plain_link"/> +
  • ${self.meetup_title(thing)}
  • + +<%def name="meetup_title(meetup)"> + + ${meetup.title}: ${meetup.datetime().strftime('%d %B %Y %I:%M%p')} + + diff --git a/r2/r2/templates/meetup.xml b/r2/r2/templates/meetup.xml new file mode 100644 index 00000000..810587f2 --- /dev/null +++ b/r2/r2/templates/meetup.xml @@ -0,0 +1,21 @@ +<%! + from r2.lib.template_helpers import add_sr, get_domain + from r2.lib.utils import rfc822format + from r2.lib.filters import safemarkdown + from routes.util import url_for + from r2.lib.utils import prettytime +%> +<% + url = add_sr(url_for(controller='meetups', action='show', id=thing._id36), force_hostname=True) + %> + + ${thing.title}: ${prettytime(thing.datetime())} + ${url} + ${url} + ${rfc822format(thing._date)} + + <% domain = get_domain(cname = c.cname, subreddit = False) %> + ${unsafe(safemarkdown(thing.description))} + + + diff --git a/r2/r2/templates/meetupindex.html b/r2/r2/templates/meetupindex.html new file mode 100644 index 00000000..01800bc8 --- /dev/null +++ b/r2/r2/templates/meetupindex.html @@ -0,0 +1,17 @@ +
    +

    Upcoming Events

    + + <%namespace file='meetupsmap.html' import='meetup_map' /> + ${meetup_map("meetup-map", thing.meetups.lookups[0].get_items()[0])} + +
      + ${thing.meetups.render()} +
    +
    + + + diff --git a/r2/r2/templates/meetupsmap.html b/r2/r2/templates/meetupsmap.html new file mode 100644 index 00000000..ca7abc04 --- /dev/null +++ b/r2/r2/templates/meetupsmap.html @@ -0,0 +1,13 @@ +<%! + from routes.util import url_for +%> + +<%def name='meetup_map(div_id,meetups)'> +
    + %for meetup in meetups: +
    + %endfor +
    + + +${meetup_map('front-map', thing.meetups)} \ No newline at end of file diff --git a/r2/r2/templates/message.html b/r2/r2/templates/message.html index 1ba17096..6d7eb0af 100644 --- a/r2/r2/templates/message.html +++ b/r2/r2/templates/message.html @@ -6,22 +6,23 @@ ## software over a computer network and provide for limited attribution for the ## Original Developer. In addition, Exhibit A has been modified to be consistent ## with Exhibit B. -## +## ## Software distributed under the License is distributed on an "AS IS" basis, ## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for ## the specific language governing rights and limitations under the License. -## +## ## The Original Code is Reddit. -## +## ## The Original Developer is the Initial Developer. The Initial Developer of ## the Original Code is CondeNet, Inc. -## +## ## All portions of the code written by CondeNet are Copyright (c) 2006-2008 ## CondeNet, Inc. All Rights Reserved. ################################################################################ <%! from r2.lib.filters import edit_comment_filter, safemarkdown + from r2.lib.utils import prettytime %> <%inherit file="comment_skeleton.html"/> @@ -39,21 +40,22 @@ <% taglinetext = '' if c.msg_location == "inbox": - taglinetext = _("from %(author)s sent %(when)s ago") + taglinetext = _("From %(author)s sent %(when)s") elif c.msg_location == "sent" or not c.msg_location: - taglinetext = _("to %(dest)s sent %(when)s ago") + taglinetext = _("To %(dest)s sent %(when)s") elif c.msg_location == "admin" and c.user_is_admin: - taglinetext = _("to %(dest)s from %(author)s sent %(when)s ago") + taglinetext = _("To %(dest)s from %(author)s sent %(when)s") taglinetext = taglinetext.replace(' ', ' ') %> - ${unsafe(taglinetext % dict(when = thing.timesince, - author= u"%s" % self.author(thing.friend), + ${unsafe(taglinetext % dict(when = prettytime(thing._date, seconds = True), + author= u"%s" % self.author(friend = thing.friend), dest = u"%s" % thing.to.name))}
    %if c.user_is_admin: ${self.admintagline()} %endif + ## End paragraph tag that started in entry def and start new one.

    ${thing.subject} @@ -73,7 +75,7 @@ <%def name="buttons()"> %if hasattr(thing, "was_comment"):

  • - ${parent.comment_button("context", thing._fullname, _("context"), 0, + ${parent.comment_button("context", thing._fullname, _("Context"), 0, thing.permalink + "?context=3")}
  • ${parent.delete_or_report_buttons(delete=False)} @@ -82,7 +84,7 @@ ${parent.buttons()} %if c.user_is_loggedin:
  • - ${parent.simple_button("reply", thing._fullname, _("reply {verb}"), "reply")} + ${parent.simple_button("reply", thing._fullname, _("Reply"), "reply")}
  • %endif %endif @@ -95,15 +97,17 @@ ${self.tagline()}

    ${self.commentBody()} + class="message-content">${self.commentBody()}
    %if thing.author == c.user: %endif -
      - ${self.buttons()} -
    + diff --git a/r2/r2/templates/morechildren.html b/r2/r2/templates/morechildren.html index 8849039a..45a2da6a 100644 --- a/r2/r2/templates/morechildren.html +++ b/r2/r2/templates/morechildren.html @@ -6,16 +6,16 @@ ## software over a computer network and provide for limited attribution for the ## Original Developer. In addition, Exhibit A has been modified to be consistent ## with Exhibit B. -## +## ## Software distributed under the License is distributed on an "AS IS" basis, ## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for ## the specific language governing rights and limitations under the License. -## +## ## The Original Code is Reddit. -## +## ## The Original Developer is the Initial Developer. The Initial Developer of ## the Original Code is CondeNet, Inc. -## +## ## All portions of the code written by CondeNet are Copyright (c) 2006-2008 ## CondeNet, Inc. All Rights Reserved. ################################################################################ @@ -26,7 +26,7 @@ <%def name="commentBody()"> -<% +<% cids = [cm._id36 for cm in thing.children] %> @@ -42,7 +42,7 @@ <%def name="arrows()"> -<%def name="buttons()"> +<%def name="buttons(parent_link=False)"> <%def name="tagline(collapse=False)"> diff --git a/r2/r2/templates/morerecursion.html b/r2/r2/templates/morerecursion.html index 09fbbd62..eb63a8e6 100644 --- a/r2/r2/templates/morerecursion.html +++ b/r2/r2/templates/morerecursion.html @@ -6,16 +6,16 @@ ## software over a computer network and provide for limited attribution for the ## Original Developer. In addition, Exhibit A has been modified to be consistent ## with Exhibit B. -## +## ## Software distributed under the License is distributed on an "AS IS" basis, ## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for ## the specific language governing rights and limitations under the License. -## +## ## The Original Code is Reddit. -## +## ## The Original Developer is the Initial Developer. The Initial Developer of ## the Original Code is CondeNet, Inc. -## +## ## All portions of the code written by CondeNet are Copyright (c) 2006-2008 ## CondeNet, Inc. All Rights Reserved. ################################################################################ @@ -35,7 +35,7 @@ <%def name="arrows()"> -<%def name="buttons()"> +<%def name="buttons(parent_link=False)"> <%def name="tagline(collapse=False)"> diff --git a/r2/r2/templates/navbutton.html b/r2/r2/templates/navbutton.html index e41ad93f..07f0bba9 100644 --- a/r2/r2/templates/navbutton.html +++ b/r2/r2/templates/navbutton.html @@ -39,5 +39,5 @@ <%def name="select_option(selected = False)"> - + diff --git a/r2/r2/templates/navmenu.html b/r2/r2/templates/navmenu.html index 125ea01f..56dfe604 100644 --- a/r2/r2/templates/navmenu.html +++ b/r2/r2/templates/navmenu.html @@ -53,6 +53,37 @@ %endif +<%def name="dropdown2()"> + %if thing: + %if not thing.enabled: +
    ${thing.title} + %if thing.default: + ${thing.default[0].upper()+thing.default[1:]} + %elif thing.selected: + ${thing.selected.selected_title()} + %elif thing.title: + ${thing.title} + %endif +
    + %else: + %if thing.title and thing.selected: +
    ${thing.title} + %if thing.selected: + ${thing.selected.selected_title()} + %elif thing.title: + ${thing.title} + %endif +
    + %endif +
    + %for option in thing: + ${plain_link(option.title, option.path, _sr_path = option.sr_path)} + %endfor +
    + %endif + %endif + + <%def name="flatlist()"> %if thing: @@ -136,7 +167,6 @@ %endif - <%def name="select()"> %if thing: <% _id = thing._id if thing._id else None %> @@ -144,7 +174,7 @@ %endif %endif ${self.form_extra()} - + %if thing.subreddits: +

    ${_("Submit article")}

    + %else: +

    ${_("Submit article to %(site)s") % dict(site=c.site.name)}

    + %endif +
    %endif %if thing.captcha: - ${thing.captcha.render()} + + + %endif
    @@ -65,14 +71,25 @@

    ${_("Submit article to %(site)s") % dict(site=c.site.name)}

    + ${thing.captcha.render()} +
    @@ -82,11 +99,13 @@

    ${_("Submit article to %(site)s") % dict(site=c.site.name)}

    onclick="continueEditing(true)">${_("Save and continue")} ${error_field("RATELIMIT", "span")} + ${error_field("SUBREDDIT_FORBIDDEN", "span")}
    + +<%def name="form_fields()"> +
    + + + ${error_field("NO_TITLE", "span")} + ${error_field("TITLE_TOO_LONG", "span")} +
    + +
    + + + +
    Enter the meetup address above
    + ${error_field("NO_LOCATION", "span")} +
    + +
    + + + ${error_field("NO_DESCRIPTION", "span")} +
    + +
    + + + ${error_field("NO_DATE", "span")} + ${error_field("INVALID_DATE", "span")} +
    + + + + diff --git a/r2/r2/templates/notenoughkarmatopost.html b/r2/r2/templates/notenoughkarmatopost.html new file mode 100644 index 00000000..929d344c --- /dev/null +++ b/r2/r2/templates/notenoughkarmatopost.html @@ -0,0 +1,6 @@ +
    +To post new articles to the the main Less Wrong site you must have at least +${g.karma_to_post} karma points. To post to the Discussion area you must have +at least ${g.discussion_karma_to_post} points. For more information about how +karma is used on Less Wrong see, About Less Wrong. +
    diff --git a/r2/r2/templates/prefoptions.html b/r2/r2/templates/prefoptions.html index 124fe70b..8855c869 100644 --- a/r2/r2/templates/prefoptions.html +++ b/r2/r2/templates/prefoptions.html @@ -24,7 +24,7 @@ from r2.lib.utils import UrlParser import random %> -<%namespace file="utils.html" import="language_tool, language_checkboxes, plain_link"/> +<%namespace file="utils.html" import="error_field, language_tool, language_checkboxes, plain_link"/> <%def name="checkbox(text, name)"> ${_("Your preferences have been updated")}

    %endif -<% - if c.user_is_loggedin: - action = "/post/options" - else: - action = "/post/unlogged_options" - if not c.frameless_cname: - action = add_sr(action, nocname=True) - %> -
    - - %if c.cname: - - %endif - - - - %if c.user_is_loggedin: - - - + + + + + + + + + + + + + + + + + + %endif + + + +
    ${_("Link options")} -

    ${checkbox(_("Don't show articles after i've liked them"), "hide_ups")}

    -

    ${checkbox(_("Don't show articles after i've disliked them"), "hide_downs")}

    - <% - # stuff I can soon delete: - _("Display") - _("Links at once") - _("Don't show me sites with a score less than") - _("Don't show me comments with a score less than") - _("Comments by default") - %> -

    - ${unsafe(_("Display %(num)s articles at once") % \ - dict(num=capture(link_options)))} -

    +%if c.user_is_loggedin: + <% + action = "/post/options" + if not c.frameless_cname: + action = add_sr(action, nocname=True) + %> + + + %if c.cname: + + %endif + + + + %if c.user_is_loggedin: + + + + + + + - - - - - - - - - - %endif - - - -
    ${_("Link options")} +

    ${checkbox(_("Don't show articles after i've liked them"), "hide_ups")}

    +

    ${checkbox(_("Don't show articles after i've disliked them"), "hide_downs")}

    + <% + # stuff I can soon delete: + _("Display") + _("Links at once") + _("Don't show me sites with a score less than") + _("Don't show me comments with a score less than") + _("Comments by default") + %> +

    + ${unsafe(_("Display %(num)s articles at once") % \ + dict(num=capture(link_options)))} +

    + <% + input = capture(num_input, c.user.pref_min_link_score, + 'min_link_score') + %> +

    + ${unsafe(_("Don't show me articles with a score less than %(num)s") % dict(num = input))} + ${_("(Blank for none)")} +

    +
    ${_("Comment options")} <% - input = capture(num_input, c.user.pref_min_link_score, - 'min_link_score') + input = capture(num_input, c.user.pref_min_comment_score, + 'min_comment_score') %>

    - ${unsafe(_("Don't show me articles with a score less than %(num)s") % dict(num = input))} - ${_("(Blank for none)")} + ${unsafe(_("Don't show me comments with a score less than %(num)s") % dict(num = input))} + ${_("(Blank for none)")}

    -
    ${_("Comment options")} - <% - input = capture(num_input, c.user.pref_min_comment_score, - 'min_comment_score') - %> -

    - ${unsafe(_("Don't show me comments with a score less than %(num)s") % dict(num = input))} - ${_("(Blank for none)")} -

    -

    - <% - input = capture(num_input, c.user.pref_num_comments, - 'num_comments') - %> - <% s = c.user.pref_num_comments %> - ${unsafe(_("Display %(num)s comments by default") % \ - dict(num = input))} - (1 - ${g.max_comments}) -

    ${_("Privacy options")} - ${checkbox(_("Make my votes public"), "public_votes")} -
    - -
    - - +

    + <% + input = capture(num_input, c.user.pref_num_comments, + 'num_comments') + %> + <% s = c.user.pref_num_comments %> + ${unsafe(_("Display %(num)s comments by default") % \ + dict(num = input))} + (1 - ${g.max_comments}) +

    ${_("Privacy options")} + ${checkbox(_("Make my votes public"), "public_votes")} +
    ${_("Kibitz options")} + ${checkbox(_("Enable Anti-Kibitzer"), "kibitz")} +   + ${_("(What's this?)")} +   + ${_("Note: currently only works with Firefox.")} +
    ${_("Location")} + + ${error_field("LOCATION_TOO_LONG", kind="span")} +
    ${_("Personal Link")} + +
    + +
    + + +%endif \ No newline at end of file diff --git a/r2/r2/templates/printable.html b/r2/r2/templates/printable.html index 28403d97..6ed93ad9 100644 --- a/r2/r2/templates/printable.html +++ b/r2/r2/templates/printable.html @@ -6,21 +6,21 @@ ## software over a computer network and provide for limited attribution for the ## Original Developer. In addition, Exhibit A has been modified to be consistent ## with Exhibit B. -## +## ## Software distributed under the License is distributed on an "AS IS" basis, ## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for ## the specific language governing rights and limitations under the License. -## +## ## The Original Code is Reddit. -## +## ## The Original Developer is the Initial Developer. The Initial Developer of ## the Original Code is CondeNet, Inc. -## +## ## All portions of the code written by CondeNet are Copyright (c) 2006-2008 ## CondeNet, Inc. All Rights Reserved. ################################################################################ -<%! +<%! from r2.lib.template_helpers import add_sr from r2.lib.strings import strings %> @@ -44,7 +44,7 @@
  • [ ${strings.reports % thing.reported} ]
  • - %endif + %endif <%def name="RenderPrintable()"> @@ -82,18 +82,18 @@ <% fullname = thing._fullname %> %if thing.can_ban and ban: %if thing.show_spam: -
  • +
  • ${state_button("unban", fullname, _("Unban"), \ "return change_state(this, 'unban');", _("Unbanned"))}
  • %else: -
  • +
  • ${state_button("ban", fullname, _("Ban"), \ "return change_state(this, 'ban');", _("Banned"))}
  • %endif %if thing.show_reports: -
  • +
  • ${state_button("ignore", fullname, _("Ignore"), \ "return change_state(this, 'ignore');", _("Ignored"))}
  • @@ -101,23 +101,30 @@ %endif -<%def name="delete_or_report_buttons(delete=True, report=True)"> +<%def name="delete_or_report_buttons(delete=True, report=True, retract=False)"> <% fullname = thing._fullname %> %if c.user_is_loggedin: %if (not thing.author or thing.author.name != c.user.name) and report: -
  • +
  • ${yes_no_button("report", fullname, _("Report"), \ "return deletetoggle(this, $ListClass.report);",\ _("Reported"), clicked=thing.report_made)}
  • %endif %if (not thing.author or thing.author.name == c.user.name) and delete: -
  • +
  • ${yes_no_button("delete", fullname, _("Delete"), \ "return deletetoggle(this, $ListClass.del);",\ _("Deleted"))}
  • %endif + %if (not thing.author or thing.author.name == c.user.name) and retract: +
  • + ${yes_no_button("retract", fullname, _("Retract"), \ + "return deletetoggle(this, $ListClass.retract);",\ + _("Retracted"), clicked=thing.retracted, tool_tip=_("Retract"))} +
  • + %endif %endif @@ -141,7 +148,7 @@ <%def name="author(author = None, friend = False, gray = False, css_base='author', parent=False)" buffered="True"> - <% author = thing.parent_author if parent else thing.author %> + <% author = thing.parent_author if parent else (author or thing.author) %> %if thing.deleted and not c.user_is_admin: [deleted] %else: @@ -165,7 +172,7 @@ <%def name="arrow(this, dir, mod)"> -<% +<% _type = "up" if dir > 0 else "down" _class = _type + (" mod" if mod else "") fullname = this._fullname @@ -176,7 +183,7 @@ %else: onclick="showcover(true, 'vote_${fullname}')" %endif - >${_('Vote')} ${_(_type)} + title="${_('Vote')} ${_(_type)}">${_('Vote')} ${_(_type)} <%def name="score(this, likes=None, inline=True, label = True, _id = True)"> @@ -189,37 +196,40 @@ score = this.score base_score = score - 1 if likes else score if likes is None else score + 1 base_score = [base_score + x for x in range(-1, 2)]; - + %> <${tag} class="votes ${_class}" ${_id}> ${thing.score_fmt(this.score)} <%def name="midcol(display=True)"> -
    - ${self.arrow(thing, 1, thing.likes)} - ${self.arrow(thing, 0, thing.likes == False)} -
    + %if not c.profilepage: +
    + ${self.arrow(thing, 1, thing.likes)} + ${self.arrow(thing, 0, thing.likes == False)} +
    + %endif ## ## ## ### originally in statebuttons -<%def name="state_button(name, fullname, title, onclick, executed, clicked=False, a_class = '', fmt=None, fmt_param = '', **kw)"> - <% - tag = "%s_%s" % (name, fullname) +<%def name="state_button(name, fullname, title, onclick, executed, clicked=False, a_class = '', fmt=None, fmt_param = '', tool_tip=None, **kw)"> + <% + tag = "%s_%s" % (name, fullname) %> <%def name="_link()" buffered="True"> - ${title} <% @@ -228,7 +238,7 @@ link = fmt % {fmt_param: link} ##who knows? ##link = link.replace("\n", " ").replace(" <", " <").replace("> ", "> ") - %> + %> %if clicked: ${executed} @@ -248,13 +258,14 @@ <%def name="yes_no_button(name, fullname, title, onclick, executed, clicked=False, **kw)"> -${state_button(name, fullname, title, onclick, executed, - yes=_('Yes'), no=_('No'), question=_('Are you sure?'), +${state_button(name, fullname, title, onclick, executed, + yes=_('Yes'), no=_('No'), question=_('Are you sure?'), clicked=clicked, **kw)} -<%def name="simple_button(name, fullname, title, nameFunc=None)"> +<%def name="simple_button(name, fullname, title, nameFunc=None, tool_tip=None)"> \ ${title} @@ -277,7 +288,7 @@ ### originally in commentbutton <%def name="comment_button(name, fullname, link_text, num, link,\ a_class='', title='', newwindow = False)"> - <% + <% cls = "" if num == 0 else "comments" if num > 0: diff --git a/r2/r2/templates/profilebar.html b/r2/r2/templates/profilebar.html index 2759b8c4..028610e6 100644 --- a/r2/r2/templates/profilebar.html +++ b/r2/r2/templates/profilebar.html @@ -20,19 +20,14 @@ ## CondeNet, Inc. All Rights Reserved. ################################################################################ -<%namespace file="utils.html" import="submit_form, plain_link"/> +<%! from r2.lib.template_helpers import static %> + +<%namespace file="utils.html" import="plain_link, img_link"/> <% user = thing.user %> %if thing.user: %endif diff --git a/r2/r2/templates/recentarticles.html b/r2/r2/templates/recentarticles.html index bc333e5a..1e40ec9d 100644 --- a/r2/r2/templates/recentarticles.html +++ b/r2/r2/templates/recentarticles.html @@ -1,15 +1,13 @@ <%namespace file="utils.html" import="plain_link"/> - \ No newline at end of file +

    + ${plain_link(_('Recent Posts'), '/recentposts')}: +

    +<% + t = thing.things + article_generator = t.item_iter(t.get_items()) +%> +%for a in article_generator: + +%endfor: diff --git a/r2/r2/templates/recentcomments.html b/r2/r2/templates/recentcomments.html index e3171c07..62078e14 100644 --- a/r2/r2/templates/recentcomments.html +++ b/r2/r2/templates/recentcomments.html @@ -1,15 +1,13 @@ <%namespace file="utils.html" import="plain_link"/> - \ No newline at end of file +

    + ${plain_link(_('Recent Comments'), '/comments')}: +

    +<% + t = thing.things + comment_generator = t.item_iter(t.get_items()) +%> +%for a in comment_generator: +
    + ${a.render()} +
    +%endfor: diff --git a/r2/r2/templates/recentpromotedarticles.html b/r2/r2/templates/recentpromotedarticles.html new file mode 100644 index 00000000..885ff222 --- /dev/null +++ b/r2/r2/templates/recentpromotedarticles.html @@ -0,0 +1,10 @@ +<%namespace file="utils.html" import="plain_link"/> +<% + t = thing.things + article_generator = t.item_iter(t.get_items()) +%> +
      + %for a in article_generator: +
    • ${a.title}
    • + %endfor: +
    diff --git a/r2/r2/templates/recentwikieditsbox.html b/r2/r2/templates/recentwikieditsbox.html new file mode 100644 index 00000000..f6d37869 --- /dev/null +++ b/r2/r2/templates/recentwikieditsbox.html @@ -0,0 +1,40 @@ +<% + from r2.lib.template_helpers import static, add_sr, join_urls + container_id = thing.feed_url +## title_id = "%s_title" % container_id +%> + diff --git a/r2/r2/templates/reddit.html b/r2/r2/templates/reddit.html index 7c34e5a4..52bf67e4 100644 --- a/r2/r2/templates/reddit.html +++ b/r2/r2/templates/reddit.html @@ -6,21 +6,21 @@ ## software over a computer network and provide for limited attribution for the ## Original Developer. In addition, Exhibit A has been modified to be consistent ## with Exhibit B. -## +## ## Software distributed under the License is distributed on an "AS IS" basis, ## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for ## the specific language governing rights and limitations under the License. -## +## ## The Original Code is Reddit. -## +## ## The Original Developer is the Initial Developer. The Initial Developer of ## the Original Code is CondeNet, Inc. -## +## ## All portions of the code written by CondeNet are Copyright (c) 2006-2008 ## CondeNet, Inc. All Rights Reserved. ################################################################################ -<%! +<%! from r2.lib.template_helpers import add_sr, static, join_urls, class_dict, path_info, get_domain from r2.lib.pages import SearchForm from pylons import request @@ -38,7 +38,17 @@ <%def name="stylesheet()"> + %if c.user_is_loggedin and c.user.pref_kibitz: + + + + @@ -46,6 +56,17 @@ + %if c.site.stylesheet: + + %endif + %if c.site.stylesheet_contents: + + %endif + %if thing.extension_handling: @@ -58,26 +79,36 @@ <%def name="javascript()"> - + + %if g.uncompressedJS: - + + + + %else: - + + %endif - - + + + <%def name="javascript_run()"> + jQuery.noConflict(); + var class_dict = ${class_dict()}; var where = ${path_info()}; %if hasattr(c.site, '_fullname'): @@ -85,46 +116,69 @@ %else: var cur_site = ''; %endif - window.onload = init; + %if c.default_sr: + var init_args = {}; + %else: + var init_args = {r: "${c.site.name}"}; + %endif + window.onload = function() { init(init_args); }; window.onpageshow = function(evt) { - if (evt.persisted) init() + if (evt.persisted) init(init_args); }; var _global_fetching_tag = "${_('Fetching title...')}"; var _global_submitting_tag = "${_('Submitting...')}"; var _global_loading_tag = "${_('Loading...')}"; - init_tinymce("http://${get_domain(subreddit=False)}/"); - + google.load("feeds", "1"); + /* Defer loading of Maps until its needed */ + function loadMaps(callback) { + google.load("maps", "3", { + "callback" : callback, + "other_params": "sensor=false" + }); + } <%def name="bodyHTML()"> - - + +
    - - <%include file="redditheader.html" args="menu=thing.header_nav()"/> - + + <%include file="redditheader.html"/> +
    - - %if thing.content: -
    - ${thing.content().render()} -
    - %endif - + +
    + %if thing.header_nav: + ${thing.header_nav().render()} + %endif + %if thing.top_filter: +
    + ${thing.top_filter.render()} +
    + %endif + ${thing.content().render()} +
    + %if thing.show_sidebar: %endif - +
    <%include file="redditfooter.html"/>
    - + + %if c.user_is_loggedin and c.user.pref_kibitz: + + %endif <%def name="sidebar(content=None)"> @@ -132,5 +186,3 @@ ${content.render()} %endif - - diff --git a/r2/r2/templates/reddit.xml b/r2/r2/templates/reddit.xml index cbda6c8d..e7cfccd3 100644 --- a/r2/r2/templates/reddit.xml +++ b/r2/r2/templates/reddit.xml @@ -23,3 +23,11 @@ <%inherit file="base.xml"/> ${thing.content().render()} + +<%def name="Title()"> + %if hasattr(thing, 'title'): + ${thing.title} + %else: + ${c.site.name}: ${_("What's new")} + %endif + diff --git a/r2/r2/templates/redditfooter.html b/r2/r2/templates/redditfooter.html index 775c4cee..d8264738 100644 --- a/r2/r2/templates/redditfooter.html +++ b/r2/r2/templates/redditfooter.html @@ -24,19 +24,21 @@ from r2.lib.template_helpers import get_domain, static, UrlParser from r2.lib.strings import strings from r2.lib import tracking + from pylons import g import random, datetime %> <%namespace file="login.html" import="login_panel"/> <%namespace file="utils.html" import="text_with_links, plain_link"/>