├── app
├── assets
│ ├── stylesheets
│ │ ├── .gitkeep
│ │ └── search.scss
│ ├── images
│ │ └── favicon.png
│ └── javascripts
│ │ └── form_storage.coffee
├── views
│ ├── replies
│ │ ├── system_events
│ │ │ ├── _close.html.erb
│ │ │ ├── _reopen.html.erb
│ │ │ ├── _excellent.html.erb
│ │ │ ├── _unexcellent.html.erb
│ │ │ ├── _ban.html.erb
│ │ │ └── _mention.html.erb
│ │ ├── index.js.erb
│ │ ├── reply_to.js.erb
│ │ ├── _system_event.html.erb
│ │ ├── update.js.erb
│ │ ├── create.js.erb
│ │ ├── _create_callback.js.erb
│ │ ├── _reply_to.html.erb
│ │ └── edit.html.erb
│ ├── admin
│ │ ├── locations
│ │ │ ├── edit.html.erb
│ │ │ ├── _base.html.erb
│ │ │ ├── _form.html.erb
│ │ │ └── index.html.erb
│ │ ├── comments
│ │ │ ├── destroy.js.erb
│ │ │ ├── edit.html.erb
│ │ │ ├── _form.html.erb
│ │ │ └── index.html.erb
│ │ ├── replies
│ │ │ ├── destroy.js.erb
│ │ │ ├── edit.html.erb
│ │ │ └── _form.html.erb
│ │ ├── nodes
│ │ │ ├── new.html.erb
│ │ │ ├── edit.html.erb
│ │ │ ├── _form.html.erb
│ │ │ └── index.html.erb
│ │ ├── topics
│ │ │ ├── new.html.erb
│ │ │ ├── edit.html.erb
│ │ │ └── _form.html.erb
│ │ ├── users
│ │ │ ├── new.html.erb
│ │ │ └── edit.html.erb
│ │ ├── site_configs
│ │ │ ├── edit.html.erb
│ │ │ ├── _form.html.erb
│ │ │ └── index.html.erb
│ │ ├── sections
│ │ │ ├── new.html.erb
│ │ │ ├── edit.html.erb
│ │ │ ├── _form.html.erb
│ │ │ └── index.html.erb
│ │ └── photos
│ │ │ ├── show.html.erb
│ │ │ └── index.html.erb
│ ├── topics
│ │ ├── preview.json.jbuilder
│ │ ├── ban.js.erb
│ │ ├── _sidebar_box_tips.html.erb
│ │ ├── new.html.erb
│ │ ├── _ban_reason.html.erb
│ │ ├── edit.html.erb
│ │ ├── translation
│ │ │ ├── _need_login_to_reply.zh-TW.html.erb
│ │ │ └── _need_login_to_reply.zh-CN.html.erb
│ │ ├── create.js.erb
│ │ ├── update.js.erb
│ │ ├── _related_topics.html.erb
│ │ ├── _node_selector.html.erb
│ │ ├── feed.builder
│ │ ├── _sidebar_box_node_recent_topics.html.erb
│ │ ├── node_feed.builder
│ │ ├── _reply_form.html.erb
│ │ └── _sidebar_for_topic_index.html.erb
│ ├── api
│ │ └── v3
│ │ │ ├── photos
│ │ │ └── create.json.jbuilder
│ │ │ ├── nodes
│ │ │ ├── index.json.jbuilder
│ │ │ └── show.json.jbuilder
│ │ │ ├── users
│ │ │ ├── index.json.jbuilder
│ │ │ ├── blocked.json.jbuilder
│ │ │ ├── topics.json.jbuilder
│ │ │ ├── followers.json.jbuilder
│ │ │ ├── following.json.jbuilder
│ │ │ ├── show.json.jbuilder
│ │ │ └── replies.json.jbuilder
│ │ │ ├── topics
│ │ │ ├── index.json.jbuilder
│ │ │ ├── replies.json.jbuilder
│ │ │ └── show.json.jbuilder
│ │ │ ├── notifications
│ │ │ └── index.json.jbuilder
│ │ │ ├── replies
│ │ │ └── show.json.jbuilder
│ │ │ └── application
│ │ │ ├── _node.json.jbuilder
│ │ │ └── _abilities.json.jbuilder
│ ├── doorkeeper
│ │ ├── applications
│ │ │ ├── new.html.erb
│ │ │ ├── edit.html.erb
│ │ │ └── _menu.html.erb
│ │ └── authorizations
│ │ │ ├── error.html.erb
│ │ │ └── show.html.erb
│ ├── users
│ │ ├── reward.js.erb
│ │ ├── _bio.html.erb
│ │ ├── replies.html.erb
│ │ ├── topics.html.erb
│ │ ├── followers.html.erb
│ │ ├── index.html.erb
│ │ ├── _user_list.html.erb
│ │ ├── blocked.html.erb
│ │ ├── _replies.html.erb
│ │ ├── city.html.erb
│ │ ├── _topics.html.erb
│ │ ├── _recent_publish_topics.html.erb
│ │ ├── _menu.html.erb
│ │ ├── _reward.html.erb
│ │ ├── favorites.html.erb
│ │ └── _repos.html.erb
│ ├── kaminari
│ │ ├── _gap.html.erb
│ │ ├── _next_page.html.erb
│ │ ├── _prev_page.html.erb
│ │ ├── _page.html.erb
│ │ └── _paginator.html.erb
│ ├── likes
│ │ ├── index.html.erb
│ │ ├── destory.js.erb
│ │ └── create.js.erb
│ ├── notifications
│ │ ├── _mention.html.erb
│ │ ├── _follow.html.erb
│ │ ├── _topic.html.erb
│ │ ├── _node_changed.html.erb
│ │ ├── mentions
│ │ │ ├── _topic.html.erb
│ │ │ ├── _reply.html.erb
│ │ │ └── _comment.html.erb
│ │ ├── _team_invite.html.erb
│ │ ├── _topic_reply.html.erb
│ │ ├── notifications
│ │ │ ├── _notification.html.erb
│ │ │ └── index.html.erb
│ │ └── _comment.html.erb
│ ├── errors
│ │ ├── 404.html.erb
│ │ └── 403.html.erb
│ ├── layouts
│ │ └── window.html.erb
│ ├── devise
│ │ ├── menu
│ │ │ ├── _login_items.html.erb
│ │ │ └── _registration_items.html.erb
│ │ ├── mailer
│ │ │ ├── unlock_instructions.html.erb
│ │ │ ├── confirmation_instructions.html.erb
│ │ │ └── reset_password_instructions.html.erb
│ │ ├── confirmations
│ │ │ └── new.html.erb
│ │ └── unlocks
│ │ │ └── new.html.erb
│ ├── topic_mailer
│ │ └── new_reply.html.erb
│ ├── account
│ │ └── create.js.erb
│ ├── team_users
│ │ ├── edit.html.erb
│ │ ├── new.html.erb
│ │ ├── index.html.erb
│ │ ├── _team_user.html.erb
│ │ └── show.html.erb
│ ├── shared
│ │ ├── _error_messages.html.erb
│ │ ├── _hot_locations.html.erb
│ │ ├── _comment.html.erb
│ │ ├── _navbar.html.erb
│ │ └── _index_sections.html.erb
│ ├── user_mailer
│ │ └── welcome.html.erb
│ ├── comments
│ │ ├── _comment.html.erb
│ │ └── create.js.erb
│ ├── search
│ │ ├── _team.html.erb
│ │ ├── _topic.html.erb
│ │ ├── _page.html.erb
│ │ ├── index.html.erb
│ │ └── _user.html.erb
│ ├── teams
│ │ ├── index.html.erb
│ │ ├── _team_list.html.erb
│ │ ├── _sidebar.html.erb
│ │ └── show.html.erb
│ ├── home
│ │ ├── markdown.html.erb
│ │ └── index.html.erb
│ └── settings
│ │ └── _menu.html.erb
├── helpers
│ ├── nodes_helper.rb
│ ├── replies_helper.rb
│ ├── locations_helper.rb
│ └── teams_helper.rb
├── jobs
│ ├── application_job.rb
│ ├── notify_reply_job.rb
│ ├── notify_topic_job.rb
│ ├── github_repo_fetcher_job.rb
│ ├── search_indexer.rb
│ ├── mention_topic_job.rb
│ └── push_job.rb
├── models
│ ├── user_sso.rb
│ ├── authorization.rb
│ ├── concerns
│ │ ├── user_avatar_delegate.rb
│ │ ├── markdown_body.rb
│ │ ├── soft_delete.rb
│ │ ├── closeable.rb
│ │ ├── searchable.rb
│ │ └── mention_topic.rb
│ ├── photo.rb
│ ├── user
│ │ ├── blockable.rb
│ │ └── followable.rb
│ ├── application_record.rb
│ ├── device.rb
│ ├── reply
│ │ └── voteable.rb
│ ├── section.rb
│ ├── team.rb
│ ├── topic
│ │ └── auto_correct.rb
│ ├── location.rb
│ └── cache_version.rb
├── channels
│ ├── application_cable
│ │ ├── channel.rb
│ │ └── connection.rb
│ ├── replies_channel.rb
│ └── notifications_channel.rb
├── controllers
│ ├── admin
│ │ ├── home_controller.rb
│ │ ├── application_controller.rb
│ │ ├── photos_controller.rb
│ │ ├── locations_controller.rb
│ │ ├── stats_controller.rb
│ │ ├── site_configs_controller.rb
│ │ ├── comments_controller.rb
│ │ └── replies_controller.rb
│ ├── devices_controller.rb
│ ├── comments_controller.rb
│ ├── oauth
│ │ ├── authorized_applications_controller.rb
│ │ └── applications_controller.rb
│ ├── nodes_controller.rb
│ ├── photos_controller.rb
│ ├── api
│ │ └── v3
│ │ │ ├── nodes_controller.rb
│ │ │ ├── photos_controller.rb
│ │ │ └── root_controller.rb
│ ├── users
│ │ └── team_actions.rb
│ ├── passwords_controller.rb
│ ├── notifications
│ │ └── notifications_controller.rb
│ ├── sessions_controller.rb
│ ├── account_controller.rb
│ ├── likes_controller.rb
│ └── home_controller.rb
├── uploaders
│ ├── avatar_uploader.rb
│ └── photo_uploader.rb
└── mailers
│ ├── user_mailer.rb
│ └── base_mailer.rb
├── lib
├── templates
│ └── erb
│ │ └── scaffold
│ │ ├── _base.html.erb
│ │ ├── new.html.erb
│ │ ├── edit.html.erb
│ │ ├── show.html.erb
│ │ └── _form.html.erb
├── homeland
│ ├── pipeline.rb
│ ├── version.rb
│ ├── username.rb
│ ├── user_notification_helper.rb
│ ├── pipeline
│ │ ├── twemoji_filter.rb
│ │ ├── floor_filter.rb
│ │ └── normalize_mention_filter.rb
│ ├── markdown.rb
│ └── plugin.rb
├── assets
│ ├── javascripts
│ │ └── google_analytics.js
│ └── stylesheets
│ │ └── jquery.atwho.css
├── tasks
│ └── bundler_audit.rake
└── rails
│ └── generators
│ └── erb
│ └── scaffold
│ └── scaffold_generator.rb
├── public
├── robots.txt
├── api-doc
│ ├── css
│ │ └── common.css
│ └── frames.html
├── avatar.png
├── favicon.ico
└── 422.html
├── .rspec
├── config
├── locales
│ ├── nodes.en.yml
│ ├── nodes.zh-CN.yml
│ ├── nodes.zh-TW.yml
│ ├── devise.en.yml
│ ├── carrierwave.zh-CN.yml
│ ├── carrierwave.zh-TW.yml
│ ├── carrierwave.en.yml
│ ├── teams.zh-CN.yml
│ ├── teams.zh-TW.yml
│ ├── admin.zh-CN.yml
│ ├── admin.zh-TW.yml
│ ├── admin.en.yml
│ ├── teams.en.yml
│ ├── topics.zh-TW.yml
│ └── topics.zh-CN.yml
├── spring.rb
├── elasticsearch.yml
├── elasticsearch.yml.default
├── environment.rb
├── initializers
│ ├── social_share_button.rb
│ ├── mime_types.rb
│ ├── session_store.rb
│ ├── rucaptcha.rb
│ ├── per_form_csrf_tokens.rb
│ ├── elasticsearch.rb
│ ├── twemoji.rb
│ ├── request_forgery_protection.rb
│ ├── filter_parameter_logging.rb
│ ├── application_controller_renderer.rb
│ ├── cookies_serializer.rb
│ ├── status-page.rb
│ ├── kaminari_config.rb
│ ├── backtrace_silencers.rb
│ ├── wrap_parameters.rb
│ ├── exception-track.rb
│ ├── redis.rb
│ ├── cors.rb
│ ├── assets.rb
│ ├── inflections.rb
│ ├── notifications.rb
│ ├── rack-attack.rb
│ └── content_security_policy.rb
├── redis.yml.default
├── memcached.yml
├── boot.rb
├── cable.yml
├── sidekiq.yml
├── database.yml.default
├── puma.example.rb
└── secrets.yml.default
├── spec
├── fixtures
│ └── test.png
├── helpers
│ ├── nodes_helper_spec.rb
│ └── locations_helper_spec.rb
├── factories
│ ├── sections.rb
│ ├── locations.rb
│ ├── comments.rb
│ ├── devices.rb
│ ├── site_configs.rb
│ ├── notifications.rb
│ ├── replies.rb
│ ├── notification_topics.rb
│ ├── nodes.rb
│ ├── teams.rb
│ ├── topics.rb
│ ├── notification_mentions.rb
│ ├── notification_topic_replies.rb
│ ├── notification_node_changeds.rb
│ ├── access_tokens.rb
│ ├── access_grants.rb
│ ├── applications.rb
│ └── team_users.rb
├── jobs
│ ├── notify_reply_job_spec.rb
│ ├── notify_topic_job_spec.rb
│ └── github_repo_fetcher_job_spec.rb
├── requests
│ └── users_spec.rb
├── models
│ ├── notification_spec.rb
│ ├── team_user_spec.rb
│ ├── device_spec.rb
│ ├── section_spec.rb
│ └── user
│ │ └── reward_fields_spec.rb
├── controllers
│ ├── admin
│ │ └── home_controller_spec.rb
│ ├── nodes_controller_spec.rb
│ ├── devices_controller_spec.rb
│ └── search_controller_spec.rb
└── api
│ └── photos_spec.rb
├── bin
├── rake
├── gen-api-doc
├── bundle
├── rails
├── rspec
├── yarn
├── spring
└── update
├── .codeclimate.yml
├── db
├── migrate
│ ├── 20170302084611_remove_hr_fields.rb
│ ├── 20160329091441_remove_old_notifications.rb
│ ├── 20160518061135_add_closed_at_to_topics.rb
│ ├── 20170222062457_add_reply_to_id_to_replies.rb
│ ├── 20180316113342_add_last_reply_id_index_to_topics.rb
│ ├── 20160119135131_update_user_name_allow_null.rb
│ ├── 20160518083347_remove_private_token_from_users.rb
│ ├── 20160710111853_add_team_id_to_topics.rb
│ ├── 20170224074052_remove_bad_fields_from_users.rb
│ ├── 20160826044113_add_level_to_oauth_applications.rb
│ ├── 20160302043713_add_user_id_index_on_notifications.rb
│ ├── 20161215014636_remove_body_html_field.rb
│ ├── 20180316130855_rename_notifications_table_name.rb
│ ├── 20160602020013_add_target_to_replies.rb
│ ├── 20160119163218_update_oauth_token_fields_limit.rb
│ ├── 20170217050010_create_exception_track_logs.rb
│ ├── 20180614093642_change_topics_excellent_to_grade.rb
│ ├── 20160819093756_add_lockable_to_devise.rb
│ ├── 20160219075601_create_devices.rb
│ ├── 20161230094832_add_text_pattern_opts_to_users.rb
│ ├── 20160912124102_add_team_users_count_to_users.rb
│ ├── 20161221022846_create_user_sso.rb
│ ├── 20160707084438_create_team_users_join_table.rb
│ ├── 20160119191013_add_indexes_to_table.rb
│ ├── 20170207141208_remove_array_action_fields.rb
│ ├── 20161228064225_remove_unneeded_indexes.rb
│ ├── 20170204090209_create_actions.rb
│ ├── 20160329032533_create_notifications.notifications.rb
│ └── 20160126061903_create_settings.rb
└── seeds.rb
├── config.ru
├── Vagrantfile
├── Dockerfile
├── Benchmarks.md
├── .gitignore
└── Rakefile
/app/assets/stylesheets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/templates/erb/scaffold/_base.html.erb:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/views/replies/system_events/_close.html.erb:
--------------------------------------------------------------------------------
1 | 关闭了讨论
2 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | user-agent: *
2 | Disallow: /cable
3 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | -f progress
3 | --require spec_helper
4 |
--------------------------------------------------------------------------------
/app/views/replies/system_events/_reopen.html.erb:
--------------------------------------------------------------------------------
1 | 重新开启了讨论
2 |
--------------------------------------------------------------------------------
/app/views/replies/system_events/_excellent.html.erb:
--------------------------------------------------------------------------------
1 | 将本帖设为了精华贴
2 |
--------------------------------------------------------------------------------
/app/views/replies/system_events/_unexcellent.html.erb:
--------------------------------------------------------------------------------
1 | 取消了精华贴
2 |
--------------------------------------------------------------------------------
/public/api-doc/css/common.css:
--------------------------------------------------------------------------------
1 | /* Override this file with custom rules */
--------------------------------------------------------------------------------
/config/locales/nodes.en.yml:
--------------------------------------------------------------------------------
1 | "en":
2 | nodes:
3 | hot_node: "Popular Node"
--------------------------------------------------------------------------------
/config/locales/nodes.zh-CN.yml:
--------------------------------------------------------------------------------
1 | "zh-CN":
2 | nodes:
3 | hot_node: "热门节点"
--------------------------------------------------------------------------------
/config/locales/nodes.zh-TW.yml:
--------------------------------------------------------------------------------
1 | "zh-TW":
2 | nodes:
3 | hot_node: "熱門節點"
4 |
--------------------------------------------------------------------------------
/public/avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding/homeland/master/public/avatar.png
--------------------------------------------------------------------------------
/lib/templates/erb/scaffold/new.html.erb:
--------------------------------------------------------------------------------
1 | <%%= render 'base' %>
2 | <%%= render 'form' %>
3 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding/homeland/master/public/favicon.ico
--------------------------------------------------------------------------------
/app/helpers/nodes_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module NodesHelper
4 | end
5 |
--------------------------------------------------------------------------------
/app/views/admin/locations/edit.html.erb:
--------------------------------------------------------------------------------
1 | <%= render 'base' %>
2 | <%= render 'form' %>
3 |
4 |
--------------------------------------------------------------------------------
/lib/templates/erb/scaffold/edit.html.erb:
--------------------------------------------------------------------------------
1 | <%%= render 'base' %>
2 | <%%= render 'form' %>
3 |
4 |
--------------------------------------------------------------------------------
/spec/fixtures/test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding/homeland/master/spec/fixtures/test.png
--------------------------------------------------------------------------------
/app/helpers/replies_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module RepliesHelper
4 | end
5 |
--------------------------------------------------------------------------------
/app/views/admin/comments/destroy.js.erb:
--------------------------------------------------------------------------------
1 | $("#comment<%= params[:id] %>").remove();
2 | App.notice("删除成功。")
--------------------------------------------------------------------------------
/app/views/topics/preview.json.jbuilder:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | json.body markdown(@body)
4 |
--------------------------------------------------------------------------------
/app/assets/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding/homeland/master/app/assets/images/favicon.png
--------------------------------------------------------------------------------
/app/jobs/application_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationJob < ActiveJob::Base
4 | end
5 |
--------------------------------------------------------------------------------
/app/views/admin/replies/destroy.js.erb:
--------------------------------------------------------------------------------
1 | $("#reply-<%= params[:id] %>").attr("class","deleted");
2 | App.notice("删除成功。")
--------------------------------------------------------------------------------
/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require_relative '../config/boot'
3 | require 'rake'
4 | Rake.application.run
5 |
--------------------------------------------------------------------------------
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | exclude_paths:
2 | - "lib/assets/**/*"
3 | - "app/controllers/admin/**/*"
4 | - "public/api-doc/**/*"
5 |
--------------------------------------------------------------------------------
/lib/homeland/pipeline.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Homeland
4 | class Pipeline
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/app/views/api/v3/photos/create.json.jbuilder:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | json.image_url @photo.image.url(:large)
4 |
--------------------------------------------------------------------------------
/app/views/doorkeeper/applications/new.html.erb:
--------------------------------------------------------------------------------
1 | <%= render "menu" %>
2 |
3 | <%= render 'form', application: @application %>
4 |
--------------------------------------------------------------------------------
/app/models/user_sso.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class UserSSO < ActiveRecord::Base
4 | belongs_to :user
5 | end
6 |
--------------------------------------------------------------------------------
/app/views/api/v3/nodes/index.json.jbuilder:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | json.nodes @nodes, partial: "node", as: :node
4 |
--------------------------------------------------------------------------------
/app/views/api/v3/users/index.json.jbuilder:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | json.users @users, partial: "user", as: :user
4 |
--------------------------------------------------------------------------------
/app/views/doorkeeper/applications/edit.html.erb:
--------------------------------------------------------------------------------
1 | <%= render "menu" %>
2 |
3 | <%= render 'form', application: @application %>
4 |
--------------------------------------------------------------------------------
/app/views/replies/index.js.erb:
--------------------------------------------------------------------------------
1 | <% @replies.each do |reply| %>
2 | <%= render 'create_callback', reply: reply %>
3 | <% end %>
4 |
--------------------------------------------------------------------------------
/bin/gen-api-doc:
--------------------------------------------------------------------------------
1 | yard --title "Ruby China API 文档" --out public/api-doc --no-private -r API.md app/controllers/api/v3/ app/views/api/v3
--------------------------------------------------------------------------------
/app/views/api/v3/topics/index.json.jbuilder:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | json.topics @topics, partial: "topic", as: :topic
4 |
--------------------------------------------------------------------------------
/app/views/api/v3/users/blocked.json.jbuilder:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | json.blocked @users, partial: "user", as: :user
4 |
--------------------------------------------------------------------------------
/app/views/api/v3/users/topics.json.jbuilder:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | json.topics @topics, partial: "topic", as: :topic
4 |
--------------------------------------------------------------------------------
/app/views/api/v3/users/followers.json.jbuilder:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | json.followers @users, partial: "user", as: :user
4 |
--------------------------------------------------------------------------------
/app/views/api/v3/users/following.json.jbuilder:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | json.following @users, partial: "user", as: :user
4 |
--------------------------------------------------------------------------------
/app/views/doorkeeper/applications/_menu.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= link_to "应用列表", oauth_applications_path %>
3 |
--------------------------------------------------------------------------------
/app/views/users/reward.js.erb:
--------------------------------------------------------------------------------
1 | $('#reward-model').remove();
2 | $('body').append("<%= j(render('reward'))%>");
3 | $('#reward-modal').modal();
4 |
--------------------------------------------------------------------------------
/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
3 | load Gem.bin_path('bundler', 'bundle')
4 |
--------------------------------------------------------------------------------
/app/views/kaminari/_gap.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= content_tag :a, raw(t('pagination.gap')), class: "page-link" %>
3 |
4 |
--------------------------------------------------------------------------------
/app/views/topics/ban.js.erb:
--------------------------------------------------------------------------------
1 | $('#ban-modal').remove();
2 | $('body').append("<%= j(render('ban', topic: @topic)) %>");
3 | $('#ban-modal').modal();
4 |
--------------------------------------------------------------------------------
/app/views/users/_bio.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
<%= t("users.bio")%>
3 |
<%= auto_link(simple_format(bio)) %>
4 |
5 |
--------------------------------------------------------------------------------
/app/views/admin/locations/_base.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :sitemap do %>
2 | <%= t('admin.menu.locations') %>
3 | <% end %>
4 |
--------------------------------------------------------------------------------
/spec/helpers/nodes_helper_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rails_helper"
4 |
5 | describe NodesHelper, type: :helper do
6 | end
7 |
--------------------------------------------------------------------------------
/app/views/api/v3/topics/replies.json.jbuilder:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | json.replies @replies, partial: "reply", as: :reply
4 | json.meta @meta
5 |
--------------------------------------------------------------------------------
/app/views/likes/index.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <% @users.each do |user| %>
3 | <%= user_avatar_tag(user, :xs) %>
4 | <% end %>
5 |
--------------------------------------------------------------------------------
/app/views/replies/reply_to.js.erb:
--------------------------------------------------------------------------------
1 | $(".reply[data-id=<%= @reply.id %>] .reply-to-block").replaceWith("<%= j(render('reply_to', reply: @reply, show_body: true)) %>")
2 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_PATH = File.expand_path('../config/application', __dir__)
3 | require_relative '../config/boot'
4 | require 'rails/commands'
5 |
--------------------------------------------------------------------------------
/app/views/api/v3/notifications/index.json.jbuilder:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | json.notifications @notifications, partial: "notification", as: :notification
4 |
--------------------------------------------------------------------------------
/app/views/notifications/_mention.html.erb:
--------------------------------------------------------------------------------
1 | <%= render partial: "notifications/mentions/#{notification.target_type.underscore}", locals: { notification: notification } %>
2 |
--------------------------------------------------------------------------------
/db/migrate/20170302084611_remove_hr_fields.rb:
--------------------------------------------------------------------------------
1 | class RemoveHrFields < ActiveRecord::Migration[5.0]
2 | def change
3 | remove_column :users, :hr
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/channels/application_cable/channel.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ApplicationCable
4 | class Channel < ActionCable::Channel::Base
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/spec/factories/sections.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :section do
5 | sequence(:name) { |n| "name#{n}" }
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/views/topics/_sidebar_box_tips.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= random_tips %>
5 |
6 |
--------------------------------------------------------------------------------
/lib/homeland/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Homeland
4 | class << self
5 | def version
6 | "3.1.0.rc"
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/factories/locations.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :location do
5 | sequence(:name) { |n| "name#{n}" }
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/views/api/v3/nodes/show.json.jbuilder:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | json.node do
4 | json.partial! partial: "node", locals: { node: @node }
5 | end
6 | json.meta @meta
7 |
--------------------------------------------------------------------------------
/app/views/replies/system_events/_ban.html.erb:
--------------------------------------------------------------------------------
1 | <% if reply.body.present? %>
2 | 屏蔽了此话题:<%= reply.body %>
3 | <% else %>
4 | 内容不符合版规屏蔽此话题
5 | <% end %>
6 |
--------------------------------------------------------------------------------
/db/migrate/20160329091441_remove_old_notifications.rb:
--------------------------------------------------------------------------------
1 | class RemoveOldNotifications < ActiveRecord::Migration[5.0]
2 | def change
3 | drop_table :notifications
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/config/locales/devise.en.yml:
--------------------------------------------------------------------------------
1 | "zh-CN":
2 | account:
3 | signed_up: 'Sign up successed.'
4 | updated: 'Profile update successed.'
5 | destroyed: 'Goodbye! You account was deleted.'
--------------------------------------------------------------------------------
/config/spring.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | %w[
4 | .ruby-version
5 | .rbenv-vars
6 | tmp/restart.txt
7 | tmp/caching-dev.txt
8 | ].each { |path| Spring.watch(path) }
9 |
--------------------------------------------------------------------------------
/db/migrate/20160518061135_add_closed_at_to_topics.rb:
--------------------------------------------------------------------------------
1 | class AddClosedAtToTopics < ActiveRecord::Migration[5.0]
2 | def change
3 | add_column :topics, :closed_at, :datetime
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/views/api/v3/users/show.json.jbuilder:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | json.user do
4 | json.partial! partial: "user", locals: { user: @user, detail: true }
5 | end
6 | json.meta @meta
7 |
--------------------------------------------------------------------------------
/config/elasticsearch.yml:
--------------------------------------------------------------------------------
1 | defaults: &defaults
2 | host: 127.0.0.1:9200
3 |
4 | development:
5 | <<: *defaults
6 |
7 | test:
8 | <<: *defaults
9 |
10 | production:
11 | <<: *defaults
--------------------------------------------------------------------------------
/app/views/api/v3/replies/show.json.jbuilder:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | json.reply do
4 | json.partial! partial: "reply", locals: { reply: @reply, detail: true }
5 | end
6 | json.meta @meta
7 |
--------------------------------------------------------------------------------
/app/views/api/v3/topics/show.json.jbuilder:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | json.topic do
4 | json.partial! partial: "topic", locals: { topic: @topic, detail: true }
5 | end
6 | json.meta @meta
7 |
--------------------------------------------------------------------------------
/app/views/api/v3/users/replies.json.jbuilder:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | json.replies @replies do |reply|
4 | json.partial! partial: "reply", locals: { reply: reply, detail: true }
5 | end
6 |
--------------------------------------------------------------------------------
/config/elasticsearch.yml.default:
--------------------------------------------------------------------------------
1 | defaults: &defaults
2 | host: 127.0.0.1:9200
3 |
4 | development:
5 | <<: *defaults
6 |
7 | test:
8 | <<: *defaults
9 |
10 | production:
11 | <<: *defaults
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Load the Rails application.
4 | require_relative "application"
5 |
6 | # Initialize the Rails application.
7 | Rails.application.initialize!
8 |
--------------------------------------------------------------------------------
/db/migrate/20170222062457_add_reply_to_id_to_replies.rb:
--------------------------------------------------------------------------------
1 | class AddReplyToIdToReplies < ActiveRecord::Migration[5.0]
2 | def change
3 | add_column :replies, :reply_to_id, :integer
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20180316113342_add_last_reply_id_index_to_topics.rb:
--------------------------------------------------------------------------------
1 | class AddLastReplyIdIndexToTopics < ActiveRecord::Migration[5.2]
2 | def change
3 | add_index :topics, :last_reply_id
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/views/likes/destory.js.erb:
--------------------------------------------------------------------------------
1 | <% if @success %>
2 | $("#<%= @element_id %>").attr("class","icon small_like").data("method","post").attr("title", "取消赞");
3 | <% else %>
4 | App.alert("提交失败.")
5 | <% end %>
6 |
--------------------------------------------------------------------------------
/config/initializers/social_share_button.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | SocialShareButton.configure do |config|
4 | config.allow_sites = %w[twitter wechat facebook google_plus weibo douban]
5 | end
6 |
--------------------------------------------------------------------------------
/config/redis.yml.default:
--------------------------------------------------------------------------------
1 | defaults: &defaults
2 | host: 127.0.0.1
3 | port: 6379
4 |
5 | development:
6 | <<: *defaults
7 |
8 | test:
9 | <<: *defaults
10 |
11 | production:
12 | <<: *defaults
--------------------------------------------------------------------------------
/db/migrate/20160119135131_update_user_name_allow_null.rb:
--------------------------------------------------------------------------------
1 | class UpdateUserNameAllowNull < ActiveRecord::Migration[4.2]
2 | def change
3 | change_column :users, :name, :string, null: true
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20160518083347_remove_private_token_from_users.rb:
--------------------------------------------------------------------------------
1 | class RemovePrivateTokenFromUsers < ActiveRecord::Migration[5.0]
2 | def change
3 | remove_column :users, :private_token
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/views/errors/404.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Oops, Page not found
4 |
5 | 似乎没有这个页面哦!大哥,回去看看别的吧。
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/views/layouts/window.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= stylesheet_link_tag "window" %>
5 | <%= yield :scripts %>
6 |
7 |
8 | <%= yield %>
9 |
10 |
--------------------------------------------------------------------------------
/app/views/likes/create.js.erb:
--------------------------------------------------------------------------------
1 | <% if @success %>
2 | $("#<%= @element_id %>").attr("class","icon small_liked").data("method","delete").attr("title", "取消赞");
3 | <% else %>
4 | App.alert("提交失败.")
5 | <% end %>
6 |
--------------------------------------------------------------------------------
/app/channels/replies_channel.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class RepliesChannel < ApplicationCable::Channel
4 | def follow(data)
5 | stream_from "topics/#{data['topic_id']}/replies"
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/lib/homeland/username.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Homeland
4 | class Username
5 | def self.sanitize(username)
6 | username.gsub(/[^\w.-]/, "_")
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/factories/comments.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :comment do
5 | body "body"
6 | association :user
7 | association :commentable
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/factories/devices.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :device do
5 | platform 1
6 | association :user
7 | token { SecureRandom.hex }
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/db/migrate/20160710111853_add_team_id_to_topics.rb:
--------------------------------------------------------------------------------
1 | class AddTeamIdToTopics < ActiveRecord::Migration[5.0]
2 | def change
3 | add_column :topics, :team_id, :integer
4 | add_index :topics, :team_id
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/db/migrate/20170224074052_remove_bad_fields_from_users.rb:
--------------------------------------------------------------------------------
1 | class RemoveBadFieldsFromUsers < ActiveRecord::Migration[5.0]
2 | def change
3 | remove_column :users, :co
4 | remove_column :users, :qq
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/app/views/devise/menu/_login_items.html.erb:
--------------------------------------------------------------------------------
1 | <% if user_signed_in? %>
2 | <%= link_to(t("common.logout"), destroy_user_session_path) %>
3 | <% else %>
4 | <%= link_to(t("common.login"), new_user_session_path) %>
5 | <% end %>
6 |
--------------------------------------------------------------------------------
/spec/factories/site_configs.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :site_config do
5 | sequence(:key) { |n| "key_#{n}" }
6 | sequence(:value) { |n| "value_#{n}" }
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/app/views/notifications/_follow.html.erb:
--------------------------------------------------------------------------------
1 | <% user = notification.actor %>
2 | <% if user.blank? %>
3 | 相关信息已删除
4 | <% else %>
5 |
6 | <%= user_name_tag(user) %> 开始关注你了。
7 |
8 | <% end %>
9 |
--------------------------------------------------------------------------------
/app/views/notifications/_topic.html.erb:
--------------------------------------------------------------------------------
1 | <% topic = notification.target %>
2 | <% if topic.blank? %>
3 | 相关信息已删除
4 | <% else %>
5 |
6 | 创建了帖子 <%= topic_title_tag(topic) %>
7 |
8 | <% end %>
9 |
--------------------------------------------------------------------------------
/spec/factories/notifications.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :notification, class: Notification do
5 | association :user
6 | association :actor, factory: :user
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/spec/factories/replies.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :reply do
5 | sequence(:body) { |n| "body#{n}" }
6 | association :user
7 | association :topic
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | # Be sure to restart your server when you modify this file.
3 |
4 | # Add new mime types for use in respond_to blocks:
5 | # Mime::Type.register "text/richtext", :rtf
6 |
--------------------------------------------------------------------------------
/config/memcached.yml:
--------------------------------------------------------------------------------
1 | defaults: &defaults
2 | host: 127.0.0.1:11211
3 | namespace: rb-1
4 | compress: true
5 |
6 | development:
7 | <<: *defaults
8 |
9 | test:
10 | <<: *defaults
11 |
12 | production:
13 | <<: *defaults
--------------------------------------------------------------------------------
/app/jobs/notify_reply_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class NotifyReplyJob < ApplicationJob
4 | queue_as :notifications
5 |
6 | def perform(reply_id)
7 | Reply.notify_reply_created(reply_id)
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/jobs/notify_topic_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class NotifyTopicJob < ApplicationJob
4 | queue_as :notifications
5 |
6 | def perform(topic_id)
7 | Topic.notify_topic_created(topic_id)
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/models/authorization.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Authorization < ApplicationRecord
4 | belongs_to :user
5 | validates :uid, :provider, presence: true
6 | validates :uid, uniqueness: { scope: :provider }
7 | end
8 |
--------------------------------------------------------------------------------
/db/migrate/20160826044113_add_level_to_oauth_applications.rb:
--------------------------------------------------------------------------------
1 | class AddLevelToOauthApplications < ActiveRecord::Migration[5.0]
2 | def change
3 | add_column :oauth_applications, :level, :integer, default: 0, null: false
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/models/concerns/user_avatar_delegate.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module UserAvatarDelegate
4 | extend ActiveSupport::Concern
5 |
6 | def user_avatar_raw
7 | self.user ? self.user[:avatar] : nil
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/models/photo.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Photo < ApplicationRecord
4 | belongs_to :user, optional: true
5 |
6 | validates_presence_of :image
7 |
8 | # 封面图
9 | mount_uploader :image, PhotoUploader
10 | end
11 |
--------------------------------------------------------------------------------
/config/initializers/session_store.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 | Rails.application.config.session_store :cookie_store, key: "_homeland_session", expire_after: 86_400 * 90
5 |
--------------------------------------------------------------------------------
/spec/factories/notification_topics.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :notification_topic, parent: :notification do
5 | notify_type "topic"
6 | association :target, factory: :topic
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/app/views/kaminari/_next_page.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= link_to_unless current_page.last?, raw([t('pagination.next'), ' '].join(' ')), url, rel: 'next', remote: remote, class: "page-link" %>
3 |
4 |
--------------------------------------------------------------------------------
/app/views/kaminari/_prev_page.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= link_to_unless current_page.first?, raw([' ', t('pagination.prev')].join(' ')), url, rel: 'prev', remote: remote, class: "page-link" %>
3 |
4 |
--------------------------------------------------------------------------------
/app/views/topics/new.html.erb:
--------------------------------------------------------------------------------
1 | <% title_tag t('topics.new_topic') %>
2 |
3 |
4 |
5 |
6 | <%= render 'form' %>
7 |
8 |
9 |
--------------------------------------------------------------------------------
/config/initializers/rucaptcha.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | memcached_config = Rails.application.config_for(:memcached)
4 | RuCaptcha.configure do
5 | self.cache_store = [:mem_cache_store, memcached_config["host"], memcached_config]
6 | end
7 |
--------------------------------------------------------------------------------
/db/migrate/20160302043713_add_user_id_index_on_notifications.rb:
--------------------------------------------------------------------------------
1 | class AddUserIdIndexOnNotifications < ActiveRecord::Migration[5.0]
2 | def change
3 | remove_index :notifications, :read
4 | add_index :notifications, :user_id
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/spec/factories/nodes.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :node do
5 | sequence(:name) { |n| "name#{n}" }
6 | section { |s| s.association(:section) }
7 | summary "summary"
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/controllers/admin/home_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Admin
4 | class HomeController < Admin::ApplicationController
5 | def index
6 | @recent_topics = Topic.recent.limit(5)
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/jobs/github_repo_fetcher_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class GitHubRepoFetcherJob < ApplicationJob
4 | queue_as :http_request
5 |
6 | def perform(user_id)
7 | User.fetch_github_repositories(user_id)
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/views/topics/_ban_reason.html.erb:
--------------------------------------------------------------------------------
1 | <% if @topic.ban? %>
2 |
3 |
此贴已暂时被屏蔽!
4 |
5 | <%= raw Setting.ban_reason_html %>
6 |
7 |
8 | <% end %>
9 |
--------------------------------------------------------------------------------
/config/initializers/per_form_csrf_tokens.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # Enable per-form CSRF tokens.
6 | Rails.application.config.action_controller.per_form_csrf_tokens = true
7 |
--------------------------------------------------------------------------------
/app/views/replies/system_events/_mention.html.erb:
--------------------------------------------------------------------------------
1 | 在
2 |
3 | <% if reply.target_type == "Topic" %>
4 | <%= topic_title_tag(reply&.target) %>
5 | <% else %>
6 | <%= topic_title_tag(reply&.target&.topic) %>
7 | <% end %>
8 | 中提及了此贴
9 |
--------------------------------------------------------------------------------
/config/boot.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
4 |
5 | require "bundler/setup" # Set up gems listed in the Gemfile.
6 | require "bootsnap/setup" # Speed up boot time by caching expensive operations.
7 |
--------------------------------------------------------------------------------
/config/cable.yml:
--------------------------------------------------------------------------------
1 | production: &defaults
2 | adapter: redis
3 | url: redis://localhost:6379/1
4 | timeout: 1
5 |
6 | development:
7 | <<: *defaults
8 | url: redis://localhost:6379/2
9 |
10 | test:
11 | <<: *defaults
12 | url: redis://localhost:6379/3
13 |
--------------------------------------------------------------------------------
/config/initializers/elasticsearch.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "elasticsearch/rails/instrumentation"
4 |
5 | config = Rails.application.config_for(:elasticsearch)
6 |
7 | Elasticsearch::Model.client = Elasticsearch::Client.new host: config["host"]
8 |
--------------------------------------------------------------------------------
/db/migrate/20161215014636_remove_body_html_field.rb:
--------------------------------------------------------------------------------
1 | class RemoveBodyHtmlField < ActiveRecord::Migration[5.0]
2 | def change
3 | remove_column :topics, :body_html
4 | remove_column :replies, :body_html
5 | remove_column :comments, :body_html
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/views/admin/comments/edit.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :sitemap do %>
2 | <%= t("admin.menu.comments")%> <%= t("common.edit")%>
3 | <% end %>
4 |
5 | <%= render 'form' %>
6 |
--------------------------------------------------------------------------------
/app/views/replies/_system_event.html.erb:
--------------------------------------------------------------------------------
1 | <%= user_avatar_tag(reply.user, :xs) %> <%= user_name_tag(reply.user) %>
2 | <%= render partial: "/replies/system_events/#{reply.action}", locals: { reply: reply } %>
3 | <%= l reply.created_at, format: :short %>
4 |
--------------------------------------------------------------------------------
/config/initializers/twemoji.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "twemoji/svg"
4 |
5 | Twemoji.configure do |config|
6 | config.asset_root = "https://twemoji.b0.upaiyun.com/2"
7 | config.file_ext = "svg"
8 | config.class_name = "twemoji"
9 | end
10 |
--------------------------------------------------------------------------------
/db/migrate/20180316130855_rename_notifications_table_name.rb:
--------------------------------------------------------------------------------
1 | class RenameNotificationsTableName < ActiveRecord::Migration[5.2]
2 | def change
3 | # Revert Notification table name to "notifications"
4 | rename_table :new_notifications, :notifications
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/spec/factories/teams.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :team do
5 | sequence(:name) { |n| "Team #{n}" }
6 | sequence(:login) { |n| "team#{n}" }
7 | sequence(:email) { |n| "team#{n}@gethomeland.com" }
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/views/admin/nodes/new.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :sitemap do %>
2 | <%= t("admin.menu.nodes")%> <%= t("common.create")%>
3 | <% end %>
4 | 新建节点
5 |
6 | <%= render 'form' %>
7 |
--------------------------------------------------------------------------------
/config/initializers/request_forgery_protection.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # Enable origin-checking CSRF mitigation.
6 | Rails.application.config.action_controller.forgery_protection_origin_check = true
7 |
--------------------------------------------------------------------------------
/app/views/admin/topics/new.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :sitemap do %>
2 | <%= t("admin.menu.topics")%> <%= t("common.create")%>
3 | <% end %>
4 | New topic
5 |
6 | <%= render 'form' %>
7 |
--------------------------------------------------------------------------------
/app/views/admin/users/new.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :sitemap do %>
2 | <%= t("admin.menu.users")%> <%= t("common.create")%>
3 | <% end %>
4 | New user
5 |
6 | <%= render 'form' %>
7 |
--------------------------------------------------------------------------------
/spec/factories/topics.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :topic do
5 | sequence(:title) { |n| "Topic Title #{n}" }
6 | sequence(:body) { |n| "Topic Body #{n}" }
7 | association :user
8 | association :node
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/views/admin/nodes/edit.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :sitemap do %>
2 | <%= t("admin.menu.nodes")%> <%= t("common.edit")%>
3 | <% end %>
4 | 修改节点
5 |
6 | <%= render 'form' %>
7 |
8 |
9 |
--------------------------------------------------------------------------------
/bin/rspec:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | begin
5 | load File.expand_path("../spring", __FILE__)
6 | rescue LoadError => e
7 | raise unless e.message.include?("spring")
8 | end
9 | require "bundler/setup"
10 | load Gem.bin_path("rspec-core", "rspec")
11 |
--------------------------------------------------------------------------------
/app/uploaders/avatar_uploader.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AvatarUploader < BaseUploader
4 | def filename
5 | if super.present?
6 | @name ||= SecureRandom.hex(3)
7 | "avatar/#{model.id}/#{@name}.#{file.extension.downcase}"
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/views/admin/site_configs/edit.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :sitemap do %>
2 | <%= t("admin.menu.site_configs")%>
3 | <%= @site_config.var %>
4 | <% end %>
5 |
6 | <%= render 'form' %>
7 |
8 |
--------------------------------------------------------------------------------
/app/views/doorkeeper/authorizations/error.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
<%= @pre_auth.error_response.body[:error_description] %>
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/views/admin/sections/new.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :sitemap do %>
2 | <%= t("admin.menu.sections") %> <%= t("common.create") %>
3 | <% end %>
4 |
5 | 新建分类
6 |
7 | <%= render 'form' %>
8 |
9 |
--------------------------------------------------------------------------------
/app/views/devise/mailer/unlock_instructions.html.erb:
--------------------------------------------------------------------------------
1 | 你好 <%= @resource.fullname %>
2 |
3 | 由于多次错误密码尝试登录,你的帐号已经暂时被锁定了,锁定时间为 1 小时,锁定期间此帐号将无法登录,也无法尝试密码。
4 |
5 | 你也可以点击下面的链接立即解锁:
6 |
7 | <%= link_to '解锁我的帐号', unlock_url(@resource, unlock_token: @token), class: 'btn' %>
8 |
--------------------------------------------------------------------------------
/app/views/admin/sections/edit.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :sitemap do %>
2 | <%= t("admin.menu.sections") %> <%= t("common.edit") %>
3 | <% end %>
4 | Editing section
5 |
6 | <%= render 'form' %>
7 |
8 |
9 |
--------------------------------------------------------------------------------
/spec/factories/notification_mentions.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :notification_mention, parent: :notification do
5 | notify_type "mention"
6 | association :target, factory: :reply
7 | association :second_target, factory: :topic
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/db/migrate/20160602020013_add_target_to_replies.rb:
--------------------------------------------------------------------------------
1 | class AddTargetToReplies < ActiveRecord::Migration[5.0]
2 | def change
3 | add_column :replies, :action, :string
4 | add_column :replies, :target_type, :string, after: :action
5 | add_column :replies, :target_id, :string, after: :target_type
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/mailers/user_mailer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class UserMailer < BaseMailer
4 | def welcome(user_id)
5 | @user = User.find_by_id(user_id)
6 | return false if @user.blank?
7 | mail(to: @user.email, subject: t("mail.welcome_subject", app_name: Setting.app_name).to_s)
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/views/topics/edit.html.erb:
--------------------------------------------------------------------------------
1 | <% title_tag t('topics.edit_topic') %>
2 |
3 |
4 |
5 |
6 | <%= render 'form' %>
7 |
8 |
9 |
--------------------------------------------------------------------------------
/spec/factories/notification_topic_replies.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :notification_topic_reply, parent: :notification do
5 | notify_type "topic_reply"
6 | association :target, factory: :reply
7 | association :second_target, factory: :topic
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/helpers/locations_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module LocationsHelper
4 | def location_name_tag(location, _options = {})
5 | return "" if location.blank?
6 | name = location.is_a?(String) == true ? location : location.name
7 | link_to(name, location_users_path(name))
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/views/admin/replies/edit.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :sitemap do %>
2 | <%= t('admin.menu.replies') %> <%= t("common.edit") %>
3 | <% end %>
4 | <%= t("common.edit") %><%= t('admin.menu.replies') %>
5 | <%= render 'form' %>
--------------------------------------------------------------------------------
/spec/factories/notification_node_changeds.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :notification_node_changed, parent: :notification do
5 | notify_type "node_changed"
6 | association :target, factory: :topic
7 | association :second_target, factory: :node
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/models/concerns/markdown_body.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # 转换 body -> html
4 | # [Plugin API]
5 | module MarkdownBody
6 | extend ActiveSupport::Concern
7 | include ActionView::Helpers::OutputSafetyHelper
8 | include ApplicationHelper
9 |
10 | def body_html
11 | markdown(body)
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/views/topic_mailer/new_reply.html.erb:
--------------------------------------------------------------------------------
1 | 你关注的帖子有了新回复
2 |
3 | <%= user_name_tag(@reply.user) %> 回复了 《<%= topic_title_tag(@topic)%>》:
4 |
5 |
6 | <%= @reply.body_html %>
7 |
8 |
9 | 帖子地址: <%= topic_title_tag(@topic) %>.
10 |
11 |
--------------------------------------------------------------------------------
/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # Configure sensitive parameters which will be filtered from the log file.
6 | Rails.application.config.filter_parameters += %i[password password_confirmation current_password token]
7 |
--------------------------------------------------------------------------------
/db/migrate/20160119163218_update_oauth_token_fields_limit.rb:
--------------------------------------------------------------------------------
1 | class UpdateOauthTokenFieldsLimit < ActiveRecord::Migration[4.2]
2 | def change
3 | change_column :oauth_access_tokens, :expires_in, :integer, limit: 8, null: true
4 | change_column :oauth_access_grants, :expires_in, :integer, limit: 8, null: true
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/app/mailers/base_mailer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class BaseMailer < ActionMailer::Base
4 | default from: Setting.mailer_sender
5 | default charset: "utf-8"
6 | default content_type: "text/html"
7 | default_url_options[:host] = Setting.domain
8 |
9 | layout "mailer"
10 | helper :topics, :users
11 | end
12 |
--------------------------------------------------------------------------------
/app/views/replies/update.js.erb:
--------------------------------------------------------------------------------
1 | <% if @reply.errors.empty? %>
2 | <% flash[:notice] = '回帖更新成功。' %>
3 | Turbolinks.visit('<%= topic_path(@reply.topic_id) %>');
4 | <% else %>
5 | $('form[tb="edit-reply"] .alert').remove();
6 | $('form[tb="edit-reply"]').prepend('<%= j render("shared/error_messages", target: @reply) %>');
7 | <% end %>
8 |
--------------------------------------------------------------------------------
/config/initializers/application_controller_renderer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | # Be sure to restart your server when you modify this file.
3 |
4 | # ActiveSupport::Reloader.to_prepare do
5 | # ApplicationController.renderer.defaults.merge!(
6 | # http_host: 'example.org',
7 | # https: false
8 | # )
9 | # end
10 |
--------------------------------------------------------------------------------
/config/initializers/cookies_serializer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # Specify a serializer for the signed and encrypted cookie jars.
6 | # Valid options are :json, :marshal, and :hybrid.
7 | Rails.application.config.action_dispatch.cookies_serializer = :json
8 |
--------------------------------------------------------------------------------
/config/initializers/status-page.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | redis_config = Rails.application.config_for(:redis)
4 |
5 | StatusPage.configure do
6 | use :cache
7 | use :redis, url: "redis://#{redis_config['host']}:#{redis_config['port']}/0"
8 | use :sidekiq
9 | use :database
10 |
11 | interval = 10
12 | end
13 |
--------------------------------------------------------------------------------
/app/uploaders/photo_uploader.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class PhotoUploader < BaseUploader
4 | # Override the filename of the uploaded files:
5 | def filename
6 | if super.present?
7 | @name ||= SecureRandom.uuid
8 | "#{Time.now.year}/#{@name}.#{file.extension.downcase}"
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/views/admin/topics/edit.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :sitemap do %>
2 | <%= t("admin.menu.topics")%> <%= t("common.edit")%>
3 | <% end %>
4 | <%= t("common.edit")%><%= t("admin.menu.topics") %>
5 |
6 | <%= render 'form' %>
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/views/topics/translation/_need_login_to_reply.zh-TW.html.erb:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/views/topics/create.js.erb:
--------------------------------------------------------------------------------
1 | <% if @topic.errors.empty? %>
2 | <% flash[:notice] = t('topics.create_topic_success') %>
3 | Turbolinks.visit('<%= topic_path(@topic.id) %>');
4 | <% else %>
5 | $('form[tb="edit-topic"] .alert').remove();
6 | $('form[tb="edit-topic"]').prepend('<%= j render("shared/error_messages", target: @topic) %>');
7 | <% end %>
8 |
--------------------------------------------------------------------------------
/app/views/topics/update.js.erb:
--------------------------------------------------------------------------------
1 | <% if @topic.errors.empty? %>
2 | <% flash[:notice] = t('topics.update_topic_success') %>
3 | Turbolinks.visit('<%= topic_path(@topic.id) %>');
4 | <% else %>
5 | $('form[tb="edit-topic"] .alert').remove();
6 | $('form[tb="edit-topic"]').prepend('<%= j render("shared/error_messages", target: @topic) %>');
7 | <% end %>
8 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # This file is used by Rack-based servers to start the application.
4 |
5 | require ::File.expand_path("../config/environment", __FILE__)
6 |
7 | # Action Cable uses EventMachine which requires that all classes are loaded in advance
8 | Rails.application.eager_load!
9 |
10 | run Rails.application
11 |
--------------------------------------------------------------------------------
/app/controllers/devices_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class DevicesController < ApplicationController
4 | before_action :authenticate_user!
5 |
6 | def destroy
7 | @device = current_user.devices.find(params[:id])
8 | @device.delete
9 | redirect_to oauth_applications_path, notice: "设备信息已删除"
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/helpers/teams_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module TeamsHelper
4 | def team_member_counts_tag(team)
5 | count_text = I18n.t("teams.team_users_count", count: team.team_users_count)
6 |
7 | content_tag(:i, nil, class: "fa fa-users") +
8 | content_tag(:span, count_text, class: "team-users-count")
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/bin/yarn:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_ROOT = File.expand_path('..', __dir__)
3 | Dir.chdir(APP_ROOT) do
4 | begin
5 | exec "yarnpkg", *ARGV
6 | rescue Errno::ENOENT
7 | $stderr.puts "Yarn executable was not detected in the system."
8 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install"
9 | exit 1
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/jobs/notify_reply_job_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rails_helper"
4 |
5 | describe NotifyReplyJob, type: :job do
6 | describe ".perform" do
7 | it "should work" do
8 | expect(Reply).to receive(:notify_reply_created).with(123).once
9 | NotifyReplyJob.perform_later(123)
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/spec/jobs/notify_topic_job_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rails_helper"
4 |
5 | describe NotifyTopicJob, type: :job do
6 | describe ".perform" do
7 | it "should work" do
8 | expect(Topic).to receive(:notify_topic_created).with(321).once
9 | NotifyTopicJob.perform_later(321)
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/views/errors/403.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :sitemap do %>404 <% end %>
2 |
3 |
4 |
>403 没有权限
5 |
6 | 抱歉,你没有做此操作的权限,如有问题,请到社区发帖询问。
7 |
8 |
9 | 返回首页
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/lib/assets/javascripts/google_analytics.js:
--------------------------------------------------------------------------------
1 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
2 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
3 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
4 | })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
5 |
--------------------------------------------------------------------------------
/spec/factories/access_tokens.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :access_token, class: Doorkeeper::AccessToken do
5 | sequence(:resource_owner_id) { |n| n }
6 | application
7 | expires_in 2.hours
8 |
9 | factory :clientless_access_token do
10 | application nil
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/models/user/blockable.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class User
4 | module Blockable
5 | extend ActiveSupport::Concern
6 |
7 | included do
8 | action_store :block, :user
9 | action_store :block, :node
10 | end
11 |
12 | def block_users?
13 | block_user_actions.first.present?
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/config/sidekiq.yml:
--------------------------------------------------------------------------------
1 | # Options here can still be overridden by cmd line args.
2 | # sidekiq -C sidekiq.yml
3 | ---
4 | :concurrency: 20
5 | :pidfile: tmp/pids/sidekiq.pid
6 | :logfile: log/sidekiq.log
7 | :queues:
8 | - [notifications, 100]
9 | - [http_request, 50]
10 | - [search_indexer, 30]
11 | - [mailer, 5]
12 | - [mailers, 5]
13 | - [default, 3]
14 |
--------------------------------------------------------------------------------
/app/channels/notifications_channel.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class NotificationsChannel < ApplicationCable::Channel
4 | def subscribed
5 | logger.info "current connections: #{ActionCable.server.connections.count}"
6 | if self.current_user_id
7 | stream_from "notifications_count/#{self.current_user_id}"
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/views/replies/create.js.erb:
--------------------------------------------------------------------------------
1 | <% if !@reply.errors.blank? %>
2 | _topicView.replyCallback(0, '<%= j(@msg) %>');
3 | <% else %>
4 | <% if @reply.upvote? %>
5 | Turbolinks.visit(location.href);
6 | <% else %>
7 | <%= render 'create_callback', reply: @reply %>
8 |
9 | _topicView.replyCallback(1, '<%= j(@msg) %>');
10 | <% end %>
11 | <% end %>
12 |
--------------------------------------------------------------------------------
/spec/factories/access_grants.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :access_grant, class: Doorkeeper::AccessGrant do
5 | sequence(:resource_owner_id) { |n| n }
6 | application
7 | expires_in 10.minutes
8 | redirect_uri "urn:ietf:wg:oauth:2.0:oob"
9 | sequence(:token) { |_n| SecureRandom.hex }
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/jobs/github_repo_fetcher_job_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rails_helper"
4 |
5 | describe GitHubRepoFetcherJob, type: :job do
6 | describe ".perform" do
7 | it "should work" do
8 | expect(User).to receive(:fetch_github_repositories).with(234).once
9 | GitHubRepoFetcherJob.perform_later(234)
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/views/topics/translation/_need_login_to_reply.zh-CN.html.erb:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/app/views/notifications/_node_changed.html.erb:
--------------------------------------------------------------------------------
1 | <%
2 | topic = notification.target
3 | node = notification.second_target
4 | %>
5 | <% if topic.blank? or node.blank? %>
6 | 相关信息已删除
7 | <% else %>
8 |
9 | 你发布的话题 <%= topic_title_tag(topic) %> 由于内容原因被管理员移动到了 <%= render_node_name(node.name, node.id) %> 节点,请注意查看节点说明。
10 |
11 | <% end %>
12 |
--------------------------------------------------------------------------------
/app/views/account/create.js.erb:
--------------------------------------------------------------------------------
1 | <% if resource.errors.empty? %>
2 | <% flash[:notice] = t('devise.sessions.signed_in') %>
3 | Turbolinks.visit('<%= topics_url %>');
4 | <% else %>
5 | $('#new_user .alert').remove();
6 | $('#new_user').prepend('<%= j render("shared/error_messages", target: resource) %>');
7 | $('.rucaptcha-image-box').html('<%= j rucaptcha_image_tag %>');
8 | <% end %>
9 |
--------------------------------------------------------------------------------
/app/views/notifications/mentions/_topic.html.erb:
--------------------------------------------------------------------------------
1 | <%
2 | topic = notification.target
3 | %>
4 |
5 | <% if topic.blank? %>
6 | 相关信息已删除
7 | <% else %>
8 |
9 | 在 <%= topic_title_tag(topic) %> 提及你:
10 |
11 |
12 | <%= topic.body_html %>
13 |
14 | <% end %>
15 |
--------------------------------------------------------------------------------
/spec/factories/applications.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :application, class: Doorkeeper::Application do
5 | sequence(:name) { |n| "name#{n}" }
6 | sequence(:uid) { |n| "uid#{n}" }
7 | sequence(:secret) { |n| "secret#{n}" }
8 | redirect_uri "http://foobar.com"
9 | association :owner, factory: :user
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/views/notifications/mentions/_reply.html.erb:
--------------------------------------------------------------------------------
1 | <%
2 | reply = notification.target
3 | topic = notification.second_target || reply.topic
4 | %>
5 |
6 | 在 <%= topic_title_tag(topic) %> 提及你:
7 |
8 | <% if reply.present? %>
9 |
10 | <%= reply.body_html %>
11 |
12 | <% end %>
13 |
--------------------------------------------------------------------------------
/lib/tasks/bundler_audit.rake:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | if Rails.env.development? || Rails.env.test?
4 | require "bundler/audit/cli"
5 |
6 | namespace :bundle do
7 | desc "Updates the ruby-advisory-db and runs audit"
8 | task :audit do
9 | %w[update check].each do |command|
10 | Bundler::Audit::CLI.start [command]
11 | end
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/db/migrate/20170217050010_create_exception_track_logs.rb:
--------------------------------------------------------------------------------
1 | # This migration comes from exception_track (originally 20170217023900)
2 | class CreateExceptionTrackLogs < ActiveRecord::Migration[5.0]
3 | def change
4 | create_table :exception_tracks do |t|
5 | t.string :title
6 | t.text :body
7 |
8 | t.timestamps
9 | end
10 |
11 | drop_table :exception_logs
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/config/initializers/kaminari_config.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Kaminari.configure do |config|
4 | config.default_per_page = 25
5 | # config.max_per_page = nil
6 | config.window = 4
7 | config.outer_window = 3
8 | # config.left = 0
9 | # config.right = 0
10 | # config.page_method_name = :page
11 | # config.param_name = :page
12 | # config.params_on_first_page = false
13 | end
14 |
--------------------------------------------------------------------------------
/db/migrate/20180614093642_change_topics_excellent_to_grade.rb:
--------------------------------------------------------------------------------
1 | class ChangeTopicsExcellentToGrade < ActiveRecord::Migration[5.2]
2 | def up
3 | rename_column :topics, :excellent, :grade
4 |
5 | # 61 is old nopoint node
6 | Topic.connection.execute("update topics set grade = -1 where node_id = 61")
7 | end
8 |
9 | def down
10 | rename_column :topics, :grade, :excellent
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/spec/factories/team_users.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :team_user do
5 | association :team
6 | association :user
7 | role :member
8 | status :accepted
9 | end
10 |
11 | factory :team_owner, parent: :team_user do
12 | role :owner
13 | end
14 |
15 | factory :team_member, parent: :team_user do
16 | role :member
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/app/views/doorkeeper/authorizations/show.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
由于应用没有配置回调地址,请使用授权码:
8 |
<%= params[:code] %>
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/views/team_users/edit.html.erb:
--------------------------------------------------------------------------------
1 | <% title_tag t("teams.edit_team_user") %>
2 |
3 | <%= render '/teams/header' %>
4 |
5 |
6 |
7 |
8 |
9 |
<%= t("teams.edit_team_user") %>
10 | <%= render '/team_users/form' %>
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/views/team_users/new.html.erb:
--------------------------------------------------------------------------------
1 | <% title_tag t("teams.new_team_user") %>
2 |
3 | <%= render '/teams/header' %>
4 |
5 |
6 |
7 |
8 |
9 |
<%= t("teams.new_team_user") %>
10 | <%= render '/team_users/form' %>
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/views/shared/_error_messages.html.erb:
--------------------------------------------------------------------------------
1 | <% if target.errors.any? %>
2 |
3 |
×
4 |
有 <%= target.errors.count %> 处问题导致无法提交:
5 |
6 | <% target.errors.full_messages.each do |msg| %>
7 | <%= msg %>
8 | <% end %>
9 |
10 |
11 | <% end %>
12 |
--------------------------------------------------------------------------------
/app/views/kaminari/_page.html.erb:
--------------------------------------------------------------------------------
1 | <% if page.current? %>
2 |
3 | <%= content_tag :a, page, remote: remote, rel: (page.next? ? 'next' : (page.prev? ? 'prev' : nil)), class: "page-link" %>
4 |
5 | <% else %>
6 |
7 | <%= link_to page, url, remote: remote, rel: (page.next? ? 'next' : (page.prev? ? 'prev' : nil)), class: "page-link" %>
8 |
9 | <% end %>
10 |
--------------------------------------------------------------------------------
/app/channels/application_cable/connection.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ApplicationCable
4 | class Connection < ActionCable::Connection::Base
5 | identified_by :current_user_id
6 |
7 | def connect
8 | self.current_user_id = find_verified_user_id
9 | end
10 |
11 | protected
12 |
13 | def find_verified_user_id
14 | cookies.signed[:user_id] || nil
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/views/devise/mailer/confirmation_instructions.html.erb:
--------------------------------------------------------------------------------
1 | 欢迎 <%= @resource.fullname %>
2 | 你已经成功注册 <%= link_to Setting.app_name, root_url %> 的账号,
3 | 接下来你需要点击下面的连接 激活 你的账号:
4 | <%= link_to '激活账号', confirmation_url(@resource, confirmation_token: @token), class: 'btn' %>
5 | 如果看不到链接,请复制下面的连接然后在浏览器里面打开:
6 | <%= confirmation_url(@resource, confirmation_token: @token) %>
7 |
--------------------------------------------------------------------------------
/app/views/devise/menu/_registration_items.html.erb:
--------------------------------------------------------------------------------
1 | <% if user_signed_in? %>
2 | 你好 <%= current_user.login %> ,
3 | <% if not params[:controller].match(/admin/) %>
4 | <%= link_to('设置', setting_path) %> |
5 | <% if admin? current_user %>后台 | <% end %>
6 | <% end %>
7 | <% else %>
8 | <%= link_to('加入社区', new_user_registration_path) %>
9 | <% end %>
10 |
--------------------------------------------------------------------------------
/lib/homeland/user_notification_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Homeland
4 | module UserNotificationHelper
5 | extend ActiveSupport::Concern
6 |
7 | included do
8 | helper_method :unread_notify_count
9 | end
10 |
11 | def unread_notify_count
12 | return 0 if current_user.blank?
13 | @unread_notify_count ||= Notification.unread_count(current_user)
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationRecord < ActiveRecord::Base
4 | self.abstract_class = true
5 |
6 | include Redis::Objects
7 |
8 | scope :recent, -> { order(id: :desc) }
9 | scope :exclude_ids, ->(ids) { where.not(id: ids.map(&:to_i)) }
10 | scope :by_week, -> { where("created_at > ?", 7.days.ago.utc) }
11 |
12 | delegate :url_helpers, to: "Rails.application.routes"
13 | end
14 |
--------------------------------------------------------------------------------
/app/controllers/comments_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class CommentsController < ApplicationController
4 | before_action :authenticate_user!
5 |
6 | def create
7 | @comment = Comment.new(comment_params)
8 | @comment.user = current_user
9 | @success = @comment.save
10 | end
11 |
12 | def comment_params
13 | params.require(:comment).permit(:commentable_type, :commentable_id, :body)
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/app/views/user_mailer/welcome.html.erb:
--------------------------------------------------------------------------------
1 | 你好 <%= @user.fullname %>
2 | <%= t("mail.you_have_successfully") %> <%= link_to Setting.app_name, root_url %> <%= t("mail.registered_an_account") %>
3 | <%= t("mail.welcome_title", app_name: Setting.app_name) %>
4 | <%= link_to '立即登录', new_user_session_url, class: 'btn btn-primary' %>
5 | <%= t("mail.login_from") %>:<%= link_to new_user_session_url, new_user_session_url %>
6 |
--------------------------------------------------------------------------------
/app/views/users/replies.html.erb:
--------------------------------------------------------------------------------
1 | <% title_tag [@user.fullname, t('users.menu.replies')].join(' · ') %>
2 |
3 |
4 | <%= render "sidebar", user: @user %>
5 |
6 | <%= render "menu" %>
7 |
8 | <%= render "replies", replies: @replies %>
9 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/db/migrate/20160819093756_add_lockable_to_devise.rb:
--------------------------------------------------------------------------------
1 | class AddLockableToDevise < ActiveRecord::Migration[5.0]
2 | def change
3 | add_column :users, :failed_attempts, :integer, default: 0, null: false # Only if lock strategy is :failed_attempts
4 | add_column :users, :unlock_token, :string # Only if unlock strategy is :email or :both
5 | add_column :users, :locked_at, :datetime
6 | add_index :users, :unlock_token, unique: true
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/spec/requests/users_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rails_helper"
4 |
5 | describe "GET /:login", type: :request do
6 | describe "login with complex cases" do
7 | it "should work" do
8 | %w[foo foo1 1234 foo-bar foo_bar foo_ foo.bar].each do |login|
9 | create(:user, login: login)
10 | get "/#{login}"
11 | assert_equal 200, response.status
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/app/views/comments/_comment.html.erb:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/app/views/search/_team.html.erb:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/db/migrate/20160219075601_create_devices.rb:
--------------------------------------------------------------------------------
1 | class CreateDevices < ActiveRecord::Migration[5.0]
2 | def change
3 | create_table :devices do |t|
4 | t.integer :platform, null: false
5 | t.integer :user_id, null: false
6 | t.string :token, null: false
7 | t.datetime :last_actived_at
8 |
9 | t.timestamps
10 | end
11 |
12 | add_index :devices, :user_id
13 | add_index :devices, [:user_id, :platform]
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/app/views/shared/_hot_locations.html.erb:
--------------------------------------------------------------------------------
1 | <% cache(["index-locations",Time.now.strftime("%Y-%m-%d")]) do %>
2 |
3 |
4 |
5 | <% Location.hot.limit(12).each do |item| %>
6 | <%= location_name_tag(item) %>
7 | <% end %>
8 |
9 |
10 | <% end %>
--------------------------------------------------------------------------------
/db/migrate/20161230094832_add_text_pattern_opts_to_users.rb:
--------------------------------------------------------------------------------
1 | class AddTextPatternOptsToUsers < ActiveRecord::Migration[5.0]
2 | def change
3 | # UPDATE users SET name = substring(name, 99) WHERE length(name) >= 100
4 | change_column :users, :login, :string, limit: 100
5 | change_column :users, :name, :string, limit: 100
6 | add_index :users, 'lower(login) varchar_pattern_ops'
7 | add_index :users, 'lower(name) varchar_pattern_ops'
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/views/shared/_comment.html.erb:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/app/views/search/_topic.html.erb:
--------------------------------------------------------------------------------
1 | <%
2 | title = hit.highlight.title.try(:first) || item.title
3 | %>
4 |
5 |
<%= link_to highlight(title), item %>
6 |
7 | <%= link_to polymorphic_url(item), item %>
8 | <%= item.created_at.to_date %>
9 |
10 |
<%= highlight(hit.highlight.body.try(:first)) %>
11 |
12 |
--------------------------------------------------------------------------------
/app/views/topics/_related_topics.html.erb:
--------------------------------------------------------------------------------
1 | <% cache([@topic, 'related_topic', Date.current]) do %>
2 | <% if @topic.related_topics.present? %>
3 |
4 |
5 |
6 | <% @topic.related_topics.each do |topic| %>
7 | <%= link_to topic.title, topic %>
8 | <% end %>
9 |
10 |
11 | <% end %>
12 | <% end %>
13 |
--------------------------------------------------------------------------------
/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | # Be sure to restart your server when you modify this file.
3 |
4 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
5 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
6 |
7 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
8 | # Rails.backtrace_cleaner.remove_silencers!
9 |
--------------------------------------------------------------------------------
/app/views/kaminari/_paginator.html.erb:
--------------------------------------------------------------------------------
1 | <%= paginator.render do -%>
2 |
13 | <% end -%>
14 |
--------------------------------------------------------------------------------
/app/views/shared/_navbar.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= render_list_items do |li|
3 | li << link_to(t("menu.topics"), Setting.has_module?(:home) ? main_app.topics_path : main_app.root_path, class: "nav-link")
4 |
5 | Homeland.navbar_plugins.each do |plugin|
6 | li << link_to(plugin.display_name, plugin.root_path, class: "nav-link") if Setting.has_module?(plugin.name)
7 | end
8 | end %>
9 | <%= raw Setting.navbar_html %>
10 |
11 |
--------------------------------------------------------------------------------
/app/views/admin/comments/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= form_for [:admin,@comment] do |f| %>
2 | <%= render "/shared/error_messages", target: @comment %>
3 |
4 | <%= f.label :body %>
5 | <%= f.text_area :body, class: "form-control" %>
6 |
7 |
8 |
9 | <%= f.submit "Save", class: "btn btn-primary", 'data-disable-with' => "Saving..." %> or
10 | <%= link_to 'Cancel', admin_comments_path, class: "btn" %>
11 |
12 | <% end %>
13 |
--------------------------------------------------------------------------------
/app/views/users/topics.html.erb:
--------------------------------------------------------------------------------
1 | <% title_tag [@user.fullname, t('users.menu.topics')].join(' · ') %>
2 |
3 |
4 | <%= render "sidebar", user: @user %>
5 |
6 | <%= render "menu" %>
7 |
8 |
9 | <%= render "topics", topics: @topics %>
10 |
11 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/db/migrate/20160912124102_add_team_users_count_to_users.rb:
--------------------------------------------------------------------------------
1 | class AddTeamUsersCountToUsers < ActiveRecord::Migration[5.0]
2 | def up
3 | add_column :users, :team_users_count, :integer
4 |
5 | User.reset_column_information
6 | say_with_time "Reset all teams' team_users counter cache" do
7 | Team.select(:id).find_each { |team| Team.reset_counters(team.id, :team_users) }
8 | end
9 | end
10 |
11 | def down
12 | remove_column :users, :team_users_count
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/app/views/users/followers.html.erb:
--------------------------------------------------------------------------------
1 | <% title_tag [@user.fullname, t('users.menu.followers')].join(' · ') %>
2 |
3 |
4 | <%= render "sidebar", user: @user %>
5 |
6 | <%= render "menu" %>
7 |
8 |
9 | <%= render "user_list", users: @users %>
10 |
11 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/Vagrantfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # -*- mode: ruby -*-
4 | # vi: set ft=ruby :
5 |
6 | Vagrant.configure(2) do |config|
7 | config.vm.box = "ubuntu/trusty64"
8 | config.vm.hostname = "homeland-dev"
9 | # For Android/iOS app dev
10 | config.vm.network "public_network"
11 | config.vm.provision "shell", path: "bin/provision.sh", privileged: false
12 |
13 | config.vm.provider "virtualbox" do |vb|
14 | vb.name = "homeland-dev"
15 | vb.memory = "2048"
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/views/notifications/_team_invite.html.erb:
--------------------------------------------------------------------------------
1 | <%
2 | team = notification.second_target
3 | team_user = notification.target
4 | %>
5 | <% if team.blank? || team_user.blank? %>
6 | 相关信息已删除
7 | <% else %>
8 |
9 | 邀请你加入 <%= user_name_tag(team) %>
10 |
11 |
12 | <%= link_to '查看邀请', main_app.user_team_user_path(team, team_user), class: 'btn btn-default btn-sm' %>
13 |
14 | <% end %>
15 |
--------------------------------------------------------------------------------
/db/migrate/20161221022846_create_user_sso.rb:
--------------------------------------------------------------------------------
1 | class CreateUserSSO < ActiveRecord::Migration[5.0]
2 | def change
3 | create_table :user_ssos do |t|
4 | t.integer :user_id, null: false
5 | t.string :uid, null: false, length: 255
6 | t.string :username
7 | t.string :email
8 | t.string :name
9 | t.string :avatar_url
10 | t.text :last_payload, null: false
11 | t.timestamps
12 | end
13 |
14 | add_index :user_ssos, :uid, unique: true
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/controllers/oauth/authorized_applications_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Oauth
4 | class AuthorizedApplicationsController < Doorkeeper::ApplicationController
5 | before_action :authenticate_resource_owner!
6 |
7 | def destroy
8 | Doorkeeper::AccessToken.revoke_all_for params[:id].to_i, current_resource_owner
9 | redirect_to oauth_applications_url, notice: I18n.t(:notice, scope: %i[doorkeeper flash authorized_applications destroy])
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/config/locales/carrierwave.zh-CN.yml:
--------------------------------------------------------------------------------
1 | "zh-CN":
2 | errors:
3 | messages:
4 | carrierwave_processing_error: 处理错误
5 | carrierwave_integrity_error: 格式不是可上载的类型
6 | extension_white_list_error: "不可上载 %{extension} 档案,可以上载的档案类型为:%{allowed_types}"
7 | rmagick_processing_error: "用 rmagick 处理影像档时发生错误,也许上载的不是影像档?原错误讯息:%{e}"
8 | mime_types_processing_error: "处理档案 MIME::Types 时发生错误,也许上载的档案不是正确的 content-type ?原错误:%{e}"
9 | mini_magick_processing_error: "使用 MiniMagick 处理影像时发生错误,也许上载的不是影像档?原错误:%{e}"
10 |
--------------------------------------------------------------------------------
/config/locales/carrierwave.zh-TW.yml:
--------------------------------------------------------------------------------
1 | "zh-TW":
2 | errors:
3 | messages:
4 | carrierwave_processing_error: 處理錯誤
5 | carrierwave_integrity_error: 格式不是可上載的類型
6 | extension_white_list_error: "不可上載 %{extension} 檔案,可以上載的檔案類型為:%{allowed_types}"
7 | rmagick_processing_error: "用 rmagick 處理影像檔時發生錯誤,也許上載的不是影像檔?原錯誤訊息:%{e}"
8 | mime_types_processing_error: "處理檔案 MIME::Types 時發生錯誤,也許上載的檔案不是正確的 content-type ?原錯誤:%{e}"
9 | mini_magick_processing_error: "使用 MiniMagick 處理影像時發生錯誤,也許上載的不是影像檔?原錯誤:%{e}"
10 |
--------------------------------------------------------------------------------
/app/controllers/admin/application_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Admin
4 | class ApplicationController < ::ApplicationController
5 | layout "admin"
6 |
7 | before_action :authenticate_user!
8 | before_action :require_admin
9 | before_action :set_active_menu
10 |
11 | def require_admin
12 | render_404 unless current_user.admin?
13 | end
14 |
15 | def set_active_menu
16 | @current = ["/" + ["admin", controller_name].join("/")]
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/app/views/admin/photos/show.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :sitemap do %>
2 | <%= t("admin.menu.photos")%> <%= t("admin.check_photo")%>
3 | <% end %>
4 | <%= notice %>
5 |
6 |
7 | User:
8 | <%= @photo.user.login %>
9 |
10 |
11 | <%= image_tag(@photo.image.url) %>
12 |
13 |
14 |
15 | <%= link_to 'Edit', edit_admin_photo_path(@photo) %> |
16 | <%= link_to 'Back', admin_photos_path %>
17 |
--------------------------------------------------------------------------------
/app/views/devise/mailer/reset_password_instructions.html.erb:
--------------------------------------------------------------------------------
1 | 你好 <%= @resource.fullname %>
2 | 有人请求找回在 <%= link_to Setting.app_name, root_url %> 上面,账号为 <%= @resource.login %> 的密码,如果你是,那么你可以通过下面的连接进入网站修改密码。
3 | <%= link_to "修改我的密码", edit_password_url(@resource, reset_password_token: @token), class: 'btn' %>
4 | 如果看不到连接,请复制下面的连接然后在浏览器里面打开:
5 | <%= edit_password_url(@resource, reset_password_token: @token) %>
6 | 如果你没有申请,可以忽略这封邮件,你的账号不会受到影响。
7 |
--------------------------------------------------------------------------------
/app/views/notifications/_topic_reply.html.erb:
--------------------------------------------------------------------------------
1 | <% reply = notification.target %>
2 | <% if reply.blank? %>
3 | 相关信息已删除
4 | <% else %>
5 | <%
6 | topic = notification.second_target || reply.topic
7 | %>
8 |
9 |
10 | 在帖子
11 | <%= topic_title_tag(topic, reply: reply) %> 回复了:
12 |
13 | <%- if reply.present? -%>
14 |
15 | <%= reply.body_html %>
16 |
17 | <%- end -%>
18 | <% end %>
19 |
--------------------------------------------------------------------------------
/app/views/teams/index.html.erb:
--------------------------------------------------------------------------------
1 | <% title_tag t('menu.teams')%>
2 |
3 |
4 |
5 |
9 |
10 | <%= render "team_list", teams: @active_teams %>
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/controllers/nodes_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class NodesController < ApplicationController
4 | before_action :authenticate_user!, only: %i[block unblock]
5 |
6 | def index
7 | @nodes = Node.all
8 | render json: @nodes, only: [:name], methods: [:id]
9 | end
10 |
11 | def block
12 | current_user.block_node(params[:id])
13 | render json: { code: 0 }
14 | end
15 |
16 | def unblock
17 | current_user.unblock_node(params[:id])
18 | render json: { code: 0 }
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/app/models/device.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Device < ApplicationRecord
4 | belongs_to :user
5 |
6 | enum platform: %i[ios android]
7 |
8 | validates :platform, :token, presence: true
9 | validates :token, uniqueness: { scope: %i[user_id platform] }
10 |
11 | def alive?
12 | return true if last_actived_at.blank?
13 | (Date.current - last_actived_at.to_date).to_i <= 14
14 | end
15 |
16 | def platform_name
17 | @platform_name ||= I18n.t "device_platform.#{self.platform}"
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/app/views/users/index.html.erb:
--------------------------------------------------------------------------------
1 | <% title_tag t('users.hot_users') %>
2 |
3 |
4 |
5 |
9 |
10 | <%= render "user_list", users: @active_users, columns: 4 %>
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/lib/templates/erb/scaffold/show.html.erb:
--------------------------------------------------------------------------------
1 | <%%= render 'base' %>
2 |
3 |
4 | <%%= link_to '修改', edit_<%= singular_table_name %>_path(@<%= file_name %>), class: "btn btn-sm btn-success" %>
5 | <%%= link_to '返回', <%= index_helper %>_path, class: "btn btn-sm btn-default" %>
6 |
7 |
8 |
9 | <% for attribute in attributes -%>
10 |
11 | <%= attribute.human_name %>:
12 | <%%= @<%= file_name %>.<%= attribute.name %> %>
13 |
14 | <% end -%>
15 |
--------------------------------------------------------------------------------
/public/api-doc/frames.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Ruby China API 文档
6 |
7 |
13 |
14 | Oops!
15 | YARD requires JavaScript!
16 |
17 |
18 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # NAME: homeland/homeland
2 | FROM homeland/base:1.0.1
3 | MAINTAINER Jason Lee "https://github.com/huacnlee"
4 |
5 | ENV RAILS_ENV 'production'
6 |
7 | ENV HOMELAND_VERSION 'master'
8 |
9 | RUN useradd ruby -s /bin/bash -m -U &&\
10 | mkdir -p /var/www &&\
11 | cd /var/www
12 | ADD . /var/www/homeland
13 | RUN cd /var/www/homeland && bundle install --deployment &&\
14 | find /var/www/homeland/vendor/bundle -name tmp -type d -exec rm -rf {} + &&\
15 | chown -R ruby:ruby /var/www
16 |
17 | WORKDIR /var/www/homeland
18 |
--------------------------------------------------------------------------------
/Benchmarks.md:
--------------------------------------------------------------------------------
1 | # 页面响应时间记录
2 |
3 | 要点,所有统计都得基于有 cache 的情况,生产环境。
4 |
5 | ## 2015-5-28
6 |
7 | - / = 36ms
8 | - /topics = 45ms
9 | - /topics/19436 = 67ms
10 | - /wiki = 26ms
11 | - /notifications = 39ms
12 | - /huacnlee = 100ms
13 | - /huacnlee/topics = 95ms
14 | - /huacnlee/favorites = 50ms
15 | - /huacnlee/followers = 85ms
16 | - /wiki/about = 37ms
17 | - /jobs = 68ms
18 | - /topics/new = 84ms
19 | - /sites = 25ms
20 | - /account/edit = 73ms
--------------------------------------------------------------------------------
/app/controllers/admin/photos_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Admin
4 | class PhotosController < Admin::ApplicationController
5 | before_action :set_photo, only: %i[show destroy]
6 |
7 | def index
8 | @photos = Photo.recent.includes(:user).page(params[:page])
9 | end
10 |
11 | def destroy
12 | @photo.destroy
13 | redirect_to(admin_photos_url)
14 | end
15 |
16 | private
17 |
18 | def set_photo
19 | @photo = Photo.find(params[:id])
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/db/migrate/20160707084438_create_team_users_join_table.rb:
--------------------------------------------------------------------------------
1 | class CreateTeamUsersJoinTable < ActiveRecord::Migration[5.0]
2 | def change
3 | add_column :users, :type, :string, limit: 20, after: :id
4 |
5 | create_table(:team_users) do |t|
6 | t.integer :team_id, index: true, null: false
7 | t.integer :user_id, index: true, null: false
8 | t.integer :role
9 | t.integer :status
10 |
11 | t.timestamps null: false
12 | end
13 |
14 | add_index :team_users, [:team_id, :user_id], unique: true
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/views/teams/_team_list.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <% teams.each do |team| %>
3 |
14 | <% end %>
15 |
16 |
--------------------------------------------------------------------------------
/spec/models/notification_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rails_helper"
4 |
5 | describe Notification, type: :model do
6 | let(:n) { create(:notification_topic_reply) }
7 |
8 | describe ".realtime_push_to_client" do
9 | it "should work" do
10 | hash = {}
11 | hash[:count] = n.user.notifications.unread.count
12 | args = ["notifications_count/#{n.user_id}", hash]
13 | expect(ActionCable.server).to receive(:broadcast).with(*args).once
14 | n.realtime_push_to_client
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/views/home/markdown.html.erb:
--------------------------------------------------------------------------------
1 | <% title_tag 'Markdown 教程' %>
2 |
3 | <% cache(['markdown-guide', Digest::MD5.hexdigest(Homeland::Markdown.example)]) do %>
4 |
5 |
6 |
7 |
Markdown 教程
8 |
9 |
10 |
<%= Homeland::Markdown.example %>
11 |
12 |
13 | <%= markdown(Homeland::Markdown.example)%>
14 |
15 |
16 |
17 |
18 | <% end %>
19 |
--------------------------------------------------------------------------------
/app/views/users/_user_list.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <% users.each do |user| %>
3 |
14 | <% end %>
15 |
16 |
--------------------------------------------------------------------------------
/app/models/user/followable.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class User
4 | module Followable
5 | extend ActiveSupport::Concern
6 |
7 | included do
8 | action_store :follow, :topic
9 | action_store :follow, :user, counter_cache: "followers_count",
10 | user_counter_cache: "following_count"
11 | end
12 |
13 | def follow_user(user)
14 | return unless user
15 | self.create_action(:follow, target: user)
16 | Notification.notify_follow(user.id, self.id)
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/spec/helpers/locations_helper_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rails_helper"
4 |
5 | describe LocationsHelper, type: :helper do
6 | it "should location_name_tag work with string" do
7 | expect(helper.location_name_tag("chengdu")).to eq(link_to("chengdu", location_users_path("chengdu")))
8 | end
9 |
10 | it "should location_name_tag work with Location instance" do
11 | location = create(:location)
12 | expect(helper.location_name_tag(location)).to eq(link_to(location.name, location_users_path(location.name)))
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/app/views/teams/_sidebar.html.erb:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/app/views/admin/sections/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= form_for [:admin, @section] do |f| %>
2 | <%= render 'shared/error_messages', target: @section %>
3 |
4 | <%= f.label :name %>
5 | <%= f.text_field :name, class: "form-control" %>
6 |
7 |
8 |
9 | <%= f.label :name %>
10 | <%= f.text_field :sort, class: "form-control" %>
11 |
12 |
13 |
14 | <%= f.submit t("common.save"), class: "btn btn-primary", 'data-disable-with' => t("common.saving") %>
15 |
16 | <% end %>
17 |
--------------------------------------------------------------------------------
/db/migrate/20160119191013_add_indexes_to_table.rb:
--------------------------------------------------------------------------------
1 | class AddIndexesToTable < ActiveRecord::Migration[4.2]
2 | def change
3 | add_index :topics, :deleted_at
4 | add_index :topics, [:node_id, :deleted_at]
5 |
6 | add_index :replies, :deleted_at
7 | add_index :replies, [:topic_id, :deleted_at]
8 |
9 | add_index :sites, :deleted_at
10 | add_index :sites, [:site_node_id, :deleted_at]
11 |
12 | remove_index :users, :location
13 | add_index :users, :location
14 |
15 | add_index :nodes, :sort
16 |
17 | add_index :photos, :user_id
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/homeland/pipeline/twemoji_filter.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Homeland
4 | class Pipeline
5 | class TwemojiFilter < HTML::Pipeline::Filter
6 | def call
7 | doc.xpath(".//text()").each do |node|
8 | content = node.to_html
9 | next unless content.include?(":")
10 | next if has_ancestor?(node, %w[pre code])
11 |
12 | html = Twemoji.parse(content)
13 |
14 | next if html == content
15 | node.replace(html)
16 | end
17 | doc
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/app/controllers/photos_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class PhotosController < ApplicationController
4 | load_and_authorize_resource
5 |
6 | def create
7 | # 浮动窗口上传
8 | @photo = Photo.new
9 | @photo.image = params[:file]
10 | if @photo.image.blank?
11 | render json: { ok: false }, status: 400
12 | return
13 | end
14 |
15 | @photo.user_id = current_user.id
16 | if @photo.save
17 | render json: { ok: true, url: @photo.image.url(:large) }
18 | else
19 | render json: { ok: false }
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/app/models/concerns/soft_delete.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SoftDelete
4 | extend ActiveSupport::Concern
5 |
6 | included do
7 | default_scope -> { where(deleted_at: nil) }
8 |
9 | alias_method :destroy!, :destroy
10 | end
11 |
12 | def destroy
13 | run_callbacks(:destroy) do
14 | if persisted?
15 | t = Time.now.utc
16 | update_columns(deleted_at: t, updated_at: t)
17 | end
18 |
19 | @destroyed = true
20 | end
21 | freeze
22 | end
23 |
24 | def deleted?
25 | deleted_at.present?
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/bin/spring:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | # This file loads spring without using Bundler, in order to be fast.
5 | # It gets overwritten when you run the `spring binstub` command.
6 |
7 | unless defined?(Spring)
8 | require "rubygems"
9 | require "bundler"
10 |
11 | if (match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m))
12 | Gem.paths = { "GEM_PATH" => [Bundler.bundle_path.to_s, *Gem.path].uniq.join(Gem.path_separator) }
13 | gem "spring", match[1]
14 | require "spring/binstub"
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/views/api/v3/application/_node.json.jbuilder:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @class NodeSerializer
4 | # 节点
5 | #
6 | # == attributes
7 | # - *id* [Integer] 编号
8 | # - *name* [String] 节点名称
9 | # - *summary* [String] 简介, Markdown 格式
10 | # - *section_id* [Integer] 大类别编号
11 | # - *section_name* [String] 大类别名称
12 | # - *topics_count* [Integer] 话题数量
13 | # - *sort* {Integer} 排序优先级
14 | # - *updated_at* [DateTime] 更新时间
15 | if node
16 | json.cache! ["v1", node] do
17 | json.(node, :id, :name, :topics_count, :summary, :section_id, :sort, :section_name, :updated_at)
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/app/views/users/blocked.html.erb:
--------------------------------------------------------------------------------
1 | <% title_tag [@user.fullname, t('users.menu.blocked')].join(' · ') %>
2 |
3 |
4 | <%= render "sidebar", user: @user %>
5 |
6 | <%= render "menu" %>
7 |
8 |
9 |
10 | <%= render "user_list", users: @block_users %>
11 |
12 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/spec/controllers/admin/home_controller_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rails_helper"
4 |
5 | describe Admin::HomeController, type: :controller do
6 | let(:user) { create :user }
7 | let(:admin) { create :admin }
8 | describe "Admin requirement" do
9 | it "should open with admin user" do
10 | sign_in admin
11 | get :index
12 | expect(response.status).to eq 200
13 | end
14 |
15 | it "should 404 with non admin user" do
16 | sign_in user
17 | get :index
18 | expect(response.status).to eq 404
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/app/models/concerns/closeable.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # 开启关闭帖子功能
4 | module Closeable
5 | extend ActiveSupport::Concern
6 |
7 | included do
8 | end
9 |
10 | def closed?
11 | closed_at.present?
12 | end
13 |
14 | def close!
15 | transaction do
16 | Reply.create_system_event!(action: "close", topic_id: self.id)
17 | update!(closed_at: Time.now)
18 | end
19 | end
20 |
21 | def open!
22 | transaction do
23 | update!(closed_at: nil)
24 | Reply.create_system_event!(action: "reopen", topic_id: self.id)
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/app/views/users/_replies.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <% replies.each do |reply| %>
3 | <% cache(['users', reply]) do %>
4 | <% next if reply.topic.blank? %>
5 |
6 |
7 | <%= link_to(reply.topic.title, topic_path(reply.topic_id)) %>
8 | at <%= timeago(reply.created_at) %>
9 |
10 |
11 | <%= reply.body_html %>
12 |
13 |
14 | <% end %>
15 | <% end %>
16 |
17 |
--------------------------------------------------------------------------------
/app/views/team_users/index.html.erb:
--------------------------------------------------------------------------------
1 | <% title_tag t('teams.users') %>
2 |
3 | <%= render '/teams/header' %>
4 |
5 |
6 | <% if can? :update, @team %>
7 |
8 | <% end %>
9 |
10 |
11 | <%= render partial: '/team_users/team_user', collection: @team_users %>
12 |
13 |
14 |
17 |
18 |
--------------------------------------------------------------------------------
/app/views/topics/_node_selector.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 | <%= render '/shared/index_sections' %>
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/config/database.yml.default:
--------------------------------------------------------------------------------
1 | default: &default
2 | adapter: postgresql
3 | pool: 64
4 | timeout: 5000
5 | encoding: utf-8
6 |
7 | #
8 | # PRODUCTION
9 | #
10 | production:
11 | <<: *default
12 | database: homeland
13 |
14 | #
15 | # Development specific
16 | #
17 | development:
18 | <<: *default
19 | database: homeland-dev
20 |
21 | # Warning: The database defined as "test" will be erased and
22 | # re-generated from your development database when you run "rake".
23 | # Do not set this db to the same as development or production.
24 | test:
25 | <<: *default
26 | database: homeland-test
27 |
--------------------------------------------------------------------------------
/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # ============ init Section, Node ================
2 | s1 = Section.create(name: 'Share')
3 | Node.create(name: 'Fun', summary: '...', section_id: s1.id)
4 | Node.create(name: 'Movie', summary: '...', section_id: s1.id)
5 | Node.create(name: 'Music', summary: '...', section_id: s1.id)
6 | s2 = Section.create(name: 'Geek')
7 | Node.create(name: 'Apple', summary: '...', section_id: s2.id)
8 | Node.create(name: 'Google', summary: '...', section_id: s2.id)
9 | Node.create(name: 'Coding', summary: '...', section_id: s2.id)
10 | Node.create(name: 'PlayStation / XBox', summary: '...', section_id: s2.id)
11 |
--------------------------------------------------------------------------------
/spec/models/team_user_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rails_helper"
4 |
5 | describe TeamUser, type: :model do
6 | let(:user) { create(:user) }
7 |
8 | describe "Create via login" do
9 | it "should work" do
10 | team_user = build(:team_user, login: user.login)
11 | expect(team_user.save).to eq true
12 | expect(team_user.user_id).to eq user.id
13 | end
14 |
15 | it "should add error when user not exists" do
16 | team_user = build(:team_user, login: "lasdjgalksdjgsad")
17 | expect(team_user.valid?).to eq false
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/app/models/reply/voteable.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Reply
4 | module Voteable
5 | extend ActiveSupport::Concern
6 |
7 | UPVOTES = %w[+1 :+1: :thumbsup: :plus1: 👍 👍🏻 👍🏼 👍🏽 👍🏾 👍🏿]
8 |
9 | included do
10 | after_commit :check_vote_chars_for_like_topic, on: :create, unless: -> { system_event? }
11 | end
12 |
13 | def upvote?
14 | (body || "").strip.start_with?(*UPVOTES)
15 | end
16 |
17 | private
18 | def check_vote_chars_for_like_topic
19 | return unless upvote?
20 | user.like(topic)
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # This file contains settings for ActionController::ParamsWrapper which
6 | # is enabled by default.
7 |
8 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
9 | ActiveSupport.on_load(:action_controller) do
10 | wrap_parameters format: [:json]
11 | end
12 |
13 | # To enable root element in JSON for ActiveRecord objects.
14 | # ActiveSupport.on_load(:active_record) do
15 | # self.include_root_in_json = true
16 | # end
17 |
--------------------------------------------------------------------------------
/db/migrate/20170207141208_remove_array_action_fields.rb:
--------------------------------------------------------------------------------
1 | class RemoveArrayActionFields < ActiveRecord::Migration[5.0]
2 | def change
3 | # Remove old Array fields, they were instead by ActionStore
4 | # ref: https://github.com/ruby-china/homeland/pull/857
5 | remove_column :users, :following_ids
6 | remove_column :users, :blocked_user_ids
7 | remove_column :users, :blocked_node_ids
8 | remove_column :users, :favorite_topic_ids
9 |
10 | remove_column :topics, :liked_user_ids
11 | remove_column :topics, :follower_ids
12 |
13 | remove_column :replies, :liked_user_ids
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/app/views/search/_page.html.erb:
--------------------------------------------------------------------------------
1 | <% if Setting.has_module?(:wiki) %>
2 | <%
3 | title = hit.highlight.title.try(:first) || item.title
4 | %>
5 |
6 |
<%= link_to highlight(title), homeland_wiki.page_path(item) %> Wiki
7 |
8 | <%= link_to homeland_wiki.page_url(item), homeland_wiki.page_path(item) %>
9 | <%= item.updated_at.to_date %>
10 |
11 |
<%= highlight(hit.highlight.body.try(:first)) %>
12 |
13 | <% end %>
14 |
--------------------------------------------------------------------------------
/app/views/users/city.html.erb:
--------------------------------------------------------------------------------
1 | <% title_tag params[:id] %>
2 |
3 |
4 |
5 |
6 |
7 | <% @users.each do |item| %>
8 |
9 |
<%= user_avatar_tag(item, :md) %>
10 |
<%= user_name_tag(item) %>
11 |
12 | <% end %>
13 |
14 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/views/comments/create.js.erb:
--------------------------------------------------------------------------------
1 | App.loading(false);
2 | comments_box = $("#<%= @comment.commentable_type %>_<%= @comment.commentable_id %>_cell_comments .card-body");
3 | comments_form = $("#<%= @comment.commentable_type %>_<%= @comment.commentable_id %>_cell_new_comment");
4 | <% if @success %>
5 | comment_line = '<%= j(render("comment", item: @comment)) %>';
6 | comments_box.append(comment_line);
7 | $(comment_line).fadeOut("fast");
8 | comments_box.find('.no-result').remove();
9 | $("abbr.timeago").timeago();
10 | $("textarea",comments_form).val("").focus();
11 | <% else %>
12 | $("textarea",comments_form).focus();
13 | <% end %>
14 |
--------------------------------------------------------------------------------
/config/puma.example.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | app_root = "/var/www/homeland"
4 | pidfile "#{app_root}/tmp/pids/puma.pid"
5 | state_path "#{app_root}/tmp/pids/puma.state"
6 | stdout_redirect "#{app_root}/log/puma.stdout.log", "#{app_root}/log/puma.stderr.log", true
7 | bind "unix:/tmp/homeland.puma.sock"
8 | daemonize true
9 | port 7000
10 | workers 4
11 | threads 8, 16
12 | preload_app!
13 |
14 | on_worker_boot do
15 | ActiveSupport.on_load(:active_record) do
16 | ActiveRecord::Base.establish_connection
17 | end
18 | end
19 |
20 | before_fork do
21 | ActiveRecord::Base.connection_pool.disconnect!
22 | end
23 |
--------------------------------------------------------------------------------
/app/controllers/api/v3/nodes_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api
4 | module V3
5 | class NodesController < Api::V3::ApplicationController
6 | # 获取 Nodes 列表
7 | #
8 | # GET /api/v3/nodes
9 | # @return [Array]
10 | def index
11 | @nodes = Node.includes(:section).all
12 | @meta = { total: Node.count }
13 | end
14 |
15 | ##
16 | # 获取单个 Node 详情
17 | #
18 | # GET /api/v3/nodes/:id
19 | # @return [NodeSerializer]
20 | def show
21 | @node = Node.find(params[:id])
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/app/views/admin/replies/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= form_for [:admin,@reply] do |f| %>
2 | <%= render 'shared/error_messages', target: @reply %>
3 |
4 |
5 | <%= f.label :body %>
6 | <%= f.text_area :body, class: "form-control" %>
7 |
8 |
9 |
10 | <%= f.label :user_id %>
11 | <%= f.text_field :user_id, class: "form-control" %>
12 |
13 |
14 |
15 | <%= f.submit t("common.save"), class: "btn btn-primary", 'data-disable-with' => t("common.saving") %>
16 | or
17 | <%= link_to 'Back', admin_replies_path %>
18 |
19 | <% end %>
20 |
--------------------------------------------------------------------------------
/app/views/devise/confirmations/new.html.erb:
--------------------------------------------------------------------------------
1 | <% title_tag 'Resend confirmation instructions' %>
2 |
3 | Resend confirmation instructions
4 |
5 | <%= form_for resource, as: resource_name, url: confirmation_path(resource_name), method: :post do |f| %>
6 | <%= f.error_notification %>
7 | <%= f.full_error :confirmation_token %>
8 |
9 |
10 | <%= f.label :email %>
11 | <%= f.text_field :email, class: "form-control" %>
12 |
13 |
14 |
15 | <%= f.button :submit, "Resend confirmation instructions" %>
16 |
17 | <% end %>
18 |
19 | <%= render "devise/shared/links" %>
20 |
--------------------------------------------------------------------------------
/config/initializers/exception-track.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "exception_notification/sidekiq"
4 |
5 | ExceptionTrack.configure do
6 | # self.environments = %i(production)
7 | end
8 |
9 | ExceptionNotification.configure do |config|
10 | config.ignored_exceptions += %w[
11 | ActionView::TemplateError
12 | ActionController::InvalidAuthenticityToken
13 | ActionController::BadRequest
14 | ActionView::MissingTemplate
15 | ActionController::UrlGenerationError
16 | ActionController::UnknownFormat
17 | ActionController::InvalidCrossOriginRequest
18 | ActionController::ParameterMissing
19 | ]
20 | end
21 |
--------------------------------------------------------------------------------
/config/initializers/redis.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "redis"
4 | require "redis-namespace"
5 | require "redis/objects"
6 |
7 | redis_config = Rails.application.config_for(:redis)
8 |
9 | $redis = Redis.new(host: redis_config["host"], port: redis_config["port"])
10 | $redis.select(0)
11 | Redis::Objects.redis = $redis
12 |
13 | sidekiq_url = "redis://#{redis_config['host']}:#{redis_config['port']}/0"
14 | Sidekiq.configure_server do |config|
15 | config.redis = { namespace: "sidekiq", url: sidekiq_url }
16 | end
17 | Sidekiq.configure_client do |config|
18 | config.redis = { namespace: "sidekiq", url: sidekiq_url }
19 | end
20 |
--------------------------------------------------------------------------------
/app/views/team_users/_team_user.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= user_avatar_tag(team_user.user, :sm, link: false) %>
3 |
4 | <%= user_name_tag(team_user.user) %>
5 | <%= team_user&.user&.name %>
6 |
7 |
8 | <%= team_user.pendding? ? team_user.status_name : team_user.role_name %>
9 |
10 |
11 | <% if can?(:update, @team) && team_user.user_id != current_user&.id %>
12 | <%= link_to icon_tag('pencil'), edit_user_team_user_path(@team, team_user) %>
13 | <% end %>
14 |
15 |
16 |
--------------------------------------------------------------------------------
/config/locales/carrierwave.en.yml:
--------------------------------------------------------------------------------
1 | "en":
2 | errors:
3 | messages:
4 | carrierwave_processing_error: failed to be processed
5 | carrierwave_integrity_error: is not of an allowed file type
6 | extension_white_list_error: "You are not allowed to upload %{extension} files, allowed types: %{allowed_types}"
7 | rmagick_processing_error: "Failed to manipulate with rmagick, maybe it is not an image?"
8 | mime_types_processing_error: "Failed to process file with MIME::Types, maybe not valid content-type? Original Error: %{e}"
9 | mini_magick_processing_error: "Failed to manipulate with MiniMagick, maybe it is not an image? Original Error: %{e}"
--------------------------------------------------------------------------------
/app/views/api/v3/application/_abilities.json.jbuilder:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # @class BaseSerializer
4 | # @!method abilities
5 | # 当前 accessToken 对应的用户对此数据的权限
6 | #
7 | # @example 表示可修改,不可删除
8 | #
9 | # { update: true, destroy: false }
10 | #
11 | # @return update [Boolean] 当前 accessToken 是否有修改权限
12 | # @return destroy [Boolean] 当前 accessToken 是否有删除权限
13 | json.abilities do
14 | json.update can?(:update, object)
15 | json.destroy can?(:destroy, object)
16 | if object && object.is_a?(Topic)
17 | %i[ban normal excellent unexcellent close open].each do |action|
18 | json.set! action, can?(action, object)
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/app/views/teams/show.html.erb:
--------------------------------------------------------------------------------
1 | <%= render '/teams/header' %>
2 |
3 |
4 |
5 |
6 |
7 | <% if @topics.blank? %>
8 |
9 | <% else %>
10 | <%= render partial: '/topics/topic', collection: @topics, locals: { suggest: false }, cached: -> (topic) { [topic, 'normal'] } %>
11 | <% end %>
12 |
13 |
16 |
17 |
18 |
19 | <%= render '/teams/sidebar' %>
20 |
21 |
--------------------------------------------------------------------------------
/app/views/admin/locations/_form.html.erb:
--------------------------------------------------------------------------------
1 |
20 |
--------------------------------------------------------------------------------
/app/views/admin/site_configs/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= form_for @site_config, url: admin_site_config_path(@site_config.var), method: 'patch' do |f| %>
2 |
7 |
8 |
12 | <% end %>
13 |
--------------------------------------------------------------------------------
/app/controllers/api/v3/photos_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api
4 | module V3
5 | class PhotosController < Api::V3::ApplicationController
6 | before_action :doorkeeper_authorize!
7 |
8 | # 上传图片,请使用 Multipart 的方式提交图片文件
9 | #
10 | # POST /api/v3/photos
11 | #
12 | # @param file - 文件信息, [required]
13 | #
14 | # == returns
15 | # - image_url 图片 URL
16 | def create
17 | requires! :file
18 |
19 | @photo = Photo.new
20 | @photo.image = params[:file]
21 | @photo.user_id = current_user.id
22 | @photo.save!
23 | end
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/app/controllers/users/team_actions.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Users
4 | module TeamActions
5 | extend ActiveSupport::Concern
6 |
7 | included do
8 | before_action :set_team, only: [:show]
9 | end
10 |
11 | private
12 |
13 | def team_show
14 | @topics = Topic.where(user_id: @team.user_ids, team_id: [nil, @team.id])
15 | .fields_for_list
16 | .last_actived.includes(:user)
17 | @topics = @topics.page(params[:page])
18 | end
19 |
20 | def only_team!
21 | render_404 if @user_type != :team
22 | end
23 |
24 | def set_team
25 | @team = @user
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/config/locales/teams.zh-CN.yml:
--------------------------------------------------------------------------------
1 | 'zh-CN':
2 | team_user_status:
3 | pendding: '等待接受'
4 | acceped: '已加入'
5 | team_user_role:
6 | owner: '组管理员'
7 | member: '成员'
8 | teams:
9 | teams: '组织'
10 | user_existed: '已经在组里面了'
11 | new_team: '创建公司/组织'
12 | edit_team: '设置公司/组织资料'
13 | new_team_user: '邀请成员'
14 | edit_team_user: '修改成员权限'
15 | delete_team_user: '移除成员'
16 | show_team_user: '查看邀请'
17 | accept_team_user: '接受邀请'
18 | reject_team_user: '拒绝'
19 | delete_team_user_confirm: '确定要将他从组里面移除掉么?'
20 | topics: '成员的话题列表'
21 | users: '成员列表'
22 | team_user_pendding: '等待接受'
23 | settings: '设置'
24 | team_users_count: "%{count} 个团队成员"
25 |
--------------------------------------------------------------------------------
/config/locales/teams.zh-TW.yml:
--------------------------------------------------------------------------------
1 | 'zh-TW':
2 | team_user_status:
3 | pendding: '等待接受'
4 | acceped: '已加入'
5 | team_user_role:
6 | owner: '團體管理員'
7 | member: '成員'
8 | teams:
9 | teams: '團體'
10 | user_existed: '已經該團體成員'
11 | new_team: '創建公司/團體'
12 | edit_team: '設置公司/團體资料'
13 | new_team_user: '邀請成員'
14 | edit_team_user: '修改成員权限'
15 | delete_team_user: '移除成員'
16 | show_team_user: '查看邀請'
17 | accept_team_user: '接受邀請'
18 | reject_team_user: '拒絕'
19 | delete_team_user_confirm: '確定要將他從團體中移除嗎?'
20 | topics: '成員話題列表'
21 | users: '成員列表'
22 | team_user_pendding: '等待接受'
23 | settings: '設置'
24 | team_users_count: "%{count} 個團體成員"
25 |
--------------------------------------------------------------------------------
/app/models/section.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Section < ApplicationRecord
4 | second_level_cache expires_in: 2.weeks
5 |
6 | has_many :nodes, dependent: :destroy
7 |
8 | validates :name, presence: true, uniqueness: true
9 |
10 | default_scope -> { order(sort: :desc) }
11 |
12 | after_save :update_cache_version
13 | after_destroy :update_cache_version
14 |
15 | def update_cache_version
16 | # 记录节点变更时间,用于清除缓存
17 | CacheVersion.section_node_updated_at = Time.now.to_i
18 | end
19 |
20 | def sorted_nodes
21 | nodes.where.sorted
22 | end
23 |
24 | def self.default
25 | @default ||= Section.first || Section.create(name: "分享")
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/config/locales/admin.zh-CN.yml:
--------------------------------------------------------------------------------
1 | "zh-CN":
2 | admin:
3 | menu:
4 | root_path: "概况"
5 | site_configs: "设置"
6 | users: "用户"
7 | sections: "分类"
8 | nodes: "节点"
9 | wiki: "Wiki"
10 | topics: "话题"
11 | replies: "回复"
12 | photos: "图片"
13 | comments: "评论"
14 | locations: "城市"
15 | exception_logs: "异常"
16 | overview: "概览"
17 | recent_topics: "最近帖子"
18 | statics: "统计信息"
19 | site_configs:
20 | settings: "全局设置"
21 | please_do_not_modify_key: "请勿修改 key"
22 | edit_setting: "修改设置"
23 | users:
24 | trust_user_can_modify_wiki: "信任用户将可以修改 Wiki"
25 | topic_list: "话题列表"
26 | check_photo: "瀏覽圖片"
27 |
--------------------------------------------------------------------------------
/config/locales/admin.zh-TW.yml:
--------------------------------------------------------------------------------
1 | "zh-TW":
2 | admin:
3 | menu:
4 | root_path: "概況"
5 | site_configs: "設置"
6 | users: "用戶"
7 | sections: "分類"
8 | nodes: "節點"
9 | wiki: "Wiki"
10 | topics: "帖子"
11 | replies: "回覆"
12 | photos: "圖片"
13 | comments: "評論"
14 | locations: "城市"
15 | exception_logs: "異常"
16 | overview: "概覽"
17 | recent_topics: "最近帖子"
18 | statics: "統計信息"
19 | site_configs:
20 | settings: "設置項"
21 | please_do_not_modify_key: "請勿修改 key"
22 | edit_setting: "修改設置"
23 | users:
24 | trust_user_can_modify_wiki: "信任用戶將可以修改 Wiki"
25 | topic_list: "話題列表"
26 | check_photo: "瀏覽圖片"
27 |
--------------------------------------------------------------------------------
/app/views/admin/site_configs/index.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :sitemap do %>
2 | <%= t("admin.site_configs.settings") %>
3 | <% end %>
4 |
5 | <%= t("admin.site_configs.settings") %>
6 |
7 |
8 |
9 | Key
10 | 说明
11 |
12 |
13 |
14 | <% Setting::KEYS_IN_ADMIN.each do |key| %>
15 |
16 | <%= key %>
17 | <%= t("setting.#{key}") %>
18 | <%= link_to icon_tag("pencil"), edit_admin_site_config_path(key) %>
19 |
20 | <% end %>
21 |
22 |
--------------------------------------------------------------------------------
/db/migrate/20161228064225_remove_unneeded_indexes.rb:
--------------------------------------------------------------------------------
1 | class RemoveUnneededIndexes < ActiveRecord::Migration[5.0]
2 | def change
3 | remove_index :devices, name: "index_devices_on_user_id_and_platform"
4 | remove_index :page_versions, name: "index_page_versions_on_page_id_and_version"
5 | remove_index :replies, name: "index_replies_on_topic_id_and_deleted_at"
6 | remove_index :sites, name: "index_sites_on_site_node_id_and_deleted_at"
7 | remove_index :team_users, name: "index_team_users_on_team_id_and_user_id"
8 | remove_index :topics, name: "index_topics_on_node_id"
9 | remove_index :new_notifications, name: "index_new_notifications_on_user_id_and_notify_type"
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/models/team.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Team < User
4 | has_many :team_users
5 | has_many :users, through: :team_users
6 |
7 | has_many :topics
8 |
9 | attr_accessor :owner_id
10 | after_create do
11 | self.team_users.create(user_id: owner_id, role: :owner, status: :accepted) if self.owner_id.present?
12 | end
13 |
14 | def user_ids
15 | @user_ids ||= self.users.pluck(:id)
16 | end
17 |
18 | def password_required?
19 | false
20 | end
21 |
22 | def owner?(user)
23 | self.team_users.accepted.exists?(role: :owner, user_id: user.id)
24 | end
25 |
26 | def member?(user)
27 | self.team_users.accepted.exists?(user_id: user.id)
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/app/views/replies/_create_callback.js.erb:
--------------------------------------------------------------------------------
1 | if($("#replies").length == 0){
2 | Turbolinks.visit(location.href);
3 | } else {
4 | if ($(".reply[data-id=<%= reply.id %>]").length == 0) {
5 | var current_floor = parseInt($("#replies").data("last-floor")) + 1;
6 | var dom = $('<%= j(render("reply", reply: reply, reply_counter: reply.topic.replies_count - 1, display_edit: true)) %>');
7 | $("#replies .items").append(dom);
8 | $("#replies .total b").text('<%= reply.topic.replies_count %>');
9 | $('#topic-sidebar .total b').text('<%= reply.topic.replies_count %>');
10 | dom.addClass('light').find("a.edit").css("display", "inline-block");
11 | _topicView.itemsUpdated();
12 | }
13 | }
14 |
15 |
--------------------------------------------------------------------------------
/spec/controllers/nodes_controller_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rails_helper"
4 |
5 | describe NodesController, type: :controller do
6 | let(:node) { create(:node) }
7 | let(:user) { create(:user) }
8 |
9 | it "should have an index action" do
10 | get :index
11 | expect(response).to have_http_status(200)
12 | end
13 |
14 | it "should have an block action" do
15 | sign_in user
16 | post :block, params: { id: node }
17 | expect(response).to have_http_status(200)
18 | end
19 |
20 | it "should have an unblock action" do
21 | sign_in user
22 | post :unblock, params: { id: node }
23 | expect(response).to have_http_status(200)
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/app/models/topic/auto_correct.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "auto-space"
4 |
5 | class Topic
6 | module AutoCorrect
7 | extend ActiveSupport::Concern
8 |
9 | CORRECT_CHARS = [
10 | ["[", "["],
11 | ["]", "]"],
12 | ["【", "["],
13 | ["】", "]"],
14 | ["(", "("],
15 | [")", ")"]
16 | ]
17 |
18 | included do
19 | before_save :auto_correct_title
20 | end
21 |
22 | private
23 | def auto_correct_title
24 | return if title.blank?
25 | title.dup
26 | CORRECT_CHARS.each do |chars|
27 | title.gsub!(chars[0], chars[1])
28 | end
29 | title.auto_space!
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/app/views/admin/locations/index.html.erb:
--------------------------------------------------------------------------------
1 | <%= render 'base' %>
2 |
3 |
4 |
5 |
6 | #
7 | 地名
8 | 会员数
9 |
10 |
11 | <% @locations.each do |item| %>
12 | ">
13 | <%= item.id %>
14 | <%= item.name %>
15 | <%= item.users_count %>
16 |
17 | <%= link_to "", edit_admin_location_path(item.id), class: "fa fa-pencil" %>
18 |
19 |
20 | <% end %>
21 |
22 | <%= paginate @locations %>
23 |
24 |
--------------------------------------------------------------------------------
/config/initializers/cors.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # Avoid CORS issues when API is called from the frontend app.
6 | # Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests.
7 |
8 | # Read more: https://github.com/cyu/rack-cors
9 |
10 | Rails.application.config.middleware.insert_before 0, Rack::Cors do
11 | allow do
12 | origins "*"
13 | resource "/api/*", headers: :any, methods: %i[get post put delete destroy]
14 | resource "/oauth/*", headers: :any, methods: %i[get post put delete destroy]
15 | end
16 | end
17 |
18 | Rails.application.config.middleware.insert_before 0, Rack::UTF8Sanitizer
19 |
--------------------------------------------------------------------------------
/app/controllers/admin/locations_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Admin
4 | class LocationsController < Admin::ApplicationController
5 | before_action :set_location, only: %i[show edit update destroy]
6 |
7 | def index
8 | @locations = Location.hot.page(params[:page])
9 | end
10 |
11 | def edit
12 | end
13 |
14 | def update
15 | if @location.update(params[:location].permit!)
16 | redirect_to(admin_locations_path, notice: "Location 更新成功。")
17 | else
18 | render action: "edit"
19 | end
20 | end
21 |
22 | private
23 |
24 | def set_location
25 | @location = Location.find(params[:id])
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/app/controllers/passwords_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class PasswordsController < Devise::PasswordsController
4 | before_action :require_no_sso!
5 |
6 | def create
7 | self.resource = resource_class.find_or_initialize_with_errors(Devise.reset_password_keys, resource_params, :not_found)
8 | if self.resource.persisted? && verify_rucaptcha?(resource)
9 | self.resource.send_reset_password_instructions
10 | end
11 |
12 | yield resource if block_given?
13 |
14 | if successfully_sent?(resource)
15 | respond_with({}, { location: after_sending_reset_password_instructions_path_for(resource_name) })
16 | else
17 | respond_with(resource)
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/app/controllers/admin/stats_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Admin
4 | class StatsController < Admin::ApplicationController
5 | # GET /stats
6 | # params:
7 | # model - Model 名称
8 | # by - day, week, month
9 | def index
10 | result = { model: params[:model] }
11 | result[:count] = klass.unscoped.count
12 | result[:week_count] = klass.unscoped.where("created_at >= ?", Date.today.beginning_of_week).count
13 | result[:month_count] = klass.unscoped.where("created_at >= ?", Date.today.beginning_of_month).count
14 | render json: result.as_json
15 | end
16 |
17 | def klass
18 | params[:model].camelize.safe_constantize
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/app/jobs/search_indexer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class SearchIndexer < ApplicationJob
4 | queue_as :search_indexer
5 |
6 | def perform(operation, type, id)
7 | obj = nil
8 |
9 | case type.downcase
10 | when "topic"
11 | obj = Topic.find_by_id(id)
12 | when "page"
13 | obj = Page.find_by_id(id)
14 | when "user"
15 | obj = User.find_by_id(id)
16 | end
17 |
18 | return false unless obj
19 |
20 | if operation == "update"
21 | obj.__elasticsearch__.update_document
22 | elsif operation == "delete"
23 | obj.__elasticsearch__.delete_document
24 | elsif operation == "index"
25 | obj.__elasticsearch__.index_document
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/app/models/concerns/searchable.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Searchable
4 | extend ActiveSupport::Concern
5 |
6 | included do
7 | include Elasticsearch::Model
8 |
9 | after_commit on: :create do
10 | SearchIndexer.perform_later("index", self.class.name, self.id)
11 | end
12 |
13 | after_update do
14 | need_update = false
15 | if self.respond_to?(:indexed_changed?)
16 | need_update = indexed_changed?
17 | end
18 |
19 | SearchIndexer.perform_later("index", self.class.name, self.id) if need_update
20 | end
21 |
22 | after_commit on: :destroy do
23 | SearchIndexer.perform_later("delete", self.class.name, self.id)
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/app/views/topics/feed.builder:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | xml.instruct! :xml, version: "1.0"
4 | xml.rss(version: "2.0") do
5 | xml.channel do
6 | xml.title t("rss.recent_topics_title", name: Setting.app_name)
7 | xml.link root_url
8 | xml.description(t("rss.recent_topics_description", name: Setting.app_name))
9 | xml.language("en-us")
10 | @topics.each do |topic|
11 | xml.item do
12 | xml.title topic.title
13 | xml.description markdown(topic.body)
14 | xml.author topic.user.login
15 | xml.pubDate(topic.created_at.strftime("%a, %d %b %Y %H:%M:%S %z"))
16 | xml.link topic_url topic
17 | xml.guid topic_url topic
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/app/views/topics/_sidebar_box_node_recent_topics.html.erb:
--------------------------------------------------------------------------------
1 | <% cache(["sidebar_for_node_recent_topics", topic.node_id, Time.now.strftime("%Y-%m-%d %H")]) do %>
2 | <%
3 | limit = [[topic.replies_count, 1].max, 10].min
4 | topics = Topic.where(:id.ne => topic.id, :node_id => topic.node_id).recent.limit(limit)
5 | %>
6 |
7 | <% if topics.present? %>
8 |
9 |
10 |
11 | <% topics.each do |item| %>
12 | <%= link_to(truncate(item.title, length: 30), topic_path(item), title: item.title) %>
13 | <% end %>
14 |
15 |
16 | <% end %>
17 | <% end %>
18 |
--------------------------------------------------------------------------------
/app/jobs/mention_topic_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # 在被提及的话题里面创建 mention 回复,连接到提及的话题
4 | class MentionTopicJob < ApplicationJob
5 | queue_as :notifications
6 |
7 | def perform(topic_ids, target_type:, target_id:, user_id:)
8 | return if topic_ids.blank?
9 |
10 | topics = Topic.where(id: topic_ids)
11 |
12 | topics.each do |topic|
13 | next if topic.replies.where(target_type: target_type, target_id: target_id).any?
14 |
15 | reply_param = {
16 | action: "mention",
17 | body: "",
18 | topic: topic,
19 | target_type: target_type,
20 | target_id: target_id,
21 | user_id: user_id
22 | }
23 |
24 | Reply.create!(reply_param)
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/db/migrate/20170204090209_create_actions.rb:
--------------------------------------------------------------------------------
1 | # This migration comes from action_store (originally 20170204035500)
2 | class CreateActions < ActiveRecord::Migration[5.0]
3 | def change
4 | create_table :actions do |t|
5 | t.string :action_type, null: false
6 | t.string :action_option
7 | t.string :target_type
8 | t.integer :target_id
9 | t.string :user_type
10 | t.integer :user_id
11 |
12 | t.timestamps
13 | end
14 |
15 | add_index :actions, [:user_type, :user_id, :action_type]
16 | add_index :actions, [:target_type, :target_id, :action_type]
17 |
18 | add_column :users, :followers_count, :integer, default: 0
19 | add_column :users, :following_count, :integer, default: 0
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/app/controllers/api/v3/root_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api
4 | module V3
5 | class RootController < Api::V3::ApplicationController
6 | before_action :doorkeeper_authorize!, only: [:hello]
7 |
8 | def not_found
9 | raise ActiveRecord::RecordNotFound
10 | end
11 |
12 | # 简单的 API 测试接口,需要验证,便于快速测试 OAuth 以及其他 API 的基本格式是否正确
13 | #
14 | # GET /api/v3/hello
15 | #
16 | # @param limit - API token
17 | # @return [UserDetailSerializer]
18 | def hello
19 | optional! :limit, values: 0..100
20 |
21 | @meta = { time: Time.now }
22 | @user = current_user
23 |
24 | render "api/v3/users/show"
25 | end
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/app/views/topics/node_feed.builder:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | xml.instruct! :xml, version: "1.0"
4 | xml.rss version: "2.0" do
5 | xml.channel do
6 | xml.title t("rss.recent_node_topics_title", name: Setting.app_name, node_name: @node.name)
7 | xml.link root_url
8 | xml.description t("rss.recent_node_topics_description", name: Setting.app_name, node_name: @node.name)
9 | @topics.each do |topic|
10 | xml.item do
11 | xml.title topic.title
12 | xml.description markdown(topic.body)
13 | xml.author topic.user.login
14 | xml.pubDate topic.created_at.strftime("%a, %d %b %Y %H:%M:%S %z")
15 | xml.link topic_url(topic)
16 | xml.guid topic_url(topic)
17 | end
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/spec/models/device_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rails_helper"
4 |
5 | RSpec.describe Device, type: :model do
6 | let(:device) { create :device }
7 |
8 | describe ".alive?" do
9 | context "more that 2 weeks" do
10 | let(:device) { create :device, last_actived_at: 3.weeks.ago }
11 | it { expect(device.alive?).to eq false }
12 | end
13 |
14 | context "last_actived_at nil" do
15 | let(:device) { create :device, last_actived_at: nil }
16 | it { expect(device.alive?).to eq true }
17 | end
18 |
19 | context "last_actived_at less than 14 days" do
20 | let(:device) { create :device, last_actived_at: 14.days.ago }
21 | it { expect(device.alive?).to eq true }
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/app/models/location.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Location < ApplicationRecord
4 | second_level_cache expires_in: 2.weeks
5 |
6 | has_many :users
7 |
8 | scope :hot, -> { order(users_count: :desc) }
9 |
10 | validates :name, presence: true, uniqueness: { case_sensitive: false }
11 |
12 | before_save { |loc| loc.name = loc.name.downcase.strip }
13 |
14 | def self.location_find_by_name(name)
15 | return nil if name.blank?
16 | name = name.downcase.strip
17 | where("name ~* ?", name).first
18 | end
19 |
20 | def self.location_find_or_create_by_name(name)
21 | name = name.strip
22 | unless (location = location_find_by_name(name))
23 | location = create(name: name)
24 | end
25 | location
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/homeland/pipeline/floor_filter.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Homeland
4 | class Pipeline
5 | class FloorFilter < HTML::Pipeline::Filter
6 | FLOOR_REGEXP = /#(\d+)([楼樓Ff])/
7 |
8 | def call
9 | doc.search(".//text()").each do |node|
10 | content = node.to_html
11 | next unless content.include?("#")
12 | next if has_ancestor?(node, %w[pre code])
13 |
14 | content.gsub!(FLOOR_REGEXP) do
15 | %(##{Regexp.last_match(1)}#{Regexp.last_match(2)} )
16 | end
17 |
18 | node.replace(content)
19 | end
20 | doc
21 | end
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .bundle
2 | node_modules/
3 | tmp/
4 | coverage/
5 | db/*.sqlite3
6 | tmp/
7 | log/*.log
8 | log/*.log*
9 | tmp/**/*
10 | .sass-cache/
11 | .yardoc/
12 | public/assets/
13 | public/packs/
14 | config/database.yml
15 | config/mongoid.yml
16 | config/config.yml
17 | config/thin.yml
18 | config/redis.yml
19 | config/mailer_daemon.yml
20 | config/secrets.yml
21 | public/photo/**/*
22 | public/user/**/*
23 | public/uploads/**/*
24 | public/system/**/*
25 | public/topics
26 | public/topics/**/*
27 | public/doc
28 | .redcar
29 | .DS_Store
30 | *.swp
31 | *.swo
32 | *.sublime-workspace
33 | .rvmrc
34 | vendor/ruby
35 | doc/wiki_repo
36 | solr/data/
37 | solr/pids/
38 | tags
39 | chromedriver.log
40 | .idea/
41 | spec/examples.txt
42 | .vagrant
43 | .byebug_history
44 | .ruby-version
45 | /storage/*
46 |
--------------------------------------------------------------------------------
/app/views/users/_topics.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%=t("common.node")%>
4 | <%=t("common.title")%>
5 | <%=t("common.replies_count")%>/<%= t("common.likes_count") %>
6 |
7 | <% topics.each do |topic| %>
8 | <%= 'deleted' if topic.deleted? %>">
9 | <%= render_node_name(topic.node_name,topic.node_id) %>
10 | <%= link_to(topic.title, topic_path(topic)) %> <%= topic_excellent_tag(topic) %> <%= timeago(topic.created_at) %>
11 | <%= topic.replies_count %>/<%= topic.likes_count %>
12 |
13 | <% end %>
14 |
15 |
--------------------------------------------------------------------------------
/db/migrate/20160329032533_create_notifications.notifications.rb:
--------------------------------------------------------------------------------
1 | # This migration comes from notifications (originally 20160328045436)
2 | class CreateNotifications < ActiveRecord::Migration[4.2]
3 | def change
4 | create_table :new_notifications do |t|
5 | t.integer :user_id, null: false
6 | t.integer :actor_id
7 | t.string :notify_type, null: false
8 | t.string :target_type
9 | t.integer :target_id
10 | t.string :second_target_type
11 | t.integer :second_target_id
12 | t.string :third_target_type
13 | t.integer :third_target_id
14 | t.datetime :read_at
15 |
16 | t.timestamps null: false
17 | end
18 |
19 | add_index :new_notifications, [:user_id, :notify_type]
20 | add_index :new_notifications, [:user_id]
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/app/views/topics/_reply_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= render "/shared/editor_toolbar" %>
2 | <%= form_with(model: Reply.new, remote: true, url: topic_replies_path(@topic), id: "new_reply") do |f| %>
3 |
4 |
5 | <%= f.text_area :body, class: "topic-editor form-control", rows: "4", tabindex: "1" %>
6 |
7 | <%= f.hidden_field :reply_to_id %>
8 |
14 | <% end %>
15 |
--------------------------------------------------------------------------------
/config/locales/admin.en.yml:
--------------------------------------------------------------------------------
1 | "en":
2 | admin:
3 | menu:
4 | root_path: "Dashboard"
5 | site_configs: "Settings"
6 | users: "Users"
7 | sections: "Sections"
8 | nodes: "Nodes"
9 | wiki: "Wiki"
10 | topics: "Topics"
11 | replies: "Replies"
12 | photos: "Photos"
13 | comments: "Comments"
14 | locations: "Locations"
15 | exception_logs: "Errors"
16 | overview: "Overview"
17 | recent_topics: "Recent Topics"
18 | statics: "Statistics"
19 | site_configs:
20 | settings: "Settings"
21 | please_do_not_modify_key: "Please do not modify key"
22 | edit_setting: "Edit Settings"
23 | users:
24 | trust_user_can_modify_wiki: "Allow user to modify Wiki"
25 | topic_list: "Topic List"
26 | check_photo: "View Photo"
27 |
--------------------------------------------------------------------------------
/lib/homeland/pipeline/normalize_mention_filter.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Homeland
4 | class Pipeline
5 | class NormalizeMentionFilter < HTML::Pipeline::TextFilter
6 | PREFIX_REGEXP = %r{(^|[^#{User::LOGIN_FORMAT}!#/\$%&*@@])}
7 | USER_REGEXP = /#{PREFIX_REGEXP}@([#{User::LOGIN_FORMAT}]{1,20})/io
8 |
9 | def call
10 | users = []
11 | # Makesure clone a new value, not change original value
12 | text = @text.clone.dup
13 | text.gsub!(USER_REGEXP) do
14 | prefix = Regexp.last_match(1)
15 | user = Regexp.last_match(2)
16 | users.push(user)
17 | "#{prefix}@user#{users.size}"
18 | end
19 | result[:normalize_mentions] = users
20 | text
21 | end
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/templates/erb/scaffold/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%
2 | form_model_name = "@#{file_name}"
3 | if controller_class_name.include?('::')
4 | namespace = controller_class_name.split('::').first.downcase
5 | form_model_name = "[:#{namespace},@#{file_name}]"
6 | end
7 | %>
8 | <%%= form_for(<%= form_model_name %>) do |f| %>
9 | <%%= render "/shared/error_messages", target: @<%= file_name %> %>
10 | <% for attribute in attributes -%>
11 |
12 | <%%= f.label :<%= attribute.name %> %>
13 | <%%= f.text_field :<%= attribute.name %>, class: "form-control" %>
14 |
15 | <% end -%>
16 |
17 |
18 | <%%= f.submit t("common.save"), class: "btn btn-primary" %> or <%%= link_to t("common.cancel"), <%= index_helper %>_path %>
19 |
20 | <%% end %>
21 |
--------------------------------------------------------------------------------
/app/views/shared/_index_sections.html.erb:
--------------------------------------------------------------------------------
1 | <% cache(["index_locations", CacheVersion.section_node_updated_at]) do %>
2 |
3 |
4 |
5 |
6 | <% Section.includes(:nodes).all.each do |section| %>
7 |
8 | <%= section.name %>
9 |
10 | <% section.nodes.sorted.each do |node| %>
11 | <%= link_to(node.name, node_topics_path(node), title: node.name, data: { id: node.id })%>
12 | <% end %>
13 |
14 |
15 | <% end %>
16 |
17 |
18 |
19 | <% end %>
20 |
--------------------------------------------------------------------------------
/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
17 |
18 |
19 |
20 |
21 |
22 |
The change you wanted was rejected.
23 |
Maybe you tried to change something you didn't have access to.
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/db/migrate/20160126061903_create_settings.rb:
--------------------------------------------------------------------------------
1 | class OldSetting < ApplicationRecord
2 | self.table_name = 'site_configs'
3 | end
4 |
5 | class CreateSettings < ActiveRecord::Migration[4.2]
6 | def self.up
7 | create_table :settings do |t|
8 | t.string :var, null: false
9 | t.text :value, null: true
10 | t.integer :thing_id, null: true
11 | t.string :thing_type, null: true, limit: 30
12 | t.timestamps
13 | end
14 |
15 | add_index :settings, %i(thing_type thing_id var), unique: true
16 |
17 | Setting.transaction do
18 | OldSetting.all.each do |item|
19 | Setting.create!(var: item.key, value: item.value)
20 | end
21 | end
22 |
23 | drop_table :site_configs
24 | end
25 |
26 | def self.down
27 | drop_table :settings
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/spec/controllers/devices_controller_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rails_helper"
4 |
5 | describe DevicesController, type: :controller do
6 | let(:user) { create(:user) }
7 | let!(:device) { create(:device, user: user) }
8 |
9 | it "DELETE /devices/:id" do
10 | sign_in user
11 | expect(device.new_record?).to eq false
12 | expect do
13 | delete :destroy, params: { id: device.id }
14 | end.to change(user.devices, :count).by(-1)
15 | expect(response).to redirect_to(oauth_applications_path)
16 | expect(user.devices.where(id: device.id).count).to eq 0
17 | end
18 |
19 | it "require login" do
20 | expect do
21 | delete :destroy, params: { id: device.id }
22 | end.to change(user.devices, :count).by(0)
23 | expect(response.status).to eq(302)
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/app/views/devise/unlocks/new.html.erb:
--------------------------------------------------------------------------------
1 | <% title_tag '重新发送解锁邮件' %>
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | <%= form_for resource, as: resource_name, url: unlock_path(resource_name), method: :post do |f| %>
10 | <%= render "shared/error_messages", target: resource %>
11 |
12 |
13 | <%= f.text_field :email, class: "form-control", placeholder: "Email" %>
14 |
15 |
16 |
17 | <%= f.submit "重新发送解锁邮件", class: 'btn btn-primary' %>
18 |
19 | <% end %>
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/views/users/_recent_publish_topics.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
<%= t("users.recent_publish_topic")%>
3 |
4 |
5 | <%= t("common.reply_count")%>
6 | <%= t("common.title")%>
7 | <%= t("common.last_reply_time")%>
8 |
9 | <% last_topics.each do |topic| %>
10 |
11 | <%= topic.replies_count %>
12 | <%= link_to(truncate(topic.title, length: 25), topic_path(topic))%>
13 | <%= l((topic.replied_at || topic.created_at), format: :short) %>
14 |
15 | <% end %>
16 |
17 |
18 |
--------------------------------------------------------------------------------
/config/initializers/assets.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # Version of your assets, change this if you want to expire all your assets.
6 | Rails.application.config.assets.version = "1.0"
7 |
8 | # Add additional assets to the asset load path.
9 | # Rails.application.config.assets.paths << Emoji.images_path
10 | # Add Yarn node_modules folder to the asset load path.
11 | Rails.application.config.assets.paths << Rails.root.join("node_modules")
12 |
13 | # Precompile additional assets.
14 | # application.js, application.css, and all non-JS/CSS in the app/assets
15 | # folder are already added.
16 | # Rails.application.config.assets.precompile += %w( admin.js admin.css )
17 | Rails.application.config.assets.precompile += %w[app.js front.css turbolinks-app.css admin.css]
18 |
--------------------------------------------------------------------------------
/spec/controllers/search_controller_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rails_helper"
4 |
5 | describe SearchController, type: :controller do
6 | describe "/search/users" do
7 | let(:user) { create(:user) }
8 | let(:users) { [create(:user), create(:user)] }
9 |
10 | it "should work" do
11 | sign_in user
12 | allow(User).to receive(:search).and_return(users)
13 | get :users
14 | res = JSON.parse(response.body)
15 | expect(response).to have_http_status(200)
16 | expect(res[0]).to include("login", "name", "avatar_url")
17 | expect(res.map { |j| j["login"] }).to match users.map(&:login)
18 | expect(res.map { |j| j["name"] }).to match users.map(&:name)
19 | expect(res.map { |j| j["avatar_url"] }).to match users.map(&:large_avatar_url)
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/app/controllers/notifications/notifications_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Notifications
4 | class NotificationsController < Notifications::ApplicationController
5 | def index
6 | @notifications = notifications.includes(:actor).order("id desc").page(params[:page])
7 |
8 | unread_ids = @notifications.reject(&:read?).select(&:id)
9 | Notification.read!(unread_ids)
10 |
11 | @notification_groups = @notifications.group_by { |note| note.created_at.to_date }
12 |
13 | Notification.realtime_push_to_client(current_user)
14 | end
15 |
16 | def clean
17 | notifications.delete_all
18 | redirect_to notifications_path
19 | end
20 |
21 | private
22 |
23 | def notifications
24 | Notification.where(user_id: current_user.id)
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # Add new inflection rules using the following format. Inflections
6 | # are locale specific, and you may define rules for as many different
7 | # locales as you wish. All of these examples are active by default:
8 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
9 | # inflect.plural /^(ox)$/i, '\1en'
10 | # inflect.singular /^(ox)en/i, '\1'
11 | # inflect.irregular 'person', 'people'
12 | # inflect.uncountable %w( fish sheep )
13 | # end
14 |
15 | # These inflection rules are supported but not enabled by default:
16 | ActiveSupport::Inflector.inflections(:en) do |inflect|
17 | # user_sso -> UserSSO
18 | inflect.acronym "SSO"
19 | # github -> GitHub
20 | inflect.acronym "GitHub"
21 | end
22 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/search.scss:
--------------------------------------------------------------------------------
1 | @import "vars";
2 |
3 | .search-results {
4 | .result {
5 | margin-bottom: 20px;
6 |
7 | em { color: $red; font-style: normal;}
8 |
9 | .title {
10 | font-size: 15px;
11 | line-height: 160%;
12 |
13 | .badge { background: $gray; color: $gray; font-weight: normal; font-size: 12px; margin-left: 4px; }
14 | }
15 | .info {
16 | margin-bottom: 6px;
17 | font-size: 14px;
18 | .url a { color: $green; }
19 |
20 | .date { color: #999; margin-left: 8px; }
21 | }
22 |
23 | .desc {
24 | color: #666;
25 | font-size: 13px;
26 | word-break: break-all;
27 | em { color: $red; }
28 | }
29 | }
30 |
31 | .user {
32 | .info { margin-top: 4px; font-size: 14px; }
33 | .info.number { color: #666; font-size: 13px; }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/views/admin/users/edit.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :sitemap do %>
2 | <%= t("admin.menu.users")%> <%= t("common.edit")%>
3 | <% end %>
4 |
5 | <% if @user.user_type == :user %>
6 |
7 |
8 | <%= link_to '删除他的最近 10 篇话题', clean_admin_user_path(@user, type: 'topics'),
9 | method: 'delete', class: 'btn btn-warning',
10 | data: { confirm: '注意!!!此动作是直接删除,无法恢复的,你确定要删除么?' } %>
11 |
12 | <%= link_to '删除他的最近 10 条回帖', clean_admin_user_path(@user, type: 'replies'),
13 | method: 'delete', class: 'btn btn-warning',
14 | data: { confirm: '注意!!!此动作是直接删除,无法恢复的,你确定要删除么?' } %>
15 |
16 |
17 | <% end %>
18 |
19 | <%= render 'form' %>
20 |
--------------------------------------------------------------------------------
/app/views/users/_menu.html.erb:
--------------------------------------------------------------------------------
1 | <%= render_list class: "users-menu nav nav-tabs" do |li|
2 | li << link_to(t("users.menu.profile"), user_path(@user), class: "nav-link")
3 | li << link_to(t("users.menu.topics"), topics_user_path(@user), class: "nav-link")
4 | li << link_to(t("users.menu.replies"), replies_user_path(@user), class: "nav-link")
5 | li << link_to(t("users.menu.favorites"), favorites_user_path(@user), class: "nav-link hide-ios hidden-mobile")
6 | li << link_to(t("users.menu.following"), following_user_path(@user), class: "nav-link hidden-mobile")
7 | li << link_to(t("users.menu.followers"), followers_user_path(@user), class: "nav-link hidden-mobile")
8 | if owner?(@user) && current_user.block_users?
9 | li << link_to(t("users.menu.blocked"), blocked_user_path(@user), class: "nav-link hide-ios hidden-mobile")
10 | end
11 | end %>
12 |
--------------------------------------------------------------------------------
/config/locales/teams.en.yml:
--------------------------------------------------------------------------------
1 | 'en':
2 | team_user_status:
3 | pendding: 'Awaiting response'
4 | acceped: 'Accepted'
5 | team_user_role:
6 | owner: 'Owner'
7 | member: 'Member'
8 | teams:
9 | teams: 'Team'
10 | user_existed: 'Already in this team'
11 | new_team: 'New team'
12 | edit_team: 'Edit team profile'
13 | new_team_user: 'Add a member'
14 | edit_team_user: 'Change team roles'
15 | delete_team_user: 'Remove member'
16 | show_team_user: 'View invitations'
17 | accept_team_user: 'Accept invitation'
18 | reject_team_user: 'Decline'
19 | delete_team_user_confirm: 'Are you sure you want to remove this user from team?'
20 | topics: 'Topics of members'
21 | users: 'Members'
22 | team_user_pendding: 'Awaiting response'
23 | settings: 'Settings'
24 | team_users_count: "%{count} team members"
25 |
--------------------------------------------------------------------------------
/spec/models/section_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rails_helper"
4 |
5 | describe Section, type: :model do
6 | describe ".default" do
7 | it "should work" do
8 | expect(Section.default).to be_a(Section)
9 | expect(Section.default.new_record?).to eq false
10 | end
11 | end
12 |
13 | describe "CacheVersion update" do
14 | let(:old) { 1.minutes.ago }
15 | it "should update on save" do
16 | CacheVersion.section_node_updated_at = old
17 | create(:section)
18 | expect(CacheVersion.section_node_updated_at).not_to eq(old)
19 | end
20 |
21 | it "should update on destroy" do
22 | section = create(:section)
23 | CacheVersion.section_node_updated_at = old
24 | section.destroy
25 | expect(CacheVersion.section_node_updated_at).not_to eq(old)
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/app/views/notifications/notifications/_notification.html.erb:
--------------------------------------------------------------------------------
1 | <%= cache(['notifications', Notifications::VERSION, notification, notification.actor, notification.read?]) do %>
2 |
15 | <% end %>
16 |
--------------------------------------------------------------------------------
/app/views/replies/_reply_to.html.erb:
--------------------------------------------------------------------------------
1 | <% if reply.reply_to && reply.reply_to&.user %>
2 | <%
3 | reply_to = reply.reply_to
4 | user = reply_to.user
5 | %>
6 |
7 |
8 | 对
9 | <% if show_body %>
10 | <%= user_avatar_tag(user, :xs) %><%= user_name_tag(user) %>
11 | <% else %>
12 | <%= link_to reply_to_topic_reply_path(@topic, reply), remote: true do %>
13 | <%= user_avatar_tag(user, :xs, link: false) %><%= user.login %>
14 |
15 | <% end %>
16 | <% end %>
17 | 回复
18 |
19 | <% if show_body %>
20 |
21 | <%= markdown(reply_to.body) %>
22 |
23 | <% end %>
24 |
25 | <% end %>
26 |
--------------------------------------------------------------------------------
/app/views/search/index.html.erb:
--------------------------------------------------------------------------------
1 | <% title_tag [params[:q], t('common.search_result')].join(' · ') %>
2 |
3 |
4 |
7 |
8 |
9 | <% if @result.records.total == 0 %>
10 |
没有搜索到任何有关 “<%= params[:q]%>” 的内容
11 | <% else %>
12 | <% @result.records.each_with_hit do |item, hit| %>
13 | <% partial_view_name = item.class.name.downcase -%>
14 | <%= render partial: (partial_view_name == 'team' ? 'user' : partial_view_name), locals: { item: item, hit: hit } %>
15 | <% end %>
16 | <% end %>
17 |
18 |
19 |
22 |
23 |
--------------------------------------------------------------------------------
/app/views/team_users/show.html.erb:
--------------------------------------------------------------------------------
1 | <% title_tag t("teams.show_team_user") %>
2 | <%= render '/teams/header' %>
3 |
4 |
5 |
6 |
7 |
8 |
<%= t("teams.show_team_user") %>
9 |
10 | 你是否要接受邀请加入 <%= user_name_tag(@team) %> ?
11 |
12 |
13 | <%= link_to t('teams.accept_team_user'), accept_user_team_user_path(@team, @team_user), data: { method: 'POST' }, class: 'btn btn-primary' %>
14 | <%= link_to t('teams.reject_team_user'), reject_user_team_user_path(@team, @team_user), data: { method: 'POST' }, class: 'btn btn-default' %>
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/views/admin/photos/index.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :sitemap do %>
2 | <%= t('admin.menu.photos') %>
3 | <% end %>
4 |
5 |
6 |
7 | #
8 | 预览
9 | 上传者
10 |
11 |
12 |
13 | <% @photos.each do |photo| %>
14 |
15 | <%= photo.id %>
16 | <%= link_to image_tag(photo.image.url(:md), style: "width: 150px"), photo.image.url, target: "_blank" if !photo[:image].blank? %>
17 | <%= photo.user.login if !photo.user.blank? %>
18 | <%= link_to '', admin_photo_path(photo), 'data-confirm' => 'Are you sure?', method: :delete, class: "fa fa-trash" %>
19 |
20 | <% end %>
21 |
22 | <%= paginate @photos %>
23 |
24 |
25 | <%= link_to 'New Photo', new_admin_photo_path %>
26 |
--------------------------------------------------------------------------------
/app/views/settings/_menu.html.erb:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/app/views/users/_reward.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
<%= user_avatar_tag(@user, :md, link: false) %>
10 |
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
11 |
12 |
13 | <% @user.reward_fields.each_with_index do |(field, val), idx| %>
14 |
15 | <% end %>
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/lib/homeland/markdown.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "html/pipeline"
4 |
5 | context = {
6 | gfm: true,
7 | video_width: 700,
8 | video_height: 387
9 | }
10 |
11 | filters = [
12 | Homeland::Pipeline::NormalizeMentionFilter,
13 | Homeland::Pipeline::EmbedVideoFilter,
14 | Homeland::Pipeline::MarkdownFilter,
15 | Homeland::Pipeline::MentionFilter,
16 | Homeland::Pipeline::FloorFilter,
17 | Homeland::Pipeline::TwemojiFilter
18 | ]
19 |
20 | TopicPipeline = HTML::Pipeline.new(filters, context)
21 |
22 | module Homeland
23 | class Markdown
24 | class << self
25 | def call(body)
26 | result = TopicPipeline.call(body)[:output].inner_html
27 | result.strip!
28 | result
29 | end
30 |
31 | def example
32 | @example ||= open(Rails.root.join("lib/homeland/markdown_guides.md")).read
33 | end
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/app/controllers/admin/site_configs_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Admin
4 | class SiteConfigsController < Admin::ApplicationController
5 | before_action :set_setting, only: %i[edit update]
6 |
7 | def index
8 | end
9 |
10 | def edit
11 | end
12 |
13 | def update
14 | if @site_config.value != setting_param[:value]
15 | @site_config.value = setting_param[:value]
16 | @site_config.save
17 | @site_config.expire_cache
18 | redirect_to admin_site_configs_path, notice: "保存成功."
19 | else
20 | redirect_to admin_site_configs_path
21 | end
22 | end
23 |
24 | def set_setting
25 | @site_config = Setting.find_by(var: params[:id]) || Setting.new(var: params[:id])
26 | end
27 |
28 | private
29 |
30 | def setting_param
31 | params[:setting].permit!
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/app/views/admin/sections/index.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :sitemap do %>
2 | <%= t("admin.menu.sections") %>
3 | <% end %>
4 |
5 |
6 | <%= link_to '新建分类', new_admin_section_path, class: "btn btn-default" %>
7 |
8 |
9 |
10 |
11 | #
12 | 名称
13 | 排序
14 |
15 |
16 |
17 | <% @sections.each do |section| %>
18 |
19 |
20 | <%= section.id %>
21 | <%= section.name %>
22 | <%= section.sort %>
23 | <%= link_to '', edit_admin_section_path(section), class: "fa fa-pencil" %>
24 | <%= link_to '', admin_section_path(section), 'data-confirm' => '确定要删除吗?', method: :delete, class: "fa fa-trash" %>
25 |
26 | <% end %>
27 |
28 |
29 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env rake
2 | # frozen_string_literal: true
3 |
4 | # Add your own tasks in files placed in lib/tasks ending in .rake,
5 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
6 |
7 | require File.expand_path("../config/application", __FILE__)
8 | require "elasticsearch/rails/tasks/import"
9 |
10 | Rails.application.load_tasks
11 |
12 | task default: "bundle:audit"
13 |
14 | require "sdoc"
15 | require "rdoc/task"
16 |
17 | rdoc_files = %w[
18 | README.md
19 | PLUGIN_DEV.md
20 | CONTRIBUTE.md
21 | API.md
22 | app/models
23 | app/controllers
24 | app/helpers
25 | lib/homeland.rb
26 | lib/single_sign_on.rb
27 | lib/homeland
28 | ]
29 |
30 | RDoc::Task.new do |rdoc|
31 | rdoc.rdoc_dir = "public/doc"
32 | rdoc.generator = "sdoc"
33 | rdoc.template = "rails"
34 | rdoc.main = "README.md"
35 | rdoc.rdoc_files.include(*rdoc_files)
36 | end
37 |
--------------------------------------------------------------------------------
/lib/assets/stylesheets/jquery.atwho.css:
--------------------------------------------------------------------------------
1 | .atwho-view {
2 | position: absolute;
3 | top: 0;
4 | left: 0;
5 | display: none;
6 | margin-top: 18px;
7 | background: #fff;
8 | border: 1px solid #e0e0e0;
9 | border-radius: 3px;
10 | box-shadow: 0 0 5px rgba(0,0,0,.1);
11 | min-width: 120px;
12 | z-index: 10
13 | }
14 | .atwho-view .cur {
15 | background: #317DDA;
16 | color: #fff
17 | }
18 | .atwho-view .cur small {
19 | color: #fff
20 | }
21 | .atwho-view strong { font-weight: normal; }
22 |
23 | .atwho-view ul {
24 | list-style: none;
25 | padding: 0;
26 | margin: auto
27 | }
28 | .atwho-view ul li {
29 | display: block;
30 | padding: 5px 10px;
31 | border-bottom: 1px solid #eee;
32 | cursor: pointer
33 | }
34 | .atwho-view ul li img { width: 16px; height: 16px; border-radius: 180px; }
35 | .atwho-view small {
36 | font-size: smaller;
37 | color: #777;
38 | font-weight: 400
39 | }
40 |
--------------------------------------------------------------------------------
/app/views/admin/topics/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= form_for [:admin,@topic] do |f| %>
2 | <%= render 'shared/error_messages', target: @topic %>
3 |
4 |
5 | <%= f.label :title %>
6 | <%= f.text_field :title, class: "form-control" %>
7 |
8 |
9 |
10 | <%= f.label :node_id %>
11 | <%= f.text_field :node_id, class: "form-control" %>
12 |
13 |
14 |
15 | <%= f.label :user_id %>
16 | <%= f.text_field :user_id, class: "form-control" %>
17 |
18 |
19 |
20 | <%= f.label :body %>
21 | <%= f.text_area :body, class: "form-control" %>
22 |
23 |
24 |
25 | <%= f.submit t("common.save"), class: "btn btn-primary", 'data-disable-with' => t("common.saving") %>
26 | or
27 | <%= link_to 'Back', admin_topics_path %>
28 |
29 | <% end %>
30 |
--------------------------------------------------------------------------------
/app/controllers/sessions_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class SessionsController < Devise::SessionsController
4 | skip_before_action :set_locale, only: [:create]
5 | before_action :require_no_sso!, only: %i[new create]
6 |
7 | def create
8 | resource = warden.authenticate!(scope: resource_name, recall: "#{controller_path}#new")
9 | set_flash_message(:notice, :signed_in) if is_navigational_format?
10 | sign_in(resource_name, resource)
11 | respond_to do |format|
12 | format.html { redirect_back_or_default(root_url) }
13 | format.json { render status: "201", json: resource.as_json(only: %i[login email]) }
14 | end
15 | end
16 |
17 | private
18 |
19 | # If not bind to a domain, request.domain is nil.
20 | def domain_or_host
21 | request.domain || request.host
22 | end
23 |
24 | def respond_to_on_destroy
25 | redirect_to topics_url
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/rails/generators/erb/scaffold/scaffold_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rails/generators/erb"
4 | require "rails/generators/resource_helpers"
5 |
6 | module Erb
7 | module Generators
8 | class ScaffoldGenerator < Base
9 | include Rails::Generators::ResourceHelpers
10 |
11 | argument :attributes, type: :array, default: [], banner: "field:type field:type"
12 |
13 | def create_root_folder
14 | empty_directory File.join("app/views", controller_file_path)
15 | end
16 |
17 | def copy_view_files
18 | available_views.each do |view|
19 | filename = filename_with_extensions(view)
20 | template filename, File.join("app/views", controller_file_path, filename)
21 | end
22 | end
23 |
24 | protected
25 |
26 | def available_views
27 | %w[index edit show new _form _base]
28 | end
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/spec/api/photos_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rails_helper"
4 | require "active_support/core_ext"
5 |
6 | describe "API", type: :request do
7 | let(:json) { JSON.parse(response.body) }
8 |
9 | describe "POST /api/v3/photos.json" do
10 | include ActionDispatch::TestProcess
11 |
12 | context "without login" do
13 | it "should response 401" do
14 | post "/api/v3/photos.json"
15 | expect(response.status).to eq 401
16 | end
17 | end
18 |
19 | context "with login" do
20 | it "should work" do
21 | login_user!
22 | post "/api/v3/photos.json", file: fixture_file_upload("test.png")
23 | @photo = Photo.last
24 | expect(response.status).to eq 200
25 | expect(@photo.user_id).to eq current_user.id
26 | expect(json["image_url"]).not_to eq nil
27 | expect(json["image_url"]).not_to eq ""
28 | end
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/app/jobs/push_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class PushJob < ApplicationJob
4 | queue_as :notifications
5 |
6 | # user_ids: 用户编号列表
7 | # note: { alert: 'Hello APNS World!', sound: 'true', badge: 1 }
8 | def perform(user_ids, note = {})
9 | return false if Setting.apns_pem.blank?
10 |
11 | note[:sound] ||= "true"
12 | devices = Device.where(user_id: user_ids).all.to_a
13 | devices.select!(&:alive?)
14 | tokens = devices.collect(&:token)
15 | return false if tokens.blank?
16 |
17 | notification = RubyPushNotifications::APNS::APNSNotification.new(tokens, aps: note)
18 | pusher = RubyPushNotifications::APNS::APNSPusher.new(Setting.apns_pem, !Rails.env.production?)
19 | pusher.push [notification]
20 | Rails.logger.tagged("PushJob") do
21 | Rails.logger.info "send to #{tokens.size} devices #{note} status: #{notification.success}"
22 | end
23 | notification.success
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/app/views/admin/nodes/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= form_for [:admin,@node] do |f| %>
2 | <%= render 'shared/error_messages', target: @node %>
3 |
4 |
5 | <%= f.label :name %>
6 | <%= f.text_field :name, class: "form-control" %>
7 |
8 |
9 |
10 | <%= f.label :name %>
11 | <%= f.select :section_id, Section.all.collect { |s| [s.name,s.id] }, {}, { class: "form-control" } %>
12 |
13 |
14 |
15 | <%= f.label :sort %>
16 | <%= f.text_field :sort, class: "form-control" %>
17 |
18 |
19 |
20 | <%= f.label :summary %>
21 | <%= f.text_area :summary, class: "form-control" %>
22 |
23 |
24 |
25 | <%= f.submit t("common.save"), class: "btn btn-primary", 'data-disable-with' => t("common.saving") %>
26 | <%= link_to 'Back', admin_nodes_path %>
27 |
28 | <% end %>
29 |
--------------------------------------------------------------------------------
/bin/update:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'fileutils'
3 | include FileUtils
4 |
5 | # path to your application root.
6 | APP_ROOT = File.expand_path('..', __dir__)
7 |
8 | def system!(*args)
9 | system(*args) || abort("\n== Command #{args} failed ==")
10 | end
11 |
12 | chdir APP_ROOT do
13 | # This script is a way to update your development environment automatically.
14 | # Add necessary update steps to this file.
15 |
16 | puts '== Installing dependencies =='
17 | system! 'gem install bundler --conservative'
18 | system('bundle check') || system!('bundle install')
19 |
20 | # Install JavaScript dependencies if using Yarn
21 | # system('bin/yarn')
22 |
23 | puts "\n== Updating database =="
24 | system! 'bin/rails db:migrate'
25 |
26 | puts "\n== Removing old logs and tempfiles =="
27 | system! 'bin/rails log:clear tmp:clear'
28 |
29 | puts "\n== Restarting application server =="
30 | system! 'bin/rails restart'
31 | end
32 |
--------------------------------------------------------------------------------
/app/models/cache_version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | # 用于记录特定的 cache version
3 | # 比如:
4 | # 记录最后更新 置顶话题的时间,以用于作为自动变换置顶那个 cache 的 key,以达到自动过期的目的
5 | # 用法例子:
6 | # 以上面个场景为例
7 | # Topic after_suggest ->
8 | # CacheVersion.topic_last_suggested_at = Time.now
9 | # View 里面 <% cache("topic/index/sidebar_suggest:#{CacheVersion.topic_last_suggested_at}") do %><% end %>
10 | class CacheVersion
11 | def self.method_missing(method, *args)
12 | method_name = method.to_s
13 | super(method, *args)
14 | rescue NoMethodError
15 | if method_name.match?(/=$/)
16 | var_name = method_name.sub("=", "")
17 | key = CacheVersion.mk_key(var_name)
18 | value = args.first.to_s
19 | # save
20 | Rails.cache.write(key, value)
21 | else
22 | key = CacheVersion.mk_key(method)
23 | Rails.cache.read(key)
24 | end
25 | end
26 |
27 | def self.mk_key(key)
28 | "cache_version:#{key}"
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/app/controllers/account_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Devise User Controller
4 | class AccountController < Devise::RegistrationsController
5 | before_action :require_no_sso!, only: %i[new create]
6 |
7 | def new
8 | super
9 | end
10 |
11 | def edit
12 | redirect_to setting_path
13 | end
14 |
15 | # POST /resource
16 | def create
17 | build_resource(sign_up_params)
18 | resource.login = params[resource_name][:login]
19 | resource.email = params[resource_name][:email]
20 | if verify_rucaptcha?(resource) && resource.save
21 | sign_in(resource_name, resource)
22 | end
23 | end
24 |
25 | private
26 |
27 | # Overwrite the default url to be used after updating a resource.
28 | # It should be edit_user_registration_path
29 | # Note: resource param can't miss, because it's the super caller way.
30 | def after_update_path_for(_)
31 | edit_user_registration_path
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/config/initializers/notifications.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # notifications Config
4 | Notifications.configure do
5 | # Class name of you User model, default: 'User'
6 | # self.user_class = 'User'
7 |
8 | # Method of user name in User model, default: 'name'
9 | self.user_name_method = "login"
10 |
11 | # Method of user avatar in User model, default: nil
12 | self.user_avatar_url_method = "large_avatar_url"
13 |
14 | # Method name of user profile page path, in User model, default: 'profile_url'
15 | self.user_profile_url_method = "profile_url"
16 |
17 | # authenticate_user method in your Controller, default: 'authenticate_user!'
18 | # If you use Devise, authenticate_user! is correct
19 | self.authenticate_user_method = "authenticate_user!"
20 |
21 | # current_user method name in your Controller, default: 'current_user'
22 | # If you use Devise, current_user is correct
23 | # self.current_user_method = 'current_user'
24 | end
25 |
--------------------------------------------------------------------------------
/lib/homeland/plugin.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Homeland
4 | class Plugin
5 | # name of plugin, use var name style.
6 | attr_accessor :name
7 |
8 | # Plugin version
9 | attr_accessor :version
10 |
11 | # Description of plugin
12 | attr_accessor :description
13 |
14 | # Display name of plugin
15 | attr_accessor :display_name
16 |
17 | # set true if plugin link wants list in top navbar
18 | attr_accessor :navbar_link
19 |
20 | # set true if plugin link wants list in user drodown menu
21 | attr_accessor :user_menu_link
22 |
23 | # set true if plugin link wants list in admin navbar
24 | attr_accessor :admin_navbar_link
25 |
26 | # path of plugin root, for example /blog
27 | attr_accessor :root_path
28 |
29 | # path of plugin admin page for example /admin/blog
30 | attr_accessor :admin_path
31 |
32 | # add RSpec test path
33 | attr_accessor :spec_path
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/app/views/users/favorites.html.erb:
--------------------------------------------------------------------------------
1 | <% title_tag [@user.fullname, t('users.menu.favorites')].join(' · ') %>
2 |
3 |
4 | <%= render "sidebar", user: @user %>
5 |
6 | <%= render "menu" %>
7 |
8 |
9 |
10 |
11 | <%= t("common.node") %>
12 | <%= t("common.title") %>
13 |
14 | <% @topics.each do |item| %>
15 | ">
16 | <%= render_node_name(item.node_name,item.node_id) %>
17 | <%= topic_title_tag(item) %>
18 |
19 | <% end %>
20 |
21 |
22 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/views/search/_user.html.erb:
--------------------------------------------------------------------------------
1 | <% if item.organization? %>
2 | <%= render 'team', item: item, hit: hit %>
3 | <% else %>
4 |
25 | <% end %>
26 |
--------------------------------------------------------------------------------
/app/assets/javascripts/form_storage.coffee:
--------------------------------------------------------------------------------
1 | @FormStorage =
2 | key: (element) ->
3 | "#{location.pathname} #{$(element).prop('id')}"
4 |
5 | init: ->
6 | if window.localStorage
7 | $(document).on 'input', 'textarea[name*=body]', ->
8 | textarea = $(this)
9 | localStorage.setItem(FormStorage.key(textarea), textarea.val())
10 |
11 | $(document).on 'submit', 'form', ->
12 | form = $(this)
13 | form.find('textarea[name*=body]').each ->
14 | localStorage.removeItem(FormStorage.key(this))
15 |
16 | $(document).on 'click', 'form a.reset', ->
17 | form = $(this).closest('form')
18 | form.find('textarea[name*=body]').each ->
19 | localStorage.removeItem(FormStorage.key(this))
20 |
21 | restore: ->
22 | if window.localStorage
23 | $('textarea[name*=body]').each ->
24 | textarea = $(this)
25 | if value = localStorage.getItem(FormStorage.key(textarea))
26 | textarea.val(value)
27 |
--------------------------------------------------------------------------------
/app/models/concerns/mention_topic.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module MentionTopic
4 | extend ActiveSupport::Concern
5 |
6 | TOPIC_LINK_REGEXP = %r{://#{Setting.domain}/topics/([\d]+)}i
7 |
8 | included do
9 | attr_accessor :mentioned_topic_ids
10 |
11 | after_save :create_releated_for_mentioned_topics
12 | end
13 |
14 | def extract_mentioned_topic_ids
15 | matched_ids = body.scan(TOPIC_LINK_REGEXP).flatten
16 | current_topic_id = self.class.name == "Topic" ? self.id : self.topic_id
17 | return if matched_ids.blank?
18 | matched_ids = matched_ids.map(&:to_i).reject { |id| id == current_topic_id }
19 | Topic.where("id IN (?)", matched_ids).pluck(:id)
20 | end
21 |
22 | private
23 | def create_releated_for_mentioned_topics
24 | topic_ids = extract_mentioned_topic_ids
25 | return if topic_ids.blank?
26 | MentionTopicJob.perform_later(topic_ids, target_type: self.class.name, target_id: self.id, user_id: self.user_id)
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/app/views/admin/comments/index.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :sitemap do %>
2 | <%= t('admin.menu.comments') %>
3 | <% end %>
4 |
5 |
6 |
7 | 被评论对象
8 | 评论人
9 | 内容
10 | At
11 |
12 |
13 |
14 | <% @comments.each do |item| %>
15 |
23 | <% end %>
24 |
25 | <%= paginate @comments %>
26 |
--------------------------------------------------------------------------------
/app/views/notifications/_comment.html.erb:
--------------------------------------------------------------------------------
1 | <%
2 | comment = notification.target
3 | second_target = notification.second_target
4 | %>
5 |
6 | <% if second_target.blank? %>
7 | 相关信息已删除
8 | <% else %>
9 | <%
10 | case notification.second_target_type
11 | when "Post" %>
12 | <% if Setting.has_module? :press %>
13 |
16 |
17 | <%= comment.body_html %>
18 |
19 | <% end %>
20 | <% when "Page" %>
21 | <% if Setting.has_module? :wiki %>
22 |
25 |
26 | <%= comment.body_html %>
27 |
28 | <% end %>
29 | <% end %>
30 | <% end %>
31 |
--------------------------------------------------------------------------------
/config/locales/topics.zh-TW.yml:
--------------------------------------------------------------------------------
1 | "zh-TW":
2 | topics:
3 | new_topic: "新建帖子"
4 | edit_topic: "修改帖子"
5 | no_replies: "暫無回覆。"
6 | recent_topics: "最近發佈的帖子"
7 | recent_replies: "最近回覆的帖子"
8 | check_recent_topics: "查看更多最近發佈的帖子..."
9 | reply_this_fllor: "回覆此樓"
10 | tips_for_create_topic: "發帖說明"
11 | subscribe_with_rss: "訂閱 %{name} 社區新帖"
12 | submit_reply: "提交回復"
13 | topic_list:
14 | hot_topic: "活躍帖子"
15 | recent: "最新創建"
16 | last_reply: "最新回覆"
17 | popular: "優質帖子"
18 | no_reply: "無人問津"
19 | excellent: "精華帖"
20 | read_topic: "瀏覽帖子"
21 | create_topic_success: "帖子創建成功。"
22 | delete_topic_success: "帖子刪除成功。"
23 | update_topic_success: "帖子修改成功。"
24 | reply_success: "回覆成功。"
25 | reply_this_floor: "回覆此樓"
26 | topic_was_deleted: "[該主題已被刪除]"
27 | have_no_new_reply: "在你讀過以後還沒有新變化"
28 | has_new_replies: "有新內容"
29 | more_like_this: "相關帖子"
30 | node_recent_topics: "節點下其他話題"
31 | have_no_avatar: "你還未設置頭像,不能創建新帖子。"
32 |
--------------------------------------------------------------------------------
/app/views/notifications/mentions/_comment.html.erb:
--------------------------------------------------------------------------------
1 | <%
2 | comment = notification.target
3 | second_target = notification.second_target
4 | %>
5 |
6 | <% if second_target.blank? %>
7 | 相关信息已删除
8 | <% else %>
9 | <%
10 | case notification.second_target_type
11 | when "Post" %>
12 | <% if Setting.has_module? :press %>
13 |
16 |
17 | <%= comment.body_html %>
18 |
19 | <% end %>
20 | <% when "Page" %>
21 | <% if Setting.has_module? :wiki %>
22 |
25 |
26 | <%= comment.body_html %>
27 |
28 | <% end %>
29 | <% end %>
30 | <% end %>
31 |
--------------------------------------------------------------------------------
/config/locales/topics.zh-CN.yml:
--------------------------------------------------------------------------------
1 | "zh-CN":
2 | topics:
3 | new_topic: "发布新话题"
4 | edit_topic: "修改话题"
5 | no_replies: "暂无回复。"
6 | recent_topics: "最近发布的话题"
7 | recent_replies: "最近回复的话题"
8 | check_recent_topics: "查看更多最近发布的话题..."
9 | reply_this_fllor: "回复此楼"
10 | tips_for_create_topic: "发帖说明"
11 | subscribe_with_rss: "订阅 %{name} 社区新话题"
12 | submit_reply: "提交回复"
13 | topic_list:
14 | hot_topic: "活跃话题"
15 | recent: "最新创建"
16 | last_reply: "最新回复"
17 | popular: "优质话题"
18 | no_reply: "无人问津"
19 | excellent: "精华帖"
20 | read_topic: "浏览话题"
21 | create_topic_success: "话题创建成功。"
22 | delete_topic_success: "话题删除成功。"
23 | update_topic_success: "话题修改成功。"
24 | reply_success: "回复成功。"
25 | reply_this_floor: "回复此楼"
26 | topic_was_deleted: "[该话题已被删除]"
27 | have_no_new_reply: "在你读过以后还没有新变化"
28 | has_new_replies: "有新内容"
29 | more_like_this: "相关话题"
30 | node_recent_topics: "节点下其他话题"
31 | have_no_avatar: "你还未设置头像,不能发布新话题。"
32 |
--------------------------------------------------------------------------------
/app/controllers/admin/comments_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Admin
4 | class CommentsController < Admin::ApplicationController
5 | before_action :set_comment, only: %i[show edit update destroy]
6 | respond_to :js, :html, only: [:destroy]
7 |
8 | def index
9 | @comments = Comment.recent.includes(:user).page(params[:page])
10 | end
11 |
12 | def edit
13 | end
14 |
15 | def update
16 | if @comment.update(params[:comment].permit!)
17 | redirect_to admin_comments_path(@admin_comment), notice: "Comment was successfully updated."
18 | else
19 | render action: "edit"
20 | end
21 | end
22 |
23 | def destroy
24 | @comment.destroy
25 | respond_with do |format|
26 | format.html { redirect_to admin_comments_path }
27 | format.js { render layout: false }
28 | end
29 | end
30 |
31 | private
32 |
33 | def set_comment
34 | @comment = Comment.find(params[:id])
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/app/views/admin/nodes/index.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :sitemap do %>
2 | <%= t("admin.menu.nodes")%>
3 | <% end %>
4 |
5 |
6 | <%= link_to '新建节点', new_admin_node_path, class: "btn btn-default" %>
7 |
8 |
9 |
10 |
11 | #
12 | 名称
13 | 分类
14 | 排序
15 | 帖子数量
16 |
17 |
18 |
19 | <% @nodes.each do |node| %>
20 |
21 | <%= node.id %>
22 | <%= link_to node.name, node_topics_path(node.id) %>
23 | <%= node&.section&.name %>
24 | <%= node.sort %>
25 | <%= node.topics_count %>
26 | <%= link_to '', edit_admin_node_path(node), class: "fa fa-pencil" %>
27 | <%= link_to '', admin_node_path(node), 'data-confirm' => 'Are you sure?' , method: :delete, class: "fa fa-trash" %>
28 |
29 | <% end %>
30 |
31 |
--------------------------------------------------------------------------------
/spec/models/user/reward_fields_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rails_helper"
4 |
5 | describe User, type: :model do
6 | describe "RewardFields" do
7 | let(:user) { create(:user) }
8 |
9 | it "should work" do
10 | params = {
11 | alipay: "alipay111",
12 | wechat: "wechat111"
13 | }
14 | expect(user.reward_enabled?).to eq false
15 | user.update_reward_fields(params)
16 | expect(user.reward_enabled?).to eq true
17 | expect(user.settings.reward_fields).to eq(params)
18 | expect(user.reward_fields).to eq(params)
19 | expect(user.reward_field(:wechat)).to eq "wechat111"
20 | expect(user.reward_field("wechat")).to eq "wechat111"
21 | expect(user.reward_field("alipay")).to eq "alipay111"
22 | expect(user.reward_field(:ddd)).to eq nil
23 | expect(user.reward_field(:facebook)).to eq nil
24 | expect(User.reward_field_label(:wechat)).to eq "微信"
25 | expect(User.reward_field_label(:alipay)).to eq "支付宝"
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/app/views/topics/_sidebar_for_topic_index.html.erb:
--------------------------------------------------------------------------------
1 | <% if current_user && current_user.newbie? %>
2 |
3 |
4 |
5 | <%= raw Setting.newbie_notices %>
6 |
7 |
8 | <% else %>
9 |
10 |
11 |
12 | <%= link_to t('topics.new_topic'), main_app.new_topic_path, class: 'btn btn-primary btn-block' %>
13 |
14 |
15 | <% end %>
16 |
17 | <%= render "topics/sidebar_box_tips" %>
18 |
19 | <%= raw Setting.topic_index_sidebar_html %>
20 |
21 | <% cache(["sidebar_statistics",Time.now.strftime("%Y-%m-%d %H")]) do %>
22 |
23 |
24 |
25 | 社区会员: <%= User.unscoped.count %> 人
26 | 帖子数: <%= Topic.unscoped.count %> 个
27 | 回帖数: <%= Reply.unscoped.count %> 条
28 |
29 |
30 | <% end %>
31 |
--------------------------------------------------------------------------------
/app/views/users/_repos.html.erb:
--------------------------------------------------------------------------------
1 | <% if Setting.has_module? :github %>
2 |
3 | <% if not user.github.blank? %>
4 |
5 |
6 | <% user.github_repositories.each do |item| %>
7 |
8 |
9 | <%= link_to truncate(item[:name], length: 25), item[:url], target: "_blank", rel: "nofollow" %>
10 | <%= item[:watchers] %>
11 |
12 |
13 | <%= truncate(item[:description], length: 100) %>
14 |
15 | <% end %>
16 |
17 |
20 | <% else %>
21 |
22 | 未设置 GitHub 信息.
23 |
24 | <% end %>
25 |
26 | <% end %>
27 |
--------------------------------------------------------------------------------
/config/initializers/rack-attack.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | if Setting.rack_attack.present?
4 | BLOCK_MESSAGE = ["你请求过快,超过了频率限制,暂时屏蔽一段时间。"]
5 |
6 | class Rack::Attack
7 | Rack::Attack.cache.store = Rails.cache
8 |
9 | ### Throttle Spammy Clients ###
10 | throttle("req/ip", limit: Setting.rack_attack["limit"] || 300, period: Setting.rack_attack["period"] || 3.minutes, &:ip)
11 |
12 | # 固定黑名单
13 | blocklist("blacklist/ip") do |req|
14 | Setting.blacklist_ips && !Setting.blacklist_ips.index(req.ip).nil?
15 | end
16 |
17 | # 允许 localhost
18 | safelist("allow from localhost") do |req|
19 | req.ip == "127.0.0.1" || req.ip == "::1"
20 | end
21 |
22 | ### Custom Throttle Response ###
23 | self.throttled_response = lambda do |_env|
24 | [503, {}, BLOCK_MESSAGE]
25 | end
26 | end
27 |
28 | ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, request_id, req|
29 | Rails.logger.info " RackAttack: #{req.ip} #{request_id} blocked."
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/app/controllers/likes_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class LikesController < ApplicationController
4 | before_action :authenticate_user!, only: %i[create destroy]
5 | before_action :set_likeable
6 |
7 | def index
8 | @users = @item.like_by_users.order("actions.id asc")
9 | render :index, layout: false
10 | end
11 |
12 | def create
13 | current_user.like(@item)
14 | render plain: @item.reload.likes_count
15 | end
16 |
17 | def destroy
18 | current_user.unlike(@item)
19 | render plain: @item.reload.likes_count
20 | end
21 |
22 | private
23 |
24 | def set_likeable
25 | @success = false
26 | @element_id = "likeable_#{params[:type]}_#{params[:id]}"
27 |
28 | defined_action = User.find_defined_action(:like, params[:type])
29 |
30 | if defined_action.blank?
31 | render plain: "-1"
32 | return false
33 | end
34 |
35 | @item = defined_action[:target_klass].find_by(id: params[:id])
36 | render plain: "-2" if @item.blank?
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/config/secrets.yml.default:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key is used for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 |
6 | # Make sure the secret is at least 30 characters and all random,
7 | # no regular words or you'll be exposed to dictionary attacks.
8 | # You can use `rails secret` to generate a secure secret key.
9 |
10 | # Make sure the secrets in this file are kept private
11 | # if you're sharing your code publicly.
12 |
13 | development:
14 | secret_key_base: 66ad08589cd561754c8cd659a5a50a3694904f9c455dd2177115697d12d2c27b873222da8646f34c8cf4598a0f108f12c05141d645a3534c9a9a9833f239f397
15 |
16 | test:
17 | secret_key_base: 66ad08589cd561754c8cd659a5a50a3694904f9c455dd2177115697d12d2c27b873222da8646f34c8cf4598a0f108f12c05141d645a3534c9a9a9833f239f397
18 |
19 | # Do not keep production secrets in the repository,
20 | # instead read values from the environment.
21 | production:
22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
23 |
--------------------------------------------------------------------------------
/app/controllers/admin/replies_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Admin
4 | class RepliesController < Admin::ApplicationController
5 | before_action :set_reply, only: %i[show edit update destroy]
6 |
7 | def index
8 | @replies = Reply.unscoped
9 | if params[:q].present?
10 | qstr = "%#{params[:q].downcase}%"
11 | @replies = @replies.where("body LIKE ?", qstr)
12 | end
13 | if params[:login].present?
14 | u = User.find_by_login(params[:login])
15 | @replies = @replies.where("user_id = ?", u.try(:id))
16 | end
17 | @replies = @replies.order(id: :desc).includes(:topic, :user)
18 | @replies = @replies.page(params[:page])
19 | end
20 |
21 | def show
22 | if @reply.topic.blank?
23 | redirect_to admin_replies_path, alert: "帖子已经不存在"
24 | end
25 | end
26 |
27 | def destroy
28 | @reply.destroy
29 | end
30 |
31 | private
32 |
33 | def set_reply
34 | @reply = Reply.unscoped.find(params[:id])
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/app/controllers/oauth/applications_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Oauth
4 | class ApplicationsController < Doorkeeper::ApplicationsController
5 | before_action :authenticate_user!
6 | include Homeland::UserNotificationHelper
7 |
8 | def index
9 | @applications = current_user.oauth_applications
10 | @authorized_applications = Doorkeeper::Application.authorized_for(current_user)
11 | @devices = current_user.devices.all
12 | end
13 |
14 | # only needed if each application must have some owner
15 | def create
16 | @application = Doorkeeper::Application.new(application_params)
17 | @application.uid = SecureRandom.hex(4)
18 | @application.owner = current_user if Doorkeeper.configuration.confirm_application_owner?
19 |
20 | if @application.save
21 | flash[:notice] = I18n.t(:notice, scope: %i[doorkeeper flash applications create])
22 | redirect_to oauth_application_url(@application)
23 | else
24 | render :new
25 | end
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/app/views/home/index.html.erb:
--------------------------------------------------------------------------------
1 | <% if !mobile? %>
2 |
3 | <%= raw Setting.index_html %>
4 |
5 | <% end %>
6 |
7 |
8 |
9 |
10 | <% cache(["home_suggest_topics", @excellent_topics]) do %>
11 | <%
12 | odd_topics, even_topics = @excellent_topics.partition.each_with_index { |t, i| i.odd? }
13 | %>
14 |
15 | <%= render partial: "topics/topic", collection: even_topics, locals: { suggest: false } %>
16 |
17 |
18 |
19 | <%= render partial: "topics/topic", collection: odd_topics, locals: { suggest: false } %>
20 |
21 | <% end %>
22 |
23 |
26 |
27 |
28 | <% if !mobile? %>
29 | <%= render "/shared/index_sections" %>
30 | <%= render "/shared/hot_locations" %>
31 | <% end %>
32 |
--------------------------------------------------------------------------------
/app/views/replies/edit.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | <%= form_for [@topic, @reply], remote: true, tb: 'edit-reply' do |f| %>
7 | <%= render "shared/error_messages", target: @reply %>
8 | <%= render "/shared/editor_toolbar" %>
9 |
10 | <%= f.text_area :body, class: "topic-editor form-control", rows: 10 %>
11 |
12 |
13 | <%= f.submit t("common.save"), class: "btn btn-primary col-xs-2 hide-ios", 'data-disable-with' => t("common.saving") %>
14 | <% if can? :destroy, @reply %>
15 | <%= link_to "删除",[@topic,@reply], class: "btn btn-danger", method: :delete, data: { confirm: "确定要删除此回复么?" } %>
16 | <% end %>
17 |
18 | <% end %>
19 |
20 |
21 |
22 |
23 | <%= render "/shared/editor_help_block" %>
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/views/notifications/notifications/index.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 | <% if @notifications.blank? %>
11 |
12 |
<%= t('notifications.no_records') %>
13 |
14 | <% else %>
15 | <% @notification_groups.each do |group, notifications| %>
16 |
17 |
<%= group %>
18 | <%= render notifications %>
19 |
20 | <% end %>
21 | <% end %>
22 |
23 |
26 |
27 |
--------------------------------------------------------------------------------
/config/initializers/content_security_policy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | # Be sure to restart your server when you modify this file.
3 |
4 | # Define an application-wide content security policy
5 | # For further information see the following documentation
6 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
7 |
8 | # Rails.application.config.content_security_policy do |policy|
9 | # policy.default_src :self, :https
10 | # policy.font_src :self, :https, :data
11 | # policy.img_src :self, :https, :data
12 | # policy.object_src :none
13 | # policy.script_src :self, :https
14 | # policy.style_src :self, :https, :unsafe_inline
15 |
16 | # # Specify URI for violation reports
17 | # # policy.report_uri "/csp-violation-report-endpoint"
18 | # end
19 |
20 | # Report CSP violations to a specified URI
21 | # For further information see the following documentation:
22 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
23 | # Rails.application.config.content_security_policy_report_only = true
24 |
--------------------------------------------------------------------------------
/app/controllers/home_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class HomeController < ApplicationController
4 | def index
5 | @excellent_topics = Topic.excellent.recent.fields_for_list.limit(20).to_a
6 | end
7 |
8 | def uploads
9 | return render_404 if Rails.env.production?
10 |
11 | # This is a temporary solution for help generate image thumb
12 | # that when you use :file upload_provider and you have no Nginx image_filter configurations.
13 | # DO NOT use this in production environment.
14 | format, version = params[:format].split("!")
15 | filename = [params[:path], format].join(".")
16 | pragma = request.headers["Pragma"] == "no-cache"
17 | thumb = Homeland::ImageThumb.new(filename, version, pragma: pragma)
18 | if thumb.exists?
19 | send_file thumb.outpath, type: "image/jpeg", disposition: "inline"
20 | else
21 | render plain: "File not found", status: 404
22 | end
23 | end
24 |
25 | def api
26 | redirect_to "/api-doc/"
27 | end
28 |
29 | def error_404
30 | render_404
31 | end
32 |
33 | def markdown
34 | end
35 | end
36 |
--------------------------------------------------------------------------------