Repository: windy/wblog Branch: master Commit: de727a0b63fa Files: 236 Total size: 266.0 KB Directory structure: gitextract_s_ecaci2/ ├── .1024 ├── .ackrc ├── .browserslistrc ├── .gitattributes ├── .gitignore ├── .rspec ├── .ruby-version ├── .travis.yml ├── Gemfile ├── Procfile.dev ├── README.md ├── README.zh-CN.md ├── Rakefile ├── app/ │ ├── assets/ │ │ ├── builds/ │ │ │ └── .keep │ │ └── stylesheets/ │ │ ├── about.scss │ │ ├── aboutme_welcome.scss │ │ ├── admin/ │ │ │ └── posts.scss │ │ ├── admin.scss │ │ ├── application.scss │ │ ├── archives.scss │ │ ├── blogs.scss │ │ ├── bootstrap_custom.scss │ │ ├── browserslist │ │ ├── comments.scss │ │ ├── fontawsome_custom.scss │ │ ├── footer.scss │ │ ├── head.scss │ │ ├── highlight.scss │ │ ├── libs/ │ │ │ └── markdown.scss │ │ ├── like_and_weixin.scss │ │ ├── new_year.scss │ │ └── qrcodes.scss │ ├── channels/ │ │ └── application_cable/ │ │ ├── channel.rb │ │ └── connection.rb │ ├── controllers/ │ │ ├── admin/ │ │ │ ├── accounts_controller.rb │ │ │ ├── all_comments_controller.rb │ │ │ ├── base_controller.rb │ │ │ ├── comments_controller.rb │ │ │ ├── dashboard_controller.rb │ │ │ ├── labels_controller.rb │ │ │ ├── posts_controller.rb │ │ │ └── sessions_controller.rb │ │ ├── application_controller.rb │ │ ├── archives_controller.rb │ │ ├── blogs_controller.rb │ │ ├── comments_controller.rb │ │ ├── concerns/ │ │ │ └── .keep │ │ ├── home_controller.rb │ │ ├── likes_controller.rb │ │ └── photos_controller.rb │ ├── helpers/ │ │ └── application_helper.rb │ ├── javascript/ │ │ ├── about.js │ │ ├── admin/ │ │ │ ├── posts.js │ │ │ └── sidebar.js │ │ ├── admin.js │ │ ├── application.js │ │ ├── base.js │ │ ├── channels/ │ │ │ ├── consumer.js │ │ │ └── index.js │ │ ├── controllers/ │ │ │ ├── admin_label_controller.js │ │ │ ├── index.js │ │ │ ├── like_controller.js │ │ │ └── qrcode_controller.js │ │ ├── ga.js.erb │ │ └── libs/ │ │ ├── add_jquery.js │ │ ├── ddscrollspy.js │ │ ├── jquery.atwho.js │ │ ├── jquery.html5-fileupload.js │ │ └── qrcode.js │ ├── jobs/ │ │ └── application_job.rb │ ├── mailers/ │ │ └── application_mailer.rb │ ├── models/ │ │ ├── administrator.rb │ │ ├── application_record.rb │ │ ├── comment.rb │ │ ├── concerns/ │ │ │ └── .keep │ │ ├── label.rb │ │ ├── like.rb │ │ ├── photo.rb │ │ └── post.rb │ ├── uploaders/ │ │ └── photo_uploader.rb │ └── views/ │ ├── admin/ │ │ ├── accounts/ │ │ │ └── edit.html.slim │ │ ├── all_comments/ │ │ │ └── index.html.slim │ │ ├── comments/ │ │ │ └── index.html.slim │ │ ├── dashboard/ │ │ │ └── index.html.slim │ │ ├── labels/ │ │ │ ├── _form.html.slim │ │ │ ├── edit.html.slim │ │ │ ├── index.html.slim │ │ │ └── new.html.slim │ │ ├── posts/ │ │ │ ├── _form.html.slim │ │ │ ├── edit.html.slim │ │ │ ├── index.html.slim │ │ │ └── new.html.slim │ │ └── sessions/ │ │ └── new.html.slim │ ├── archives/ │ │ └── index.html.slim │ ├── blogs/ │ │ ├── _comment.html.slim │ │ ├── _post.html.slim │ │ ├── _post_head.html.slim │ │ ├── _qrcode.html.slim │ │ ├── edit.html.slim │ │ └── show.html.slim │ ├── comments/ │ │ ├── _comment_content.html.slim │ │ ├── _comment_pre.html.slim │ │ └── create.html.slim │ ├── common/ │ │ ├── _copyright.en.html.slim │ │ ├── _copyright.html.slim │ │ ├── _no_blog_here.en.html.slim │ │ ├── _no_blog_here.html.slim │ │ ├── _welcome.en.html.slim │ │ ├── _welcome.html.slim │ │ └── _welcome_new_year.html.slim │ ├── home/ │ │ ├── _post_head.html.slim │ │ ├── about.html.slim │ │ └── index.html.slim │ ├── kaminari/ │ │ ├── _first_page.html.slim │ │ ├── _gap.html.slim │ │ ├── _last_page.html.slim │ │ ├── _next_page.html.slim │ │ ├── _page.html.slim │ │ ├── _paginator.html.slim │ │ └── _prev_page.html.slim │ ├── layouts/ │ │ ├── _footer.html.slim │ │ ├── admin.html.slim │ │ ├── application.html.slim │ │ ├── mailer.html.erb │ │ └── mailer.text.erb │ └── shared/ │ └── admin/ │ ├── _flash_messages.html.slim │ ├── _header.html.slim │ └── _sidebar.html.slim ├── babel.config.js ├── bin/ │ ├── bundle │ ├── dev │ ├── rails │ ├── rake │ ├── setup │ ├── spring │ └── yarn ├── config/ │ ├── application.rb │ ├── application.yml.example │ ├── backup.rb.example │ ├── boot.rb │ ├── cable.yml │ ├── credentials.yml.enc │ ├── database.yml.example │ ├── deploy/ │ │ └── production.rb │ ├── deploy.rb │ ├── environment.rb │ ├── environments/ │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers/ │ │ ├── application_controller_renderer.rb │ │ ├── assets.rb │ │ ├── backtrace_silencers.rb │ │ ├── browser_warrior.rb │ │ ├── content_security_policy.rb │ │ ├── cookies_serializer.rb │ │ ├── filter_parameter_logging.rb │ │ ├── inflections.rb │ │ ├── kaminari_config.rb │ │ ├── mime_types.rb │ │ ├── permissions_policy.rb │ │ ├── sidekiq.rb │ │ ├── simple_form.rb │ │ ├── simple_form_bootstrap.rb │ │ └── wrap_parameters.rb │ ├── locales/ │ │ ├── en.yml │ │ ├── simple_form.en.yml │ │ ├── simple_form.zh-CN.yml │ │ └── zh-CN.yml │ ├── logrotate.conf.example │ ├── monit.conf.example │ ├── nginx.conf.example │ ├── nginx.ssl.conf.example │ ├── puma.rb │ ├── routes.rb │ ├── secret.yml │ ├── sidekiq.yml │ ├── spring.rb │ └── storage.yml ├── config.ru ├── db/ │ ├── migrate/ │ │ ├── 20160420082319_create_posts.rb │ │ ├── 20160420082536_create_comments.rb │ │ ├── 20160420082629_create_labels.rb │ │ ├── 20160420082734_create_likes.rb │ │ ├── 20160420082811_create_photos.rb │ │ ├── 20160421035040_create_join_table_post_label.rb │ │ ├── 20210614151036_create_active_storage_tables.active_storage.rb │ │ ├── 20210614151102_create_administrators.rb │ │ ├── 20250120142353_add_service_name_to_active_storage_blobs.active_storage.rb │ │ ├── 20250120142354_create_active_storage_variant_records.active_storage.rb │ │ └── 20250120142355_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb │ ├── schema.rb │ └── seeds.rb ├── doc/ │ └── .gitkeep ├── lib/ │ ├── assets/ │ │ └── .keep │ ├── markdown.rb │ ├── tasks/ │ │ └── .keep │ └── templates/ │ └── slim/ │ └── scaffold/ │ └── _form.html.slim ├── log/ │ └── .keep ├── package.json ├── postcss.config.js ├── public/ │ ├── 404.html │ ├── 422.html │ ├── 500.html │ └── robots.txt ├── spec/ │ ├── controllers/ │ │ ├── admin/ │ │ │ ├── comments_controller_spec.rb │ │ │ ├── dashboard_controller_spec.rb │ │ │ ├── posts_controller_spec.rb │ │ │ └── sessions_controller_spec.rb │ │ ├── archives_controller_spec.rb │ │ ├── blogs_controller_spec.rb │ │ ├── home_controller_spec.rb │ │ └── likes_controller_spec.rb │ ├── factories/ │ │ ├── comments.rb │ │ ├── labels.rb │ │ ├── posts.rb │ │ └── subscribes.rb │ ├── models/ │ │ ├── like_spec.rb │ │ └── post_spec.rb │ ├── rails_helper.rb │ ├── spec_helper.rb │ └── support/ │ ├── capybara.rb │ ├── database_cleaner.rb │ └── factory_bot.rb ├── storage/ │ └── .keep ├── test/ │ ├── application_system_test_case.rb │ ├── channels/ │ │ └── application_cable/ │ │ └── connection_test.rb │ ├── controllers/ │ │ └── .keep │ ├── fixtures/ │ │ └── files/ │ │ └── .keep │ ├── helpers/ │ │ └── .keep │ ├── integration/ │ │ └── .keep │ ├── mailers/ │ │ └── .keep │ ├── models/ │ │ └── .keep │ ├── system/ │ │ └── .keep │ └── test_helper.rb ├── tmp/ │ └── .keep └── vendor/ └── .keep ================================================ FILE CONTENTS ================================================ ================================================ FILE: .1024 ================================================ # .1024 Configuration file for project run commands, compilation and debug settings (optional); # Any changes made will be auto-saved and take effect immediately. # For more information, please refer to the documentation: https://docs.clacky.ai/clacky-workspace/configure # Command to run when "Run" button clicked run_commands: ['bin/dev'] # Command to install or update dependencies, will execute each time a new thread created to ensure dependencies up-to-date dependency_command: gem install bundler && bundle install && yarn install && rails db:migrate ================================================ FILE: .ackrc ================================================ --ignore-file=ext:svg --ignore-dir=public --ignore-dir=tmp --ignore-dir=node_modules ================================================ FILE: .browserslistrc ================================================ defaults ================================================ FILE: .gitattributes ================================================ # See https://git-scm.com/docs/gitattributes for more about git attribute files. # Mark the database schema as having been generated. db/schema.rb linguist-generated # Mark the yarn lockfile as having been generated. yarn.lock linguist-generated # Mark any vendored files as having been vendored. vendor/* linguist-vendored ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files for more about ignoring files. # # If you find yourself ignoring temporary files generated by your text editor # or operating system, you probably want to add a global ignore instead: # git config --global core.excludesfile '~/.gitignore_global' # Ignore bundler config. /.bundle # Ignore the default SQLite database. /db/*.sqlite3 /db/*.sqlite3-journal # Ignore all logfiles and tempfiles. /log/* /tmp/* !/log/.keep !/tmp/.keep # Ignore uploaded files in development. /storage/* !/storage/.keep /public/assets # Ignore master key for decrypting credentials and more. /config/master.key /public/packs /public/packs-test /node_modules /yarn-error.log yarn-debug.log* .yarn-integrity *.swp /public/uploads/* /public/assets/* /tags /config/application.yml /config/mongoid.yml /config/database.yml /config/master.key **.orig *.old *.bak *~ .env .byebug_history .DS_Store /app/assets/builds/* !/app/assets/builds/.keep # Clacky-specific files .1024* !.1024 .breakpoints ================================================ FILE: .rspec ================================================ --color --require spec_helper --require rails_helper ================================================ FILE: .ruby-version ================================================ 3.1.2 ================================================ FILE: .travis.yml ================================================ # http://about.travis-ci.org/docs/user/build-configuration/ env: global: - CC_TEST_REPORTER_ID=787a2f89b15c637323c7340d65ec17e898ac44480706b4b4122ea040c2a88f1d language: ruby before_install: - "cat /etc/timezone" - "grep -i processor /proc/cpuinfo | wc -l" - "echo 'gem: --no-ri --no-rdoc' > ~/.gemrc" - "gem install bundler" - "bundle -v" before_script: - "curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter" - "chmod +x ./cc-test-reporter" - "./cc-test-reporter before-build" - "cp -f config/database.yml.example config/database.yml" - "cp -f config/application.yml.example config/application.yml" - "bundle exec rake db:drop db:create db:schema:load --trace 2>&1" script: bundle exec rspec after_script: - "./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT" services: - postgresql rvm: - 2.5.3 ================================================ FILE: Gemfile ================================================ source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby '3.1.2' gem 'rails', '~> 7.1.4.2' gem 'puma', '~> 4.x' # gem 'puma-daemon', require: false gem 'turbo-rails' gem 'jbuilder', '~> 2.7' gem 'bootsnap', '>= 1.4.4', require: false gem 'stimulus-rails' gem 'cssbundling-rails' gem 'jsbundling-rails' gem 'propshaft', '~> 1.1.0' gem 'rexml', '~> 3.2', '>= 3.2.4' group :development, :test do gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] end group :development do gem 'web-console', '>= 4.1.0' gem 'rack-mini-profiler', '~> 2.0' gem 'listen', '~> 3.9' gem 'spring' end group :test do gem 'capybara', '>= 3.26' gem 'selenium-webdriver' gem 'webdrivers' end gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] gem 'pg', '>= 1.1' gem 'concurrent-ruby', '1.3.4' gem 'carrierwave' gem 'redcarpet' gem 'rouge' gem 'mini_magick' gem 'html_truncator' gem 'nokogiri' gem 'figaro' gem 'simple_form', '~> 5.0' gem 'slim-rails' gem 'high_voltage', '~> 3.1' # gem 'browser_warrior', '>= 0.11.0' gem 'sidekiq', '~> 5' gem 'bcrypt' gem 'kaminari', github: 'kaminari/kaminari' gem 'rails-i18n', '~> 7.0.10' gem 'mina', '~> 1.2.2', require: false gem 'mina-ng-puma', '>= 1.4.0', require: false gem 'mina-multistage', require: false gem 'mina-sidekiq', require: false gem 'mina-logs', require: false group :development do gem 'rails_apps_testing' gem 'faker' end group :development, :test do gem 'rspec-rails' gem 'factory_bot_rails' end group :test do gem 'database_cleaner' gem 'launchy' end ================================================ FILE: Procfile.dev ================================================ web: env RUBY_DEBUG_OPEN=true bin/rails server css: yarn build:css --watch --poll js: yarn build --watch ================================================ FILE: README.md ================================================ WBlog ======= [![Build Status](https://travis-ci.org/windy/wblog.svg?branch=master)](https://travis-ci.org/windy/wblog) [![Maintainability](https://api.codeclimate.com/v1/badges/545d8372a9dda70b77fe/maintainability)](https://codeclimate.com/github/windy/wblog/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/545d8372a9dda70b77fe/test_coverage)](https://codeclimate.com/github/windy/wblog/test_coverage) The missing open source blog system on Ruby on Rails 7.x. WBlog is open source blog which built for mobile first, it's licenced on MIT, use it for free! ~~New: WBlog is using Ruby on Rails 6.1 now.~~ New: WBlog has updated from webpacker to jsbundling & cssbundling with esbuild and sass. New: WBlog is using Ruby on Rails 7.1 now. [中文说明文档](/README.zh-CN.md) Characteristic: * Modern clean reading feelings * Markdown support, give nice formatted articles * Mobile first, responsive page for iPhone, iPad, iMac. * Independent comment system, subscribe system, picture manage system A real example comes from my own blog( Chinese ): Some [screenshots](#screenshots) ### System dependencies * Ruby ( >= 3.1.2 ) * Postgresql ( >= 9.x ) * Nginx ( >= 1.4 ) * node ( >= 1.18 ) ### Features * Responsive, iPhone, iPad, Notebook, PC, all are supported * QR Code, Like button make your article easily sharing with your friends * Inpendent comment system, subscribe system, that all belong to you * Markdown supported, code highlight, especially for programmer, like you * Personalize it, commercialize it, it all depends on you ### Goal Make it to the best Ruby on Rails Blog system in the world. ### Running in development mode WBlog MUST run in Linux or OSX. I assume you are using OS X 10. You can run it like a Ruby on Rails project as usual: 0. Check dependencies ```shell ruby -v # 3.1.2 postgres --version # 9.x.x npm -v # 1.18.x ``` 1. Clone it `git clone git@github.com:windy/wblog.git` `cd wblog` 2. Install dependencies & configure ```shell # install rails dependencies gem install bundler bundle install # install node dependencies npm install yarn -g yarn install # copy and update project config file cp config/application.yml.example config/application.yml cp config/database.yml.example config/database.yml ``` Update `application.yml` & `database.yml` 's content as you need, then run setup: ```shell bin/setup ``` 3. Start it one command: ```shell bin/dev ``` It's all. or using multi terminal: ```shell # rails bin/rails s ``` ```shell # js compile bin/yarn build --watch ``` ```shell # css compile bin/yarn build:css --watch ``` Open browser with `http://localhost:3000` If there is any error found, please check your database's user and password( default is admin/admin ) 4. Post the first blog visit: http://localhost:3000/admin, input your username and password configurated in `db/seeds.rb`. then, post a new article. OK, That's all. ### Deployment WBlog uses `mina` as automation deployment tool, uses `puma` as the Rack container. WBlog recommends `nginx` as reverse proxy server. It will be very fast. Ruby on Rails project deployment is another topic, I would NOT talk it here. You can read WBlog wiki for more information: [WBlog 的发布流程(Chinese only now)](https://github.com/windy/wblog/wiki) ### Stack * Ruby on Rails 7.1 * Ruby 3.1.2 * Turbo * Bootstrap 4 * mina * slim * puma * Postgresql ## Related open source blog systems * writings.io( Ruby on Rails 4.0.2 ): a multi users blog system * jekyll( Ruby Gem, Markdown): Static blog system * octopress( Github Pages ): * middleman( Ruby Gem ): Another static blog system * robbin_site( Padrino ): ## License MIT. ### Screenshots Home Page: ![screenshot home](https://github.com/windy/wblog/raw/master/doc/wblog_s_en/home.png) Home Page for mobile: ![screenshot home small](https://github.com/windy/wblog/raw/master/doc/wblog_s_en/home-small.png) Home Page Hover Status for mobile: ![screenshot home hover](https://github.com/windy/wblog/raw/master/doc/wblog_s_en/home-small-hover.png) Blog Show Page: ![screenshot post](https://github.com/windy/wblog/raw/master/doc/wblog_s_en/post.png) Blog Show Page Hover Status: ![screenshot post hover](https://github.com/windy/wblog/raw/master/doc/wblog_s_en/post-hover.png) Admin Login Page: ![screenshot admin](https://github.com/windy/wblog/raw/master/doc/wblog_s_en/admin-login.png) Admin Dashboard Page: ![screenshot admin](https://github.com/windy/wblog/raw/master/doc/wblog_s_en/admin-dashboard.png) Admin New Blog Page: ![screenshot admin](https://github.com/windy/wblog/raw/master/doc/wblog_s_en/admin-post.png) Admin Blogs Manage Page: ![screenshot admin](https://github.com/windy/wblog/raw/master/doc/wblog_s_en/admin-posts.png) ================================================ FILE: README.zh-CN.md ================================================ WBlog ======= [![Build Status](https://travis-ci.org/windy/wblog.svg?branch=master)](https://travis-ci.org/windy/wblog) [![Maintainability](https://api.codeclimate.com/v1/badges/545d8372a9dda70b77fe/maintainability)](https://codeclimate.com/github/windy/wblog/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/545d8372a9dda70b77fe/test_coverage)](https://codeclimate.com/github/windy/wblog/test_coverage) 为移动而生的 Ruby on Rails 开源博客. WBlog 基于 MIT 协议, 自由使用. ~~现已全面支持 Ruby on Rails 6.1 版本!!!~~ New: 现已经从 webpacker 升级至 jsbundling & cssbundling. New: 现已全面升级到 Ruby on Rails 7.1 版本!!! * 用户极为友好的阅读体验 * 自带干净的评论系统 * 简洁而不简单的发布博客流程 访问我的博客以体验: 截图如下: <#screenshots> ### WBlog 的设计目标 * 优先以手机用户体验为主 * 独立干净的评论系统 * 良好的博客语法高亮支持 * 可邮件订阅 * Markdown 支持 * 尽可能独立 ### 特色 * 优先支持移动端访问 * 响应式设计, 支持所有屏幕终端, 并且支持微信扫码继续阅读和分享 * 自带评论系统, 干净而方便 * Markdown 支持, 博客语法高亮, 方便技术性博客 * 开源可商用, 定制能力强 ### 目标 `Ruby on Rails` 下最好用的独立博客建站系统 ### 开发环境 WBlog 是一个标准的 Ruby on Rails 应用. 开发环境依赖于: * Ruby ( = 3.1.2 ) * Postgresql ( >= 9.x ) * node ( >= 18 ) 配置 WBlog: ```shell # rails 依赖 gem install bundler bundle install # node 依赖 npm install yarn -g yarn install # 配置更新 cp config/application.yml.example config/application.yml cp config/database.yml.example config/database.yml ``` 更新对应配置: application.yml & database.yml. 就这样, 可以尝试启动了: ```shell bin/dev ``` 登录 http://localhsot:3000/admin 来发布第一篇博客. ### 发布应用 WBlog 采用了 `mina` 作为自动化发布工具, 使用 `nginx`, `puma` 为相关容器. 对应的发布流程在: [WBlog 的发布流程](https://github.com/windy/wblog/wiki) ### 技术栈 * Ruby on Rails 7.1 * Ruby 3.1.2 * Bootstrap 4 * mina * slim * Postgresql ## Ruby 相关开源博客推荐 * writings.io( Ruby on Rails 4.0.2 ): * jekyll( Ruby Gem, Markdown, Static ): * octopress( Github Pages ): * middleman( Ruby Gem, Static ): * robbin_site( Padrino ): ### Screenshots 首页: ![screenshot home](https://github.com/windy/wblog/raw/master/doc/wblog_s/home.png) 小屏首页: ![screenshot home small](https://github.com/windy/wblog/raw/master/doc/wblog_s/home-small.png) 展开的小屏首页: ![screenshot home hover](https://github.com/windy/wblog/raw/master/doc/wblog_s/home-small-hover.png) 博客详情页: ![screenshot post](https://github.com/windy/wblog/raw/master/doc/wblog_s/post.png) 展开的博客详情页: ![screenshot post hover](https://github.com/windy/wblog/raw/master/doc/wblog_s/post-hover.png) 管理员登录页: ![screenshot admin](https://github.com/windy/wblog/raw/master/doc/wblog_s/admin-login.png) 管理页面板: ![screenshot admin](https://github.com/windy/wblog/raw/master/doc/wblog_s/admin-dashboard.png) 发布新博客页: ![screenshot admin](https://github.com/windy/wblog/raw/master/doc/wblog_s/admin-post.png) 博客管理页: ![screenshot admin](https://github.com/windy/wblog/raw/master/doc/wblog_s/admin-posts.png) ================================================ FILE: Rakefile ================================================ # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. require_relative "config/application" Rails.application.load_tasks ================================================ FILE: app/assets/builds/.keep ================================================ ================================================ FILE: app/assets/stylesheets/about.scss ================================================ .home-about-page { .fixed { position: fixed; top: 0; width: 100%; z-index: 99; left: 0; } .responsive-button { padding: 0.7rem 1rem; } a { word-wrap: break-word; color: #2199e8; &:hover { text-decoration: none; opacity: 0.8; } } //@media only screen and (min-width: 40.063em) { @media screen and (max-width: 39.9375em) { .top-bar-wrapper { .top-bar, .top-bar-left, .top-bar-right { width: auto !important; } } } .top-bar-wrapper { background: 0 0; transition: background .5s ease-in-out,padding .5s ease-in-out; &.active { background: #000; border-bottom: 1px solid #666; .top-bar { margin: 0.5rem 0; } } .top-bar { background: 0 0; margin: 1.5rem 0; .name { font-size: 1.325rem; } } .top-bar ul{ & { background: 0 0; } li, li a { background: 0 0; font-size: 1rem; color: #fefefe; } li a:hover { background: #666; } li a.active { background: #4C4C4C; } } } //} p { font-size: 1.3rem; line-height: 1.8; font-weight: 300; } .intro { background-color: #7A7A7A; background: url('intro-bg.jpg') no-repeat bottom center scroll; background-size: cover; height: 100vh; width: 100%; display: table; .intro-heading { display: table-cell; vertical-align: middle; text-align: center; .heading, .sub-heading { color: #eee; } .version { color: #3FBFFF; &:hover { color: #BFEAFF; } } .sub-heading { margin-top: 2rem; p { line-height: 2; } } .circle { color: #eee; width: 4rem; height: 4rem; font-size: 3rem; border: 2px solid #eee; display: inline-block; border-radius: 50%; margin-top: 2rem; line-height: 1.2; &:hover { opacity: 0.7; } .fa { font-size: 80%; } } } } #about { .wrapper { .time { color: #aaa; margin-bottom: 0.5rem; margin-top: 1.5rem; } p { font-size: 1rem; line-height: 2; } ul > li { margin: 0.5rem 0; } } } #about, #skill { background-color: #000; color: #eee; padding-top: 10rem; padding-bottom: 10rem; h1, h2 { color: #eee; text-align: center; margin-bottom: 2rem; } } #skill { background: url('mp.jpg') no-repeat bottom center scroll; background-color: #BABABA; background-size: cover; .skills { color: #eeeeee; background-color: rgba(63, 63, 63, 0.6); padding: 2rem 2rem; font-size: 1.125rem; li { margin: 1rem 0; } } } #work { background: url('download-bg.jpg') no-repeat bottom center scroll; background-size: cover; padding-top: 10rem; padding-bottom: 10rem; h1, h2 { color: #eee; text-align: center; margin-bottom: 2rem; } p { color: #eee; text-align: center; } .works { color: #eee; background-color: rgba(63, 63, 63, 0.6); list-style: none; padding: 2rem 2rem; margin-left: 0; >li { margin: 1rem 0; } .project-description { margin-bottom: 2rem; font-size: 90%; li { margin: 0.5rem 0; color: #aaa; &:hover { color: #ccc; } } } .name { margin-right: 0.5rem; font-size: 1.125rem; font-weight: 600; &:after { margin-left: 0.5rem; font-weight: 400; content: '--'; } } .brief { margin-right: 1rem; color: #dfdfdf; } .link { margin-right: 1rem; color: #dfdfdf; } .time { color: #aaa; font-size: 95%; } } } #contact { background: #000; background-size: cover; padding-top: 10rem; padding-bottom: 2rem; .modified-at { padding-top: 8rem; color: #aaa; font-size: 1rem; } h1, h2 { color: #eee; text-align: center; margin-bottom: 2rem; } p { color: #eee; text-align: center; } .mail_to { margin-bottom: 2rem; margin-top: 3rem; } .contact-ul { text-align: center; li { display: inline-block; } li a { display: inline-block; padding: 0.5rem 2rem; border: 1px solid #219AB3; border-radius: 0.5rem; color: #219AB3; margin: 0.5rem 1rem; transition: all .3s ease-in-out; &:hover { outline: 0; color: #000; background-color: #219ab3; } i { margin-right: 0.5rem; font-size: 1.1rem; } i.douban { font-style: normal; font-size: 95%; vertical-align: baseline; } } } } .footer { border: none; padding: 1.5rem; background-color: #353535; text-align: center; color: #eee; font-size: 1.125rem; } } ================================================ FILE: app/assets/stylesheets/aboutme_welcome.scss ================================================ .self-introduce { @media screen and (min-width: 64em) { margin-top: 1.875rem; } .box { margin-bottom: 2rem; } h1, h2, h3, h4, h5, h6 { font-weight: 400; margin-bottom: 1rem; } p, .aboutme-index { color: #5D5D5D; } } .aboutme-index { list-style-type: none; margin-left: 0; padding-left: 0; li { margin-left: 0; padding-bottom: 0.5rem; span { &:first-child{ margin-right: 0.5rem; } } } } ================================================ FILE: app/assets/stylesheets/admin/posts.scss ================================================ .admin-posts-edit-page, .admin-posts-index-page { td { i.fa { margin-right: 0.2rem; } span { margin-right: 0.5rem; } } #content-input { min-height: 30rem; } #preview { padding: 0.5rem; border: 1px solid #eee; border-top: none; margin-bottom: 0.5rem; min-height: 5rem; border-radius: 0.5rem; } #upload_photo { float: right; margin-top: -2rem; } } ================================================ FILE: app/assets/stylesheets/admin.scss ================================================ @use 'fontawsome_custom'; @use 'bootstrap_custom'; @use 'libs/markdown'; @use 'admin-lte/dist/css/adminlte'; @use 'admin/posts'; @use 'select2/dist/css/select2.css'; // @use '@ttskch/select2-bootstrap4-theme/dist/select2-bootstrap4.css'; .turbo-progress-bar { height: 4px; background-color: red; } .main-sidebar { li.nav-item { width: 100%; a { text-overflow: ellipsis; } } } .login-box { margin: 7% auto; } ================================================ FILE: app/assets/stylesheets/application.scss ================================================ @use 'bootstrap_custom'; @use 'fontawsome_custom'; @use 'libs/markdown'; @use 'aboutme_welcome'; @use 'archives'; @use 'blogs'; @use 'head'; @use 'qrcodes'; @use 'comments'; @use 'highlight'; @use 'footer'; @use 'like_and_weixin'; @use 'about'; @use 'new_year'; .turbo-progress-bar { height: 2px; background-color: red; } img { display: block; } ================================================ FILE: app/assets/stylesheets/archives.scss ================================================ .archives-field { padding-top: 0.5rem; margin-top: 1rem; padding-bottom: 0.5rem; i { margin-right: 0.5rem; } span { margin-right: 1rem; } .search-result-wrapper p { font-size: 0.785rem; color: #999; } .blog-title { color: #111111; &:hover { color: red; text-decoration: none; } border: none; display: inline-block; margin-bottom: 0.3rem; em { color: red; font-style: normal; } } li { list-style: none; color: #666666; border-bottom: 1px dashed #CCCCCC; margin-bottom: 1rem; } .tags-field { font-size: 90%; } .load-more { width: 100%; text-align: center; button { background-color: transparent; color: #333333; border: 1px solid #DDDDDD; &:focus { outline-style: none; } } } .no-more-field p { text-align: center; color: #444444; border-bottom: 1px solid #DDDDDD; padding-bottom: 0.5rem; padding-top: 1rem; } } ================================================ FILE: app/assets/stylesheets/blogs.scss ================================================ .home-index-page, .blogs-show-page { .blog-title { margin-top: 1rem; line-height: 1.5; } .ptag { margin-bottom: 0.5rem; color: #888; font-size: 95%; span { margin-left: 0.5rem; &:first-child { margin-left: 0; } } .has-tip { border: none; } } .content { padding-top: 1rem; } .read-more { margin-top: 1rem; display: inline-block; padding: 1rem; border: 1px solid #ccc; color: #333; &:hover { color: #111; border-color: #999; text-decoration: none; } } .published-at { margin-top: 1rem; color: #999; @media screen and (min-width: 40.063em) { text-align: right; } } .blog-over { margin-bottom: 1rem; margin-top: 2rem; border-bottom: 1px solid #DCDCDC; } .recent-title { margin-top: 3rem; font-weight: 400; } .recent-content { padding-left: 1.2rem; @media screen and (min-width: 64em) { padding-bottom: 3rem; } li { list-style: disc; } li a { margin-left: -0.215rem; } } #qrcode-home { padding: 1rem 2rem 1rem 0; } .qrcode { display: inline-block; float: right; margin-top: -4.215rem; i { margin-right: 0.5rem; } } .qrcode-wrapper { float: right; margin-top: -2rem; text-align: right; table { float: right; } p { clear: both; } } img { width: 100%; } .social-share { display: none; } .wechat_qrcode { width: 200px; margin-bottom: 20px; } } ================================================ FILE: app/assets/stylesheets/bootstrap_custom.scss ================================================ @use "sass:color"; $link-color: #2199e8 !default; $link-decoration: none !default; $link-hover-color: color.adjust($link-color, $lightness: -15%) !default; $link-hover-decoration: none !default; @forward 'bootstrap/scss/bootstrap'; ================================================ FILE: app/assets/stylesheets/browserslist ================================================ last 2 versions ie >= 9 Android >= 2.3 ios >= 7 ================================================ FILE: app/assets/stylesheets/comments.scss ================================================ #alert-container { border-radius: unset; } .comment-field { background-color: #333333; padding-top: 3rem; padding-bottom: 1rem; textarea { min-height: 7rem; } input[type='text'], textarea { border: 1px solid #333333; background-color: #F7F7F7; box-shadow: none; width: 100%; padding: 0.5rem; color: #333; margin-bottom: 1rem; &:focus { box-shadow: none; background-color: white; } } .has-tip { color: #008cba; } .next { float: right; i { margin-left: 0.275rem; } } .prev i { margin-right: 0.275rem; margin-top: 0.5rem; } } .comment-diag { color: #EBEBEB; border-top: 1px solid #5E5E5E; padding-top: 1rem; .created-at { color: #b3b3b3; } .comment-content { padding-bottom: 1rem; padding-left: 0.275rem; border-bottom: 1px dashed #8a8a8a; margin-bottom: 0; p { margin-bottom: 0.325rem; } } .name { padding-top: 1rem; padding-left: 0.275rem; } } .comment-submit[disabled] { opacity: 0.4; cursor: not-allowed; } .comment-submit { background-color: #008cba; opacity: 0.8; padding: 0.8rem 1.5rem; margin-bottom: 1rem; border: none; color: white; &:hover { opacity: 1; } } .comment-success { color: #66FAB5; } .comment-fail { color: #FF7A7A; } .comment-wrapper { &:hover { background-color: #444444; } .name { color: #DDDDDD; } .comment-content { word-wrap: break-word; color: #DDDDDD; word-break: break-all; } } ================================================ FILE: app/assets/stylesheets/fontawsome_custom.scss ================================================ $fa-font-path: '@fortawesome/fontawesome-free/webfonts'; @import '@fortawesome/fontawesome-free/scss/fontawesome'; @import '@fortawesome/fontawesome-free/scss/regular'; @import '@fortawesome/fontawesome-free/scss/solid'; @import '@fortawesome/fontawesome-free/scss/brands'; ================================================ FILE: app/assets/stylesheets/footer.scss ================================================ .footer { border-top: 1px solid #dddddd; padding: 1rem 0 2rem; text-align: right; color: #666; .link { margin-right: 1rem; } .license { margin-top: 1rem; span { margin-left: 0.5rem; } } } ================================================ FILE: app/assets/stylesheets/head.scss ================================================ .my-navbar { padding-left: 1.5rem; padding-right: 1.5rem; a.navbar-brand:hover { color: #aaa; } .nav-link { padding-right: 0.75rem !important; padding-left: 0.75rem !important; } } ================================================ FILE: app/assets/stylesheets/highlight.scss ================================================ .highlight { .hll { background-color: #49483e } .c { color: #75715e } /* Comment */ .err { color: #960050; background-color: #1e0010 } /* Error */ .k { color: #66d9ef } /* Keyword */ .l { color: #ae81ff } /* Literal */ .n { color: #f8f8f2 } /* Name */ .o { color: #f92672 } /* Operator */ .p { color: #f8f8f2 } /* Punctuation */ .cm { color: #75715e } /* Comment.Multiline */ .cp { color: #75715e } /* Comment.Preproc */ .c1 { color: #75715e } /* Comment.Single */ .cs { color: #75715e } /* Comment.Special */ .ge { font-style: italic } /* Generic.Emph */ .gs { font-weight: bold } /* Generic.Strong */ .kc { color: #66d9ef } /* Keyword.Constant */ .kd { color: #66d9ef } /* Keyword.Declaration */ .kn { color: #f92672 } /* Keyword.Namespace */ .kp { color: #66d9ef } /* Keyword.Pseudo */ .kr { color: #66d9ef } /* Keyword.Reserved */ .kt { color: #66d9ef } /* Keyword.Type */ .ld { color: #e6db74 } /* Literal.Date */ .m { color: #ae81ff } /* Literal.Number */ .s { color: #e6db74 } /* Literal.String */ .na { color: #a6e22e } /* Name.Attribute */ .nb { color: #f8f8f2 } /* Name.Builtin */ .nc { color: #a6e22e } /* Name.Class */ .no { color: #66d9ef } /* Name.Constant */ .nd { color: #a6e22e } /* Name.Decorator */ .ni { color: #f8f8f2 } /* Name.Entity */ .ne { color: #a6e22e } /* Name.Exception */ .nf { color: #a6e22e } /* Name.Function */ .nl { color: #f8f8f2 } /* Name.Label */ .nn { color: #f8f8f2 } /* Name.Namespace */ .nx { color: #a6e22e } /* Name.Other */ .py { color: #f8f8f2 } /* Name.Property */ .nt { color: #f92672 } /* Name.Tag */ .nv { color: #f8f8f2 } /* Name.Variable */ .ow { color: #f92672 } /* Operator.Word */ .w { color: #f8f8f2 } /* Text.Whitespace */ .mf { color: #ae81ff } /* Literal.Number.Float */ .mh { color: #ae81ff } /* Literal.Number.Hex */ .mi { color: #ae81ff } /* Literal.Number.Integer */ .mo { color: #ae81ff } /* Literal.Number.Oct */ .sb { color: #e6db74 } /* Literal.String.Backtick */ .sc { color: #e6db74 } /* Literal.String.Char */ .sd { color: #e6db74 } /* Literal.String.Doc */ .s2 { color: #e6db74 } /* Literal.String.Double */ .se { color: #ae81ff } /* Literal.String.Escape */ .sh { color: #e6db74 } /* Literal.String.Heredoc */ .si { color: #e6db74 } /* Literal.String.Interpol */ .sx { color: #e6db74 } /* Literal.String.Other */ .sr { color: #e6db74 } /* Literal.String.Regex */ .s1 { color: #e6db74 } /* Literal.String.Single */ .ss { color: #e6db74 } /* Literal.String.Symbol */ .bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */ .vc { color: #f8f8f2 } /* Name.Variable.Class */ .vg { color: #f8f8f2 } /* Name.Variable.Global */ .vi { color: #f8f8f2 } /* Name.Variable.Instance */ .il { color: #ae81ff } /* Literal.Number.Integer.Long */ .gh { } /* Generic Heading & Diff Header */ .gu { color: #75715e; } /* Generic.Subheading & Diff Unified/Comment? */ .gd { color: #f92672; } /* Generic.Deleted & Diff Deleted */ .gi { color: #a6e22e; } /* Generic.Inserted & Diff Inserted */ } ================================================ FILE: app/assets/stylesheets/libs/markdown.scss ================================================ .markdown { word-wrap: break-word; overflow: hidden; color: #4f4444; @media screen and (min-width: 40.063em) { h1, h2, h3, h4, h5, h6 { font-size: 1.875rem; } } @media screen and (max-width: 40em) { h1, h2, h3, h4, h5, h6 { font-size: 1.2rem; } } h1, h2, h3, h4, h5, h6 { margin-bottom: 1.5rem; font-weight: 400; line-height: 1.5; } p { margin-bottom: 1.5rem; line-height: 1.8; } pre { padding: 1rem 0.5rem; margin: 1rem 0; font-weight: 500; line-height: 1.5; white-space: pre; word-wrap: normal; overflow-x: auto; background-color: #272822; color: #f8f8f2; border-radius: 5px; > code { border: none; background-color: transparent; margin: 0; padding: 0; } } code { font-weight: 500; border: none; margin: 0; background: #5d5e59; padding: 0.2rem 0.5rem; border-radius: 5px; color: #f8f8f2; display: inline-block; margin: 0.3rem 0.2rem 0.3rem 0; } blockquote { padding: 0.5625rem 1.25rem 0 1.1875rem; border-left: 1px solid #cacaca; color: #8a8a8a; } ol, ul, dl { padding-left: 1.2rem; li { margin-bottom: 0.3rem; } } } ================================================ FILE: app/assets/stylesheets/like_and_weixin.scss ================================================ .like-button { color: #eaa296; background-color: transparent; border: 1px solid #e79385; border-radius: 10rem; padding: 0.5rem 1rem; &:hover { color: #e07662; border-color: #e27d6b; } &:focus { outline-style: none; } span { margin-left: 0.23rem; } } .like-button.liked { background-color: #EAA296; color: #FFF; &:focus { outline-style: none; } } ================================================ FILE: app/assets/stylesheets/new_year.scss ================================================ .new-year { margin: 1rem 0; text-align: center; font-size: 1.1rem; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; box-shadow: 0 4px 15px 0 rgba(102, 126, 234, 0.3); strong { color: white; } .close { color: white; opacity: 0.8; &:hover { opacity: 1; } } } ================================================ FILE: app/assets/stylesheets/qrcodes.scss ================================================ #image-tag { float: right; width: 200px; margin-bottom: 1rem; } ================================================ FILE: app/channels/application_cable/channel.rb ================================================ module ApplicationCable class Channel < ActionCable::Channel::Base end end ================================================ FILE: app/channels/application_cable/connection.rb ================================================ module ApplicationCable class Connection < ActionCable::Connection::Base end end ================================================ FILE: app/controllers/admin/accounts_controller.rb ================================================ class Admin::AccountsController < Admin::BaseController def edit end def update if current_admin.authenticate(params.require(:administrator)[:current_password]) if current_admin.update(admin_params) admin_sign_out redirect_to admin_login_path, notice: 'Account has been updated, please log in again' else render 'edit' end else flash.now[:alert] = 'Old password is wrong, try again' render 'edit' end end private def admin_params params.require(:administrator).permit(:name, :password, :password_confirmation) end end ================================================ FILE: app/controllers/admin/all_comments_controller.rb ================================================ class Admin::AllCommentsController < Admin::BaseController def index @comments = Comment.order(created_at: :desc).page(params[:page]).per(25) end def destroy comment = Comment.find(params[:id]) if comment.destroy flash[:notice] = '删除评论成功' redirect_to admin_all_comments_path else flash[:alert] = '删除失败' redirect_to admin_all_comments_path end end end ================================================ FILE: app/controllers/admin/base_controller.rb ================================================ class Admin::BaseController < ActionController::Base layout 'admin' protect_from_forgery with: :exception before_action :authenticate_admin! helper_method :current_admin private def authenticate_admin! if current_admin.blank? redirect_to admin_login_path return end if current_admin.password_digest != session[:current_admin_token] redirect_to admin_login_path, alert: 'Password was changed, please log in again' return end end def current_admin @_current_admin ||= session[:current_admin_id] && Administrator.find_by(id: session[:current_admin_id]) end def admin_sign_in(admin) session[:current_admin_id] = admin.id session[:current_admin_token] = admin.password_digest end def admin_sign_out session[:current_admin_id] = nil session[:current_admin_token] = nil @_current_admin = nil end end ================================================ FILE: app/controllers/admin/comments_controller.rb ================================================ class Admin::CommentsController < Admin::BaseController before_action do @post = Post.find( params[:post_id] ) end def index @comments = @post.comments.order(created_at: :desc) end def destroy comment = @post.comments.find(params[:id]) if comment.destroy flash[:notice] = '删除评论成功' redirect_to admin_post_comments_path(@post) else flash[:alert] = '删除失败' redirect_to admin_post_comments_path(@post) end end end ================================================ FILE: app/controllers/admin/dashboard_controller.rb ================================================ class Admin::DashboardController < Admin::BaseController def index @posts_count = Post.all.count @comments_count = Comment.all.count end end ================================================ FILE: app/controllers/admin/labels_controller.rb ================================================ class Admin::LabelsController < Admin::BaseController def index @labels = Label.all.page(params[:page]) end def new @label = Label.new end def edit @label = Label.find(params[:id]) end def create @label = Label.new(label_params) if @label.save redirect_to admin_labels_path, notice: '创建成功' else render :new end end def update @label = Label.find(params[:id]) @label.update!(label_params) redirect_to admin_labels_path, notice: '更新成功' end def destroy @label = Label.find(params[:id]) @label.destroy! redirect_to admin_labels_path, notice: '删除成功' end private def label_params params.require(:label).permit(:name) end end ================================================ FILE: app/controllers/admin/posts_controller.rb ================================================ class Admin::PostsController < Admin::BaseController def new @post = Post.new end def edit @post = Post.find( params[:id] ) end def destroy @post = Post.find( params[:id] ) if @post.destroy flash[:notice] = '删除博客成功' redirect_to admin_posts_path else flash[:error] = '删除博客失败' redirect_to admin_posts_path end end def index @posts = Post.order(created_at: :desc).page(params[:page]).per(25) end def create @post = Post.new( post_params ) if @post.save flash[:notice] = '创建博客成功' redirect_to admin_posts_path else flash.now[:error] = '创建失败' render :new, status: 422 end end def update @post = Post.find( params[:id] ) if @post.update( post_params ) flash[:notice] = '更新博客成功' redirect_to admin_posts_path else flash[:error] = '更新博客失败' render :edit end end def preview render plain: Post.render_html(params[:content] || "") end private def post_params params.require(:post).permit(:title, :content, label_ids: []) end end ================================================ FILE: app/controllers/admin/sessions_controller.rb ================================================ class Admin::SessionsController < Admin::BaseController skip_before_action :authenticate_admin!, only: [:new, :create] before_action do @full_render = true end def new end def create admin = Administrator.find_by(name: params[:name]) if admin && admin.authenticate(params[:password]) admin_sign_in(admin) redirect_to admin_root_path else flash.now[:alert] = 'Username or password is wrong' render 'new' end end def destroy admin_sign_out redirect_to admin_login_path end end ================================================ FILE: app/controllers/application_controller.rb ================================================ class ApplicationController < ActionController::Base end ================================================ FILE: app/controllers/archives_controller.rb ================================================ class ArchivesController < ApplicationController def index if (@q = params[:q]).blank? @posts = Post.order(created_at: :desc).page(params[:page]) else @q_size = Post.where('title like ?', "%#{@q}%").size @posts = Post.where('title like ?', "%#{@q}%").order(created_at: :desc).page(params[:page]) end end end ================================================ FILE: app/controllers/blogs_controller.rb ================================================ class BlogsController < ApplicationController def show cookies[:cable_id] = SecureRandom.uuid @post = Post.find(params[:id]) @post.visited @prev = Post.where('created_at < ?', @post.created_at).order(created_at: :desc).first @next = Post.where('created_at > ?', @post.created_at).order(created_at: :asc).first @comments = @post.comments.order(created_at: :desc) @likes_count = @post.likes.count end def edit @post = Post.find(params[:id]) redirect_to edit_admin_post_path(@post) end end ================================================ FILE: app/controllers/comments_controller.rb ================================================ class CommentsController < ApplicationController layout false def create cookies[:name] = comment_params[:name] cookies[:email] = comment_params[:email] @post = Post.find( params[:blog_id] ) @comments = @post.comments.order(created_at: :desc) # # 某些原因暂时关闭评论 flash.now[:notice] = '评论功能未开放' return @comment = @post.comments.build(comment_params) if @comment.save flash.now[:notice] = '发表成功' # 重置评论 @comment = Comment.new end end def refresh @post = Post.find(params[:blog_id]) @comments = @post.comments.order(created_at: :desc) end private def comment_params params.require(:comment).permit(:content, :name, :email) end end ================================================ FILE: app/controllers/concerns/.keep ================================================ ================================================ FILE: app/controllers/home_controller.rb ================================================ class HomeController < ApplicationController def index @newest = Post.order(created_at: :desc).first @recent = Post.order(created_at: :desc).to_a[1..3] end def about end end ================================================ FILE: app/controllers/likes_controller.rb ================================================ class LikesController < ApplicationController layout false def index post = Post.find( params[:blog_id] ) render :json=> { success: true, count: post.liked_count } end def create post = Post.find( params[:blog_id] ) like = post.likes.build if like.save render :json=> { success: true, id: like.id.to_s, count: post.liked_count } else render :json=> { success: false, count: post.liked_count } end end def destroy post = Post.find( params[:blog_id] ) like = post.likes.find(params[:id]) if like.destroy render :json=> { success: true, count: post.reload.liked_count } else render :json=> { success: false, count: post.reload.liked_count } end end end ================================================ FILE: app/controllers/photos_controller.rb ================================================ class PhotosController < ApplicationController def create @photo = Photo.new(image: params["Filedata"]) @photo.save! render plain: md_url(@photo.image.url) end private def md_url(url) "![](#{url})" end end ================================================ FILE: app/helpers/application_helper.rb ================================================ module ApplicationHelper # Generate `{controller}-{action}-page` class for body element def body_class path = controller_path.tr('/_', '-') action_name_map = { index: 'index', new: 'edit', edit: 'edit', update: 'edit', patch: 'edit', create: 'edit', destory: 'index' } mapped_action_name = action_name_map[action_name.to_sym] || action_name body_class_page = if controller.is_a?(HighVoltage::StaticPage) && params.key?(:id) && params[:id] !~ /\A[-+]?[0-9]*\.?[0-9]+\Z/ id_name = params[:id].tr('_', '-') + '-page' format('%s-%s', 'pages', id_name) else format('%s-%s-page', path, mapped_action_name) end body_class_page end # Admin active for helper def admin_active_for(controller_name, navbar_name) if controller_name.to_s == admin_root_path return controller_name.to_s == navbar_name.to_s ? "active" : "" end navbar_name.to_s.include?(controller_name.to_s) ? 'active' : '' end def current_path request.env['PATH_INFO'] end def flash_class(level) case level when 'notice', 'success' then 'alert alert-success alert-dismissible' when 'info' then 'alert alert-info alert-dismissible' when 'warning' then 'alert alert-warning alert-dismissible' when 'alert', 'error' then 'alert alert-danger alert-dismissible' end end def format_time(time) time.strftime("%Y-%m-%d %H:%M") end def format_date(time) time.strftime("%Y.%m.%d") end def search_highlight(title, q) return title if q.blank? title.sub(q, "#{q}") end end ================================================ FILE: app/javascript/about.js ================================================ import './libs/ddscrollspy' $(document).on('turbo:load', function(){ if($('.home-about-page').length === 0) { return; } $(window).scroll(function(){ if($(this).scrollTop() > 0) $('.top-bar-wrapper').addClass('active') else $('.top-bar-wrapper').removeClass('active') }) $('#about-top-bar').ddscrollSpy({highlightclass: 'active'}) $('#about-anchor-link').click(function(e){ e.preventDefault() $('html, body').animate({ scrollTop: $('#about').offset().top },500) }) }) ================================================ FILE: app/javascript/admin/posts.js ================================================ $(document).on('turbo:load', function(){ $('a#upload_photo').click(function(){ $('input[type=file]').show().focus().click().hide() return false }) var opt = { type: 'POST', url: "/photos", success: function(data,status,xhr){ txtBox = $("#content-input") caret_pos = txtBox.caret('pos') src_merged = "\n" + data + "\n" source = txtBox.val() before_text = source.slice(0, caret_pos) txtBox.val(before_text + src_merged + source.slice(caret_pos+1, source.count)) txtBox.caret('pos',caret_pos + src_merged.length) txtBox.focus() } } $('input[type=file]').fileUpload(opt) $('#preview-link').on('show.bs.tab', function(e){ $('#preview').text('Loading...') $.ajax({ url: '/admin/posts/preview', type: 'POST', data: { content: $('#content-input').val() }, success: function(data){ $('#preview').html(data) } }) }) }) ================================================ FILE: app/javascript/admin/sidebar.js ================================================ // (function() { // window.App = window.App || {} // window.App.adminSidebar = { // saveSidebarScrollPosition: function() { // var sidebar = this.page().find('.sidebar'); // var sidebarScrollTop = sidebar.scrollTop(); // localStorage.setItem('admin-SidebarScrollTop', sidebarScrollTop); // }, // restoreSidebarScrollPosition: function() { // var sidebar = this.page().find('.sidebar'); // var sidebarScrollTop = localStorage.getItem('admin-SidebarScrollTop'); // sidebar.scrollTop(sidebarScrollTop); // }, // clearSidebarScrollPosition: function() { // localStorage.setItem('admin-SidebarScrollTop', 0); // }, // page: function() { // return $('.admin-page'); // } // }; // }).call(this); // $(document).on('turbo:load', function() { // var component = $('.admin-page'); // if (component.length > 0) { // App.adminSidebar.restoreSidebarScrollPosition(); // } // }); // $(document).on('turbo:before-render', function() { // var component = $('.admin-page'); // if (component.length > 0) { // App.adminSidebar.saveSidebarScrollPosition(); // } else { // App.adminSidebar.clearSidebarScrollPosition(); // } // }); ================================================ FILE: app/javascript/admin.js ================================================ import './base' import './libs/jquery.html5-fileupload' import './libs/jquery.atwho' import 'admin-lte' import './admin/sidebar' import './admin/posts' ================================================ FILE: app/javascript/application.js ================================================ // Entry point for the build script in your package.json // import './base' import './about' ================================================ FILE: app/javascript/base.js ================================================ // base dependency library, it should be only shared by `admin.js` and `application.js`. // import './libs/add_jquery' import 'bootstrap/dist/js/bootstrap' import RailsUjs from '@rails/ujs' import "@hotwired/turbo-rails" import * as ActiveStorage from '@rails/activestorage' // Turbo.session.drive = false RailsUjs.start() ActiveStorage.start() import './channels' import "./controllers" $(document).on('turbo:load', function(){ $('[data-toggle="tooltip"]').tooltip() }) ================================================ FILE: app/javascript/channels/consumer.js ================================================ // Action Cable provides the framework to deal with WebSockets in Rails. // You can generate new channels where WebSocket features live using the `bin/rails generate channel` command. import { createConsumer } from "@rails/actioncable" export default createConsumer() ================================================ FILE: app/javascript/channels/index.js ================================================ // Load all the channels within this directory and all subdirectories. // Channel files must be named *_channel.js. ================================================ FILE: app/javascript/controllers/admin_label_controller.js ================================================ import { Controller } from "@hotwired/stimulus" import select2 from 'select2' window.select2 = select2(); export default class extends Controller { static targets = [ 'label' ] connect() { $(this.labelTarget).select2({ multiple: true, tags: false, }) } disconnected() { $(this.labelTarget).select2('destroy') } } ================================================ FILE: app/javascript/controllers/index.js ================================================ // Load all the controllers within this directory and all subdirectories. // Controller files must be named *_controller.js or *_controller.ts. import { Application } from "@hotwired/stimulus" const application = Application.start() import LikeController from "./like_controller" application.register("like", LikeController) import QrcodeController from "./qrcode_controller" application.register("qrcode", QrcodeController) import AdminLabelController from "./admin_label_controller" application.register("admin-label", AdminLabelController) ================================================ FILE: app/javascript/controllers/like_controller.js ================================================ import { Controller } from "@hotwired/stimulus" import Cookies from 'js-cookie' export default class extends Controller { static targets = [ 'button' ] toggle(e) { let button = $(this.buttonTarget) if( button.hasClass('liked') ){ $.ajax({ url: button.data('url') + '/' + Cookies.get('like'), type: 'DELETE', success: function(res){ button.removeClass('liked') button.children('.count').text(res.count) Cookies.remove('like') } }) }else{ $.ajax({ url: button.data('url'), type: 'POST', success: function(res){ button.addClass('liked') button.children('.count').text(res.count) Cookies.set('like', res.id) } }) } } } ================================================ FILE: app/javascript/controllers/qrcode_controller.js ================================================ import { Controller } from "@hotwired/stimulus" import '../libs/qrcode' export default class extends Controller { static targets = [ 'wrapper' ] connect() { } greet(e) { e.preventDefault() $('#image-tag').empty() new QRCode( $('#image-tag')[0], $('#image-tag').data('url') ) $(this.wrapperTarget).toggle() } } ================================================ FILE: app/javascript/ga.js.erb ================================================ <% if ENV['GA'].present? %> var script = document.createElement('script'); script.src = 'https://www.googletagmanager.com/gtag/js?id=<%= ENV['GA'] %>'; window.document.getElementsByTagName('head')[0].appendChild(script); window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); document.addEventListener('turbo:load', function(event){ if (typeof gtag === 'function') { if(typeof gon !== 'undefined' && gon.user_id){ gtag('set', { 'user_id': gon.user_id }) } gtag('config', '<%= ENV['GA'] %>', { 'page_location': event.data.url }); } }); <% end %> ================================================ FILE: app/javascript/libs/add_jquery.js ================================================ import $ from 'jquery' window.jQuery = $ window.$ = $ ================================================ FILE: app/javascript/libs/ddscrollspy.js ================================================ /* * DD ScrollSpy Menu Script (c) Dynamic Drive (www.dynamicdrive.com) * Last updated: Aug 1st, 14' * Visit http://www.dynamicdrive.com/ for this script and 100s more. */ // Aug 1st, 14': Updated to v1.2, which supports showing a progress bar inside each menu item (except in iOS devices). Other minor improvements. if (!Array.prototype.filter){ Array.prototype.filter = function(fun /*, thisp */){ "use strict"; if (this == null) throw new TypeError(); var t = Object(this); var len = t.length >>> 0; if (typeof fun != "function") throw new TypeError(); var res = []; var thisp = arguments[1]; for (var i = 0; i < len; i++){ if (i in t){ var val = t[i]; // in case fun mutates this if (fun.call(thisp, val, i, t)) res.push(val); } } return res; }; } (function($){ var defaults = { spytarget: window, scrolltopoffset: 0, scrollbehavior: 'smooth', scrollduration: 500, highlightclass: 'selected', enableprogress: '', mincontentheight: 30 } var isiOS = /iPhone|iPad|iPod/i.test(navigator.userAgent) // detect iOS devices function inrange(el, range, field){ // check if "playing field" is inside range var rangespan = range[1]-range[0], fieldspan = field[1]-field[0] if ( (range[0]-field[0]) >= 0 && (range[0]-field[0]) < fieldspan ){ // if top of range is on field return true } else{ if ( (range[0]-field[0]) <= 0 && (range[0]+rangespan) > field[0] ){ // if part of range overlaps field return true } } return false } $.fn.ddscrollSpy = function(options){ var $window = $(window) var $body=(window.opera)? (document.compatMode=="CSS1Compat"? $('html') : $('body')) : $('html,body') return this.each(function(){ var o = $.extend({}, defaults, options) o.enableprogress = (isiOS)? '' : o.enableprogress // disable enableprogress in iOS var targets = [], curtarget = '' var cantscrollpastindex = -1 // index of target content that can't be scrolled past completely when scrollbar is at the end of the doc var $spytarget = $( o.spytarget ).eq(0) var spyheight = $spytarget.outerHeight() var spyscrollheight = (o.spytarget == window)? $body.get(0).scrollHeight : $spytarget.get(0).scrollHeight var $menu = $(this) var totaltargetsheight = 0 // total height of target contents function spyonmenuitems($menu){ var $menuitems = $menu.find('a[href^="#"]') targets = [] curtarget = '' totaltargetsheight = 0 $menuitems.each(function(i){ var $item = $(this) var $target = $( $item.attr('href') ) var target = $target.get(0) var $progress = null // progress DIV that gets dynamically added inside menu A element if o.enableprogress enabled if ($target.length == 0) // if no matching links found return true $item .off('click.goto') .on('click.goto', function(e){ if ( o.spytarget == window && (o.scrollbehavior == 'jump' || !history.pushState)) window.location.hash = $item.attr('href') if (o.scrollbehavior == 'smooth' || o.scrolltopoffset !=0){ var $scrollparent = (o.spytarget == window)? $body : $spytarget var addoffset = 1 // add 1 pixel to scrollTop when scrolling to an element to make sure the browser always returns the correct target element (strange bug) if (o.scrollbehavior == 'smooth' && (history.pushState || o.spytarget != window)){ $scrollparent.animate( {scrollTop: targets[i].offsettop + addoffset}, o.scrollduration, function(){ if (o.spytarget == window && history.pushState){ //history.pushState(null, null, $item.attr('href')) } }) } else{ $scrollparent.prop('scrollTop', targets[i].offsettop + addoffset) } e.preventDefault() } }) if (o.enableprogress){ // if o.enableprogress enabled if ($item.find('div.' + o.enableprogress).length == 0){ //if no progress DIV found inside menu item $item.css({position: 'relative', overflow: 'hidden'}) // add some required style to parent A element $('
').appendTo($item) } $progress = $item.find('div.' + o.enableprogress) } var targetoffset = (o.spytarget == window)? $target.offset().top : (target.offsetParent == o.spytarget)? target.offsetTop : target.offsetTop - o.spytarget.offsetTop targetoffset += o.scrolltopoffset var targetheight = ( parseInt($target.data('spyrange')) > 0 )? parseInt($target.data('spyrange')) : ( $target.outerHeight() || o.mincontentheight) var offsetbottom = targetoffset + targetheight if (cantscrollpastindex == -1 && offsetbottom > (spyscrollheight - spyheight)){ // determine index of first target which can't be scrolled past cantscrollpastindex = i } targets.push( {$menuitem: $item, $des: $target, offsettop: targetoffset, height: targetheight, $progress: $progress, index: i} ) }) if (targets.length > 0) totaltargetsheight = targets[targets.length-1].offsettop + targets[targets.length-1].height } function highlightitem(){ if (targets.length == 0) return var prevtarget = curtarget var scrolltop = $spytarget.scrollTop() var cantscrollpasttarget = false var shortlist = targets.filter(function(el, index){ // filter target elements that are currently visible on screen return inrange(el, [el.offsettop, el.offsettop + el.height], [scrolltop, scrolltop + spyheight]) }) if (shortlist.length > 0){ curtarget = shortlist.shift() // select the first element that's visible on screen if (prevtarget && prevtarget != curtarget) prevtarget.$menuitem.removeClass(o.highlightclass) if (!curtarget.$menuitem.hasClass(o.highlightclass)) // if there was a previously selected menu link and it's not the same as current curtarget.$menuitem.addClass(o.highlightclass) // highlight its menu item if (curtarget.index >= cantscrollpastindex && scrolltop >= (spyscrollheight - spyheight)){ // if we're at target that can't be scrolled past and we're at end of document if (o.enableprogress){ // if o.enableprogress enabled for (var i=0; i curtarget.index){ targets[i].$menuitem.find('div.' + o.enableprogress).css('left', '-100%') } } } } else if (scrolltop > totaltargetsheight){ // if no target content visible on screen but scroll bar has scrolled past very last content already if (o.enableprogress){ // if o.enableprogress enabled curtarget.$menuitem.removeClass(o.highlightclass) for (var i=0; i 0 )? parseInt($target.data('spyrange')) : ( $target.outerHeight() || o.mincontentheight) if (o.enableprogress){ // if o.enableprogress enabled var offsetbottom = targetoffset + targets[i].height // recalculate cantscrollpastindex if (cantscrollpastindex == -1 && offsetbottom > (spyscrollheight - spyheight)){ cantscrollpastindex = i } } } totaltargetsheight = targets[targets.length-1].offsettop + targets[targets.length-1].height } spyonmenuitems($menu) $menu.on('updatespy', function(){ spyonmenuitems($menu) highlightitem() }) $spytarget.on('scroll resize', function(){ highlightitem() }) highlightitem() $window.on('load resize', function(){ updatetargetpos() }) }) // end return } })(jQuery); ================================================ FILE: app/javascript/libs/jquery.atwho.js ================================================ // /* Implement Github like autocomplete mentions http://ichord.github.com/At.js Copyright (c) 2013 chord.luo@gmail.com Licensed under the MIT license. */ (function() { (function(factory) { if (typeof define === 'function' && define.amd) { return define(['jquery'], factory); } else { return factory(window.jQuery); } })(function($) { "use strict"; var Caret, Mirror, methods, pluginName; pluginName = 'caret'; Caret = (function() { function Caret($inputor) { this.$inputor = $inputor; this.domInputor = this.$inputor[0]; } Caret.prototype.getPos = function() { var end, endRange, inputor, len, normalizedValue, pos, range, start, textInputRange; inputor = this.domInputor; inputor.focus(); if (document.selection) { /* #assume we select "HATE" in the inputor such as textarea -> { }. * start end-point. * / * < I really [HATE] IE > between the brackets is the selection range. * \ * end end-point. */ range = document.selection.createRange(); pos = 0; if (range && range.parentElement() === inputor) { normalizedValue = inputor.value.replace(/\r\n/g, "\n"); /* SOMETIME !!! "/r/n" is counted as two char. one line is two, two will be four. balalala. so we have to using the normalized one's length.; */ len = normalizedValue.length; /* <[ I really HATE IE ]>: the whole content in the inputor will be the textInputRange. */ textInputRange = inputor.createTextRange(); /* _here must be the position of bookmark. / <[ I really [HATE] IE ]> [---------->[ ] : this is what moveToBookmark do. < I really [[HATE] IE ]> : here is result. \ two brackets in should be in line. */ textInputRange.moveToBookmark(range.getBookmark()); endRange = inputor.createTextRange(); /* [--------------------->[] : if set false all end-point goto end. < I really [[HATE] IE []]> */ endRange.collapse(false); /* ___VS____ / \ < I really [[HATE] IE []]> \_endRange end-point. " > -1" mean the start end-point will be the same or right to the end end-point * simplelly, all in the end. */ if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) { start = end = len; } else { /* I really |HATE] IE ]> <-| I really[ [HATE] IE ]> <-[ I reall[y [HATE] IE ]> will return how many unit have moved. */ start = -textInputRange.moveStart("character", -len); end = -textInputRange.moveEnd("character", -len); } } } else { start = inputor.selectionStart; } return start; }; Caret.prototype.setPos = function(pos) { var inputor, range; inputor = this.domInputor; if (document.selection) { range = inputor.createTextRange(); range.move("character", pos); return range.select(); } else { return inputor.setSelectionRange(pos, pos); } }; Caret.prototype.getPosition = function(pos) { var $inputor, at_rect, format, h, html, mirror, start_range, x, y; $inputor = this.$inputor; format = function(value) { return value.replace(//g, '>').replace(/`/g, '`').replace(/"/g, '"').replace(/\r\n|\r|\n/g, "
"); }; if (pos === void 0) { pos = this.getPos(); } start_range = $inputor.val().slice(0, pos); html = "" + format(start_range) + ""; html += "|"; mirror = new Mirror($inputor); at_rect = mirror.create(html).rect(); x = at_rect.left - $inputor.scrollLeft(); y = at_rect.top - $inputor.scrollTop(); h = at_rect.height; return { left: x, top: y, height: h }; }; Caret.prototype.getOffset = function(pos) { var $inputor, h, offset, position, x, y; $inputor = this.$inputor; offset = $inputor.offset(); position = this.getPosition(pos); x = offset.left + position.left; y = offset.top + position.top; h = position.height; return { left: x, top: y, height: h }; }; Caret.prototype.getIEPosition = function(pos) { var h, inputorOffset, offset, x, y; offset = this.getIEOffset(pos); inputorOffset = this.$inputor.offset(); x = offset.left - inputorOffset.left; y = offset.top - inputorOffset.top; h = offset.height; return { left: x, top: y, height: h }; }; Caret.prototype.getIEOffset = function(pos) { var h, range, x, y; range = this.domInputor.createTextRange(); if (pos) { range.move('character', pos); } x = range.boundingLeft + $inputor.scrollLeft(); y = range.boundingTop + $(window).scrollTop() + $inputor.scrollTop(); h = range.boundingHeight; return { left: x, top: y, height: h }; }; return Caret; })(); Mirror = (function() { Mirror.prototype.css_attr = ["overflowY", "height", "width", "paddingTop", "paddingLeft", "paddingRight", "paddingBottom", "marginTop", "marginLeft", "marginRight", "marginBottom", "fontFamily", "borderStyle", "borderWidth", "wordWrap", "fontSize", "lineHeight", "overflowX", "text-align"]; function Mirror($inputor) { this.$inputor = $inputor; } Mirror.prototype.mirrorCss = function() { var css, _this = this; css = { position: 'absolute', left: -9999, top: 0, zIndex: -20000, 'white-space': 'pre-wrap' }; $.each(this.css_attr, function(i, p) { return css[p] = _this.$inputor.css(p); }); return css; }; Mirror.prototype.create = function(html) { this.$mirror = $('
'); this.$mirror.css(this.mirrorCss()); this.$mirror.html(html); this.$inputor.after(this.$mirror); return this; }; Mirror.prototype.rect = function() { var $flag, pos, rect; $flag = this.$mirror.find("#caret"); pos = $flag.position(); rect = { left: pos.left, top: pos.top, height: $flag.height() }; this.$mirror.remove(); return rect; }; return Mirror; })(); methods = { pos: function(pos) { if (pos) { return this.setPos(pos); } else { return this.getPos(); } }, position: function(pos) { if (document.selection) { return this.getIEPosition(pos); } else { return this.getPosition(pos); } }, offset: function(pos) { if (document.selection) { return this.getIEOffset(pos); } else { return this.getOffset(pos); } } }; return $.fn.caret = function(method) { var caret; caret = new Caret(this); if (methods[method]) { return methods[method].apply(caret, Array.prototype.slice.call(arguments, 1)); } else { return $.error("Method " + method + " does not exist on jQuery.caret"); } }; }); }).call(this); /* Implement Github like autocomplete mentions http://ichord.github.com/At.js Copyright (c) 2013 chord.luo@gmail.com Licensed under the MIT license. */ (function() { var __slice = [].slice; (function(factory) { if (typeof define === 'function' && define.amd) { return define(['jquery'], factory); } else { return factory(window.jQuery); } })(function($) { var $CONTAINER, Api, App, Controller, DEFAULT_CALLBACKS, DEFAULT_TPL, KEY_CODE, Model, View; App = (function() { function App(inputor) { this.current_flag = null; this.controllers = {}; this.$inputor = $(inputor); this.listen(); } App.prototype.controller = function(key) { return this.controllers[key || this.current_flag]; }; App.prototype.set_context_for = function(key) { this.current_flag = key; return this; }; App.prototype.reg = function(flag, setting) { var controller, _base; controller = (_base = this.controllers)[flag] || (_base[flag] = new Controller(this, flag)); if (setting.alias) { this.controllers[setting.alias] = controller; } controller.init(setting); return this; }; App.prototype.listen = function() { var _this = this; return this.$inputor.on('keyup.atwho', function(e) { return _this.on_keyup(e); }).on('keydown.atwho', function(e) { return _this.on_keydown(e); }).on('scroll.atwho', function(e) { var _ref; return (_ref = _this.controller()) != null ? _ref.view.hide() : void 0; }).on('blur.atwho', function(e) { var c; if (c = _this.controller()) { return c.view.hide(c.get_opt("display_timeout")); } }); }; App.prototype.dispatch = function() { var _this = this; return $.map(this.controllers, function(c) { if (c.look_up()) { return _this.set_context_for(c.key); } }); }; App.prototype.on_keyup = function(e) { var _ref; switch (e.keyCode) { case KEY_CODE.ESC: e.preventDefault(); if ((_ref = this.controller()) != null) { _ref.view.hide(); } break; case KEY_CODE.DOWN: case KEY_CODE.UP: $.noop(); break; default: this.dispatch(); } }; App.prototype.on_keydown = function(e) { var view, _ref; view = (_ref = this.controller()) != null ? _ref.view : void 0; if (!(view && view.visible())) { return; } switch (e.keyCode) { case KEY_CODE.ESC: e.preventDefault(); view.hide(); break; case KEY_CODE.UP: e.preventDefault(); view.prev(); break; case KEY_CODE.DOWN: e.preventDefault(); view.next(); break; case KEY_CODE.TAB: case KEY_CODE.ENTER: if (!view.visible()) { return; } e.preventDefault(); view.choose(); break; default: $.noop(); } }; return App; })(); Controller = (function() { var uuid, _uuid; _uuid = 0; uuid = function() { return _uuid += 1; }; function Controller(app, key) { this.app = app; this.key = key; this.$inputor = this.app.$inputor; this.id = this.$inputor[0].id || uuid(); this.setting = null; this.query = null; this.pos = 0; $CONTAINER.append(this.$el = $("
")); this.model = new Model(this); this.view = new View(this); } Controller.prototype.init = function(setting) { this.setting = $.extend({}, this.setting || $.fn.atwho["default"], setting); return this.model.reload(this.setting.data); }; Controller.prototype.call_default = function() { var args, func_name; func_name = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; try { return DEFAULT_CALLBACKS[func_name].apply(this, args); } catch (error) { return $.error("" + error + " Or maybe At.js doesn't have function " + func_name); } }; Controller.prototype.trigger = function(name, data) { var alias, event_name; data.push(this); alias = this.get_opt('alias'); event_name = alias ? "" + name + "-" + alias + ".atwho" : "" + name + ".atwho"; return this.$inputor.trigger(event_name, data); }; Controller.prototype.callbacks = function(func_name) { return this.get_opt("callbacks")[func_name] || DEFAULT_CALLBACKS[func_name]; }; Controller.prototype.get_opt = function(key, default_value) { try { return this.setting[key]; } catch (e) { return null; } }; Controller.prototype.catch_query = function() { var caret_pos, content, end, query, start, subtext; content = this.$inputor.val(); caret_pos = this.$inputor.caret('pos'); subtext = content.slice(0, caret_pos); query = this.callbacks("matcher").call(this, this.key, subtext, this.get_opt('start_with_space')); if (typeof query === "string" && query.length <= this.get_opt('max_len', 20)) { start = caret_pos - query.length; end = start + query.length; this.pos = start; query = { 'text': query.toLowerCase(), 'head_pos': start, 'end_pos': end }; this.trigger("matched", [this.key, query.text]); } else { this.view.hide(); } return this.query = query; }; Controller.prototype.rect = function() { var c, scale_bottom; c = this.$inputor.caret('offset', this.pos - 1); scale_bottom = document.selection ? 0 : 2; return { left: c.left, top: c.top, bottom: c.top + c.height + scale_bottom }; }; Controller.prototype.insert = function(str) { var $inputor, source, start_str, text; $inputor = this.$inputor; str = '' + str; source = $inputor.val(); start_str = source.slice(0, this.query['head_pos'] || 0); text = "" + start_str + str + " " + (source.slice(this.query['end_pos'] || 0)); $inputor.val(text); $inputor.caret('pos', start_str.length + str.length + 1); return $inputor.change(); }; Controller.prototype.render_view = function(data) { var search_key; search_key = this.get_opt("search_key"); data = this.callbacks("sorter").call(this, this.query.text, data.slice(0, 1001), search_key); return this.view.render(data.slice(0, this.get_opt('limit'))); }; Controller.prototype.look_up = function() { var query, _callback; if (!(query = this.catch_query())) { return; } _callback = function(data) { if (data && data.length > 0) { return this.render_view(data); } else { return this.view.hide(); } }; this.model.query(query.text, $.proxy(_callback, this)); return query; }; return Controller; })(); Model = (function() { var _storage; _storage = {}; function Model(context) { this.context = context; this.key = this.context.key; } Model.prototype.saved = function() { return this.fetch() > 0; }; Model.prototype.query = function(query, callback) { var data, search_key, _ref; data = this.fetch(); search_key = this.context.get_opt("search_key"); callback(data = this.context.callbacks('filter').call(this.context, query, data, search_key)); if (!(data && data.length > 0)) { return (_ref = this.context.callbacks('remote_filter')) != null ? _ref.call(this.context, query, callback) : void 0; } }; Model.prototype.fetch = function() { return _storage[this.key] || []; }; Model.prototype.save = function(data) { return _storage[this.key] = this.context.callbacks("before_save").call(this.context, data || []); }; Model.prototype.load = function(data) { if (!(this.saved() || !data)) { return this._load(data); } }; Model.prototype.reload = function(data) { return this._load(data); }; Model.prototype._load = function(data) { var _this = this; if (typeof data === "string") { return $.ajax(data, { dataType: "json" }).done(function(data) { return _this.save(data); }); } else { return this.save(data); } }; return Model; })(); View = (function() { function View(context) { this.context = context; this.key = this.context.key; this.id = this.context.get_opt("alias") || ("at-view-" + (this.key.charCodeAt(0))); this.$el = $("
    "); this.timeout_id = null; this.context.$el.append(this.$el); this.bind_event(); } View.prototype.bind_event = function() { var $menu, _this = this; $menu = this.$el.find('ul'); return $menu.on('mouseenter.view', 'li', function(e) { $menu.find('.cur').removeClass('cur'); return $(e.currentTarget).addClass('cur'); }).on('click', function(e) { _this.choose(); return e.preventDefault(); }); }; View.prototype.visible = function() { return this.$el.is(":visible"); }; View.prototype.choose = function() { var $li; $li = this.$el.find(".cur"); this.context.insert(this.context.callbacks("before_insert").call(this.context, $li.data("value"), $li)); this.context.trigger("inserted", [$li]); return this.hide(); }; View.prototype.reposition = function() { var offset, rect; rect = this.context.rect(); if (rect.bottom + this.$el.height() - $(window).scrollTop() > $(window).height()) { rect.bottom = rect.top - this.$el.height(); } offset = { left: rect.left, top: rect.bottom }; this.$el.offset(offset); return this.context.trigger("reposition", [offset]); }; View.prototype.next = function() { var cur, next; cur = this.$el.find('.cur').removeClass('cur'); next = cur.next(); if (!next.length) { next = this.$el.find('li:first'); } return next.addClass('cur'); }; View.prototype.prev = function() { var cur, prev; cur = this.$el.find('.cur').removeClass('cur'); prev = cur.prev(); if (!prev.length) { prev = this.$el.find('li:last'); } return prev.addClass('cur'); }; View.prototype.show = function() { if (!this.visible()) { this.$el.show(); } return this.reposition(); }; View.prototype.hide = function(time) { var callback, _this = this; if (isNaN(time && this.visible())) { return this.$el.hide(); } else { callback = function() { return _this.hide(); }; clearTimeout(this.timeout_id); return this.timeout_id = setTimeout(callback, time); } }; View.prototype.render = function(list) { var $li, $ul, item, li, tpl, _i, _len; if (!$.isArray(list || list.length <= 0)) { this.hide(); return; } this.$el.find('ul').empty(); $ul = this.$el.find('ul'); tpl = this.context.get_opt('tpl', DEFAULT_TPL); for (_i = 0, _len = list.length; _i < _len; _i++) { item = list[_i]; li = this.context.callbacks("tpl_eval").call(this.context, tpl, item); $li = $(this.context.callbacks("highlighter").call(this.context, li, this.context.query.text)); $li.data("atwho-info", item); $ul.append($li); } this.show(); return $ul.find("li:first").addClass("cur"); }; return View; })(); KEY_CODE = { DOWN: 40, UP: 38, ESC: 27, TAB: 9, ENTER: 13 }; DEFAULT_CALLBACKS = { before_save: function(data) { var item, _i, _len, _results; if (!$.isArray(data)) { return data; } _results = []; for (_i = 0, _len = data.length; _i < _len; _i++) { item = data[_i]; if ($.isPlainObject(item)) { _results.push(item); } else { _results.push({ name: item }); } } return _results; }, matcher: function(flag, subtext, should_start_with_space) { var match, regexp; flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); if (should_start_with_space) { flag = '(?:^|\\s)' + flag; } regexp = new RegExp(flag + '([A-Za-z0-9_\+\-]*)$|' + flag + '([^\\x00-\\xff]*)$', 'gi'); match = regexp.exec(subtext); if (match) { return match[2] || match[1]; } else { return null; } }, filter: function(query, data, search_key) { var item, _i, _len, _results; _results = []; for (_i = 0, _len = data.length; _i < _len; _i++) { item = data[_i]; if (~item[search_key].toLowerCase().indexOf(query)) { _results.push(item); } } return _results; }, remote_filter: null, sorter: function(query, items, search_key) { var item, _i, _len, _results; if (!query) { return items; } _results = []; for (_i = 0, _len = items.length; _i < _len; _i++) { item = items[_i]; item.atwho_order = item[search_key].toLowerCase().indexOf(query); if (item.atwho_order > -1) { _results.push(item); } } return _results.sort(function(a, b) { return a.atwho_order - b.atwho_order; }); }, tpl_eval: function(tpl, map) { try { return tpl.replace(/\$\{([^\}]*)\}/g, function(tag, key, pos) { return map[key]; }); } catch (error) { return ""; } }, highlighter: function(li, query) { var regexp; if (!query) { return li; } regexp = new RegExp(">\\s*(\\w*)(" + query.replace("+", "\\+") + ")(\\w*)\\s*<", 'ig'); return li.replace(regexp, function(str, $1, $2, $3) { return '> ' + $1 + '' + $2 + '' + $3 + ' <'; }); }, before_insert: function(value, $li) { return value; } }; DEFAULT_TPL = "
  • ${name}
  • "; Api = { init: function(options) { var $this, app; app = ($this = $(this)).data("atwho"); if (!app) { $this.data('atwho', (app = new App(this))); } return app.reg(options.at, options); }, load: function(key, data) { var c; if (c = this.controller(key)) { return c.model.load(data); } }, run: function() { return this.dispatch(); } }; $CONTAINER = $("
    "); $.fn.atwho = function(method) { var _args; _args = arguments; $('body').append($CONTAINER); return this.filter('textarea, input').each(function() { var app; if (typeof method === 'object' || !method) { return Api.init.apply(this, _args); } else if (Api[method]) { if (app = $(this).data('atwho')) { return Api[method].apply(app, Array.prototype.slice.call(_args, 1)); } } else { return $.error("Method " + method + " does not exist on jQuery.caret"); } }); }; return $.fn.atwho["default"] = { at: void 0, alias: void 0, data: null, tpl: DEFAULT_TPL, callbacks: DEFAULT_CALLBACKS, search_key: "name", limit: 5, max_len: 20, start_with_space: true, display_timeout: 300 }; }); }).call(this); ================================================ FILE: app/javascript/libs/jquery.html5-fileupload.js ================================================ /* * jQuery HTML5 File Upload * * Author: timdream at gmail.com * Web: http://timc.idv.tw/html5-file-upload/ * * Ajax File Upload that use real xhr, * built with getAsBinary, sendAsBinary, FormData, FileReader, ArrayBuffer, BlobBuilder and etc. * works in Firefox 3, Chrome 5, Safari 5 and higher * * Image resizing and uploading currently works in Fx 3 and up, and Chrome 9 (dev) and up only. * Extra settings will allow current Webkit users to upload the original image * or send the resized image in base64 form. * * Usage: * $.fileUploadSupported // a boolean value indicates if the browser is supported. * $.imageUploadSupported // a boolean value indicates if the browser could resize image and upload in binary form. * $.fileUploadAsBase64Supported // a boolean value indicate if the browser upload files in based64. * $.imageUploadAsBase64Supported // a boolean value indicate if the browser could resize image and upload in based64. * $('input[type=file]').fileUpload(ajaxSettings); //Make a input[type=file] select-and-send file upload widget * $('#any-element').fileUpload(ajaxSettings); //Make a element receive dropped file * //TBD $('form#fileupload').fileUpload(ajaxSettings); //Send a ajax form with file * //TBD $('canvas').fileUpload(ajaxSettings); //Upload given canvas as if it's an png image. * * ajaxSettings is the object contains $.ajax settings that will be passed to. * Available extended settings are: * fileType: * regexp check against filename extension; You should always checked it again on server-side. * e.g. /^(gif|jpe?g|png|tiff?)$/i for images * fileMaxSize: * Maxium file size allowed in bytes. Use scientific notation for converience. * e.g. 1E4 for 1KB, 1E8 for 1MB, 1E9 for 10MB. * If you really care the difference between 1024 and 1000, use Math.pow(2, 10) * fileError(info, textStatus, textDescription): * callback function when there is any error preventing file upload to start, * $.ajax and ajax events won't be called when error. * Use $.noop to overwrite default alert function. * imageMaxWidth, imageMaxHeight: * Use any of the two settings to enable client-size image resizing. * Image will be resized to fit into given rectangle. * File size and type limit checking will be ignored. * allowUploadOriginalImage: * Set to true if you accept original image to be uploaded as a fallback * when image resizing functionality is not availible (such as Webkit browsers). * File size and type limit will be enforced. * allowDataInBase64: * Alternatively, you may wish to resize the image anyway and send the data * in base64. The data will be 133% larger and you will need to process it further with * server-side script. * This setting might work with browsers which could read file but cannot send it in original * binary (no known browser are designed this way though) * forceResize: * Set to true will cause the image being re-sampled even if the resized image * has the same demension as the original one. * imageType: * Acceptable values are: 'jpeg', 'png', or 'auto'. * * TBD: * ability to change settings after binding (you can unbind and bind again as a workaround) * multipole file handling * form intergation * */ (function($) { // Don't do logging if window.log function does not exist. var log = window.console.log || $.noop; // jQuery.ajax config var config = { fileError: function (info, textStatus, textDescription) { window.alert(textDescription); } }; // Feature detection // Read as binary string: FileReader API || Gecko-specific function (Fx3) var canReadAsBinaryString = (window.FileReader || window.File.prototype.getAsBinary); // Read file using FormData interface var canReadFormData = !!(window.FormData); // Read file into data: URL: FileReader API || Gecko-specific function (Fx3) var canReadAsBase64 = (window.FileReader || window.File.prototype.getAsDataURL); var canResizeImageToBase64 = !!(document.createElement('canvas').toDataURL); var canResizeImageToBinaryString = canResizeImageToBase64 && window.atob; var canResizeImageToFile = !!(document.createElement('canvas').mozGetAsFile); // Send file in multipart/form-data with binary xhr (Gecko-specific function) // || xhr.send(blob) that sends blob made with ArrayBuffer. var canSendBinaryString = ( (window.XMLHttpRequest && window.XMLHttpRequest.prototype.sendAsBinary) || (window.ArrayBuffer && window.BlobBuilder) ); // Send file as in FormData object var canSendFormData = !!(window.FormData); // Send image base64 data by extracting data: URL var canSendImageInBase64 = !!(document.createElement('canvas').toDataURL); var isSupported = ( (canReadAsBinaryString && canSendBinaryString) || (canReadFormData && canSendFormData) ); var isImageSupported = ( canReadAsBase64 && ( (canResizeImageToBinaryString && canSendBinaryString) || (canResizeImageToFile && canSendFormData) ) ); var isSupportedInBase64 = canReadAsBase64; var isImageSupportedInBase64 = canReadAsBase64 && canResizeImageToBase64; var dataURLtoBase64 = function (dataurl) { return dataurl.substring(dataurl.indexOf(',')+1, dataurl.length); } // Step 1: check file info and attempt to read the file // paramaters: Ajax settings, File object var handleFile = function (settings, file) { var info = { // properties of standard File object || Gecko 1.9 properties type: file.type || '', // MIME type size: file.size || file.fileSize, name: file.name || file.fileName }; settings.resizeImage = !!(settings.imageMaxWidth || settings.imageMaxHeight); if (settings.resizeImage && !isImageSupported && settings.allowUploadOriginalImage) { log('WARN: Fall back to upload original un-resized image.'); settings.resizeImage = false; } if (settings.resizeImage) { settings.imageMaxWidth = settings.imageMaxWidth || Infinity; settings.imageMaxHeight = settings.imageMaxHeight || Infinity; } if (!settings.resizeImage) { if (settings.fileType && settings.fileType.test) { // Not using MIME types if (!settings.fileType.test(info.name.substr(info.name.lastIndexOf('.')+1))) { log('ERROR: Invalid Filetype.'); settings.fileError.call(this, info, 'INVALID_FILETYPE', 'Invalid filetype.'); return; } } if (settings.fileMaxSize && file.size > settings.fileMaxSize) { log('ERROR: File exceeds size limit.'); settings.fileError.call(this, info, 'FILE_EXCEEDS_SIZE_LIMIT', 'File exceeds size limit.'); return; } } if (!settings.resizeImage && canReadFormData) { log('INFO: Bypass file reading, insert file object into FormData object directly.'); handleForm(settings, 'file', file, info); } else if (window.FileReader) { log('INFO: Using FileReader to do asynchronously file reading.'); var reader = new FileReader(); reader.onerror = function (ev) { if (ev.target.error) { switch (ev.target.error) { case 8: log('ERROR: File not found.'); settings.fileError.call(this, info, 'FILE_NOT_FOUND', 'File not found.'); break; case 24: log('ERROR: File not readable.'); settings.fileError.call(this, info, 'IO_ERROR', 'File not readable.'); break; case 18: log('ERROR: File cannot be access due to security constrant.'); settings.fileError.call(this, info, 'SECURITY_ERROR', 'File cannot be access due to security constrant.'); break; case 20: //User Abort break; } } } if (!settings.resizeImage) { if (canSendBinaryString) { reader.onloadend = function (ev) { var bin = ev.target.result; handleForm(settings, 'bin', bin, info); }; reader.readAsBinaryString(file); } else if (settings.allowDataInBase64) { reader.onloadend = function (ev) { handleForm( settings, 'base64', dataURLtoBase64(ev.target.result), info ); }; reader.readAsDataURL(file); } else { log('ERROR: No available method to extract file; allowDataInBase64 not set.'); settings.fileError.call(this, info, 'NO_BIN_SUPPORT_AND_BASE64_NOT_SET', 'No available method to extract file; allowDataInBase64 not set.'); } } else { reader.onloadend = function (ev) { var dataurl = ev.target.result; handleImage(settings, dataurl, info); }; reader.readAsDataURL(file); } } else if (window.File.prototype.getAsBinary) { log('WARN: FileReader does not exist, UI will be blocked when reading big file.'); if (!settings.resizeImage) { try { var bin = file.getAsBinary(); } catch (e) { log('ERROR: File not readable.'); settings.fileError.call(this, info, 'IO_ERROR', 'File not readable.'); return; } handleForm(settings, 'bin', bin, info); } else { try { var bin = file.getAsDataURL(); } catch (e) { log('ERROR: File not readable.'); settings.fileError.call(this, info, 'IO_ERROR', 'File not readable.'); return; } handleImage(settings, dataurl, info); } } else { log('ERROR: No available method to extract file; this browser is not supported.'); settings.fileError.call(this, info, 'NOT_SUPPORT', 'ERROR: No available method to extract file; this browser is not supported.'); } }; // step 1.5: inject file into , paste the pixels into , // read the final image var handleImage = function (settings, dataurl, info) { var img = new Image(); img.onerror = function () { log('ERROR: failed to load, file is not a supported image format.'); settings.fileError.call(this, info, 'FILE_NOT_IMAGE', 'File is not a supported image format.'); }; img.onload = function () { var ratio = Math.max( img.width/settings.imageMaxWidth, img.height/settings.imageMaxHeight, 1 ); var d = { w: Math.floor(Math.max(img.width/ratio, 1)), h: Math.floor(Math.max(img.height/ratio, 1)) } log( 'INFO: Original image size: ' + img.width.toString(10) + 'x' + img.height.toString(10) + ', resized image size: ' + d.w + 'x' + d.h + '.' ); if (!settings.forceResize && img.width === d.w && img.height === d.h) { log('INFO: Image demension is the same, send the original file.'); if (canResizeImageToBinaryString) { handleForm( settings, 'bin', window.atob(dataURLtoBase64(dataurl)), info ); } else if (settings.allowDataInBase64) { handleForm( settings, 'base64', dataURLtoBase64(dataurl), info ); } else { log('ERROR: No available method to send the original file; allowDataInBase64 not set.'); settings.fileError.call(this, info, 'NO_BIN_SUPPORT_AND_BASE64_NOT_SET', 'No available method to extract file; allowDataInBase64 not set.'); } return; } var canvas = document.createElement('canvas'); canvas.setAttribute('width', d.w); canvas.setAttribute('height', d.h); canvas.getContext('2d').drawImage( img, 0, 0, img.width, img.height, 0, 0, d.w, d.h ); if (!settings.imageType || settings.imageType === 'auto') { if (info.type === 'image/jpeg') settings.imageType = 'jpeg'; else settings.imageType = 'png'; } var ninfo = { type: 'image/' + settings.imageType, name: info.name.substr(0, info.name.indexOf('.')) + '.resized.' + settings.imageType }; if (canResizeImageToFile && canSendFormData) { // Gecko 2 (Fx4) non-standard function var nfile = canvas.mozGetAsFile( ninfo.name, 'image/' + settings.imageType ); ninfo.size = file.size || file.fileSize; handleForm( settings, 'file', nfile, ninfo ); } else if (canResizeImageToBinaryString && canSendBinaryString) { // Read the image as DataURL, convert it back to binary string. var bin = window.atob(dataURLtoBase64(canvas.toDataURL('image/' + settings.imageType))); ninfo.size = bin.length; handleForm( settings, 'bin', bin, ninfo ); } else if (settings.allowDataInBase64 && canResizeImageToBase64 && canSendImageInBase64) { handleForm( settings, 'base64', dataURLtoBase64(canvas.toDataURL('image/' + settings.imageType)), ninfo ); } else { log('ERROR: No available method to extract image; allowDataInBase64 not set.'); settings.fileError.call(this, info, 'NO_BIN_SUPPORT_AND_BASE64_NOT_SET', 'No available method to extract file; allowDataInBase64 not set.'); } } img.src = dataurl; } // Step 2: construct form data and send the file // paramaters: Ajax settings, File object, binary string of file || null, file info assoc array var handleForm = function (settings, type, data, info) { if (canSendFormData && type === 'file') { // FormData API saves the day log('INFO: Using FormData to construct form.'); var formdata = new FormData(); formdata.append('Filedata', data); // Prevent jQuery form convert FormData object into string. settings.processData = false; // Prevent jQuery from overwrite automatically generated xhr content-Type header // by unsetting the default contentType and inject data only right before xhr.send() settings.contentType = null; settings.__beforeSend = settings.beforeSend; settings.beforeSend = function (xhr, s) { s.data = formdata; if (s.__beforeSend) return s.__beforeSend.call(this, xhr, s); } //settings.data = formdata; } else if (canSendBinaryString && type === 'bin') { log('INFO: Concat our own multipart/form-data data string.'); // A placeholder MIME type if (!info.type) info.type = 'application/octet-stream'; if (/[^\x20-\x7E]/.test(info.name)) { log('INFO: Filename contains non-ASCII code, do UTF8-binary string conversion.'); info.name_bin = unescape(encodeURIComponent(info.name)); } //filtered out non-ASCII chars in filenames // info.name = info.name.replace(/[^\x20-\x7E]/g, '_'); // multipart/form-data boundary var bd = 'xhrupload-' + parseInt(Math.random()*(2 << 16)); settings.contentType = 'multipart/form-data; boundary=' + bd; var formdata = '--' + bd + '\n' // RFC 1867 Format, simulate form file upload + 'content-disposition: form-data; name="Filedata";' + ' filename="' + (info.name_bin || info.name) + '"\n' + 'Content-Type: ' + info.type + '\n\n' + data + '\n\n' + '--' + bd + '--'; if (window.XMLHttpRequest.prototype.sendAsBinary) { // Use xhr.sendAsBinary that takes binary string log('INFO: Pass binary string to xhr.'); settings.data = formdata; } else { // make a blob log('INFO: Convert binary string into Blob.'); var buf = new ArrayBuffer(formdata.length); var view = new Uint8Array(buf); $.each( formdata, function (i, o) { view[i] = o.charCodeAt(0); } ); var bb = new BlobBuilder(); bb.append(buf); var blob = bb.getBlob(); settings.processData = false; settings.__beforeSend = settings.beforeSend; settings.beforeSend = function (xhr, s) { s.data = blob; if (s.__beforeSend) return s.__beforeSend.call(this, xhr, s); }; } } else if (settings.allowDataInBase64 && type === 'base64') { log('INFO: Concat our own multipart/form-data data string; send the file in base64 because binary xhr is not supported.'); // A placeholder MIME type if (!info.type) info.type = 'application/octet-stream'; // multipart/form-data boundary var bd = 'xhrupload-' + parseInt(Math.random()*(2 << 16)); settings.contentType = 'multipart/form-data; boundary=' + bd; settings.data = '--' + bd + '\n' // RFC 1867 Format, simulate form file upload + 'content-disposition: form-data; name="Filedata";' + ' filename="' + encodeURIComponent(info.name) + '.base64"\n' + 'Content-Transfer-Encoding: base64\n' // Vaild MIME header, but won't work with PHP file upload handling. + 'Content-Type: ' + info.type + '\n\n' + data + '\n\n' + '--' + bd + '--'; } else { log('ERROR: Data is not given in processable form.'); settings.fileError.call(this, info, 'INTERNAL_ERROR', 'Data is not given in processable form.'); return; } xhrupload(settings); }; // Step 3: start sending out file var xhrupload = function (settings) { log('INFO: Sending file.'); if (typeof settings.data === 'string' && canSendBinaryString) { log('INFO: Using xhr.sendAsBinary.'); settings.___beforeSend = settings.beforeSend; settings.beforeSend = function (xhr, s) { xhr.send = xhr.sendAsBinary; if (s.___beforeSend) return s.___beforeSend.call(this, xhr, s); } } $.ajax(settings); }; $.fn.fileUpload = function(settings) { this.each(function(i, el) { if ($(el).is('input[type=file]')) { log('INFO: binding onchange event to a input[type=file].'); $(el).bind( 'change', function () { if (!this.files.length) { log('ERROR: no file selected.'); return; } else if (this.files.length > 1) { log('WARN: Multiple file upload not implemented yet, only first file will be uploaded.'); } handleFile($.extend({}, config, settings), this.files[0]); if (this.form.length === 1) { this.form.reset(); } else { log('WARN: Unable to reset file selection, upload won\'t be triggered again if user selects the same file.'); } return; } ); } if ($(el).is('form')) { log('ERROR:
    not implemented yet.'); } else { log('INFO: binding ondrop event.'); $(el).bind( 'dragover', // dragover behavior should be blocked for drop to invoke. function(ev) { return false; } ).bind( 'drop', function (ev) { if (!ev.originalEvent.dataTransfer.files) { log('ERROR: No FileList object present; user might had dropped text.'); return false; } if (!ev.originalEvent.dataTransfer.files.length) { log('ERROR: User had dropped a virual file (e.g. "My Computer")'); return false; } if (!ev.originalEvent.dataTransfer.files.length > 1) { log('WARN: Multiple file upload not implemented yet, only first file will be uploaded.'); } handleFile($.extend({}, config, settings), ev.originalEvent.dataTransfer.files[0]); return false; } ); } }); return this; }; $.fileUploadSupported = isSupported; $.imageUploadSupported = isImageSupported; $.fileUploadAsBase64Supported = isSupportedInBase64; $.imageUploadAsBase64Supported = isImageSupportedInBase64; })(jQuery); ================================================ FILE: app/javascript/libs/qrcode.js ================================================ /** * @fileoverview * - Using the 'QRCode for Javascript library' * - Fixed dataset of 'QRCode for Javascript library' for support full-spec. * - this library has no dependencies. * * @author davidshimjs * @see http://www.d-project.com/ * @see http://jeromeetienne.github.com/jquery-qrcode/ */ var QRCode; (function () { //--------------------------------------------------------------------- // QRCode for JavaScript // // Copyright (c) 2009 Kazuhiko Arase // // URL: http://www.d-project.com/ // // Licensed under the MIT license: // http://www.opensource.org/licenses/mit-license.php // // The word "QR Code" is registered trademark of // DENSO WAVE INCORPORATED // http://www.denso-wave.com/qrcode/faqpatent-e.html // //--------------------------------------------------------------------- function QR8bitByte(data) { this.mode = QRMode.MODE_8BIT_BYTE; this.data = data; this.parsedData = []; // Added to support UTF-8 Characters for (var i = 0, l = this.data.length; i < l; i++) { var byteArray = []; var code = this.data.charCodeAt(i); if (code > 0x10000) { byteArray[0] = 0xF0 | ((code & 0x1C0000) >>> 18); byteArray[1] = 0x80 | ((code & 0x3F000) >>> 12); byteArray[2] = 0x80 | ((code & 0xFC0) >>> 6); byteArray[3] = 0x80 | (code & 0x3F); } else if (code > 0x800) { byteArray[0] = 0xE0 | ((code & 0xF000) >>> 12); byteArray[1] = 0x80 | ((code & 0xFC0) >>> 6); byteArray[2] = 0x80 | (code & 0x3F); } else if (code > 0x80) { byteArray[0] = 0xC0 | ((code & 0x7C0) >>> 6); byteArray[1] = 0x80 | (code & 0x3F); } else { byteArray[0] = code; } this.parsedData.push(byteArray); } this.parsedData = Array.prototype.concat.apply([], this.parsedData); if (this.parsedData.length != this.data.length) { this.parsedData.unshift(191); this.parsedData.unshift(187); this.parsedData.unshift(239); } } QR8bitByte.prototype = { getLength: function (buffer) { return this.parsedData.length; }, write: function (buffer) { for (var i = 0, l = this.parsedData.length; i < l; i++) { buffer.put(this.parsedData[i], 8); } } }; function QRCodeModel(typeNumber, errorCorrectLevel) { this.typeNumber = typeNumber; this.errorCorrectLevel = errorCorrectLevel; this.modules = null; this.moduleCount = 0; this.dataCache = null; this.dataList = []; } QRCodeModel.prototype={addData:function(data){var newData=new QR8bitByte(data);this.dataList.push(newData);this.dataCache=null;},isDark:function(row,col){if(row<0||this.moduleCount<=row||col<0||this.moduleCount<=col){throw new Error(row+","+col);} return this.modules[row][col];},getModuleCount:function(){return this.moduleCount;},make:function(){this.makeImpl(false,this.getBestMaskPattern());},makeImpl:function(test,maskPattern){this.moduleCount=this.typeNumber*4+17;this.modules=new Array(this.moduleCount);for(var row=0;row=7){this.setupTypeNumber(test);} if(this.dataCache==null){this.dataCache=QRCodeModel.createData(this.typeNumber,this.errorCorrectLevel,this.dataList);} this.mapData(this.dataCache,maskPattern);},setupPositionProbePattern:function(row,col){for(var r=-1;r<=7;r++){if(row+r<=-1||this.moduleCount<=row+r)continue;for(var c=-1;c<=7;c++){if(col+c<=-1||this.moduleCount<=col+c)continue;if((0<=r&&r<=6&&(c==0||c==6))||(0<=c&&c<=6&&(r==0||r==6))||(2<=r&&r<=4&&2<=c&&c<=4)){this.modules[row+r][col+c]=true;}else{this.modules[row+r][col+c]=false;}}}},getBestMaskPattern:function(){var minLostPoint=0;var pattern=0;for(var i=0;i<8;i++){this.makeImpl(true,i);var lostPoint=QRUtil.getLostPoint(this);if(i==0||minLostPoint>lostPoint){minLostPoint=lostPoint;pattern=i;}} return pattern;},createMovieClip:function(target_mc,instance_name,depth){var qr_mc=target_mc.createEmptyMovieClip(instance_name,depth);var cs=1;this.make();for(var row=0;row>i)&1)==1);this.modules[Math.floor(i/3)][i%3+this.moduleCount-8-3]=mod;} for(var i=0;i<18;i++){var mod=(!test&&((bits>>i)&1)==1);this.modules[i%3+this.moduleCount-8-3][Math.floor(i/3)]=mod;}},setupTypeInfo:function(test,maskPattern){var data=(this.errorCorrectLevel<<3)|maskPattern;var bits=QRUtil.getBCHTypeInfo(data);for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<6){this.modules[i][8]=mod;}else if(i<8){this.modules[i+1][8]=mod;}else{this.modules[this.moduleCount-15+i][8]=mod;}} for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<8){this.modules[8][this.moduleCount-i-1]=mod;}else if(i<9){this.modules[8][15-i-1+1]=mod;}else{this.modules[8][15-i-1]=mod;}} this.modules[this.moduleCount-8][8]=(!test);},mapData:function(data,maskPattern){var inc=-1;var row=this.moduleCount-1;var bitIndex=7;var byteIndex=0;for(var col=this.moduleCount-1;col>0;col-=2){if(col==6)col--;while(true){for(var c=0;c<2;c++){if(this.modules[row][col-c]==null){var dark=false;if(byteIndex>>bitIndex)&1)==1);} var mask=QRUtil.getMask(maskPattern,row,col-c);if(mask){dark=!dark;} this.modules[row][col-c]=dark;bitIndex--;if(bitIndex==-1){byteIndex++;bitIndex=7;}}} row+=inc;if(row<0||this.moduleCount<=row){row-=inc;inc=-inc;break;}}}}};QRCodeModel.PAD0=0xEC;QRCodeModel.PAD1=0x11;QRCodeModel.createData=function(typeNumber,errorCorrectLevel,dataList){var rsBlocks=QRRSBlock.getRSBlocks(typeNumber,errorCorrectLevel);var buffer=new QRBitBuffer();for(var i=0;itotalDataCount*8){throw new Error("code length overflow. (" +buffer.getLengthInBits() +">" +totalDataCount*8 +")");} if(buffer.getLengthInBits()+4<=totalDataCount*8){buffer.put(0,4);} while(buffer.getLengthInBits()%8!=0){buffer.putBit(false);} while(true){if(buffer.getLengthInBits()>=totalDataCount*8){break;} buffer.put(QRCodeModel.PAD0,8);if(buffer.getLengthInBits()>=totalDataCount*8){break;} buffer.put(QRCodeModel.PAD1,8);} return QRCodeModel.createBytes(buffer,rsBlocks);};QRCodeModel.createBytes=function(buffer,rsBlocks){var offset=0;var maxDcCount=0;var maxEcCount=0;var dcdata=new Array(rsBlocks.length);var ecdata=new Array(rsBlocks.length);for(var r=0;r=0)?modPoly.get(modIndex):0;}} var totalCodeCount=0;for(var i=0;i=0){d^=(QRUtil.G15<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G15)));} return((data<<10)|d)^QRUtil.G15_MASK;},getBCHTypeNumber:function(data){var d=data<<12;while(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)>=0){d^=(QRUtil.G18<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)));} return(data<<12)|d;},getBCHDigit:function(data){var digit=0;while(data!=0){digit++;data>>>=1;} return digit;},getPatternPosition:function(typeNumber){return QRUtil.PATTERN_POSITION_TABLE[typeNumber-1];},getMask:function(maskPattern,i,j){switch(maskPattern){case QRMaskPattern.PATTERN000:return(i+j)%2==0;case QRMaskPattern.PATTERN001:return i%2==0;case QRMaskPattern.PATTERN010:return j%3==0;case QRMaskPattern.PATTERN011:return(i+j)%3==0;case QRMaskPattern.PATTERN100:return(Math.floor(i/2)+Math.floor(j/3))%2==0;case QRMaskPattern.PATTERN101:return(i*j)%2+(i*j)%3==0;case QRMaskPattern.PATTERN110:return((i*j)%2+(i*j)%3)%2==0;case QRMaskPattern.PATTERN111:return((i*j)%3+(i+j)%2)%2==0;default:throw new Error("bad maskPattern:"+maskPattern);}},getErrorCorrectPolynomial:function(errorCorrectLength){var a=new QRPolynomial([1],0);for(var i=0;i5){lostPoint+=(3+sameCount-5);}}} for(var row=0;row=256){n-=255;} return QRMath.EXP_TABLE[n];},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)};for(var i=0;i<8;i++){QRMath.EXP_TABLE[i]=1<>>(7-index%8))&1)==1;},put:function(num,length){for(var i=0;i>>(length-i-1))&1)==1);}},getLengthInBits:function(){return this.length;},putBit:function(bit){var bufIndex=Math.floor(this.length/8);if(this.buffer.length<=bufIndex){this.buffer.push(0);} if(bit){this.buffer[bufIndex]|=(0x80>>>(this.length%8));} this.length++;}};var QRCodeLimitLength=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]]; function _isSupportCanvas() { return typeof CanvasRenderingContext2D != "undefined"; } // android 2.x doesn't support Data-URI spec function _getAndroid() { var android = false; var sAgent = navigator.userAgent; if (/android/i.test(sAgent)) { // android android = true; var aMat = sAgent.toString().match(/android ([0-9]\.[0-9])/i); if (aMat && aMat[1]) { android = parseFloat(aMat[1]); } } return android; } var svgDrawer = (function() { var Drawing = function (el, htOption) { this._el = el; this._htOption = htOption; }; Drawing.prototype.draw = function (oQRCode) { var _htOption = this._htOption; var _el = this._el; var nCount = oQRCode.getModuleCount(); var nWidth = Math.floor(_htOption.width / nCount); var nHeight = Math.floor(_htOption.height / nCount); this.clear(); function makeSVG(tag, attrs) { var el = document.createElementNS('http://www.w3.org/2000/svg', tag); for (var k in attrs) if (attrs.hasOwnProperty(k)) el.setAttribute(k, attrs[k]); return el; } var svg = makeSVG("svg" , {'viewBox': '0 0 ' + String(nCount) + " " + String(nCount), 'width': '100%', 'height': '100%', 'fill': _htOption.colorLight}); svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink"); _el.appendChild(svg); svg.appendChild(makeSVG("rect", {"fill": _htOption.colorLight, "width": "100%", "height": "100%"})); svg.appendChild(makeSVG("rect", {"fill": _htOption.colorDark, "width": "1", "height": "1", "id": "template"})); for (var row = 0; row < nCount; row++) { for (var col = 0; col < nCount; col++) { if (oQRCode.isDark(row, col)) { var child = makeSVG("use", {"x": String(col), "y": String(row)}); child.setAttributeNS("http://www.w3.org/1999/xlink", "href", "#template") svg.appendChild(child); } } } }; Drawing.prototype.clear = function () { while (this._el.hasChildNodes()) this._el.removeChild(this._el.lastChild); }; return Drawing; })(); var useSVG = document.documentElement.tagName.toLowerCase() === "svg"; // Drawing in DOM by using Table tag var Drawing = useSVG ? svgDrawer : !_isSupportCanvas() ? (function () { var Drawing = function (el, htOption) { this._el = el; this._htOption = htOption; }; /** * Draw the QRCode * * @param {QRCode} oQRCode */ Drawing.prototype.draw = function (oQRCode) { var _htOption = this._htOption; var _el = this._el; var nCount = oQRCode.getModuleCount(); var nWidth = Math.floor(_htOption.width / nCount); var nHeight = Math.floor(_htOption.height / nCount); var aHTML = ['']; for (var row = 0; row < nCount; row++) { aHTML.push(''); for (var col = 0; col < nCount; col++) { aHTML.push(''); } aHTML.push(''); } aHTML.push('
    '); _el.innerHTML = aHTML.join(''); // Fix the margin values as real size. var elTable = _el.childNodes[0]; var nLeftMarginTable = (_htOption.width - elTable.offsetWidth) / 2; var nTopMarginTable = (_htOption.height - elTable.offsetHeight) / 2; if (nLeftMarginTable > 0 && nTopMarginTable > 0) { elTable.style.margin = nTopMarginTable + "px " + nLeftMarginTable + "px"; } }; /** * Clear the QRCode */ Drawing.prototype.clear = function () { this._el.innerHTML = ''; }; return Drawing; })() : (function () { // Drawing in Canvas function _onMakeImage() { this._elImage.src = this._elCanvas.toDataURL("image/png"); this._elImage.style.display = "block"; this._elCanvas.style.display = "none"; } // Android 2.1 bug workaround // http://code.google.com/p/android/issues/detail?id=5141 if (this && this._android && this._android <= 2.1) { var factor = 1 / window.devicePixelRatio; var drawImage = CanvasRenderingContext2D.prototype.drawImage; CanvasRenderingContext2D.prototype.drawImage = function (image, sx, sy, sw, sh, dx, dy, dw, dh) { if (("nodeName" in image) && /img/i.test(image.nodeName)) { for (var i = arguments.length - 1; i >= 1; i--) { arguments[i] = arguments[i] * factor; } } else if (typeof dw == "undefined") { arguments[1] *= factor; arguments[2] *= factor; arguments[3] *= factor; arguments[4] *= factor; } drawImage.apply(this, arguments); }; } /** * Check whether the user's browser supports Data URI or not * * @private * @param {Function} fSuccess Occurs if it supports Data URI * @param {Function} fFail Occurs if it doesn't support Data URI */ function _safeSetDataURI(fSuccess, fFail) { var self = this; self._fFail = fFail; self._fSuccess = fSuccess; // Check it just once if (self._bSupportDataURI === null) { var el = document.createElement("img"); var fOnError = function() { self._bSupportDataURI = false; if (self._fFail) { self._fFail.call(self); } }; var fOnSuccess = function() { self._bSupportDataURI = true; if (self._fSuccess) { self._fSuccess.call(self); } }; el.onabort = fOnError; el.onerror = fOnError; el.onload = fOnSuccess; el.src = "data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; // the Image contains 1px data. return; } else if (self._bSupportDataURI === true && self._fSuccess) { self._fSuccess.call(self); } else if (self._bSupportDataURI === false && self._fFail) { self._fFail.call(self); } }; /** * Drawing QRCode by using canvas * * @constructor * @param {HTMLElement} el * @param {Object} htOption QRCode Options */ var Drawing = function (el, htOption) { this._bIsPainted = false; this._android = _getAndroid(); this._htOption = htOption; this._elCanvas = document.createElement("canvas"); this._elCanvas.width = htOption.width; this._elCanvas.height = htOption.height; el.appendChild(this._elCanvas); this._el = el; this._oContext = this._elCanvas.getContext("2d"); this._bIsPainted = false; this._elImage = document.createElement("img"); this._elImage.alt = "Scan me!"; this._elImage.style.display = "none"; this._el.appendChild(this._elImage); this._bSupportDataURI = null; }; /** * Draw the QRCode * * @param {QRCode} oQRCode */ Drawing.prototype.draw = function (oQRCode) { var _elImage = this._elImage; var _oContext = this._oContext; var _htOption = this._htOption; var nCount = oQRCode.getModuleCount(); var nWidth = _htOption.width / nCount; var nHeight = _htOption.height / nCount; var nRoundedWidth = Math.round(nWidth); var nRoundedHeight = Math.round(nHeight); _elImage.style.display = "none"; this.clear(); for (var row = 0; row < nCount; row++) { for (var col = 0; col < nCount; col++) { var bIsDark = oQRCode.isDark(row, col); var nLeft = col * nWidth; var nTop = row * nHeight; _oContext.strokeStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight; _oContext.lineWidth = 1; _oContext.fillStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight; _oContext.fillRect(nLeft, nTop, nWidth, nHeight); // 안티 앨리어싱 방지 처리 _oContext.strokeRect( Math.floor(nLeft) + 0.5, Math.floor(nTop) + 0.5, nRoundedWidth, nRoundedHeight ); _oContext.strokeRect( Math.ceil(nLeft) - 0.5, Math.ceil(nTop) - 0.5, nRoundedWidth, nRoundedHeight ); } } this._bIsPainted = true; }; /** * Make the image from Canvas if the browser supports Data URI. */ Drawing.prototype.makeImage = function () { if (this._bIsPainted) { _safeSetDataURI.call(this, _onMakeImage); } }; /** * Return whether the QRCode is painted or not * * @return {Boolean} */ Drawing.prototype.isPainted = function () { return this._bIsPainted; }; /** * Clear the QRCode */ Drawing.prototype.clear = function () { this._oContext.clearRect(0, 0, this._elCanvas.width, this._elCanvas.height); this._bIsPainted = false; }; /** * @private * @param {Number} nNumber */ Drawing.prototype.round = function (nNumber) { if (!nNumber) { return nNumber; } return Math.floor(nNumber * 1000) / 1000; }; return Drawing; })(); /** * Get the type by string length * * @private * @param {String} sText * @param {Number} nCorrectLevel * @return {Number} type */ function _getTypeNumber(sText, nCorrectLevel) { var nType = 1; var length = _getUTF8Length(sText); for (var i = 0, len = QRCodeLimitLength.length; i <= len; i++) { var nLimit = 0; switch (nCorrectLevel) { case QRErrorCorrectLevel.L : nLimit = QRCodeLimitLength[i][0]; break; case QRErrorCorrectLevel.M : nLimit = QRCodeLimitLength[i][1]; break; case QRErrorCorrectLevel.Q : nLimit = QRCodeLimitLength[i][2]; break; case QRErrorCorrectLevel.H : nLimit = QRCodeLimitLength[i][3]; break; } if (length <= nLimit) { break; } else { nType++; } } if (nType > QRCodeLimitLength.length) { throw new Error("Too long data"); } return nType; } function _getUTF8Length(sText) { var replacedText = encodeURI(sText).toString().replace(/\%[0-9a-fA-F]{2}/g, 'a'); return replacedText.length + (replacedText.length != sText ? 3 : 0); } /** * @class QRCode * @constructor * @example * new QRCode(document.getElementById("test"), "http://jindo.dev.naver.com/collie"); * * @example * var oQRCode = new QRCode("test", { * text : "http://naver.com", * width : 128, * height : 128 * }); * * oQRCode.clear(); // Clear the QRCode. * oQRCode.makeCode("http://map.naver.com"); // Re-create the QRCode. * * @param {HTMLElement|String} el target element or 'id' attribute of element. * @param {Object|String} vOption * @param {String} vOption.text QRCode link data * @param {Number} [vOption.width=256] * @param {Number} [vOption.height=256] * @param {String} [vOption.colorDark="#000000"] * @param {String} [vOption.colorLight="#ffffff"] * @param {QRCode.CorrectLevel} [vOption.correctLevel=QRCode.CorrectLevel.H] [L|M|Q|H] */ QRCode = function (el, vOption) { this._htOption = { width : 256, height : 256, typeNumber : 4, colorDark : "#000000", colorLight : "#ffffff", correctLevel : QRErrorCorrectLevel.H }; if (typeof vOption === 'string') { vOption = { text : vOption }; } // Overwrites options if (vOption) { for (var i in vOption) { this._htOption[i] = vOption[i]; } } if (typeof el == "string") { el = document.getElementById(el); } if (this._htOption.useSVG) { Drawing = svgDrawer; } this._android = _getAndroid(); this._el = el; this._oQRCode = null; this._oDrawing = new Drawing(this._el, this._htOption); if (this._htOption.text) { this.makeCode(this._htOption.text); } }; /** * Make the QRCode * * @param {String} sText link data */ QRCode.prototype.makeCode = function (sText) { this._oQRCode = new QRCodeModel(_getTypeNumber(sText, this._htOption.correctLevel), this._htOption.correctLevel); this._oQRCode.addData(sText); this._oQRCode.make(); this._el.title = sText; this._oDrawing.draw(this._oQRCode); this.makeImage(); }; /** * Make the Image from Canvas element * - It occurs automatically * - Android below 3 doesn't support Data-URI spec. * * @private */ QRCode.prototype.makeImage = function () { if (typeof this._oDrawing.makeImage == "function" && (!this._android || this._android >= 3)) { this._oDrawing.makeImage(); } }; /** * Clear the QRCode */ QRCode.prototype.clear = function () { this._oDrawing.clear(); }; /** * @name QRCode.CorrectLevel */ QRCode.CorrectLevel = QRErrorCorrectLevel; })(); window.QRCode = QRCode; ================================================ FILE: app/jobs/application_job.rb ================================================ class ApplicationJob < ActiveJob::Base # Automatically retry jobs that encountered a deadlock # retry_on ActiveRecord::Deadlocked # Most jobs are safe to ignore if the underlying records are no longer available # discard_on ActiveJob::DeserializationError end ================================================ FILE: app/mailers/application_mailer.rb ================================================ class ApplicationMailer < ActionMailer::Base default from: 'from@example.com' layout 'mailer' end ================================================ FILE: app/models/administrator.rb ================================================ class Administrator < ApplicationRecord validates :name, presence: true, uniqueness: true has_secure_password end ================================================ FILE: app/models/application_record.rb ================================================ class ApplicationRecord < ActiveRecord::Base self.abstract_class = true end ================================================ FILE: app/models/comment.rb ================================================ class Comment < ApplicationRecord belongs_to :post validates :name, presence: true validates :email, presence: true, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i } validates :content, presence: true, length: { minimum: 4 } def reply_emails Comment.where(post_id: self.post_id).collect(&:email).uniq - [ self.email ] - Subscribe.unsubscribe_list - [ ENV['ADMIN_USER'] ] end end ================================================ FILE: app/models/concerns/.keep ================================================ ================================================ FILE: app/models/label.rb ================================================ class Label < ApplicationRecord has_and_belongs_to_many :posts validates :name, presence: true end ================================================ FILE: app/models/like.rb ================================================ class Like < ApplicationRecord belongs_to :post end ================================================ FILE: app/models/photo.rb ================================================ class Photo < ApplicationRecord mount_uploader :image, PhotoUploader end ================================================ FILE: app/models/post.rb ================================================ require 'markdown' class Post < ActiveRecord::Base has_many :comments has_and_belongs_to_many :labels has_many :likes validates :title, :presence=>true, :uniqueness=> true validates :content, :presence=>true, :length => { :minimum=> 3 } def content_html self.class.render_html(self.content) end def self.render_html(content) rd = CodeHTML.new md = Redcarpet::Markdown.new(rd, autolink: true, fenced_code_blocks: true) md.render(content) end def visited self.visited_count += 1 self.save self.visited_count end # truncate content for home page display def sub_content HTML_Truncator.truncate(content_html, 300, length_in_chars: true) end # truncate content for meta description display def meta_content html = HTML_Truncator.truncate(content_html, 100, :length_in_chars => true, ellipsis: '') # Easily get text for Nokogiri html = '
    ' + html + '
    ' Nokogiri.parse(html).text() end def labels_content( need_blank=false ) content = self.labels.collect { |label| label.name }.join(", ") content = I18n.t('none') if content.blank? and !need_blank content end def liked_count self.likes.size end def liked_by?(like_id) !! self.likes.where(id: like_id).first end end ================================================ FILE: app/uploaders/photo_uploader.rb ================================================ class PhotoUploader < CarrierWave::Uploader::Base include CarrierWave::MiniMagick storage :file def store_dir "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end process :resize_to_limit => [1200,nil] version :medium do process :resize_to_limit => [640,nil] end version :small do process :resize_to_limit => [400,nil] end def extension_white_list %w(jpg jpeg gif png) end end ================================================ FILE: app/views/admin/accounts/edit.html.slim ================================================ - content_for :title do | Account Setting .card.card-primary .card-header.with-border h3.card-title | Edit Account = simple_form_for current_admin, url: admin_account_path do |f| .card-body = f.input :name, label: 'Administrator Name' = f.input :current_password, label: 'Current Password', required: true = f.input :password, label: 'New Password( Blank if not modify )' = f.input :password_confirmation, label: 'Confirm New Password' .card-footer = f.submit 'Update Account', class: 'btn btn-primary' ================================================ FILE: app/views/admin/all_comments/index.html.slim ================================================ .card .card-header .float-sm-left h3.card-title 评论管理 .float-sm-right .card-body table.table.table-hover thead tr th 内容 th 所属文章 th 时间 th #{t('admin.posts_head.operation')} tbody - @comments.each do |comment| tr td #{comment.content} td #{comment.post.title} td #{format_time(comment.created_at)} td = link_to t('destroy'), admin_all_comment_path(comment), method: 'DELETE', 'data-confirm' => '确认删除?' .card-footer .float-sm-left | 总计: #{@comments.total_count} .float-sm-right = paginate @comments ================================================ FILE: app/views/admin/comments/index.html.slim ================================================ .card .card-header .float-sm-left h3.card-title ' #{t('admin.comments')} | / #{@post.title} .card-body table.table.table-hover thead tr th #{t('admin.comments_head.name')} th #{t('admin.comments_head.email')} th #{t('admin.comments_head.content')} th #{t('admin.comments_head.created_at')} th #{t('admin.comments_head.operation')} tbody - @comments.each do |comment| tr td #{comment.name} td = mail_to comment.email td = simple_format(comment.content) td = format_time(comment.created_at) td = link_to t('admin.comments_head.reply'), blog_path(@post, anchor: 'new_comment'), target: '_blank', class: 'edit-post-link' ' = link_to t('admin.comments_head.destroy'), admin_post_comment_path(@post, comment), method: 'DELETE', 'data-confirm'=> '确认删除?' ================================================ FILE: app/views/admin/dashboard/index.html.slim ================================================ .container-fluid .row .col-lg-6 .card.card-primary.card-outline .card-body h5.card-title 博客数量: #{@posts_count} p.card-text .col-lg-6 .card.card-primary.card-outline .card-body h5.card-title 评论数量: #{@comments_count} p.card-text ================================================ FILE: app/views/admin/labels/_form.html.slim ================================================ .row .col-sm-6 = simple_form_for([:admin, @label], html: {novalidate: '' }) do |f| = f.input :name = f.button :submit ================================================ FILE: app/views/admin/labels/edit.html.slim ================================================ .card .card-header .float-sm-left h3.card-title 修改标签 .card-body = render 'form' ================================================ FILE: app/views/admin/labels/index.html.slim ================================================ .card .card-header .float-sm-left h3.card-title 标签管理 .float-sm-right = link_to '创建标签', new_admin_label_path .card-body table.table.table-hover thead tr th 标签名 th 被引用次数 th 创建时间 th 操作 tbody - @labels.each do |label| tr td = label.name td = label.posts.count td = format_time(label.created_at) td = link_to '编辑', edit_admin_label_path(label) |   = link_to '删除', admin_label_path(label), method: 'DELETE', 'data-confirm' => '确认删除?' .card-footer .float-sm-left | 总计: #{@labels.total_count} .float-sm-right = paginate @labels ================================================ FILE: app/views/admin/labels/new.html.slim ================================================ .card .card-header .float-sm-left h3.card-title 新建标签 .card-body = render 'form' ================================================ FILE: app/views/admin/posts/_form.html.slim ================================================ .row data-controller='admin-label' .col-sm-8 = simple_form_for([:admin, @post], html: {novalidate: '' }) do |f| = f.input :title, label: t('admin.posts_attributes.title') = f.association :labels, input_html: { data: { 'admin-label-target': 'label', 'labels-data': Label.all.map(&:name) } }, label: t('admin.posts_attributes.labels') / tabs and upload file field ul.nav.nav-tabs#tabs li.nav-item a.nav-link.active data-toggle="tab" href="#content" #{t('admin.posts_attributes.content')} li.nav-item a.nav-link#preview-link data-toggle="tab" href="#preview" #{t('admin.posts_attributes.preview')} = link_to t('admin.posts_attributes.upload_photo'), "#", id: 'upload_photo' input type="file" style="display: none;" .tab-content .tab-pane.fade.show.active#content = f.input :content, :as=> :text, :label => false, input_html: { id: 'content-input' } .tab-pane.fade.markdown#preview = f.button :submit ================================================ FILE: app/views/admin/posts/edit.html.slim ================================================ .card .card-header .float-sm-left h3.card-title 修改博客 .card-body = render 'form' ================================================ FILE: app/views/admin/posts/index.html.slim ================================================ .card .card-header .float-sm-left h3.card-title #{t('admin.posts')} .float-sm-right = link_to '创建博客', new_admin_post_path .card-body table.table.table-hover thead tr th #{t('admin.posts_head.title')} th #{t('admin.posts_head.summary')} th #{t('admin.posts_head.operation')} tbody - @posts.each do |post| tr td = link_to post.title, blog_path(post) td i.fa.fa-calendar span #{format_time(post.created_at)} i.fa.fa-list span #{ post.labels_content } i.fa.fa-eye span #{ post.visited_count } i.fa.fa-heart span #{ post.liked_count } td = link_to t('comment'), admin_post_comments_path(post.id), class: 'edit-post-link' |   = link_to t('edit'), edit_admin_post_path(post), class: 'edit-post-link' |   = link_to t('destroy'), admin_post_path(post), method: 'DELETE', 'data-confirm' => '确认删除?' .card-footer .float-sm-left | 总计: #{@posts.total_count} .float-sm-right = paginate @posts ================================================ FILE: app/views/admin/posts/new.html.slim ================================================ .card .card-header .float-sm-left h3.card-title #{t('admin.new_post')} .card-body = render 'form' ================================================ FILE: app/views/admin/sessions/new.html.slim ================================================ - content_for :title, 'Log in' main section.content-messages = render 'shared/admin/flash_messages' section.content .login-box .login-logo span | Dashboard .card .card-body.login-card-body p.login-box-msg Log in = form_tag admin_login_path do .input-group.mb-3 = text_field_tag :name, nil, placeholder: 'Username', class: 'form-control' .input-group-append.input-group-text span.fas.fa-envelope .input-group.mb-3 = password_field_tag :password, nil, placeholder: 'Password', class: 'form-control' .input-group-append.input-group-text span.fas.fa-lock .row .col-4 = submit_tag 'Log in', class: 'btn btn-primary btn-block btn-flat' ================================================ FILE: app/views/archives/index.html.slim ================================================ - content_for(:title) do | #{t('title.timeline')} .container .row.justify-content-center .col-sm-12.col-lg-9 ul.archives-field .search-wrapper = form_with url: archives_path, method: 'GET' do |f| .form-group = f.search_field :q, value: @q, placeholder: t('archive.search'), class: 'form-control' - @posts.each do |post| li = link_to blog_path(post), class: 'blog-title' do == search_highlight(post.title, @q) p.tags-field i.fa.fa-calendar span = format_date(post.created_at) i.fa.fa-eye span = post.labels_content i.fa.fa-torah span = post.visited_count i.fa.fa-heart span = post.liked_count - if @q.present? .search-result-wrapper p.text-muted 共 #{@q_size || 0} 条结果 = paginate @posts, q: @q ================================================ FILE: app/views/blogs/_comment.html.slim ================================================ turbo-frame#comment-frame .container .row.justify-content-center .col-12.col-lg-9 - comment = @comment || Comment.new = simple_form_for comment, url: blog_comments_path(post), remote: true do |f| = f.text_area :content, placeholder: t('comment_placeholder.content') .row .col-12.col-lg-6 = f.text_field :name, value: cookies[:name], placeholder: t('comment_placeholder.name') .row .col-12.col-lg-6 = f.text_field :email, value: cookies[:email], placeholder: t('comment_placeholder.email') button.comment-submit type='submit' data-disable-with=t('comment_placeholder.submitting') #{t('comment_placeholder.submit')} - if info = flash.now[:notice] #alert-container.alert.alert-success span.text #{info} // ai fix button class="close" type='button' data-dismiss='alert' span × - elsif comment.errors.any? #alert-container.alert.alert-warning span.text #{comment.errors[:content].first} button class="close" type='button' data-dismiss='alert' span × = render partial: 'comments/comment_content', locals: { comments: comments } ================================================ FILE: app/views/blogs/_post.html.slim ================================================ = render partial: 'post_head', locals: { post: post } .content.markdown == post.content_html p.ptag.published-at | #{t('announce_at')} span #{format_date(post.created_at)} = render 'common/copyright' hr.blog-over p data-controller='like' button.like-button class="#{'liked' if post.liked_by?(cookies[:like])}" type='button' data-url=blog_likes_path(post) data-like-target='button' data-action='click->like#toggle' span.count #{@likes_count} span Like .qrcode-controller data-controller='qrcode' .qrcode a#qrcode-link href='#' data-action='click->qrcode#greet' i.fi-link | #{t('qr_code')} .social-share data-qrcode-target='wrapper' .qrcode-wrapper = render partial: "qrcode", locals: { str: blog_url(post) } ================================================ FILE: app/views/blogs/_post_head.html.slim ================================================ / require: locals: { post : post } h2.blog-title #{post.title} p.ptag span.fa.fa-list span #{post.labels_content} ================================================ FILE: app/views/blogs/_qrcode.html.slim ================================================ .qrcode-image #image-tag data-url=str p #{t('qrcodetips')} ================================================ FILE: app/views/blogs/edit.html.slim ================================================ h1 Blogs#edit p Find me in app/views/blogs/edit.html.slim ================================================ FILE: app/views/blogs/show.html.slim ================================================ - content_for(:meta) do meta name="description" content=@post.meta_content meta name="keywords" content=@post.labels_content - content_for(:title) do | #{@post.title} / data-url=refresh_blog_comments_path(@post) data-post_id=@post.id .container.blog-wrapper .row.justify-content-center .col-xs-12.col-lg-9 = render partial: "post", :locals=> { post: @post } .comment-field = render partial: 'comment', locals: { comments: @comments, post: @post } p .container .row.justify-content-center .col-xs-12.col-lg-9 - if @prev = link_to blog_path(@prev), class: 'prev' do i.fa.fa-arrow-left | 上一篇 - if @next = link_to blog_path(@next), class: 'next' do | 下一篇 i.fa.fa-arrow-right ================================================ FILE: app/views/comments/_comment_content.html.slim ================================================ .comment-diag - comments.each do |comment| .comment-wrapper p.name | #{comment.name} | #{" • "} span.created-at | #{format_time(comment.created_at) } = render partial: 'comments/comment_pre', locals: { comment: comment } ================================================ FILE: app/views/comments/_comment_pre.html.slim ================================================ .comment-content = simple_format(comment.content) ================================================ FILE: app/views/comments/create.html.slim ================================================ = render partial: 'blogs/comment', locals: { post: @post, comments: @comments } ================================================ FILE: app/views/common/_copyright.en.html.slim ================================================ p.copyright.published-at.ptag | © Creative Commons - ShareAlike - NonCommercial - Attribution ================================================ FILE: app/views/common/_copyright.html.slim ================================================ p.copyright.published-at.ptag | © 自由转载 - 非商用 - 非衍生 - 保持署名 ================================================ FILE: app/views/common/_no_blog_here.en.html.slim ================================================ h2.blog-title #{t('home.no_blog_here')} p | No post here, please visit = link_to ' Manage Post ', new_admin_post_path | to create the first post. ================================================ FILE: app/views/common/_no_blog_here.html.slim ================================================ h2.blog-title #{t('home.no_blog_here')} p | 这里还没有博客, 请访问 = link_to '管理页面', new_admin_post_path | 来创建第一篇博客 ================================================ FILE: app/views/common/_welcome.en.html.slim ================================================ /* adjust stylesheet: .self-introduce-index */ h4 WELCOME p I'm Li Yafei, WinDy is my English name. h4 ABOUT ul.aboutme-index li span Industry: span Web Development, Startups, Life li span Location: span Nan Shan District, ShenZhen, China li span More: span = link_to 'About Me', about_path ================================================ FILE: app/views/common/_welcome.html.slim ================================================ /* 样式调整请找 stylesheet: .self-introduce-index */ .box h4 欢迎 p 我是技术达人李亚飞 .box h4 关于我 ul.aboutme-index li span 领域: span 技术, 创业, 生活 li span 位置: span 中国 - 深圳 - 南山 li span 更多: span = link_to '关于我', about_path ================================================ FILE: app/views/common/_welcome_new_year.html.slim ================================================ - if (1.days.from_now).strftime('%-m-%-d') =~ /^1-[123]$/ .new-year.alert.alert-success.alert-dismissible.fade.show role='alert' strong 🎉 我的朋友,祝你元旦快乐! button.close type='button' data-dismiss='alert' aria-label='Close' span aria-hidden='true' × ================================================ FILE: app/views/home/_post_head.html.slim ================================================ / require: locals: { post : post } h2.blog-title #{post.title} p.ptag span i.fi-pricetag-multiple span #{post.labels_content} ================================================ FILE: app/views/home/about.html.slim ================================================ - content_for(:title) do | #{t('title.about')} - content_for(:main) do nav#about-top-bar class="navbar navbar-expand-lg navbar-dark bg-dark my-navbar fixed-top" a.navbar-brand href=root_path 回到博客 button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar-collapse" span class="navbar-toggler-icon" div class="collapse navbar-collapse" id="navbar-collapse" ul.navbar-nav.ml-auto li class="nav-item" a.nav-link href='#about' 关于 li class="nav-item" a.nav-link href='#skill' 技能 li class="nav-item" a.nav-link href='#work' 作品 li class="nav-item" a.nav-link href='#contact' 联系 #intro header.intro.container-fluid .row.intro-heading .col-12 h1.heading 李亚飞 .sub-heading p 懒,驱动人类进步的关键力量。 a.circle#about-anchor-link href='#about' i.fa.fa-angle-double-down section#about.container-fluid .row.justify-content-center .col-12.col-lg-8.col-xl-6 h1.title 关于我 p 我是李亚飞,坐标深圳,连续创业者,技术达人,有众多开源项目和多场技术分享,创立过 3 家以技术产品驱动的创业公司,现 ShowMeBug 产品创始人&至简天成科技CEO,前单麦科技联合创始人&CTO。 .wrapper .time 2024.04-至今 p Clacky CEO ul li 全球首创:L3 Agentic AI CDE li 公测地址:clacky.ai .wrapper .time 2019.03 ~ 2024.04 p ShowMeBug创始人,至简天成科技CEO ul li 中国数字化面试开拓者 li 以全栈产研为核心的团队 li 近百万用户,6000家企业客户 li 开箱即用地址:showmebug.com .wrapper .time 2016.04 ~ 2018.12 p 深圳百分之八十网络技术有限公司联合创始人 & CTO,旗下主要产品单麦小程序平台是帮助商家一键制作小程序的SAAS服务平台,我与另一位联合创始人一道创立并运营了整个公司与产品,我的主要职责包括共同决策产品方向,负责研发团队日常管理,参与市场部销售计划制定与支持工作。 ul li 组建以敏捷开发,全栈工程师为核心的高效率研发团队 li 带领团队连续 52 次版本迭代,每周发布新版本 li 单麦小程序平台客户体验满意度90%以上,上线小程序几千家,服务用户15万+ li 单麦小程序平台成为微信小程序生态 TOP10 获奖者 li 2018 年 12 月并购退出 .wrapper .time 2015 p SmartX 早期员工,SmartX 是位于中国的全球领先的超融合存储方案供应商,利用软件技术方案帮助企业大规模降低存储硬件的成本。作为早期创始员工,担任前端架构师角色,顺利保证 SmartX OS 的快速交付。 .wrapper .time 2014 p Cywin.cn 联合创始人 & 技术负责人,Cywin.cn 是一家股权众筹平台,对标美国的 Angelist.co,帮助创业公司完成股权融资的交易平台。作为项目的发起人之一,负责产品的研发工作。 .wrapper .time 2009 ~ 2014 p 深信服(2018年上市)是一家著名的网络安全公司。加入时研发规模600人,作为当时研发部成长最快的工程师之一,担任首界自动化产品线的主管兼技术负责人(现研发效能部门),带领技术团队进行研发质量和内部工具链研发方面的工作。 section#skill.container-fluid .row.justify-content-center .col-12.col-lg-8.col-xl-6 .skills h1.title 开发技能栈 ul li Ruby on Rails ( 精通 ) li Linux / OSX ( 非常熟悉 ) li Git / Svn ( 非常熟悉 ) li AngularJS / React / VueJS / ES6 / Jquery ( 非常熟悉 ) li Bootstrap / Foundation 6 ( 非常熟悉 ) li HTML5 ( 熟悉 ) li CSS3 ( 熟悉 ) li Agile Development li Testing Automation li Deploying Automation li PostgreSQL / Mysql / Mongodb section#work.container-fluid .row.justify-content-center .col-12.col-lg-8.col-xl-6 h1.title 个人主要作品 ul.works li span.time 2015.3 - 2016.x .project span.name Lina span.brief 一个专注于 API 接口编写的框架 span.link a href='https://github.com/windy/lina' target='_blank' https://github.com/windy/lina ul.project-description li 自动生成文档 li 零学习成本, 集成 Rails 的 API 开发最佳实践 li 自动校验参数, 快速构建安全可靠的 API li ' Github 地址: a href='https://github.com/windy/lina' https://github.com/windy/lina li span.time 2014.12 - 2015.3 .project span.name 青角落 span.brief 有趣的互联网人, 知识 span.link a href='http://jiaoluo.yafeilee.me' target='_blank' http://jiaoluo.yafeilee.me ul.project-description li 朋友的创业项目, 独立开发 li 现代的功能: 微信登录, 支付系统, 视频托管, 用户社区. li Ruby on Rails 架构, 使用 Turoblinks, RJS 进行前端交互. li 七牛云存储集成, 微信集成, 支付宝集成. li span.time 2014.3 - 2014.10 .project span.name Cywin span.brief 一个股权众筹的商业平台 span.link a href='https://github.com/windy/cywin' target='_blank' https://github.com/windy/cywin ul.project-description li 全栈独立负责整个项目的后端, 前端, 架构在 Ruby on Rails, AngularJS, Foundation 5. li 花费周期 6 个月, 上线. li ' 代码在 2015 年 3 月已经 a href='https://github.com/windy/cywin' target='_blank' 开源 | . li span.time 2014.2 - 2014.3 .project span.name WBlog span.brief Rails 社区正缺了一个独立博客建站系统 span.link a href='https://github.com/windy/wblog' target='_blank' https://github.com/windy/wblog ul.project-description li Ruby on Rails 开源博客系统, 帮助更多的朋友构建高定制性的博客. li 具备博客管理, 点赞, 评论, 二维码, 自适应等现代网站所有特性. li span.time 2011.x - 2013.10 .project span.name ATM/ATT span.brief 测试业界领先的关键字驱动的自动化测试平台 span.link 深信服内部产品 ul.project-description li 作为 Leader 带领团队架构并开发. li XMLRPC 作为数据交换, 平台具备任务调度, Agent 服务, 用例编写, 权限管理, Svn集成, 自动布署. li 关键字驱动方案是测试业内领先的技术框架. li 几乎所有服务使用 100% Ruby 代码完成. section#contact .grid-x .small-12.large-8.large-offset-2.cell h1.title 联系我 p 保持放松, 请随时邮件与我联系 p.mail_to = mail_to 'lyfi2003@gmail.com' ul.contact-ul li a href='https://github.com/windy' target='_blank' i.fab.fa-github | Github li a href='http://ruby-china.org/lyfi2003' target='_blank' i.fa.fa-heart | RubyChina li a href='http://www.douban.com/people/41759170/' target='_blank' i.douban | 豆 | Douban p.modified-at | 本页更新于 2025.03.01 .footer .grid-x .small-12.cell div Copyright © 2012 - 2025 yafeilee.com ================================================ FILE: app/views/home/index.html.slim ================================================ - if ENV['INTRODUCE'].present? - content_for(:meta) do meta name="description" content="#{ENV['INTRODUCE'].dup.force_encoding('UTF-8')}" - content_for(:title) do | #{t('title.home')} .container .row .col-sm-12.col-lg-8 - unless @newest = render 'common/no_blog_here' - else = render partial: 'post_head', locals: { post: @newest } .content.markdown == @newest.sub_content = link_to t('home.read'), blog_path(@newest), class: 'read-more' p.published-at #{t('home.created_at')} #{format_date(@newest.created_at)} - if @recent.present? h4.recent-title #{t('home.recent')} ul.recent-content - @recent.each do |re| li = link_to re.title, blog_path(re) .col-lg-4.self-introduce / Adjust it in common/welcome = render 'common/welcome' h4 #{t('subscribes.title')} .row .col-12.col-md-6.col-lg-12 = image_tag 'wechat_qrcode.jpg', class: 'wechat_qrcode' ================================================ FILE: app/views/kaminari/_first_page.html.slim ================================================ li.page-item = link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, remote: remote, class: 'page-link' ================================================ FILE: app/views/kaminari/_gap.html.slim ================================================ li.page-item.disabled = link_to raw(t 'views.pagination.truncate'), '#', class: 'page-link' ================================================ FILE: app/views/kaminari/_last_page.html.slim ================================================ li.page-item = link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, remote: remote, class: 'page-link' ================================================ FILE: app/views/kaminari/_next_page.html.slim ================================================ li.page-item = link_to_unless current_page.last?, raw(t 'views.pagination.next'), url, rel: 'next', remote: remote, class: 'page-link' ================================================ FILE: app/views/kaminari/_page.html.slim ================================================ - if page.current? li.page-item.active = content_tag :a, page, data: { remote: remote }, rel: page.rel, class: 'page-link' - else li.page-item = link_to page, url, remote: remote, rel: page.rel, class: 'page-link' ================================================ FILE: app/views/kaminari/_paginator.html.slim ================================================ = paginator.render do nav ul.pagination == first_page_tag unless current_page.first? == prev_page_tag unless current_page.first? - each_page do |page| - if page.left_outer? || page.right_outer? || page.inside_window? == page_tag page - elsif !page.was_truncated? == gap_tag == next_page_tag unless current_page.last? == last_page_tag unless current_page.last? ================================================ FILE: app/views/kaminari/_prev_page.html.slim ================================================ li.page-item = link_to_unless current_page.first?, raw(t 'views.pagination.previous'), url, rel: 'prev', remote: remote, class: 'page-link' ================================================ FILE: app/views/layouts/_footer.html.slim ================================================ .container .row .col-sm-12 .footer div span.link = ENV['SITE_ADDRESS'] span.time = ENV['SITE_YEAR'] .license | Designed by span = link_to 'WinDy', 'http://yafeilee.com/about', target: '_blank' .license | Built with span = link_to 'wblog', 'https://github.com/windy/wblog', target: '_blank' - if ENV['SITE_BEIAN'].present? .license = link_to ENV['SITE_BEIAN'], 'http://beian.miit.gov.cn/', target: '_blank' ================================================ FILE: app/views/layouts/admin.html.slim ================================================ doctype html html head meta charset='utf-8' meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" meta name="renderer" content="webkit" meta http-equiv="cleartype" content="on" meta name="HandheldFriendly" content="True" meta name="MobileOptimized" content="320" meta name="turbo-cache-control" content="no-cache" - if content_for?(:title) title = format("%s - Dashboard", yield(:title)) - else title Dashboard = csrf_meta_tags = action_cable_meta_tag = content_for?(:head) ? yield(:head) : '' = stylesheet_link_tag 'admin', media: 'all', 'data-turbo-track': 'reload' / = javascript_include_tag 'admin', 'data-turbo-track': 'reload' = javascript_include_tag 'admin', 'data-turbo-track': Rails.env.production? ? "reload" : "", type: "module" = favicon_link_tag asset_path("favicon.png") body.layout-fixed.sidebar-expand-lg.bg-body-tertiary.app-loaded.sidebar-open.admin-page class=body_class - if @full_render = yield - else .app-wrapper = render 'shared/admin/header' = render 'shared/admin/sidebar' main.app-main .content-messages = render 'shared/admin/flash_messages' .app-content-header .app-content = yield / aside.control-sidebar.control-sidebar-dark / /! Control sidebar content goes here / .p-3 / h5 Title / p Sidebar content / /! /.control-sidebar / /! Main Footer .app-footer /! To the right .float-end.d-none.d-sm-inline | Anything you want /! Default to the left strong | Copyright © 2019 - All rights reserved. | Theme by AdminLTE.io ================================================ FILE: app/views/layouts/application.html.slim ================================================ doctype html html head meta charset='utf-8' meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" meta name="renderer" content="webkit" meta http-equiv="cleartype" content="on" meta name="HandheldFriendly" content="True" meta name="MobileOptimized" content="320" meta name="turbo-cache-control" content="no-cache" - if content_for?(:meta) = yield(:meta) title = content_for?(:title) ? yield(:title) + " | #{ENV['SITE_NAME']}" : ENV['SITE_NAME'] = csrf_meta_tags = action_cable_meta_tag = content_for?(:head) ? yield(:head) : '' = stylesheet_link_tag 'application', media: 'all', 'data-turbo-track': 'reload' = javascript_include_tag 'application', 'data-turbo-track': Rails.env.production? ? "reload" : "", type: "module" / = javascript_include_tag 'application', 'data-turbo-track': Rails.env.production? ? "reload" : "" / = javascript_tag 'ga', 'data-turbo-track': 'reload' = favicon_link_tag asset_path("favicon.png") body class=body_class - if content_for?(:main) = yield(:main) - else nav class="navbar navbar-expand-lg navbar-dark bg-dark my-navbar" a class="navbar-brand" href="/" #{ENV['SITE_NAME']} button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar-collapse" span class="navbar-toggler-icon" div class="collapse navbar-collapse" id="navbar-collapse" ul.navbar-nav.ml-auto li class="nav-item" = link_to '首页', root_path, class: 'nav-link' li class="nav-item" = link_to '时间线', archives_path, class: 'nav-link' li class="nav-item" = link_to '关于', about_path, class: 'nav-link' li class="nav-item" a class="nav-link" href=archives_path .fa.fa-search main = render 'common/welcome_new_year' = yield = render "layouts/footer" ================================================ FILE: app/views/layouts/mailer.html.erb ================================================ <%= yield %> ================================================ FILE: app/views/layouts/mailer.text.erb ================================================ <%= yield %> ================================================ FILE: app/views/shared/admin/_flash_messages.html.slim ================================================ - flash.each do |key, value| = content_tag :div, class: flash_class(key) do button.close[type="button" data-dismiss="alert"] | × = value ================================================ FILE: app/views/shared/admin/_header.html.slim ================================================ nav class="app-header navbar navbar-expand bg-body" /! Left navbar links ul.navbar-nav li.nav-item a.nav-link data-lte-toggle='sidebar' data-widget="pushmenu" href="javascript:void(0)" i.fas.fa-bars /! SEARCH FORM - if content_for?(:search) = yield(:search) /! Right navbar links ul.navbar-nav.ml-auto /! Messages Dropdown Menu li.nav-item.dropdown.show a.nav-link.dropdown-toggle href="javascript:void(0)" data-toggle="dropdown" span = current_admin.name ul.dropdown-menu.dropdown-menu-right li.user-header = link_to 'Account Setting', edit_admin_account_path, class: 'dropdown-item' .dropdown-divider = link_to 'Logout', admin_logout_path, method: :delete, class: 'dropdown-item' ================================================ FILE: app/views/shared/admin/_sidebar.html.slim ================================================ /! Main Sidebar Container aside.app-sidebar.bg-body-secondary.shadow data-bs-theme="dark" .sidebar-brand /! Brand Logo a.brand-link href=admin_root_path = image_tag 'logo.png', class: 'brand-image img-circle elevation-3' span.brand-text.fw-light Dashboard /! Sidebar .sidebar-wrapper data-overlayscrollbars="host" .os-size-observer .os-size-observer-listener /! Sidebar Menu nav.mt-2 ul.nav.sidebar-menu.flex-column data-lte-toggle="treeview" data-accordion="false" li.nav-header 数据 li.nav-item = link_to admin_root_path, class: "nav-link #{admin_active_for(admin_root_path, current_path)}" do i.nav-icon.fas.fa-tachometer-alt span 数据中心 li.nav-header 博客 li.nav-item = link_to admin_posts_path, class: "nav-link #{admin_active_for(admin_posts_path, current_path)}" do i.nav-icon.fas.fa-pen-square span 博客管理 li.nav-item = link_to admin_all_comments_path, class: "nav-link #{admin_active_for(admin_all_comments_path, current_path)}" do i.nav-icon.fas.fa-comment span 评论管理 li.nav-header 标签 li.nav-item = link_to admin_labels_path, class: "nav-link #{admin_active_for(admin_labels_path, current_path)}" do i.nav-icon.fas.fa-list span 标签管理 ================================================ FILE: babel.config.js ================================================ module.exports = function(api) { var validEnv = ['development', 'test', 'production'] var currentEnv = api.env() var isDevelopmentEnv = api.env('development') var isProductionEnv = api.env('production') var isTestEnv = api.env('test') if (!validEnv.includes(currentEnv)) { throw new Error( 'Please specify a valid `NODE_ENV` or ' + '`BABEL_ENV` environment variables. Valid values are "development", ' + '"test", and "production". Instead, received: ' + JSON.stringify(currentEnv) + '.' ) } return { presets: [ isTestEnv && [ '@babel/preset-env', { targets: { node: 'current' } } ], (isProductionEnv || isDevelopmentEnv) && [ '@babel/preset-env', { forceAllTransforms: true, useBuiltIns: 'entry', corejs: 3, modules: false, exclude: ['transform-typeof-symbol'] } ] ].filter(Boolean), plugins: [ 'babel-plugin-macros', '@babel/plugin-syntax-dynamic-import', isTestEnv && 'babel-plugin-dynamic-import-node', '@babel/plugin-transform-destructuring', [ '@babel/plugin-proposal-class-properties', { loose: true } ], [ '@babel/plugin-proposal-object-rest-spread', { useBuiltIns: true } ], [ '@babel/plugin-transform-runtime', { helpers: false } ], [ '@babel/plugin-transform-regenerator', { async: false } ] ].filter(Boolean) } } ================================================ FILE: bin/bundle ================================================ #!/usr/bin/env ruby # frozen_string_literal: true # # This file was generated by Bundler. # # The application 'bundle' is installed as part of a gem, and # this file is here to facilitate running it. # require "rubygems" m = Module.new do module_function def invoked_as_script? File.expand_path($0) == File.expand_path(__FILE__) end def env_var_version ENV["BUNDLER_VERSION"] end def cli_arg_version return unless invoked_as_script? # don't want to hijack other binstubs return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` bundler_version = nil update_index = nil ARGV.each_with_index do |a, i| if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN bundler_version = a end next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ bundler_version = $1 update_index = i end bundler_version end def gemfile gemfile = ENV["BUNDLE_GEMFILE"] return gemfile if gemfile && !gemfile.empty? File.expand_path("../../Gemfile", __FILE__) end def lockfile lockfile = case File.basename(gemfile) when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) else "#{gemfile}.lock" end File.expand_path(lockfile) end def lockfile_version return unless File.file?(lockfile) lockfile_contents = File.read(lockfile) return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ Regexp.last_match(1) end def bundler_version @bundler_version ||= env_var_version || cli_arg_version || lockfile_version end def bundler_requirement return "#{Gem::Requirement.default}.a" unless bundler_version bundler_gem_version = Gem::Version.new(bundler_version) requirement = bundler_gem_version.approximate_recommendation return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.7.0") requirement += ".a" if bundler_gem_version.prerelease? requirement end def load_bundler! ENV["BUNDLE_GEMFILE"] ||= gemfile activate_bundler end def activate_bundler gem_error = activation_error_handling do gem "bundler", bundler_requirement end return if gem_error.nil? require_error = activation_error_handling do require "bundler/version" end return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" exit 42 end def activation_error_handling yield nil rescue StandardError, LoadError => e e end end m.load_bundler! if m.invoked_as_script? load Gem.bin_path("bundler", "bundle") end ================================================ FILE: bin/dev ================================================ #!/usr/bin/env sh if gem list --no-installed --exact --silent foreman; then echo "Installing foreman..." gem install foreman fi # Default to port 3000 if not specified export PORT="${PORT:-3000}" exec foreman start -f Procfile.dev --env /dev/null "$@" ================================================ FILE: bin/rails ================================================ #!/usr/bin/env ruby APP_PATH = File.expand_path("../config/application", __dir__) require_relative "../config/boot" require "rails/commands" ================================================ FILE: bin/rake ================================================ #!/usr/bin/env ruby require_relative "../config/boot" require "rake" Rake.application.run ================================================ FILE: bin/setup ================================================ #!/usr/bin/env ruby require 'fileutils' # path to your application root. APP_ROOT = File.expand_path('..', __dir__) def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") end FileUtils.chdir APP_ROOT do # This script is a way to setup or update your development environment automatically. # This script is idempotent, so that you can run it at anytime and get an expectable outcome. # Add necessary setup steps to this file. puts '== Installing dependencies ==' system! 'gem install bundler --conservative' system('bundle check') || system!('bundle install') # Install JavaScript dependencies system('bin/yarn') # puts "\n== Copying sample files ==" unless File.exist?('config/database.yml') FileUtils.cp 'config/database.yml.example', 'config/database.yml' end unless File.exist?('config/application.yml') FileUtils.cp 'config/application.yml.example', 'config/application.yml' end puts "\n== Preparing database ==" system! 'bin/rails db:prepare' puts "\n== Preparing db:seed ==" system! 'bin/rails db:seed' puts "\n== Removing old logs and tempfiles ==" system! 'bin/rails log:clear tmp:clear' puts "\n== Restarting application server ==" system! 'bin/rails restart' end ================================================ FILE: bin/spring ================================================ #!/usr/bin/env ruby if !defined?(Spring) && [nil, "development", "test"].include?(ENV["RAILS_ENV"]) gem "bundler" require "bundler" # Load Spring without loading other gems in the Gemfile, for speed. Bundler.locked_gems&.specs&.find { |spec| spec.name == "spring" }&.tap do |spring| Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path gem "spring", spring.version require "spring/binstub" rescue Gem::LoadError # Ignore when Spring is not installed. end end ================================================ FILE: bin/yarn ================================================ #!/usr/bin/env ruby APP_ROOT = File.expand_path('..', __dir__) Dir.chdir(APP_ROOT) do yarn = ENV["PATH"].split(File::PATH_SEPARATOR). select { |dir| File.expand_path(dir) != __dir__ }. product(["yarn", "yarn.cmd", "yarn.ps1"]). map { |dir, file| File.expand_path(file, dir) }. find { |file| File.executable?(file) } if yarn exec yarn, *ARGV else $stderr.puts "Yarn executable was not detected in the system." $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" exit 1 end end ================================================ FILE: config/application.rb ================================================ require_relative "boot" require "rails/all" # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) module Wblog class Application < Rails::Application config.generators do |g| g.test_framework :rspec, fixtures: true, view_specs: false, helper_specs: false, routing_specs: false, controller_specs: false, request_specs: false g.fixture_replacement :factory_bot, dir: "spec/factories" end config.generators.assets = false config.generators.helper = false config.time_zone = 'Beijing' config.i18n.available_locales = [:en, :'zh-CN'] config.i18n.default_locale = :'zh-CN' # Initialize configuration defaults for originally generated Rails version. config.load_defaults 7.0 # Please, add to the `ignore` list any other `lib` subdirectories that do # not contain `.rb` files, or that should not be reloaded or eager loaded. # Common ones are `templates`, `generators`, or `middleware`, for example. config.autoload_lib(ignore: %w(assets tasks)) # Configuration for the application, engines, and railties goes here. # # These settings can be overridden in specific environments using the files # in config/environments, which are processed later. # # config.time_zone = "Central Time (US & Canada)" # config.eager_load_paths << Rails.root.join("extras") end end ================================================ FILE: config/application.yml.example ================================================ # This section MUST be configured SITE_NAME: "WinDy's Blog" # domain name used by action cable DOMAIN: '' PROTOCOL: http # sidekiq db SIDEKIQ_DB: '1' # google anlytics GA: '' # Rails secret token, use `rake secret` to generate new one here. SECRET_KEY_BASE: 'e4122773d4324fce978c52cde790d84d14f7194f377aea41b7b8302d1d10150e6076a3b7e5e0c1f24ca330cf0a058482c95ea37908bba1722d0761ba5d4e566a' #locale: en or zh-CN, zh-CN is default value LOCALE: 'zh-CN' # sidekiq redis namespace, if you configure two blog in on VPS, you should change it. # default is wblog REDIS_NAMESPACE: 'wblog' # META description for SEO INTRODUCE: '这是李亚飞的博客, 李亚飞是暂住在深圳的一名 Ruby 程序员, 这里是关于技术, 创业, 生活的思考' #Website Address for footer display without http SITE_ADDRESS: 'yafeilee.com' #Website Year for footer display SITE_YEAR: '© 2012 - 2019' #Website beian for footer display SITE_BEIAN: '粤ICP备19030132号-3' # optional # google analytics, blank it if you don't need GOOGLE: '' # CDN ( optional ) CDN: '' # Email Setting, see more: /config/environments/production.rb MAIL_SERVER: '' DOMAIN_NAME: '' MAIL_USERNAME: '' MAIL_PASSWORD: '' ================================================ FILE: config/backup.rb.example ================================================ # encoding: utf-8 ## backup gem example ## Howto: ## $ gem install backup ## $ backup generate:model --trigger wblog --archives --storages='local' --compressor='gzip' ## $ cp config/backup.rb.example ~/Backup/models/wblog.rb ## $ backup perform --trigger wblog Model.new(:wblog, 'Description for wblog') do database PostgreSQL do |db| db.name = "wblog_production" db.username = "postgres" db.password = "postgres" db.host = "localhost" db.port = 5432 end archive :rails_config do |archive| archive.add "/data/www/wblog/shared/config/" archive.add "/etc/nginx/conf.d/" end store_with Local do |local| local.path = "/data/www/backups/" local.keep = 5 end # Use FTP instead of Local in production environments #store_with FTP do |server| #server.username = "" #server.password = "" #server.ip = "" #server.port = 21 #server.path = "~/backups/" #server.keep = 5 ## server.keep = Time.now - 2592000 # Remove all backups older than 1 month. #server.passive_mode = false #end compress_with Gzip end ================================================ FILE: config/boot.rb ================================================ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) require "bundler/setup" # Set up gems listed in the Gemfile. require "bootsnap/setup" # Speed up boot time by caching expensive operations. ================================================ FILE: config/cable.yml ================================================ development: adapter: async test: adapter: test production: adapter: redis url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> channel_prefix: wblog_production ================================================ FILE: config/credentials.yml.enc ================================================ lNIHR59bC+oahS9eflbwKRqAEUPxasN91LCSNg875kdit4+EUfInV7wQEZ8bsnI+aZxxEF+Rbx9reUR42p5yCSJQCRv8BB6W0bwX65lPypdeivt+puy1SmcLdWWkBcLAyFUAQiVw1Qk+/XrZWqT/P1pIVX1fMBJ/cAIddTZvooIl2PXMEJYmGFUq0sjCYpfsZqrGB1R2WT/7Ks8YAiOG2nyzfaY1OrhRd7UkaCk37SolIYoyrf8gTgZ56mcEcBn9801YZ6hNAmdwUb66TeUMwq+HyJJtwPPDCnqOxlUjbEQVnlqJ27qTvatQj3z2w+NK6UzgYjoZ+m1TQlg0pXqnnemsf2PdWCpmlutUyNxnFZX/Zshdi9gLfXDvUtvtHsUDuWMX7HUQ5bFGNoLiKdRMhxkirxG9qDepDwuo--yAFRd2ZelQPqJupj--VZZY7ftgfjWkvrItf9JYig== ================================================ FILE: config/database.yml.example ================================================ development: adapter: postgresql host: localhost encoding: unicode database: wblog_development pool: 5 username: postgres password: postgres template: template0 test: adapter: postgresql host: localhost encoding: unicode database: wblog_test pool: 5 username: postgres password: postgres template: template0 production: adapter: postgresql host: localhost encoding: unicode database: wblog_production pool: 16 username: postgres password: postgres template: template0 ================================================ FILE: config/deploy/production.rb ================================================ set :domain, 'yafeilee.com' set :deploy_to, '/data/www/wblog' set :repository, 'git@github.com:windy/wblog.git' set :branch, 'master' set :user, 'ruby' ================================================ FILE: config/deploy.rb ================================================ STDOUT.sync = true set :stages, %w(production) set :default_stage, 'production' require 'mina/bundler' require 'mina/rails' require 'mina/git' require 'mina/rbenv' require 'mina/puma' require "mina_sidekiq/tasks" require 'mina/logs' require 'mina/multistage' set :asset_dirs, fetch(:asset_dirs, []).push('app/javascript') set :shared_dirs, fetch(:shared_dirs, []).push('log', 'public/uploads', 'public/packs', 'node_modules', 'storage') set :shared_files, fetch(:shared_files, []).push('config/database.yml', 'config/application.yml') set :puma_config, ->{ "#{fetch(:current_path)}/config/puma.rb" } set :sidekiq_pid, ->{ "#{fetch(:shared_path)}/tmp/pids/sidekiq.pid" } task :remote_environment do invoke :'rbenv:load' end task :setup do command %[mkdir -p "#{fetch(:shared_path)}/tmp/sockets"] command %[chmod g+rx,u+rwx "#{fetch(:shared_path)}/tmp/sockets"] command %[mkdir -p "#{fetch(:shared_path)}/tmp/pids"] command %[chmod g+rx,u+rwx "#{fetch(:shared_path)}/tmp/pids"] command %[mkdir -p "#{fetch(:shared_path)}/log"] command %[chmod g+rx,u+rwx "#{fetch(:shared_path)}/log"] command %[mkdir -p "#{fetch(:shared_path)}/public/uploads"] command %[chmod g+rx,u+rwx "#{fetch(:shared_path)}/public/uploads"] command %[mkdir -p "#{fetch(:shared_path)}/public/packs"] command %[chmod g+rx,u+rwx "#{fetch(:shared_path)}/public/packs"] command %[mkdir -p "#{fetch(:shared_path)}/node_modules"] command %[chmod g+rx,u+rwx "#{fetch(:shared_path)}/node_modules"] command %[mkdir -p "#{fetch(:shared_path)}/storage"] command %[chmod g+rx,u+rwx "#{fetch(:shared_path)}/storage"] command %[mkdir -p "#{fetch(:shared_path)}/config"] command %[chmod g+rx,u+rwx "#{fetch(:shared_path)}/config"] command %[touch "#{fetch(:shared_path)}/config/application.yml"] command %[echo "-----> Be sure to edit '#{fetch(:shared_path)}/config/application.yml'"] command %[touch "#{fetch(:shared_path)}/config/database.yml"] command %[echo "-----> Be sure to edit '#{fetch(:shared_path)}/config/database.yml'"] end desc "Clear bootsnap cache" task :clear_bootsnap do command %[echo "Clear bootsnap cache..."] command %[rm -rf "#{fetch(:shared_path)}/tmp/bootsnap-*"] end desc "Deploys the current version to the server." task :deploy do command %[echo "-----> Server: #{fetch(:domain)}"] command %[echo "-----> Path: #{fetch(:deploy_to)}"] command %[echo "-----> Branch: #{fetch(:branch)}"] deploy do invoke :'git:clone' invoke :'deploy:link_shared_paths' invoke :'bundle:install' invoke :'rails:db_migrate' invoke :clear_bootsnap invoke :'rails:assets_precompile' invoke :'deploy:cleanup' on :launch do invoke :'rbenv:load' invoke :'sidekiq:quiet' invoke :'puma:smart_restart' invoke :'sidekiq:restart' end end end desc "Prepare the first deploy on server." task :first_deploy do command %[echo "-----> Server: #{fetch(:domain)}"] command %[echo "-----> Path: #{fetch(:deploy_to)}"] command %[echo "-----> Branch: #{fetch(:branch)}"] deploy do invoke :'git:clone' invoke :'deploy:link_shared_paths' invoke :'bundle:install' invoke :'rails:assets_precompile' invoke :'deploy:cleanup' on :launch do invoke :'rbenv:load' invoke :'rails:db_create' invoke :'rails', 'db:migrate' invoke :'rails', 'db:seed' end end end ================================================ FILE: config/environment.rb ================================================ # Load the Rails application. require_relative "application" # Initialize the Rails application. Rails.application.initialize! ================================================ FILE: config/environments/development.rb ================================================ require "active_support/core_ext/integer/time" Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # In the development environment your application's code is reloaded any time # it changes. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.enable_reloading = true config.file_watcher = ActiveSupport::EventedFileUpdateChecker # Do not eager load code on boot. config.eager_load = false # Show full error reports. config.consider_all_requests_local = true # Enable server timing config.server_timing = true # Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. if Rails.root.join("tmp/caching-dev.txt").exist? config.action_controller.perform_caching = true config.action_controller.enable_fragment_cache_logging = true config.cache_store = :memory_store config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{2.days.to_i}" } else config.action_controller.perform_caching = false config.cache_store = :null_store end # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false config.action_mailer.perform_caching = false # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log # Raise exceptions for disallowed deprecations. config.active_support.disallowed_deprecation = :raise # Tell Active Support which deprecation messages to disallow. config.active_support.disallowed_deprecation_warnings = [] # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load # Highlight code that triggered database queries in logs. config.active_record.verbose_query_logs = true # Highlight code that enqueued background job in logs. config.active_job.verbose_enqueue_logs = true # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true # Annotate rendered view with file names. # config.action_view.annotate_rendered_view_with_filenames = true # Uncomment if you wish to allow Action Cable access from any origin. # config.action_cable.disable_request_forgery_protection = true # Raise error when a before_action's only/except options reference missing actions config.action_controller.raise_on_missing_callback_actions = true # Allow Clacky domains to access the application config.hosts << /.*/ end ================================================ FILE: config/environments/production.rb ================================================ require "active_support/core_ext/integer/time" Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # Code is not reloaded between requests. config.enable_reloading = false # Eager load code on boot. This eager loads most of Rails and # your application in memory, allowing both threaded web servers # and those relying on copy on write to perform better. # Rake tasks automatically ignore this option for performance. config.eager_load = true # Full error reports are disabled and caching is turned on. config.consider_all_requests_local = false config.action_controller.perform_caching = true # Ensures that a master key has been made available in ENV["RAILS_MASTER_KEY"], config/master.key, or an environment # key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files). # config.require_master_key = true # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead. # config.public_file_server.enabled = false # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.asset_host = "http://assets.example.com" # Specifies the header that your server uses for sending files. # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local # Mount Action Cable outside main process or domain. # config.action_cable.mount_path = nil # config.action_cable.url = "wss://example.com/cable" # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] config.action_cable.allowed_request_origins = [ "#{ENV['PROTOCOL']}://#{ENV['DOMAIN']}" ] # Assume all access to the app is happening through a SSL-terminating reverse proxy. # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies. # config.assume_ssl = true # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # config.force_ssl = true # Log to STDOUT by default config.logger = ActiveSupport::Logger.new(STDOUT) .tap { |logger| logger.formatter = ::Logger::Formatter.new } .then { |logger| ActiveSupport::TaggedLogging.new(logger) } # Prepend all log lines with the following tags. config.log_tags = [ :request_id ] # "info" includes generic and useful information about system operation, but avoids logging too much # information to avoid inadvertent exposure of personally identifiable information (PII). If you # want to log everything, set the level to "debug". config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") # Use a different cache store in production. # config.cache_store = :mem_cache_store # Use a real queuing backend for Active Job (and separate queues per environment). # config.active_job.queue_adapter = :resque # config.active_job.queue_name_prefix = "wblog_production" config.action_mailer.perform_caching = false # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true # Don't log any deprecations. config.active_support.report_deprecations = false # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false # Enable DNS rebinding protection and other `Host` header attacks. # config.hosts = [ # "example.com", # Allow requests from example.com # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` # ] # Skip DNS rebinding protection for the default health check endpoint. # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } end ================================================ FILE: config/environments/test.rb ================================================ require "active_support/core_ext/integer/time" # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # While tests run files are not watched, reloading is not necessary. config.enable_reloading = false # Eager loading loads your entire application. When running a single test locally, # this is usually not necessary, and can slow down your test suite. However, it's # recommended that you enable it in continuous integration systems to ensure eager # loading is working properly before deploying your code. config.eager_load = ENV["CI"].present? # Configure public file server for tests with Cache-Control for performance. config.public_file_server.enabled = true config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{1.hour.to_i}" } # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false config.cache_store = :null_store # Render exception templates for rescuable exceptions and raise for other exceptions. config.action_dispatch.show_exceptions = :rescuable # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false # Store uploaded files on the local file system in a temporary directory. config.active_storage.service = :test config.action_mailer.perform_caching = false # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr # Raise exceptions for disallowed deprecations. config.active_support.disallowed_deprecation = :raise # Tell Active Support which deprecation messages to disallow. config.active_support.disallowed_deprecation_warnings = [] # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true # Annotate rendered view with file names. # config.action_view.annotate_rendered_view_with_filenames = true # Raise error when a before_action's only/except options reference missing actions config.action_controller.raise_on_missing_callback_actions = true end ================================================ FILE: config/initializers/application_controller_renderer.rb ================================================ # Be sure to restart your server when you modify this file. # ActiveSupport::Reloader.to_prepare do # ApplicationController.renderer.defaults.merge!( # http_host: 'example.org', # https: false # ) # end ================================================ FILE: config/initializers/assets.rb ================================================ # Be sure to restart your server when you modify this file. # Version of your assets, change this if you want to expire all your assets. Rails.application.config.assets.version = '1.0' # Add additional assets to the asset load path. # Rails.application.config.assets.paths << Emoji.images_path # Add Yarn node_modules folder to the asset load path. Rails.application.config.assets.paths << Rails.root.join('node_modules') Rails.application.config.assets.paths << Rails.root.join('node_modules/@fortawesome/fontawesome-free/webfonts') # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in the app/assets # folder are already added. Rails.application.config.assets.precompile += %w( admin.js admin.css ) ================================================ FILE: config/initializers/backtrace_silencers.rb ================================================ # Be sure to restart your server when you modify this file. # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. # Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) } # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code # by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'". Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"] ================================================ FILE: config/initializers/browser_warrior.rb ================================================ # BrowserWarrior.detect do |browser| # next true if Rails.env.test? # next true if browser.bot? # next true unless browser.known? # # Allow weixin callback # next true if browser.platform.other? # # See https://github.com/fnando/browser#usage for more usage # next true if browser.wechat? # next true if browser.weibo? # next true if browser.facebook? # # Block known non-modern browser # next false if browser.chrome?("<= 65") # next false if browser.safari?("< 10") # next false if browser.firefox?("< 52") # next false if browser.ie?("< 11") # next false if browser.edge?("< 15") # next false if browser.opera?("< 50") # # Allow by default # next true # end ================================================ FILE: config/initializers/content_security_policy.rb ================================================ # Be sure to restart your server when you modify this file. # Define an application-wide content security policy. # See the Securing Rails Applications Guide for more information: # https://guides.rubyonrails.org/security.html#content-security-policy-header # Rails.application.configure do # config.content_security_policy do |policy| # policy.default_src :self, :https # policy.font_src :self, :https, :data # policy.img_src :self, :https, :data # policy.object_src :none # policy.script_src :self, :https # policy.style_src :self, :https # # Specify URI for violation reports # # policy.report_uri "/csp-violation-report-endpoint" # end # # # Generate session nonces for permitted importmap, inline scripts, and inline styles. # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } # config.content_security_policy_nonce_directives = %w(script-src style-src) # # # Report violations without enforcing the policy. # # config.content_security_policy_report_only = true # end ================================================ FILE: config/initializers/cookies_serializer.rb ================================================ # Be sure to restart your server when you modify this file. # Specify a serializer for the signed and encrypted cookie jars. # Valid options are :json, :marshal, and :hybrid. Rails.application.config.action_dispatch.cookies_serializer = :json ================================================ FILE: config/initializers/filter_parameter_logging.rb ================================================ # Be sure to restart your server when you modify this file. # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. # Use this to limit dissemination of sensitive information. # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. Rails.application.config.filter_parameters += [ :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn ] ================================================ FILE: config/initializers/inflections.rb ================================================ # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections # are locale specific, and you may define rules for as many different # locales as you wish. All of these examples are active by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.plural /^(ox)$/i, "\\1en" # inflect.singular /^(ox)en/i, "\\1" # inflect.irregular "person", "people" # inflect.uncountable %w( fish sheep ) # end # These inflection rules are supported but not enabled by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.acronym "RESTful" # end ================================================ FILE: config/initializers/kaminari_config.rb ================================================ # frozen_string_literal: true Kaminari.configure do |config| # config.default_per_page = 25 # config.max_per_page = nil # config.window = 4 # config.outer_window = 0 # config.left = 0 # config.right = 0 # config.page_method_name = :page # config.param_name = :page # config.max_pages = nil # config.params_on_first_page = false end ================================================ FILE: config/initializers/mime_types.rb ================================================ # Be sure to restart your server when you modify this file. # Add new mime types for use in respond_to blocks: # Mime::Type.register "text/richtext", :rtf ================================================ FILE: config/initializers/permissions_policy.rb ================================================ # Be sure to restart your server when you modify this file. # Define an application-wide HTTP permissions policy. For further # information see: https://developers.google.com/web/updates/2018/06/feature-policy # Rails.application.config.permissions_policy do |policy| # policy.camera :none # policy.gyroscope :none # policy.microphone :none # policy.usb :none # policy.fullscreen :self # policy.payment :self, "https://secure.example.com" # end ================================================ FILE: config/initializers/sidekiq.rb ================================================ Sidekiq.configure_server do |config| config.redis = { db: ENV['SIDEKIQ_DB'].presence || '1' } end Sidekiq.configure_client do |config| config.redis = { db: ENV['SIDEKIQ_DB'].presence || '1' } end ================================================ FILE: config/initializers/simple_form.rb ================================================ # frozen_string_literal: true # # Uncomment this and change the path if necessary to include your own # components. # See https://github.com/plataformatec/simple_form#custom-components to know # more about custom components. # Dir[Rails.root.join('lib/components/**/*.rb')].each { |f| require f } # # Use this setup block to configure all options available in SimpleForm. SimpleForm.setup do |config| # Wrappers are used by the form builder to generate a # complete input. You can remove any component from the # wrapper, change the order or even add your own to the # stack. The options given below are used to wrap the # whole input. config.wrappers :default, class: :input, hint_class: :field_with_hint, error_class: :field_with_errors, valid_class: :field_without_errors do |b| ## Extensions enabled by default # Any of these extensions can be disabled for a # given input by passing: `f.input EXTENSION_NAME => false`. # You can make any of these extensions optional by # renaming `b.use` to `b.optional`. # Determines whether to use HTML5 (:email, :url, ...) # and required attributes b.use :html5 # Calculates placeholders automatically from I18n # You can also pass a string as f.input placeholder: "Placeholder" b.use :placeholder ## Optional extensions # They are disabled unless you pass `f.input EXTENSION_NAME => true` # to the input. If so, they will retrieve the values from the model # if any exists. If you want to enable any of those # extensions by default, you can change `b.optional` to `b.use`. # Calculates maxlength from length validations for string inputs # and/or database column lengths b.optional :maxlength # Calculate minlength from length validations for string inputs b.optional :minlength # Calculates pattern from format validations for string inputs b.optional :pattern # Calculates min and max from length validations for numeric inputs b.optional :min_max # Calculates readonly automatically from readonly attributes b.optional :readonly ## Inputs # b.use :input, class: 'input', error_class: 'is-invalid', valid_class: 'is-valid' b.use :label_input b.use :hint, wrap_with: { tag: :span, class: :hint } b.use :error, wrap_with: { tag: :span, class: :error } ## full_messages_for # If you want to display the full error message for the attribute, you can # use the component :full_error, like: # # b.use :full_error, wrap_with: { tag: :span, class: :error } end # The default wrapper to be used by the FormBuilder. config.default_wrapper = :default # Define the way to render check boxes / radio buttons with labels. # Defaults to :nested for bootstrap config. # inline: input + label # nested: label > input config.boolean_style = :nested # Default class for buttons config.button_class = 'btn' # Method used to tidy up errors. Specify any Rails Array method. # :first lists the first message for each field. # Use :to_sentence to list all errors for each field. # config.error_method = :first # Default tag used for error notification helper. config.error_notification_tag = :div # CSS class to add for error notification helper. config.error_notification_class = 'error_notification' # Series of attempts to detect a default label method for collection. # config.collection_label_methods = [ :to_label, :name, :title, :to_s ] # Series of attempts to detect a default value method for collection. # config.collection_value_methods = [ :id, :to_s ] # You can wrap a collection of radio/check boxes in a pre-defined tag, defaulting to none. # config.collection_wrapper_tag = nil # You can define the class to use on all collection wrappers. Defaulting to none. # config.collection_wrapper_class = nil # You can wrap each item in a collection of radio/check boxes with a tag, # defaulting to :span. # config.item_wrapper_tag = :span # You can define a class to use in all item wrappers. Defaulting to none. # config.item_wrapper_class = nil # How the label text should be generated altogether with the required text. # config.label_text = lambda { |label, required, explicit_label| "#{required} #{label}" } # You can define the class to use on all labels. Default is nil. # config.label_class = nil # You can define the default class to be used on forms. Can be overriden # with `html: { :class }`. Defaulting to none. # config.default_form_class = nil # You can define which elements should obtain additional classes # config.generate_additional_classes_for = [:wrapper, :label, :input] # Whether attributes are required by default (or not). Default is true. # config.required_by_default = true # Tell browsers whether to use the native HTML5 validations (novalidate form option). # These validations are enabled in SimpleForm's internal config but disabled by default # in this configuration, which is recommended due to some quirks from different browsers. # To stop SimpleForm from generating the novalidate option, enabling the HTML5 validations, # change this configuration to true. config.browser_validations = false # Collection of methods to detect if a file type was given. # config.file_methods = [ :mounted_as, :file?, :public_filename, :attached? ] # Custom mappings for input types. This should be a hash containing a regexp # to match as key, and the input type that will be used when the field name # matches the regexp as value. # config.input_mappings = { /count/ => :integer } # Custom wrappers for input types. This should be a hash containing an input # type as key and the wrapper that will be used for all inputs with specified type. # config.wrapper_mappings = { string: :prepend } # Namespaces where SimpleForm should look for custom input classes that # override default inputs. # config.custom_inputs_namespaces << "CustomInputs" # Default priority for time_zone inputs. # config.time_zone_priority = nil # Default priority for country inputs. # config.country_priority = nil # When false, do not use translations for labels. # config.translate_labels = true # Automatically discover new inputs in Rails' autoload path. # config.inputs_discovery = true # Cache SimpleForm inputs discovery # config.cache_discovery = !Rails.env.development? # Default class for inputs # config.input_class = nil # Define the default class of the input wrapper of the boolean input. config.boolean_label_class = 'checkbox' # Defines if the default input wrapper class should be included in radio # collection wrappers. # config.include_default_input_wrapper_class = true # Defines which i18n scope will be used in Simple Form. # config.i18n_scope = 'simple_form' # Defines validation classes to the input_field. By default it's nil. # config.input_field_valid_class = 'is-valid' # config.input_field_error_class = 'is-invalid' end ================================================ FILE: config/initializers/simple_form_bootstrap.rb ================================================ # frozen_string_literal: true # Please do not make direct changes to this file! # This generator is maintained by the community around simple_form-bootstrap: # https://github.com/rafaelfranca/simple_form-bootstrap # All future development, tests, and organization should happen there. # Background history: https://github.com/plataformatec/simple_form/issues/1561 # Uncomment this and change the path if necessary to include your own # components. # See https://github.com/plataformatec/simple_form#custom-components # to know more about custom components. # Dir[Rails.root.join('lib/components/**/*.rb')].each { |f| require f } # Use this setup block to configure all options available in SimpleForm. SimpleForm.setup do |config| # Default class for buttons config.button_class = 'btn btn-primary' # Define the default class of the input wrapper of the boolean input. config.boolean_label_class = 'form-check-label' # How the label text should be generated altogether with the required text. config.label_text = lambda { |label, required, explicit_label| "#{label} #{required}" } # Define the way to render check boxes / radio buttons with labels. config.boolean_style = :inline # You can wrap each item in a collection of radio/check boxes with a tag config.item_wrapper_tag = :div # Defines if the default input wrapper class should be included in radio # collection wrappers. config.include_default_input_wrapper_class = false # CSS class to add for error notification helper. config.error_notification_class = 'alert alert-danger' # Method used to tidy up errors. Specify any Rails Array method. # :first lists the first message for each field. # :to_sentence to list all errors for each field. config.error_method = :to_sentence # add validation classes to `input_field` config.input_field_error_class = 'is-invalid' config.input_field_valid_class = 'is-valid' # vertical forms # # vertical default_wrapper config.wrappers :vertical_form, tag: 'div', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b| b.use :html5 b.use :placeholder b.optional :maxlength b.optional :minlength b.optional :pattern b.optional :min_max b.optional :readonly b.use :label, class: 'form-control-label' b.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback' } b.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' } end # vertical input for boolean config.wrappers :vertical_boolean, tag: 'fieldset', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b| b.use :html5 b.optional :readonly b.wrapper :form_check_wrapper, tag: 'div', class: 'form-check' do |bb| bb.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' bb.use :label, class: 'form-check-label' bb.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback' } bb.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' } end end # vertical input for radio buttons and check boxes config.wrappers :vertical_collection, item_wrapper_class: 'form-check', tag: 'fieldset', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b| b.use :html5 b.optional :readonly b.wrapper :legend_tag, tag: 'legend', class: 'col-form-label pt-0' do |ba| ba.use :label_text end b.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' } b.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' } end # vertical input for inline radio buttons and check boxes config.wrappers :vertical_collection_inline, item_wrapper_class: 'form-check form-check-inline', tag: 'fieldset', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b| b.use :html5 b.optional :readonly b.wrapper :legend_tag, tag: 'legend', class: 'col-form-label pt-0' do |ba| ba.use :label_text end b.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' } b.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' } end # vertical file input config.wrappers :vertical_file, tag: 'div', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b| b.use :html5 b.use :placeholder b.optional :maxlength b.optional :minlength b.optional :readonly b.use :label b.use :input, class: 'form-control-file', error_class: 'is-invalid', valid_class: 'is-valid' b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' } b.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' } end # vertical multi select config.wrappers :vertical_multi_select, tag: 'div', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b| b.use :html5 b.optional :readonly b.use :label, class: 'form-control-label' b.wrapper tag: 'div', class: 'd-flex flex-row justify-content-between align-items-center' do |ba| ba.use :input, class: 'form-control mx-1', error_class: 'is-invalid', valid_class: 'is-valid' end b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' } b.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' } end # vertical range input config.wrappers :vertical_range, tag: 'div', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b| b.use :html5 b.use :placeholder b.optional :readonly b.optional :step b.use :label b.use :input, class: 'form-control-range', error_class: 'is-invalid', valid_class: 'is-valid' b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' } b.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' } end # horizontal forms # # horizontal default_wrapper config.wrappers :horizontal_form, tag: 'div', class: 'form-group row', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b| b.use :html5 b.use :placeholder b.optional :maxlength b.optional :minlength b.optional :pattern b.optional :min_max b.optional :readonly b.use :label, class: 'col-sm-3 col-form-label' b.wrapper :grid_wrapper, tag: 'div', class: 'col-sm-9' do |ba| ba.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' ba.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback' } ba.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' } end end # horizontal input for boolean config.wrappers :horizontal_boolean, tag: 'div', class: 'form-group row', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b| b.use :html5 b.optional :readonly b.wrapper tag: 'label', class: 'col-sm-3' do |ba| ba.use :label_text end b.wrapper :grid_wrapper, tag: 'div', class: 'col-sm-9' do |wr| wr.wrapper :form_check_wrapper, tag: 'div', class: 'form-check' do |bb| bb.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' bb.use :label, class: 'form-check-label' bb.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' } bb.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' } end end end # horizontal input for radio buttons and check boxes config.wrappers :horizontal_collection, item_wrapper_class: 'form-check', tag: 'div', class: 'form-group row', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b| b.use :html5 b.optional :readonly b.use :label, class: 'col-sm-3 form-control-label' b.wrapper :grid_wrapper, tag: 'div', class: 'col-sm-9' do |ba| ba.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' ba.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' } ba.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' } end end # horizontal input for inline radio buttons and check boxes config.wrappers :horizontal_collection_inline, item_wrapper_class: 'form-check form-check-inline', tag: 'div', class: 'form-group row', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b| b.use :html5 b.optional :readonly b.use :label, class: 'col-sm-3 form-control-label' b.wrapper :grid_wrapper, tag: 'div', class: 'col-sm-9' do |ba| ba.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' ba.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' } ba.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' } end end # horizontal file input config.wrappers :horizontal_file, tag: 'div', class: 'form-group row', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b| b.use :html5 b.use :placeholder b.optional :maxlength b.optional :minlength b.optional :readonly b.use :label, class: 'col-sm-3 form-control-label' b.wrapper :grid_wrapper, tag: 'div', class: 'col-sm-9' do |ba| ba.use :input, error_class: 'is-invalid', valid_class: 'is-valid' ba.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' } ba.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' } end end # horizontal multi select config.wrappers :horizontal_multi_select, tag: 'div', class: 'form-group row', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b| b.use :html5 b.optional :readonly b.use :label, class: 'col-sm-3 control-label' b.wrapper :grid_wrapper, tag: 'div', class: 'col-sm-9' do |ba| ba.wrapper tag: 'div', class: 'd-flex flex-row justify-content-between align-items-center' do |bb| bb.use :input, class: 'form-control mx-1', error_class: 'is-invalid', valid_class: 'is-valid' end ba.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' } ba.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' } end end # horizontal range input config.wrappers :horizontal_range, tag: 'div', class: 'form-group row', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b| b.use :html5 b.use :placeholder b.optional :readonly b.optional :step b.use :label, class: 'col-sm-3 form-control-label' b.wrapper :grid_wrapper, tag: 'div', class: 'col-sm-9' do |ba| ba.use :input, class: 'form-control-range', error_class: 'is-invalid', valid_class: 'is-valid' ba.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' } ba.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' } end end # inline forms # # inline default_wrapper config.wrappers :inline_form, tag: 'span', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b| b.use :html5 b.use :placeholder b.optional :maxlength b.optional :minlength b.optional :pattern b.optional :min_max b.optional :readonly b.use :label, class: 'sr-only' b.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' b.use :error, wrap_with: { tag: 'div', class: 'invalid-feedback' } b.optional :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' } end # inline input for boolean config.wrappers :inline_boolean, tag: 'span', class: 'form-check flex-wrap justify-content-start mr-sm-2', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b| b.use :html5 b.optional :readonly b.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' b.use :label, class: 'form-check-label' b.use :error, wrap_with: { tag: 'div', class: 'invalid-feedback' } b.optional :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' } end # bootstrap custom forms # # custom input for boolean config.wrappers :custom_boolean, tag: 'fieldset', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b| b.use :html5 b.optional :readonly b.wrapper :form_check_wrapper, tag: 'div', class: 'custom-control custom-checkbox' do |bb| bb.use :input, class: 'custom-control-input', error_class: 'is-invalid', valid_class: 'is-valid' bb.use :label, class: 'custom-control-label' bb.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback' } bb.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' } end end config.wrappers :custom_boolean_switch, tag: 'fieldset', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b| b.use :html5 b.optional :readonly b.wrapper :form_check_wrapper, tag: 'div', class: 'custom-control custom-checkbox-switch' do |bb| bb.use :input, class: 'custom-control-input', error_class: 'is-invalid', valid_class: 'is-valid' bb.use :label, class: 'custom-control-label' bb.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback' } bb.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' } end end # custom input for radio buttons and check boxes config.wrappers :custom_collection, item_wrapper_class: 'custom-control', tag: 'fieldset', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b| b.use :html5 b.optional :readonly b.wrapper :legend_tag, tag: 'legend', class: 'col-form-label pt-0' do |ba| ba.use :label_text end b.use :input, class: 'custom-control-input', error_class: 'is-invalid', valid_class: 'is-valid' b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' } b.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' } end # custom input for inline radio buttons and check boxes config.wrappers :custom_collection_inline, item_wrapper_class: 'custom-control custom-control-inline', tag: 'fieldset', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b| b.use :html5 b.optional :readonly b.wrapper :legend_tag, tag: 'legend', class: 'col-form-label pt-0' do |ba| ba.use :label_text end b.use :input, class: 'custom-control-input', error_class: 'is-invalid', valid_class: 'is-valid' b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' } b.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' } end # custom file input config.wrappers :custom_file, tag: 'div', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b| b.use :html5 b.use :placeholder b.optional :maxlength b.optional :minlength b.optional :readonly b.use :label, class: 'form-control-label' b.wrapper :custom_file_wrapper, tag: 'div', class: 'custom-file' do |ba| ba.use :input, class: 'custom-file-input', error_class: 'is-invalid', valid_class: 'is-valid' ba.use :label, class: 'custom-file-label' ba.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback' } end b.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' } end # custom multi select config.wrappers :custom_multi_select, tag: 'div', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b| b.use :html5 b.optional :readonly b.use :label, class: 'form-control-label' b.wrapper tag: 'div', class: 'd-flex flex-row justify-content-between align-items-center' do |ba| ba.use :input, class: 'custom-select mx-1', error_class: 'is-invalid', valid_class: 'is-valid' end b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' } b.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' } end # custom range input config.wrappers :custom_range, tag: 'div', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b| b.use :html5 b.use :placeholder b.optional :readonly b.optional :step b.use :label, class: 'form-control-label' b.use :input, class: 'custom-range', error_class: 'is-invalid', valid_class: 'is-valid' b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' } b.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' } end # Input Group - custom component # see example app and config at https://github.com/rafaelfranca/simple_form-bootstrap # config.wrappers :input_group, tag: 'div', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b| # b.use :html5 # b.use :placeholder # b.optional :maxlength # b.optional :minlength # b.optional :pattern # b.optional :min_max # b.optional :readonly # b.use :label, class: 'form-control-label' # b.wrapper :input_group_tag, tag: 'div', class: 'input-group' do |ba| # ba.optional :prepend # ba.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' # ba.optional :append # end # b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' } # b.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' } # end # Floating Labels form # # floating labels default_wrapper config.wrappers :floating_labels_form, tag: 'div', class: 'form-label-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b| b.use :html5 b.use :placeholder b.optional :maxlength b.optional :minlength b.optional :pattern b.optional :min_max b.optional :readonly b.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' b.use :label, class: 'form-control-label' b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback' } b.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' } end # custom multi select config.wrappers :floating_labels_select, tag: 'div', class: 'form-label-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b| b.use :html5 b.optional :readonly b.use :input, class: 'custom-select custom-select-lg', error_class: 'is-invalid', valid_class: 'is-valid' b.use :label, class: 'form-control-label' b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback' } b.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' } end # The default wrapper to be used by the FormBuilder. config.default_wrapper = :vertical_form # Custom wrappers for input types. This should be a hash containing an input # type as key and the wrapper that will be used for all inputs with specified type. config.wrapper_mappings = { boolean: :vertical_boolean, check_boxes: :vertical_collection, date: :vertical_multi_select, datetime: :vertical_multi_select, file: :vertical_file, radio_buttons: :vertical_collection, range: :vertical_range, time: :vertical_multi_select } # enable custom form wrappers # config.wrapper_mappings = { # boolean: :custom_boolean, # check_boxes: :custom_collection, # date: :custom_multi_select, # datetime: :custom_multi_select, # file: :custom_file, # radio_buttons: :custom_collection, # range: :custom_range, # time: :custom_multi_select # } end ================================================ FILE: config/initializers/wrap_parameters.rb ================================================ # Be sure to restart your server when you modify this file. # This file contains settings for ActionController::ParamsWrapper which # is enabled by default. # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. ActiveSupport.on_load(:action_controller) do wrap_parameters format: [:json] end # To enable root element in JSON for ActiveRecord objects. # ActiveSupport.on_load(:active_record) do # self.include_root_in_json = true # end ================================================ FILE: config/locales/en.yml ================================================ en: comment: "Comment" edit: 'Edit' destroy: 'Destroy' announce_at: "Published at " qr_code: 'QR Code' visited_count: "Viewed " nocontent: "No More Content Here" prev: '← Prev Post' next: 'Next Post →' none: 'None' qrcodetips: 'Continue reading with mobile phone or share it to Wechat Puppy' views: pagination: previous: 'Prev' next: 'Next' home: no_blog_here: 'There is no post here' read: 'Read More >>' created_at: 'Posted At' recent: 'RECENT' subscribes: title: 'SUBSCRIBE' email: 'Email Subscribe' wechat: 'Wechat Scan QR Code' rss: 'RSS Subscribe' submit: 'Submit' submit_success: 'Submit Successfully' head: home: 'Home' tech: 'Tech' life: 'Life' creator: 'Startup' timeline: 'Timeline' about: 'About' title: home: 'Home' timeline: 'Timeline' about: 'About Me' comment_attributes: email: 'Format Validation Failed' comment_placeholder: content: 'Write your viewpoint...' name: 'Your Name' email: 'Your Email' submit: 'Submit' submitting: 'Submitting' publish_success: 'Publish Successfully' publish_fail: 'Publish Failed' archive: search: 'Search' admin: new_post: 'New Post' posts: 'Manage Posts' posts_attributes: title: 'Title' type: 'Type' labels: 'Labels' already_labels: 'Labels Haven: ' content: 'Content' preview: 'Preview' upload_photo: 'Upload Photo' submit: 'Submit' posts_head: title: 'Title' summary: 'Summary' operation: 'Operation' comments: 'Manage Comment' comments_head: name: 'Name' email: 'Email' content: 'Content' created_at: 'Created at' operation: 'Operation' reply: 'Reply' destroy: 'Destroy' subscribes: 'Manage Subscribes' subscribes_head: email: 'Email' enable: 'Enable Flag' created_at: 'Created at' operation: 'Operation' op: enable: 'Enable' disable: 'Disable' back: 'Back to Home' logout: 'Logout' dashboard: head: 'System Information' info: 'Info' data: 'Data' posts_count: 'Posts Count' comments_count: 'Comments Count' visited_count: 'Visited Count' subscribes_count: 'Subscribes Count' session: title: 'Login dashboard' username: 'Username' username_placeholder: 'Admin username' password: 'Password' password_placeholder: 'Admin password' login_button: 'Login now' login_success: 'Login successfully' no_configuration: 'There is no admin user, login failed' username_error: 'Admin username error' password_error: 'Admin password error' ================================================ FILE: config/locales/simple_form.en.yml ================================================ en: simple_form: "yes": 'Yes' "no": 'No' required: text: 'required' mark: '*' # You can uncomment the line below if you need to overwrite the whole required html. # When using html, text and mark won't be used. # html: '*' error_notification: default_message: "Some errors were found, please take a look:" # Labels and hints examples # labels: # password: 'Password' # user: # new: # email: 'E-mail para efetuar o sign in.' # edit: # email: 'E-mail.' # hints: # username: 'User name to sign in.' # password: 'No special characters, please.' ================================================ FILE: config/locales/simple_form.zh-CN.yml ================================================ en: simple_form: "yes": '是' "no": '否' required: text: '必须的' mark: '*' # You can uncomment the line below if you need to overwrite the whole required html. # When using html, text and mark won't be used. # html: '*' error_notification: default_message: "请检查以下问题:" # Examples # labels: # defaults: # password: 'Password' # user: # new: # email: 'E-mail to sign in.' # edit: # email: 'E-mail.' # hints: # defaults: # username: 'User name to sign in.' # password: 'No special characters, please.' # include_blanks: # defaults: # age: 'Rather not say' # prompts: # defaults: # age: 'Select your age' ================================================ FILE: config/locales/zh-CN.yml ================================================ zh-CN: comment: "评论" edit: '编辑' destroy: '删除' announce_at: "发表于 " qr_code: '二维码' visited_count: "浏览数 " nocontent: "没有新的内容" prev: '← 上一篇' next: '下一篇 →' none: '无' qrcodetips: '扫一扫继续阅读或者帮我分享至朋友圈' views: pagination: previous: '上一页' next: '下一页' home: no_blog_here: '这里没有博客' read: '阅读全文 >>' created_at: '发表于 ' recent: '近期博客' subscribes: title: '订阅我' email: '邮件订阅' wechat: '微信扫一扫' rss: 'RSS 订阅' submit: '提交' submit_success: '订阅成功' head: home: '首页' tech: '技术' life: '生活' creator: '创业' timeline: '时间线' about: '关于我' title: home: '首页' timeline: '时间线' about: '关于我' activerecord: errors: models: comment: attributes: email: blank: '邮件地址错误' invalid: '邮件地址错误' name: blank: '请输入你的名字' invalid: '请输入你的名字' content: blank: '请输入有价值的评论' invalid: '请输入有价值的评论' too_short: '请输入有价值的评论' comment_placeholder: content: '发表你的评论...' name: '名字' email: '邮箱, your@example.com' submit: '提交' submitting: '提交中' publish_success: '发表成功' publish_fail: '发表失败' archive: search: '搜索博客标题' admin: new_post: '新建博客' posts: '博客管理' posts_attributes: title: '标题' type: '分类' labels: '标签' already_labels: '已有标签: ' content: '正文' preview: '预览' upload_photo: '上传图片' submit: '提交' posts_head: title: '标题' summary: '概要' operation: '操作' comments: '评论管理' comments_head: name: '名字' email: '邮箱' content: '内容' created_at: '创建时间' operation: '操作' reply: '回复' destroy: '删除' subscribes: '订阅管理' subscribes_head: email: '邮箱' enable: '是否启用' created_at: '创建时间' operation: '操作' op: enable: '启用' disable: '禁用' back: '返回首页' logout: '退出' dashboard: head: '系统信息' info: '信息' data: '数据' posts_count: '文章数' comments_count: '评论数' visited_count: '浏览数' subscribes_count: '订阅数' session: title: '登录后台' username: '用户名' username_placeholder: '管理员账号' password: '密码' password_placeholder: '管理员密码' login_button: '登录' login_success: '登录成功' no_configuration: '系统未配置管理员, 无法登录' username_error: '用户名错误' password_error: '密码错误' ================================================ FILE: config/logrotate.conf.example ================================================ # truncate your rails log every day # Usage: # `cp logrotate.conf.example /etc/logrotate.d/wblog` /data/www/wblog/current/log/*.log { daily missingok rotate 7 compress delaycompress notifempty copytruncate su ruby ruby } ================================================ FILE: config/monit.conf.example ================================================ # Watch your rails app & sidekiq process and restart it automatically # Usage: # `sudo apt-get install monit -y` # `cp monit.conf.example /etc/monit/conf.d/` # `service monit restart` check process wblog_puma with pidfile /data/www/wblog/shared/tmp/pids/puma.pid start program = "/bin/sh -c 'cd /data/www/wblog/current; PATH=bin:/home/ruby/.rbenv/shims:/home/ruby/.rbenv/bin:$PATH LC_ALL=en_US.UTF-8 RAILS_ENV="production" bundle exec puma -q -d -e production -C config/puma.rb'" as uid ruby and gid ruby with timeout 90 seconds stop program = "/bin/sh -c 'cd /data/www/wblog/current; PATH=bin:/home/ruby/.rbenv/shims:/home/ruby/.rbenv/bin:$PATH LC_ALL=en_US.UTF-8 RAILS_ENV="production" bundle exec pumactl -F /data/www/wblog/current/config/puma.rb stop'" as uid ruby and gid ruby with timeout 90 seconds group wblog check process wblog_sidekiq with pidfile /data/www/wblog/shared/tmp/pids/sidekiq.pid start program = "/bin/sh -c 'cd /data/www/wblog/current; PATH=bin:/home/ruby/.rbenv/shims:/home/ruby/.rbenv/bin:$PATH LC_ALL=en_US.UTF-8 RAILS_ENV="production" bundle exec sidekiq -d -e production -C /data/www/wblog/current/config/sidekiq.yml -i 0 -P /data/www/wblog/shared/tmp/pids/sidekiq.pid -L /data/www/wblog/current/log/sidekiq.log'" as uid ruby and gid ruby with timeout 90 seconds stop program = "/bin/sh -c 'cd /data/www/wblog/current; PATH=bin:/home/ruby/.rbenv/shims:/home/ruby/.rbenv/bin:$PATH LC_ALL=en_US.UTF-8 RAILS_ENV="production" bundle exec sidekiqctl stop /data/www/wblog/shared/tmp/pids/sidekiq.pid 11'" as uid ruby and gid ruby with timeout 90 seconds group wblog #check process wblog_clockwork # with pidfile /data/www/wblog/shared/tmp/pids/clockworkd.clock.pid # start program = "/bin/sh -c 'cd /data/www/wblog/current; PATH=bin:/home/ruby/.rbenv/shims:/home/ruby/.rbenv/bin:$PATH LC_ALL=en_US.UTF-8 RAILS_ENV=production bundle exec clockworkd -c /data/www/wblog/current/config/clock.rb -i clock -d /data/www/wblog/current --pid-dir /data/www/wblog/shared/tmp/pids --log --log-dir /data/www/wblog/shared/log start'" as uid ruby and gid ruby with timeout 90 seconds # stop program = "/bin/sh -c 'cd /data/www/wblog/current; PATH=bin:/home/ruby/.rbenv/shims:/home/ruby/.rbenv/bin:$PATH LC_ALL=en_US.UTF-8 RAILS_ENV=production bundle exec clockworkd -c /data/www/wblog/current/config/clock.rb -i clock -d /data/www/wblog/current --pid-dir /data/www/wblog/shared/tmp/pids --log --log-dir /data/www/wblog/shared/log stop'" as uid ruby and gid ruby with timeout 90 seconds # group wblog ================================================ FILE: config/nginx.conf.example ================================================ upstream wblog { server unix:///data/www/wblog/shared/tmp/sockets/puma.sock fail_timeout=0; } server { listen 80; server_name example.com; root /data/www/wblog/current/public; location ^~ /packs/ { gzip_static on; expires max; add_header Cache-Control public; } location /cable { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://wblog; } location ~ ^/(uploads)/ { expires max; break; } try_files $uri/index.html $uri @wblog; location @wblog { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_redirect off; proxy_pass http://wblog; } error_page 500 502 503 504 /500.html; client_max_body_size 20M; keepalive_timeout 10; } ================================================ FILE: config/nginx.ssl.conf.example ================================================ upstream wblog { server unix:///data/www/wblog/shared/tmp/sockets/puma.sock fail_timeout=0; } server { listen 80; # FIXME: update your domain here server_name example.com; return 301 https://$server_name$request_uri; } server { listen 443 ssl; # FIXME: update your domain here server_name example.com; root /data/www/wblog/current/public; # FIXME: update your domain here ssl_certificate /etc/nginx/sslkeys/yourdomain.com.key.pem; ssl_certificate_key /etc/nginx/sslkeys/yourdomain.com.key; ssl_dhparam /etc/nginx/sslkeys/dhparam.pem; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # ssl optimizations ssl_session_cache shared:SSL:30m; ssl_session_timeout 30m; add_header Strict-Transport-Security "max-age=31536000"; ssl_prefer_server_ciphers on; ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH'; location ^~ /packs/ { gzip_static on; expires max; add_header Cache-Control public; } location ~ ^/(uploads)/ { expires max; break; } location /cable { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://wblog; } try_files $uri/index.html $uri @wblog; location @wblog { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-Proto $scheme; proxy_redirect off; proxy_pass http://wblog; } error_page 500 502 503 504 /500; error_page 404 /404; client_max_body_size 20M; keepalive_timeout 10; } ================================================ FILE: config/puma.rb ================================================ # require 'puma/daemon' if ENV['RAILS_ENV'] == 'production' app_root = '/data/www/wblog/shared' pidfile "#{app_root}/tmp/pids/puma.pid" state_path "#{app_root}/tmp/pids/puma.state" bind "unix://#{app_root}/tmp/sockets/puma.sock" activate_control_app "unix://#{app_root}/tmp/sockets/pumactl.sock" daemonize true workers 2 threads 8, 16 prune_bundler stdout_redirect "#{app_root}/log/puma_access.log", "#{app_root}/log/puma_error.log", true else plugin :tmp_restart end ================================================ FILE: config/routes.rb ================================================ require 'sidekiq/web' Sidekiq::Web.set :session_secret, Rails.application.secrets[:secret_key_base] class AdminConstraint def matches?(request) return false unless request.session[:current_admin_id].present? admin = Administrator.find_by(id: request.session[:current_admin_id]) admin.present? end end Rails.application.routes.draw do resources :blogs, only: [:show, :edit] do resources :likes, only: [:index, :create, :destroy] resources :comments, only: [:index, :create] do collection do get :refresh end end end resources :archives, only: [:index] resources :photos, only: [:create] get '/about', to: 'home#about' namespace :admin do get 'login', to: 'sessions#new', as: :login post 'login', to: 'sessions#create' delete 'logout', to: 'sessions#destroy', as: :logout resource :account, only: [:edit, :update] resources :posts, only: [:index, :new, :edit, :create, :update, :destroy] do collection do post :preview end resources :comments, only: [:index, :destroy] end resources :all_comments, only: [:index, :destroy] resources :labels root to: 'dashboard#index' end mount Sidekiq::Web => '/sidekiq', constraints: AdminConstraint.new #mount ActionCable.server => '/cable' root to: 'home#index' end ================================================ FILE: config/secret.yml ================================================ development: secret_key_base: df19bc95da417d0dc49e659249d64706168116e77fb348533c32d817cb0a36fe5b6e88303ad680448ddbea65f418a5b8a7293b8c9cbc1bd0f4c4469284e80af7 test: secret_key_base: 0d0438783b9226801805416e41998780841716fab8794206f17742a7724301dbc67c629a15037afd4df58782c02fe0a91912cbdc7e48453b3aa1fbb6eeacd3bb # Do not keep production secrets in the repository, # instead read values from the environment. production: secret_key_base: <%= ENV['SECRET_KEY_BASE'] %> ================================================ FILE: config/sidekiq.yml ================================================ :concurrency: 5 :queues: - critical - default - low production: :concurrency: 25 ================================================ FILE: config/spring.rb ================================================ %w( .ruby-version .rbenv-vars tmp/restart.txt tmp/caching-dev.txt config/application.yml ).each { |path| Spring.watch(path) } ================================================ FILE: config/storage.yml ================================================ test: service: Disk root: <%= Rails.root.join("tmp/storage") %> local: service: Disk root: <%= Rails.root.join("storage") %> # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) # amazon: # service: S3 # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> # region: us-east-1 # bucket: your_own_bucket # Remember not to checkin your GCS keyfile to a repository # google: # service: GCS # project: your_project # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> # bucket: your_own_bucket # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) # microsoft: # service: AzureStorage # storage_account_name: your_account_name # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> # container: your_container_name # mirror: # service: Mirror # primary: local # mirrors: [ amazon, google, microsoft ] ================================================ FILE: config.ru ================================================ # This file is used by Rack-based servers to start the application. require_relative "config/environment" run Rails.application Rails.application.load_server ================================================ FILE: db/migrate/20160420082319_create_posts.rb ================================================ class CreatePosts < ActiveRecord::Migration[6.1] def change create_table :posts do |t| t.string :title t.text :content t.integer :visited_count, default: 0 t.timestamps end end end ================================================ FILE: db/migrate/20160420082536_create_comments.rb ================================================ class CreateComments < ActiveRecord::Migration[6.1] def change create_table :comments do |t| t.string :name t.string :email t.text :content t.references :post t.timestamps end end end ================================================ FILE: db/migrate/20160420082629_create_labels.rb ================================================ class CreateLabels < ActiveRecord::Migration[6.1] def change create_table :labels do |t| t.string :name t.timestamps end end end ================================================ FILE: db/migrate/20160420082734_create_likes.rb ================================================ class CreateLikes < ActiveRecord::Migration[6.1] def change create_table :likes do |t| t.references :post t.timestamps end end end ================================================ FILE: db/migrate/20160420082811_create_photos.rb ================================================ class CreatePhotos < ActiveRecord::Migration[6.1] def change create_table :photos do |t| t.string :image t.timestamps end end end ================================================ FILE: db/migrate/20160421035040_create_join_table_post_label.rb ================================================ class CreateJoinTablePostLabel < ActiveRecord::Migration[6.1] def change create_join_table :posts, :labels do |t| t.index [:post_id, :label_id] t.index [:label_id, :post_id] end end end ================================================ FILE: db/migrate/20210614151036_create_active_storage_tables.active_storage.rb ================================================ # This migration comes from active_storage (originally 20170806125915) class CreateActiveStorageTables < ActiveRecord::Migration[5.2] def change create_table :active_storage_blobs do |t| t.string :key, null: false t.string :filename, null: false t.string :content_type t.text :metadata t.string :service_name, null: false t.bigint :byte_size, null: false t.string :checksum, null: false t.datetime :created_at, null: false t.index [ :key ], unique: true end create_table :active_storage_attachments do |t| t.string :name, null: false t.references :record, null: false, polymorphic: true, index: false t.references :blob, null: false t.datetime :created_at, null: false t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true t.foreign_key :active_storage_blobs, column: :blob_id end create_table :active_storage_variant_records do |t| t.belongs_to :blob, null: false, index: false t.string :variation_digest, null: false t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true t.foreign_key :active_storage_blobs, column: :blob_id end end end ================================================ FILE: db/migrate/20210614151102_create_administrators.rb ================================================ class CreateAdministrators < ActiveRecord::Migration[6.1] def change create_table :administrators do |t| t.string :name t.string :password_digest t.timestamps end add_index :administrators, :name, unique: true end end ================================================ FILE: db/migrate/20250120142353_add_service_name_to_active_storage_blobs.active_storage.rb ================================================ # This migration comes from active_storage (originally 20190112182829) class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0] def up return unless table_exists?(:active_storage_blobs) unless column_exists?(:active_storage_blobs, :service_name) add_column :active_storage_blobs, :service_name, :string if configured_service = ActiveStorage::Blob.service.name ActiveStorage::Blob.unscoped.update_all(service_name: configured_service) end change_column :active_storage_blobs, :service_name, :string, null: false end end def down return unless table_exists?(:active_storage_blobs) remove_column :active_storage_blobs, :service_name end end ================================================ FILE: db/migrate/20250120142354_create_active_storage_variant_records.active_storage.rb ================================================ # This migration comes from active_storage (originally 20191206030411) class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0] def change return unless table_exists?(:active_storage_blobs) # Use Active Record's configured type for primary key create_table :active_storage_variant_records, id: primary_key_type, if_not_exists: true do |t| t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type t.string :variation_digest, null: false t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true t.foreign_key :active_storage_blobs, column: :blob_id end end private def primary_key_type config = Rails.configuration.generators config.options[config.orm][:primary_key_type] || :primary_key end def blobs_primary_key_type pkey_name = connection.primary_key(:active_storage_blobs) pkey_column = connection.columns(:active_storage_blobs).find { |c| c.name == pkey_name } pkey_column.bigint? ? :bigint : pkey_column.type end end ================================================ FILE: db/migrate/20250120142355_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb ================================================ # This migration comes from active_storage (originally 20211119233751) class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0] def change return unless table_exists?(:active_storage_blobs) change_column_null(:active_storage_blobs, :checksum, true) end end ================================================ FILE: db/schema.rb ================================================ # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. # # This file is the source Rails uses to define your schema when running `bin/rails # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to # be faster and is potentially less error prone than running all of your # migrations from scratch. Old migrations may fail to apply correctly if those # migrations use external dependencies or application code. # # It's strongly recommended that you check this file into your version control system. ActiveRecord::Schema[7.1].define(version: 2025_01_20_142355) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false t.bigint "record_id", null: false t.bigint "blob_id", null: false t.datetime "created_at", precision: nil, null: false t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true end create_table "active_storage_blobs", force: :cascade do |t| t.string "key", null: false t.string "filename", null: false t.string "content_type" t.text "metadata" t.string "service_name", null: false t.bigint "byte_size", null: false t.string "checksum" t.datetime "created_at", precision: nil, null: false t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true end create_table "active_storage_variant_records", force: :cascade do |t| t.bigint "blob_id", null: false t.string "variation_digest", null: false t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true end create_table "administrators", force: :cascade do |t| t.string "name" t.string "password_digest" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["name"], name: "index_administrators_on_name", unique: true end create_table "comments", force: :cascade do |t| t.string "name" t.string "email" t.text "content" t.bigint "post_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["post_id"], name: "index_comments_on_post_id" end create_table "labels", force: :cascade do |t| t.string "name" t.datetime "created_at", null: false t.datetime "updated_at", null: false end create_table "labels_posts", id: false, force: :cascade do |t| t.bigint "post_id", null: false t.bigint "label_id", null: false t.index ["label_id", "post_id"], name: "index_labels_posts_on_label_id_and_post_id" t.index ["post_id", "label_id"], name: "index_labels_posts_on_post_id_and_label_id" end create_table "likes", force: :cascade do |t| t.bigint "post_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["post_id"], name: "index_likes_on_post_id" end create_table "photos", force: :cascade do |t| t.string "image" t.datetime "created_at", null: false t.datetime "updated_at", null: false end create_table "posts", force: :cascade do |t| t.string "title" t.text "content" t.integer "visited_count", default: 0 t.datetime "created_at", null: false t.datetime "updated_at", null: false end add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" end ================================================ FILE: db/seeds.rb ================================================ # This file should contain all the record creation needed to seed the database with default values. # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). require 'faker' # Clear existing data puts "Clearing existing data..." Like.delete_all Comment.delete_all Post.all.each { |post| post.labels.clear } # Remove associations Post.delete_all Label.delete_all Administrator.delete_all puts 'Creating admin user...' Administrator.create!( name: 'admin', password: 'admin' ) puts 'Creating labels...' labels = [ 'Technology', 'Programming', 'Ruby on Rails', 'Web Development', 'Software Engineering' ].map do |name| Label.create!(name: name) end puts 'Creating blog posts...' posts = 10.times.map do |i| post = Post.create!( title: Faker::Lorem.unique.sentence(word_count: 5), content: "## #{Faker::Lorem.sentence}\n\n" + Faker::Markdown.random + "\n\n### #{Faker::Lorem.sentence}\n\n" + Faker::Lorem.paragraphs(number: 3).join("\n\n") + "\n\n```ruby\n" + "class Example\n def test\n puts 'Hello World'\n end\nend\n" + "```\n\n" + Faker::Lorem.paragraphs(number: 2).join("\n\n"), created_at: Faker::Date.between(from: 1.year.ago, to: Date.today), visited_count: 0 ) # Associate 1-3 random labels with each post post.labels << labels.sample(rand(1..3)) post end puts 'Creating comments...' 15.times do post = posts.sample Comment.create!( post: post, name: Faker::Name.name, email: Faker::Internet.email, content: Faker::Lorem.paragraph(sentence_count: 2, supplemental: true, random_sentences_to_add: 3), created_at: Faker::Time.between(from: post.created_at, to: Date.today) ) end puts 'Creating likes...' 20.times do Like.create!( post: posts.sample ) end puts 'Seed data created successfully!' ================================================ FILE: doc/.gitkeep ================================================ ================================================ FILE: lib/assets/.keep ================================================ ================================================ FILE: lib/markdown.rb ================================================ require 'rouge' require 'rouge/plugins/redcarpet' class CodeHTML < Redcarpet::Render::HTML include Rouge::Plugins::Redcarpet def initialize(extensions = {}) super extensions.merge(link_attributes: { target: "_blank" }) end end ================================================ FILE: lib/tasks/.keep ================================================ ================================================ FILE: lib/templates/slim/scaffold/_form.html.slim ================================================ = simple_form_for(@<%= singular_table_name %>) do |f| = f.error_notification = f.error_notification message: f.object.errors[:base].to_sentence if f.object.errors[:base].present? .form-inputs <%- attributes.each do |attribute| -%> = f.<%= attribute.reference? ? :association : :input %> :<%= attribute.name %> <%- end -%> .form-actions = f.button :submit ================================================ FILE: log/.keep ================================================ ================================================ FILE: package.json ================================================ { "name": "wblog", "private": true, "dependencies": { "@fortawesome/fontawesome-free": "^5.15.4", "@hotwired/stimulus": "^3.2.2", "@hotwired/turbo-rails": "^8.0.12", "@rails/actioncable": "^6.0.0", "@rails/activestorage": "^6.0.0", "@rails/ujs": "^6.0.0", "@ttskch/select2-bootstrap4-theme": "^1.5.2", "admin-lte": "^4.0.0-beta3", "bootstrap": "^4.3.1", "daterangepicker": "^3.0.5", "jquery": "^3.3.1", "js-cookie": "^2.2.1", "moment": "^2.29.2", "moment-timezone": "^0.5.33", "popper.js": "^1.14.7", "sass": "^1.83.1", "select2": "^4.1.0-rc.0", "tempusdominus-core": "^5.19.0" }, "version": "0.1.0", "devDependencies": { "esbuild": "^0.24.2" }, "scripts": { "build:css": "sass ./app/assets/stylesheets/application.scss:./app/assets/builds/application.css ./app/assets/stylesheets/admin.scss:./app/assets/builds/admin.css --no-source-map --load-path=node_modules --silence-deprecation=import --quiet-deps", "build": "esbuild app/javascript/*.js --bundle --sourcemap --format=esm --outdir=app/assets/builds --public-path=/assets" } } ================================================ FILE: postcss.config.js ================================================ module.exports = { plugins: [ require('postcss-import'), require('postcss-flexbugs-fixes'), require('postcss-preset-env')({ autoprefixer: { flexbox: 'no-2009' }, stage: 3 }) ] } ================================================ FILE: public/404.html ================================================ The page you were looking for doesn't exist (404)

    The page you were looking for doesn't exist.

    You may have mistyped the address or the page may have moved.

    If you are the application owner check the logs for more information.

    ================================================ FILE: public/422.html ================================================ The change you wanted was rejected (422)

    The change you wanted was rejected.

    Maybe you tried to change something you didn't have access to.

    If you are the application owner check the logs for more information.

    ================================================ FILE: public/500.html ================================================ We're sorry, but something went wrong (500)

    We're sorry, but something went wrong.

    If you are the application owner check the logs for more information.

    ================================================ FILE: public/robots.txt ================================================ # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file ================================================ FILE: spec/controllers/admin/comments_controller_spec.rb ================================================ require 'rails_helper' RSpec.describe Admin::CommentsController, type: :controller do end ================================================ FILE: spec/controllers/admin/dashboard_controller_spec.rb ================================================ require 'rails_helper' RSpec.describe Admin::DashboardController, type: :controller do before do session[:login] = true end describe "GET 'index'" do it "returns http success" do get 'index' expect(response).to be_successful end end end ================================================ FILE: spec/controllers/admin/posts_controller_spec.rb ================================================ require 'rails_helper' RSpec.describe Admin::PostsController, type: :controller do before do session[:login] = true end it "preview should return ok" do post :preview expect(response.body).to eq("") post :preview, params: { content: '123' } expect(response.body).to eq("

    123

    \n") end it "update" do post = create(:post) patch 'update', params: { id: post.id, labels: 'think, go ' } expect(post.reload.labels.size).to eq(2) end it "destroy" do post = create(:post) label = create(:label) post.labels << label post.save! expect(label.posts.size).to eq(1) delete 'destroy', params: { id: post.id } expect( Post.all.size ).to eq(0) expect( label.reload.posts.size ).to eq(0) end it "create" do post_params = attributes_for(:post) post 'create', params: post_params.merge( labels: 'think, go ' ) post = Post.first expect( post.labels.size ).to eq(2) end it "create fail and see labels_content" do post 'create', params: { labels: 'think, go ' } expect( assigns(:post).labels_content ).to eq('think, go') end end ================================================ FILE: spec/controllers/admin/sessions_controller_spec.rb ================================================ require 'rails_helper' RSpec.describe Admin::SessionsController, type: :controller do before do session[:login] = true end describe "GET 'new'" do it "returns http success" do get 'new' expect(response).to be_successful end end end ================================================ FILE: spec/controllers/archives_controller_spec.rb ================================================ require 'rails_helper' RSpec.describe ArchivesController, type: :controller do it "get index" do create_list(:post_list, 3) get :index expect(response.status).to eq(200) end end ================================================ FILE: spec/controllers/blogs_controller_spec.rb ================================================ require 'rails_helper' RSpec.describe BlogsController, type: :controller do describe 'get INDEX' do it 'index should get by order desc' do create_list(:post_list, 5) first = Post.first first.update(title: 'first') second = Post.order(created_at: :desc)[1] get :index expect(assigns(:newest)).to eq(first) expect(assigns(:recent)[0]).to eq(second) end end describe "get SHOW" do it 'get POST' do post = create(:post) comment1 = build(:comment_no_post) comment1.post = post comment1.save! comment2 = build(:comment_no_post) comment2.post = post comment2.save! get :show, params: { id: post.id } expect(assigns(:comments)[0]).to eq(comment2) expect(assigns(:comments)[1]).to eq(comment1) end it "#prev, #next" do posts = create_list(:post_list, 3) posts = Post.order(created_at: :asc) selected = posts[1] s_prev = posts[0] s_next = posts[2] get :show, params: { id: selected.id } expect(assigns(:prev)).to eq(s_prev) expect(assigns(:next)).to eq(s_next) # 下界 selected = posts[0] get :show, params: { id: selected.id } expect(assigns(:prev)).to be_nil expect(assigns(:next)).to eq(posts[1]) # 测试上界 selected = posts[2] get :show, params: { id: selected.id } expect(assigns(:prev)).to eq(posts[1]) expect(assigns(:next)).to be_nil # 测试未来时间 create(:post, created_at: Time.now + 100) selected = posts[1] get :show, params: { id: selected.id } expect(assigns(:prev)).to eq(posts[0]) expect(assigns(:next)).to eq(posts[2]) end end end ================================================ FILE: spec/controllers/home_controller_spec.rb ================================================ require 'rails_helper' RSpec.describe HomeController, type: :controller do describe "GET 'index'" do it "returns http success" do get 'index' expect(response).to be_successful end end end ================================================ FILE: spec/controllers/likes_controller_spec.rb ================================================ require 'rails_helper' RSpec.describe LikesController, type: :controller do it "get index" do a = Post.create!(title: 'one', content: '1'*31) get 'index', params: { blog_id: a.id } expect(JSON.parse(response.body)['count']).to eq(0) a.likes << Like.new a.save! get 'index', params: { blog_id: a.id } expect(JSON.parse(response.body)['count']).to eq(1) end it "post create" do a = Post.create!(title: 'one', content: '1'*31) post 'create', params: { blog_id: a.id } expect(a.likes.size).to eq(1) end it "DELETE destroy" do a = Post.create!(title: 'one', content: '1'*31) like = Like.new a.likes << like a.save! delete 'destroy', params: { blog_id: a.id, id: like.id } expect(a.reload.likes.size).to eq(0) end end ================================================ FILE: spec/factories/comments.rb ================================================ FactoryBot.define do factory :comment do content { 'content' * 10 } name { 'commentor' } email { 'tester@xx.com' } association :post end factory :comment_no_post, class: Comment do content { 'content' * 10 } name { 'commentor' } email { 'tester@xx.com' } end end ================================================ FILE: spec/factories/labels.rb ================================================ FactoryBot.define do factory :label do name { 'label' } end end ================================================ FILE: spec/factories/posts.rb ================================================ FactoryBot.define do factory :post do title { 'this is a post title' } content { 'content' * 10 } end factory :post_list, class: Post do sequence(:title) { |n| "#{n}: post title" } content { 'content' * 10 } sequence(:created_at) { |n| n.days.ago } end end ================================================ FILE: spec/factories/subscribes.rb ================================================ # Read about factories at https://github.com/thoughtbot/factory_girl FactoryBot.define do factory :subscribe do email { "tester@mail.com" } enable { false } end end ================================================ FILE: spec/models/like_spec.rb ================================================ require 'rails_helper' RSpec.describe Like, type: :model do it "add like" do a = Post.create!(title: 'one', content: '1'*31) like = Like.new like.post = a expect(like.save).to eq(true) end end ================================================ FILE: spec/models/post_spec.rb ================================================ require 'rails_helper' RSpec.describe Post, type: :model do it "validates should be ok" do expect(create(:post)).to be_truthy end end ================================================ FILE: spec/rails_helper.rb ================================================ # This file is copied to spec/ when you run 'rails generate rspec:install' require 'spec_helper' ENV['RAILS_ENV'] ||= 'test' require File.expand_path('../config/environment', __dir__) # Prevent database truncation if the environment is production abort("The Rails environment is running in production mode!") if Rails.env.production? require 'rspec/rails' # Add additional requires below this line. Rails is not loaded until this point! # Requires supporting ruby files with custom matchers and macros, etc, in # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are # run as spec files by default. This means that files in spec/support that end # in _spec.rb will both be required and run as specs, causing the specs to be # run twice. It is recommended that you do not name files matching this glob to # end with _spec.rb. You can configure this pattern with the --pattern # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. # # The following line is provided for convenience purposes. It has the downside # of increasing the boot-up time by auto-requiring all files in the support # directory. Alternatively, in the individual `*_spec.rb` files, manually # require only the support files necessary. # Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f } # Checks for pending migrations and applies them before tests are run. # If you are not using ActiveRecord, you can remove these lines. begin ActiveRecord::Migration.maintain_test_schema! rescue ActiveRecord::PendingMigrationError => e puts e.to_s.strip exit 1 end RSpec.configure do |config| # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures config.fixture_path = "#{::Rails.root}/spec/fixtures" # If you're not using ActiveRecord, or you'd prefer not to run each of your # examples within a transaction, remove the following line or assign false # instead of true. config.use_transactional_fixtures = false # You can uncomment this line to turn off ActiveRecord support entirely. # config.use_active_record = false # RSpec Rails can automatically mix in different behaviours to your tests # based on their file location, for example enabling you to call `get` and # `post` in specs under `spec/controllers`. # # You can disable this behaviour by removing the line below, and instead # explicitly tag your specs with their type, e.g.: # # RSpec.describe UsersController, type: :controller do # # ... # end # # The different available types are documented in the features, such as in # https://relishapp.com/rspec/rspec-rails/docs config.infer_spec_type_from_file_location! # Filter lines from Rails gems in backtraces. config.filter_rails_from_backtrace! # arbitrary gems may also be filtered via: # config.filter_gems_from_backtrace("gem name") end ================================================ FILE: spec/spec_helper.rb ================================================ # This file was generated by the `rails generate rspec:install` command. Conventionally, all # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. # The generated `.rspec` file contains `--require spec_helper` which will cause # this file to always be loaded, without a need to explicitly require it in any # files. # # Given that it is always loaded, you are encouraged to keep this file as # light-weight as possible. Requiring heavyweight dependencies from this file # will add to the boot time of your test suite on EVERY test run, even for an # individual file that may not need all of that loaded. Instead, consider making # a separate helper file that requires the additional dependencies and performs # the additional setup, and require it from the spec files that actually need # it. # # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration RSpec.configure do |config| # rspec-expectations config goes here. You can use an alternate # assertion/expectation library such as wrong or the stdlib/minitest # assertions if you prefer. config.expect_with :rspec do |expectations| # This option will default to `true` in RSpec 4. It makes the `description` # and `failure_message` of custom matchers include text for helper methods # defined using `chain`, e.g.: # be_bigger_than(2).and_smaller_than(4).description # # => "be bigger than 2 and smaller than 4" # ...rather than: # # => "be bigger than 2" expectations.include_chain_clauses_in_custom_matcher_descriptions = true end # rspec-mocks config goes here. You can use an alternate test double # library (such as bogus or mocha) by changing the `mock_with` option here. config.mock_with :rspec do |mocks| # Prevents you from mocking or stubbing a method that does not exist on # a real object. This is generally recommended, and will default to # `true` in RSpec 4. mocks.verify_partial_doubles = true end # This option will default to `:apply_to_host_groups` in RSpec 4 (and will # have no way to turn it off -- the option exists only for backwards # compatibility in RSpec 3). It causes shared context metadata to be # inherited by the metadata hash of host groups and examples, rather than # triggering implicit auto-inclusion in groups with matching metadata. config.shared_context_metadata_behavior = :apply_to_host_groups # The settings below are suggested to provide a good initial experience # with RSpec, but feel free to customize to your heart's content. =begin # This allows you to limit a spec run to individual examples or groups # you care about by tagging them with `:focus` metadata. When nothing # is tagged with `:focus`, all examples get run. RSpec also provides # aliases for `it`, `describe`, and `context` that include `:focus` # metadata: `fit`, `fdescribe` and `fcontext`, respectively. config.filter_run_when_matching :focus # Allows RSpec to persist some state between runs in order to support # the `--only-failures` and `--next-failure` CLI options. We recommend # you configure your source control system to ignore this file. config.example_status_persistence_file_path = "spec/examples.txt" # Limits the available syntax to the non-monkey patched syntax that is # recommended. For more details, see: # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode config.disable_monkey_patching! # Many RSpec users commonly either run the entire suite or an individual # file, and it's useful to allow more verbose output when running an # individual spec file. if config.files_to_run.one? # Use the documentation formatter for detailed output, # unless a formatter has already been configured # (e.g. via a command-line flag). config.default_formatter = "doc" end # Print the 10 slowest examples and example groups at the # end of the spec run, to help surface which specs are running # particularly slow. config.profile_examples = 10 # Run specs in random order to surface order dependencies. If you find an # order dependency and want to debug it, you can fix the order by providing # the seed, which is printed after each run. # --seed 1234 config.order = :random # Seed global randomization in this process using the `--seed` CLI option. # Setting this allows you to use `--seed` to deterministically reproduce # test failures related to randomization by passing the same `--seed` value # as the one that triggered the failure. Kernel.srand config.seed =end end ================================================ FILE: spec/support/capybara.rb ================================================ Capybara.asset_host = 'http://localhost:3000' ================================================ FILE: spec/support/database_cleaner.rb ================================================ RSpec.configure do |config| config.before(:suite) do DatabaseCleaner.clean_with(:truncation) end config.before(:each) do DatabaseCleaner.strategy = :transaction end config.before(:each, :js => true) do DatabaseCleaner.strategy = :truncation end config.before(:each) do DatabaseCleaner.start end config.append_after(:each) do DatabaseCleaner.clean end end ================================================ FILE: spec/support/factory_bot.rb ================================================ RSpec.configure do |config| config.include FactoryBot::Syntax::Methods end ================================================ FILE: storage/.keep ================================================ ================================================ FILE: test/application_system_test_case.rb ================================================ require "test_helper" class ApplicationSystemTestCase < ActionDispatch::SystemTestCase driven_by :selenium, using: :chrome, screen_size: [1400, 1400] end ================================================ FILE: test/channels/application_cable/connection_test.rb ================================================ require "test_helper" class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase # test "connects with cookies" do # cookies.signed[:user_id] = 42 # # connect # # assert_equal connection.user_id, "42" # end end ================================================ FILE: test/controllers/.keep ================================================ ================================================ FILE: test/fixtures/files/.keep ================================================ ================================================ FILE: test/helpers/.keep ================================================ ================================================ FILE: test/integration/.keep ================================================ ================================================ FILE: test/mailers/.keep ================================================ ================================================ FILE: test/models/.keep ================================================ ================================================ FILE: test/system/.keep ================================================ ================================================ FILE: test/test_helper.rb ================================================ ENV['RAILS_ENV'] ||= 'test' require_relative "../config/environment" require "rails/test_help" class ActiveSupport::TestCase # Run tests in parallel with specified workers parallelize(workers: :number_of_processors) # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. fixtures :all # Add more helper methods to be used by all tests here... end ================================================ FILE: tmp/.keep ================================================ ================================================ FILE: vendor/.keep ================================================