From 48cb0e0450334f23d629e59b1ba0c54d639e19a6 Mon Sep 17 00:00:00 2001 From: alan <547533434@qq.com> Date: Sat, 31 Jan 2015 15:04:48 +0800 Subject: [PATCH 01/68] email verify --- Gemfile | 2 +- Gemfile.lock | 9 +++++++++ app/models/user.rb | 2 +- config/application.rb | 6 +++--- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 037c606c8..f59c92077 100644 --- a/Gemfile +++ b/Gemfile @@ -20,7 +20,7 @@ gem "builder", "3.0.0" gem 'acts-as-taggable-on', '2.4.1' gem 'spreadsheet' gem 'ruby-ole' -#gem 'email_verifier', path: 'lib/email_verifier' +gem 'email_verifier', path: 'lib/email_verifier' group :development do gem 'grape-swagger' diff --git a/Gemfile.lock b/Gemfile.lock index 355ca422d..8c5816e13 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -12,6 +12,13 @@ PATH coderay (>= 1.0.0) erubis (>= 2.6.6) +PATH + remote: lib/email_verifier + specs: + email_verifier (0.0.7) + dnsruby (>= 1.5) + rails (>= 3.0.0) + PATH remote: lib/rack-mini-profiler specs: @@ -91,6 +98,7 @@ GEM descendants_tracker (0.0.4) thread_safe (~> 0.3, >= 0.3.1) diff-lcs (1.2.5) + dnsruby (1.57.0) equalizer (0.0.9) erubis (2.7.0) execjs (2.2.1) @@ -315,6 +323,7 @@ DEPENDENCIES capybara (~> 2.4.1) coderay (~> 1.0.6) coffee-rails (~> 3.2.1) + email_verifier! factory_girl (~> 4.4.0) faker fastercsv (~> 1.5.0) diff --git a/app/models/user.rb b/app/models/user.rb index 06f59c764..bef65fe54 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -188,7 +188,7 @@ class User < Principal validates_confirmation_of :password, :allow_nil => true validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true validate :validate_password_length - #validates_email_realness_of :mail + validates_email_realness_of :mail before_create :set_mail_notification before_save :update_hashed_password before_destroy :remove_references_before_destroy diff --git a/config/application.rb b/config/application.rb index c7f2f3ea5..647675784 100644 --- a/config/application.rb +++ b/config/application.rb @@ -17,9 +17,9 @@ module RedmineApp # -- all .rb files in that directory are automatically loaded. #verifier if email is real - # EmailVerifier.config do |config| - # config.verifier_email = "lizanle521@126.com" - # end + EmailVerifier.config do |config| + config.verifier_email = "alanlong9278@126.com" + end config.generators do |g| g.test_framework :rspec, From d61199f66b4182d0b7c8f476cab5270408ee210d Mon Sep 17 00:00:00 2001 From: alan <547533434@qq.com> Date: Sat, 31 Jan 2015 16:43:10 +0800 Subject: [PATCH 02/68] merge szzh --- Gemfile.lock | 4 ---- lib/email_verifier | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 8c5816e13..b26f17113 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -170,8 +170,6 @@ GEM metaclass (~> 0.0.1) multi_json (1.10.1) multi_xml (0.5.5) - mysql2 (0.3.11) - mysql2 (0.3.11-x86-mingw32) nenv (0.2.0) net-ldap (0.3.1) nokogiri (1.6.3) @@ -316,7 +314,6 @@ PLATFORMS DEPENDENCIES activerecord-jdbc-adapter (= 1.2.5) - activerecord-jdbcmysql-adapter acts-as-taggable-on (= 2.4.1) better_errors! builder (= 3.0.0) @@ -337,7 +334,6 @@ DEPENDENCIES jquery-rails (~> 2.0.2) kaminari mocha (~> 1.1.0) - mysql2 (= 0.3.11) net-ldap (~> 0.3.1) nokogiri (~> 1.6.3) paperclip (~> 3.5.4) diff --git a/lib/email_verifier b/lib/email_verifier index 3cabcc643..222a9bdd7 160000 --- a/lib/email_verifier +++ b/lib/email_verifier @@ -1 +1 @@ -Subproject commit 3cabcc643f36939939685e6f55273dfbf89da545 +Subproject commit 222a9bdd72014f197baf2131ab71cc41660111ed From 8f1946711528b6456521a3d44a26511d8087031b Mon Sep 17 00:00:00 2001 From: whimlex Date: Sat, 31 Jan 2015 17:18:07 +0800 Subject: [PATCH 03/68] =?UTF-8?q?=E8=8B=B1=E6=96=87=E5=9B=BD=E9=99=85?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/locales/en.yml | 111 ++++++++++++++++++++++++++++++------------ config/locales/zh.yml | 77 ++++++++++++++++++----------- 2 files changed, 127 insertions(+), 61 deletions(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index cb89070ff..b21793f57 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -161,13 +161,8 @@ en: label_approve: Approve label_refusal: Refusal notice_account_updated: Account was successfully updated. - notice_account_invalid_creditentials: Invalid user or password - notice_account_password_updated: Password was successfully updated. notice_account_wrong_password: Wrong password notice_account_register_done: Account was successfully created. To activate your account, click on the link that was emailed to you. - notice_account_unknown_email: Unknown user. - notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password. - notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you. notice_account_activated: Your account has been activated. You can now log in. notice_successful_create: Successful creation. notice_successful_update: Successful update. @@ -243,7 +238,6 @@ en: field_is_required: Required field_firstname: Name field_lastname: Last name - field_mail: Email field_job_category: Job category # added by bai field_filename: File field_file_dense: File Dense @@ -279,14 +273,11 @@ en: field_homepage: Homepage field_parent: Subproject of field_is_in_roadmap: Issues displayed in roadmap - field_login: Account/Email field_mail_notification: Email notifications field_admin: Administrator field_last_login_on: Last connection field_language: Language field_effective_date: Date - field_new_password: New password - field_password_confirmation: Confirmation field_version: Version field_type: Type field_host: Host @@ -646,7 +637,6 @@ en: label_information: Information label_information_plural: Information label_please_login: Please log in - label_login_with_open_id_option: or login with OpenID label_home: Home label_my_page: My page label_my_account: My account @@ -655,9 +645,6 @@ en: label_my_page_block: My page block label_administration: Administration label_login: Login - # edit by meng - # Logout - label_logout: Logout # end label_help: Help label_reported_issues: Reported issues @@ -1018,8 +1005,6 @@ en: label_gantt_progress_line: Progress line label_files_filter: Files Filter: - button_submit: Submit - button_save: Save button_check_all: Check all button_uncheck_all: Uncheck all button_collapse_all: Collapse all @@ -1099,7 +1084,6 @@ en: text_tip_issue_end_day: issue ending this day text_tip_issue_begin_end_day: issue beginning and ending this day text_caracters_maximum: "%{count} characters maximum." - text_caracters_minimum: "Must be at least %{count} characters long." text_length_between: "Length between %{min} and %{max} characters." text_tracker_no_workflow: No workflow defined for this tracker text_unallowed_characters: Unallowed characters @@ -1438,7 +1422,6 @@ en: label_tags_user_mail: User E-mail: label_tags_user_name: User Name: label_tags_numbers: Tag numbers: - label_max_number: Open label nickname is displayed on the web site of your,Must be at most 25 characters long. label_all_revisions: All revisions: label_repository_name: Repository name label_upassword_info: The password can be shared in the group @@ -1734,7 +1717,6 @@ en: label_your_course: your course label_have_message : have a new message - label_login_prompt: Email/NickName :lable_not_receive_mail: Click here don't receive email form site! #added by linchun as competition# @@ -1756,17 +1738,83 @@ en: # edit by meng # Trustie账户> 登陆 lable_user_name: Username + label_login_prompt: Email/Trustie account field_password: Password field_identity_url: OpenID URL - label_stay_logged_in: Keep me signed in - label_password_lost: Forget password? + label_stay_logged_in: "Keep me signed in" + label_password_lost: "Forget password?" button_login: Login + # account_controller中判断用户名或密码输入有误的提示信息 + notice_account_invalid_creditentials: "Invalid user or password." + # account_controller中判断未激活的提示信息 + notice_account_invalid_creditentials_new: "Please check your email to activate your account." # Trustie账户> 注册 + # 页面中密码和确认密码不一致信息,正则判断,邮件地址合法等信息由application_helper中error_messages_for方法来判断提示信息 + # rails本身机制ActiveRecord提供 + # 在model层的 validates_方法进行辅助验校 + # 其输出的提示信息在国际化yml中的activerecord中配置 label_register: Sign up + label_login_with_open_id_option: or login with OpenID + field_login: Account/Email + label_max_number: "Trustie username is your public identity displayed on Trustie website, only for letters and numbers." + field_password: Password + text_caracters_minimum: "Must be at least %{count} characters long." + field_password_confirmation: Confirmation + field_mail: Email + label_mail_attention: "QQ-mail may not receive this e-mail, if other mailboxes not received, probably in spam." + label_mail_attention1: "Activation email sent for Gmail and Edu-mail which sometimes slower, please be patient." + button_submit: Submit + # register中js判断密码设置是否合法提示信息 + setting_password_min_length_limit: "Password length greater than at least %{count} characters." + setting_password_error: "Password length inadequate or password inconsistent." + setting_password_success: "Password set successfully." + # account_controller中register方法判断注册成功的提示信息 + notice_account_register_done: "Successful account creation, please use the link in the registration confirmation email to activate your account, if our messages are not in your inbox, it could in the spam box, please check it carefully." + + + # Trustie账户模块 > 忘记密码 + label_password_forget: Forget password + # field_mail: 邮件地址 + # (Trustie账户模块> 注册)变量 + # button_submit: 提交 + # (Trustie账户模块> 注册 )变量 + notice_account_unknown_email: Unknown user + # account_controller中lost_password方法判断的邮件发送提示信息 + notice_account_lost_email_sent: "An email with a temporary login link will be sent to that email address, which you can use to login and configure a new password." + + + # Trustie账户模块 > 重置密码 + # label_password_forget: 忘记密码 + # (Trustie账户模块> 忘记密码)变量 + field_new_password: New password + # text_caracters_minimum: "至少需要 %{count} 个字符。" + # (Trustie账户模块> 注册)变量 + # field_password_confirmation: 密码确认 + # (Trustie账户模块> 注册)变量 + notice_account_password_updated: "Password was successfully updated." + notice_can_t_change_password: "This account uses an external authentication source. Impossible to change the password." + button_save: Save + + + # Trustie账户模块 > 登出 + label_logout: Logout + # Trustie账户模块 > 激活 + label_regiter_account: Registering for an account + label_email_valid: E-mail activation + notice_email_register_time: "Please click on the link in the email to continue to complete the registration within 24 hours" + notice_email_arrival: "An activation email has been sent to the email address you register." + label_check_email: "Now check your email" + label_mail_resend: "Resend the activation email" + notice_account_activated: "Your Trustie account has been activated." + + + + + # 托管平台主页 # edit by meng # 托管平台主页> 顶部菜单 @@ -1807,18 +1855,18 @@ en: label_rights_reserved: ©2007~2014 label_contact_us: Contact # 英文版不需要显示国内许可证 ,需要页面做判断 - #label_license: 湘ICP备09019772 + # label_license: 湘ICP备09019772 # 项目托管平台 - # 项目托管平台主页 >主旨 + # 项目托管平台主页 > 主旨 label_project_trustie: label_project_trustie_theme: - # 项目托管平台主页 >热门项目栏 + # 项目托管平台主页 > 热门项目栏 lable_hot_projects: Hot Projects label_project_new: New project label_join_project: Join a project @@ -1827,7 +1875,7 @@ en: label_project_score_tips: "Considering all activities of the project, project's score reflects the activity level of project" label_project_score: Score - # 项目托管平台主页 >用户动态栏 + # 项目托管平台主页 > 用户动态栏 lable_user_active: User Movements field_user_active_published: released field_user_active_uploaded: uploaded @@ -1854,7 +1902,7 @@ en: # "缺陷 #1869 (已解决):subject" # tracker.name和status在数据库中以中文字段形式存储 - # 项目托管平台主页 >贴吧动态栏 + # 项目托管平台主页 > 贴吧动态栏 lable_bar_active: Bar Posts label_my_question: My-question label_my_feedback: My-feedback @@ -1864,7 +1912,7 @@ en: label_final_reply: Last-reply - # 项目托管平台 >新建项目 + # 项目托管平台 > 新建项目 label_project_new_description: "A project can be used to do anything that requires distributed collaboration." field_name: Name field_description: Description @@ -1876,7 +1924,7 @@ en: button_create: Create - # 项目托管平台 >加入项目 + # 项目托管平台 > 加入项目 @@ -1886,17 +1934,17 @@ en: # 课程托管平台主页 - # 课程托管平台主页 >主旨 + # 课程托管平台主页 > 主旨 label_course_trustie: label_course_trustie_theme: - # 课程托管平台主页 > + # 课程托管平台主页 > # 竞赛托管平台主页 - # 竞赛托管平台主页 >主旨 + # 竞赛托管平台主页 > 主旨 label_contest_trustie: label_contest_trustie_theme: - # 竞赛托管平台主页 > + # 竞赛托管平台主页 > # edit by meng @@ -2062,7 +2110,6 @@ en: modal_valid_passing: can be used. label_company_name: Company Name - notice_account_invalid_creditentials_new: You have not to the mailbox activation label_school_no_course: The school did not offer any courses, you can view other school curriculum label_school_less_course: The school offers courses in less, you can view other school curriculum label_file_not_found: Sorry, the file can't be downloaded now! diff --git a/config/locales/zh.yml b/config/locales/zh.yml index 6afc95979..b7a0dad4f 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -602,7 +602,6 @@ zh: label_information: 信息 label_information_plural: 信息 label_please_login: 请登录 - label_login_with_open_id_option: 或使用OpenID登录 label_home: 主页 label_web_title: 浏览器标题 label_site_title: 网站标题 @@ -690,7 +689,6 @@ zh: label_my_projects: 我的项目 label_my_page_block: 我的工作台模块 label_administration: 管理 - label_login: 登录 label_help: 帮助 label_reported_issues: 已报告的问题 label_assigned_to_me_issues: 指派给我的问题 @@ -2144,13 +2142,13 @@ zh: label_login_prompt: 邮箱/登录名 field_password: 密码 field_identity_url: OpenID URL - label_stay_logged_in: 保持登录状态 - label_password_lost: 忘记密码? + label_stay_logged_in: "保持登录状态" + label_password_lost: "忘记密码?" button_login: 登录 # account_controller中判断用户名或密码输入有误的提示信息 - notice_account_invalid_creditentials: 无效的用户名或密码 + notice_account_invalid_creditentials: "无效的用户名或密码" # account_controller中判断未激活的提示信息 - notice_account_invalid_creditentials_new: 您还未到邮箱激活 + notice_account_invalid_creditentials_new: "您还未到邮箱激活" # Trustie账户模块> 注册 @@ -2161,23 +2159,23 @@ zh: label_register: 注册 label_login_with_open_id_option: 或使用OpenID登录 field_login: 登录名 - label_max_number: 登录名是在网站中显示的您的公开标识,只能为英文和数字。 + label_max_number: "登录名是在网站中显示的您的公开标识,只能为英文和数字。" field_password: 密码 text_caracters_minimum: "至少需要 %{count} 个字符。" field_password_confirmation: 密码确认 field_mail: 邮件地址 - label_mail_attention: qq邮箱可能收不到此邮件,其他邮箱如果没有收到可能在垃圾邮件中, - label_mail_attention1: 其中gmail与教育网邮箱的激活邮件有时比较慢,请耐心等待。 + label_mail_attention: "qq邮箱可能收不到此邮件,其他邮箱如果没有收到可能在垃圾邮件中," + label_mail_attention1: "其中gmail与教育网邮箱的激活邮件有时比较慢,请耐心等待。" button_submit: 提交 # register中js判断密码设置是否合法提示信息 setting_password_min_length_limit: "密码长度至少大于 %{count} 个字符。" - setting_password_error: 密码长度不够或密码不一致 - setting_password_success: 密码设置成功 + setting_password_error: "密码长度不够或密码不一致" + setting_password_success: "密码设置成功" # account_controller中register方法判断注册成功的提示信息 - notice_account_register_done: 帐号创建成功,请使用注册确认邮件中的链接来激活您的帐号, 如果您的邮件没有在收件箱中可能在垃圾箱中,请您注意查收。 + notice_account_register_done: "帐号创建成功,请使用注册确认邮件中的链接来激活您的帐号,如果您的邮件没有在收件箱中可能在垃圾箱中,请您注意查收。" - # Trustie账户模块 >忘记密码 + # Trustie账户模块 > 忘记密码 label_password_forget: 忘记密码 # field_mail: 邮件地址 # (Trustie账户模块> 注册)变量 @@ -2188,7 +2186,7 @@ zh: notice_account_lost_email_sent: 系统已将引导您设置新密码的邮件发送给您。 - # Trustie账户模块 >重置密码 + # Trustie账户模块 > 重置密码 # label_password_forget: 忘记密码 # (Trustie账户模块> 忘记密码)变量 field_new_password: 新密码 @@ -2201,11 +2199,11 @@ zh: button_save: 保存 - # Trustie账户模块 >登出 + # Trustie账户模块 > 登出 label_logout: 退出 - # Trustie账户模块 >激活 + # Trustie账户模块 > 激活 label_regiter_account: 注册帐号 label_email_valid: 邮箱激活 notice_email_register_time: 请在24小时内点击邮件中的链接继续完成注册 @@ -2217,9 +2215,25 @@ zh: - # 托管平台主页 + # Trustie个人主页 + # edit by meng + # Trustie个人主页> + + + + + + + + + + + + + + # Trustie托管平台主页 # edit by meng - # 托管平台主页> 顶部菜单> + # 托管平台主页> 顶部菜单 field_homepage: 主页 label_project_deposit: 项目托管 label_course_practice: 课程实践 @@ -2228,9 +2242,14 @@ zh: label_contest_innovate: 创新竞赛 label_software_user: 软件创客 label_requirement_enterprise: 软件众包 - label_stores_index: 资源搜索 + label_stores_index: 资源搜索 + # 托管平台主页> 顶部菜单> 账户信息 + # (Trustie账户模块> 注册)变量 + # label_register: 注册 + label_login: 登录 + # (Trustie账户模块> 登出)变量 + # label_logout: 退出 - # 托管平台主页> 顶部菜单> @@ -2268,11 +2287,11 @@ zh: # 项目托管平台主页 - # 项目托管平台主页 >主旨 + # 项目托管平台主页 > 主旨 label_project_trustie: Trustie在线项目托管平台 label_project_trustie_theme: ", 面向中国大学生与软件从业者,提供社交化的项目管理、代码托管、资源共享、合作交流。" - # 项目托管平台主页 >热门项目栏 + # 项目托管平台主页 > 热门项目栏 lable_hot_projects: 热门项目 label_project_new: 新建项目 label_join_project: 加入项目 @@ -2281,7 +2300,7 @@ zh: label_project_score_tips: 项目得分,综合考虑了项目的各项活动,反映了该项目的活跃程度 label_project_score: 项目评分 - # 项目托管平台主页 >用户动态栏 + # 项目托管平台主页 > 用户动态栏 lable_user_active: 用户动态 field_user_active_published: 发表了 field_user_active_uploaded: 上传了 @@ -2306,7 +2325,7 @@ zh: # "缺陷 #1869 (已解决):subject" # 而tracker.name和status在数据库中以中文字段形式存储 - # 项目托管平台主页 >用户动态栏 + # 项目托管平台主页 > 用户动态栏 lable_bar_active: 贴吧动态 label_my_question: 我要提问 label_my_feedback: 我要反馈 @@ -2316,7 +2335,7 @@ zh: label_final_reply: 最后回复 - # 项目托管平台 >新建项目 + # 项目托管平台 > 新建项目 label_project_new_description: '项目可以是软件开发项目,也可以是协作研究项目。' field_name: 名称 field_description: 描述 @@ -2327,7 +2346,7 @@ zh: field_hidden_repo: 隐藏代码库 button_create: 提交 - # 项目托管平台 >加入项目 + # 项目托管平台 > 加入项目 @@ -2335,7 +2354,7 @@ zh: # 课程托管平台 - # 课程托管平台主页 >主旨 + # 课程托管平台主页 > 主旨 label_course_trustie: Trustie在线课程实践平台 label_course_trustie_theme: ", 面向中国高校教师与大学生,提供社交化的课程管理、资源共享、合作实验、协同研究。" # 课程托管平台主页 > @@ -2345,7 +2364,7 @@ zh: # 竞赛托管平台 - # 竞赛托管平台主页 >主旨 + # 竞赛托管平台主页 > 主旨 label_contest_trustie: Trustie在线竞赛实战平台 label_contest_trustie_theme: ", 面向中国大学生与编程爱好者,提供社交化的竞赛管理、应用管理、代码托管、合作交流。" # 竞赛托管平台主页 > @@ -2355,7 +2374,7 @@ zh: # 邮件系统 - # 邮件 >激活邮件 + # 邮件 > 激活邮件 # edit by meng # 邮件中文格式 mail_issue_greetings: "亲爱的Trustie用户,您好!" From 7391f3b5cffac583ffdd43ce94ef7d1eb23cbc12 Mon Sep 17 00:00:00 2001 From: alan <547533434@qq.com> Date: Mon, 2 Feb 2015 09:52:05 +0800 Subject: [PATCH 04/68] Merge branch 'szzh' of http://xianbo_trustie2@repository.trustie.net/xianbo/trustie2.git into szzh Conflicts: config/locales/zh.yml Signed-off-by: alan <547533434@qq.com> --- config/database.yml.example | 52 ------------------------------------- 1 file changed, 52 deletions(-) delete mode 100644 config/database.yml.example diff --git a/config/database.yml.example b/config/database.yml.example deleted file mode 100644 index 2ff6231d4..000000000 --- a/config/database.yml.example +++ /dev/null @@ -1,52 +0,0 @@ -# Default setup is given for MySQL with ruby1.9. If you're running Redmine -# with MySQL and ruby1.8, replace the adapter name with `mysql`. -# Examples for PostgreSQL, SQLite3 and SQL Server can be found at the end. -# Line indentation must be 2 spaces (no tabs). - -production: - adapter: mysql2 - database: redmine - host: localhost - username: root - password: "" - encoding: utf8 - -development: - adapter: mysql2 - database: redmine_development - host: 10.107.17.20 - username: root - password: "1234" - encoding: utf8 - -# Warning: The database defined as "test" will be erased and -# re-generated from your development database when you run "rake". -# Do not set this db to the same as development or production. -test: - adapter: mysql2 - database: redmine_test - host: 10.107.17.20 - username: root - password: "1234" - encoding: utf8 - -# PostgreSQL configuration example -#production: -# adapter: postgresql -# database: redmine -# host: localhost -# username: postgres -# password: "postgres" - -# SQLite3 configuration example -#production: -# adapter: sqlite3 -# database: db/redmine.sqlite3 - -# SQL Server configuration example -#production: -# adapter: sqlserver -# database: redmine -# host: localhost -# username: jenkins -# password: jenkins From 725b2749e2b14bf4484a39b376e2a251d38c58dc Mon Sep 17 00:00:00 2001 From: lizanle <491823689@qq.com> Date: Mon, 2 Feb 2015 09:52:50 +0800 Subject: [PATCH 05/68] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=B8=80=E4=BA=9B?= =?UTF-8?q?=E5=AF=B9=E4=BB=A3=E7=A0=81=E7=90=86=E8=A7=A3=E7=9A=84=E6=B3=A8?= =?UTF-8?q?=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/acts_as_activity_provider.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/plugins/acts_as_activity_provider/lib/acts_as_activity_provider.rb b/lib/plugins/acts_as_activity_provider/lib/acts_as_activity_provider.rb index 35d168732..5a54187b3 100644 --- a/lib/plugins/acts_as_activity_provider/lib/acts_as_activity_provider.rb +++ b/lib/plugins/acts_as_activity_provider/lib/acts_as_activity_provider.rb @@ -24,6 +24,10 @@ module Redmine module ClassMethods def acts_as_activity_provider(options = {}) + # Time 2015-01-31 13:54:34 + # Author lizanle + # Description mod.included_modules -> array + # 返回mod包含的 modules数组 unless self.included_modules.include?(Redmine::Acts::ActivityProvider::InstanceMethods) cattr_accessor :activity_provider_options send :include, Redmine::Acts::ActivityProvider::InstanceMethods From 8f5e613477b50080d0d3249c2efeb1d3e17629fe Mon Sep 17 00:00:00 2001 From: lizanle <491823689@qq.com> Date: Mon, 2 Feb 2015 09:53:14 +0800 Subject: [PATCH 06/68] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=B8=80=E4=BA=9B?= =?UTF-8?q?=E5=AF=B9=E4=BB=A3=E7=A0=81=E7=90=86=E8=A7=A3=E7=9A=84=E6=B3=A8?= =?UTF-8?q?=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/issue.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/models/issue.rb b/app/models/issue.rb index a7b1a5943..78d20dca1 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -64,7 +64,9 @@ class Issue < ActiveRecord::Base }, :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o}}, :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') } - + # Time 2015-01-31 13:52:53 + # Author lizanle + # Description 将hash传进去 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]}, :author_key => :author_id From aa2590d4519d3924dea381da112b266f166e7403 Mon Sep 17 00:00:00 2001 From: alan <547533434@qq.com> Date: Tue, 3 Feb 2015 10:16:46 +0800 Subject: [PATCH 07/68] Signed-off-by: alan <547533434@qq.com> --- app/models/forum.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/forum.rb b/app/models/forum.rb index e47d18b02..f5581b232 100644 --- a/app/models/forum.rb +++ b/app/models/forum.rb @@ -5,7 +5,7 @@ class Forum < ActiveRecord::Base has_many :memos, :dependent => :destroy, conditions: "parent_id IS NULL" belongs_to :creator, :class_name => "User", :foreign_key => 'creator_id' - after_create :expire_forum_cache + after_create :expire_forum_cache, :send_email after_update :expire_forum_cache before_destroy :expire_forum_cache safe_attributes 'name', @@ -23,7 +23,7 @@ class Forum < ActiveRecord::Base acts_as_taggable scope :by_join_date, order("created_at DESC") - after_create :send_email + #after_create :send_email def reset_counters! self.class.reset_counters!(id) end From a752ade770ae598de3dd4c0eefaa857f63e8e93a Mon Sep 17 00:00:00 2001 From: sw <939547590@qq.com> Date: Tue, 3 Feb 2015 15:02:45 +0800 Subject: [PATCH 08/68] =?UTF-8?q?#1922=20=20=20=E6=B5=8B=E8=AF=95=E7=89=88?= =?UTF-8?q?--=E5=B9=B3=E5=8F=B0=E4=B8=8A=E4=BC=A0=E9=99=84=E4=BB=B6?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E9=99=84=E4=BB=B6=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/attachments/_links.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/attachments/_links.html.erb b/app/views/attachments/_links.html.erb index e604409d3..0c2b0d7c2 100644 --- a/app/views/attachments/_links.html.erb +++ b/app/views/attachments/_links.html.erb @@ -1,7 +1,7 @@
<% for attachment in attachments %>

-

+
<% if options[:length] %> <%= link_to_attachment attachment, :class => 'icon icon-attachment', :download => true,:length => options[:length] -%> From 9708c50899dc5dc6f1594295a8568613f9cd7917 Mon Sep 17 00:00:00 2001 From: z9hang Date: Tue, 3 Feb 2015 15:10:21 +0800 Subject: [PATCH 09/68] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BD=9C=E5=93=81?= =?UTF-8?q?=E8=AF=84=E5=88=86=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/mobile/apis/homeworks.rb | 26 ++++++++++++++++++ app/services/homework_service.rb | 46 +++++++++++++++++++++++++++++--- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/app/api/mobile/apis/homeworks.rb b/app/api/mobile/apis/homeworks.rb index cba295064..7a8c87987 100644 --- a/app/api/mobile/apis/homeworks.rb +++ b/app/api/mobile/apis/homeworks.rb @@ -70,6 +70,32 @@ module Mobile present :status, 0 end + desc "作品打分" + params do + requires :token, type: String + requires :is_teacher, type: String,desc: '是否为教师(匿评作品详情返回的结果中可获取此参数的值)' + requires :is_anonymous_comments, type: String, desc: '是否为匿评(匿评作品详情返回的结果中可获取此参数的值)' + optional :stars_value, type: Integer,desc: '用户给出的评分' + optional :cur_page,type: Integer,desc: '匿评作品详情返回的结果中可获取此参数的值' + optional :cur_type, type: Integer,desc: '匿评作品详情返回的结果中可获取此参数的值' + optional :user_message, type: String, desc: '用户评论' + end + + post ':homework_id/scoring' do + cs_params = { + new_form: params.reject{|k,v| [:token,:is_teacher,:is_anonymous_comments,:stars_value,:cur_page,:cur_type,:homework_id].include?(k)}, + token: params[:token], + is_teacher: params[:is_teacher], + is_anonymous_comments: params[:is_anonymous_comments], + stars_value: params[:stars_value], + cur_page: params[:cur_page], + cur_type: params[:cur_type], + homework_id: params[:homework_id] + } + Homeworks.get_service.add_score_and_jour cs_params,current_user + present :status, 0 + end + end end diff --git a/app/services/homework_service.rb b/app/services/homework_service.rb index a2c063e15..abf89229c 100644 --- a/app/services/homework_service.rb +++ b/app/services/homework_service.rb @@ -141,16 +141,56 @@ class HomeworkService end #作品打分/留言 - def add_score_and_jour params + def add_score_and_jour params,current_user @is_teacher,@is_anonymous_comments,@m_score = params[:is_teacher]=="true",params[:is_anonymous_comments]=="true",params[:stars_value] @cur_page,@cur_type = params[:cur_page] || 1,params[:cur_type] || 5 @homework = HomeworkAttach.find(params[:homework_id]) + comment_status = @homework.bid.comment_status + if @is_anonymous_comments && comment_status != 1 + case comment_status + when 0 + raise '尚未开启匿评!' + when 2 + raise '匿评已结束!' + end + end + if @is_anonymous_comments && ((@m_score.nil? || @m_score.blank?) || !(params[:new_form] && params[:new_form][:user_message] && params[:new_form][:user_message] != "")) + raise '您尚未打分或评论!' + end #保存评分 - @homework.rate(@m_score.to_i,User.current.id,:quality) if @m_score + homework = @homework + is_teacher = @is_teacher ? 1 : 0 + #保存评分@homework.rate(@m_score.to_i,User.current.id,:quality, (@is_teacher ? 1 : 0)) + if @m_score + rate = @homework.rates(:quality).where(:rater_id => current_user.id, :is_teacher_score => is_teacher).first + if rate + rate.stars = @m_score + rate.save! + else + @homework.rates(:quality).new(:stars => @m_score, :rater_id => current_user.id, :is_teacher_score => is_teacher).save! + end + + if homework.is_teacher_score == 0 + if is_teacher == 1 + homework.score = @m_score + homework.is_teacher_score = 1 + else + sql = "SELECT AVG(stars) as stars FROM seems_rateable_rates WHERE rateable_type = 'HomeworkAttach' AND rateable_id = #{homework.id}" + score= HomeworkAttach.find_by_sql(sql).first.stars + homework.score = score + end + else + if is_teacher == 1 + homework.score = @m_score + homework.is_teacher_score = 1 + end + end + homework.save! + end #保存评论 @is_comprehensive_evaluation = @is_teacher ? 1 : (@is_anonymous_comments ? 2 : 3) #判断当前评论是老师评论?匿评?留言 if params[:new_form] && params[:new_form][:user_message] && params[:new_form][:user_message] != "" #有没有留言 - @homework.addjours User.current.id, params[:new_form][:user_message],0,@is_comprehensive_evaluation + @homework.addjours current_user.id, params[:new_form][:user_message],0,@is_comprehensive_evaluation end end From 87e658a0f1367a73afaf5f2e1abd243054a3b0ac Mon Sep 17 00:00:00 2001 From: z9hang Date: Tue, 3 Feb 2015 15:46:17 +0800 Subject: [PATCH 10/68] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E8=AF=84=E8=AE=BA?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=EF=BC=8C=E5=8C=BF=E8=AF=84=E7=BB=93=E6=9D=9F?= =?UTF-8?q?=E5=90=8E=E4=BE=9D=E7=84=B6=E5=8F=AF=E4=BB=A5=E8=AF=84=E8=AE=BA?= =?UTF-8?q?=EF=BC=88=E5=B1=9E=E4=BA=8E=E7=95=99=E8=A8=80=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/homework_service.rb | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/app/services/homework_service.rb b/app/services/homework_service.rb index abf89229c..b468e1110 100644 --- a/app/services/homework_service.rb +++ b/app/services/homework_service.rb @@ -146,13 +146,8 @@ class HomeworkService @cur_page,@cur_type = params[:cur_page] || 1,params[:cur_type] || 5 @homework = HomeworkAttach.find(params[:homework_id]) comment_status = @homework.bid.comment_status - if @is_anonymous_comments && comment_status != 1 - case comment_status - when 0 - raise '尚未开启匿评!' - when 2 - raise '匿评已结束!' - end + if @is_anonymous_comments && comment_status == 0 + raise '尚未开启匿评!' end if @is_anonymous_comments && ((@m_score.nil? || @m_score.blank?) || !(params[:new_form] && params[:new_form][:user_message] && params[:new_form][:user_message] != "")) raise '您尚未打分或评论!' From 8216453a027f9486229244ab42a51a18f3642821 Mon Sep 17 00:00:00 2001 From: z9hang Date: Tue, 3 Feb 2015 16:03:30 +0800 Subject: [PATCH 11/68] =?UTF-8?q?=E5=8C=BF=E8=AF=84=E4=BD=9C=E5=93=81?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E8=BF=94=E5=9B=9E=E7=BB=93=E6=9E=9C=E4=B8=AD?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0comment=5Fstatus=E5=AD=97=E6=AE=B5=200:?= =?UTF-8?q?=E6=89=80=E5=B1=9E=E4=BD=9C=E4=B8=9A=E5=B0=9A=E6=9C=AA=E5=BC=80?= =?UTF-8?q?=E5=90=AF=E5=8C=BF=E8=AF=84=EF=BC=8C1=EF=BC=9A=E5=8C=BF?= =?UTF-8?q?=E8=AF=84=E4=B8=AD=202=EF=BC=9A=E5=8C=BF=E8=AF=84=E7=BB=93?= =?UTF-8?q?=E6=9D=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/mobile/entities/homework_attach.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/api/mobile/entities/homework_attach.rb b/app/api/mobile/entities/homework_attach.rb index 256dcdf61..690ec8e94 100644 --- a/app/api/mobile/entities/homework_attach.rb +++ b/app/api/mobile/entities/homework_attach.rb @@ -17,6 +17,8 @@ module Mobile case field when :homework_times f.bid.courses.first.homeworks.index(f.bid) + 1 unless (f.bid.nil? || f.bid.courses.nil? || f.bid.courses.first.nil?) + when :comment_status + f.bid.comment_status end end end @@ -28,6 +30,8 @@ module Mobile homework_attach_expose :homework_times homework_attach_expose :description homework_attach_expose :created_at + #comment_status 0:所属作业尚未开启匿评,1:匿评中 2:匿评结束 + homework_attach_expose :comment_status expose :attachments,using: Mobile::Entities::Attachment do |f, opt| if f.respond_to?(:attachments) f.send(:attachments) From 6d79c2cf83f6322c35df3ce0b4c502876356b70a Mon Sep 17 00:00:00 2001 From: z9hang Date: Wed, 4 Feb 2015 11:57:37 +0800 Subject: [PATCH 12/68] =?UTF-8?q?=E6=89=8B=E6=9C=BA=E7=AB=AF=EF=BC=A1?= =?UTF-8?q?=EF=BC=B0=EF=BC=A9=E6=B7=BB=E5=8A=A0=E6=96=B0=E9=97=BB=E8=AF=84?= =?UTF-8?q?=E8=AE=BA=E5=AE=9E=E4=BD=93Comment=EF=BC=8C=E6=96=B0=E9=97=BB?= =?UTF-8?q?=E5=AE=9E=E4=BD=93=E4=B8=AD=E5=BC=95=E7=94=A8Comment,=E8=AF=BE?= =?UTF-8?q?=E7=A8=8B=E6=96=B0=E9=97=BB=E5=88=97=E8=A1=A8=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=9D=83=E9=99=90=E5=88=A4=E6=96=AD=EF=BC=8C=E5=B9=B6=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=E8=AF=A5=E6=8E=A5=E5=8F=A3=E4=B8=8D=E5=8F=AF=E7=94=A8?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/mobile/apis/comments.rb | 22 ++++++++++++++++++++++ app/api/mobile/apis/courses.rb | 3 ++- app/api/mobile/entities/comment.rb | 30 ++++++++++++++++++++++++++++++ app/api/mobile/entities/news.rb | 6 +++++- app/services/comment_service.rb | 12 ++++++++++++ app/services/courses_service.rb | 7 +++++-- 6 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 app/api/mobile/apis/comments.rb create mode 100644 app/api/mobile/entities/comment.rb create mode 100644 app/services/comment_service.rb diff --git a/app/api/mobile/apis/comments.rb b/app/api/mobile/apis/comments.rb new file mode 100644 index 000000000..fb6d01fb4 --- /dev/null +++ b/app/api/mobile/apis/comments.rb @@ -0,0 +1,22 @@ +#coding=utf-8 +module Mobile + module Apis + class Comments < Grape::API + resource :comments do + desc '课程通知评论' + params do + requires :token, type: String + requires :comment, type: String + end + post ':id' do + cs = CommentService.new + comments = cs.news_comments params,current_user + raise "create comments failed #{comments.errors.full_messages}" if comments.new_record? + present :data, comments, with: Mobile::Entities::Comment + present :status, 0 + end + + end + end + end +end \ No newline at end of file diff --git a/app/api/mobile/apis/courses.rb b/app/api/mobile/apis/courses.rb index 7c1aa5e7e..2e678bad4 100644 --- a/app/api/mobile/apis/courses.rb +++ b/app/api/mobile/apis/courses.rb @@ -188,10 +188,11 @@ module Mobile desc "课程通知列表" params do + optional :token, type: String end get ":course_id/news" do cs = CoursesService.new - news = cs.course_news_list params + news = cs.course_news_list params,current_user.nil? ? User.find(2):current_user present :data, news, with: Mobile::Entities::News present :status, 0 end diff --git a/app/api/mobile/entities/comment.rb b/app/api/mobile/entities/comment.rb new file mode 100644 index 000000000..803d0c6d0 --- /dev/null +++ b/app/api/mobile/entities/comment.rb @@ -0,0 +1,30 @@ +module Mobile + module Entities + class Comment < Grape::Entity + include Redmine::I18n + def self.comment_expose(field) + expose field do |f,opt| + if f.is_a?(Hash) && f.key?(field) + f[field] + elsif f.is_a?(::Comment) + if f.respond_to?(field) + if field == :created_on + format_time(f.send(field)) + else + f.send(field) + end + end + end + end + end + comment_expose :id + expose :author, using: Mobile::Entities::User do |c, opt| + if c.is_a? ::Comment + c.author + end + end + comment_expose :comments + comment_expose :created_on + end + end +end \ No newline at end of file diff --git a/app/api/mobile/entities/news.rb b/app/api/mobile/entities/news.rb index 7c77f8c82..406db59e4 100644 --- a/app/api/mobile/entities/news.rb +++ b/app/api/mobile/entities/news.rb @@ -34,7 +34,11 @@ module Mobile #评论数量 news_expose :comments_count #评论 - news_expose :comments + expose :comments, using: Mobile::Entities::Comment do |f, opt| + if f.is_a?(Hash) && f.key?(:comments) + f[:comments] + end + end end diff --git a/app/services/comment_service.rb b/app/services/comment_service.rb new file mode 100644 index 000000000..b6727a941 --- /dev/null +++ b/app/services/comment_service.rb @@ -0,0 +1,12 @@ +class CommentService + #评论 + def news_comments params,current_user + raise Unauthorized unless @news.commentable? + @news = News.find(params[:id]) + @comment = Comment.new + @comment.safe_attributes = params[:comment] + @comment.author = current_user + @news.comments << @comment + @comment + end +end \ No newline at end of file diff --git a/app/services/courses_service.rb b/app/services/courses_service.rb index 41b6f2c68..afd674610 100644 --- a/app/services/courses_service.rb +++ b/app/services/courses_service.rb @@ -106,11 +106,14 @@ class CoursesService end #课程通知列表 - def course_news_list params + def course_news_list params,current_user if params[:course_id] && @course==nil @course = Course.find(params[:course_id]) end - scope = @course ? @course.news.course_visible : News.course_visible + if current_user.nil? || !(current_user.admin? || @course.is_public == 1 || (@course.is_public == 0 && current_user.member_of_course?(@course))) + raise '403' + end + scope = @course ? @course.news.course_visible(current_user) : News.course_visible(current_user) news = [] scope.each do |n| news << {:title => n.title,:author_name => n.author.name,:author_id => n.author.id, :description => n.description,:created_on => format_time(n.created_on),:comments_count => n.comments_count} From 3cd97f8a7013c7e7f407b60d4e1b24cf60710d46 Mon Sep 17 00:00:00 2001 From: z9hang Date: Wed, 4 Feb 2015 13:47:52 +0800 Subject: [PATCH 13/68] =?UTF-8?q?API=E6=B7=BB=E5=8A=A0comment=E6=A8=A1?= =?UTF-8?q?=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/mobile/api.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/api/mobile/api.rb b/app/api/mobile/api.rb index bee78a20e..bad8c1a77 100644 --- a/app/api/mobile/api.rb +++ b/app/api/mobile/api.rb @@ -6,6 +6,7 @@ module Mobile require_relative 'apis/watches' require_relative 'apis/upgrade' require_relative 'apis/homeworks' + require_relative 'apis/comments' class API < Grape::API version 'v1', using: :path format :json @@ -37,6 +38,7 @@ module Mobile mount Apis::Watches mount Apis::Upgrade mount Apis::Homeworks + mount Apis::Comment #add_swagger_documentation ({api_version: 'v1', base_path: 'http://u06.shellinfo.cn/trustie/api'}) #add_swagger_documentation ({api_version: 'v1', base_path: '/api'}) if Rails.env.development? From 4fdc1b97a5022185d322c32a92fecab8d4594721 Mon Sep 17 00:00:00 2001 From: z9hang Date: Wed, 4 Feb 2015 14:07:00 +0800 Subject: [PATCH 14/68] =?UTF-8?q?=E9=85=8D=E7=BD=AEAPI=20comment=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=EF=BC=8C=E4=BF=AE=E6=AD=A3=E6=98=BE=E7=A4=BA=E8=AF=BE?= =?UTF-8?q?=E7=A8=8B=E9=80=9A=E7=9F=A5=E6=8E=A5=E5=8F=A3=E6=9C=AA=E5=88=A4?= =?UTF-8?q?=E6=96=AD=E6=9D=83=E9=99=90=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/mobile/api.rb | 2 +- app/api/mobile/apis/courses.rb | 2 +- app/services/comment_service.rb | 2 +- app/services/courses_service.rb | 9 +++++++++ 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/api/mobile/api.rb b/app/api/mobile/api.rb index bad8c1a77..8f7f0342f 100644 --- a/app/api/mobile/api.rb +++ b/app/api/mobile/api.rb @@ -38,7 +38,7 @@ module Mobile mount Apis::Watches mount Apis::Upgrade mount Apis::Homeworks - mount Apis::Comment + mount Apis::Comments #add_swagger_documentation ({api_version: 'v1', base_path: 'http://u06.shellinfo.cn/trustie/api'}) #add_swagger_documentation ({api_version: 'v1', base_path: '/api'}) if Rails.env.development? diff --git a/app/api/mobile/apis/courses.rb b/app/api/mobile/apis/courses.rb index 2e678bad4..8f8c08c3f 100644 --- a/app/api/mobile/apis/courses.rb +++ b/app/api/mobile/apis/courses.rb @@ -199,7 +199,7 @@ module Mobile desc "显示课程通知" params do - + optional :token, type: String end get "news/:id" do cs = CoursesService.new diff --git a/app/services/comment_service.rb b/app/services/comment_service.rb index b6727a941..23f4355e6 100644 --- a/app/services/comment_service.rb +++ b/app/services/comment_service.rb @@ -1,8 +1,8 @@ class CommentService #评论 def news_comments params,current_user - raise Unauthorized unless @news.commentable? @news = News.find(params[:id]) + raise Unauthorized unless @news.commentable? @comment = Comment.new @comment.safe_attributes = params[:comment] @comment.author = current_user diff --git a/app/services/courses_service.rb b/app/services/courses_service.rb index afd674610..77b5e325d 100644 --- a/app/services/courses_service.rb +++ b/app/services/courses_service.rb @@ -131,9 +131,18 @@ class CoursesService #显示课程通知(包括评论) 需验证权限 def show_course_news params,current_user @news = News.find(params[:id]) + @course = @news.course + if @course + if current_user.nil? || !(current_user.admin? || @course.is_public == 1 || (@course.is_public == 0 && current_user.member_of_course?(@course))) + raise '403' + end + else + raise 'news in unknown course' + end @comments = @news.comments @comments.reverse! if current_user.wants_comments_in_reverse_order? {:news => @news,:comments => @comments} + #comments = [] #@comments.each do |comment| # comments << {:author_id => comment.author_id,:author_name => comment.author.name,:commont_content => comment.comments,:time => format_time(comment.created_on)} From d6b82c3549c5aaaf4e232d3de38c4380cc8fbfb3 Mon Sep 17 00:00:00 2001 From: z9hang Date: Wed, 4 Feb 2015 14:39:46 +0800 Subject: [PATCH 15/68] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E8=AF=BE=E7=A8=8B?= =?UTF-8?q?=E9=80=9A=E7=9F=A5=E8=AF=84=E8=AE=BA=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/mobile/apis/comments.rb | 7 +++++-- app/services/comment_service.rb | 11 +++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/app/api/mobile/apis/comments.rb b/app/api/mobile/apis/comments.rb index fb6d01fb4..402bce66f 100644 --- a/app/api/mobile/apis/comments.rb +++ b/app/api/mobile/apis/comments.rb @@ -6,11 +6,14 @@ module Mobile desc '课程通知评论' params do requires :token, type: String - requires :comment, type: String + requires :comments, type: String end post ':id' do cs = CommentService.new - comments = cs.news_comments params,current_user + cs_params = { + id: params[:id], + comment: params.reject{|k,v| [:id].include?(k)}} + comments = cs.news_comments cs_params,current_user raise "create comments failed #{comments.errors.full_messages}" if comments.new_record? present :data, comments, with: Mobile::Entities::Comment present :status, 0 diff --git a/app/services/comment_service.rb b/app/services/comment_service.rb index 23f4355e6..c7e4b29a7 100644 --- a/app/services/comment_service.rb +++ b/app/services/comment_service.rb @@ -2,9 +2,16 @@ class CommentService #评论 def news_comments params,current_user @news = News.find(params[:id]) - raise Unauthorized unless @news.commentable? + @course = @news.course + if @course.nil? + raise 'news in unknown course' + end + raise Unauthorized unless @news.commentable?(current_user) + if current_user.nil? || !(current_user.admin? || @course.is_public == 1 || (@course.is_public == 0 && current_user.member_of_course?(@course))) + raise '403' + end @comment = Comment.new - @comment.safe_attributes = params[:comment] + @comment.send(:safe_attributes=,params[:comment],current_user) @comment.author = current_user @news.comments << @comment @comment From d6813e5996cb5a5ed2b0e5b6edf5b28edd219d90 Mon Sep 17 00:00:00 2001 From: z9hang Date: Thu, 5 Feb 2015 15:54:50 +0800 Subject: [PATCH 16/68] =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E6=96=B0=E9=97=BB?= =?UTF-8?q?=E6=8A=A5=E9=94=99=E9=97=AE=E9=A2=98=EF=BC=8C=E4=BD=9C=E4=B8=9A?= =?UTF-8?q?=E7=95=99=E8=A8=80=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/mobile/apis/comments.rb | 19 +++++++++++++++++++ app/services/comment_service.rb | 20 ++++++++++++++++++++ app/services/courses_service.rb | 2 -- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/app/api/mobile/apis/comments.rb b/app/api/mobile/apis/comments.rb index 402bce66f..fb30f59f4 100644 --- a/app/api/mobile/apis/comments.rb +++ b/app/api/mobile/apis/comments.rb @@ -19,6 +19,25 @@ module Mobile present :status, 0 end + desc '作业留言(教师布置的作业)' + params do + requires :token, type: String + requires :id, type: Integer,desc: '老师布置的作业id' + requires :message,type: String, desc: '留言' + optional :reference_content, type: String ,desc: '引用的内容' + optional :reference_user_id, type: Integer,desc: '被引用的人' + end + post 'create_homework_message' do + cs_params = { + id: params[:id], + token: params[:token], + reference_content: params[:reference_content], + bid_message: params.reject{|k,v| [:id,:token,:reference_content].include?(k)}} + cs = CommentService.new + cs.homework_message cs_params,current_user + present :status, 0 + end + end end end diff --git a/app/services/comment_service.rb b/app/services/comment_service.rb index c7e4b29a7..84a7d09cf 100644 --- a/app/services/comment_service.rb +++ b/app/services/comment_service.rb @@ -16,4 +16,24 @@ class CommentService @news.comments << @comment @comment end + + #作业留言 + def homework_message params,current_user + @bid = Bid.find(params[:id], :include => [{:homeworks => :user}]) + if params[:bid_message][:message].size>0 + if params[:reference_content] + message = params[:bid_message][:message] + "\n" + params[:reference_content] + else + message = params[:bid_message][:message] + @m = message + end + refer_user_id = params[:bid_message][:reference_user_id].to_i + @bid.add_jour(current_user, message, refer_user_id) + end + #@user = @bid.author + #@jours = @bid.journals_for_messages.where('m_parent_id IS NULL').order('created_on DESC') + #@jour = paginateHelper @jours,10 + @bid.set_commit(@feedback_count) + end + end \ No newline at end of file diff --git a/app/services/courses_service.rb b/app/services/courses_service.rb index 77b5e325d..341ecd422 100644 --- a/app/services/courses_service.rb +++ b/app/services/courses_service.rb @@ -136,8 +136,6 @@ class CoursesService if current_user.nil? || !(current_user.admin? || @course.is_public == 1 || (@course.is_public == 0 && current_user.member_of_course?(@course))) raise '403' end - else - raise 'news in unknown course' end @comments = @news.comments @comments.reverse! if current_user.wants_comments_in_reverse_order? From ef203ff40c0bf8250bbf3a084c733e2970a18424 Mon Sep 17 00:00:00 2001 From: z9hang Date: Thu, 5 Feb 2015 16:11:35 +0800 Subject: [PATCH 17/68] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E4=BD=9C=E4=B8=9A?= =?UTF-8?q?=E7=95=99=E8=A8=80=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/mobile/apis/comments.rb | 11 ++++++----- app/models/bid.rb | 4 +++- app/services/comment_service.rb | 3 ++- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/app/api/mobile/apis/comments.rb b/app/api/mobile/apis/comments.rb index fb30f59f4..c6d925868 100644 --- a/app/api/mobile/apis/comments.rb +++ b/app/api/mobile/apis/comments.rb @@ -22,19 +22,20 @@ module Mobile desc '作业留言(教师布置的作业)' params do requires :token, type: String - requires :id, type: Integer,desc: '老师布置的作业id' + #requires :id, type: Integer,desc: '老师布置的作业id' requires :message,type: String, desc: '留言' - optional :reference_content, type: String ,desc: '引用的内容' - optional :reference_user_id, type: Integer,desc: '被引用的人' + #optional :reference_content, type: String ,desc: '引用的内容' + #optional :reference_user_id, type: Integer,desc: '被引用的人' end - post 'create_homework_message' do + post ':id/create_homework_message' do cs_params = { id: params[:id], token: params[:token], reference_content: params[:reference_content], bid_message: params.reject{|k,v| [:id,:token,:reference_content].include?(k)}} cs = CommentService.new - cs.homework_message cs_params,current_user + message = cs.homework_message cs_params,current_user + present :data, message, with: Mobile::Entities::Jours present :status, 0 end diff --git a/app/models/bid.rb b/app/models/bid.rb index f423266b8..eb7e5d83f 100644 --- a/app/models/bid.rb +++ b/app/models/bid.rb @@ -93,7 +93,9 @@ class Bid < ActiveRecord::Base # 'deadline' def add_jour(user, notes, reference_user_id = 0, options = {}) if options.count == 0 - self.journals_for_messages << JournalsForMessage.new(:user_id => user.id, :notes => notes, :reply_id => reference_user_id) + jfm = JournalsForMessage.new(:user_id => user.id, :notes => notes, :reply_id => reference_user_id) + self.journals_for_messages << jfm + jfm else jfm = self.journals_for_messages.build(options) jfm.save diff --git a/app/services/comment_service.rb b/app/services/comment_service.rb index 84a7d09cf..5d40bf6bc 100644 --- a/app/services/comment_service.rb +++ b/app/services/comment_service.rb @@ -28,12 +28,13 @@ class CommentService @m = message end refer_user_id = params[:bid_message][:reference_user_id].to_i - @bid.add_jour(current_user, message, refer_user_id) + jfm = @bid.add_jour(current_user, message, refer_user_id) end #@user = @bid.author #@jours = @bid.journals_for_messages.where('m_parent_id IS NULL').order('created_on DESC') #@jour = paginateHelper @jours,10 @bid.set_commit(@feedback_count) + jfm end end \ No newline at end of file From 0dfa42837709576213e1abc7a3f3a6c9d7ee7b43 Mon Sep 17 00:00:00 2001 From: z9hang Date: Thu, 5 Feb 2015 17:32:19 +0800 Subject: [PATCH 18/68] =?UTF-8?q?=E6=89=8B=E6=9C=BA=EF=BC=A1=EF=BC=B0?= =?UTF-8?q?=EF=BC=A9=E6=B7=BB=E5=8A=A0=E5=9B=9E=E5=A4=8D=E7=95=99=E8=A8=80?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/mobile/apis/comments.rb | 20 +++++++++++++++++- app/api/mobile/entities/jours.rb | 3 +++ app/services/comment_service.rb | 35 ++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/app/api/mobile/apis/comments.rb b/app/api/mobile/apis/comments.rb index c6d925868..3f252070f 100644 --- a/app/api/mobile/apis/comments.rb +++ b/app/api/mobile/apis/comments.rb @@ -22,7 +22,6 @@ module Mobile desc '作业留言(教师布置的作业)' params do requires :token, type: String - #requires :id, type: Integer,desc: '老师布置的作业id' requires :message,type: String, desc: '留言' #optional :reference_content, type: String ,desc: '引用的内容' #optional :reference_user_id, type: Integer,desc: '被引用的人' @@ -39,6 +38,25 @@ module Mobile present :status, 0 end + desc '回复留言' + params do + requires :token, type: String + requires :reference_id, type: Integer,desc: '所属留言树的根留言id(最顶层的非回复的留言,留言对象中的m_parent_id)' + requires :reference_user_id,type: Integer ,desc: '被回复的留言的作者id' + #requires :reference_message_id,type: Integer,desc: '被回复的留言的id' + requires :user_notes,type: String,desc: '留言的内容' + requires :jour_type,type: String,desc: '等于父留言的jour_type' + requires :jour_id,type:Integer, desc: '等于父留言的jour_id' + end + post ':reference_message_id/create_reply'do + cs = CommentService.new + message = cs.create_reply params,current_user + raise "create reply failed #{message.errors.full_messages}" if message.new_record? + present :data, message, with: Mobile::Entities::Jours + present :status, 0 + end + + end end end diff --git a/app/api/mobile/entities/jours.rb b/app/api/mobile/entities/jours.rb index 5a9f48cbc..10fd0f893 100644 --- a/app/api/mobile/entities/jours.rb +++ b/app/api/mobile/entities/jours.rb @@ -18,12 +18,15 @@ module Mobile end end jours_expose :id + jours_expose :jour_type + jours_expose :jour_id expose :user,using: Mobile::Entities::User do |f, opt| f.user end jours_expose :created_on jours_expose :notes jours_expose :m_reply_id + jours_expose :m_parent_id expose :reply_user,using: Mobile::Entities::User do |f, opt| f.at_user end diff --git a/app/services/comment_service.rb b/app/services/comment_service.rb index 5d40bf6bc..5a5667a92 100644 --- a/app/services/comment_service.rb +++ b/app/services/comment_service.rb @@ -37,4 +37,39 @@ class CommentService jfm end + #回复留言接口 + def create_reply params,current_user + # 这里是创建回复所使用的方法,此方法只针对回复,每一个新的留言并不在此方法管理范围内。 + # 由于多个地方用到了留言,而之前的表设计也有jour_type/jour_id这类信息 + # 所以在方法 add_reply_adapter 中判断所有调用此方法的来源页面, + # 为了保证兼容以往所有的代码,保证以往的方法可以调用,在返回页面中都做了各式各样的判断。 + # 页面保证 render new_respond/journal_reply + # 修改 add_reply_adapter 中可以确保留言创建成功 + # 删除留言功能要调用destroy,也记得在destroy.js中修改 + + # deny api. api useless + parent_id = params[:reference_id] + author_id = current_user.id + reply_user_id = params[:reference_user_id] + reply_id = params[:reference_message_id] # 暂时不实现 + content = params[:user_notes] + jour_type = params[:jour_type] + jour_id = params[:jour_id] + @show_name = params[:show_name] == "true" + options = { + :jour_id => jour_id, + :jour_type => jour_type, + :user_id => author_id, + :status => true, + :m_parent_id => parent_id, + :m_reply_id => reply_id, + :reply_id => reply_user_id, + :notes => content, + :is_readed => false} + @jfm = ::JournalsForMessage.new(options) + #@save_succ = true if @jfm.errors.empty? + @jfm.save + @jfm + end + end \ No newline at end of file From 95b757ba0901cda67104c94ad465cdc4de3c691d Mon Sep 17 00:00:00 2001 From: sw <939547590@qq.com> Date: Fri, 6 Feb 2015 09:32:32 +0800 Subject: [PATCH 19/68] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E9=97=AE=E5=8D=B7?= =?UTF-8?q?=E5=8F=91=E5=B8=83=E6=88=90=E5=8A=9F=E5=92=8C=E5=8F=96=E6=B6=88?= =?UTF-8?q?=E5=8F=91=E5=B8=83=E6=88=90=E5=8A=9F=E5=90=8E=E5=BC=B9=E6=A1=86?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/poll/_alert.html.erb | 27 +++++++++++++++++++++++++++ app/views/poll/publish_poll.js.erb | 10 +++++++++- app/views/poll/republish_poll.js.erb | 10 +++++++++- config/locales/zh.yml | 1 + public/stylesheets/polls.css | 6 ++++++ 5 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 app/views/poll/_alert.html.erb diff --git a/app/views/poll/_alert.html.erb b/app/views/poll/_alert.html.erb new file mode 100644 index 000000000..b3de53d1f --- /dev/null +++ b/app/views/poll/_alert.html.erb @@ -0,0 +1,27 @@ + + + + + + + +
+
+
+

+ <%= message%> +

+ +
+
+
+
+ + + diff --git a/app/views/poll/publish_poll.js.erb b/app/views/poll/publish_poll.js.erb index ad052f8f2..6074df6c6 100644 --- a/app/views/poll/publish_poll.js.erb +++ b/app/views/poll/publish_poll.js.erb @@ -1,2 +1,10 @@ $("#polls_<%= @poll.id %>").html("<%= escape_javascript(render :partial => 'poll',:locals => {:poll => @poll}) %>"); -alert("发布成功"); \ No newline at end of file +$('#ajax-modal').html("<%= escape_javascript(render :partial => 'alert', locals: { :message => l(:label_memo_create_succ)}) %>"); +showModal('ajax-modal', '180px'); +$('#ajax-modal').css('height','111px'); +$('#ajax-modal').siblings().remove(); +$('#ajax-modal').before("" + + ""); +$('#ajax-modal').parent().removeClass("alert_praise"); +$('#ajax-modal').parent().css("top","").css("left",""); +$('#ajax-modal').parent().addClass("poll_alert_form"); \ No newline at end of file diff --git a/app/views/poll/republish_poll.js.erb b/app/views/poll/republish_poll.js.erb index a2d8e28fa..94369678d 100644 --- a/app/views/poll/republish_poll.js.erb +++ b/app/views/poll/republish_poll.js.erb @@ -1,2 +1,10 @@ $("#polls_<%= @poll.id %>").html("<%= escape_javascript(render :partial => 'poll',:locals => {:poll => @poll}) %>"); -alert("取消成功"); \ No newline at end of file +$('#ajax-modal').html("<%= escape_javascript(render :partial => 'alert', locals: { :message => l(:label_poll_republish_success)}) %>"); +showModal('ajax-modal', '180px'); +$('#ajax-modal').css('height','80px'); +$('#ajax-modal').siblings().remove(); +$('#ajax-modal').before("" + + ""); +$('#ajax-modal').parent().removeClass("alert_praise"); +$('#ajax-modal').parent().css("top","").css("left",""); +$('#ajax-modal').parent().addClass("poll_alert_form"); \ No newline at end of file diff --git a/config/locales/zh.yml b/config/locales/zh.yml index d5c56fe13..31ec29e18 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -2486,6 +2486,7 @@ zh: label_poll_result: 问卷调查_问卷统计 label_answer: 答案: label_poll_answer_valid_result: 以上为有效问答题答案! + label_poll_republish_success: 取消成功 label_answer_total: 总计: label_join_project: 加入项目 label_technical_support: 技术支持: diff --git a/public/stylesheets/polls.css b/public/stylesheets/polls.css index f0d670a1d..a0d64645a 100644 --- a/public/stylesheets/polls.css +++ b/public/stylesheets/polls.css @@ -145,3 +145,9 @@ a:hover.btn_pu{ background:#3cb761;} .polls_title_w { width:330px; overflow: hidden;white-space: nowrap;text-overflow: ellipsis;} .polls_de_grey{ color:#b1b1b1;padding-left: 5px;} .ml5{ margin-left:5px;} + +/******确定弹框***********/ +.poll_alert_form{width:140px;height:180px;position:fixed;z-index:100;left:50%;top:50%;margin:-100px 0 0 -150px; background:#fff; -moz-border-radius:5px; -webkit-border-radius:5px; border-radius:5px; box-shadow:0px 0px 8px #194a81; overflow:auto;} +.polls_alert_btn_box{width: 100%;margin: 0 auto;padding-left: 45px;} +.polls_alert_upload_box{ width:120px; margin:15px auto;} +.polls_alert_box_p{ font-size:14px; padding-left: 45px;padding-top: 10px;} From d902d76ae21d7bc53bca83ef9cabecb29e0a91ae Mon Sep 17 00:00:00 2001 From: lizanle <491823689@qq.com> Date: Fri, 6 Feb 2015 10:15:18 +0800 Subject: [PATCH 20/68] =?UTF-8?q?=E5=B0=86google=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=E6=8D=A2=E4=B8=BA=E4=BA=86=E7=99=BE=E5=BA=A6=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/layouts/_base_footer.html.erb | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/app/views/layouts/_base_footer.html.erb b/app/views/layouts/_base_footer.html.erb index 4716ed25a..0c7cca7d5 100644 --- a/app/views/layouts/_base_footer.html.erb +++ b/app/views/layouts/_base_footer.html.erb @@ -40,18 +40,15 @@
<%= debug(params) if Rails.env.development? %> - + +
From b1903dba116716a2470fcef3fab6d99dd4e9c98e Mon Sep 17 00:00:00 2001 From: z9hang Date: Fri, 6 Feb 2015 10:35:38 +0800 Subject: [PATCH 21/68] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=AF=BE=E7=A8=8B?= =?UTF-8?q?=E7=95=99=E8=A8=80=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/mobile/apis/comments.rb | 16 ++++++++++++++++ app/services/comment_service.rb | 6 ++++++ 2 files changed, 22 insertions(+) diff --git a/app/api/mobile/apis/comments.rb b/app/api/mobile/apis/comments.rb index 3f252070f..a7eea735d 100644 --- a/app/api/mobile/apis/comments.rb +++ b/app/api/mobile/apis/comments.rb @@ -38,6 +38,22 @@ module Mobile present :status, 0 end + desc '课程留言' + params do + requires :token, type: String + requires :course_message,type: String, desc: '留言' + end + post ':id/leave_course_message' do + cs_params = { + id: params[:id], + token: params[:token], + new_form: params.reject{|k,v| [:id,:token].include?(k)}} + cs = CommentService.new + message = cs.leave_course_message cs_params,current_user + present :data, message, with: Mobile::Entities::Jours + present :status, 0 + end + desc '回复留言' params do requires :token, type: String diff --git a/app/services/comment_service.rb b/app/services/comment_service.rb index 5a5667a92..42dc8c415 100644 --- a/app/services/comment_service.rb +++ b/app/services/comment_service.rb @@ -36,6 +36,12 @@ class CommentService @bid.set_commit(@feedback_count) jfm end + #课程留言接口 + def leave_course_message params,current_user + message = params[:new_form][:course_message] + feedback = Course.add_new_jour(current_user, message, params[:id]) + feedback + end #回复留言接口 def create_reply params,current_user From 0b41afa5716eca611d5038218e9b29a00435a0cf Mon Sep 17 00:00:00 2001 From: sw <939547590@qq.com> Date: Fri, 6 Feb 2015 10:51:29 +0800 Subject: [PATCH 22/68] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BD=9C=E4=B8=9A?= =?UTF-8?q?=E5=8C=BF=E8=AF=84=E7=95=8C=E9=9D=A2=EF=BC=8C=E5=B7=A6=E4=BE=A7?= =?UTF-8?q?=E9=99=84=E4=BB=B6=E5=90=8D=E6=98=BE=E7=A4=BA=E6=B7=B7=E4=B9=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/attachments/_links.html.erb | 10 ++++++++-- app/views/memos/show.html.erb | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/views/attachments/_links.html.erb b/app/views/attachments/_links.html.erb index 0c2b0d7c2..989ed776d 100644 --- a/app/views/attachments/_links.html.erb +++ b/app/views/attachments/_links.html.erb @@ -1,7 +1,10 @@
+<% is_float ||= false %> <% for attachment in attachments %>

-

+ <%if is_float%> +
+ <% end%> <% if options[:length] %> <%= link_to_attachment attachment, :class => 'icon icon-attachment', :download => true,:length => options[:length] -%> @@ -9,7 +12,10 @@ <%= link_to_attachment attachment, :class => 'icon icon-attachment', :download => true -%> <% end %> -
+ <%if is_float%> +
+ <% end%> + <% if attachment.is_text? %> <%= link_to image_tag('magnifier.png'), :controller => 'attachments', diff --git a/app/views/memos/show.html.erb b/app/views/memos/show.html.erb index 0330b65e4..995529fe9 100644 --- a/app/views/memos/show.html.erb +++ b/app/views/memos/show.html.erb @@ -67,7 +67,7 @@

<% if @memo.attachments.any?%> <% options = {:author => true, :deletable => @memo.deleted_attach_able_by?(User.current) } %> - <%= render :partial => 'attachments/links', :locals => {:attachments => @memo.attachments, :options => options} %> + <%= render :partial => 'attachments/links', :locals => {:attachments => @memo.attachments, :options => options, :is_float => true} %> <% end %>

@@ -136,7 +136,7 @@

<% if reply.attachments.any?%> <% options = {:author => true, :deletable => reply.deleted_attach_able_by?(User.current) } %> - <%= render :partial => 'attachments/links', :locals => {:attachments => reply.attachments, :options => options} %> + <%= render :partial => 'attachments/links', :locals => {:attachments => reply.attachments, :options => options, :is_float => true} %> <% end %>

From 4cdb3c83ddb69c6334f68e5053d7b5bad642cdca Mon Sep 17 00:00:00 2001 From: sw <939547590@qq.com> Date: Fri, 6 Feb 2015 11:22:29 +0800 Subject: [PATCH 23/68] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E5=9C=A8=E6=8C=87=E5=AE=9A=E8=AF=BE=E7=A8=8B?= =?UTF-8?q?=E5=86=85=E4=BD=9C=E4=B8=9A=E6=95=B0=E9=87=8F=E7=9A=84=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/helpers/api_helper.rb | 24 ++++++++++++++++++++++++ app/helpers/homework_attach_helper.rb | 18 ++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/app/helpers/api_helper.rb b/app/helpers/api_helper.rb index 8ff6f725c..b227a96ab 100644 --- a/app/helpers/api_helper.rb +++ b/app/helpers/api_helper.rb @@ -37,4 +37,28 @@ module ApiHelper end result end + + ######################################################### + #sw + #获取课程未匿评数量 + #param: user => "用户", course_id => "查询的课程ID" + #return: 作业的数量 + ######################################################### + def get_course_anonymous_evaluation user,course_id + course = Course.find course_id + count = 0 + if course + is_teacher = is_course_teacher user,course + if is_teacher #如果是老师,显示学生提交的作业数 + course.homeworks.each do |bid| + count += bid.homeworks.count + end + else #如果是学生,显示未匿评的数量 + course.homeworks.each do |bid| + count += get_student_not_batch_homework_list bid,user + end + end + end + count + end end \ No newline at end of file diff --git a/app/helpers/homework_attach_helper.rb b/app/helpers/homework_attach_helper.rb index c41ba54ee..0b98283ff 100644 --- a/app/helpers/homework_attach_helper.rb +++ b/app/helpers/homework_attach_helper.rb @@ -130,4 +130,22 @@ module HomeworkAttachHelper WHERE homework_attaches.bid_id = #{bid.id} AND homework_evaluations.user_id = #{user.id} ORDER BY m_score DESC") student_batch_homework_list end + + ######################################################### + #sw + #获取学生未进行匿评的数量 + #param: bid => 作业 user => 用户 + #return 指定用户未进行匿评的作业的数量 + #user必须是学生用户 + ####################################################### + def get_student_not_batch_homework_list bid,user + HomeworkAttach.find_by_sql("SELECT * FROM(SELECT homework_attaches.*, + (SELECT AVG(stars) FROM seems_rateable_rates WHERE rateable_type = 'HomeworkAttach' AND rateable_id = homework_attaches.id AND is_teacher_score = 1) AS t_score, + (SELECT AVG(stars) FROM seems_rateable_rates WHERE rateable_type = 'HomeworkAttach' AND rateable_id = homework_attaches.id AND is_teacher_score = 0) AS s_score, + (SELECT stars FROM seems_rateable_rates WHERE rateable_type = 'HomeworkAttach' AND rateable_id = homework_attaches.id AND rater_id = #{user.id} AND is_teacher_score = 0) AS m_score + FROM homework_attaches + INNER JOIN homework_evaluations ON homework_evaluations.homework_attach_id = homework_attaches.id + WHERE homework_attaches.bid_id = #{bid.id} AND homework_evaluations.user_id = #{user.id}) AS table1 + WHERE table1.m_score IS NULL").count + end end \ No newline at end of file From 859889b750f659daf2ee248e03e4cec25e71c56e Mon Sep 17 00:00:00 2001 From: sw <939547590@qq.com> Date: Fri, 6 Feb 2015 11:27:37 +0800 Subject: [PATCH 24/68] =?UTF-8?q?=E7=AE=80=E5=8C=96=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E5=8C=BF=E8=AF=84=E6=95=B0=E9=87=8F=E7=9A=84=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/helpers/homework_attach_helper.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/helpers/homework_attach_helper.rb b/app/helpers/homework_attach_helper.rb index 0b98283ff..72d381b28 100644 --- a/app/helpers/homework_attach_helper.rb +++ b/app/helpers/homework_attach_helper.rb @@ -140,8 +140,6 @@ module HomeworkAttachHelper ####################################################### def get_student_not_batch_homework_list bid,user HomeworkAttach.find_by_sql("SELECT * FROM(SELECT homework_attaches.*, - (SELECT AVG(stars) FROM seems_rateable_rates WHERE rateable_type = 'HomeworkAttach' AND rateable_id = homework_attaches.id AND is_teacher_score = 1) AS t_score, - (SELECT AVG(stars) FROM seems_rateable_rates WHERE rateable_type = 'HomeworkAttach' AND rateable_id = homework_attaches.id AND is_teacher_score = 0) AS s_score, (SELECT stars FROM seems_rateable_rates WHERE rateable_type = 'HomeworkAttach' AND rateable_id = homework_attaches.id AND rater_id = #{user.id} AND is_teacher_score = 0) AS m_score FROM homework_attaches INNER JOIN homework_evaluations ON homework_evaluations.homework_attach_id = homework_attaches.id From cc83b30fc6c230a54182cf4a3c0979f69210db4d Mon Sep 17 00:00:00 2001 From: z9hang Date: Fri, 6 Feb 2015 14:29:42 +0800 Subject: [PATCH 25/68] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E6=90=9C=E7=B4=A2?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E6=8E=A5=E5=8F=A3=EF=BC=8C=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=AF=BE=E7=A8=8B=E5=8A=A8=E6=80=81=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/mobile/apis/courses.rb | 10 ++++++++++ app/api/mobile/apis/users.rb | 18 ++++++++++-------- app/services/courses_service.rb | 8 ++++++++ 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/app/api/mobile/apis/courses.rb b/app/api/mobile/apis/courses.rb index 8f8c08c3f..7b3fc7cd5 100644 --- a/app/api/mobile/apis/courses.rb +++ b/app/api/mobile/apis/courses.rb @@ -209,6 +209,16 @@ module Mobile present :status, 0 end + desc '课程动态' + params do + requires :token, type: String + end + get "course_dynamic/:id" do + cs = CoursesService.new + count = cs.course_dynamic(params,current_user) + present :data, count + present :status, 0 + end end end diff --git a/app/api/mobile/apis/users.rb b/app/api/mobile/apis/users.rb index 69260716e..d780b9db4 100644 --- a/app/api/mobile/apis/users.rb +++ b/app/api/mobile/apis/users.rb @@ -22,13 +22,15 @@ module Mobile desc "显示用户" params do - + requires :id, type: Integer end - get ':id' do - us = UsersService.new - ue = us.show_user params - present :data, ue,with: Mobile::Entities::User - present :status, 0 + route_param :id do + get do + us = UsersService.new + ue = us.show_user params + present :data, ue,with: Mobile::Entities::User + present :status, 0 + end end desc "修改用户" @@ -77,11 +79,11 @@ module Mobile present :status, 0 end - desc "用户搜索" + desc "用户搜索" params do requires :name, type: String, desc: '用户名关键字' end - get 'search' do + get 'search/search_user' do us = UsersService.new user = us.search_user params present :data, user, with: Mobile::Entities::User diff --git a/app/services/courses_service.rb b/app/services/courses_service.rb index 341ecd422..d987514c2 100644 --- a/app/services/courses_service.rb +++ b/app/services/courses_service.rb @@ -2,6 +2,7 @@ class CoursesService include ApplicationHelper include CoursesHelper include HomeworkAttachHelper + include ApiHelper #TODO:尚未整合权限系统 #参数school_id为0或不传时返回所有课程,否则返回对应学校的课程 #参数per_page_count分页功能,每页显示的课程数 @@ -326,6 +327,11 @@ class CoursesService end end + def course_dynamic params,current_user + count = get_course_anonymous_evaluation current_user,params[:id] + count + end + private def show_homework_info course,bid,current_user,is_course_teacher author = bid.author.lastname + bid.author.firstname @@ -361,4 +367,6 @@ class CoursesService :description => description, :homework_state => state,:open_anonymous_evaluation => open_anonymous_evaluation} end + + end \ No newline at end of file From d913f89a82b640bfd9b11a9d3c92cc31919b351b Mon Sep 17 00:00:00 2001 From: z9hang Date: Fri, 6 Feb 2015 15:52:34 +0800 Subject: [PATCH 26/68] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=AF=BE=E7=A8=8B?= =?UTF-8?q?=E5=8A=A8=E6=80=81=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/mobile/apis/courses.rb | 2 +- app/api/mobile/entities/course_dynamic.rb | 17 +++++++++++++++++ app/helpers/api_helper.rb | 5 ++--- app/services/courses_service.rb | 17 ++++++++++++++--- 4 files changed, 34 insertions(+), 7 deletions(-) create mode 100644 app/api/mobile/entities/course_dynamic.rb diff --git a/app/api/mobile/apis/courses.rb b/app/api/mobile/apis/courses.rb index 7b3fc7cd5..7dbfaffb0 100644 --- a/app/api/mobile/apis/courses.rb +++ b/app/api/mobile/apis/courses.rb @@ -216,7 +216,7 @@ module Mobile get "course_dynamic/:id" do cs = CoursesService.new count = cs.course_dynamic(params,current_user) - present :data, count + present :data, count, with: Mobile::Entities::CourseDynamic present :status, 0 end diff --git a/app/api/mobile/entities/course_dynamic.rb b/app/api/mobile/entities/course_dynamic.rb new file mode 100644 index 000000000..11f8c2682 --- /dev/null +++ b/app/api/mobile/entities/course_dynamic.rb @@ -0,0 +1,17 @@ +module Mobile + module Entities + class CourseDynamic < Grape::Entity + def self.course_dynamic_expose(field) + expose field do |c,opt| + c[field] if (c.is_a?(Hash) && c.key?(field)) + end + end + + course_dynamic_expose :course_name + course_dynamic_expose :need_anonymous_comments_count + course_dynamic_expose :student_commit_number + course_dynamic_expose :news_count + course_dynamic_expose :message_count + end + end +end \ No newline at end of file diff --git a/app/helpers/api_helper.rb b/app/helpers/api_helper.rb index b227a96ab..c865a500b 100644 --- a/app/helpers/api_helper.rb +++ b/app/helpers/api_helper.rb @@ -44,8 +44,7 @@ module ApiHelper #param: user => "用户", course_id => "查询的课程ID" #return: 作业的数量 ######################################################### - def get_course_anonymous_evaluation user,course_id - course = Course.find course_id + def get_course_anonymous_evaluation user,course count = 0 if course is_teacher = is_course_teacher user,course @@ -59,6 +58,6 @@ module ApiHelper end end end - count + [count,is_teacher] end end \ No newline at end of file diff --git a/app/services/courses_service.rb b/app/services/courses_service.rb index d987514c2..f4201774a 100644 --- a/app/services/courses_service.rb +++ b/app/services/courses_service.rb @@ -327,9 +327,20 @@ class CoursesService end end - def course_dynamic params,current_user - count = get_course_anonymous_evaluation current_user,params[:id] - count + def course_dynamic(params,current_user) + course = Course.find(params[:id]) + if current_user.nil? || !(current_user.admin? || course.is_public == 1 || (course.is_public == 0 && current_user.member_of_course?(course))) + raise '403' + end + count,is_teacher = get_course_anonymous_evaluation current_user,course + if is_teacher + student_commit_number = count + else + need_anonymous_comments_count = count + end + news_count = course.news.count + message_count = course.journals_for_messages.count + {:course_name => course.name,:need_anonymous_comments_count=>need_anonymous_comments_count,:student_commit_number=>student_commit_number,:news_count=> news_count,:message_count=>message_count} end private From 6f9e2bad63584cdf8301bc32e507aea330632c9f Mon Sep 17 00:00:00 2001 From: alan <547533434@qq.com> Date: Fri, 6 Feb 2015 18:15:08 +0800 Subject: [PATCH 27/68] mail weekly --- Gemfile | 2 +- Gemfile.lock | 3 + app/controllers/courses_controller.rb | 1 + app/controllers/messages_controller.rb | 1 + app/controllers/news_controller.rb | 1 + app/controllers/projects_controller.rb | 2 +- app/models/journals_for_message.rb | 4 +- app/models/mailer.rb | 75 +++- app/models/user.rb | 4 +- .../mailer/send_for_user_activities.html.erb | 337 ++++++++++++++++++ .../mailer/send_for_user_activities.text.erb | 253 +++++++++++++ config/initializers/send_mail.rb | 23 ++ config/locales/zh.yml | 20 +- 13 files changed, 718 insertions(+), 8 deletions(-) create mode 100644 app/views/mailer/send_for_user_activities.html.erb create mode 100644 app/views/mailer/send_for_user_activities.text.erb create mode 100644 config/initializers/send_mail.rb diff --git a/Gemfile b/Gemfile index 037c606c8..1bc9e6629 100644 --- a/Gemfile +++ b/Gemfile @@ -21,7 +21,7 @@ gem 'acts-as-taggable-on', '2.4.1' gem 'spreadsheet' gem 'ruby-ole' #gem 'email_verifier', path: 'lib/email_verifier' - +gem 'rufus-scheduler' group :development do gem 'grape-swagger' gem 'grape-swagger-ui', git: 'https://github.com/guange2015/grape-swagger-ui.git' diff --git a/Gemfile.lock b/Gemfile.lock index 355ca422d..861c49754 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -252,6 +252,8 @@ GEM ruby-ole (1.2.11.7) ruby-openid (2.1.8) rubyzip (1.1.6) + rufus-scheduler (3.0.8) + tzinfo sass (3.3.10) sass-rails (3.2.6) railties (~> 3.2.0) @@ -339,6 +341,7 @@ DEPENDENCIES rspec-rails (= 2.13.1) ruby-ole ruby-openid (~> 2.1.4) + rufus-scheduler sass-rails (~> 3.2.3) seems_rateable! selenium-webdriver (~> 2.42.0) diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb index 61944ca2c..a55abd123 100644 --- a/app/controllers/courses_controller.rb +++ b/app/controllers/courses_controller.rb @@ -5,6 +5,7 @@ class CoursesController < ApplicationController helper :members helper :words + before_filter :authorize1, :only => [:show, :feedback] menu_item :overview menu_item :feedback, :only => :feedback menu_item :homework, :only => :homework diff --git a/app/controllers/messages_controller.rb b/app/controllers/messages_controller.rb index 8d6943f02..d2a253c2a 100644 --- a/app/controllers/messages_controller.rb +++ b/app/controllers/messages_controller.rb @@ -17,6 +17,7 @@ class MessagesController < ApplicationController include ApplicationHelper + before_filter :authorize1, :only => [:show] menu_item :boards default_search_scope :messages before_filter :find_board, :only => [:new, :preview,:edit] diff --git a/app/controllers/news_controller.rb b/app/controllers/news_controller.rb index 2df17d73f..b44e8a348 100644 --- a/app/controllers/news_controller.rb +++ b/app/controllers/news_controller.rb @@ -17,6 +17,7 @@ class NewsController < ApplicationController layout 'base_projects'# by young + before_filter :authorize1, :only => [:show] default_search_scope :news model_object News before_filter :find_model_object, :except => [:new, :create, :index] diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 02bfc9e19..5054fd5c1 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -19,7 +19,7 @@ # Description 封装代码,简化代码,格式化代码, class ProjectsController < ApplicationController layout :select_project_layout - + before_filter :authorize1, :only => [:show] menu_item :overview, :only => :show menu_item :roadmap, :only => :roadmap menu_item :settings, :only => :settings diff --git a/app/models/journals_for_message.rb b/app/models/journals_for_message.rb index c71fbaf47..39618e43b 100644 --- a/app/models/journals_for_message.rb +++ b/app/models/journals_for_message.rb @@ -22,8 +22,8 @@ class JournalsForMessage < ActiveRecord::Base :foreign_key => 'jour_id', :conditions => "#{self.table_name}.jour_type = 'Project' " belongs_to :course, - :foreign_key => 'jour_id', - :conditions => "#{self.table_name}.jour_type = 'Course' " + :foreign_key => 'jour_id' + belongs_to :jour, :polymorphic => true belongs_to :user diff --git a/app/models/mailer.rb b/app/models/mailer.rb index 2cae982a7..85ce46e8b 100644 --- a/app/models/mailer.rb +++ b/app/models/mailer.rb @@ -27,6 +27,80 @@ class Mailer < ActionMailer::Base { :host => Setting.host_name, :protocol => Setting.protocol } end + # author: alan + # 根据用户选择发送个人日报或周报 + # 发送内容: 项目【缺陷,讨论区,新闻】,课程【通知,留言,新闻】, 贴吧, 个人留言 + def send_for_user_activities(user, date_to, days) + date_from = date_to - days.days + + # 生成token用于直接点击登录 + token = Token.new(:user =>user , :action => 'autologin') + token.save + @token = token + + @user_url = url_for(my_account_url(user,:token => @token.value)) + # 查询user参加的项目及课程 + projects = user.projects + courses = user.courses + project_ids = projects.map{|project| project.id}.join(",") + course_ids = courses.map {|course| course.id}.join(",") + + # 查询user的缺陷,包括发布的,跟踪的以及被指派的缺陷 + @issues = Issue.find_by_sql("select DISTINCT i.* from issues i, watchers w + where (i.assigned_to_id = #{user.id} or i.author_id = #{user.id} + or (w.watchable_type = 'Issue' and w.watchable_id = i.id and w.user_id = #{user.id})) + and (i.created_on between #{date_from} and #{date_to}) order by i.created_on desc") + + # 查询课程作业,包括老师发布的作业,以及user提交作业 + @bids ||= [] # 老师发布的作业 + courses.each do |course| + @bids << course.homeworks.where("created_at between #{date_from} and #{date_to} order by i.created_on desc") + end + # user 提交的作业 + @homeworks = HomeworkAttach.where("user_id=#{user.id} and (created_at between #{date_from} and #{date_to})") + + # 查询user在课程。项目中发布的讨论帖子 + messages = Message.find_by_sql("select DISTINCT * from messages where author_id = #{user.id} and (created_on between #{date_from} and #{date_to}) order by i.created_on desc") + @course_messages ||= [] + @project_messages ||= [] + messages.each do |msg| + if msg.project + @project_messages << msg + elsif msg.course + @course_messages << msg + end + end + + # 查询user在课程中发布的通知,项目中发的新闻 + @course_news = News.find_by_sql("select DISTINCT n.* from news n + where n.course_id in (#{course_ids}) + and (created_on between #{date_from} and #{date_to}) order by i.created_on desc") + @project_news = News.find_by_sql("select DISTINCT n.* from news n where n.project_id in (#{project_ids}) + and (created_on between #{date_from} and #{date_to}) order by i.created_on desc") + + # 查询user在课程及个人中留言 + @course_journal_messages = JournalsForMessage.find_by_sql("select DISTINCT * from journals_for_messages where + jour_type='Course' and user_id = #{user.id} + and (created_on between #{date_from} and #{date_to}) order by i.created_on desc") + @user_journal_messages = user.journals_for_messages.where("m_parent_id IS NULL and (created_on between #{date_from} and #{date_to})").order('created_on DESC') + + # 查询课程课件更新 + @attachments ||= [] + courses.each do |course| + @attachments << course.attachments.where("created_on between #{date_from} and #{date_to}").order('created_at DESC') + end + # 查询user新建贴吧或发布帖子 + @forums = Forum.find_by_sql("select DISTINCT * from forums where creator_id = #{user.id} and (created_at between #{date_from} and #{date_to}) order by i.created_on desc") + @memos = Memo.find_by_sql("select DISTINCT m.* from memos m, forums f where (m.author_id = #{user.id} or (m.forum_id = f.id and f.creator_id = #{user.id})) + and (created_at between #{date_from} and #{date_to}) order by i.created_on desc") + if days == 1 + subject = "[ #{user.show_name} : #{date_from} - #{l(:label_day_mail)}]" + else + subject = "[ #{user.show_name} : #{l(:label_week_mail)}]" + end + mail :to => user.mail,:subject => "[ #{user.show_name} : #{l(:notice_successful_create)}]" + + end # 贴吧新建贴吧发送邮件 # example Mailer.forum(forum).deliver def forum_add(forum) @@ -140,7 +214,6 @@ class Mailer < ActionMailer::Base @author = issue.author @issue = issue user = User.find_by_mail(recipients) - token = Token.new(:user =>user , :action => 'autologin') token.save @token = token diff --git a/app/models/user.rb b/app/models/user.rb index 06f59c764..dd06db838 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -24,7 +24,7 @@ class User < Principal DEVELOPER = 3 include Redmine::SafeAttributes - + seems_rateable_rater # Different ways of displaying/sorting users USER_FORMATS = { :firstname_lastname => { @@ -149,7 +149,7 @@ class User < Principal scope :by_join_date, order("created_on DESC") ############################# added by liuping 关注 acts_as_watchable - seems_rateable_rater + has_one :user_extensions,:dependent => :destroy ## end diff --git a/app/views/mailer/send_for_user_activities.html.erb b/app/views/mailer/send_for_user_activities.html.erb new file mode 100644 index 000000000..22b071a1f --- /dev/null +++ b/app/views/mailer/send_for_user_activities.html.erb @@ -0,0 +1,337 @@ + + + + + +
+
+

<%= l(:label_course_overview)%>

+ <% unless @course_news.first.nil? %> +
    + +

    + <%= l(:label_course_news) %> + (<%= @course_news.count %>) +

    + + <% @course_news.each do |course_new|%> +
  • + + [ + + <%= link_to course_new.course.name, [:controller => 'courses', :action => 'show', :id => course_new.course_id, :token => @token.value], + :class=> "wmail_column", + :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + ] + + <%= link_to course_new.author, user_activities_url(course_new.author,:token => @token.value), :class => "wmail_name", + :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + <%= l(:label_project_notice) %> + + <%= link_to course_new.title, [:controller => 'news', :action => 'show', :id => course_new.id,:token => @token.value], + :class => 'wmail_info', + :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + %> + <%= course_new.created_on %> +
  • + <% end %> + +
    +
+ <% end %> + <% if !@bids.first.nil? || !@homeworks.first.nil? %> +
    + +

    <%= l(:label_homework_overview) %>>(<%= @bids.count %>)

    + <% unless @bids.first.nil?%> + <% @bids.each do |bid| %> +
  • + + [ + + <%= link_to bid.courses.first.name, [:controller => 'courses', :action => 'show', :id => bid.courses.first.id, :token => @token.value], + :class=> "wmail_column", + :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + ] + + <%= link_to bid.author, user_activities_url(bid.author,:token => @token.value), :class => "wmail_name", + :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + <%= l(:label_course_homework) %> + + <%= link_to bid.name, course_for_bid(:id => bid.id,:token => @token.value), + :class => 'wmail_info', + :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + %> + <%= bid.created_on %> +
  • + <% end %> + <% end %> + <% unless @homeworks.first.nil? %> + <% @homeworks.each do |homework| %> +
  • + + [ + + <%= link_to homework.bid.courses.first.name, [:controller => 'courses', :action => 'show', :id => homework.bid.courses.first.id, :token => @token.value], + :class=> "wmail_column", + :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + ] + + <%= link_to homework.user, user_activities_url(homework.user,:token => @token.value), :class => "wmail_name", + :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + <%= l(:label_course_submit_homework) %> + + <%= link_to homework.name, course_for_bid(:id => homework.bid.id,:token => @token.value), + :class => 'wmail_info', + :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + %> + <%= homework.created_on %> +
  • + <% end %> + <% end %> + +
    +
+ <% end %> + + <% unless @course_journal_messages.first.nil? %> +
    + +

    + <%= l(:view_course_journals_for_messages) %> + (<%= @course_journal_messages.count %>) +

    + + <% @course_journal_messages.each do |course_journal_message|%> +
  • + + [ + + <%= link_to course_journal_message.course.name, [:controller => 'courses', :action => 'show', :id => course_journal_message.jour_id, :token => @token.value], + :class=> "wmail_column", + :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + ] + + <%= link_to course_journal_message.user, user_activities_url(course_journal_message.user,:token => @token.value), :class => "wmail_name", + :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + <%= l(:label_send_course_journals_for_messages) %> + + <%= link_to course_journal_message.notes, [:controller => 'courses', :action => 'feedback', :id => course_journal_message.jour_id,:token => @token.value], + :class => 'wmail_info', + :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + %> + <%= course_journal_message.created_on %> +
  • + <% end %> + +
    +
+ <% end %> + + <% unless @course_messages.first.nil? %> +
    + +

    + <%= l(:view_borad_course) %> + (<%= @course_journal_messages.count %>) +

    + + <% @course_messages.each do |course_message|%> +
  • + + [ + + <%= link_to course_message.course.name, [:controller => 'courses', :action => 'show', :id => course_message.course.id, :token => @token.value], + :class=> "wmail_column", + :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + ] + + <%= link_to course_message.author, user_activities_url(course_message.author,:token => @token.value), :class => "wmail_name", + :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + <%= l(:label_send_course_messages) %> + + <%= link_to course_message.subject,url_for(course_message.event_url(:token => @token.value)), + :class => 'wmail_info', + :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + %> + <%= course_message.created_on %> +
  • + <% end %> + +
    +
+ <% end %> + + <% unless @attachments.first.nil? %> +
    +

    + <%= l(:label_course_attendingcontestwork_download) %> + (<%= @attachments.count %>) +

    + + <% @attachments.each do |attachment|%> +
  • + + [ + + <%= link_to attachment.course.name, [:controller => 'courses', :action => 'show', :id => attachment.course.id, :token => @token.value], + :class=> "wmail_column", + :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + ] + + <%= link_to attachment.author, user_activities_url(attachment.author,:token => @token.value), :class => "wmail_name", + :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + <%= l(:label_course_file_upload) %> + + <%= link_to attachment.filename,course_files_path(@course,:token => @token.value), + :class => 'wmail_info', + :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + %> + <%= course_message.created_on %> +
  • + <% end %> + +
+ <% end %> +
+ + +
+

<%= l(:label_project_overview)%>

+ <% unless @issues.first.nil? %> +
    +

    + <%= l(:label_issue_tracking) %> + (<%= @issues.count %>) +

    + + <% @issues.each do |issue|%> +
  • + + [ + + <%= link_to issue.project.name, [:controller => 'projects', :action => 'show', :id => issue.project.id, :token => @token.value], + :class=> "wmail_column", + :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + ] + + <%= link_to issue.author, user_activities_url(issue.author,:token => @token.value), :class => "wmail_name", + :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + <%= l(:label_project_issue) %> + + <%= link_to issue.subject,url_for(:controller => 'issues', :action => 'show', :id => issue.id, :token => @token.value), + :class => 'wmail_info', + :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + %> + <%= issue.created_on %> +
  • + <% end %> + +
    +
+ <% end %> + + <% unless @project_messages.first.nil? %> +
    +

    + <%= l(:project_moule_boards_show) %> + (<%= @course_journal_messages.count %>) +

    + + <% @project_messages.each do |project_message|%> +
  • + + [ + + <%= link_to project_message.project.name, [:controller => 'projects', :action => 'show', :id => project_message.project.id, :token => @token.value], + :class=> "wmail_column", + :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + ] + + <%= link_to project_message.author, user_activities_url(project_message.author,:token => @token.value), :class => "wmail_name", + :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + <%= l(:label_send_course_messages) %> + + <%= link_to project_message.subject,url_for(project_message.event_url(:token => @token.value)), + :class => 'wmail_info', + :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + %> + <%= project_message.created_on %> +
  • + <% end %> + +
    +
+ <% end %> + + + +
+ +
+

<%= l(:label_activities) %>

+ <% unless @user_journal_messages.first.nil? %> +
    +

    + <%= l(:label_user_message) %> + (<%= @user_journal_messages.count %>) +

    + + <% @user_journal_messages.each do |user_journal_message|%> +
  • + + + <%= link_to user_journal_message.user, user_activities_url(course_journal_message.user,:token => @token.value), + :class => "wmail_name", + :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + <%= l(:label_show_your_message) %> + + <%= link_to user_journal_message.notes, [:controller => 'courses', :action => 'feedback', :id => course_journal_message.jour_id,:token => @token.value], + :class => 'wmail_info', + :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + %> + <%= user_journal_message.created_on %>
  • + + <% end %> + +
    +
+ <% end %> +
+
+ <%= link_to l(:mail_footer), @user_url, :style => "margin-top:20px;color:#2775d2; margin-left:10px;" %> +
+ + +
+ diff --git a/app/views/mailer/send_for_user_activities.text.erb b/app/views/mailer/send_for_user_activities.text.erb new file mode 100644 index 000000000..03ea31ec3 --- /dev/null +++ b/app/views/mailer/send_for_user_activities.text.erb @@ -0,0 +1,253 @@ +<%= l(:label_course_overview)%> + <% unless @course_news.first.nil? %> + <%= l(:label_course_news) %> + (<%= @course_news.count %>) + + + <% @course_news.each do |course_new|%> + + ▪ + [ + + <%= link_to course_new.course.name, [:controller => 'courses', :action => 'show', :id => course_new.course_id, :token => @token.value], + :class=> "wmail_column", + :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + ] + + <%= link_to course_new.author, user_activities_url(course_new.author,:token => @token.value), :class => "wmail_name", + :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + <%= l(:label_project_notice) %> + + <%= link_to course_new.title, [:controller => 'news', :action => 'show', :id => course_new.id,:token => @token.value], + :class => 'wmail_info', + :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + %> <%= course_new.created_on %> + + <% end %> + + <% end %> + <% if !@bids.first.nil? || !@homeworks.first.nil? %> + <%= l(:label_homework_overview) %><%= @bids.count %> + <% unless @bids.first.nil?%> + <% @bids.each do |bid| %> + ▪ + [ + + <%= link_to bid.courses.first.name, [:controller => 'courses', :action => 'show', :id => bid.courses.first.id, :token => @token.value], + :class=> "wmail_column", + :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + ] + + <%= link_to bid.author, user_activities_url(bid.author,:token => @token.value), :class => "wmail_name", + :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + <%= l(:label_course_homework) %> + + <%= link_to bid.name, course_for_bid(:id => bid.id,:token => @token.value), + :class => 'wmail_info', + :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + %> + <%= bid.created_on %> + + <% end %> + <% end %> + <% unless @homeworks.first.nil? %> + <% @homeworks.each do |homework| %> + ▪[ + + <%= link_to homework.bid.courses.first.name, [:controller => 'courses', :action => 'show', :id => homework.bid.courses.first.id, :token => @token.value], + :class=> "wmail_column", + :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + ] + + <%= link_to homework.user, user_activities_url(homework.user,:token => @token.value), :class => "wmail_name", + :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + <%= l(:label_course_submit_homework) %> + + <%= link_to homework.name, course_for_bid(:id => homework.bid.id,:token => @token.value), + :class => 'wmail_info', + :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + %> + <%= homework.created_on %> + <% end %> + <% end %> + + + + <% end %> + + <% unless @course_journal_messages.first.nil? %> + + <%= l(:view_course_journals_for_messages) %> (<%= @course_journal_messages.count %>) + + + <% @course_journal_messages.each do |course_journal_message|%> + + [ + + <%= link_to course_journal_message.course.name, [:controller => 'courses', :action => 'show', :id => course_journal_message.jour_id, :token => @token.value], + :class=> "wmail_column", + :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + ] + + <%= link_to course_journal_message.user, user_activities_url(course_journal_message.user,:token => @token.value), :class => "wmail_name", + :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + <%= l(:label_send_course_journals_for_messages) %> + + <%= link_to course_journal_message.notes, [:controller => 'courses', :action => 'feedback', :id => course_journal_message.jour_id,:token => @token.value], + :class => 'wmail_info', + :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + %> + <%= course_journal_message.created_on %> + + <% end %> + + + <% end %> + + <% unless @course_messages.first.nil? %> + + <%= l(:view_borad_course) %> + (<%= @course_journal_messages.count %>) + + + <% @course_messages.each do |course_message|%> + + ▪ + [ + + <%= link_to course_message.course.name, [:controller => 'courses', :action => 'show', :id => course_message.course.id, :token => @token.value], + :class=> "wmail_column", + :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + ] + + <%= link_to course_message.author, user_activities_url(course_message.author,:token => @token.value), :class => "wmail_name", + :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + <%= l(:label_send_course_messages) %> + + <%= link_to course_message.subject,url_for(course_message.event_url(:token => @token.value)), + :class => 'wmail_info', + :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + %> + <%= course_message.created_on %> + + <% end %> + + + <% end %> + + <% unless @attachments.first.nil? %> + + <%= l(:label_course_attendingcontestwork_download) %> + (<%= @attachments.count %>) + + + <% @attachments.each do |attachment|%> + ▪[ + + <%= link_to attachment.course.name, [:controller => 'courses', :action => 'show', :id => attachment.course.id, :token => @token.value], + :class=> "wmail_column", + :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + ] + + <%= link_to attachment.author, user_activities_url(attachment.author,:token => @token.value), :class => "wmail_name", + :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + <%= l(:label_course_file_upload) %> + + <%= link_to attachment.filename,course_files_path(@course,:token => @token.value), + :class => 'wmail_info', + :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + %> + <%= course_message.created_on %> + + <% end %> + + + <% end %> +
+ + +<%= l(:label_project_overview)%> + <% unless @issues.first.nil? %> + + <%= l(:label_issue_tracking) %> + (<%= @issues.count %>) + + <% @issues.each do |issue|%> + ▪ + [ + + <%= link_to issue.project.name, [:controller => 'projects', :action => 'show', :id => issue.project.id, :token => @token.value], + :class=> "wmail_column", + :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + ] + + <%= link_to issue.author, user_activities_url(issue.author,:token => @token.value), :class => "wmail_name", + :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + <%= l(:label_project_issue) %> + + <%= link_to issue.subject,url_for(:controller => 'issues', :action => 'show', :id => issue.id, :token => @token.value), + :class => 'wmail_info', + :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + %> + <%= issue.created_on %> + <% end %> + + + <% end %> + + <% unless @project_messages.first.nil? %> + + <%= l(:project_moule_boards_show) %> + (<%= @course_journal_messages.count %>) + + <% @project_messages.each do |project_message|%> + ▪[ + + <%= link_to project_message.project.name, [:controller => 'projects', :action => 'show', :id => project_message.project.id, :token => @token.value], + :class=> "wmail_column", + :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + ] + + <%= link_to project_message.author, user_activities_url(project_message.author,:token => @token.value), :class => "wmail_name", + :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + <%= l(:label_send_course_messages) %> + + <%= link_to project_message.subject,url_for(project_message.event_url(:token => @token.value)), + :class => 'wmail_info', + :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + %> + <%= project_message.created_on %> + <% end %> + + + <% end %> + + + +<%= l(:label_activities) %> + <% unless @user_journal_messages.first.nil? %> + + <%= l(:label_user_message) %> + (<%= @user_journal_messages.count %>) + + <% @user_journal_messages.each do |user_journal_message|%> + ▪ + + <%= link_to user_journal_message.user, user_activities_url(course_journal_message.user,:token => @token.value), + :class => "wmail_name", + :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + <%= l(:label_show_your_message) %> + + <%= link_to user_journal_message.notes, [:controller => 'courses', :action => 'feedback', :id => course_journal_message.jour_id,:token => @token.value], + :class => 'wmail_info', + :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + %> + <%= user_journal_message.created_on %> + + <% end %> + + + <% end %> +< + + <%= link_to l(:mail_footer), @user_url, :style => "margin-top:20px;color:#2775d2; margin-left:10px;" %> diff --git a/config/initializers/send_mail.rb b/config/initializers/send_mail.rb new file mode 100644 index 000000000..4cfd3cc03 --- /dev/null +++ b/config/initializers/send_mail.rb @@ -0,0 +1,23 @@ +#!/usr/bin/env ruby + +require 'rubygems' +require 'rufus-scheduler' + +#users = User.where("mail_notification = 'week' or mail_notification = 'day'") + +scheduler = Rufus::Scheduler.new +scheduler.cron('*/1 * * * *') do + users = User.where("login like '%alan%'") + users.each do |user| + # if user.mail_notification == "week" + # cycle = '*/1 * * * *' + # else + # cycle = '*/2 * * * *' + # end + Rails.logger.info "send mail to #{user.show_name}(#{user.mail}) at #{Time.now}" + Thread.start do + Mailer.send_for_user_activities(user, Date.today, 7).deliver + end + end +end + diff --git a/config/locales/zh.yml b/config/locales/zh.yml index 42a1143ec..8899256a9 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -248,6 +248,9 @@ zh: # end field_name: 名称 field_enterprise_name: 组织名称 + + label_week_mail: 一周动态 + label_day_mail: 一日动态 #added by huang field_tea_name: 教师 field_couurse_time: 学时 @@ -497,6 +500,9 @@ zh: permission_paret_in_homework: 加入作业 permission_view_homework_attaches: 查看作业附件 permission_view_course_journals_for_messages: 查看课程留言 + view_course_journals_for_messages: 课程留言 + label_send_course_journals_for_messages: 发布了留言 + label_send_course_messages: 发布了讨论 permission_select_course_modules: 选择课程模块 permission_view_course_files: 查看课程资源 permission_add_course: 新建课程 @@ -511,6 +517,7 @@ zh: permission_upload_attachments: 资源上传 project_module_issue_tracking: 问题跟踪 + project_moule_boards_show: 项目论坛 project_module_time_tracking: 时间跟踪 project_module_news: 新闻 project_module_documents: 文档 @@ -657,6 +664,8 @@ zh: label_user_login_attending_contest: 您还没有登录,请登录后参赛 label_user_login_score_and_comment: 您还没有登录,请登录后对作品进行打分评价 label_user_login_notificationcomment: 您还没有登录,请登录后参加评论 + label_user_message: 您的留言 + label_show_your_message: 给您的留言 #end #by huang # modified by bai label_college: 高校进入 @@ -727,6 +736,7 @@ zh: label_attachment: 文件 label_attachment_new: 新建文件 label_file_upload: 上传资料 + label_course_file_upload: 上传了课件 label_attachment_delete: 删除文件 label_attachment_plural: 文件 label_file_added: 文件已添加 @@ -745,6 +755,8 @@ zh: label_settings: 配置 label_overview: 近期动态 label_course_overview: "课程动态" + label_project_overview: "项目动态" + label_homework_overview: 作业动态 label_question_student: 作业交流 #bai label_homework_commit: 提交作业 #huang label_homework_info: 提交情况 #huang @@ -992,6 +1004,7 @@ zh: label_project_newother: "查看其他评论" label_project_newshare: "分享了" label_project_notice: "发布了通知:" + label_project_issue: "发布了问题:" label_project_newadd: "添加了" label_project_unadd: "暂无项目,赶快去创建吧!" label_project_un: "该用户暂未参与任何项目!" @@ -1591,6 +1604,7 @@ zh: label_exist_repository_path: 定义已有版本库URL路径,定义格式file://, http://, https://, svn:// label_project_no_activity: 该项目暂无动态! label_course_homework_un: 暂未发布任何作业 + label_course_homework: 发布了作业 label_follow_no_requirement: 暂未关注任何需求! label_no_user_respond_you: 暂无任何用户对您进行反馈! label_tags_issue: 问题名称: @@ -1662,6 +1676,7 @@ zh: label_project_no_follow: 该项目暂未被关注! label_no_bid_project: 暂无参与项目 label_no_course_project: 暂无已提交的作业! + label_course_submit_homework: 提交了作业 label_bids_reward_method: 奖励方式 : label_bids_reward_what: 输入奖励内容 label_call_bonus: 奖金 @@ -1769,6 +1784,7 @@ zh: label_wiki_number: wiki的数量 label_message_number: 留言的数量 label_activity_number: 个人动态数量 + label_activities: 个人动态 label_issue_message_number: 对issue的留言数量 label_code_submit_number: 代码提交次数 label_topic_number: 讨论区发言数量 @@ -1954,6 +1970,7 @@ zh: label_hot_project: '热门项目' label_borad_project: 项目讨论区 label_borad_course: 课程讨论区 + view_borad_course: 课程讨论 label_memo_create_succ: 发布成功 label_memo_create_fail: 发布失败 label_forum_create_succ: 贴吧新建成功 @@ -2117,6 +2134,7 @@ zh: label_attendingcontestwork_release_person: 发布人员 label_attendingcontestwork_adaptive_system: 系统支持 label_attendingcontestwork_download: 作品下载 + label_course_attendingcontestwork_download: 课件下载 label_attendingcontestwork_developers: 开发人员 label_attendingcontestwork_average_scores: 平均评分 label_attendingcontestwork_release_time: 发布时间 @@ -2370,7 +2388,7 @@ zh: mail_issue_from_project: "项目问题跟踪" mail_issue_attachments: "附件:" mail_issue_reply: "我要回复" - + mail_footer: "退订Trustie社区任务提醒?" # 课程资源上传 # edit by meng # 课程资源上传> From 1166a1463bd10802a6af7b9c65027d3241e0b557 Mon Sep 17 00:00:00 2001 From: alan <547533434@qq.com> Date: Sat, 7 Feb 2015 09:33:54 +0800 Subject: [PATCH 28/68] Signed-off-by: alan <547533434@qq.com> --- app/models/forum.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/forum.rb b/app/models/forum.rb index f5581b232..baa8e6260 100644 --- a/app/models/forum.rb +++ b/app/models/forum.rb @@ -5,7 +5,7 @@ class Forum < ActiveRecord::Base has_many :memos, :dependent => :destroy, conditions: "parent_id IS NULL" belongs_to :creator, :class_name => "User", :foreign_key => 'creator_id' - after_create :expire_forum_cache, :send_email + after_create :expire_forum_cache after_update :expire_forum_cache before_destroy :expire_forum_cache safe_attributes 'name', From f380d781e72bfbcdddb0d523d250d89366a48dfd Mon Sep 17 00:00:00 2001 From: sw <939547590@qq.com> Date: Mon, 9 Feb 2015 10:28:33 +0800 Subject: [PATCH 29/68] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=A0=B9=E6=8D=AE?= =?UTF-8?q?=E5=B7=B2=E6=9C=89=E8=AF=BE=E7=A8=8B=E5=A4=8D=E5=88=B6=E6=88=90?= =?UTF-8?q?=E6=96=B0=E8=AF=BE=E7=A8=8B=E7=9A=84=E8=B7=AF=E7=94=B1=E4=BB=A5?= =?UTF-8?q?=E5=8F=8A=E6=96=B9=E6=B3=95=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/courses_controller.rb | 28 +++++++++++++++++++++++++-- config/routes.rb | 1 + 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb index 61944ca2c..19f9c0fa2 100644 --- a/app/controllers/courses_controller.rb +++ b/app/controllers/courses_controller.rb @@ -955,10 +955,34 @@ class CoursesController < ApplicationController else render_403 end - end - + #根据已有课程复制课程 + #param id:已有课程ID + def copy_course + if @course + @new_course = Course.new @course.attributes + @new_course.tea_id = User.current.id + @new_course.created_at = DateTime.now + @new_course.updated_at = DateTime.now + @new_course.endup_time = nil + if @new_course.save + r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first + m = Member.new(:user => User.current, :roles => [r]) + m.project_id = -1 + course = CourseInfos.new(:user_id => User.current.id, :course_id => @new_course.id) + #user_grades = UserGrade.create(:user_id => User.current.id, :course_id => @course.id) + if @new_course.is_public == 1 + course_status = CourseStatus.create(:course_id => @new_course.id, :watchers_count => 0, :changesets_count => 0, :grade => 0, :course_type => 1) + end + @new_course.members << m + @new_course.course_infos << course + redirect_to settings_course_url @new_course + end + else + render_404 + end + end private diff --git a/config/routes.rb b/config/routes.rb index a06679fba..bf0b73e8d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -682,6 +682,7 @@ RedmineApp::Application.routes.draw do match 'valid_ajax', :to => 'courses#valid_ajax', :via => :get post 'join_in/join_group', :to => 'courses#join_group', :as => 'join_group' delete 'join_in/join_group', :to => 'courses#unjoin_group' + get 'copy_course' end collection do match 'join_private_courses', :via => [:get, :post] From 103a411b40b0cae04485da9cd1f672bb01008972 Mon Sep 17 00:00:00 2001 From: z9hang Date: Mon, 9 Feb 2015 16:24:45 +0800 Subject: [PATCH 30/68] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E6=90=9C=E7=B4=A2=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/mobile/apis/users.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/api/mobile/apis/users.rb b/app/api/mobile/apis/users.rb index d780b9db4..9a5307be6 100644 --- a/app/api/mobile/apis/users.rb +++ b/app/api/mobile/apis/users.rb @@ -82,6 +82,7 @@ module Mobile desc "用户搜索" params do requires :name, type: String, desc: '用户名关键字' + requires :search_by, type: String,desc: '搜索依据:0 昵称,1 用户名,2 邮箱' end get 'search/search_user' do us = UsersService.new From 59a2c8283a4e660cc5514f12a60d58ae1fdcbe9a Mon Sep 17 00:00:00 2001 From: whimlex Date: Mon, 9 Feb 2015 22:07:21 +0800 Subject: [PATCH 31/68] =?UTF-8?q?=E5=9B=BD=E9=99=85=E5=8C=96=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/helpers/welcome_helper.rb | 10 +- app/views/layouts/base_users.html.erb | 4 +- app/views/my/account.html.erb | 10 +- app/views/projects/_join_project.html.erb | 10 +- app/views/tags/_show_attachments.html.erb | 10 +- app/views/tags/_show_contests.html.erb | 2 +- app/views/tags/_show_courses.html.erb | 2 +- app/views/tags/_tag_add.html.erb | 2 +- app/views/tags/index.html.erb | 2 +- app/views/tags/show_all.html.erb | 2 +- app/views/users/_my_joinedcourse.html.erb | 4 +- app/views/users/show.html.erb | 2 +- app/views/welcome/_search_course.html.erb | 8 +- app/views/welcome/_search_project.html.erb | 10 +- app/views/welcome/contest.html.erb | 2 +- config/locales/account/en.yml | 90 +++ config/locales/account/zh.yml | 92 +++ config/locales/admins/en.yml | 13 + config/locales/admins/zh.yml | 16 + config/locales/commons/en.yml | 264 ++++++++ config/locales/commons/zh.yml | 267 ++++++++ config/locales/contacts/en.yml | 3 + config/locales/contacts/zh.yml | 22 + config/locales/contests/en.yml | 4 + config/locales/contests/zh.yml | 6 + config/locales/courses/en.yml | 4 + config/locales/courses/zh.yml | 17 + config/locales/en.yml | 467 +++---------- config/locales/mailers/en.yml | 4 + config/locales/mailers/zh.yml | 23 + config/locales/my/en.yml | 91 +++ config/locales/my/zh.yml | 101 +++ config/locales/navigatiors/en.yml | 8 + config/locales/navigatiors/zh.yml | 81 +++ config/locales/projects/en.yml | 6 + config/locales/projects/zh.yml | 60 ++ config/locales/users/en.yml | 19 + config/locales/users/zh.yml | 139 ++++ config/locales/zh.yml | 737 +++++---------------- lib/redmine/plugin.rb | 2 +- 40 files changed, 1633 insertions(+), 983 deletions(-) create mode 100644 config/locales/account/en.yml create mode 100644 config/locales/account/zh.yml create mode 100644 config/locales/admins/en.yml create mode 100644 config/locales/admins/zh.yml create mode 100644 config/locales/commons/en.yml create mode 100644 config/locales/commons/zh.yml create mode 100644 config/locales/contacts/en.yml create mode 100644 config/locales/contacts/zh.yml create mode 100644 config/locales/contests/en.yml create mode 100644 config/locales/contests/zh.yml create mode 100644 config/locales/courses/en.yml create mode 100644 config/locales/courses/zh.yml create mode 100644 config/locales/mailers/en.yml create mode 100644 config/locales/mailers/zh.yml create mode 100644 config/locales/my/en.yml create mode 100644 config/locales/my/zh.yml create mode 100644 config/locales/navigatiors/en.yml create mode 100644 config/locales/navigatiors/zh.yml create mode 100644 config/locales/projects/en.yml create mode 100644 config/locales/projects/zh.yml create mode 100644 config/locales/users/en.yml create mode 100644 config/locales/users/zh.yml diff --git a/app/helpers/welcome_helper.rb b/app/helpers/welcome_helper.rb index d101fd36c..eba3c0b3c 100644 --- a/app/helpers/welcome_helper.rb +++ b/app/helpers/welcome_helper.rb @@ -314,28 +314,28 @@ module WelcomeHelper str = ' '.html_safe case event.event_type when 'news' - str << content_tag("span", l(:field_user_active_published)) << + str << content_tag("span", l('user.active.published')) << content_tag("span", find_all_event_type(event)) << ': '.html_safe << link_to(strip_tags(event.event_description).gsub(/ /,''), event.event_url, {:title => event.event_description}) when 'issue', 'message' , 'bid' , 'wiki-page' , 'document' - str << content_tag("span", l(:field_user_active_published)) << + str << content_tag("span", l('user.active.published')) << content_tag("span", find_all_event_type(event)) << ': '.html_safe << link_to(event.event_title, event.event_url, {:title => event.event_title}) when 'reply' ,'Reply', 'Memo' - str << content_tag("span", l(:field_user_active_published)) << + str << content_tag("span", l('user.active.published')) << content_tag("span", find_all_event_type(event)) << ': '.html_safe << link_to(strip_tags(event.event_description).gsub(/ /,''), event.event_url, {:title => event.event_description}) when 'attachment' - str << content_tag('span', l(:field_user_active_uploaded)) << + str << content_tag('span', l('user.active.uploaded')) << content_tag('span', find_all_event_type(event)) << ': '.html_safe << link_to(event.event_title, event.event_url, {:title => event.event_title}) << link_to((' ['.html_safe+l(:label_downloads_list).to_s << ']'), project_files_path(event.container.project), :class => "attachments_list_color") else - str << content_tag("span", l(:field_user_active_updated)) << + str << content_tag("span", l('user.active.updated')) << content_tag("span", find_all_event_type(event)) << ': '.html_safe << link_to(event.event_title, event.event_url, {:title => event.event_title}) end diff --git a/app/views/layouts/base_users.html.erb b/app/views/layouts/base_users.html.erb index 3b2bd8dfe..a7ed0aa25 100644 --- a/app/views/layouts/base_users.html.erb +++ b/app/views/layouts/base_users.html.erb @@ -68,7 +68,7 @@ - <%=link_to "主页", home_path %> > + <%=link_to l(:field_homepage), home_path %> > <%=link_to @user.name, user_path %> @@ -343,7 +343,7 @@ <% if @user.user_extensions.identity == 2 %> <%= render_menu :user_enterprise_menu %> <% else %> - <%= render_menu :user_menu,@user %> + <%= render_menu :user_menu, @user %> <% end %>
diff --git a/app/views/my/account.html.erb b/app/views/my/account.html.erb index 47cfb4221..554f50633 100644 --- a/app/views/my/account.html.erb +++ b/app/views/my/account.html.erb @@ -151,9 +151,9 @@ <% if !User.current.user_extensions.nil? && !User.current.user_extensions.student_id.nil? %> - <%= text_field_tag :no, User.current.user_extensions.student_id, :placeholder => "请输入学号" %> + <%= text_field_tag :no, User.current.user_extensions.student_id, :placeholder => l(:label_account_identity_studentID) %> <% else %> - <%= text_field_tag :no, nil, :placeholder => "请输入学号" %> + <%= text_field_tag :no, nil, :placeholder => l(:label_account_identity_studentID) %> <% end %> @@ -217,18 +217,18 @@   <% if User.current.user_extensions.nil? %> - + readonly> <% else %> <% if User.current.user_extensions.identity == 3 || User.current.user_extensions.identity == 2 %> - + readonly> <% elsif User.current.user_extensions.school.nil? %> - + readonly> <% else %> diff --git a/app/views/projects/_join_project.html.erb b/app/views/projects/_join_project.html.erb index fdc60904d..98ab5dcd2 100644 --- a/app/views/projects/_join_project.html.erb +++ b/app/views/projects/_join_project.html.erb @@ -1,7 +1,7 @@ - 快速进入项目通道 + <%= l('project.join.title')%>
-
+

+<%= @subject %> +

+<% if @attachments.first || @course_news.first || @bids.first || + @homeworks.first || @course_journal_messages.first|| @course_messages.first %> +

<%= l(:label_course_overview)%>

<% unless @course_news.first.nil? %>
    @@ -45,24 +20,24 @@ <% @course_news.each do |course_new|%> -
  • +
  • [ - - <%= link_to course_new.course.name, [:controller => 'courses', :action => 'show', :id => course_new.course_id, :token => @token.value], + + <%= link_to truncate(course_new.course.name,length: 30,omission: '...'), course_url(course_new.course, :token => @token.value), :class=> "wmail_column", - :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + :style=> " font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> ] <%= link_to course_new.author, user_activities_url(course_new.author,:token => @token.value), :class => "wmail_name", - :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> <%= l(:label_project_notice) %> - <%= link_to course_new.title, [:controller => 'news', :action => 'show', :id => course_new.id,:token => @token.value], + <%= link_to truncate(course_new.title,length: 30,omission: '...'), news_url(course_new,:token => @token.value), :class => 'wmail_info', - :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + :style => "color:#5a5a5a; float:left; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> - <%= course_new.created_on %> + <%= format_time(course_new.created_on) %>
  • <% end %> @@ -72,50 +47,50 @@ <% if !@bids.first.nil? || !@homeworks.first.nil? %>
      -

      <%= l(:label_homework_overview) %>>(<%= @bids.count %>)

      +

      <%= l(:label_homework_overview) %>(<%= @bids.count %>)

      <% unless @bids.first.nil?%> <% @bids.each do |bid| %> -
    • +
    • [ - <%= link_to bid.courses.first.name, [:controller => 'courses', :action => 'show', :id => bid.courses.first.id, :token => @token.value], + <%= link_to truncate(bid.courses.first.name,length: 30,omission: '...'), course_url(bid.courses.first, :token => @token.value), :class=> "wmail_column", - :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + :style=> " font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> ] <%= link_to bid.author, user_activities_url(bid.author,:token => @token.value), :class => "wmail_name", - :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> <%= l(:label_course_homework) %> - <%= link_to bid.name, course_for_bid(:id => bid.id,:token => @token.value), + <%= link_to truncate(bid.name,length: 30,omission: '...'), course_for_bid_url(:id => bid.id,:token => @token.value), :class => 'wmail_info', - :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + :style => "color:#5a5a5a; float:left; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> - <%= bid.created_on %> + <%= format_time(bid.created_on) %>
    • <% end %> <% end %> <% unless @homeworks.first.nil? %> <% @homeworks.each do |homework| %> -
    • +
    • [ - <%= link_to homework.bid.courses.first.name, [:controller => 'courses', :action => 'show', :id => homework.bid.courses.first.id, :token => @token.value], + <%= link_to truncate(homework.bid.courses.first.name,length: 30,omission: '...'), course_url(homework.bid.courses.first, :token => @token.value), :class=> "wmail_column", - :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + :style=> " font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> ] <%= link_to homework.user, user_activities_url(homework.user,:token => @token.value), :class => "wmail_name", - :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> <%= l(:label_course_submit_homework) %> - <%= link_to homework.name, course_for_bid(:id => homework.bid.id,:token => @token.value), + <%= link_to truncate(homework.name,length: 30,omission: '...'), course_for_bid_url(:id => homework.bid.id,:token => @token.value), :class => 'wmail_info', - :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + :style => "color:#5a5a5a; float:left; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> - <%= homework.created_on %> + <%= format_time(homework.created_on) %>
    • <% end %> <% end %> @@ -133,24 +108,24 @@ <% @course_journal_messages.each do |course_journal_message|%> -
    • +
    • [ - <%= link_to course_journal_message.course.name, [:controller => 'courses', :action => 'show', :id => course_journal_message.jour_id, :token => @token.value], + <%= link_to truncate(course_journal_message.course.name,length: 30,omission: '...'), course_url(course_journal_message.course, :token => @token.value), :class=> "wmail_column", - :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + :style=> " font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> ] <%= link_to course_journal_message.user, user_activities_url(course_journal_message.user,:token => @token.value), :class => "wmail_name", - :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> <%= l(:label_send_course_journals_for_messages) %> - <%= link_to course_journal_message.notes, [:controller => 'courses', :action => 'feedback', :id => course_journal_message.jour_id,:token => @token.value], + <%= link_to truncate(course_journal_message.notes,length: 30,omission: '...'), course_feedback_url(course_journal_message.course,:token => @token.value), :class => 'wmail_info', - :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + :style => "color:#5a5a5a; float:left; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> - <%= course_journal_message.created_on %> + <%= format_time(course_journal_message.created_on) %>
    • <% end %> @@ -167,24 +142,24 @@ <% @course_messages.each do |course_message|%> -
    • +
    • [ - <%= link_to course_message.course.name, [:controller => 'courses', :action => 'show', :id => course_message.course.id, :token => @token.value], + <%= link_to truncate(course_message.course.name,length: 30,omission: '...'), course_url(course_message.course.id, :token => @token.value), :class=> "wmail_column", - :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + :style=> " font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> ] <%= link_to course_message.author, user_activities_url(course_message.author,:token => @token.value), :class => "wmail_name", - :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> <%= l(:label_send_course_messages) %> - <%= link_to course_message.subject,url_for(course_message.event_url(:token => @token.value)), + <%= link_to truncate(course_message.subject,length: 30,omission: '...'),board_message_url(course_message, :board_id => course_message.board_id,:token => @token.value), :class => 'wmail_info', - :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + :style => "color:#5a5a5a; float:left; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> - <%= course_message.created_on %> + <%= format_time(course_message.created_on) %>
    • <% end %> @@ -200,34 +175,34 @@ <% @attachments.each do |attachment|%> -
    • +
    • [ - <%= link_to attachment.course.name, [:controller => 'courses', :action => 'show', :id => attachment.course.id, :token => @token.value], + <%= link_to truncate(attachment.course.name,length: 30,omission: '...'), course_url(attachment.course, :token => @token.value), :class=> "wmail_column", - :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + :style=> " font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> ] <%= link_to attachment.author, user_activities_url(attachment.author,:token => @token.value), :class => "wmail_name", - :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> <%= l(:label_course_file_upload) %> - <%= link_to attachment.filename,course_files_path(@course,:token => @token.value), + <%= link_to truncate(attachment.filename,length: 30,omission: '...'),course_files_url(attachment.course,:token => @token.value), :class => 'wmail_info', - :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + :style => "color:#5a5a5a; float:left; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> - <%= course_message.created_on %> + <%= format_time(attachment.created_on) %>
    • <% end %> - +
    <% end %>
- - + <% end %> + <% if @issues.first || @project_messages.first %>
-

<%= l(:label_project_overview)%>

+

<%= l(:label_project_overview_new)%>

<% unless @issues.first.nil? %>

    @@ -236,24 +211,24 @@

    <% @issues.each do |issue|%> -
  • +
  • [ - <%= link_to issue.project.name, [:controller => 'projects', :action => 'show', :id => issue.project.id, :token => @token.value], + <%= link_to truncate(issue.project.name,length: 30,omission: '...'), project_url(issue.project, :token => @token.value), :class=> "wmail_column", - :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + :style=> " font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> ] <%= link_to issue.author, user_activities_url(issue.author,:token => @token.value), :class => "wmail_name", - :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> <%= l(:label_project_issue) %> - <%= link_to issue.subject,url_for(:controller => 'issues', :action => 'show', :id => issue.id, :token => @token.value), + <%= link_to truncate(issue.subject,length: 30,omission: '...'),issue_url(issue, :token => @token.value), :class => 'wmail_info', - :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + :style => "color:#5a5a5a; float:left; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> - <%= issue.created_on %> + <%= format_time(issue.created_on) %>
  • <% end %> @@ -265,28 +240,28 @@

      <%= l(:project_moule_boards_show) %> - (<%= @course_journal_messages.count %>) + (<%= @project_messages.count %>)

      <% @project_messages.each do |project_message|%> -
    • +
    • [ - <%= link_to project_message.project.name, [:controller => 'projects', :action => 'show', :id => project_message.project.id, :token => @token.value], + <%= link_to truncate(project_message.project.name,length: 30,omission: '...'), project_url(project_message.project, :token => @token.value), :class=> "wmail_column", - :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + :style=> " font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> ] <%= link_to project_message.author, user_activities_url(project_message.author,:token => @token.value), :class => "wmail_name", - :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> <%= l(:label_send_course_messages) %> - <%= link_to project_message.subject,url_for(project_message.event_url(:token => @token.value)), + <%= link_to truncate(project_message.subject,length: 30,omission: '...'),board_message_url(project_message, :board_id => project_message.board_id,:token => @token.value), :class => 'wmail_info', - :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + :style => "color:#5a5a5a; float:left; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> - <%= project_message.created_on %> + <%= format_time(project_message.created_on) %>
    • <% end %> @@ -297,10 +272,11 @@
- + <% end %> + <% unless @user_journal_messages.first.nil? %>

<%= l(:label_activities) %>

- <% unless @user_journal_messages.first.nil? %> +

    <%= l(:label_user_message) %> @@ -308,26 +284,86 @@

    <% @user_journal_messages.each do |user_journal_message|%> -
  • +
  • - <%= link_to user_journal_message.user, user_activities_url(course_journal_message.user,:token => @token.value), + <%= link_to user_journal_message.user, user_activities_url(user_journal_message.user,:token => @token.value), :class => "wmail_name", - :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> <%= l(:label_show_your_message) %> - <%= link_to user_journal_message.notes, [:controller => 'courses', :action => 'feedback', :id => course_journal_message.jour_id,:token => @token.value], + <%= link_to truncate(user_journal_message.notes,length: 30,omission: '...'),feedback_url(@user,:token => @token.value), :class => 'wmail_info', - :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + :style => "color:#5a5a5a; float:left; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> - <%= user_journal_message.created_on %>
  • + <%= format_time(user_journal_message.created_on) %> <% end %>
- <% end %> +
+ <% end %> + <% if @forums.first || @memos.first %> +
+

<%= l(:lable_bar_active) %>

+ <% unless @forums.first.nil? %> +
    +

    + <%= l(:label_user_forum) %> + (<%= @forums.count %>) +

    + + <% @forums.each do |forum|%> +
  • + + + <%= link_to forum.author, user_activities_url(forum.author,:token => @token.value), + :class => "wmail_name", + :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + <%= l(:label_forum_new) %> + + <%= link_to truncate(forum.name,length: 30,omission: '...'),forum_url(forum,:token => @token.value), + :class => 'wmail_info', + :style => "color:#5a5a5a; float:left; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + %> + <%= format_time(forum.created_at) %>
  • + + <% end %> + +
    +
+ <% end %> + <% unless @memos.first.nil? %> +
    +

    + <%= l(:label_user_message_forum) %> + (<%= @memos.count %>) +

    + + <% @memos.each do |memo|%> +
  • + + + <%= link_to memo.author, user_activities_url(memo.author,:token => @token.value), + :class => "wmail_name", + :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + <%= l(:label_memo_new_from_forum) %> + + <%= link_to truncate(memo.subject,length: 30,omission: '...'),forum_memo_url(memo.forum, (memo.parent_id.nil? ? memo : memo.parent_id)), + :class => 'wmail_info', + :style => "color:#5a5a5a; float:left; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + %> + <%= format_time(memo.created_at) %>
  • + + <% end %> + +
    +
+ <% end %> +
+<% end %>
<%= link_to l(:mail_footer), @user_url, :style => "margin-top:20px;color:#2775d2; margin-left:10px;" %>
diff --git a/app/views/mailer/send_for_user_activities.text.erb b/app/views/mailer/send_for_user_activities.text.erb index 03ea31ec3..b7d0553e3 100644 --- a/app/views/mailer/send_for_user_activities.text.erb +++ b/app/views/mailer/send_for_user_activities.text.erb @@ -1,3 +1,6 @@ +<%= @subject %> +<% if @attachments.first || @course_news.first || @bids.first || + @homeworks.first || @course_journal_messages.first|| @course_messages.first %> <%= l(:label_course_overview)%> <% unless @course_news.first.nil? %> <%= l(:label_course_news) %> @@ -9,19 +12,17 @@ ▪ [ - <%= link_to course_new.course.name, [:controller => 'courses', :action => 'show', :id => course_new.course_id, :token => @token.value], - :class=> "wmail_column", - :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + <%= link_to truncate(course_new.course.name,length: 30,omission: '...'), course_url(course_new.course, :token => @token.value)%> ] - <%= link_to course_new.author, user_activities_url(course_new.author,:token => @token.value), :class => "wmail_name", - :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + <%= link_to course_new.author, user_activities_url(course_new.author,:token => @token.value) + %> <%= l(:label_project_notice) %> - <%= link_to course_new.title, [:controller => 'news', :action => 'show', :id => course_new.id,:token => @token.value], - :class => 'wmail_info', - :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" - %> <%= course_new.created_on %> + <%= link_to truncate(course_new.title,length: 30,omission: '...'), news_url(course_new,:token => @token.value) + + + %> <%= format_time(course_new.created_on) %> <% end %> @@ -33,20 +34,20 @@ ▪ [ - <%= link_to bid.courses.first.name, [:controller => 'courses', :action => 'show', :id => bid.courses.first.id, :token => @token.value], - :class=> "wmail_column", - :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + <%= link_to truncate(bid.courses.first.name,length: 30,omission: '...'),course_url(bid.courses.first, :token => @token.value) + + %> ] - <%= link_to bid.author, user_activities_url(bid.author,:token => @token.value), :class => "wmail_name", - :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + <%= link_to bid.author, user_activities_url(bid.author,:token => @token.value) + %> <%= l(:label_course_homework) %> - <%= link_to bid.name, course_for_bid(:id => bid.id,:token => @token.value), - :class => 'wmail_info', - :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + <%= link_to truncate(bid.name,length: 30,omission: '...'), course_for_bid_url(bid,:token => @token.value) + + %> - <%= bid.created_on %> + <%= format_time(bid.created_on) %> <% end %> <% end %> @@ -54,20 +55,20 @@ <% @homeworks.each do |homework| %> ▪[ - <%= link_to homework.bid.courses.first.name, [:controller => 'courses', :action => 'show', :id => homework.bid.courses.first.id, :token => @token.value], - :class=> "wmail_column", - :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + <%= link_to truncate(homework.bid.courses.first.name,length: 30,omission: '...'), course_url(homework.bid.courses.first, :token => @token.value) + + %> ] - <%= link_to homework.user, user_activities_url(homework.user,:token => @token.value), :class => "wmail_name", - :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + <%= link_to homework.user, user_activities_url(homework.user,:token => @token.value) + %> <%= l(:label_course_submit_homework) %> - <%= link_to homework.name, course_for_bid(:id => homework.bid.id,:token => @token.value), - :class => 'wmail_info', - :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + <%= link_to truncate(homework.name,length: 30,omission: '...'), course_for_bid_url(homework.bid,:token => @token.value) + + %> - <%= homework.created_on %> + <%= format_time(homework.created_at) %> <% end %> <% end %> @@ -84,20 +85,20 @@ [ - <%= link_to course_journal_message.course.name, [:controller => 'courses', :action => 'show', :id => course_journal_message.jour_id, :token => @token.value], - :class=> "wmail_column", - :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + <%= link_to truncate(course_journal_message.course.name,length: 30,omission: '...'), course_url(course_journal_message.course, :token => @token.value) + + %> ] - <%= link_to course_journal_message.user, user_activities_url(course_journal_message.user,:token => @token.value), :class => "wmail_name", - :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + <%= link_to course_journal_message.user, user_activities_url(course_journal_message.user,:token => @token.value) + %> <%= l(:label_send_course_journals_for_messages) %> - <%= link_to course_journal_message.notes, [:controller => 'courses', :action => 'feedback', :id => course_journal_message.jour_id,:token => @token.value], - :class => 'wmail_info', - :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + <%= link_to truncate(course_journal_message.notes,length: 30,omission: '...'), course_feedback_url(course_journal_message.course,:token => @token.value) + + %> - <%= course_journal_message.created_on %> + <%= format_time(course_journal_message.created_on) %> <% end %> @@ -115,20 +116,20 @@ ▪ [ - <%= link_to course_message.course.name, [:controller => 'courses', :action => 'show', :id => course_message.course.id, :token => @token.value], - :class=> "wmail_column", - :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + <%= link_to truncate(course_message.course.name,length: 30,omission: '...'), course_url(course_message.course, :token => @token.value) + + %> ] - <%= link_to course_message.author, user_activities_url(course_message.author,:token => @token.value), :class => "wmail_name", - :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + <%= link_to course_message.author, user_activities_url(course_message.author,:token => @token.value) + %> <%= l(:label_send_course_messages) %> - <%= link_to course_message.subject,url_for(course_message.event_url(:token => @token.value)), - :class => 'wmail_info', - :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + <%= link_to truncate(course_message.subject,length: 30,omission: '...'),board_message_url(course_message, :board_id => course_message.board_id,:token => @token.value) + + %> - <%= course_message.created_on %> + <%= format_time(course_message.created_on) %> <% end %> @@ -144,29 +145,29 @@ <% @attachments.each do |attachment|%> ▪[ - <%= link_to attachment.course.name, [:controller => 'courses', :action => 'show', :id => attachment.course.id, :token => @token.value], - :class=> "wmail_column", - :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + <%= link_to truncate(attachment.course.name,length: 30,omission: '...'), course_url(attachment.course, :token => @token.value) + + %> ] - <%= link_to attachment.author, user_activities_url(attachment.author,:token => @token.value), :class => "wmail_name", - :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + <%= link_to attachment.author, user_activities_url(attachment.author,:token => @token.value) + %> <%= l(:label_course_file_upload) %> - <%= link_to attachment.filename,course_files_path(@course,:token => @token.value), - :class => 'wmail_info', - :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + <%= link_to truncate(attachment.filename,length: 30,omission: '...'),course_files_url(attachment.course,:token => @token.value) + + %> - <%= course_message.created_on %> + <%= format_time(attachment.created_on) %> <% end %> <% end %> -
+<% end %> - -<%= l(:label_project_overview)%> +<% @issues.first || @project_messages.first %> +<%= l(:label_project_overview_new)%> <% unless @issues.first.nil? %> <%= l(:label_issue_tracking) %> @@ -176,20 +177,20 @@ ▪ [ - <%= link_to issue.project.name, [:controller => 'projects', :action => 'show', :id => issue.project.id, :token => @token.value], - :class=> "wmail_column", - :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + <%= link_to truncate(issue.project.name,length: 30,omission: '...'), project_url(issue.project, :token => @token.value) + + %> ] - <%= link_to issue.author, user_activities_url(issue.author,:token => @token.value), :class => "wmail_name", - :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + <%= link_to issue.author, user_activities_url(issue.author,:token => @token.value) + %> <%= l(:label_project_issue) %> - <%= link_to issue.subject,url_for(:controller => 'issues', :action => 'show', :id => issue.id, :token => @token.value), - :class => 'wmail_info', - :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + <%= link_to truncate(issue. subject,length: 30,omission: '...'),issue_url(issue, :token => @token.value) + + %> - <%= issue.created_on %> + <%= format_time(issue.created_on) %> <% end %> @@ -198,34 +199,34 @@ <% unless @project_messages.first.nil? %> <%= l(:project_moule_boards_show) %> - (<%= @course_journal_messages.count %>) + (<%= @project_messages.count %>) <% @project_messages.each do |project_message|%> ▪[ - <%= link_to project_message.project.name, [:controller => 'projects', :action => 'show', :id => project_message.project.id, :token => @token.value], - :class=> "wmail_column", - :style=> "width:90px; font-weight: bold; display:block; float:left; color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> - ] + <%= link_to truncate(project_message.project.name,length: 30,omission: '...'), project_url(project_message.project, :token => @token.value) + + %> + ] - <%= link_to project_message.author, user_activities_url(project_message.author,:token => @token.value), :class => "wmail_name", - :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;"%> + <%= link_to project_message.author, board_message_url(project_message, :board_id => project_message.board_id,:token => @token.value) + %> <%= l(:label_send_course_messages) %> - <%= link_to project_message.subject,url_for(project_message.event_url(:token => @token.value)), - :class => 'wmail_info', - :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + <%= link_to truncate(project_message. subject,length: 30,omission: '...'),board_message_url(project_message, :board_id => project_message.board_id,:token => @token.value) + + %> - <%= project_message.created_on %> + <%= format_time(project_message.created_on) %> <% end %> <% end %> - +<% unless @user_journal_messages.first.nil? %> <%= l(:label_activities) %> - <% unless @user_journal_messages.first.nil? %> + <%= l(:label_user_message) %> (<%= @user_journal_messages.count %>) @@ -233,21 +234,63 @@ <% @user_journal_messages.each do |user_journal_message|%> ▪ - <%= link_to user_journal_message.user, user_activities_url(course_journal_message.user,:token => @token.value), - :class => "wmail_name", - :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; width:50px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> + <%= link_to user_journal_message.user, user_activities_url(user_journal_message.user,:token => @token.value) + + %> <%= l(:label_show_your_message) %> - <%= link_to user_journal_message.notes, [:controller => 'courses', :action => 'feedback', :id => course_journal_message.jour_id,:token => @token.value], - :class => 'wmail_info', - :style => "color:#5a5a5a; float:left; width:400px; margin-right:5px; display:block;color:#1b55a7;overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" + <%= link_to truncate(user_journal_message.notes,length: 30,omission: '...'), feedback_url(@user,:token => @token.value) + + %> - <%= user_journal_message.created_on %> + <%= format_time(user_journal_message.created_on) %> <% end %> - <% end %> -< +<% end %> +<% if @forums.first || @memos.first %> + <%= l(:lable_bar_active) %> + <% unless @forums.first.nil? %> + + <%= l(:label_user_forum) %> + (<%= @forums.count %>) + + <% @forums.each do |forum|%> + ▪ + + <%= link_to forum.author, user_activities_url(forum.author,:token => @token.value) %> + <%= l(:label_forum_new) %> + + <%= link_to truncate(forum.name,length: 30,omission: '...'),forum_url(forum,:token => @token.value) + %> + <%= format_time(forum.created_at) %> + + <% end %> + + + <% end %> + <% unless @memos.first.nil? %> + + <%= l(:label_user_message_forum) %> + (<%= @memos.count %>) + + <% @memos.each do |memo|%> + ▪ + + <%= link_to memo.author, user_activities_url(memo.author,:token => @token.value)%> + <%= l(:label_memo_new_from_forum) %> + + <%= link_to truncate(memo.subject,length: 30,omission: '...'),forum_memo_url(memo.forum, (memo.parent_id.nil? ? memo : memo.parent_id)) + %> + <%= format_time(memo.created_at) %> + + <% end %> + + + + <% end %> + +<% end %> <%= link_to l(:mail_footer), @user_url, :style => "margin-top:20px;color:#2775d2; margin-left:10px;" %> diff --git a/config/application.rb b/config/application.rb index 100beb93c..3b36d7cb7 100644 --- a/config/application.rb +++ b/config/application.rb @@ -17,9 +17,7 @@ module RedmineApp # -- all .rb files in that directory are automatically loaded. #verifier if email is real - EmailVerifier.config do |config| - config.verifier_email = "alanlong9278@126.com" - end + config.generators do |g| g.test_framework :rspec, diff --git a/config/initializers/send_mail.rb b/config/initializers/send_mail.rb index 4cfd3cc03..86b3a53fd 100644 --- a/config/initializers/send_mail.rb +++ b/config/initializers/send_mail.rb @@ -6,18 +6,22 @@ require 'rufus-scheduler' #users = User.where("mail_notification = 'week' or mail_notification = 'day'") scheduler = Rufus::Scheduler.new -scheduler.cron('*/1 * * * *') do - users = User.where("login like '%alan%'") +scheduler.cron('0 0 * * 1') do + users = User.where("mail_notification = 'week'") users.each do |user| - # if user.mail_notification == "week" - # cycle = '*/1 * * * *' - # else - # cycle = '*/2 * * * *' - # end - Rails.logger.info "send mail to #{user.show_name}(#{user.mail}) at #{Time.now}" + #Rails.logger.info "send mail to #{user.show_name}(#{user.mail}) at #{Time.now}" Thread.start do Mailer.send_for_user_activities(user, Date.today, 7).deliver end end end +scheduler.cron('0 0 * * *') do + users = User.where("mail_notification = 'day'") + users.each do |user| + #Rails.logger.info "send mail to #{user.show_name}(#{user.mail}) at #{Time.now}" + Thread.start do + Mailer.send_for_user_activities(user, Date.today, 1).deliver + end + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 9d6172f23..db5d698ef 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -616,8 +616,8 @@ en: label_document_new: New document label_document_plural: Documents label_document_added: Document added - label_forum_message_added: Message added - label_forum_add: Forum added + label_forum_message_added: Forum's message added + #label_forum_add: Forum added label_document_public_info: "If you don't choose public, only the project's members can see the document." label_role: Role label_role_plural: Roles @@ -2089,4 +2089,4 @@ en: label_anonymous: Anonymous label_submit_comments: Submit_comments label_course_empty_select: You have not selected course! - label_enterprise_page_made: enterprise_page + label_enterprise_page_made: enterprise_page diff --git a/config/locales/zh.yml b/config/locales/zh.yml index 472bf6fe2..ac35990fa 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -573,7 +573,7 @@ zh: label_document_new: 新建文档 label_document_plural: 文档 label_document_added: 文档已添加 - label_forum_message_added: 发帖成功 + label_forum_message_added: 贴吧发帖成功 label_forum_add: 贴吧创建成功 label_message_reply: 回帖人 label_document_public_info: (打钩为公开,不打钩则不公开,若不公开,仅项目成员可见该文档。) @@ -664,6 +664,8 @@ zh: label_user_login_attending_contest: 您还没有登录,请登录后参赛 label_user_login_score_and_comment: 您还没有登录,请登录后对作品进行打分评价 label_user_login_notificationcomment: 您还没有登录,请登录后参加评论 + label_user_forum: 您的贴吧 + label_user_message_forum: 您的帖子 label_user_message: 您的留言 label_show_your_message: 给您的留言 #end @@ -755,7 +757,8 @@ zh: label_settings: 配置 label_overview: 近期动态 label_course_overview: "课程动态" - label_project_overview: "项目动态" + label_project_overview_new: "项目动态" + label_forums_overview: "贴吧动态" label_homework_overview: 作业动态 label_question_student: 作业交流 #bai label_homework_commit: 提交作业 #huang @@ -968,7 +971,9 @@ zh: label_theme: 主题 label_default: 默认 label_search_titles_only: 仅在标题中搜索 - label_user_mail_option_all: "收取我的项目的所有通知" + label_user_mail_option_all: "收取我的所有通知" + label_user_mail_option_week: "按周收取我的所有通知" + label_user_mail_option_day: "按天收取我的所有通知" label_must_answer: "必答" label_poll_title: 问卷调查_问卷页面 #huang diff --git a/lib/redmine/notifiable.rb b/lib/redmine/notifiable.rb index 42a81980f..26ad31966 100644 --- a/lib/redmine/notifiable.rb +++ b/lib/redmine/notifiable.rb @@ -20,9 +20,7 @@ module Redmine notifications << Notifiable.new('message_posted') notifications << Notifiable.new('wiki_content_added') notifications << Notifiable.new('wiki_content_updated') - notifications << Notifiable.new('forum_add') - notifications << Notifiable.new('forum_message_added', 'forum_add') - notifications + notifications << Notifiable.new('forum_message_added') end end end From aa8f58530cd7a641ae8bec96805fbe9e58b058ad Mon Sep 17 00:00:00 2001 From: alan <547533434@qq.com> Date: Wed, 11 Feb 2015 23:34:10 +0800 Subject: [PATCH 37/68] =?UTF-8?q?=E7=BB=99=E9=BB=84=E6=80=BB=E6=8F=90?= =?UTF-8?q?=E4=BE=9B=E6=8E=A5=E5=8F=A3=20Signed-off-by:=20alan=20<54753343?= =?UTF-8?q?4@qq.com>?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/projects_controller.rb | 11 +++++++++ app/models/mailer.rb | 20 ++++++++++++++++ app/models/memo_observer.rb | 1 - app/services/users_service.rb | 24 +++++++++++++++++++ .../mailer/send_invite_in_project.html.erb | 17 +++++++++++++ .../mailer/send_invite_in_project.text.erb | 2 ++ config/locales/zh.yml | 1 + 7 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 app/views/mailer/send_invite_in_project.html.erb create mode 100644 app/views/mailer/send_invite_in_project.text.erb diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 5054fd5c1..41ec5fbf4 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -249,6 +249,17 @@ class ProjectsController < ApplicationController # Description 项目动态展示方法,删除了不必要的代码 def show # 试图跳转到请求的按钮 + if params[:login] + login = params[:login] + login = login.sub(/%40/,'@') + mail = params[:login] + password = params[:password] + us = UsersService.new + user = us.register_auto(login,mail, password) + Member.create(:role_ids => [4], :user_id => user.id,:project_id => @project.id) + UserGrade.create(:user_id => user.id, :project_id => @project.id) + User.current = user unless User.current.nil? + end if params[:jump] && redirect_to_project_menu_item(@project, params[:jump]) return end diff --git a/app/models/mailer.rb b/app/models/mailer.rb index e8af704d8..4da1f9b42 100644 --- a/app/models/mailer.rb +++ b/app/models/mailer.rb @@ -27,6 +27,17 @@ class Mailer < ActionMailer::Base { :host => Setting.host_name, :protocol => Setting.protocol } end + # author: alan + # 发送邀请未注册用户加入项目邮件 + # 功能: 在加入项目的同时自动注册用户 + def send_invite_in_project(email, project, invitor) + @subject = "#{invitor.name} #{l(:label_invite_project)} #{project.name} " + password = newpass(6) + @project_url = url_for(:controller => 'projects', :action => 'show', :id => project.id, + :password => password, :login => email) + mail :to => email, :subject => @subject + end + # author: alan # 根据用户选择发送个人日报或周报 # 发送内容: 项目【缺陷,讨论区,新闻】,课程【通知,留言,新闻】, 贴吧, 个人留言 @@ -828,4 +839,13 @@ class Mailer < ActionMailer::Base end end end + + # author: alan + # 功能: 生成len位随机字符串 + def newpass(len) + chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a + newpass = "" + 1.upto(len) { |i| newpass << chars[rand(chars.size-1)] } + return newpass + end end diff --git a/app/models/memo_observer.rb b/app/models/memo_observer.rb index e50bcf5a2..66cabe923 100644 --- a/app/models/memo_observer.rb +++ b/app/models/memo_observer.rb @@ -3,7 +3,6 @@ class MemoObserver < ActiveRecord::Observer thread1=Thread.new do Mailer.forum_message_added(memo).deliver if Setting.notified_events.include?('forum_message_added') - end end end diff --git a/app/services/users_service.rb b/app/services/users_service.rb index 897171b55..b9962a2a8 100644 --- a/app/services/users_service.rb +++ b/app/services/users_service.rb @@ -44,7 +44,31 @@ class UsersService #location = get_user_location @user #{:id => @user.id, :img_url => img_url, :nickname => @user.login, :gender => gender, :work_unit => work_unit, :mail => @user.mail, :location => location, :brief_introduction => @user.user_extensions.brief_introduction} end + def register_auto(login,mail,password) + @user = User.new + @user.admin = false + @user.register + @user.login = login + @user.mail =mail + password_confirmation = password + should_confirmation_password = true + if !password.blank? && !password_confirmation.blank? && should_confirmation_password + @user.password, @user.password_confirmation = password, password_confirmation + elsif !password.blank? && !should_confirmation_password + @user.password = password + else + @user.password = "" + end + @user = automatically_register(@user) + + if @user.id != nil + ue = @user.user_extensions ||= UserExtensions.new + ue.user_id = @user.id + ue.save + end + @user + end #显示用户 #id用户id def show_user(params) diff --git a/app/views/mailer/send_invite_in_project.html.erb b/app/views/mailer/send_invite_in_project.html.erb new file mode 100644 index 000000000..980adc12d --- /dev/null +++ b/app/views/mailer/send_invite_in_project.html.erb @@ -0,0 +1,17 @@ + +
+
    + +
  • <%= l(:mail_issue_content)%> + +

    <%= @subject %>

    +

    <%= link_to @project_url, @project_url%>

    +
    +
  • + +
+ +
+
+ + diff --git a/app/views/mailer/send_invite_in_project.text.erb b/app/views/mailer/send_invite_in_project.text.erb new file mode 100644 index 000000000..6ee038055 --- /dev/null +++ b/app/views/mailer/send_invite_in_project.text.erb @@ -0,0 +1,2 @@ +<%= @subject %> +<%= link_to @project_url, @project_url%> \ No newline at end of file diff --git a/config/locales/zh.yml b/config/locales/zh.yml index ac35990fa..2d132d92f 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -1917,6 +1917,7 @@ zh: label_bids_task_list: 作业列表 label_join_course: 加入 + label_invite_project: 邀请您加入项目 label_exit_course: 退出 label_exit_group: 退出当前分班 label_new_join: 加入 From c00deabf7507739edd242f3cb83bbd6b413e61f8 Mon Sep 17 00:00:00 2001 From: alan <547533434@qq.com> Date: Thu, 12 Feb 2015 00:15:10 +0800 Subject: [PATCH 38/68] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E3=80=8A=E9=82=AE?= =?UTF-8?q?=E4=BB=B6=E6=A0=B7=E5=BC=8F=E3=80=8B=20Signed-off-by:=20alan=20?= =?UTF-8?q?<547533434@qq.com>?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/mailer.rb | 2 +- app/views/mailer/send_for_user_activities.html.erb | 4 ++-- app/views/mailer/send_for_user_activities.text.erb | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/models/mailer.rb b/app/models/mailer.rb index 4da1f9b42..14c01b93d 100644 --- a/app/models/mailer.rb +++ b/app/models/mailer.rb @@ -134,7 +134,7 @@ class Mailer < ActionMailer::Base @issue_author_url = url_for(user_activities_url(@author)) recipients ||= [] if @forum.author.mail_notification != 'day' && @forum.author.mail_notification != 'week' - recipients << @forum.author.mail + recipients << @forum.creator.mail end if @author.mail_notification != 'day' && @author.mail_notification != 'week' recipients << @author.mail diff --git a/app/views/mailer/send_for_user_activities.html.erb b/app/views/mailer/send_for_user_activities.html.erb index ed7877360..1735bd899 100644 --- a/app/views/mailer/send_for_user_activities.html.erb +++ b/app/views/mailer/send_for_user_activities.html.erb @@ -319,7 +319,7 @@
  • - <%= link_to forum.author, user_activities_url(forum.author,:token => @token.value), + <%= link_to forum.creator, user_activities_url(forum.creator,:token => @token.value), :class => "wmail_name", :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> <%= l(:label_forum_new) %> @@ -349,7 +349,7 @@ <%= link_to memo.author, user_activities_url(memo.author,:token => @token.value), :class => "wmail_name", :style => "color:#fe5722; float:left;display:block; margin-right:5px; margin-left:5px; overflow:hidden; white-space: nowrap; text-overflow:ellipsis;" %> - <%= l(:label_memo_new_from_forum) %> + <%= memo.parent_id.nil? ? l(:label_memo_new_from_forum) : l(:label_reply) %> <%= link_to truncate(memo.subject,length: 30,omission: '...'),forum_memo_url(memo.forum, (memo.parent_id.nil? ? memo : memo.parent_id)), :class => 'wmail_info', diff --git a/app/views/mailer/send_for_user_activities.text.erb b/app/views/mailer/send_for_user_activities.text.erb index b7d0553e3..4de42b8aa 100644 --- a/app/views/mailer/send_for_user_activities.text.erb +++ b/app/views/mailer/send_for_user_activities.text.erb @@ -259,7 +259,7 @@ <% @forums.each do |forum|%> ▪ - <%= link_to forum.author, user_activities_url(forum.author,:token => @token.value) %> + <%= link_to forum.creator, user_activities_url(forum.creator,:token => @token.value) %> <%= l(:label_forum_new) %> <%= link_to truncate(forum.name,length: 30,omission: '...'),forum_url(forum,:token => @token.value) @@ -279,7 +279,7 @@ ▪ <%= link_to memo.author, user_activities_url(memo.author,:token => @token.value)%> - <%= l(:label_memo_new_from_forum) %> + <%= memo.parent_id.nil? ? l(:label_memo_new_from_forum) : l(:label_reply) %> <%= link_to truncate(memo.subject,length: 30,omission: '...'),forum_memo_url(memo.forum, (memo.parent_id.nil? ? memo : memo.parent_id)) %> From 196ceff138478718051aa639d25885ca7a21e1c7 Mon Sep 17 00:00:00 2001 From: z9hang Date: Thu, 12 Feb 2015 15:51:52 +0800 Subject: [PATCH 39/68] =?UTF-8?q?=E8=BF=94=E5=9B=9E=E8=AF=BE=E7=A8=8B?= =?UTF-8?q?=E7=9A=84=E6=8E=A5=E5=8F=A3=E8=A1=A5=E4=B8=8A=E5=BD=93=E5=89=8D?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E6=98=AF=E5=90=A6=E4=B8=BA=E8=AF=BE=E7=A8=8B?= =?UTF-8?q?=E6=88=90=E5=91=98=EF=BC=8C=E8=AF=BE=E7=A8=8B=E8=80=81=E5=B8=88?= =?UTF-8?q?=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/courses_service.rb | 4 ++-- app/services/homework_service.rb | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/services/courses_service.rb b/app/services/courses_service.rb index 0304722de..7fa78b635 100644 --- a/app/services/courses_service.rb +++ b/app/services/courses_service.rb @@ -211,7 +211,7 @@ class CoursesService @course.members << m @course.course_infos << course end - @course + {:course => @course,:img_url => url_to_avatar(@course),:current_user_is_member => current_user.member_of_course?(@course),:current_user_is_teacher => is_course_teacher(current_user,@course)} end #验证编辑课程的权限 @@ -248,7 +248,7 @@ class CoursesService course_status = CourseStatus.create(:course_id => course.id, :grade => 0) end end - course + {:course => course,:img_url => url_to_avatar(course),:current_user_is_member => current_user.member_of_course?(course),:current_user_is_teacher => is_course_teacher(current_user,course)} end #退出课程 diff --git a/app/services/homework_service.rb b/app/services/homework_service.rb index b468e1110..dd2d2356e 100644 --- a/app/services/homework_service.rb +++ b/app/services/homework_service.rb @@ -6,6 +6,7 @@ class HomeworkService include WordsHelper include ApiHelper include HomeworkAttachHelper + include CoursesHelper # 作业详情(老师才显示启动匿评,学生不显示 ) # many_times 第几次(作业) @@ -276,7 +277,7 @@ class HomeworkService hw = bid.homeworks.where("user_id = #{current_user.id}") my_homeworks << hw[0] unless (hw.nil? || hw[0].nil?) end - course_list << {:course => mp.course,:img_url => url_to_avatar(mp.course),:my_homework => my_homeworks} + course_list << {:course => mp.course,:img_url => url_to_avatar(mp.course),:my_homework => my_homeworks,:current_user_is_member => current_user.member_of_course?(mp.course),:current_user_is_teacher => is_course_teacher(current_user,mp.course)} end course_list end From 2f762973b46fd3d1fedcd156fdd51538e0c57532 Mon Sep 17 00:00:00 2001 From: z9hang Date: Sun, 1 Mar 2015 16:32:10 +0800 Subject: [PATCH 40/68] =?UTF-8?q?=E8=AF=BE=E7=A8=8B=E5=8A=A8=E6=80=81?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E8=BF=94=E5=9B=9E=E6=95=B0=E6=8D=AE=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/mobile/entities/course_dynamic.rb | 9 ++++----- app/services/courses_service.rb | 8 +++++++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/api/mobile/entities/course_dynamic.rb b/app/api/mobile/entities/course_dynamic.rb index 11f8c2682..5e22e3df7 100644 --- a/app/api/mobile/entities/course_dynamic.rb +++ b/app/api/mobile/entities/course_dynamic.rb @@ -6,12 +6,11 @@ module Mobile c[field] if (c.is_a?(Hash) && c.key?(field)) end end - + course_dynamic_expose :type + course_dynamic_expose :count course_dynamic_expose :course_name - course_dynamic_expose :need_anonymous_comments_count - course_dynamic_expose :student_commit_number - course_dynamic_expose :news_count - course_dynamic_expose :message_count + course_dynamic_expose :course_id + course_dynamic_expose :course_img_url end end end \ No newline at end of file diff --git a/app/services/courses_service.rb b/app/services/courses_service.rb index 7fa78b635..ab0a53744 100644 --- a/app/services/courses_service.rb +++ b/app/services/courses_service.rb @@ -345,7 +345,13 @@ class CoursesService end news_count = course.news.count message_count = course.journals_for_messages.count - {:course_name => course.name,:need_anonymous_comments_count=>need_anonymous_comments_count,:student_commit_number=>student_commit_number,:news_count=> news_count,:message_count=>message_count} + result = [] + result << {:course_name => course.name,:course_id => course.id,:course_img_url => url_to_avatar(course),:type => 1,:count => message_count} + result << {:course_name => course.name,:course_id => course.id,:course_img_url => url_to_avatar(course),:type => 2,:count => need_anonymous_comments_count} + result << {:course_name => course.name,:course_id => course.id,:course_img_url => url_to_avatar(course),:type => 3,:count => student_commit_number} + result << {:course_name => course.name,:course_id => course.id,:course_img_url => url_to_avatar(course),:type => 4,:count => news_count} + #{:course_name => course.name,:need_anonymous_comments_count=>need_anonymous_comments_count,:student_commit_number=>student_commit_number,:news_count=> news_count,:message_count=>message_count} + result end private From 18271946d79c68971267e644d3770551b6d880c8 Mon Sep 17 00:00:00 2001 From: z9hang Date: Sun, 1 Mar 2015 17:31:38 +0800 Subject: [PATCH 41/68] =?UTF-8?q?=E4=BD=9C=E4=B8=9A=E8=AF=A6=E6=83=85?= =?UTF-8?q?=EF=BC=8C=E8=AF=BE=E7=A8=8B=E4=BD=9C=E4=B8=9A=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E8=BF=94=E5=9B=9E=E6=95=B0=E6=8D=AE=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E4=BF=AE=E6=94=B9=EF=BC=88=E5=A2=9E=E5=8A=A0=E4=BA=86?= =?UTF-8?q?=E4=BD=9C=E4=B8=9A=E4=BD=9C=E8=80=85=E7=9B=B8=E5=85=B3=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/mobile/entities/homework.rb | 10 ++++++++-- app/controllers/courses_controller.rb | 3 ++- app/services/courses_service.rb | 6 +++--- app/services/homework_service.rb | 2 +- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/app/api/mobile/entities/homework.rb b/app/api/mobile/entities/homework.rb index 9be9bc53e..5b996c1af 100644 --- a/app/api/mobile/entities/homework.rb +++ b/app/api/mobile/entities/homework.rb @@ -18,8 +18,14 @@ module Mobile homework_expose :id #课程名称 homework_expose :course_name - #课程老师 - homework_expose :course_teacher + #作业发布者 + expose :author,using: Mobile::Entities::User do |f, opt| + f[:author] + end + + #作业发布者真名 + homework_expose :author_real_name + #作业次数 homework_expose :homework_times #作业名称 diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb index 19f9c0fa2..b6fc21bf9 100644 --- a/app/controllers/courses_controller.rb +++ b/app/controllers/courses_controller.rb @@ -98,7 +98,8 @@ class CoursesController < ApplicationController #更新课程信息 def update cs = CoursesService.new - @course = cs.edit_course params,@course,User.current + c = cs.edit_course params,@course,User.current + @course = c[:course] if @course.errors.full_messages.count <= 0 respond_to do |format| format.html { diff --git a/app/services/courses_service.rb b/app/services/courses_service.rb index ab0a53744..1f5ca7894 100644 --- a/app/services/courses_service.rb +++ b/app/services/courses_service.rb @@ -3,7 +3,7 @@ class CoursesService include CoursesHelper include HomeworkAttachHelper include ApiHelper - #TODO:尚未整合权限系统 + #参数school_id为0或不传时返回所有课程,否则返回对应学校的课程 #参数per_page_count分页功能,每页显示的课程数 #参数page分页功能,当前页码 @@ -356,7 +356,7 @@ class CoursesService private def show_homework_info course,bid,current_user,is_course_teacher - author = bid.author.lastname + bid.author.firstname + author_real_name = bid.author.lastname + bid.author.firstname many_times = course.homeworks.index(bid) + 1 name = bid.name homework_count = bid.homeworks.count #已提交的作业数量 @@ -369,7 +369,7 @@ class CoursesService end #end open_anonymous_evaluation = bid.open_anonymous_evaluation - {:course_name => course.name,:id => bid.id, :course_teacher => author, :homework_times => many_times, :homework_name => name, :homework_count => homework_count,:student_questions_count => student_questions_count, + {:course_name => course.name,:id => bid.id, :author => bid.author,:author_real_name => author_real_name, :homework_times => many_times, :homework_name => name, :homework_count => homework_count,:student_questions_count => student_questions_count, :description => description, :homework_state => state,:open_anonymous_evaluation => open_anonymous_evaluation,:homework_for_anonymous_comments => homework_for_anonymous_comments} end diff --git a/app/services/homework_service.rb b/app/services/homework_service.rb index dd2d2356e..c5a333125 100644 --- a/app/services/homework_service.rb +++ b/app/services/homework_service.rb @@ -26,7 +26,7 @@ class HomeworkService state = @bid.comment_status #end open_anonymous_evaluation = @bid.open_anonymous_evaluation - {:course_name => course.name,:id => @bid.id, :course_teacher => author, :homework_times => many_times, :homework_name => name, :homework_count => homework_count,:student_questions_count => student_questions_count, + {:course_name => course.name,:id => @bid.id, :author => @bid.author,:author_real_name =>author, :homework_times => many_times, :homework_name => name, :homework_count => homework_count,:student_questions_count => student_questions_count, :description => description, :homework_state => state,:open_anonymous_evaluation => open_anonymous_evaluation} end From d999a9f4a64bc74b532029fd65a4fa0497bbd1e2 Mon Sep 17 00:00:00 2001 From: sw <939547590@qq.com> Date: Mon, 2 Mar 2015 14:33:35 +0800 Subject: [PATCH 42/68] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=96=B0=E5=BB=BA?= =?UTF-8?q?=E8=AF=BE=E7=A8=8B=E6=97=B6=E6=8A=A5=E9=94=99=E7=9A=84BUG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/courses_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb index b6fc21bf9..3c755ac41 100644 --- a/app/controllers/courses_controller.rb +++ b/app/controllers/courses_controller.rb @@ -499,7 +499,7 @@ class CoursesController < ApplicationController def create cs = CoursesService.new - @course = cs.create_course params,User.current + @course = cs.create_course(params,User.current)[:course] if @course.new_record? respond_to do |format| format.html { render :action => 'new', :layout => 'base' } #Added by young From f374bfd7e22acc466e1000ec6d014c2d25c668db Mon Sep 17 00:00:00 2001 From: alan <547533434@qq.com> Date: Mon, 2 Mar 2015 14:47:50 +0800 Subject: [PATCH 43/68] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E3=80=8A=E7=AA=97?= =?UTF-8?q?=E5=8F=A3=E8=B7=B3=E8=BD=AC=E9=97=AE=E9=A2=98=E3=80=8B=20Signed?= =?UTF-8?q?-off-by:=20alan=20<547533434@qq.com>?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Gemfile | 1 + app/controllers/account_controller.rb | 2 +- config/environments/development.rb | 6 +- config/environments/production.rb | 2 +- config/initializers/session_store.rb | 1 + lib/dalli-2.7.2/.yardoc/checksums | 13 + lib/dalli-2.7.2/.yardoc/object_types | Bin 0 -> 7548 bytes lib/dalli-2.7.2/.yardoc/objects/root.dat | Bin 0 -> 121591 bytes lib/dalli-2.7.2/.yardoc/proxy_types | Bin 0 -> 4 bytes lib/dalli-2.7.2/Gemfile | 12 + lib/dalli-2.7.2/History.md | 412 +++++++++++ lib/dalli-2.7.2/LICENSE | 20 + lib/dalli-2.7.2/Performance.md | 42 ++ lib/dalli-2.7.2/README.md | 224 ++++++ lib/dalli-2.7.2/Rakefile | 42 ++ lib/dalli-2.7.2/dalli.gemspec | 29 + .../middleware/session/dalli_store.rb | 81 ++ .../lib/active_support/cache/dalli_store.rb | 363 +++++++++ lib/dalli-2.7.2/lib/dalli.rb | 46 ++ lib/dalli-2.7.2/lib/dalli/cas/client.rb | 58 ++ lib/dalli-2.7.2/lib/dalli/client.rb | 439 +++++++++++ lib/dalli-2.7.2/lib/dalli/compressor.rb | 29 + lib/dalli-2.7.2/lib/dalli/options.rb | 64 ++ lib/dalli-2.7.2/lib/dalli/railtie.rb | 7 + lib/dalli-2.7.2/lib/dalli/ring.rb | 142 ++++ lib/dalli-2.7.2/lib/dalli/server.rb | 696 ++++++++++++++++++ lib/dalli-2.7.2/lib/dalli/socket.rb | 108 +++ lib/dalli-2.7.2/lib/dalli/version.rb | 3 + lib/dalli-2.7.2/lib/rack/session/dalli.rb | 75 ++ lib/dalli-2.7.2/test/benchmark_test.rb | 242 ++++++ lib/dalli-2.7.2/test/helper.rb | 55 ++ lib/dalli-2.7.2/test/memcached_mock.rb | 121 +++ lib/dalli-2.7.2/test/sasldb | 1 + lib/dalli-2.7.2/test/test_active_support.rb | 439 +++++++++++ lib/dalli-2.7.2/test/test_cas_client.rb | 107 +++ lib/dalli-2.7.2/test/test_compressor.rb | 53 ++ lib/dalli-2.7.2/test/test_dalli.rb | 625 ++++++++++++++++ lib/dalli-2.7.2/test/test_encoding.rb | 32 + lib/dalli-2.7.2/test/test_failover.rb | 128 ++++ lib/dalli-2.7.2/test/test_network.rb | 54 ++ lib/dalli-2.7.2/test/test_rack_session.rb | 341 +++++++++ lib/dalli-2.7.2/test/test_ring.rb | 85 +++ lib/dalli-2.7.2/test/test_sasl.rb | 110 +++ lib/dalli-2.7.2/test/test_serializer.rb | 30 + lib/dalli-2.7.2/test/test_server.rb | 80 ++ 45 files changed, 5416 insertions(+), 4 deletions(-) create mode 100644 config/initializers/session_store.rb create mode 100644 lib/dalli-2.7.2/.yardoc/checksums create mode 100644 lib/dalli-2.7.2/.yardoc/object_types create mode 100644 lib/dalli-2.7.2/.yardoc/objects/root.dat create mode 100644 lib/dalli-2.7.2/.yardoc/proxy_types create mode 100644 lib/dalli-2.7.2/Gemfile create mode 100644 lib/dalli-2.7.2/History.md create mode 100644 lib/dalli-2.7.2/LICENSE create mode 100644 lib/dalli-2.7.2/Performance.md create mode 100644 lib/dalli-2.7.2/README.md create mode 100644 lib/dalli-2.7.2/Rakefile create mode 100644 lib/dalli-2.7.2/dalli.gemspec create mode 100644 lib/dalli-2.7.2/lib/action_dispatch/middleware/session/dalli_store.rb create mode 100644 lib/dalli-2.7.2/lib/active_support/cache/dalli_store.rb create mode 100644 lib/dalli-2.7.2/lib/dalli.rb create mode 100644 lib/dalli-2.7.2/lib/dalli/cas/client.rb create mode 100644 lib/dalli-2.7.2/lib/dalli/client.rb create mode 100644 lib/dalli-2.7.2/lib/dalli/compressor.rb create mode 100644 lib/dalli-2.7.2/lib/dalli/options.rb create mode 100644 lib/dalli-2.7.2/lib/dalli/railtie.rb create mode 100644 lib/dalli-2.7.2/lib/dalli/ring.rb create mode 100644 lib/dalli-2.7.2/lib/dalli/server.rb create mode 100644 lib/dalli-2.7.2/lib/dalli/socket.rb create mode 100644 lib/dalli-2.7.2/lib/dalli/version.rb create mode 100644 lib/dalli-2.7.2/lib/rack/session/dalli.rb create mode 100644 lib/dalli-2.7.2/test/benchmark_test.rb create mode 100644 lib/dalli-2.7.2/test/helper.rb create mode 100644 lib/dalli-2.7.2/test/memcached_mock.rb create mode 100644 lib/dalli-2.7.2/test/sasldb create mode 100644 lib/dalli-2.7.2/test/test_active_support.rb create mode 100644 lib/dalli-2.7.2/test/test_cas_client.rb create mode 100644 lib/dalli-2.7.2/test/test_compressor.rb create mode 100644 lib/dalli-2.7.2/test/test_dalli.rb create mode 100644 lib/dalli-2.7.2/test/test_encoding.rb create mode 100644 lib/dalli-2.7.2/test/test_failover.rb create mode 100644 lib/dalli-2.7.2/test/test_network.rb create mode 100644 lib/dalli-2.7.2/test/test_rack_session.rb create mode 100644 lib/dalli-2.7.2/test/test_ring.rb create mode 100644 lib/dalli-2.7.2/test/test_sasl.rb create mode 100644 lib/dalli-2.7.2/test/test_serializer.rb create mode 100644 lib/dalli-2.7.2/test/test_server.rb diff --git a/Gemfile b/Gemfile index 1bc9e6629..3f07ad408 100644 --- a/Gemfile +++ b/Gemfile @@ -22,6 +22,7 @@ gem 'spreadsheet' gem 'ruby-ole' #gem 'email_verifier', path: 'lib/email_verifier' gem 'rufus-scheduler' +gem 'dalli', path: 'lib/dalli-2.7.2' group :development do gem 'grape-swagger' gem 'grape-swagger-ui', git: 'https://github.com/guange2015/grape-swagger-ui.git' diff --git a/app/controllers/account_controller.rb b/app/controllers/account_controller.rb index ff66b8d46..87c028354 100644 --- a/app/controllers/account_controller.rb +++ b/app/controllers/account_controller.rb @@ -315,7 +315,7 @@ class AccountController < ApplicationController code = /\d*/ #根据home_url生产正则表达式 eval("code = " + "/^" + home_url.gsub(/\//,"\\\/") + "\\\/*(welcome)?\\\/*(\\\/index\\\/*.*)?\$/") - if code=~params[:back_url] && last_login_on != '' + if (code=~params[:back_url] || params[:back_url].to_s.include?('lost_password')) && last_login_on != '' redirect_to user_activities_path(user) else if last_login_on == '' diff --git a/config/environments/development.rb b/config/environments/development.rb index 3b894d207..80d1c9db8 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -8,10 +8,12 @@ RedmineApp::Application.configure do # Log error messages when you accidentally call methods on nil. config.whiny_nils = true config.logger = Logger.new('log/development.log', 'daily') # daily, weekly or monthly + + # Show full error reports and disable caching config.consider_all_requests_local = true - config.action_controller.perform_caching = false - config.cache_store = :file_store, "#{Rails.root }/public/tmp/" + config.action_controller.perform_caching = true + config.cache_store = :dalli_store # Don't care if the mailer can't send config.action_mailer.raise_delivery_errors = true diff --git a/config/environments/production.rb b/config/environments/production.rb index 48b2514cf..0b61ed132 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -3,7 +3,7 @@ RedmineApp::Application.configure do # The production environment is meant for finished, "live" apps. # Code is not reloaded between requests config.cache_classes = true - + config.cache_store = :dalli_store ##### # Customize the default logger (http://ruby-doc.org/core/classes/Logger.html) # diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb new file mode 100644 index 000000000..603b22a9f --- /dev/null +++ b/config/initializers/session_store.rb @@ -0,0 +1 @@ +Rails.application.config.session_store ActionDispatch::Session::CacheStore, :expire_after => 20.minutes \ No newline at end of file diff --git a/lib/dalli-2.7.2/.yardoc/checksums b/lib/dalli-2.7.2/.yardoc/checksums new file mode 100644 index 000000000..10e718cb2 --- /dev/null +++ b/lib/dalli-2.7.2/.yardoc/checksums @@ -0,0 +1,13 @@ +lib/dalli.rb b1fd9d39df06608fcae5bcf46e9940f95b186d22 +lib/dalli/ring.rb e2cd42d8b963e669e2c8a83791fa56ec94f9ec55 +lib/dalli/server.rb a42d734f9b3d654886c86f04fb4f0352e7147b1e +lib/dalli/client.rb 11afa0d702c68a151c87ea6e7ccdc863cf03884f +lib/dalli/socket.rb 18b7243332ec2dafa9a17c195944e321b684e67e +lib/dalli/options.rb d7ecb4c52b4ae2b222f319813234297e0951f82a +lib/dalli/version.rb fff3231b7f52d7fa1dabb78bf1f67dcef95c5378 +lib/dalli/railtie.rb 63dc0fe85790a10225e867774f2c611d1c1ac46c +lib/dalli/compressor.rb 13b0cf3f607bd8bc9f969679b0b6e9dcb0a059d7 +lib/dalli/cas/client.rb 983ded7ec738ed4502658150123e9c5ad7e3faa1 +lib/rack/session/dalli.rb 2696ad72e8f9d7f5ceb232db0c8d9a8916192edb +lib/active_support/cache/dalli_store.rb 010d880e0f297d92b26c8f44e446add9d4fedfa2 +lib/action_dispatch/middleware/session/dalli_store.rb 62236273ea28a91502871f31aa600e038358931a diff --git a/lib/dalli-2.7.2/.yardoc/object_types b/lib/dalli-2.7.2/.yardoc/object_types new file mode 100644 index 0000000000000000000000000000000000000000..81127d629334e414b5938c25e1ed54a68043afbc GIT binary patch literal 7548 zcmb7J>u=md5C;hbD5?Z4Pzt4`xwiC0%1eM;A#_}v_S7VI+@)0^A6Blt$yxE)#$M-9 z{xSYMc0Dupdg5lomp=TPnVr|p{&w%3_x`!yygHKUL3DpSPzmU zUh!7^jq&QI^?y8XXHm!?DuX?_^7vi-{ry=G9>6~@nSXlTT$W{=rTA?as`yaOt31yN z1>dfAzIom#2p4GWvVpR6JudS=g$t0s*8SL7!MhM9L0Rs8+K8>;fcj_X`L}GHf^uPO0`!%wsGFrY5n)!Ps+IWClEBUz90o~+9$)W)!Esn% zwFl=>Q%azR2aBv!X|TivC^QCQHfdiV+71TVcou*vR8&NcWW2w?O+rQOK&;@1Ww{wH z*e5Ii#QI$(IU?Z3$4uv^RkeCSF)sei0MX~y0s4FZ%_%Akub?hOff@DTVdz0 zrK;4znERzHpp&t)7#y+@^L5wJrQWEq&@(94NtL2h0ZsUaX9^m#iBvMeAn%_koo4B) zcGH~gLh!0NWN24nQuUmT_fCwD3KPiczo#csRY=Ss%Wjk%rHIh@MY~nb_A7p`bA$0Q zm3@hIr+thnrA#AH>opQgvm{OSJ%}P`7gqX0=1CA@yle|a(v+aL%l4A0q@%Z6nn;F) zh|>_;g>}0@!5{-#4OYIRB34oad2T}2reI18bN0>&!K|<3M%c%E*RCS{v&WaTV_?@k$JG7-uY&Dft4H@cIq@*1T5inmO z?F;U3z;AW6qd)~hJJf7qkfQmI5$#t&p*u4;7S>c*~)L_JQP7Ox<-a8l( zvj-#13Nlv@D)5<#k5KQdK&)q;+KAA%EU-zPh-f=D8PE<|mN{!qFr;T*;VP%*6%X3b z5k@UgwRdD8)EesdJv$apnWe`)3XJoL4=O+?y~~3LmkP4xGgp_O^-Aa zod_RxecL~c1k*MwtHCH^sEzZz#i?x&hZ*0(*ZbZ_26>j@V7}3dFDoUFAxpP<1^Bsn zr&p?GMrMPTF>Zp}Z;;cp(tca(7pvK)Eq1Thnx=-T$n=fK79I48*2AT}f_>w9F&dO{ zB2yYeZt>0>Sv#+FV<2 z$#omHsPaQ+c=9+d6_&;=ZF4PmvTFivkiXmNc**w9z6$}}(6x{g? z6B*z=*}XT6a9ZJ=)@|FOd2ZpF`9Y_+y4?n8d`vrMW$TwraK~L)xb9mj-#aUd zL;rv2$m2XP9uj%>q67bg1ef5g;Y%?b zjr{4(@b^t$_%Em9*>Gn(*%mqxIs?N^aKHNqp?_%c|9I}tcly(#Lz+&u=RTlxt)nFg z6{E?zk16ElCsPX~4oC*1iEbd~2iU~u++9~o*2E+$ix8+7_- Y*T&Vq!puj*sV|=Se~Qu5;jC8t4{6(|>Hq)$ literal 0 HcmV?d00001 diff --git a/lib/dalli-2.7.2/.yardoc/objects/root.dat b/lib/dalli-2.7.2/.yardoc/objects/root.dat new file mode 100644 index 0000000000000000000000000000000000000000..9e329595266921d3866db8cfbe420e10d2bfb28b GIT binary patch literal 121591 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 literal 0 HcmV?d00001 diff --git a/lib/dalli-2.7.2/.yardoc/proxy_types b/lib/dalli-2.7.2/.yardoc/proxy_types new file mode 100644 index 0000000000000000000000000000000000000000..beefda1ae32c2cef8eb53a4f3c8407a532a22b51 GIT binary patch literal 4 LcmZSKsAd2F0U`j1 literal 0 HcmV?d00001 diff --git a/lib/dalli-2.7.2/Gemfile b/lib/dalli-2.7.2/Gemfile new file mode 100644 index 000000000..aeb2bb358 --- /dev/null +++ b/lib/dalli-2.7.2/Gemfile @@ -0,0 +1,12 @@ +source 'https://rubygems.org' + +gemspec + +gem 'rake' +gem 'kgio', :platform => :mri +gem 'appraisal' +gem 'connection_pool' + +group :test do + gem 'simplecov' +end diff --git a/lib/dalli-2.7.2/History.md b/lib/dalli-2.7.2/History.md new file mode 100644 index 000000000..3ba705502 --- /dev/null +++ b/lib/dalli-2.7.2/History.md @@ -0,0 +1,412 @@ +Dalli Changelog +===================== + +2.7.2 +========== + +- The fix for #423 didn't make it into the released 2.7.1 gem somehow. + +2.7.1 +========== + +- Rack session will check if servers are up on initialization (arthurnn, #423) +- Add support for IPv6 addresses in hex form, ie: "[::1]:11211" (dplummer, #428) +- Add symbol support for namespace (jingkai #431) +- Support expiration intervals longer than 30 days (leonid-shevtsov #436) + +2.7.0 +========== + +- BREAKING CHANGE: + Dalli::Client#add and #replace now return a truthy value, not boolean true or false. +- Multithreading support with dalli\_store: + Use :pool\_size to create a pool of shared, threadsafe Dalli clients in Rails: +```ruby + config.cache_store = :dalli_store, "cache-1.example.com", "cache-2.example.com", :compress => true, :pool_size => 5, :expires_in => 300 +``` + This will ensure the Rails.cache singleton does not become a source of contention. + **PLEASE NOTE** Rails's :mem\_cache\_store does not support pooling as of +Rails 4.0. You must use :dalli\_store. + +- Implement `version` for retrieving version of connected servers [dterei, #384] +- Implement `fetch_multi` for batched read/write [sorentwo, #380] +- Add more support for safe updates with multiple writers: [philipmw, #395] + `require 'dalli/cas/client'` augments Dalli::Client with the following methods: + * Get value with CAS: `[value, cas] = get_cas(key)` + `get_cas(key) {|value, cas| ...}` + * Get multiple values with CAS: `get_multi_cas(k1, k2, ...) {|value, metadata| cas = metadata[:cas]}` + * Set value with CAS: `new_cas = set_cas(key, value, cas, ttl, options)` + * Replace value with CAS: `replace_cas(key, new_value, cas, ttl, options)` + * Delete value with CAS: `delete_cas(key, cas)` +- Fix bug with get key with "Not found" value [uzzz, #375] + +2.6.4 +======= + +- Fix ADD command, aka `write(unless_exist: true)` (pitr, #365) +- Upgrade test suite from mini\_shoulda to minitest. +- Even more performance improvements for get\_multi (xaop, #331) + +2.6.3 +======= + +- Support specific stats by passing `:items` or `:slabs` to `stats` method [bukhamseen] +- Fix 'can't modify frozen String' errors in `ActiveSupport::Cache::DalliStore` [dblock] +- Protect against objects with custom equality checking [theron17] +- Warn if value for key is too large to store [locriani] + +2.6.2 +======= + +- Properly handle missing RubyInline + +2.6.1 +======= + +- Add optional native C binary search for ring, add: + +gem 'RubyInline' + + to your Gemfile to get a 10% speedup when using many servers. + You will see no improvement if you are only using one server. + +- More get_multi performance optimization [xaop, #315] +- Add lambda support for cache namespaces [joshwlewis, #311] + +2.6.0 +======= + +- read_multi optimization, now checks local_cache [chendo, #306] +- Re-implement get_multi to be non-blocking [tmm1, #295] +- Add `dalli` accessor to dalli_store to access the underlying +Dalli::Client, for things like `get_multi`. +- Add `Dalli::GzipCompressor`, primarily for compatibility with nginx's HttpMemcachedModule using `memcached_gzip_flag` + +2.5.0 +======= + +- Don't escape non-ASCII keys, memcached binary protocol doesn't care. [#257] +- :dalli_store now implements LocalCache [#236] +- Removed lots of old session_store test code, tests now all run without a default memcached server [#275] +- Changed Dalli ActiveSupport adapter to always attempt instrumentation [brianmario, #284] +- Change write operations (add/set/replace) to return false when value is too large to store [brianmario, #283] +- Allowing different compressors per client [naseem] + +2.4.0 +======= +- Added the ability to swap out the compressed used to [de]compress cache data [brianmario, #276] +- Fix get\_multi performance issues with lots of memcached servers [tmm1] +- Throw more specific exceptions [tmm1] +- Allowing different types of serialization per client [naseem] + +2.3.0 +======= +- Added the ability to swap out the serializer used to [de]serialize cache data [brianmario, #274] + +2.2.1 +======= + +- Fix issues with ENV-based connections. [#266] +- Fix problem with SessionStore in Rails 4.0 [#265] + +2.2.0 +======= + +- Add Rack session with\_lock helper, for Rails 4.0 support [#264] +- Accept connection string in the form of a URL (e.g., memcached://user:pass@hostname:port) [glenngillen] +- Add touch operation [#228, uzzz] + +2.1.0 +======= + +- Add Railtie to auto-configure Dalli when included in Gemfile [#217, steveklabnik] + +2.0.5 +======= + +- Create proper keys for arrays of objects passed as keys [twinturbo, #211] +- Handle long key with namespace [#212] +- Add NODELAY to TCP socket options [#206] + +2.0.4 +======= + +- Dalli no longer needs to be reset after Unicorn/Passenger fork [#208] +- Add option to re-raise errors rescued in the session and cache stores. [pitr, #200] +- DalliStore#fetch called the block if the cached value == false [#205] +- DalliStore should have accessible options [#195] +- Add silence and mute support for DalliStore [#207] +- Tracked down and fixed socket corruption due to Timeout [#146] + +2.0.3 +======= + +- Allow proper retrieval of stored `false` values [laserlemon, #197] +- Allow non-ascii and whitespace keys, only the text protocol has those restrictions [#145] +- Fix DalliStore#delete error-handling [#196] + +2.0.2 +======= + +- Fix all dalli\_store operations to handle nil options [#190] +- Increment and decrement with :initial => nil now return nil (lawrencepit, #112) + +2.0.1 +======= + +- Fix nil option handling in dalli\_store#write [#188] + +2.0.0 +======= + +- Reimplemented the Rails' dalli\_store to remove use of + ActiveSupport::Cache::Entry which added 109 bytes overhead to every + value stored, was a performance bottleneck and duplicated a lot of + functionality already in Dalli. One benchmark went from 4.0 sec to 3.0 + sec with the new dalli\_store. [#173] +- Added reset\_stats operation [#155] +- Added support for configuring keepalive on TCP connections to memcached servers (@bianster, #180) + +Notes: + + * data stored with dalli\_store 2.x is NOT backwards compatible with 1.x. + Upgraders are advised to namespace their keys and roll out the 2.x + upgrade slowly so keys do not clash and caches are warmed. + `config.cache_store = :dalli_store, :expires_in => 24.hours.to_i, :namespace => 'myapp2'` + * data stored with plain Dalli::Client API is unchanged. + * removed support for dalli\_store's race\_condition\_ttl option. + * removed support for em-synchrony and unix socket connection options. + * removed support for Ruby 1.8.6 + * removed memcache-client compability layer and upgrade documentation. + + +1.1.5 +======= + +- Coerce input to incr/decr to integer via #to\_i [#165] +- Convert test suite to minitest/spec (crigor, #166) +- Fix encoding issue with keys [#162] +- Fix double namespacing with Rails and dalli\_store. [#160] + +1.1.4 +======= + +- Use 127.0.0.1 instead of localhost as default to avoid IPv6 issues +- Extend DalliStore's :expires\_in when :race\_condition\_ttl is also used. +- Fix :expires\_in option not propogating from DalliStore to Client, GH-136 +- Added support for native Rack session store. Until now, Dalli's + session store has required Rails. Now you can use Dalli to store + sessions for any Rack application. + + require 'rack/session/dalli' + use Rack::Session::Dalli, :memcache_server => 'localhost:11211', :compression => true + +1.1.3 +======= + +- Support Rails's autoloading hack for loading sessions with objects + whose classes have not be required yet, GH-129 +- Support Unix sockets for connectivity. Shows a 2x performance + increase but keep in mind they only work on localhost. (dfens) + +1.1.2 +======= + +- Fix incompatibility with latest Rack session API when destroying + sessions, thanks @twinge! + +1.1.1 +======= + +v1.1.0 was a bad release. Yanked. + +1.1.0 +======= + +- Remove support for Rails 2.3, add support for Rails 3.1 +- Fix socket failure retry logic, now you can restart memcached and Dalli won't complain! +- Add support for fibered operation via em-synchrony (eliaslevy) +- Gracefully handle write timeouts, GH-99 +- Only issue bug warning for unexpected StandardErrors, GH-102 +- Add travis-ci build support (ryanlecompte) +- Gracefully handle errors in get_multi (michaelfairley) +- Misc fixes from crash2burn, fphilipe, igreg, raggi + +1.0.5 +======= + +- Fix socket failure retry logic, now you can restart memcached and Dalli won't complain! + +1.0.4 +======= + +- Handle non-ASCII key content in dalli_store +- Accept key array for read_multi in dalli_store +- Fix multithreaded race condition in creation of mutex + +1.0.3 +======= + +- Better handling of application marshalling errors +- Work around jruby IO#sysread compatibility issue + + +1.0.2 +======= + + - Allow browser session cookies (blindsey) + - Compatibility fixes (mwynholds) + - Add backwards compatibility module for memcache-client, require 'dalli/memcache-client'. It makes + Dalli more compatible with memcache-client and prints out a warning any time you do something that + is no longer supported so you can fix your code. + +1.0.1 +======= + + - Explicitly handle application marshalling bugs, GH-56 + - Add support for username/password as options, to allow multiple bucket access + from the same Ruby process, GH-52 + - Add support for >1MB values with :value_max_bytes option, GH-54 (r-stu31) + - Add support for default TTL, :expires_in, in Rails 2.3. (Steven Novotny) + config.cache_store = :dalli_store, 'localhost:11211', {:expires_in => 4.hours} + + +1.0.0 +======= + +Welcome gucki as a Dalli committer! + + - Fix network and namespace issues in get_multi (gucki) + - Better handling of unmarshalling errors (mperham) + +0.11.2 +======= + + - Major reworking of socket error and failover handling (gucki) + - Add basic JRuby support (mperham) + +0.11.1 +====== + + - Minor fixes, doc updates. + - Add optional support for kgio sockets, gives a 10-15% performance boost. + +0.11.0 +====== + +Warning: this release changes how Dalli marshals data. I do not guarantee compatibility until 1.0 but I will increment the minor version every time a release breaks compatibility until 1.0. + +IT IS HIGHLY RECOMMENDED YOU FLUSH YOUR CACHE BEFORE UPGRADING. + + - multi() now works reentrantly. + - Added new Dalli::Client option for default TTLs, :expires_in, defaults to 0 (aka forever). + - Added new Dalli::Client option, :compression, to enable auto-compression of values. + - Refactor how Dalli stores data on the server. Values are now tagged + as "marshalled" or "compressed" so they can be automatically deserialized + without the client having to know how they were stored. + +0.10.1 +====== + + - Prefer server config from environment, fixes Heroku session store issues (thanks JoshMcKin) + - Better handling of non-ASCII values (size -> bytesize) + - Assert that keys are ASCII only + +0.10.0 +====== + +Warning: this release changed how Rails marshals data with Dalli. Unfortunately previous versions double marshalled values. It is possible that data stored with previous versions of Dalli will not work with this version. + +IT IS HIGHLY RECOMMENDED YOU FLUSH YOUR CACHE BEFORE UPGRADING. + + - Rework how the Rails cache store does value marshalling. + - Rework old server version detection to avoid a socket read hang. + - Refactor the Rails 2.3 :dalli\_store to be closer to :mem\_cache\_store. + - Better documentation for session store config (plukevdh) + +0.9.10 +---- + + - Better server retry logic (next2you) + - Rails 3.1 compatibility (gucki) + + +0.9.9 +---- + + - Add support for *_multi operations for add, set, replace and delete. This implements + pipelined network operations; Dalli disables network replies so we're not limited by + latency, allowing for much higher throughput. + + dc = Dalli::Client.new + dc.multi do + dc.set 'a', 1 + dc.set 'b', 2 + dc.set 'c', 3 + dc.delete 'd' + end + - Minor fix to set the continuum sorted by value (kangster) + - Implement session store with Rails 2.3. Update docs. + +0.9.8 +----- + + - Implement namespace support + - Misc fixes + + +0.9.7 +----- + + - Small fix for NewRelic integration. + - Detect and fail on older memcached servers (pre-1.4). + +0.9.6 +----- + + - Patches for Rails 3.0.1 integration. + +0.9.5 +----- + + - Major design change - raw support is back to maximize compatibility with Rails + and the increment/decrement operations. You can now pass :raw => true to most methods + to bypass (un)marshalling. + - Support symbols as keys (ddollar) + - Rails 2.3 bug fixes + + +0.9.4 +----- + + - Dalli support now in rack-bug (http://github.com/brynary/rack-bug), give it a try! + - Namespace support for Rails 2.3 (bpardee) + - Bug fixes + + +0.9.3 +----- + + - Rails 2.3 support (beanieboi) + - Rails SessionStore support + - Passenger integration + - memcache-client upgrade docs, see Upgrade.md + + +0.9.2 +---- + + - Verify proper operation in Heroku. + + +0.9.1 +---- + + - Add fetch and cas operations (mperham) + - Add incr and decr operations (mperham) + - Initial support for SASL authentication via the MEMCACHE_{USERNAME,PASSWORD} environment variables, needed for Heroku (mperham) + +0.9.0 +----- + + - Initial gem release. diff --git a/lib/dalli-2.7.2/LICENSE b/lib/dalli-2.7.2/LICENSE new file mode 100644 index 000000000..c9f5cacab --- /dev/null +++ b/lib/dalli-2.7.2/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) Mike Perham + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/dalli-2.7.2/Performance.md b/lib/dalli-2.7.2/Performance.md new file mode 100644 index 000000000..1079be259 --- /dev/null +++ b/lib/dalli-2.7.2/Performance.md @@ -0,0 +1,42 @@ +Performance +==================== + +Caching is all about performance, so I carefully track Dalli performance to ensure no regressions. +You can optionally use kgio to give Dalli a 10-20% performance boost: `gem install kgio`. + +Note I've added some benchmarks over time to Dalli that the other libraries don't necessarily have. + +memcache-client +--------------- + +Testing 1.8.5 with ruby 1.9.3p0 (2011-10-30 revision 33570) [x86_64-darwin11.2.0] + + user system total real + set:plain:memcache-client 1.860000 0.310000 2.170000 ( 2.188030) + set:ruby:memcache-client 1.830000 0.290000 2.120000 ( 2.130212) + get:plain:memcache-client 1.830000 0.340000 2.170000 ( 2.176156) + get:ruby:memcache-client 1.900000 0.330000 2.230000 ( 2.235045) + multiget:ruby:memcache-client 0.860000 0.120000 0.980000 ( 0.987348) + missing:ruby:memcache-client 1.630000 0.320000 1.950000 ( 1.954867) + mixed:ruby:memcache-client 3.690000 0.670000 4.360000 ( 4.364469) + + +dalli +----- + +Testing with Rails 3.2.1 +Using kgio socket IO +Testing 2.0.0 with ruby 1.9.3p125 (2012-02-16 revision 34643) [x86_64-darwin11.3.0] + + user system total real + mixed:rails:dalli 1.580000 0.570000 2.150000 ( 3.008839) + set:plain:dalli 0.730000 0.300000 1.030000 ( 1.567098) + setq:plain:dalli 0.520000 0.120000 0.640000 ( 0.634402) + set:ruby:dalli 0.800000 0.300000 1.100000 ( 1.640348) + get:plain:dalli 0.840000 0.330000 1.170000 ( 1.668425) + get:ruby:dalli 0.850000 0.330000 1.180000 ( 1.665716) + multiget:ruby:dalli 0.700000 0.260000 0.960000 ( 0.965423) + missing:ruby:dalli 0.720000 0.320000 1.040000 ( 1.511720) + mixed:ruby:dalli 1.660000 0.640000 2.300000 ( 3.320743) + mixedq:ruby:dalli 1.630000 0.510000 2.140000 ( 2.629734) + incr:ruby:dalli 0.270000 0.100000 0.370000 ( 0.547618) diff --git a/lib/dalli-2.7.2/README.md b/lib/dalli-2.7.2/README.md new file mode 100644 index 000000000..d53093c4d --- /dev/null +++ b/lib/dalli-2.7.2/README.md @@ -0,0 +1,224 @@ +Dalli [![Build Status](https://secure.travis-ci.org/mperham/dalli.png)](http://travis-ci.org/mperham/dalli) [![Dependency Status](https://gemnasium.com/mperham/dalli.png)](https://gemnasium.com/mperham/dalli) [![Code Climate](https://codeclimate.com/github/mperham/dalli.png)](https://codeclimate.com/github/mperham/dalli) +===== + +Dalli is a high performance pure Ruby client for accessing memcached servers. It works with memcached 1.4+ only as it uses the newer binary protocol. It should be considered a replacement for the memcache-client gem. + +The name is a variant of Salvador Dali for his famous painting [The Persistence of Memory](http://en.wikipedia.org/wiki/The_Persistence_of_Memory). + +![Persistence of Memory](http://www.virtualdali.com/assets/paintings/31PersistenceOfMemory.jpg) + +Dalli's initial development was sponsored by [CouchBase](http://www.couchbase.com/). Many thanks to them! + + +Design +------------ + +I decided to write Dalli after maintaining memcache-client for two years for a few specific reasons: + + 0. The code is mostly old and gross. The bulk of the code is a single 1000 line .rb file. + 1. It has a lot of options that are infrequently used which complicate the codebase. + 2. The implementation has no single point to attach monitoring hooks. + 3. Uses the old text protocol, which hurts raw performance. + +So a few notes. Dalli: + + 0. uses the exact same algorithm to choose a server so existing memcached clusters with TBs of data will work identically to memcache-client. + 1. is approximately 20% faster than memcache-client (which itself was heavily optimized) in Ruby 1.9.2. + 2. contains explicit "chokepoint" methods which handle all requests; these can be hooked into by monitoring tools (NewRelic, Rack::Bug, etc) to track memcached usage. + 3. supports SASL for use in managed environments, e.g. Heroku. + 4. provides proper failover with recovery and adjustable timeouts + + +Supported Ruby versions and implementations +------------------------------------------------ + +Dalli should work identically on: + + * JRuby 1.6+ + * Ruby 1.9.3+ + * Rubinius 2.0 + +If you have problems, please enter an issue. + + +Installation and Usage +------------------------ + +Remember, Dalli **requires** memcached 1.4+. You can check the version with `memcached -h`. Please note that memcached that Mac OS X Snow Leopard ships with is 1.2.8 and won't work. Install 1.4.x using Homebrew with + + brew install memcached + +On Ubuntu you can install it by running: + + apt-get install memcached + +You can verify your installation using this piece of code: + +```bash +gem install dalli +``` + +```ruby +require 'dalli' +options = { :namespace => "app_v1", :compress => true } +dc = Dalli::Client.new('localhost:11211', options) +dc.set('abc', 123) +value = dc.get('abc') +``` + +The test suite requires memcached 1.4.3+ with SASL enabled (brew install memcached --enable-sasl ; mv /usr/bin/memcached /usr/bin/memcached.old). Currently only supports the PLAIN mechanism. + +Dalli has no runtime dependencies and never will. You can optionally install the 'kgio' gem to +give Dalli a 20-30% performance boost. + + +Usage with Rails 3.x and 4.x +--------------------------- + +In your Gemfile: + +```ruby +gem 'dalli' +``` + +In `config/environments/production.rb`: + +```ruby +config.cache_store = :dalli_store +``` + +Here's a more comprehensive example that sets a reasonable default for maximum cache entry lifetime (one day), enables compression for large values and namespaces all entries for this rails app. Remove the namespace if you have multiple apps which share cached values. + +```ruby +config.cache_store = :dalli_store, 'cache-1.example.com', 'cache-2.example.com', + { :namespace => NAME_OF_RAILS_APP, :expires_in => 1.day, :compress => true } +``` + +If your servers are specified in `ENV["MEMCACHE_SERVERS"]` (e.g. on Heroku when using a third-party hosted addon), simply provide `nil` for the servers: + +```ruby +config.cache_store = :dalli_store, nil, { :namespace => NAME_OF_RAILS_APP, :expires_in => 1.day, :compress => true } +``` + +To use Dalli for Rails session storage that times out after 20 minutes, in `config/initializers/session_store.rb`: + +For Rails >= 3.2.4: + +```ruby +Rails.application.config.session_store ActionDispatch::Session::CacheStore, :expire_after => 20.minutes +``` + +For Rails 3.x: + +```ruby +require 'action_dispatch/middleware/session/dalli_store' +Rails.application.config.session_store :dalli_store, :memcache_server => ['host1', 'host2'], :namespace => 'sessions', :key => '_foundation_session', :expire_after => 20.minutes +``` + +Dalli does not support Rails 2.x. + + +Multithreading and Rails +-------------------------- + +If you use Puma or another threaded app server, as of Dalli 2.7, you can use a pool +of Dalli clients with Rails to ensure the `Rails.cache` singleton does not become a +source of thread contention. You must add `gem 'connection_pool'` to your Gemfile and +add :pool\_size to your `dalli_store` config: + +```ruby +config.cache_store = :dalli_store, 'cache-1.example.com', { :pool_size => 5 } +``` + +You can then use the Rails cache as normal or check out a Dalli client directly from the pool: + +```ruby +Rails.cache.fetch('foo', :expires_in => 300) do + 'bar' +end + +Rails.cache.dalli.with do |client| + # client is a Dalli::Client instance which you can + # use ONLY within this block +end +``` + + +Configuration +------------------------ + +Dalli::Client accepts the following options. All times are in seconds. + +**expires_in**: Global default for key TTL. Default is 0, which means no expiry. + +**failover**: Boolean, if true Dalli will failover to another server if the main server for a key is down. + +**compress**: Boolean, if true Dalli will gzip-compress values larger than 1K. + +**compression_min_size**: Minimum value byte size for which to attempt compression. Default is 1K. + +**compression_max_size**: Maximum value byte size for which to attempt compression. Default is unlimited. + +**serializer**: The serializer to use for objects being stored (ex. JSON). +Default is Marshal. + +**socket_timeout**: Timeout for all socket operations (connect, read, write). Default is 0.5. + +**socket_max_failures**: When a socket operation fails after socket_timeout, the same operation is retried. This is to not immediately mark a server down when there's a very slight network problem. Default is 2. + +**socket_failure_delay**: Before retrying a socket operation, the process sleeps for this amount of time. Default is 0.01. Set to nil for no delay. + +**down_retry_delay**: When a server has been marked down due to many failures, the server will be checked again for being alive only after this amount of time. Don't set this value to low, otherwise each request which tries the failed server might hang for the maximum **socket_timeout**. Default is 1 second. + +**value_max_bytes**: The maximum size of a value in memcached. Defaults to 1MB, this can be increased with memcached's -I parameter. You must also configure Dalli to allow the larger size here. + +**username**: The username to use for authenticating this client instance against a SASL-enabled memcached server. Heroku users should not need to use this normally. + +**password**: The password to use for authenticating this client instance against a SASL-enabled memcached server. Heroku users should not need to use this normally. + +**keepalive**: Boolean. If true, Dalli will enable keep-alive for socket connections. Default is true. + +**compressor**: The compressor to use for objects being stored. +Default is zlib, implemented under `Dalli::Compressor`. +If serving compressed data using nginx's HttpMemcachedModule, set `memcached_gzip_flag 2` and use `Dalli::GzipCompressor` + +Features and Changes +------------------------ + +By default, Dalli is thread-safe. Disable thread-safety at your own peril. + +Dalli does not need anything special in Unicorn/Passenger since 2.0.4. +It will detect sockets shared with child processes and gracefully reopen the +socket. + +Note that Dalli does not require ActiveSupport or Rails. You can safely use it in your own Ruby projects. + +[View the Client API](http://www.rubydoc.info/github/mperham/dalli/Dalli/Client) + +Helping Out +------------- + +If you have a fix you wish to provide, please fork the code, fix in your local project and then send a pull request on github. Please ensure that you include a test which verifies your fix and update History.md with a one sentence description of your fix so you get credit as a contributor. + +We're not accepting new compressors. They are trivial to add in an initializer. See #385 (LZ4), #406 (Snappy) + +Thanks +------------ + +Eric Wong - for help using his [kgio](http://unicorn.bogomips.org/kgio/index.html) library. + +Brian Mitchell - for his remix-stash project which was helpful when implementing and testing the binary protocol support. + +[CouchBase](http://couchbase.com) - for their project sponsorship + + +Author +---------- + +Mike Perham, [mikeperham.com](http://mikeperham.com), [@mperham](http://twitter.com/mperham) + + +Copyright +----------- + +Copyright (c) Mike Perham. See LICENSE for details. diff --git a/lib/dalli-2.7.2/Rakefile b/lib/dalli-2.7.2/Rakefile new file mode 100644 index 000000000..61752a9a8 --- /dev/null +++ b/lib/dalli-2.7.2/Rakefile @@ -0,0 +1,42 @@ +require 'appraisal' +require 'rake/testtask' +Rake::TestTask.new(:test) do |test| + test.libs << 'test' + test.pattern = 'test/**/test_*.rb' + test.warning = true + test.verbose = true +end + +Rake::TestTask.new(:bench) do |test| + test.libs << 'test' + test.pattern = 'test/benchmark_test.rb' +end + +begin + require 'metric_fu' + MetricFu::Configuration.run do |config| + config.rcov[:rcov_opts] << "-Itest:lib" + end +rescue LoadError +end + +task :default => :test + +task :test_all do + system('rake test RAILS_VERSION="~> 3.0.0"') + system('rake test RAILS_VERSION=">= 3.0.0"') +end + +# 'gem install rdoc' to upgrade RDoc if this is giving you errors +begin + require 'rdoc/task' + RDoc::Task.new do |rd| + rd.rdoc_files.include("lib/**/*.rb") + end +rescue LoadError + puts "Unable to load rdoc, run 'gem install rdoc' to fix this." +end + +require 'rake/clean' +CLEAN.include "**/*.rbc" +CLEAN.include "**/.DS_Store" diff --git a/lib/dalli-2.7.2/dalli.gemspec b/lib/dalli-2.7.2/dalli.gemspec new file mode 100644 index 000000000..3d4cf537f --- /dev/null +++ b/lib/dalli-2.7.2/dalli.gemspec @@ -0,0 +1,29 @@ +require './lib/dalli/version' + +Gem::Specification.new do |s| + s.name = %q{dalli} + s.version = Dalli::VERSION + s.license = "MIT" + + s.authors = ["Mike Perham"] + s.description = %q{High performance memcached client for Ruby} + s.email = %q{mperham@gmail.com} + s.files = Dir.glob("lib/**/*") + [ + "LICENSE", + "README.md", + "History.md", + "Rakefile", + "Gemfile", + "dalli.gemspec", + "Performance.md", + ] + s.homepage = %q{http://github.com/mperham/dalli} + s.rdoc_options = ["--charset=UTF-8"] + s.require_paths = ["lib"] + s.summary = %q{High performance memcached client for Ruby} + s.test_files = Dir.glob("test/**/*") + s.add_development_dependency(%q, [">= 4.2.0"]) + s.add_development_dependency(%q, [">= 0"]) + s.add_development_dependency(%q, ["~> 4"]) +end + diff --git a/lib/dalli-2.7.2/lib/action_dispatch/middleware/session/dalli_store.rb b/lib/dalli-2.7.2/lib/action_dispatch/middleware/session/dalli_store.rb new file mode 100644 index 000000000..abbbaa290 --- /dev/null +++ b/lib/dalli-2.7.2/lib/action_dispatch/middleware/session/dalli_store.rb @@ -0,0 +1,81 @@ +require 'active_support/cache' +require 'action_dispatch/middleware/session/abstract_store' +require 'dalli' + +# Dalli-based session store for Rails 3.0. +module ActionDispatch + module Session + class DalliStore < AbstractStore + def initialize(app, options = {}) + # Support old :expires option + options[:expire_after] ||= options[:expires] + + super + + @default_options = { :namespace => 'rack:session' }.merge(@default_options) + + @pool = options[:cache] || begin + Dalli::Client.new( + @default_options[:memcache_server], @default_options) + end + @namespace = @default_options[:namespace] + + @raise_errors = !!@default_options[:raise_errors] + + super + end + + def reset + @pool.reset + end + + private + + def get_session(env, sid) + sid = generate_sid unless sid and !sid.empty? + begin + session = @pool.get(sid) || {} + rescue Dalli::DalliError => ex + # re-raise ArgumentError so Rails' session abstract_store.rb can autoload any missing models + raise ArgumentError, ex.message if ex.message =~ /unmarshal/ + Rails.logger.warn("Session::DalliStore#get: #{ex.message}") + session = {} + end + [sid, session] + end + + def set_session(env, sid, session_data, options = nil) + options ||= env[ENV_SESSION_OPTIONS_KEY] + expiry = options[:expire_after] + @pool.set(sid, session_data, expiry) + sid + rescue Dalli::DalliError + Rails.logger.warn("Session::DalliStore#set: #{$!.message}") + raise if @raise_errors + false + end + + def destroy_session(env, session_id, options) + begin + @pool.delete(session_id) + rescue Dalli::DalliError + Rails.logger.warn("Session::DalliStore#destroy_session: #{$!.message}") + raise if @raise_errors + end + return nil if options[:drop] + generate_sid + end + + def destroy(env) + if sid = current_session_id(env) + @pool.delete(sid) + end + rescue Dalli::DalliError + Rails.logger.warn("Session::DalliStore#destroy: #{$!.message}") + raise if @raise_errors + false + end + + end + end +end diff --git a/lib/dalli-2.7.2/lib/active_support/cache/dalli_store.rb b/lib/dalli-2.7.2/lib/active_support/cache/dalli_store.rb new file mode 100644 index 000000000..fe4b80e3c --- /dev/null +++ b/lib/dalli-2.7.2/lib/active_support/cache/dalli_store.rb @@ -0,0 +1,363 @@ +# encoding: ascii +require 'dalli' + +module ActiveSupport + module Cache + class DalliStore + + attr_reader :silence, :options + alias_method :silence?, :silence + + # Silence the logger. + def silence! + @silence = true + self + end + + # Silence the logger within a block. + def mute + previous_silence, @silence = defined?(@silence) && @silence, true + yield + ensure + @silence = previous_silence + end + + ESCAPE_KEY_CHARS = /[\x00-\x20%\x7F-\xFF]/ + + # Creates a new DalliStore object, with the given memcached server + # addresses. Each address is either a host name, or a host-with-port string + # in the form of "host_name:port". For example: + # + # ActiveSupport::Cache::DalliStore.new("localhost", "server-downstairs.localnetwork:8229") + # + # If no addresses are specified, then DalliStore will connect to + # localhost port 11211 (the default memcached port). + # + # Connection Pool support + # + # If you are using multithreaded Rails, the Rails.cache singleton can become a source + # of contention. You can use a connection pool of Dalli clients with Rails.cache by + # passing :pool_size and/or :pool_timeout: + # + # config.cache_store = :dalli_store, 'localhost:11211', :pool_size => 10 + # + # Both pool options default to 5. You must include the `connection_pool` gem if you + # wish to use pool support. + # + def initialize(*addresses) + addresses = addresses.flatten + options = addresses.extract_options! + @options = options.dup + + pool_options = {} + pool_options[:size] = options[:pool_size] if options[:pool_size] + pool_options[:timeout] = options[:pool_timeout] if options[:pool_timeout] + + @options[:compress] ||= @options[:compression] + + addresses.compact! + servers = if addresses.empty? + nil # use the default from Dalli::Client + else + addresses + end + if pool_options.empty? + @data = Dalli::Client.new(servers, @options) + else + @data = ::ConnectionPool.new(pool_options) { Dalli::Client.new(servers, @options.merge(:threadsafe => false)) } + end + + extend Strategy::LocalCache + end + + ## + # Access the underlying Dalli::Client or ConnectionPool instance for + # access to get_multi, etc. + def dalli + @data + end + + def with(&block) + @data.with(&block) + end + + def fetch(name, options=nil) + options ||= {} + name = expanded_key name + + if block_given? + unless options[:force] + entry = instrument(:read, name, options) do |payload| + read_entry(name, options).tap do |result| + if payload + payload[:super_operation] = :fetch + payload[:hit] = !!result + end + end + end + end + + if !entry.nil? + instrument(:fetch_hit, name, options) { |payload| } + entry + else + result = instrument(:generate, name, options) do |payload| + yield + end + write(name, result, options) + result + end + else + read(name, options) + end + end + + def read(name, options=nil) + options ||= {} + name = expanded_key name + + instrument(:read, name, options) do |payload| + entry = read_entry(name, options) + payload[:hit] = !!entry if payload + entry + end + end + + def write(name, value, options=nil) + options ||= {} + name = expanded_key name + + instrument(:write, name, options) do |payload| + with do |connection| + options = options.merge(:connection => connection) + write_entry(name, value, options) + end + end + end + + def exist?(name, options=nil) + options ||= {} + name = expanded_key name + + log(:exist, name, options) + !read_entry(name, options).nil? + end + + def delete(name, options=nil) + options ||= {} + name = expanded_key name + + instrument(:delete, name, options) do |payload| + delete_entry(name, options) + end + end + + # Reads multiple keys from the cache using a single call to the + # servers for all keys. Keys must be Strings. + def read_multi(*names) + names.extract_options! + mapping = names.inject({}) { |memo, name| memo[expanded_key(name)] = name; memo } + instrument(:read_multi, names) do + results = {} + if local_cache + mapping.keys.each do |key| + if value = local_cache.read_entry(key, options) + results[key] = value + end + end + end + + data = with { |c| c.get_multi(mapping.keys - results.keys) } + results.merge!(data) + results.inject({}) do |memo, (inner, _)| + entry = results[inner] + # NB Backwards data compatibility, to be removed at some point + value = (entry.is_a?(ActiveSupport::Cache::Entry) ? entry.value : entry) + memo[mapping[inner]] = value + local_cache.write_entry(inner, value, options) if local_cache + memo + end + end + end + + # Fetches data from the cache, using the given keys. If there is data in + # the cache with the given keys, then that data is returned. Otherwise, + # the supplied block is called for each key for which there was no data, + # and the result will be written to the cache and returned. + def fetch_multi(*names) + options = names.extract_options! + mapping = names.inject({}) { |memo, name| memo[expanded_key(name)] = name; memo } + + instrument(:fetch_multi, names) do + with do |connection| + results = connection.get_multi(mapping.keys) + + connection.multi do + mapping.inject({}) do |memo, (expanded, name)| + memo[name] = results[expanded] + if memo[name].nil? + value = yield(name) + memo[name] = value + options = options.merge(:connection => connection) + write_entry(expanded, value, options) + end + + memo + end + end + end + end + end + + # Increment a cached value. This method uses the memcached incr atomic + # operator and can only be used on values written with the :raw option. + # Calling it on a value not stored with :raw will fail. + # :initial defaults to the amount passed in, as if the counter was initially zero. + # memcached counters cannot hold negative values. + def increment(name, amount = 1, options=nil) + options ||= {} + name = expanded_key name + initial = options.has_key?(:initial) ? options[:initial] : amount + expires_in = options[:expires_in] + instrument(:increment, name, :amount => amount) do + with { |c| c.incr(name, amount, expires_in, initial) } + end + rescue Dalli::DalliError => e + logger.error("DalliError: #{e.message}") if logger + raise if raise_errors? + nil + end + + # Decrement a cached value. This method uses the memcached decr atomic + # operator and can only be used on values written with the :raw option. + # Calling it on a value not stored with :raw will fail. + # :initial defaults to zero, as if the counter was initially zero. + # memcached counters cannot hold negative values. + def decrement(name, amount = 1, options=nil) + options ||= {} + name = expanded_key name + initial = options.has_key?(:initial) ? options[:initial] : 0 + expires_in = options[:expires_in] + instrument(:decrement, name, :amount => amount) do + with { |c| c.decr(name, amount, expires_in, initial) } + end + rescue Dalli::DalliError => e + logger.error("DalliError: #{e.message}") if logger + raise if raise_errors? + nil + end + + # Clear the entire cache on all memcached servers. This method should + # be used with care when using a shared cache. + def clear(options=nil) + instrument(:clear, 'flushing all keys') do + with { |c| c.flush_all } + end + rescue Dalli::DalliError => e + logger.error("DalliError: #{e.message}") if logger + raise if raise_errors? + nil + end + + # Clear any local cache + def cleanup(options=nil) + end + + # Get the statistics from the memcached servers. + def stats + with { |c| c.stats } + end + + def reset + with { |c| c.reset } + end + + def logger + Dalli.logger + end + + def logger=(new_logger) + Dalli.logger = new_logger + end + + protected + + # Read an entry from the cache. + def read_entry(key, options) # :nodoc: + entry = with { |c| c.get(key, options) } + # NB Backwards data compatibility, to be removed at some point + entry.is_a?(ActiveSupport::Cache::Entry) ? entry.value : entry + rescue Dalli::DalliError => e + logger.error("DalliError: #{e.message}") if logger + raise if raise_errors? + nil + end + + # Write an entry to the cache. + def write_entry(key, value, options) # :nodoc: + # cleanup LocalCache + cleanup if options[:unless_exist] + method = options[:unless_exist] ? :add : :set + expires_in = options[:expires_in] + connection = options.delete(:connection) + connection.send(method, key, value, expires_in, options) + rescue Dalli::DalliError => e + logger.error("DalliError: #{e.message}") if logger + raise if raise_errors? + false + end + + # Delete an entry from the cache. + def delete_entry(key, options) # :nodoc: + with { |c| c.delete(key) } + rescue Dalli::DalliError => e + logger.error("DalliError: #{e.message}") if logger + raise if raise_errors? + false + end + + private + # Expand key to be a consistent string value. Invoke +cache_key+ if + # object responds to +cache_key+. Otherwise, to_param method will be + # called. If the key is a Hash, then keys will be sorted alphabetically. + def expanded_key(key) # :nodoc: + return key.cache_key.to_s if key.respond_to?(:cache_key) + + case key + when Array + if key.size > 1 + key = key.collect{|element| expanded_key(element)} + else + key = key.first + end + when Hash + key = key.sort_by { |k,_| k.to_s }.collect{|k,v| "#{k}=#{v}"} + end + + key = key.to_param + if key.respond_to? :force_encoding + key = key.dup + key.force_encoding('binary') + end + key + end + + def instrument(operation, key, options=nil) + log(operation, key, options) + + payload = { :key => key } + payload.merge!(options) if options.is_a?(Hash) + ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload){ yield(payload) } + end + + def log(operation, key, options=nil) + return unless logger && logger.debug? && !silence? + logger.debug("Cache #{operation}: #{key}#{options.blank? ? "" : " (#{options.inspect})"}") + end + + def raise_errors? + !!@options[:raise_errors] + end + end + end +end diff --git a/lib/dalli-2.7.2/lib/dalli.rb b/lib/dalli-2.7.2/lib/dalli.rb new file mode 100644 index 000000000..62e3b42fe --- /dev/null +++ b/lib/dalli-2.7.2/lib/dalli.rb @@ -0,0 +1,46 @@ +require 'dalli/compressor' +require 'dalli/client' +require 'dalli/ring' +require 'dalli/server' +require 'dalli/socket' +require 'dalli/version' +require 'dalli/options' +require 'dalli/railtie' if defined?(::Rails::Railtie) + +module Dalli + # generic error + class DalliError < RuntimeError; end + # socket/server communication error + class NetworkError < DalliError; end + # no server available/alive error + class RingError < DalliError; end + # application error in marshalling serialization + class MarshalError < DalliError; end + # application error in marshalling deserialization or decompression + class UnmarshalError < DalliError; end + + def self.logger + @logger ||= (rails_logger || default_logger) + end + + def self.rails_logger + (defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger) || + (defined?(RAILS_DEFAULT_LOGGER) && RAILS_DEFAULT_LOGGER.respond_to?(:debug) && RAILS_DEFAULT_LOGGER) + end + + def self.default_logger + require 'logger' + l = Logger.new(STDOUT) + l.level = Logger::INFO + l + end + + def self.logger=(logger) + @logger = logger + end + +end + +if defined?(RAILS_VERSION) && RAILS_VERSION < '3' + raise Dalli::DalliError, "Dalli #{Dalli::VERSION} does not support Rails version < 3.0" +end diff --git a/lib/dalli-2.7.2/lib/dalli/cas/client.rb b/lib/dalli-2.7.2/lib/dalli/cas/client.rb new file mode 100644 index 000000000..33295b474 --- /dev/null +++ b/lib/dalli-2.7.2/lib/dalli/cas/client.rb @@ -0,0 +1,58 @@ +require 'dalli/client' + +module Dalli + class Client + ## + # Get the value and CAS ID associated with the key. If a block is provided, + # value and CAS will be passed to the block. + def get_cas(key) + (value, cas) = perform(:cas, key) + value = (!value || value == 'Not found') ? nil : value + if block_given? + yield value, cas + else + [value, cas] + end + end + + ## + # Fetch multiple keys efficiently, including available metadata such as CAS. + # If a block is given, yields key/data pairs one a time. Data is an array: + # [value, cas_id] + # If no block is given, returns a hash of + # { 'key' => [value, cas_id] } + def get_multi_cas(*keys) + if block_given? + get_multi_yielder(keys) {|*args| yield(*args)} + else + Hash.new.tap do |hash| + get_multi_yielder(keys) {|k, data| hash[k] = data} + end + end + end + + ## + # Set the key-value pair, verifying existing CAS. + # Returns the resulting CAS value if succeeded, and falsy otherwise. + def set_cas(key, value, cas, ttl=nil, options=nil) + ttl ||= @options[:expires_in].to_i + perform(:set, key, value, ttl, cas, options) + end + + ## + # Conditionally add a key/value pair, verifying existing CAS, only if the + # key already exists on the server. Returns the new CAS value if the + # operation succeeded, or falsy otherwise. + def replace_cas(key, value, cas, ttl=nil, options=nil) + ttl ||= @options[:expires_in].to_i + perform(:replace, key, value, ttl, cas, options) + end + + # Delete a key/value pair, verifying existing CAS. + # Returns true if succeeded, and falsy otherwise. + def delete_cas(key, cas=0) + perform(:delete, key, cas) + end + + end +end diff --git a/lib/dalli-2.7.2/lib/dalli/client.rb b/lib/dalli-2.7.2/lib/dalli/client.rb new file mode 100644 index 000000000..6d65b61b2 --- /dev/null +++ b/lib/dalli-2.7.2/lib/dalli/client.rb @@ -0,0 +1,439 @@ +require 'digest/md5' +require 'set' + +# encoding: ascii +module Dalli + class Client + + ## + # Dalli::Client is the main class which developers will use to interact with + # the memcached server. Usage: + # + # Dalli::Client.new(['localhost:11211:10', 'cache-2.example.com:11211:5', '192.168.0.1:22122:5'], + # :threadsafe => true, :failover => true, :expires_in => 300) + # + # servers is an Array of "host:port:weight" where weight allows you to distribute cache unevenly. + # Both weight and port are optional. If you pass in nil, Dalli will use the MEMCACHE_SERVERS + # environment variable or default to 'localhost:11211' if it is not present. + # + # Options: + # - :namespace - prepend each key with this value to provide simple namespacing. + # - :failover - if a server is down, look for and store values on another server in the ring. Default: true. + # - :threadsafe - ensure that only one thread is actively using a socket at a time. Default: true. + # - :expires_in - default TTL in seconds if you do not pass TTL as a parameter to an individual operation, defaults to 0 or forever + # - :compress - defaults to false, if true Dalli will compress values larger than 1024 bytes before sending them to memcached. + # - :serializer - defaults to Marshal + # - :compressor - defaults to zlib + # + def initialize(servers=nil, options={}) + @servers = normalize_servers(servers || ENV["MEMCACHE_SERVERS"] || '127.0.0.1:11211') + @options = normalize_options(options) + @ring = nil + end + + # + # The standard memcached instruction set + # + + ## + # Turn on quiet aka noreply support. + # All relevant operations within this block will be effectively + # pipelined as Dalli will use 'quiet' operations where possible. + # Currently supports the set, add, replace and delete operations. + def multi + old, Thread.current[:dalli_multi] = Thread.current[:dalli_multi], true + yield + ensure + Thread.current[:dalli_multi] = old + end + + ## + # Get the value associated with the key. + def get(key, options=nil) + perform(:get, key) + end + + ## + # Fetch multiple keys efficiently. + # If a block is given, yields key/value pairs one at a time. + # Otherwise returns a hash of { 'key' => 'value', 'key2' => 'value1' } + def get_multi(*keys) + if block_given? + get_multi_yielder(keys) {|k, data| yield k, data.first} + else + Hash.new.tap do |hash| + get_multi_yielder(keys) {|k, data| hash[k] = data.first} + end + end + end + + def fetch(key, ttl=nil, options=nil) + ttl ||= @options[:expires_in].to_i + val = get(key, options) + if val.nil? && block_given? + val = yield + add(key, val, ttl, options) + end + val + end + + ## + # compare and swap values using optimistic locking. + # Fetch the existing value for key. + # If it exists, yield the value to the block. + # Add the block's return value as the new value for the key. + # Add will fail if someone else changed the value. + # + # Returns: + # - nil if the key did not exist. + # - false if the value was changed by someone else. + # - true if the value was successfully updated. + def cas(key, ttl=nil, options=nil, &block) + ttl ||= @options[:expires_in].to_i + (value, cas) = perform(:cas, key) + value = (!value || value == 'Not found') ? nil : value + if value + newvalue = block.call(value) + perform(:set, key, newvalue, ttl, cas, options) + end + end + + def set(key, value, ttl=nil, options=nil) + ttl ||= @options[:expires_in].to_i + perform(:set, key, value, ttl, 0, options) + end + + ## + # Conditionally add a key/value pair, if the key does not already exist + # on the server. Returns truthy if the operation succeeded. + def add(key, value, ttl=nil, options=nil) + ttl ||= @options[:expires_in].to_i + perform(:add, key, value, ttl, options) + end + + ## + # Conditionally add a key/value pair, only if the key already exists + # on the server. Returns truthy if the operation succeeded. + def replace(key, value, ttl=nil, options=nil) + ttl ||= @options[:expires_in].to_i + perform(:replace, key, value, ttl, 0, options) + end + + def delete(key) + perform(:delete, key, 0) + end + + ## + # Append value to the value already stored on the server for 'key'. + # Appending only works for values stored with :raw => true. + def append(key, value) + perform(:append, key, value.to_s) + end + + ## + # Prepend value to the value already stored on the server for 'key'. + # Prepending only works for values stored with :raw => true. + def prepend(key, value) + perform(:prepend, key, value.to_s) + end + + def flush(delay=0) + time = -delay + ring.servers.map { |s| s.request(:flush, time += delay) } + end + + alias_method :flush_all, :flush + + ## + # Incr adds the given amount to the counter on the memcached server. + # Amt must be a positive integer value. + # + # If default is nil, the counter must already exist or the operation + # will fail and will return nil. Otherwise this method will return + # the new value for the counter. + # + # Note that the ttl will only apply if the counter does not already + # exist. To increase an existing counter and update its TTL, use + # #cas. + def incr(key, amt=1, ttl=nil, default=nil) + raise ArgumentError, "Positive values only: #{amt}" if amt < 0 + ttl ||= @options[:expires_in].to_i + perform(:incr, key, amt.to_i, ttl, default) + end + + ## + # Decr subtracts the given amount from the counter on the memcached server. + # Amt must be a positive integer value. + # + # memcached counters are unsigned and cannot hold negative values. Calling + # decr on a counter which is 0 will just return 0. + # + # If default is nil, the counter must already exist or the operation + # will fail and will return nil. Otherwise this method will return + # the new value for the counter. + # + # Note that the ttl will only apply if the counter does not already + # exist. To decrease an existing counter and update its TTL, use + # #cas. + def decr(key, amt=1, ttl=nil, default=nil) + raise ArgumentError, "Positive values only: #{amt}" if amt < 0 + ttl ||= @options[:expires_in].to_i + perform(:decr, key, amt.to_i, ttl, default) + end + + ## + # Touch updates expiration time for a given key. + # + # Returns true if key exists, otherwise nil. + def touch(key, ttl=nil) + ttl ||= @options[:expires_in].to_i + resp = perform(:touch, key, ttl) + resp.nil? ? nil : true + end + + ## + # Collect the stats for each server. + # You can optionally pass a type including :items or :slabs to get specific stats + # Returns a hash like { 'hostname:port' => { 'stat1' => 'value1', ... }, 'hostname2:port' => { ... } } + def stats(type=nil) + type = nil if ![nil, :items,:slabs].include? type + values = {} + ring.servers.each do |server| + values["#{server.hostname}:#{server.port}"] = server.alive? ? server.request(:stats,type.to_s) : nil + end + values + end + + ## + # Reset stats for each server. + def reset_stats + ring.servers.map do |server| + server.alive? ? server.request(:reset_stats) : nil + end + end + + ## + ## Make sure memcache servers are alive, or raise an Dalli::RingError + def alive! + ring.server_for_key("") + end + + ## + ## Version of the memcache servers. + def version + values = {} + ring.servers.each do |server| + values["#{server.hostname}:#{server.port}"] = server.alive? ? server.request(:version) : nil + end + values + end + + ## + # Close our connection to each server. + # If you perform another operation after this, the connections will be re-established. + def close + if @ring + @ring.servers.each { |s| s.close } + @ring = nil + end + end + alias_method :reset, :close + + # Stub method so a bare Dalli client can pretend to be a connection pool. + def with + yield self + end + + private + + def groups_for_keys(*keys) + groups = mapped_keys(keys).flatten.group_by do |key| + begin + ring.server_for_key(key) + rescue Dalli::RingError + Dalli.logger.debug { "unable to get key #{key}" } + nil + end + end + return groups + end + + def mapped_keys(keys) + keys.flatten.map {|a| validate_key(a.to_s)} + end + + def make_multi_get_requests(groups) + groups.each do |server, keys_for_server| + begin + # TODO: do this with the perform chokepoint? + # But given the fact that fetching the response doesn't take place + # in that slot it's misleading anyway. Need to move all of this method + # into perform to be meaningful + server.request(:send_multiget, keys_for_server) + rescue DalliError, NetworkError => e + Dalli.logger.debug { e.inspect } + Dalli.logger.debug { "unable to get keys for server #{server.hostname}:#{server.port}" } + end + end + end + + def perform_multi_response_start(servers) + servers.each do |server| + next unless server.alive? + begin + server.multi_response_start + rescue DalliError, NetworkError => e + Dalli.logger.debug { e.inspect } + Dalli.logger.debug { "results from this server will be missing" } + servers.delete(server) + end + end + servers + end + + ## + # Normalizes the argument into an array of servers. If the argument is a string, it's expected to be of + # the format "memcache1.example.com:11211[,memcache2.example.com:11211[,memcache3.example.com:11211[...]]] + def normalize_servers(servers) + if servers.is_a? String + return servers.split(",") + else + return servers + end + end + + def ring + @ring ||= Dalli::Ring.new( + @servers.map do |s| + server_options = {} + if s =~ %r{\Amemcached://} + uri = URI.parse(s) + server_options[:username] = uri.user + server_options[:password] = uri.password + s = "#{uri.host}:#{uri.port}" + end + Dalli::Server.new(s, @options.merge(server_options)) + end, @options + ) + end + + # Chokepoint method for instrumentation + def perform(*all_args, &blk) + return blk.call if blk + op, key, *args = *all_args + + key = key.to_s + key = validate_key(key) + begin + server = ring.server_for_key(key) + ret = server.request(op, key, *args) + ret + rescue NetworkError => e + Dalli.logger.debug { e.inspect } + Dalli.logger.debug { "retrying request with new server" } + retry + end + end + + def validate_key(key) + raise ArgumentError, "key cannot be blank" if !key || key.length == 0 + key = key_with_namespace(key) + if key.length > 250 + max_length_before_namespace = 212 - (namespace || '').size + key = "#{key[0, max_length_before_namespace]}:md5:#{Digest::MD5.hexdigest(key)}" + end + return key + end + + def key_with_namespace(key) + (ns = namespace) ? "#{ns}:#{key}" : key + end + + def key_without_namespace(key) + (ns = namespace) ? key.sub(%r(\A#{ns}:), '') : key + end + + def namespace + return nil unless @options[:namespace] + @options[:namespace].is_a?(Proc) ? @options[:namespace].call.to_s : @options[:namespace].to_s + end + + def normalize_options(opts) + if opts[:compression] + Dalli.logger.warn "DEPRECATED: Dalli's :compression option is now just :compress => true. Please update your configuration." + opts[:compress] = opts.delete(:compression) + end + begin + opts[:expires_in] = opts[:expires_in].to_i if opts[:expires_in] + rescue NoMethodError + raise ArgumentError, "cannot convert :expires_in => #{opts[:expires_in].inspect} to an integer" + end + opts + end + + ## + # Yields, one at a time, keys and their values+attributes. + def get_multi_yielder(keys) + perform do + return {} if keys.empty? + ring.lock do + begin + groups = groups_for_keys(keys) + if unfound_keys = groups.delete(nil) + Dalli.logger.debug { "unable to get keys for #{unfound_keys.length} keys because no matching server was found" } + end + make_multi_get_requests(groups) + + servers = groups.keys + return if servers.empty? + servers = perform_multi_response_start(servers) + + start = Time.now + loop do + # remove any dead servers + servers.delete_if { |s| s.sock.nil? } + break if servers.empty? + + # calculate remaining timeout + elapsed = Time.now - start + timeout = servers.first.options[:socket_timeout] + if elapsed > timeout + readable = nil + else + sockets = servers.map(&:sock) + readable, _ = IO.select(sockets, nil, nil, timeout - elapsed) + end + + if readable.nil? + # no response within timeout; abort pending connections + servers.each do |server| + Dalli.logger.debug { "memcached at #{server.name} did not response within timeout" } + server.multi_response_abort + end + break + + else + readable.each do |sock| + server = sock.server + + begin + server.multi_response_nonblock.each_pair do |key, value_list| + yield key_without_namespace(key), value_list + end + + if server.multi_response_completed? + servers.delete(server) + end + rescue NetworkError + servers.delete(server) + end + end + end + end + end + end + end + end + + end +end diff --git a/lib/dalli-2.7.2/lib/dalli/compressor.rb b/lib/dalli-2.7.2/lib/dalli/compressor.rb new file mode 100644 index 000000000..353593ab9 --- /dev/null +++ b/lib/dalli-2.7.2/lib/dalli/compressor.rb @@ -0,0 +1,29 @@ +require 'zlib' +require 'stringio' + +module Dalli + class Compressor + def self.compress(data) + Zlib::Deflate.deflate(data) + end + + def self.decompress(data) + Zlib::Inflate.inflate(data) + end + end + + class GzipCompressor + def self.compress(data) + io = StringIO.new("w") + gz = Zlib::GzipWriter.new(io) + gz.write(data) + gz.close + io.string + end + + def self.decompress(data) + io = StringIO.new(data, "rb") + Zlib::GzipReader.new(io).read + end + end +end diff --git a/lib/dalli-2.7.2/lib/dalli/options.rb b/lib/dalli-2.7.2/lib/dalli/options.rb new file mode 100644 index 000000000..b3c405203 --- /dev/null +++ b/lib/dalli-2.7.2/lib/dalli/options.rb @@ -0,0 +1,64 @@ +require 'thread' +require 'monitor' + +module Dalli + + # Make Dalli threadsafe by using a lock around all + # public server methods. + # + # Dalli::Server.extend(Dalli::Threadsafe) + # + module Threadsafe + def self.extended(obj) + obj.init_threadsafe + end + + def request(op, *args) + @lock.synchronize do + super + end + end + + def alive? + @lock.synchronize do + super + end + end + + def close + @lock.synchronize do + super + end + end + + def multi_response_start + @lock.synchronize do + super + end + end + + def multi_response_nonblock + @lock.synchronize do + super + end + end + + def multi_response_abort + @lock.synchronize do + super + end + end + + def lock! + @lock.mon_enter + end + + def unlock! + @lock.mon_exit + end + + def init_threadsafe + @lock = Monitor.new + end + end +end diff --git a/lib/dalli-2.7.2/lib/dalli/railtie.rb b/lib/dalli-2.7.2/lib/dalli/railtie.rb new file mode 100644 index 000000000..9c5d5d2b9 --- /dev/null +++ b/lib/dalli-2.7.2/lib/dalli/railtie.rb @@ -0,0 +1,7 @@ +module Dalli + class Railtie < ::Rails::Railtie + config.before_configuration do + config.cache_store = :dalli_store + end + end +end diff --git a/lib/dalli-2.7.2/lib/dalli/ring.rb b/lib/dalli-2.7.2/lib/dalli/ring.rb new file mode 100644 index 000000000..11c234021 --- /dev/null +++ b/lib/dalli-2.7.2/lib/dalli/ring.rb @@ -0,0 +1,142 @@ +require 'digest/sha1' +require 'zlib' + +module Dalli + class Ring + POINTS_PER_SERVER = 160 # this is the default in libmemcached + + attr_accessor :servers, :continuum + + def initialize(servers, options) + @servers = servers + @continuum = nil + if servers.size > 1 + total_weight = servers.inject(0) { |memo, srv| memo + srv.weight } + continuum = [] + servers.each do |server| + entry_count_for(server, servers.size, total_weight).times do |idx| + hash = Digest::SHA1.hexdigest("#{server.hostname}:#{server.port}:#{idx}") + value = Integer("0x#{hash[0..7]}") + continuum << Dalli::Ring::Entry.new(value, server) + end + end + @continuum = continuum.sort { |a, b| a.value <=> b.value } + end + + threadsafe! unless options[:threadsafe] == false + @failover = options[:failover] != false + end + + def server_for_key(key) + if @continuum + hkey = hash_for(key) + 20.times do |try| + entryidx = binary_search(@continuum, hkey) + server = @continuum[entryidx].server + return server if server.alive? + break unless @failover + hkey = hash_for("#{try}#{key}") + end + else + server = @servers.first + return server if server && server.alive? + end + + raise Dalli::RingError, "No server available" + end + + def lock + @servers.each { |s| s.lock! } + begin + return yield + ensure + @servers.each { |s| s.unlock! } + end + end + + private + + def threadsafe! + @servers.each do |s| + s.extend(Dalli::Threadsafe) + end + end + + def hash_for(key) + Zlib.crc32(key) + end + + def entry_count_for(server, total_servers, total_weight) + ((total_servers * POINTS_PER_SERVER * server.weight) / Float(total_weight)).floor + end + + # Native extension to perform the binary search within the continuum + # space. Fallback to a pure Ruby version if the compilation doesn't work. + # optional for performance and only necessary if you are using multiple + # memcached servers. + begin + require 'inline' + inline do |builder| + builder.c <<-EOM + int binary_search(VALUE ary, unsigned int r) { + long upper = RARRAY_LEN(ary) - 1; + long lower = 0; + long idx = 0; + ID value = rb_intern("value"); + VALUE continuumValue; + unsigned int l; + + while (lower <= upper) { + idx = (lower + upper) / 2; + + continuumValue = rb_funcall(RARRAY_PTR(ary)[idx], value, 0); + l = NUM2UINT(continuumValue); + if (l == r) { + return idx; + } + else if (l > r) { + upper = idx - 1; + } + else { + lower = idx + 1; + } + } + return upper; + } + EOM + end + rescue LoadError + # Find the closest index in the Ring with value <= the given value + def binary_search(ary, value) + upper = ary.size - 1 + lower = 0 + idx = 0 + + while (lower <= upper) do + idx = (lower + upper) / 2 + comp = ary[idx].value <=> value + + if comp == 0 + return idx + elsif comp > 0 + upper = idx - 1 + else + lower = idx + 1 + end + end + return upper + end + end + + class Entry + attr_reader :value + attr_reader :server + + def initialize(val, srv) + @value = val + @server = srv + end + end + + end +end diff --git a/lib/dalli-2.7.2/lib/dalli/server.rb b/lib/dalli-2.7.2/lib/dalli/server.rb new file mode 100644 index 000000000..38a4fb62d --- /dev/null +++ b/lib/dalli-2.7.2/lib/dalli/server.rb @@ -0,0 +1,696 @@ +require 'socket' +require 'timeout' + +module Dalli + class Server + attr_accessor :hostname + attr_accessor :port + attr_accessor :weight + attr_accessor :options + attr_reader :sock + + DEFAULTS = { + # seconds between trying to contact a remote server + :down_retry_delay => 1, + # connect/read/write timeout for socket operations + :socket_timeout => 0.5, + # times a socket operation may fail before considering the server dead + :socket_max_failures => 2, + # amount of time to sleep between retries when a failure occurs + :socket_failure_delay => 0.01, + # max size of value in bytes (default is 1 MB, can be overriden with "memcached -I ") + :value_max_bytes => 1024 * 1024, + :compressor => Compressor, + # min byte size to attempt compression + :compression_min_size => 1024, + # max byte size for compression + :compression_max_size => false, + :serializer => Marshal, + :username => nil, + :password => nil, + :keepalive => true + } + + def initialize(attribs, options = {}) + (@hostname, @port, @weight) = parse_hostname(attribs) + @port ||= 11211 + @port = Integer(@port) + @weight ||= 1 + @weight = Integer(@weight) + @fail_count = 0 + @down_at = nil + @last_down_at = nil + @options = DEFAULTS.merge(options) + @sock = nil + @msg = nil + @error = nil + @pid = nil + @inprogress = nil + end + + def name + "#{@hostname}:#{@port}" + end + + # Chokepoint method for instrumentation + def request(op, *args) + verify_state + raise Dalli::NetworkError, "#{hostname}:#{port} is down: #{@error} #{@msg}. If you are sure it is running, ensure memcached version is > 1.4." unless alive? + begin + send(op, *args) + rescue Dalli::NetworkError + raise + rescue Dalli::MarshalError => ex + Dalli.logger.error "Marshalling error for key '#{args.first}': #{ex.message}" + Dalli.logger.error "You are trying to cache a Ruby object which cannot be serialized to memcached." + Dalli.logger.error ex.backtrace.join("\n\t") + false + rescue Dalli::DalliError + raise + rescue => ex + Dalli.logger.error "Unexpected exception in Dalli: #{ex.class.name}: #{ex.message}" + Dalli.logger.error "This is a bug in Dalli, please enter an issue in Github if it does not already exist." + Dalli.logger.error ex.backtrace.join("\n\t") + down! + end + end + + def alive? + return true if @sock + + if @last_down_at && @last_down_at + options[:down_retry_delay] >= Time.now + time = @last_down_at + options[:down_retry_delay] - Time.now + Dalli.logger.debug { "down_retry_delay not reached for #{hostname}:#{port} (%.3f seconds left)" % time } + return false + end + + connect + !!@sock + rescue Dalli::NetworkError + false + end + + def close + return unless @sock + @sock.close rescue nil + @sock = nil + @pid = nil + @inprogress = false + end + + def lock! + end + + def unlock! + end + + def serializer + @options[:serializer] + end + + def compressor + @options[:compressor] + end + + # Start reading key/value pairs from this connection. This is usually called + # after a series of GETKQ commands. A NOOP is sent, and the server begins + # flushing responses for kv pairs that were found. + # + # Returns nothing. + def multi_response_start + verify_state + write_noop + @multi_buffer = '' + @position = 0 + @inprogress = true + end + + # Did the last call to #multi_response_start complete successfully? + def multi_response_completed? + @multi_buffer.nil? + end + + # Attempt to receive and parse as many key/value pairs as possible + # from this server. After #multi_response_start, this should be invoked + # repeatedly whenever this server's socket is readable until + # #multi_response_completed?. + # + # Returns a Hash of kv pairs received. + def multi_response_nonblock + raise 'multi_response has completed' if @multi_buffer.nil? + + @multi_buffer << @sock.read_available + buf = @multi_buffer + pos = @position + values = {} + + while buf.bytesize - pos >= 24 + header = buf.slice(pos, 24) + (key_length, _, body_length, cas) = header.unpack(KV_HEADER) + + if key_length == 0 + # all done! + @multi_buffer = nil + @position = nil + @inprogress = false + break + + elsif buf.bytesize - pos >= 24 + body_length + flags = buf.slice(pos + 24, 4).unpack('N')[0] + key = buf.slice(pos + 24 + 4, key_length) + value = buf.slice(pos + 24 + 4 + key_length, body_length - key_length - 4) if body_length - key_length - 4 > 0 + + pos = pos + 24 + body_length + + begin + values[key] = [deserialize(value, flags), cas] + rescue DalliError + end + + else + # not enough data yet, wait for more + break + end + end + @position = pos + + values + rescue SystemCallError, Timeout::Error, EOFError => e + failure!(e) + end + + # Abort an earlier #multi_response_start. Used to signal an external + # timeout. The underlying socket is disconnected, and the exception is + # swallowed. + # + # Returns nothing. + def multi_response_abort + @multi_buffer = nil + @position = nil + @inprogress = false + failure!(RuntimeError.new('External timeout')) + rescue NetworkError + true + end + + # NOTE: Additional public methods should be overridden in Dalli::Threadsafe + + private + + def verify_state + failure!(RuntimeError.new('Already writing to socket')) if @inprogress + failure!(RuntimeError.new('Cannot share client between multiple processes')) if @pid && @pid != Process.pid + end + + def failure!(exception) + message = "#{hostname}:#{port} failed (count: #{@fail_count}) #{exception.class}: #{exception.message}" + Dalli.logger.info { message } + + @fail_count += 1 + if @fail_count >= options[:socket_max_failures] + down! + else + close + sleep(options[:socket_failure_delay]) if options[:socket_failure_delay] + raise Dalli::NetworkError, "Socket operation failed, retrying..." + end + end + + def down! + close + + @last_down_at = Time.now + + if @down_at + time = Time.now - @down_at + Dalli.logger.debug { "#{hostname}:#{port} is still down (for %.3f seconds now)" % time } + else + @down_at = @last_down_at + Dalli.logger.warn { "#{hostname}:#{port} is down" } + end + + @error = $! && $!.class.name + @msg = @msg || ($! && $!.message && !$!.message.empty? && $!.message) + raise Dalli::NetworkError, "#{hostname}:#{port} is down: #{@error} #{@msg}" + end + + def up! + if @down_at + time = Time.now - @down_at + Dalli.logger.warn { "#{hostname}:#{port} is back (downtime was %.3f seconds)" % time } + end + + @fail_count = 0 + @down_at = nil + @last_down_at = nil + @msg = nil + @error = nil + end + + def multi? + Thread.current[:dalli_multi] + end + + def get(key) + req = [REQUEST, OPCODES[:get], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:get]) + write(req) + generic_response(true) + end + + def send_multiget(keys) + req = "" + keys.each do |key| + req << [REQUEST, OPCODES[:getkq], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:getkq]) + end + # Could send noop here instead of in multi_response_start + write(req) + end + + def set(key, value, ttl, cas, options) + (value, flags) = serialize(key, value, options) + ttl = sanitize_ttl(ttl) + + guard_max_value(key, value) do + req = [REQUEST, OPCODES[multi? ? :setq : :set], key.bytesize, 8, 0, 0, value.bytesize + key.bytesize + 8, 0, cas, flags, ttl, key, value].pack(FORMAT[:set]) + write(req) + cas_response unless multi? + end + end + + def add(key, value, ttl, options) + (value, flags) = serialize(key, value, options) + ttl = sanitize_ttl(ttl) + + guard_max_value(key, value) do + req = [REQUEST, OPCODES[multi? ? :addq : :add], key.bytesize, 8, 0, 0, value.bytesize + key.bytesize + 8, 0, 0, flags, ttl, key, value].pack(FORMAT[:add]) + write(req) + cas_response unless multi? + end + end + + def replace(key, value, ttl, cas, options) + (value, flags) = serialize(key, value, options) + ttl = sanitize_ttl(ttl) + + guard_max_value(key, value) do + req = [REQUEST, OPCODES[multi? ? :replaceq : :replace], key.bytesize, 8, 0, 0, value.bytesize + key.bytesize + 8, 0, cas, flags, ttl, key, value].pack(FORMAT[:replace]) + write(req) + cas_response unless multi? + end + end + + def delete(key, cas) + req = [REQUEST, OPCODES[multi? ? :deleteq : :delete], key.bytesize, 0, 0, 0, key.bytesize, 0, cas, key].pack(FORMAT[:delete]) + write(req) + generic_response unless multi? + end + + def flush(ttl) + req = [REQUEST, OPCODES[:flush], 0, 4, 0, 0, 4, 0, 0, 0].pack(FORMAT[:flush]) + write(req) + generic_response + end + + def decr_incr(opcode, key, count, ttl, default) + expiry = default ? sanitize_ttl(ttl) : 0xFFFFFFFF + default ||= 0 + (h, l) = split(count) + (dh, dl) = split(default) + req = [REQUEST, OPCODES[opcode], key.bytesize, 20, 0, 0, key.bytesize + 20, 0, 0, h, l, dh, dl, expiry, key].pack(FORMAT[opcode]) + write(req) + body = generic_response + body ? body.unpack('Q>').first : body + end + + def decr(key, count, ttl, default) + decr_incr :decr, key, count, ttl, default + end + + def incr(key, count, ttl, default) + decr_incr :incr, key, count, ttl, default + end + + def write_append_prepend(opcode, key, value) + write_generic [REQUEST, OPCODES[opcode], key.bytesize, 0, 0, 0, value.bytesize + key.bytesize, 0, 0, key, value].pack(FORMAT[opcode]) + end + + def write_generic(bytes) + write(bytes) + generic_response + end + + def write_noop + req = [REQUEST, OPCODES[:noop], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop]) + write(req) + end + + # Noop is a keepalive operation but also used to demarcate the end of a set of pipelined commands. + # We need to read all the responses at once. + def noop + write_noop + multi_response + end + + def append(key, value) + write_append_prepend :append, key, value + end + + def prepend(key, value) + write_append_prepend :prepend, key, value + end + + def stats(info='') + req = [REQUEST, OPCODES[:stat], info.bytesize, 0, 0, 0, info.bytesize, 0, 0, info].pack(FORMAT[:stat]) + write(req) + keyvalue_response + end + + def reset_stats + write_generic [REQUEST, OPCODES[:stat], 'reset'.bytesize, 0, 0, 0, 'reset'.bytesize, 0, 0, 'reset'].pack(FORMAT[:stat]) + end + + def cas(key) + req = [REQUEST, OPCODES[:get], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:get]) + write(req) + data_cas_response + end + + def version + write_generic [REQUEST, OPCODES[:version], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop]) + end + + def touch(key, ttl) + ttl = sanitize_ttl(ttl) + write_generic [REQUEST, OPCODES[:touch], key.bytesize, 4, 0, 0, key.bytesize + 4, 0, 0, ttl, key].pack(FORMAT[:touch]) + end + + # http://www.hjp.at/zettel/m/memcached_flags.rxml + # Looks like most clients use bit 0 to indicate native language serialization + # and bit 1 to indicate gzip compression. + FLAG_SERIALIZED = 0x1 + FLAG_COMPRESSED = 0x2 + + def serialize(key, value, options=nil) + marshalled = false + value = unless options && options[:raw] + marshalled = true + begin + self.serializer.dump(value) + rescue => ex + # Marshalling can throw several different types of generic Ruby exceptions. + # Convert to a specific exception so we can special case it higher up the stack. + exc = Dalli::MarshalError.new(ex.message) + exc.set_backtrace ex.backtrace + raise exc + end + else + value.to_s + end + compressed = false + if @options[:compress] && value.bytesize >= @options[:compression_min_size] && + (!@options[:compression_max_size] || value.bytesize <= @options[:compression_max_size]) + value = self.compressor.compress(value) + compressed = true + end + + flags = 0 + flags |= FLAG_COMPRESSED if compressed + flags |= FLAG_SERIALIZED if marshalled + [value, flags] + end + + def deserialize(value, flags) + value = self.compressor.decompress(value) if (flags & FLAG_COMPRESSED) != 0 + value = self.serializer.load(value) if (flags & FLAG_SERIALIZED) != 0 + value + rescue TypeError + raise if $!.message !~ /needs to have method `_load'|exception class\/object expected|instance of IO needed|incompatible marshal file format/ + raise UnmarshalError, "Unable to unmarshal value: #{$!.message}" + rescue ArgumentError + raise if $!.message !~ /undefined class|marshal data too short/ + raise UnmarshalError, "Unable to unmarshal value: #{$!.message}" + rescue Zlib::Error + raise UnmarshalError, "Unable to uncompress value: #{$!.message}" + end + + def data_cas_response + (extras, _, status, count, _, cas) = read_header.unpack(CAS_HEADER) + data = read(count) if count > 0 + if status == 1 + nil + elsif status != 0 + raise Dalli::DalliError, "Response error #{status}: #{RESPONSE_CODES[status]}" + elsif data + flags = data[0...extras].unpack('N')[0] + value = data[extras..-1] + data = deserialize(value, flags) + end + [data, cas] + end + + CAS_HEADER = '@4CCnNNQ' + NORMAL_HEADER = '@4CCnN' + KV_HEADER = '@2n@6nN@16Q' + + def guard_max_value(key, value) + if value.bytesize <= @options[:value_max_bytes] + yield + else + Dalli.logger.warn "Value for #{key} over max size: #{@options[:value_max_bytes]} <= #{value.bytesize}" + false + end + end + + # https://code.google.com/p/memcached/wiki/NewCommands#Standard_Protocol + # > An expiration time, in seconds. Can be up to 30 days. After 30 days, is treated as a unix timestamp of an exact date. + MAX_ACCEPTABLE_EXPIRATION_INTERVAL = 30*24*60*60 # 30 days + def sanitize_ttl(ttl) + if ttl > MAX_ACCEPTABLE_EXPIRATION_INTERVAL + Dalli.logger.debug "Expiration interval too long for Memcached, converting to an expiration timestamp" + Time.now.to_i + ttl + else + ttl + end + end + + def generic_response(unpack=false) + (extras, _, status, count) = read_header.unpack(NORMAL_HEADER) + data = read(count) if count > 0 + if status == 1 + nil + elsif status == 2 || status == 5 + false # Not stored, normal status for add operation + elsif status != 0 + raise Dalli::DalliError, "Response error #{status}: #{RESPONSE_CODES[status]}" + elsif data + flags = data[0...extras].unpack('N')[0] + value = data[extras..-1] + unpack ? deserialize(value, flags) : value + else + true + end + end + + def cas_response + (_, _, status, count, _, cas) = read_header.unpack(CAS_HEADER) + read(count) if count > 0 # this is potential data that we don't care about + if status == 1 + nil + elsif status == 2 || status == 5 + false # Not stored, normal status for add operation + elsif status != 0 + raise Dalli::DalliError, "Response error #{status}: #{RESPONSE_CODES[status]}" + else + cas + end + end + + def keyvalue_response + hash = {} + loop do + (key_length, _, body_length, _) = read_header.unpack(KV_HEADER) + return hash if key_length == 0 + key = read(key_length) + value = read(body_length - key_length) if body_length - key_length > 0 + hash[key] = value + end + end + + def multi_response + hash = {} + loop do + (key_length, _, body_length, _) = read_header.unpack(KV_HEADER) + return hash if key_length == 0 + flags = read(4).unpack('N')[0] + key = read(key_length) + value = read(body_length - key_length - 4) if body_length - key_length - 4 > 0 + hash[key] = deserialize(value, flags) + end + end + + def write(bytes) + begin + @inprogress = true + result = @sock.write(bytes) + @inprogress = false + result + rescue SystemCallError, Timeout::Error => e + failure!(e) + end + end + + def read(count) + begin + @inprogress = true + data = @sock.readfull(count) + @inprogress = false + data + rescue SystemCallError, Timeout::Error, EOFError => e + failure!(e) + end + end + + def read_header + read(24) || raise(Dalli::NetworkError, 'No response') + end + + def connect + Dalli.logger.debug { "Dalli::Server#connect #{hostname}:#{port}" } + + begin + @pid = Process.pid + @sock = KSocket.open(hostname, port, self, options) + @version = version # trigger actual connect + sasl_authentication if need_auth? + up! + rescue Dalli::DalliError # SASL auth failure + raise + rescue SystemCallError, Timeout::Error, EOFError, SocketError => e + # SocketError = DNS resolution failure + failure!(e) + end + end + + def split(n) + [n >> 32, 0xFFFFFFFF & n] + end + + REQUEST = 0x80 + RESPONSE = 0x81 + + RESPONSE_CODES = { + 0 => 'No error', + 1 => 'Key not found', + 2 => 'Key exists', + 3 => 'Value too large', + 4 => 'Invalid arguments', + 5 => 'Item not stored', + 6 => 'Incr/decr on a non-numeric value', + 0x20 => 'Authentication required', + 0x81 => 'Unknown command', + 0x82 => 'Out of memory', + } + + OPCODES = { + :get => 0x00, + :set => 0x01, + :add => 0x02, + :replace => 0x03, + :delete => 0x04, + :incr => 0x05, + :decr => 0x06, + :flush => 0x08, + :noop => 0x0A, + :version => 0x0B, + :getkq => 0x0D, + :append => 0x0E, + :prepend => 0x0F, + :stat => 0x10, + :setq => 0x11, + :addq => 0x12, + :replaceq => 0x13, + :deleteq => 0x14, + :incrq => 0x15, + :decrq => 0x16, + :auth_negotiation => 0x20, + :auth_request => 0x21, + :auth_continue => 0x22, + :touch => 0x1C, + } + + HEADER = "CCnCCnNNQ" + OP_FORMAT = { + :get => 'a*', + :set => 'NNa*a*', + :add => 'NNa*a*', + :replace => 'NNa*a*', + :delete => 'a*', + :incr => 'NNNNNa*', + :decr => 'NNNNNa*', + :flush => 'N', + :noop => '', + :getkq => 'a*', + :version => '', + :stat => 'a*', + :append => 'a*a*', + :prepend => 'a*a*', + :auth_request => 'a*a*', + :auth_continue => 'a*a*', + :touch => 'Na*', + } + FORMAT = OP_FORMAT.inject({}) { |memo, (k, v)| memo[k] = HEADER + v; memo } + + + ####### + # SASL authentication support for NorthScale + ####### + + def need_auth? + @options[:username] || ENV['MEMCACHE_USERNAME'] + end + + def username + @options[:username] || ENV['MEMCACHE_USERNAME'] + end + + def password + @options[:password] || ENV['MEMCACHE_PASSWORD'] + end + + def sasl_authentication + Dalli.logger.info { "Dalli/SASL authenticating as #{username}" } + + # negotiate + req = [REQUEST, OPCODES[:auth_negotiation], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop]) + write(req) + + (extras, type, status, count) = read_header.unpack(NORMAL_HEADER) + raise Dalli::NetworkError, "Unexpected message format: #{extras} #{count}" unless extras == 0 && count > 0 + content = read(count) + return (Dalli.logger.debug("Authentication not required/supported by server")) if status == 0x81 + mechanisms = content.split(' ') + raise NotImplementedError, "Dalli only supports the PLAIN authentication mechanism" if !mechanisms.include?('PLAIN') + + # request + mechanism = 'PLAIN' + msg = "\x0#{username}\x0#{password}" + req = [REQUEST, OPCODES[:auth_request], mechanism.bytesize, 0, 0, 0, mechanism.bytesize + msg.bytesize, 0, 0, mechanism, msg].pack(FORMAT[:auth_request]) + write(req) + + (extras, type, status, count) = read_header.unpack(NORMAL_HEADER) + raise Dalli::NetworkError, "Unexpected message format: #{extras} #{count}" unless extras == 0 && count > 0 + content = read(count) + return Dalli.logger.info("Dalli/SASL: #{content}") if status == 0 + + raise Dalli::DalliError, "Error authenticating: #{status}" unless status == 0x21 + raise NotImplementedError, "No two-step authentication mechanisms supported" + # (step, msg) = sasl.receive('challenge', content) + # raise Dalli::NetworkError, "Authentication failed" if sasl.failed? || step != 'response' + end + + def parse_hostname(str) + res = str.match(/\A(\[([\h:]+)\]|[^:]+)(:(\d+))?(:(\d+))?\z/) + return res[2] || res[1], res[4], res[6] + end + end +end diff --git a/lib/dalli-2.7.2/lib/dalli/socket.rb b/lib/dalli-2.7.2/lib/dalli/socket.rb new file mode 100644 index 000000000..54b321a4d --- /dev/null +++ b/lib/dalli-2.7.2/lib/dalli/socket.rb @@ -0,0 +1,108 @@ +begin + require 'kgio' + puts "Using kgio socket IO" if defined?($TESTING) && $TESTING + + class Dalli::Server::KSocket < Kgio::Socket + attr_accessor :options, :server + + def kgio_wait_readable + IO.select([self], nil, nil, options[:socket_timeout]) || raise(Timeout::Error, "IO timeout") + end + + def kgio_wait_writable + IO.select(nil, [self], nil, options[:socket_timeout]) || raise(Timeout::Error, "IO timeout") + end + + def self.open(host, port, server, options = {}) + addr = Socket.pack_sockaddr_in(port, host) + sock = start(addr) + sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, true) + sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true) if options[:keepalive] + sock.options = options + sock.server = server + sock.kgio_wait_writable + sock + end + + alias :write :kgio_write + + def readfull(count) + value = '' + loop do + value << kgio_read!(count - value.bytesize) + break if value.bytesize == count + end + value + end + + def read_available + value = '' + loop do + ret = kgio_tryread(8196) + case ret + when nil + raise EOFError, 'end of stream' + when :wait_readable + break + else + value << ret + end + end + value + end + + end + + if ::Kgio.respond_to?(:wait_readable=) + ::Kgio.wait_readable = :kgio_wait_readable + ::Kgio.wait_writable = :kgio_wait_writable + end + +rescue LoadError + + puts "Using standard socket IO (#{RUBY_DESCRIPTION})" if defined?($TESTING) && $TESTING + class Dalli::Server::KSocket < TCPSocket + attr_accessor :options, :server + + def self.open(host, port, server, options = {}) + Timeout.timeout(options[:socket_timeout]) do + sock = new(host, port) + sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, true) + sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true) if options[:keepalive] + sock.options = { :host => host, :port => port }.merge(options) + sock.server = server + sock + end + end + + def readfull(count) + value = '' + begin + loop do + value << read_nonblock(count - value.bytesize) + break if value.bytesize == count + end + rescue Errno::EAGAIN, Errno::EWOULDBLOCK + if IO.select([self], nil, nil, options[:socket_timeout]) + retry + else + raise Timeout::Error, "IO timeout: #{options.inspect}" + end + end + value + end + + def read_available + value = '' + loop do + begin + value << read_nonblock(8196) + rescue Errno::EAGAIN, Errno::EWOULDBLOCK + break + end + end + value + end + + end +end diff --git a/lib/dalli-2.7.2/lib/dalli/version.rb b/lib/dalli-2.7.2/lib/dalli/version.rb new file mode 100644 index 000000000..5b5dfddaf --- /dev/null +++ b/lib/dalli-2.7.2/lib/dalli/version.rb @@ -0,0 +1,3 @@ +module Dalli + VERSION = '2.7.2' +end diff --git a/lib/dalli-2.7.2/lib/rack/session/dalli.rb b/lib/dalli-2.7.2/lib/rack/session/dalli.rb new file mode 100644 index 000000000..70940cb09 --- /dev/null +++ b/lib/dalli-2.7.2/lib/rack/session/dalli.rb @@ -0,0 +1,75 @@ +require 'rack/session/abstract/id' +require 'dalli' + +module Rack + module Session + class Dalli < Abstract::ID + attr_reader :pool, :mutex + + DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge \ + :namespace => 'rack:session', + :memcache_server => 'localhost:11211' + + def initialize(app, options={}) + super + @mutex = Mutex.new + mserv = @default_options[:memcache_server] + mopts = @default_options.reject{|k,v| !DEFAULT_OPTIONS.include? k } + @pool = options[:cache] || ::Dalli::Client.new(mserv, mopts) + @pool.alive! + end + + def generate_sid + loop do + sid = super + break sid unless @pool.get(sid) + end + end + + def get_session(env, sid) + with_lock(env, [nil, {}]) do + unless sid and !sid.empty? and session = @pool.get(sid) + sid, session = generate_sid, {} + unless @pool.add(sid, session) + raise "Session collision on '#{sid.inspect}'" + end + end + [sid, session] + end + end + + def set_session(env, session_id, new_session, options) + return false unless session_id + expiry = options[:expire_after] + expiry = expiry.nil? ? 0 : expiry + 1 + + with_lock(env, false) do + @pool.set session_id, new_session, expiry + session_id + end + end + + def destroy_session(env, session_id, options) + with_lock(env) do + @pool.delete(session_id) + generate_sid unless options[:drop] + end + end + + def with_lock(env, default=nil) + @mutex.lock if env['rack.multithread'] + yield + rescue ::Dalli::DalliError, Errno::ECONNREFUSED + raise if $!.message =~ /undefined class/ + if $VERBOSE + warn "#{self} is unable to find memcached server." + warn $!.inspect + end + default + ensure + @mutex.unlock if @mutex.locked? + end + + end + end +end diff --git a/lib/dalli-2.7.2/test/benchmark_test.rb b/lib/dalli-2.7.2/test/benchmark_test.rb new file mode 100644 index 000000000..783a8b475 --- /dev/null +++ b/lib/dalli-2.7.2/test/benchmark_test.rb @@ -0,0 +1,242 @@ +require 'helper' +require 'benchmark' +require 'active_support/cache/dalli_store' + +describe 'performance' do + before do + puts "Testing #{Dalli::VERSION} with #{RUBY_DESCRIPTION}" + # We'll use a simple @value to try to avoid spending time in Marshal, + # which is a constant penalty that both clients have to pay + @value = [] + @marshalled = Marshal.dump(@value) + + @servers = ['127.0.0.1:19122', 'localhost:19122'] + @key1 = "Short" + @key2 = "Sym1-2-3::45"*8 + @key3 = "Long"*40 + @key4 = "Medium"*8 + # 5 and 6 are only used for multiget miss test + @key5 = "Medium2"*8 + @key6 = "Long3"*40 + @counter = 'counter' + end + + it 'runs benchmarks' do + memcached do + + Benchmark.bm(37) do |x| + + n = 2500 + + @ds = ActiveSupport::Cache::DalliStore.new(@servers) + x.report("mixed:rails:dalli") do + n.times do + @ds.read @key1 + @ds.write @key2, @value + @ds.fetch(@key3) { @value } + @ds.fetch(@key2) { @value } + @ds.fetch(@key1) { @value } + @ds.write @key2, @value, :unless_exists => true + @ds.delete @key2 + @ds.increment @counter, 1, :initial => 100 + @ds.increment @counter, 1, :expires_in => 12 + @ds.decrement @counter, 1 + end + end + + x.report("mixed:rails-localcache:dalli") do + n.times do + @ds.with_local_cache do + @ds.read @key1 + @ds.write @key2, @value + @ds.fetch(@key3) { @value } + @ds.fetch(@key2) { @value } + @ds.fetch(@key1) { @value } + @ds.write @key2, @value, :unless_exists => true + @ds.delete @key2 + @ds.increment @counter, 1, :initial => 100 + @ds.increment @counter, 1, :expires_in => 12 + @ds.decrement @counter, 1 + end + end + end + + @ds.clear + sizeable_data = "some view partial data" * 50 + [@key1, @key2, @key3, @key4, @key5, @key6].each do |key| + @ds.write(key, sizeable_data) + end + + x.report("read_multi_big:rails:dalli") do + n.times do + @ds.read_multi @key1, @key2, @key3, @key4 + @ds.read @key1 + @ds.read @key2 + @ds.read @key3 + @ds.read @key4 + @ds.read @key1 + @ds.read @key2 + @ds.read @key3 + @ds.read_multi @key1, @key2, @key3 + end + end + + x.report("read_multi_big:rails-localcache:dalli") do + n.times do + @ds.with_local_cache do + @ds.read_multi @key1, @key2, @key3, @key4 + @ds.read @key1 + @ds.read @key2 + @ds.read @key3 + @ds.read @key4 + end + @ds.with_local_cache do + @ds.read @key1 + @ds.read @key2 + @ds.read @key3 + @ds.read_multi @key1, @key2, @key3 + end + end + end + + @m = Dalli::Client.new(@servers) + x.report("set:plain:dalli") do + n.times do + @m.set @key1, @marshalled, 0, :raw => true + @m.set @key2, @marshalled, 0, :raw => true + @m.set @key3, @marshalled, 0, :raw => true + @m.set @key1, @marshalled, 0, :raw => true + @m.set @key2, @marshalled, 0, :raw => true + @m.set @key3, @marshalled, 0, :raw => true + end + end + + @m = Dalli::Client.new(@servers) + x.report("setq:plain:dalli") do + @m.multi do + n.times do + @m.set @key1, @marshalled, 0, :raw => true + @m.set @key2, @marshalled, 0, :raw => true + @m.set @key3, @marshalled, 0, :raw => true + @m.set @key1, @marshalled, 0, :raw => true + @m.set @key2, @marshalled, 0, :raw => true + @m.set @key3, @marshalled, 0, :raw => true + end + end + end + + @m = Dalli::Client.new(@servers) + x.report("set:ruby:dalli") do + n.times do + @m.set @key1, @value + @m.set @key2, @value + @m.set @key3, @value + @m.set @key1, @value + @m.set @key2, @value + @m.set @key3, @value + end + end + + @m = Dalli::Client.new(@servers) + x.report("get:plain:dalli") do + n.times do + @m.get @key1, :raw => true + @m.get @key2, :raw => true + @m.get @key3, :raw => true + @m.get @key1, :raw => true + @m.get @key2, :raw => true + @m.get @key3, :raw => true + end + end + + @m = Dalli::Client.new(@servers) + x.report("get:ruby:dalli") do + n.times do + @m.get @key1 + @m.get @key2 + @m.get @key3 + @m.get @key1 + @m.get @key2 + @m.get @key3 + end + end + + @m = Dalli::Client.new(@servers) + x.report("multiget:ruby:dalli") do + n.times do + # We don't use the keys array because splat is slow + @m.get_multi @key1, @key2, @key3, @key4, @key5, @key6 + end + end + + @m = Dalli::Client.new(@servers) + x.report("missing:ruby:dalli") do + n.times do + begin @m.delete @key1; rescue; end + begin @m.get @key1; rescue; end + begin @m.delete @key2; rescue; end + begin @m.get @key2; rescue; end + begin @m.delete @key3; rescue; end + begin @m.get @key3; rescue; end + end + end + + @m = Dalli::Client.new(@servers) + x.report("mixed:ruby:dalli") do + n.times do + @m.set @key1, @value + @m.set @key2, @value + @m.set @key3, @value + @m.get @key1 + @m.get @key2 + @m.get @key3 + @m.set @key1, @value + @m.get @key1 + @m.set @key2, @value + @m.get @key2 + @m.set @key3, @value + @m.get @key3 + end + end + + @m = Dalli::Client.new(@servers) + x.report("mixedq:ruby:dalli") do + @m.multi do + n.times do + @m.set @key1, @value + @m.set @key2, @value + @m.set @key3, @value + @m.get @key1 + @m.get @key2 + @m.get @key3 + @m.set @key1, @value + @m.get @key1 + @m.set @key2, @value + @m.replace @key2, @value + @m.delete @key3 + @m.add @key3, @value + @m.get @key2 + @m.set @key3, @value + @m.get @key3 + end + end + end + + @m = Dalli::Client.new(@servers) + x.report("incr:ruby:dalli") do + counter = 'foocount' + n.times do + @m.incr counter, 1, 0, 1 + end + n.times do + @m.decr counter, 1 + end + + assert_equal 0, @m.incr(counter, 0) + end + + end + end + + end +end diff --git a/lib/dalli-2.7.2/test/helper.rb b/lib/dalli-2.7.2/test/helper.rb new file mode 100644 index 000000000..9f72ec5e9 --- /dev/null +++ b/lib/dalli-2.7.2/test/helper.rb @@ -0,0 +1,55 @@ +$TESTING = true +require 'rubygems' +# require 'simplecov' +# SimpleCov.start +require 'minitest/pride' +require 'minitest/autorun' +require 'mocha/setup' +require 'memcached_mock' + +ENV['MEMCACHED_SASL_PWDB'] = "#{File.dirname(__FILE__)}/sasldb" + +WANT_RAILS_VERSION = ENV['RAILS_VERSION'] || '>= 3.0.0' +gem 'rails', WANT_RAILS_VERSION +require 'rails' +puts "Testing with Rails #{Rails.version}" + +require 'dalli' +require 'logger' + +Dalli.logger = Logger.new(STDOUT) +Dalli.logger.level = Logger::ERROR + +class MiniTest::Spec + include MemcachedMock::Helper + + def assert_error(error, regexp=nil, &block) + ex = assert_raises(error, &block) + assert_match(regexp, ex.message, "#{ex.class.name}: #{ex.message}\n#{ex.backtrace.join("\n\t")}") + end + + def op_cas_succeeds(rsp) + rsp.is_a?(Integer) && rsp > 0 + end + + def op_replace_succeeds(rsp) + rsp.is_a?(Integer) && rsp > 0 + end + + # add and set must have the same return value because of DalliStore#write_entry + def op_addset_succeeds(rsp) + rsp.is_a?(Integer) && rsp > 0 + end + + def with_activesupport + require 'active_support/all' + require 'active_support/cache/dalli_store' + yield + end + + def with_actionpack + require 'action_dispatch' + require 'action_controller' + yield + end +end diff --git a/lib/dalli-2.7.2/test/memcached_mock.rb b/lib/dalli-2.7.2/test/memcached_mock.rb new file mode 100644 index 000000000..7fdd8c1a8 --- /dev/null +++ b/lib/dalli-2.7.2/test/memcached_mock.rb @@ -0,0 +1,121 @@ +require "socket" + +$started = {} + +module MemcachedMock + def self.start(port=19123, &block) + server = TCPServer.new("localhost", port) + session = server.accept + block.call session + end + + def self.delayed_start(port=19123, wait=1, &block) + server = TCPServer.new("localhost", port) + sleep wait + block.call server + end + + module Helper + # Forks the current process and starts a new mock Memcached server on + # port 22122. + # + # memcached_mock(lambda {|sock| socket.write('123') }) do + # assert_equal "PONG", Dalli::Client.new('localhost:22122').get('abc') + # end + # + def memcached_mock(proc, meth = :start) + return unless supports_fork? + begin + pid = fork do + trap("TERM") { exit } + + MemcachedMock.send(meth) do |*args| + proc.call(*args) + end + end + + sleep 0.3 # Give time for the socket to start listening. + yield + ensure + if pid + Process.kill("TERM", pid) + Process.wait(pid) + end + end + end + + PATHS = %w( + /usr/local/bin/ + /opt/local/bin/ + /usr/bin/ + ) + + def find_memcached + output = `memcached -h | head -1`.strip + if output && output =~ /^memcached (\d.\d.\d+)/ && $1 > '1.4' + return (puts "Found #{output} in PATH"; '') + end + PATHS.each do |path| + output = `memcached -h | head -1`.strip + if output && output =~ /^memcached (\d\.\d\.\d+)/ && $1 > '1.4' + return (puts "Found #{output} in #{path}"; path) + end + end + + raise Errno::ENOENT, "Unable to find memcached 1.4+ locally" + end + + def memcached(port=19122, args='', options={}) + memcached_server(port, args) + yield Dalli::Client.new(["localhost:#{port}", "127.0.0.1:#{port}"], options) + end + + def memcached_cas(port=19122, args='', options={}) + memcached_server(port, args) + require 'dalli/cas/client' + yield Dalli::Client.new(["localhost:#{port}", "127.0.0.1:#{port}"], options) + end + + def memcached_server(port=19122, args='') + Memcached.path ||= find_memcached + + cmd = "#{Memcached.path}memcached #{args} -p #{port}" + + $started[port] ||= begin + #puts "Starting: #{cmd}..." + pid = IO.popen(cmd).pid + at_exit do + begin + Process.kill("TERM", pid) + Process.wait(pid) + rescue Errno::ECHILD, Errno::ESRCH + end + end + sleep 0.1 + pid + end + end + + def supports_fork? + !defined?(RUBY_ENGINE) || RUBY_ENGINE != 'jruby' + end + + def memcached_kill(port) + pid = $started.delete(port) + if pid + begin + Process.kill("TERM", pid) + Process.wait(pid) + rescue Errno::ECHILD, Errno::ESRCH + end + end + end + + end +end + +module Memcached + class << self + attr_accessor :path + end +end diff --git a/lib/dalli-2.7.2/test/sasldb b/lib/dalli-2.7.2/test/sasldb new file mode 100644 index 000000000..26cb137aa --- /dev/null +++ b/lib/dalli-2.7.2/test/sasldb @@ -0,0 +1 @@ +testuser:testtest::::::: diff --git a/lib/dalli-2.7.2/test/test_active_support.rb b/lib/dalli-2.7.2/test/test_active_support.rb new file mode 100644 index 000000000..4af18163f --- /dev/null +++ b/lib/dalli-2.7.2/test/test_active_support.rb @@ -0,0 +1,439 @@ +# encoding: utf-8 +require 'helper' +require 'connection_pool' + +class MockUser + def cache_key + "users/1/21348793847982314" + end +end + +describe 'ActiveSupport' do + describe 'active_support caching' do + + it 'has accessible options' do + @dalli = ActiveSupport::Cache.lookup_store(:dalli_store, 'localhost:19122', :expires_in => 5.minutes, :frob => 'baz') + assert_equal 'baz', @dalli.options[:frob] + end + + it 'allow mute and silence' do + @dalli = ActiveSupport::Cache.lookup_store(:dalli_store, 'localhost:19122') + @dalli.mute do + assert op_addset_succeeds(@dalli.write('foo', 'bar', nil)) + assert_equal 'bar', @dalli.read('foo', nil) + end + refute @dalli.silence? + @dalli.silence! + assert_equal true, @dalli.silence? + end + + it 'handle nil options' do + @dalli = ActiveSupport::Cache.lookup_store(:dalli_store, 'localhost:19122') + assert op_addset_succeeds(@dalli.write('foo', 'bar', nil)) + assert_equal 'bar', @dalli.read('foo', nil) + assert_equal 18, @dalli.fetch('lkjsadlfk', nil) { 18 } + assert_equal 18, @dalli.fetch('lkjsadlfk', nil) { 18 } + assert_equal 1, @dalli.increment('lkjsa', 1, nil) + assert_equal 2, @dalli.increment('lkjsa', 1, nil) + assert_equal 1, @dalli.decrement('lkjsa', 1, nil) + assert_equal true, @dalli.delete('lkjsa') + end + + it 'support fetch' do + with_activesupport do + memcached do + connect + dvalue = @dalli.fetch('someotherkeywithoutspaces', :expires_in => 1.second) { 123 } + assert_equal 123, dvalue + + o = Object.new + o.instance_variable_set :@foo, 'bar' + dvalue = @dalli.fetch(rand_key, :raw => true) { o } + assert_equal o, dvalue + + dvalue = @dalli.fetch(rand_key) { o } + assert_equal o, dvalue + + @dalli.write('false', false) + dvalue = @dalli.fetch('false') { flunk } + assert_equal false, dvalue + + user = MockUser.new + @dalli.write(user.cache_key, false) + dvalue = @dalli.fetch(user) { flunk } + assert_equal false, dvalue + end + end + end + + it 'support keys with spaces on Rails3' do + with_activesupport do + memcached do + connect + dvalue = @dalli.fetch('some key with spaces', :expires_in => 1.second) { 123 } + assert_equal 123, dvalue + end + end + end + + it 'support read_multi' do + with_activesupport do + memcached do + connect + x = rand_key + y = rand_key + assert_equal({}, @dalli.read_multi(x, y)) + @dalli.write(x, '123') + @dalli.write(y, 123) + assert_equal({ x => '123', y => 123 }, @dalli.read_multi(x, y)) + end + end + end + + it 'support read_multi with an array' do + with_activesupport do + memcached do + connect + x = rand_key + y = rand_key + assert_equal({}, @dalli.read_multi([x, y])) + @dalli.write(x, '123') + @dalli.write(y, 123) + assert_equal({}, @dalli.read_multi([x, y])) + @dalli.write([x, y], '123') + assert_equal({ [x, y] => '123' }, @dalli.read_multi([x, y])) + end + end + end + + it 'support raw read_multi' do + with_activesupport do + memcached do + connect + @dalli.write("abc", 5, :raw => true) + @dalli.write("cba", 5, :raw => true) + assert_equal({'abc' => '5', 'cba' => '5' }, @dalli.read_multi("abc", "cba")) + end + end + end + + it 'support read_multi with LocalCache' do + with_activesupport do + memcached do + connect + x = rand_key + y = rand_key + assert_equal({}, @dalli.read_multi(x, y)) + @dalli.write(x, '123') + @dalli.write(y, 456) + + @dalli.with_local_cache do + assert_equal({ x => '123', y => 456 }, @dalli.read_multi(x, y)) + Dalli::Client.any_instance.expects(:get).with(any_parameters).never + + dres = @dalli.read(x) + assert_equal dres, '123' + end + + Dalli::Client.any_instance.unstub(:get) + + # Fresh LocalStore + @dalli.with_local_cache do + @dalli.read(x) + Dalli::Client.any_instance.expects(:get_multi).with([y.to_s]).returns(y.to_s => 456) + + assert_equal({ x => '123', y => 456}, @dalli.read_multi(x, y)) + end + end + end + end + + it 'supports fetch_multi' do + with_activesupport do + memcached do + connect + + x = rand_key.to_s + y = rand_key + hash = { x => 'ABC', y => 'DEF' } + + @dalli.write(y, '123') + + results = @dalli.fetch_multi(x, y) { |key| hash[key] } + + assert_equal({ x => 'ABC', y => '123' }, results) + assert_equal('ABC', @dalli.read(x)) + assert_equal('123', @dalli.read(y)) + end + end + end + + it 'support read, write and delete' do + with_activesupport do + memcached do + connect + y = rand_key + assert_nil @dalli.read(y) + dres = @dalli.write(y, 123) + assert op_addset_succeeds(dres) + + dres = @dalli.read(y) + assert_equal 123, dres + + dres = @dalli.delete(y) + assert_equal true, dres + + user = MockUser.new + dres = @dalli.write(user.cache_key, "foo") + assert op_addset_succeeds(dres) + + dres = @dalli.read(user) + assert_equal "foo", dres + + dres = @dalli.delete(user) + assert_equal true, dres + + bigkey = '123456789012345678901234567890' + @dalli.write(bigkey, 'double width') + assert_equal 'double width', @dalli.read(bigkey) + assert_equal({bigkey => "double width"}, @dalli.read_multi(bigkey)) + end + end + end + + it 'support read, write and delete with LocalCache' do + with_activesupport do + memcached do + connect + y = rand_key.to_s + @dalli.with_local_cache do + Dalli::Client.any_instance.expects(:get).with(y, {}).once.returns(123) + dres = @dalli.read(y) + assert_equal 123, dres + + Dalli::Client.any_instance.expects(:get).with(y, {}).never + + dres = @dalli.read(y) + assert_equal 123, dres + + @dalli.write(y, 456) + dres = @dalli.read(y) + assert_equal 456, dres + + @dalli.delete(y) + Dalli::Client.any_instance.expects(:get).with(y, {}).once.returns(nil) + dres = @dalli.read(y) + assert_equal nil, dres + end + end + end + end + + it 'support unless_exist with LocalCache' do + with_activesupport do + memcached do + connect + y = rand_key.to_s + @dalli.with_local_cache do + Dalli::Client.any_instance.expects(:add).with(y, 123, nil, {:unless_exist => true}).once.returns(true) + dres = @dalli.write(y, 123, :unless_exist => true) + assert_equal true, dres + + Dalli::Client.any_instance.expects(:add).with(y, 321, nil, {:unless_exist => true}).once.returns(false) + + dres = @dalli.write(y, 321, :unless_exist => true) + assert_equal false, dres + + Dalli::Client.any_instance.expects(:get).with(y, {}).once.returns(123) + + dres = @dalli.read(y) + assert_equal 123, dres + end + end + end + end + + it 'support increment/decrement commands' do + with_activesupport do + memcached do + connect + assert op_addset_succeeds(@dalli.write('counter', 0, :raw => true)) + assert_equal 1, @dalli.increment('counter') + assert_equal 2, @dalli.increment('counter') + assert_equal 1, @dalli.decrement('counter') + assert_equal "1", @dalli.read('counter', :raw => true) + + assert_equal 1, @dalli.increment('counterX') + assert_equal 2, @dalli.increment('counterX') + assert_equal 2, @dalli.read('counterX', :raw => true).to_i + + assert_equal 5, @dalli.increment('counterY1', 1, :initial => 5) + assert_equal 6, @dalli.increment('counterY1', 1, :initial => 5) + assert_equal 6, @dalli.read('counterY1', :raw => true).to_i + + assert_equal nil, @dalli.increment('counterZ1', 1, :initial => nil) + assert_equal nil, @dalli.read('counterZ1') + + assert_equal 5, @dalli.decrement('counterY2', 1, :initial => 5) + assert_equal 4, @dalli.decrement('counterY2', 1, :initial => 5) + assert_equal 4, @dalli.read('counterY2', :raw => true).to_i + + assert_equal nil, @dalli.decrement('counterZ2', 1, :initial => nil) + assert_equal nil, @dalli.read('counterZ2') + + user = MockUser.new + assert op_addset_succeeds(@dalli.write(user, 0, :raw => true)) + assert_equal 1, @dalli.increment(user) + assert_equal 2, @dalli.increment(user) + assert_equal 1, @dalli.decrement(user) + assert_equal "1", @dalli.read(user, :raw => true) + end + end + end + + it 'support exist command' do + with_activesupport do + memcached do + connect + @dalli.write(:foo, 'a') + @dalli.write(:false_value, false) + + assert_equal true, @dalli.exist?(:foo) + assert_equal true, @dalli.exist?(:false_value) + + assert_equal false, @dalli.exist?(:bar) + + user = MockUser.new + @dalli.write(user, 'foo') + assert_equal true, @dalli.exist?(user) + end + end + end + + it 'support other esoteric commands' do + with_activesupport do + memcached do + connect + ds = @dalli.stats + assert_equal 1, ds.keys.size + assert ds[ds.keys.first].keys.size > 0 + + @dalli.reset + end + end + end + + it 'respect "raise_errors" option' do + with_activesupport do + memcached(29125) do + @dalli = ActiveSupport::Cache.lookup_store(:dalli_store, 'localhost:29125') + @dalli.write 'foo', 'bar' + assert_equal @dalli.read('foo'), 'bar' + + memcached_kill(29125) + + assert_equal @dalli.read('foo'), nil + + @dalli = ActiveSupport::Cache.lookup_store(:dalli_store, 'localhost:29125', :raise_errors => true) + + exception = [Dalli::RingError, { :message => "No server available" }] + + assert_raises(*exception) { @dalli.read 'foo' } + assert_raises(*exception) { @dalli.read 'foo', :raw => true } + assert_raises(*exception) { @dalli.write 'foo', 'bar' } + assert_raises(*exception) { @dalli.exist? 'foo' } + assert_raises(*exception) { @dalli.increment 'foo' } + assert_raises(*exception) { @dalli.decrement 'foo' } + assert_raises(*exception) { @dalli.delete 'foo' } + assert_equal @dalli.read_multi('foo', 'bar'), {} + assert_raises(*exception) { @dalli.delete 'foo' } + assert_raises(*exception) { @dalli.fetch('foo') { 42 } } + end + end + end + end + + it 'handle crazy characters from far-away lands' do + with_activesupport do + memcached do + connect + key = "fooƒ" + value = 'bafƒ' + assert op_addset_succeeds(@dalli.write(key, value)) + assert_equal value, @dalli.read(key) + end + end + end + + it 'normalize options as expected' do + with_activesupport do + memcached do + @dalli = ActiveSupport::Cache::DalliStore.new('localhost:19122', :expires_in => 1, :namespace => 'foo', :compress => true) + assert_equal 1, @dalli.instance_variable_get(:@data).instance_variable_get(:@options)[:expires_in] + assert_equal 'foo', @dalli.instance_variable_get(:@data).instance_variable_get(:@options)[:namespace] + assert_equal ["localhost:19122"], @dalli.instance_variable_get(:@data).instance_variable_get(:@servers) + end + end + end + + it 'handles nil server with additional options' do + with_activesupport do + memcached do + @dalli = ActiveSupport::Cache::DalliStore.new(nil, :expires_in => 1, :namespace => 'foo', :compress => true) + assert_equal 1, @dalli.instance_variable_get(:@data).instance_variable_get(:@options)[:expires_in] + assert_equal 'foo', @dalli.instance_variable_get(:@data).instance_variable_get(:@options)[:namespace] + assert_equal ["127.0.0.1:11211"], @dalli.instance_variable_get(:@data).instance_variable_get(:@servers) + end + end + end + + it 'supports connection pooling' do + with_activesupport do + memcached do + @dalli = ActiveSupport::Cache::DalliStore.new('localhost:19122', :expires_in => 1, :namespace => 'foo', :compress => true, :pool_size => 3) + assert_equal nil, @dalli.read('foo') + assert @dalli.write('foo', 1) + assert_equal 1, @dalli.fetch('foo') { raise 'boom' } + assert_equal true, @dalli.dalli.is_a?(ConnectionPool) + assert_equal 1, @dalli.increment('bar') + assert_equal 0, @dalli.decrement('bar') + assert_equal true, @dalli.delete('bar') + assert_equal [true], @dalli.clear + assert_equal 1, @dalli.stats.size + end + end + end + + it 'allow keys to be frozen' do + with_activesupport do + memcached do + connect + key = "foo" + key.freeze + assert op_addset_succeeds(@dalli.write(key, "value")) + end + end + end + + it 'allow keys from a hash' do + with_activesupport do + memcached do + connect + map = { "one" => "one", "two" => "two" } + map.each_pair do |k, v| + assert op_addset_succeeds(@dalli.write(k, v)) + end + assert_equal map, @dalli.read_multi(*(map.keys)) + end + end + end + + def connect + @dalli = ActiveSupport::Cache.lookup_store(:dalli_store, 'localhost:19122', :expires_in => 10.seconds, :namespace => lambda{33.to_s(36)}) + @dalli.clear + end + + def rand_key + rand(1_000_000_000) + end +end diff --git a/lib/dalli-2.7.2/test/test_cas_client.rb b/lib/dalli-2.7.2/test/test_cas_client.rb new file mode 100644 index 000000000..423e36022 --- /dev/null +++ b/lib/dalli-2.7.2/test/test_cas_client.rb @@ -0,0 +1,107 @@ +require 'helper' +require 'memcached_mock' + +describe 'Dalli::Cas::Client' do + describe 'using a live server' do + it 'supports get with CAS' do + memcached_cas do |dc| + dc.flush + + expected = { 'blah' => 'blerg!' } + get_block_called = false + stored_value = stored_cas = nil + # Validate call-with-block + dc.get_cas('gets_key') do |v, cas| + get_block_called = true + stored_value = v + stored_cas = cas + end + assert get_block_called + assert_nil stored_value + + dc.set('gets_key', expected) + + # Validate call-with-return-value + stored_value, stored_cas = dc.get_cas('gets_key') + assert_equal stored_value, expected + assert(stored_cas != 0) + end + end + + it 'supports multi-get with CAS' do + memcached_cas do |dc| + dc.close + dc.flush + + expected_hash = {'a' => 'foo', 'b' => 123} + expected_hash.each_pair do |k, v| + dc.set(k, v) + end + + # Invocation without block + resp = dc.get_multi_cas(%w(a b c d e f)) + resp.each_pair do |k, data| + value, cas = [data.first, data.second] + assert_equal expected_hash[k], value + assert(cas && cas != 0) + end + + # Invocation with block + dc.get_multi_cas(%w(a b c d e f)) do |k, data| + value, cas = [data.first, data.second] + assert_equal expected_hash[k], value + assert(cas && cas != 0) + end + end + end + + it 'supports replace-with-CAS operation' do + memcached_cas do |dc| + dc.flush + cas = dc.set('key', 'value') + + # Accepts CAS, replaces, and returns new CAS + cas = dc.replace_cas('key', 'value2', cas) + assert cas.is_a?(Integer) + + assert_equal 'value2', dc.get('key') + end + end + + it 'supports delete with CAS' do + memcached_cas do |dc| + cas = dc.set('some_key', 'some_value') + dc.delete_cas('some_key', cas) + assert_nil dc.get('some_key') + end + end + + it 'handles CAS round-trip operations' do + memcached_cas do |dc| + dc.flush + + expected = {'blah' => 'blerg!'} + dc.set('some_key', expected) + + value, cas = dc.get_cas('some_key') + assert_equal value, expected + assert(!cas.nil? && cas != 0) + + # Set operation, first with wrong then with correct CAS + expected = {'blah' => 'set succeeded'} + assert(dc.set_cas('some_key', expected, cas+1) == false) + assert op_addset_succeeds(cas = dc.set_cas('some_key', expected, cas)) + + # Replace operation, first with wrong then with correct CAS + expected = {'blah' => 'replace succeeded'} + assert(dc.replace_cas('some_key', expected, cas+1) == false) + assert op_addset_succeeds(cas = dc.replace_cas('some_key', expected, cas)) + + # Delete operation, first with wrong then with correct CAS + assert(dc.delete_cas('some_key', cas+1) == false) + assert dc.delete_cas('some_key', cas) + end + end + + end +end diff --git a/lib/dalli-2.7.2/test/test_compressor.rb b/lib/dalli-2.7.2/test/test_compressor.rb new file mode 100644 index 000000000..8b024862b --- /dev/null +++ b/lib/dalli-2.7.2/test/test_compressor.rb @@ -0,0 +1,53 @@ +# encoding: utf-8 +require 'helper' +require 'json' +require 'memcached_mock' + +class NoopCompressor + def self.compress(data) + data + end + + def self.decompress(data) + data + end +end + +describe 'Compressor' do + + it 'default to Dalli::Compressor' do + memcached_kill(29199) do |dc| + memcache = Dalli::Client.new('127.0.0.1:29199') + memcache.set 1,2 + assert_equal Dalli::Compressor, memcache.instance_variable_get('@ring').servers.first.compressor + end + end + + it 'support a custom compressor' do + memcached_kill(29199) do |dc| + memcache = Dalli::Client.new('127.0.0.1:29199', :compressor => NoopCompressor) + memcache.set 1,2 + begin + assert_equal NoopCompressor, memcache.instance_variable_get('@ring').servers.first.compressor + + memcached(19127) do |newdc| + assert newdc.set("string-test", "a test string") + assert_equal("a test string", newdc.get("string-test")) + end + end + end + end +end + +describe 'GzipCompressor' do + + it 'compress and uncompress data using Zlib::GzipWriter/Reader' do + memcached(19127,nil,{:compress=>true,:compressor=>Dalli::GzipCompressor}) do |dc| + data = (0...1025).map{65.+(rand(26)).chr}.join + assert dc.set("test", data) + assert_equal Dalli::GzipCompressor, dc.instance_variable_get('@ring').servers.first.compressor + assert_equal(data, dc.get("test")) + end + end + +end diff --git a/lib/dalli-2.7.2/test/test_dalli.rb b/lib/dalli-2.7.2/test/test_dalli.rb new file mode 100644 index 000000000..fb6799184 --- /dev/null +++ b/lib/dalli-2.7.2/test/test_dalli.rb @@ -0,0 +1,625 @@ +require 'helper' +require 'memcached_mock' + +describe 'Dalli' do + describe 'options parsing' do + it 'handle deprecated options' do + dc = Dalli::Client.new('foo', :compression => true) + assert dc.instance_variable_get(:@options)[:compress] + refute dc.instance_variable_get(:@options)[:compression] + end + + it 'not warn about valid options' do + dc = Dalli::Client.new('foo', :compress => true) + # Rails.logger.expects :warn + assert dc.instance_variable_get(:@options)[:compress] + end + + it 'raises error with invalid expires_in' do + bad_data = [{:bad => 'expires in data'}, Hash, [1,2,3]] + bad_data.each do |bad| + assert_raises ArgumentError do + Dalli::Client.new('foo', {:expires_in => bad}) + end + end + end + + it 'return string type for namespace attribute' do + dc = Dalli::Client.new('foo', :namespace => :wunderschoen) + assert_equal "wunderschoen", dc.send(:namespace) + dc.close + + dc = Dalli::Client.new('foo', :namespace => Proc.new{:wunderschoen}) + assert_equal "wunderschoen", dc.send(:namespace) + dc.close + end + end + + describe 'key validation' do + it 'not allow blanks' do + memcached do |dc| + dc.set ' ', 1 + assert_equal 1, dc.get(' ') + dc.set "\t", 1 + assert_equal 1, dc.get("\t") + dc.set "\n", 1 + assert_equal 1, dc.get("\n") + assert_raises ArgumentError do + dc.set "", 1 + end + assert_raises ArgumentError do + dc.set nil, 1 + end + end + end + + it 'allow namespace to be a symbol' do + memcached(19122, '', :namespace => :wunderschoen) do |dc| + dc.set "x" * 251, 1 + assert 1, dc.get("#{'x' * 200}:md5:#{Digest::MD5.hexdigest('x' * 251)}") + end + end + end + + it "default to localhost:11211" do + dc = Dalli::Client.new + ring = dc.send(:ring) + s1 = ring.servers.first.hostname + assert_equal 1, ring.servers.size + dc.close + + dc = Dalli::Client.new('localhost:11211') + ring = dc.send(:ring) + s2 = ring.servers.first.hostname + assert_equal 1, ring.servers.size + dc.close + + dc = Dalli::Client.new(['localhost:11211']) + ring = dc.send(:ring) + s3 = ring.servers.first.hostname + assert_equal 1, ring.servers.size + dc.close + + assert_equal '127.0.0.1', s1 + assert_equal s2, s3 + end + + it "accept comma separated string" do + dc = Dalli::Client.new("server1.example.com:11211,server2.example.com:11211") + ring = dc.send(:ring) + assert_equal 2, ring.servers.size + s1,s2 = ring.servers.map(&:hostname) + assert_equal "server1.example.com", s1 + assert_equal "server2.example.com", s2 + end + + it "accept array of servers" do + dc = Dalli::Client.new(["server1.example.com:11211","server2.example.com:11211"]) + ring = dc.send(:ring) + assert_equal 2, ring.servers.size + s1,s2 = ring.servers.map(&:hostname) + assert_equal "server1.example.com", s1 + assert_equal "server2.example.com", s2 + end + + describe 'using a live server' do + + it "support get/set" do + memcached do |dc| + dc.flush + + val1 = "1234567890"*105000 + assert_equal false, dc.set('a', val1) + + val1 = "1234567890"*100000 + dc.set('a', val1) + val2 = dc.get('a') + assert_equal val1, val2 + + assert op_addset_succeeds(dc.set('a', nil)) + assert_nil dc.get('a') + end + end + + it 'supports delete' do + memcached do |dc| + dc.set('some_key', 'some_value') + assert_equal 'some_value', dc.get('some_key') + + dc.delete('some_key') + assert_nil dc.get('some_key') + end + end + + it 'returns nil for nonexist key' do + memcached do |dc| + assert_equal nil, dc.get('notexist') + end + end + + it 'allows "Not found" as value' do + memcached do |dc| + dc.set('key1', 'Not found') + assert_equal 'Not found', dc.get('key1') + end + end + + it "support stats" do + memcached do |dc| + # make sure that get_hits would not equal 0 + dc.get(:a) + + stats = dc.stats + servers = stats.keys + assert(servers.any? do |s| + stats[s]["get_hits"].to_i != 0 + end, "general stats failed") + + stats_items = dc.stats(:items) + servers = stats_items.keys + assert(servers.all? do |s| + stats_items[s].keys.any? do |key| + key =~ /items:[0-9]+:number/ + end + end, "stats items failed") + + stats_slabs = dc.stats(:slabs) + servers = stats_slabs.keys + assert(servers.all? do |s| + stats_slabs[s].keys.any? do |key| + key == "active_slabs" + end + end, "stats slabs failed") + + # reset_stats test + results = dc.reset_stats + assert(results.all? { |x| x }) + stats = dc.stats + servers = stats.keys + + # check if reset was performed + servers.each do |s| + assert_equal 0, dc.stats[s]["get_hits"].to_i + end + end + end + + it "support the fetch operation" do + memcached do |dc| + dc.flush + + expected = { 'blah' => 'blerg!' } + executed = false + value = dc.fetch('fetch_key') do + executed = true + expected + end + assert_equal expected, value + assert_equal true, executed + + executed = false + value = dc.fetch('fetch_key') do + executed = true + expected + end + assert_equal expected, value + assert_equal false, executed + end + end + + it "support the fetch operation with falsey values" do + memcached do |dc| + dc.flush + + dc.set("fetch_key", false) + res = dc.fetch("fetch_key") { flunk "fetch block called" } + assert_equal false, res + + dc.set("fetch_key", nil) + res = dc.fetch("fetch_key") { "bob" } + assert_equal 'bob', res + end + end + + it "support the cas operation" do + memcached do |dc| + dc.flush + + expected = { 'blah' => 'blerg!' } + + resp = dc.cas('cas_key') do |value| + fail('Value it not exist') + end + assert_nil resp + + mutated = { 'blah' => 'foo!' } + dc.set('cas_key', expected) + resp = dc.cas('cas_key') do |value| + assert_equal expected, value + mutated + end + assert op_cas_succeeds(resp) + + resp = dc.get('cas_key') + assert_equal mutated, resp + end + end + + it "support multi-get" do + memcached do |dc| + dc.close + dc.flush + resp = dc.get_multi(%w(a b c d e f)) + assert_equal({}, resp) + + dc.set('a', 'foo') + dc.set('b', 123) + dc.set('c', %w(a b c)) + # Invocation without block + resp = dc.get_multi(%w(a b c d e f)) + expected_resp = { 'a' => 'foo', 'b' => 123, 'c' => %w(a b c) } + assert_equal(expected_resp, resp) + + # Invocation with block + dc.get_multi(%w(a b c d e f)) do |k, v| + assert(expected_resp.has_key?(k) && expected_resp[k] == v) + expected_resp.delete(k) + end + assert expected_resp.empty? + + # Perform a big multi-get with 1000 elements. + arr = [] + dc.multi do + 1000.times do |idx| + dc.set idx, idx + arr << idx + end + end + + result = dc.get_multi(arr) + assert_equal(1000, result.size) + assert_equal(50, result['50']) + end + end + + it 'support raw incr/decr' do + memcached do |client| + client.flush + + assert op_addset_succeeds(client.set('fakecounter', 0, 0, :raw => true)) + assert_equal 1, client.incr('fakecounter', 1) + assert_equal 2, client.incr('fakecounter', 1) + assert_equal 3, client.incr('fakecounter', 1) + assert_equal 1, client.decr('fakecounter', 2) + assert_equal "1", client.get('fakecounter', :raw => true) + + resp = client.incr('mycounter', 0) + assert_nil resp + + resp = client.incr('mycounter', 1, 0, 2) + assert_equal 2, resp + resp = client.incr('mycounter', 1) + assert_equal 3, resp + + resp = client.set('rawcounter', 10, 0, :raw => true) + assert op_cas_succeeds(resp) + + resp = client.get('rawcounter', :raw => true) + assert_equal '10', resp + + resp = client.incr('rawcounter', 1) + assert_equal 11, resp + end + end + + it "support incr/decr operations" do + memcached do |dc| + dc.flush + + resp = dc.decr('counter', 100, 5, 0) + assert_equal 0, resp + + resp = dc.decr('counter', 10) + assert_equal 0, resp + + resp = dc.incr('counter', 10) + assert_equal 10, resp + + current = 10 + 100.times do |x| + resp = dc.incr('counter', 10) + assert_equal current + ((x+1)*10), resp + end + + resp = dc.decr('10billion', 0, 5, 10) + # go over the 32-bit mark to verify proper (un)packing + resp = dc.incr('10billion', 10_000_000_000) + assert_equal 10_000_000_010, resp + + resp = dc.decr('10billion', 1) + assert_equal 10_000_000_009, resp + + resp = dc.decr('10billion', 0) + assert_equal 10_000_000_009, resp + + resp = dc.incr('10billion', 0) + assert_equal 10_000_000_009, resp + + assert_nil dc.incr('DNE', 10) + assert_nil dc.decr('DNE', 10) + + resp = dc.incr('big', 100, 5, 0xFFFFFFFFFFFFFFFE) + assert_equal 0xFFFFFFFFFFFFFFFE, resp + resp = dc.incr('big', 1) + assert_equal 0xFFFFFFFFFFFFFFFF, resp + + # rollover the 64-bit value, we'll get something undefined. + resp = dc.incr('big', 1) + refute_equal 0x10000000000000000, resp + dc.reset + end + end + + it 'support the append and prepend operations' do + memcached do |dc| + dc.flush + assert op_addset_succeeds(dc.set('456', 'xyz', 0, :raw => true)) + assert_equal true, dc.prepend('456', '0') + assert_equal true, dc.append('456', '9') + assert_equal '0xyz9', dc.get('456', :raw => true) + assert_equal '0xyz9', dc.get('456') + + assert_equal false, dc.append('nonexist', 'abc') + assert_equal false, dc.prepend('nonexist', 'abc') + end + end + + it 'supports replace operation' do + memcached do |dc| + dc.flush + dc.set('key', 'value') + assert op_replace_succeeds(dc.replace('key', 'value2')) + + assert_equal 'value2', dc.get('key') + end + end + + it 'support touch operation' do + memcached do |dc| + begin + dc.flush + dc.set 'key', 'value' + assert_equal true, dc.touch('key', 10) + assert_equal true, dc.touch('key') + assert_equal 'value', dc.get('key') + assert_nil dc.touch('notexist') + rescue Dalli::DalliError => e + # This will happen when memcached is in lesser version than 1.4.8 + assert_equal 'Response error 129: Unknown command', e.message + end + end + end + + it 'support version operation' do + memcached do |dc| + v = dc.version + servers = v.keys + assert(servers.any? do |s| + v[s] != nil + end, "version failed") + end + end + + it 'allow TCP connections to be configured for keepalive' do + memcached(19122, '', :keepalive => true) do |dc| + dc.set(:a, 1) + ring = dc.send(:ring) + server = ring.servers.first + socket = server.instance_variable_get('@sock') + + optval = socket.getsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE) + optval = optval.unpack 'i' + + assert_equal true, (optval[0] != 0) + end + end + + it "pass a simple smoke test" do + memcached do |dc| + resp = dc.flush + refute_nil resp + assert_equal [true, true], resp + + assert op_addset_succeeds(dc.set(:foo, 'bar')) + assert_equal 'bar', dc.get(:foo) + + resp = dc.get('123') + assert_equal nil, resp + + assert op_addset_succeeds(dc.set('123', 'xyz')) + + resp = dc.get('123') + assert_equal 'xyz', resp + + assert op_addset_succeeds(dc.set('123', 'abc')) + + dc.prepend('123', '0') + dc.append('123', '0') + + assert_raises Dalli::UnmarshalError do + resp = dc.get('123') + end + + dc.close + dc = nil + + dc = Dalli::Client.new('localhost:19122') + + assert op_addset_succeeds(dc.set('456', 'xyz', 0, :raw => true)) + + resp = dc.prepend '456', '0' + assert_equal true, resp + + resp = dc.append '456', '9' + assert_equal true, resp + + resp = dc.get('456', :raw => true) + assert_equal '0xyz9', resp + + assert op_addset_succeeds(dc.set('456', false)) + + resp = dc.get('456') + assert_equal false, resp + + resp = dc.stats + assert_equal Hash, resp.class + + dc.close + end + end + + it "support multithreaded access" do + memcached do |cache| + cache.flush + workers = [] + + cache.set('f', 'zzz') + assert op_cas_succeeds((cache.cas('f') do |value| + value << 'z' + end)) + assert_equal 'zzzz', cache.get('f') + + # Have a bunch of threads perform a bunch of operations at the same time. + # Verify the result of each operation to ensure the request and response + # are not intermingled between threads. + 10.times do + workers << Thread.new do + 100.times do + cache.set('a', 9) + cache.set('b', 11) + inc = cache.incr('cat', 10, 0, 10) + cache.set('f', 'zzz') + res = cache.cas('f') do |value| + value << 'z' + end + refute_nil res + assert_equal false, cache.add('a', 11) + assert_equal({ 'a' => 9, 'b' => 11 }, cache.get_multi(['a', 'b'])) + inc = cache.incr('cat', 10) + assert_equal 0, inc % 5 + cache.decr('cat', 5) + assert_equal 11, cache.get('b') + + assert_equal %w(a b), cache.get_multi('a', 'b', 'c').keys.sort + + end + end + end + + workers.each { |w| w.join } + cache.flush + end + end + + it "handle namespaced keys" do + memcached do |dc| + dc = Dalli::Client.new('localhost:19122', :namespace => 'a') + dc.set('namespaced', 1) + dc2 = Dalli::Client.new('localhost:19122', :namespace => 'b') + dc2.set('namespaced', 2) + assert_equal 1, dc.get('namespaced') + assert_equal 2, dc2.get('namespaced') + end + end + + it "handle nil namespace" do + memcached do |dc| + dc = Dalli::Client.new('localhost:19122', :namespace => nil) + assert_equal 'key', dc.send(:validate_key, 'key') + end + end + + it 'truncate cache keys that are too long' do + memcached do + dc = Dalli::Client.new('localhost:19122', :namespace => 'some:namspace') + key = "this cache key is far too long so it must be hashed and truncated and stuff" * 10 + value = "some value" + assert op_addset_succeeds(dc.set(key, value)) + assert_equal value, dc.get(key) + end + end + + it "handle namespaced keys in multi_get" do + memcached do |dc| + dc = Dalli::Client.new('localhost:19122', :namespace => 'a') + dc.set('a', 1) + dc.set('b', 2) + assert_equal({'a' => 1, 'b' => 2}, dc.get_multi('a', 'b')) + end + end + + it "handle application marshalling issues" do + memcached do |dc| + old = Dalli.logger + Dalli.logger = Logger.new(nil) + begin + assert_equal false, dc.set('a', Proc.new { true }) + ensure + Dalli.logger = old + end + end + end + + describe 'with compression' do + it 'allow large values' do + memcached do |dc| + dalli = Dalli::Client.new(dc.instance_variable_get(:@servers), :compress => true) + + value = "0"*1024*1024 + assert_equal false, dc.set('verylarge', value) + dalli.set('verylarge', value) + end + end + end + + describe 'in low memory conditions' do + + it 'handle error response correctly' do + memcached(19125, '-m 1 -M') do |dc| + failed = false + value = "1234567890"*100 + 1_000.times do |idx| + begin + assert op_addset_succeeds(dc.set(idx, value)) + rescue Dalli::DalliError + failed = true + assert((800..960).include?(idx), "unexpected failure on iteration #{idx}") + break + end + end + assert failed, 'did not fail under low memory conditions' + end + end + + it 'fit more values with compression' do + memcached(19126, '-m 1 -M') do |dc| + dalli = Dalli::Client.new('localhost:19126', :compress => true) + failed = false + value = "1234567890"*1000 + 10_000.times do |idx| + begin + assert op_addset_succeeds(dalli.set(idx, value)) + rescue Dalli::DalliError + failed = true + assert((6000..7800).include?(idx), "unexpected failure on iteration #{idx}") + break + end + end + assert failed, 'did not fail under low memory conditions' + end + end + + end + + end +end diff --git a/lib/dalli-2.7.2/test/test_encoding.rb b/lib/dalli-2.7.2/test/test_encoding.rb new file mode 100644 index 000000000..2ab3ece64 --- /dev/null +++ b/lib/dalli-2.7.2/test/test_encoding.rb @@ -0,0 +1,32 @@ +# encoding: utf-8 +require 'helper' +require 'memcached_mock' + +describe 'Encoding' do + + describe 'using a live server' do + it 'support i18n content' do + memcached do |dc| + key = 'foo' + utf_key = utf8 = 'ƒ©åÍÎ' + + assert dc.set(key, utf8) + assert_equal utf8, dc.get(key) + + dc.set(utf_key, utf8) + assert_equal utf8, dc.get(utf_key) + end + end + + it 'support content expiry' do + memcached do |dc| + key = 'foo' + assert dc.set(key, 'bar', 1) + assert_equal 'bar', dc.get(key) + sleep 1.2 + assert_equal nil, dc.get(key) + end + end + + end +end diff --git a/lib/dalli-2.7.2/test/test_failover.rb b/lib/dalli-2.7.2/test/test_failover.rb new file mode 100644 index 000000000..990b6bc31 --- /dev/null +++ b/lib/dalli-2.7.2/test/test_failover.rb @@ -0,0 +1,128 @@ +require 'helper' + +describe 'failover' do + + describe 'timeouts' do + it 'not lead to corrupt sockets' do + memcached(29125) do + dc = Dalli::Client.new ['localhost:29125'] + begin + Timeout.timeout 0.01 do + 1_000.times do + dc.set("test_123", {:test => "123"}) + end + flunk("Did not timeout") + end + rescue Timeout::Error + end + + assert_equal({:test => '123'}, dc.get("test_123")) + end + end + end + + + describe 'assuming some bad servers' do + + it 'silently reconnect if server hiccups' do + memcached(29125) do + dc = Dalli::Client.new ['localhost:29125'] + dc.set 'foo', 'bar' + foo = dc.get 'foo' + assert_equal foo, 'bar' + + memcached_kill(29125) + memcached(29125) do + + foo = dc.get 'foo' + assert_nil foo + + memcached_kill(29125) + end + end + end + + it 'handle graceful failover' do + memcached(29125) do + memcached(29126) do + dc = Dalli::Client.new ['localhost:29125', 'localhost:29126'] + dc.set 'foo', 'bar' + foo = dc.get 'foo' + assert_equal foo, 'bar' + + memcached_kill(29125) + + dc.set 'foo', 'bar' + foo = dc.get 'foo' + assert_equal foo, 'bar' + + memcached_kill(29126) + + assert_raises Dalli::RingError, :message => "No server available" do + dc.set 'foo', 'bar' + end + end + end + end + + it 'handle them gracefully in get_multi' do + memcached(29125) do + memcached(29126) do + dc = Dalli::Client.new ['localhost:29125', 'localhost:29126'] + dc.set 'a', 'a1' + result = dc.get_multi ['a'] + assert_equal result, {'a' => 'a1'} + + memcached_kill(29125) + + result = dc.get_multi ['a'] + assert_equal result, {'a' => 'a1'} + end + end + end + + it 'handle graceful failover in get_multi' do + memcached(29125) do + memcached(29126) do + dc = Dalli::Client.new ['localhost:29125', 'localhost:29126'] + dc.set 'foo', 'foo1' + dc.set 'bar', 'bar1' + result = dc.get_multi ['foo', 'bar'] + assert_equal result, {'foo' => 'foo1', 'bar' => 'bar1'} + + memcached_kill(29125) + + dc.set 'foo', 'foo1' + dc.set 'bar', 'bar1' + result = dc.get_multi ['foo', 'bar'] + assert_equal result, {'foo' => 'foo1', 'bar' => 'bar1'} + + memcached_kill(29126) + + result = dc.get_multi ['foo', 'bar'] + assert_equal result, {} + end + end + end + + it 'stats it still properly report' do + memcached(29125) do + memcached(29126) do + dc = Dalli::Client.new ['localhost:29125', 'localhost:29126'] + result = dc.stats + assert_instance_of Hash, result['localhost:29125'] + assert_instance_of Hash, result['localhost:29126'] + + memcached_kill(29125) + + dc = Dalli::Client.new ['localhost:29125', 'localhost:29126'] + result = dc.stats + assert_instance_of NilClass, result['localhost:29125'] + assert_instance_of Hash, result['localhost:29126'] + + memcached_kill(29126) + end + end + end + end +end diff --git a/lib/dalli-2.7.2/test/test_network.rb b/lib/dalli-2.7.2/test/test_network.rb new file mode 100644 index 000000000..7399c1737 --- /dev/null +++ b/lib/dalli-2.7.2/test/test_network.rb @@ -0,0 +1,54 @@ +require 'helper' + +describe 'Network' do + + describe 'assuming a bad network' do + + it 'handle no server available' do + assert_raises Dalli::RingError, :message => "No server available" do + dc = Dalli::Client.new 'localhost:19333' + dc.get 'foo' + end + end + + describe 'with a fake server' do + it 'handle connection reset' do + memcached_mock(lambda {|sock| sock.close }) do + assert_raises Dalli::RingError, :message => "No server available" do + dc = Dalli::Client.new('localhost:19123') + dc.get('abc') + end + end + end + + it 'handle malformed response' do + memcached_mock(lambda {|sock| sock.write('123') }) do + assert_raises Dalli::RingError, :message => "No server available" do + dc = Dalli::Client.new('localhost:19123') + dc.get('abc') + end + end + end + + it 'handle connect timeouts' do + memcached_mock(lambda {|sock| sleep(0.6); sock.close }, :delayed_start) do + assert_raises Dalli::RingError, :message => "No server available" do + dc = Dalli::Client.new('localhost:19123') + dc.get('abc') + end + end + end + + it 'handle read timeouts' do + memcached_mock(lambda {|sock| sleep(0.6); sock.write('giraffe') }) do + assert_raises Dalli::RingError, :message => "No server available" do + dc = Dalli::Client.new('localhost:19123') + dc.get('abc') + end + end + end + + end + + end +end diff --git a/lib/dalli-2.7.2/test/test_rack_session.rb b/lib/dalli-2.7.2/test/test_rack_session.rb new file mode 100644 index 000000000..657b061a7 --- /dev/null +++ b/lib/dalli-2.7.2/test/test_rack_session.rb @@ -0,0 +1,341 @@ +require 'helper' + +require 'rack/session/dalli' +require 'rack/lint' +require 'rack/mock' +require 'thread' + +describe Rack::Session::Dalli do + Rack::Session::Dalli::DEFAULT_OPTIONS[:memcache_server] = 'localhost:19129' + + before do + memcached(19129) do + end + + # test memcache connection + Rack::Session::Dalli.new(incrementor) + end + + let(:session_key) { Rack::Session::Dalli::DEFAULT_OPTIONS[:key] } + let(:session_match) do + /#{session_key}=([0-9a-fA-F]+);/ + end + let(:incrementor_proc) do + lambda do |env| + env["rack.session"]["counter"] ||= 0 + env["rack.session"]["counter"] += 1 + Rack::Response.new(env["rack.session"].inspect).to_a + end + end + let(:drop_session) do + Rack::Lint.new(proc do |env| + env['rack.session.options'][:drop] = true + incrementor_proc.call(env) + end) + end + let(:renew_session) do + Rack::Lint.new(proc do |env| + env['rack.session.options'][:renew] = true + incrementor_proc.call(env) + end) + end + let(:defer_session) do + Rack::Lint.new(proc do |env| + env['rack.session.options'][:defer] = true + incrementor_proc.call(env) + end) + end + let(:skip_session) do + Rack::Lint.new(proc do |env| + env['rack.session.options'][:skip] = true + incrementor_proc.call(env) + end) + end + let(:incrementor) { Rack::Lint.new(incrementor_proc) } + + it "faults on no connection" do + assert_raises Dalli::RingError do + Rack::Session::Dalli.new(incrementor, :memcache_server => 'nosuchserver') + end + end + + it "connects to existing server" do + assert_silent do + rsd = Rack::Session::Dalli.new(incrementor, :namespace => 'test:rack:session') + rsd.pool.set('ping', '') + end + end + + it "passes options to MemCache" do + rsd = Rack::Session::Dalli.new(incrementor, :namespace => 'test:rack:session') + assert_equal('test:rack:session', rsd.pool.instance_eval { @options[:namespace] }) + end + + it "creates a new cookie" do + rsd = Rack::Session::Dalli.new(incrementor) + res = Rack::MockRequest.new(rsd).get("/") + assert res["Set-Cookie"].include?("#{session_key}=") + assert_equal '{"counter"=>1}', res.body + end + + it "determines session from a cookie" do + rsd = Rack::Session::Dalli.new(incrementor) + req = Rack::MockRequest.new(rsd) + res = req.get("/") + cookie = res["Set-Cookie"] + assert_equal '{"counter"=>2}', req.get("/", "HTTP_COOKIE" => cookie).body + assert_equal '{"counter"=>3}', req.get("/", "HTTP_COOKIE" => cookie).body + end + + it "determines session only from a cookie by default" do + rsd = Rack::Session::Dalli.new(incrementor) + req = Rack::MockRequest.new(rsd) + res = req.get("/") + sid = res["Set-Cookie"][session_match, 1] + assert_equal '{"counter"=>1}', req.get("/?rack.session=#{sid}").body + assert_equal '{"counter"=>1}', req.get("/?rack.session=#{sid}").body + end + + it "determines session from params" do + rsd = Rack::Session::Dalli.new(incrementor, :cookie_only => false) + req = Rack::MockRequest.new(rsd) + res = req.get("/") + sid = res["Set-Cookie"][session_match, 1] + assert_equal '{"counter"=>2}', req.get("/?rack.session=#{sid}").body + assert_equal '{"counter"=>3}', req.get("/?rack.session=#{sid}").body + end + + it "survives nonexistant cookies" do + bad_cookie = "rack.session=blarghfasel" + rsd = Rack::Session::Dalli.new(incrementor) + res = Rack::MockRequest.new(rsd). + get("/", "HTTP_COOKIE" => bad_cookie) + assert_equal '{"counter"=>1}', res.body + cookie = res["Set-Cookie"][session_match] + refute_match(/#{bad_cookie}/, cookie) + end + + it "survives nonexistant blank cookies" do + bad_cookie = "rack.session=" + rsd = Rack::Session::Dalli.new(incrementor) + res = Rack::MockRequest.new(rsd). + get("/", "HTTP_COOKIE" => bad_cookie) + cookie = res["Set-Cookie"][session_match] + refute_match(/#{bad_cookie}$/, cookie) + end + + it "maintains freshness" do + rsd = Rack::Session::Dalli.new(incrementor, :expire_after => 3) + res = Rack::MockRequest.new(rsd).get('/') + assert res.body.include?('"counter"=>1') + cookie = res["Set-Cookie"] + res = Rack::MockRequest.new(rsd).get('/', "HTTP_COOKIE" => cookie) + assert_equal cookie, res["Set-Cookie"] + assert res.body.include?('"counter"=>2') + puts 'Sleeping to expire session' if $DEBUG + sleep 4 + res = Rack::MockRequest.new(rsd).get('/', "HTTP_COOKIE" => cookie) + refute_equal cookie, res["Set-Cookie"] + assert res.body.include?('"counter"=>1') + end + + it "does not send the same session id if it did not change" do + rsd = Rack::Session::Dalli.new(incrementor) + req = Rack::MockRequest.new(rsd) + + res0 = req.get("/") + cookie = res0["Set-Cookie"][session_match] + assert_equal '{"counter"=>1}', res0.body + + res1 = req.get("/", "HTTP_COOKIE" => cookie) + assert_nil res1["Set-Cookie"] + assert_equal '{"counter"=>2}', res1.body + + res2 = req.get("/", "HTTP_COOKIE" => cookie) + assert_nil res2["Set-Cookie"] + assert_equal '{"counter"=>3}', res2.body + end + + it "deletes cookies with :drop option" do + rsd = Rack::Session::Dalli.new(incrementor) + req = Rack::MockRequest.new(rsd) + drop = Rack::Utils::Context.new(rsd, drop_session) + dreq = Rack::MockRequest.new(drop) + + res1 = req.get("/") + session = (cookie = res1["Set-Cookie"])[session_match] + assert_equal '{"counter"=>1}', res1.body + + res2 = dreq.get("/", "HTTP_COOKIE" => cookie) + assert_nil res2["Set-Cookie"] + assert_equal '{"counter"=>2}', res2.body + + res3 = req.get("/", "HTTP_COOKIE" => cookie) + refute_equal session, res3["Set-Cookie"][session_match] + assert_equal '{"counter"=>1}', res3.body + end + + it "provides new session id with :renew option" do + rsd = Rack::Session::Dalli.new(incrementor) + req = Rack::MockRequest.new(rsd) + renew = Rack::Utils::Context.new(rsd, renew_session) + rreq = Rack::MockRequest.new(renew) + + res1 = req.get("/") + session = (cookie = res1["Set-Cookie"])[session_match] + assert_equal '{"counter"=>1}', res1.body + + res2 = rreq.get("/", "HTTP_COOKIE" => cookie) + new_cookie = res2["Set-Cookie"] + new_session = new_cookie[session_match] + refute_equal session, new_session + assert_equal '{"counter"=>2}', res2.body + + res3 = req.get("/", "HTTP_COOKIE" => new_cookie) + assert_equal '{"counter"=>3}', res3.body + + # Old cookie was deleted + res4 = req.get("/", "HTTP_COOKIE" => cookie) + assert_equal '{"counter"=>1}', res4.body + end + + it "omits cookie with :defer option but still updates the state" do + rsd = Rack::Session::Dalli.new(incrementor) + count = Rack::Utils::Context.new(rsd, incrementor) + defer = Rack::Utils::Context.new(rsd, defer_session) + dreq = Rack::MockRequest.new(defer) + creq = Rack::MockRequest.new(count) + + res0 = dreq.get("/") + assert_nil res0["Set-Cookie"] + assert_equal '{"counter"=>1}', res0.body + + res0 = creq.get("/") + res1 = dreq.get("/", "HTTP_COOKIE" => res0["Set-Cookie"]) + assert_equal '{"counter"=>2}', res1.body + res2 = dreq.get("/", "HTTP_COOKIE" => res0["Set-Cookie"]) + assert_equal '{"counter"=>3}', res2.body + end + + it "omits cookie and state update with :skip option" do + rsd = Rack::Session::Dalli.new(incrementor) + count = Rack::Utils::Context.new(rsd, incrementor) + skip = Rack::Utils::Context.new(rsd, skip_session) + sreq = Rack::MockRequest.new(skip) + creq = Rack::MockRequest.new(count) + + res0 = sreq.get("/") + assert_nil res0["Set-Cookie"] + assert_equal '{"counter"=>1}', res0.body + + res0 = creq.get("/") + res1 = sreq.get("/", "HTTP_COOKIE" => res0["Set-Cookie"]) + assert_equal '{"counter"=>2}', res1.body + res2 = sreq.get("/", "HTTP_COOKIE" => res0["Set-Cookie"]) + assert_equal '{"counter"=>2}', res2.body + end + + it "updates deep hashes correctly" do + hash_check = proc do |env| + session = env['rack.session'] + unless session.include? 'test' + session.update :a => :b, :c => { :d => :e }, + :f => { :g => { :h => :i} }, 'test' => true + else + session[:f][:g][:h] = :j + end + [200, {}, [session.inspect]] + end + rsd = Rack::Session::Dalli.new(hash_check) + req = Rack::MockRequest.new(rsd) + + res0 = req.get("/") + session_id = (cookie = res0["Set-Cookie"])[session_match, 1] + ses0 = rsd.pool.get(session_id, true) + + req.get("/", "HTTP_COOKIE" => cookie) + ses1 = rsd.pool.get(session_id, true) + + refute_equal ses0, ses1 + end + + # anyone know how to do this better? + it "cleanly merges sessions when multithreaded" do + unless $DEBUG + assert_equal 1, 1 # fake assertion to appease the mighty bacon + next + end + warn 'Running multithread test for Session::Dalli' + rsd = Rack::Session::Dalli.new(incrementor) + req = Rack::MockRequest.new(rsd) + + res = req.get('/') + assert_equal '{"counter"=>1}', res.body + cookie = res["Set-Cookie"] + session_id = cookie[session_match, 1] + + delta_incrementor = lambda do |env| + # emulate disconjoinment of threading + env['rack.session'] = env['rack.session'].dup + Thread.stop + env['rack.session'][(Time.now.usec*rand).to_i] = true + incrementor.call(env) + end + tses = Rack::Utils::Context.new rsd, delta_incrementor + treq = Rack::MockRequest.new(tses) + tnum = rand(7).to_i+5 + r = Array.new(tnum) do + Thread.new(treq) do |run| + run.get('/', "HTTP_COOKIE" => cookie, 'rack.multithread' => true) + end + end.reverse.map{|t| t.run.join.value } + r.each do |request| + assert_equal cookie, request['Set-Cookie'] + assert request.body.include?('"counter"=>2') + end + + session = rsd.pool.get(session_id) + assert_equal tnum+1, session.size # counter + assert_equal 2, session['counter'] # meeeh + + tnum = rand(7).to_i+5 + r = Array.new(tnum) do |i| + app = Rack::Utils::Context.new rsd, time_delta + req = Rack::MockRequest.new app + Thread.new(req) do |run| + run.get('/', "HTTP_COOKIE" => cookie, 'rack.multithread' => true) + end + end.reverse.map{|t| t.run.join.value } + r.each do |request| + assert_equal cookie, request['Set-Cookie'] + assert request.body.include?('"counter"=>3') + end + + session = rsd.pool.get(session_id) + assert_equal tnum+1, session.size + assert_equal 3, session['counter'] + + drop_counter = proc do |env| + env['rack.session'].delete 'counter' + env['rack.session']['foo'] = 'bar' + [200, {'Content-Type'=>'text/plain'}, env['rack.session'].inspect] + end + tses = Rack::Utils::Context.new rsd, drop_counter + treq = Rack::MockRequest.new(tses) + tnum = rand(7).to_i+5 + r = Array.new(tnum) do + Thread.new(treq) do |run| + run.get('/', "HTTP_COOKIE" => cookie, 'rack.multithread' => true) + end + end.reverse.map{|t| t.run.join.value } + r.each do |request| + assert_equal cookie, request['Set-Cookie'] + assert request.body.include?('"foo"=>"bar"') + end + + session = rsd.pool.get(session_id) + assert_equal r.size+1, session.size + assert_nil session['counter'] + assert_equal 'bar', session['foo'] + end +end diff --git a/lib/dalli-2.7.2/test/test_ring.rb b/lib/dalli-2.7.2/test/test_ring.rb new file mode 100644 index 000000000..de4862a9c --- /dev/null +++ b/lib/dalli-2.7.2/test/test_ring.rb @@ -0,0 +1,85 @@ +require 'helper' + +describe 'Ring' do + + describe 'a ring of servers' do + + it "have the continuum sorted by value" do + servers = [stub(:hostname => "localhost", :port => "11211", :weight => 1), + stub(:hostname => "localhost", :port => "9500", :weight => 1)] + ring = Dalli::Ring.new(servers, {}) + previous_value = 0 + ring.continuum.each do |entry| + assert entry.value > previous_value + previous_value = entry.value + end + end + + it 'raise when no servers are available/defined' do + ring = Dalli::Ring.new([], {}) + assert_raises Dalli::RingError, :message => "No server available" do + ring.server_for_key('test') + end + end + + describe 'containing only a single server' do + it "raise correctly when it's not alive" do + servers = [ + Dalli::Server.new("localhost:12345"), + ] + ring = Dalli::Ring.new(servers, {}) + assert_raises Dalli::RingError, :message => "No server available" do + ring.server_for_key('test') + end + end + + it "return the server when it's alive" do + servers = [ + Dalli::Server.new("localhost:19191"), + ] + ring = Dalli::Ring.new(servers, {}) + memcached(19191) do |mc| + ring = mc.send(:ring) + assert_equal ring.servers.first.port, ring.server_for_key('test').port + end + end + end + + describe 'containing multiple servers' do + it "raise correctly when no server is alive" do + servers = [ + Dalli::Server.new("localhost:12345"), + Dalli::Server.new("localhost:12346"), + ] + ring = Dalli::Ring.new(servers, {}) + assert_raises Dalli::RingError, :message => "No server available" do + ring.server_for_key('test') + end + end + + it "return an alive server when at least one is alive" do + servers = [ + Dalli::Server.new("localhost:12346"), + Dalli::Server.new("localhost:19191"), + ] + ring = Dalli::Ring.new(servers, {}) + memcached(19191) do |mc| + ring = mc.send(:ring) + assert_equal ring.servers.first.port, ring.server_for_key('test').port + end + end + end + + it 'detect when a dead server is up again' do + memcached(19997) do + down_retry_delay = 0.5 + dc = Dalli::Client.new(['localhost:19997', 'localhost:19998'], :down_retry_delay => down_retry_delay) + assert_equal 1, dc.stats.values.compact.count + + memcached(19998) do + assert_equal 2, dc.stats.values.compact.count + end + end + end + end +end diff --git a/lib/dalli-2.7.2/test/test_sasl.rb b/lib/dalli-2.7.2/test/test_sasl.rb new file mode 100644 index 000000000..eca91c6a2 --- /dev/null +++ b/lib/dalli-2.7.2/test/test_sasl.rb @@ -0,0 +1,110 @@ +require 'helper' + +describe 'Sasl' do + + # https://github.com/seattlerb/minitest/issues/298 + def self.xit(msg, &block) + end + + describe 'a server requiring authentication' do + before do + @server = mock() + @server.stubs(:request).returns(true) + @server.stubs(:weight).returns(1) + @server.stubs(:hostname).returns("localhost") + @server.stubs(:port).returns("19124") + end + + describe 'without authentication credentials' do + before do + ENV['MEMCACHE_USERNAME'] = 'foo' + ENV['MEMCACHE_PASSWORD'] = 'wrongpwd' + end + + after do + ENV['MEMCACHE_USERNAME'] = nil + ENV['MEMCACHE_PASSWORD'] = nil + end + + it 'provide one test that passes' do + assert true + end + + it 'gracefully handle authentication failures' do + memcached(19124, '-S') do |dc| + assert_error Dalli::DalliError, /32/ do + dc.set('abc', 123) + end + end + end + end + + it 'fail SASL authentication with wrong options' do + memcached(19124, '-S') do |dc| + dc = Dalli::Client.new('localhost:19124', :username => 'foo', :password => 'wrongpwd') + assert_error Dalli::DalliError, /32/ do + dc.set('abc', 123) + end + end + end + + # OSX: Create a SASL user for the memcached application like so: + # + # saslpasswd2 -a memcached -c testuser + # + # with password 'testtest' + describe 'in an authenticated environment' do + before do + ENV['MEMCACHE_USERNAME'] = 'testuser' + ENV['MEMCACHE_PASSWORD'] = 'testtest' + end + + after do + ENV['MEMCACHE_USERNAME'] = nil + ENV['MEMCACHE_PASSWORD'] = nil + end + + xit 'pass SASL authentication' do + memcached(19124, '-S') do |dc| + # I get "Dalli::DalliError: Error authenticating: 32" in OSX + # but SASL works on Heroku servers. YMMV. + assert_equal true, dc.set('abc', 123) + assert_equal 123, dc.get('abc') + results = dc.stats + assert_equal 1, results.size + assert_equal 38, results.values.first.size + end + end + end + + xit 'pass SASL authentication with options' do + memcached(19124, '-S') do |dc| + dc = Dalli::Client.new('localhost:19124', :username => 'testuser', :password => 'testtest') + # I get "Dalli::DalliError: Error authenticating: 32" in OSX + # but SASL works on Heroku servers. YMMV. + assert_equal true, dc.set('abc', 123) + assert_equal 123, dc.get('abc') + results = dc.stats + assert_equal 1, results.size + assert_equal 38, results.values.first.size + end + end + + it 'pass SASL as URI' do + Dalli::Server.expects(:new).with("localhost:19124", + :username => "testuser", :password => "testtest").returns(@server) + dc = Dalli::Client.new('memcached://testuser:testtest@localhost:19124') + dc.flush_all + end + + it 'pass SASL as ring of URIs' do + Dalli::Server.expects(:new).with("localhost:19124", + :username => "testuser", :password => "testtest").returns(@server) + Dalli::Server.expects(:new).with("otherhost:19125", + :username => "testuser2", :password => "testtest2").returns(@server) + dc = Dalli::Client.new(['memcached://testuser:testtest@localhost:19124', + 'memcached://testuser2:testtest2@otherhost:19125']) + dc.flush_all + end + end +end diff --git a/lib/dalli-2.7.2/test/test_serializer.rb b/lib/dalli-2.7.2/test/test_serializer.rb new file mode 100644 index 000000000..e8118f249 --- /dev/null +++ b/lib/dalli-2.7.2/test/test_serializer.rb @@ -0,0 +1,30 @@ +# encoding: utf-8 +require 'helper' +require 'json' +require 'memcached_mock' + +describe 'Serializer' do + + it 'default to Marshal' do + memcached_kill(29198) do |dc| + memcache = Dalli::Client.new('127.0.0.1:29198') + memcache.set 1,2 + assert_equal Marshal, memcache.instance_variable_get('@ring').servers.first.serializer + end + end + + it 'support a custom serializer' do + memcached_kill(29198) do |dc| + memcache = Dalli::Client.new('127.0.0.1:29198', :serializer => JSON) + memcache.set 1,2 + begin + assert_equal JSON, memcache.instance_variable_get('@ring').servers.first.serializer + + memcached(19128) do |newdc| + assert newdc.set("json_test", {"foo" => "bar"}) + assert_equal({"foo" => "bar"}, newdc.get("json_test")) + end + end + end + end +end diff --git a/lib/dalli-2.7.2/test/test_server.rb b/lib/dalli-2.7.2/test/test_server.rb new file mode 100644 index 000000000..547587176 --- /dev/null +++ b/lib/dalli-2.7.2/test/test_server.rb @@ -0,0 +1,80 @@ +require 'helper' + +describe Dalli::Server do + describe 'hostname parsing' do + it 'handles no port or weight' do + s = Dalli::Server.new('localhost') + assert_equal 'localhost', s.hostname + assert_equal 11211, s.port + assert_equal 1, s.weight + end + + it 'handles a port, but no weight' do + s = Dalli::Server.new('localhost:11212') + assert_equal 'localhost', s.hostname + assert_equal 11212, s.port + assert_equal 1, s.weight + end + + it 'handles a port and a weight' do + s = Dalli::Server.new('localhost:11212:2') + assert_equal 'localhost', s.hostname + assert_equal 11212, s.port + assert_equal 2, s.weight + end + + it 'handles ipv4 addresses' do + s = Dalli::Server.new('127.0.0.1') + assert_equal '127.0.0.1', s.hostname + assert_equal 11211, s.port + assert_equal 1, s.weight + end + + it 'handles ipv6 addresses' do + s = Dalli::Server.new('[::1]') + assert_equal '::1', s.hostname + assert_equal 11211, s.port + assert_equal 1, s.weight + end + + it 'handles ipv6 addresses with port' do + s = Dalli::Server.new('[::1]:11212') + assert_equal '::1', s.hostname + assert_equal 11212, s.port + assert_equal 1, s.weight + end + + it 'handles ipv6 addresses with port and weight' do + s = Dalli::Server.new('[::1]:11212:2') + assert_equal '::1', s.hostname + assert_equal 11212, s.port + assert_equal 2, s.weight + end + + it 'handles a FQDN' do + s = Dalli::Server.new('my.fqdn.com') + assert_equal 'my.fqdn.com', s.hostname + assert_equal 11211, s.port + assert_equal 1, s.weight + end + + it 'handles a FQDN with port and weight' do + s = Dalli::Server.new('my.fqdn.com:11212:2') + assert_equal 'my.fqdn.com', s.hostname + assert_equal 11212, s.port + assert_equal 2, s.weight + end + end + + describe 'ttl translation' do + it 'does not translate ttls under 30 days' do + s = Dalli::Server.new('localhost') + assert_equal s.send(:sanitize_ttl, 30*24*60*60), 30*24*60*60 + end + + it 'translates ttls over 30 days into timestamps' do + s = Dalli::Server.new('localhost') + assert_equal s.send(:sanitize_ttl, 30*24*60*60 + 1), Time.now.to_i + 30*24*60*60+1 + end + end +end From 90d9e77b324c8583d5bec10d95959fbccb4115b2 Mon Sep 17 00:00:00 2001 From: sw <939547590@qq.com> Date: Mon, 2 Mar 2015 17:31:04 +0800 Subject: [PATCH 44/68] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=AF=BE=E7=A8=8B?= =?UTF-8?q?=E4=B8=BB=E9=A1=B5=E6=B2=A1=E6=9C=892015=E5=B9=B4=E8=AF=BE?= =?UTF-8?q?=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/welcome/course.html.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/welcome/course.html.erb b/app/views/welcome/course.html.erb index d427a9aea..1d8a56060 100644 --- a/app/views/welcome/course.html.erb +++ b/app/views/welcome/course.html.erb @@ -96,11 +96,11 @@ year_now -= 1 course_term = "秋季学期" elsif month_now < 9 - course_term = "秋季学期" + course_term = "春季学期" end %> <%# (month_now >= 3 && month_now < 9) ? course_term = "春季学期" : course_term = "秋季学期" %> - <% @school_id.nil? ? cur_school_course = [] : cur_school_course = find_miracle_course(10,7,@school_id, year_now, course_term) %> + <% cur_school_course = @school_id.nil? ? [] : find_miracle_course(10,7,@school_id, year_now, course_term) %> <% if cur_school_course.count == 0 %> From 32264fb4a6fc5f09de1d9559718bcc5b66e4772c Mon Sep 17 00:00:00 2001 From: z9hang Date: Tue, 3 Mar 2015 09:11:47 +0800 Subject: [PATCH 45/68] =?UTF-8?q?=E8=AF=BE=E7=A8=8B=E5=8A=A8=E6=80=81?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E4=BF=AE=E6=94=B9=E4=B8=BA=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E6=89=80=E6=9C=89=E8=AF=BE=E7=A8=8B=E5=8A=A8=E6=80=81=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/mobile/apis/courses.rb | 2 +- app/services/courses_service.rb | 42 ++++++++++++++++++++++----------- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/app/api/mobile/apis/courses.rb b/app/api/mobile/apis/courses.rb index 8b167980d..6d68d549a 100644 --- a/app/api/mobile/apis/courses.rb +++ b/app/api/mobile/apis/courses.rb @@ -213,7 +213,7 @@ module Mobile present :status, 0 end - desc '课程动态' + desc '用户课程动态' params do requires :token, type: String end diff --git a/app/services/courses_service.rb b/app/services/courses_service.rb index 1f5ca7894..7e80f415c 100644 --- a/app/services/courses_service.rb +++ b/app/services/courses_service.rb @@ -333,24 +333,38 @@ class CoursesService end def course_dynamic(params,current_user) - course = Course.find(params[:id]) - if current_user.nil? || !(current_user.admin? || course.is_public == 1 || (course.is_public == 0 && current_user.member_of_course?(course))) - raise '403' + + @user = User.find(params[:id]) + if !current_user.admin? && !@user.active? + raise '404' + return end - count,is_teacher = get_course_anonymous_evaluation current_user,course - if is_teacher - student_commit_number = count + if current_user == @user || current_user.admin? + membership = @user.coursememberships.all else - need_anonymous_comments_count = count + membership = @user.coursememberships.all(:conditions => Course.visible_condition(current_user)) end - news_count = course.news.count - message_count = course.journals_for_messages.count + membership.sort! {|older, newer| newer.created_on <=> older.created_on } result = [] - result << {:course_name => course.name,:course_id => course.id,:course_img_url => url_to_avatar(course),:type => 1,:count => message_count} - result << {:course_name => course.name,:course_id => course.id,:course_img_url => url_to_avatar(course),:type => 2,:count => need_anonymous_comments_count} - result << {:course_name => course.name,:course_id => course.id,:course_img_url => url_to_avatar(course),:type => 3,:count => student_commit_number} - result << {:course_name => course.name,:course_id => course.id,:course_img_url => url_to_avatar(course),:type => 4,:count => news_count} - #{:course_name => course.name,:need_anonymous_comments_count=>need_anonymous_comments_count,:student_commit_number=>student_commit_number,:news_count=> news_count,:message_count=>message_count} + membership.each do |mp| + course = mp.course + unless current_user.nil? || !(current_user.admin? || course.is_public == 1 || (course.is_public == 0 && current_user.member_of_course?(course))) + count,is_teacher = get_course_anonymous_evaluation current_user,course + if is_teacher + student_commit_number = count + else + need_anonymous_comments_count = count + end + news_count = course.news.count + message_count = course.journals_for_messages.count + + result << {:course_name => course.name,:course_id => course.id,:course_img_url => url_to_avatar(course),:type => 1,:count => message_count} + result << {:course_name => course.name,:course_id => course.id,:course_img_url => url_to_avatar(course),:type => 2,:count => need_anonymous_comments_count} + result << {:course_name => course.name,:course_id => course.id,:course_img_url => url_to_avatar(course),:type => 3,:count => student_commit_number} + result << {:course_name => course.name,:course_id => course.id,:course_img_url => url_to_avatar(course),:type => 4,:count => news_count} + #{:course_name => course.name,:need_anonymous_comments_count=>need_anonymous_comments_count,:student_commit_number=>student_commit_number,:news_count=> news_count,:message_count=>message_count} + end + end result end From 0ba6c6d3bab62069b56882d288214a045282e577 Mon Sep 17 00:00:00 2001 From: alan <547533434@qq.com> Date: Tue, 3 Mar 2015 16:03:59 +0800 Subject: [PATCH 46/68] =?UTF-8?q?=E8=BF=98=E5=8E=9Fmemcached=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E7=9A=84=E6=96=87=E4=BB=B6[memcache=E5=AE=9E=E9=AA=8C?= =?UTF-8?q?=E8=BF=98=E6=B2=A1=E5=BC=84=E5=A5=BD]=20Signed-off-by:=20alan?= =?UTF-8?q?=20<547533434@qq.com>?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/environments/development.rb | 6 ++---- config/environments/production.rb | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/config/environments/development.rb b/config/environments/development.rb index 80d1c9db8..3b894d207 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -8,12 +8,10 @@ RedmineApp::Application.configure do # Log error messages when you accidentally call methods on nil. config.whiny_nils = true config.logger = Logger.new('log/development.log', 'daily') # daily, weekly or monthly - - # Show full error reports and disable caching config.consider_all_requests_local = true - config.action_controller.perform_caching = true - config.cache_store = :dalli_store + config.action_controller.perform_caching = false + config.cache_store = :file_store, "#{Rails.root }/public/tmp/" # Don't care if the mailer can't send config.action_mailer.raise_delivery_errors = true diff --git a/config/environments/production.rb b/config/environments/production.rb index 0b61ed132..48b2514cf 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -3,7 +3,7 @@ RedmineApp::Application.configure do # The production environment is meant for finished, "live" apps. # Code is not reloaded between requests config.cache_classes = true - config.cache_store = :dalli_store + ##### # Customize the default logger (http://ruby-doc.org/core/classes/Logger.html) # From 9233213127d6e00e01fafe8855372df50e23ab49 Mon Sep 17 00:00:00 2001 From: sw <939547590@qq.com> Date: Tue, 3 Mar 2015 17:35:46 +0800 Subject: [PATCH 47/68] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=9C=AA=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E6=97=B6=E8=BF=9B=E5=85=A5=E6=8C=87=E5=AE=9A=E5=AD=A6?= =?UTF-8?q?=E6=A0=A1=E8=AF=BE=E7=A8=8B=E5=88=97=E8=A1=A8=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E5=8D=B4=E6=98=AF=E5=9B=BD=E9=98=B2=E7=A7=91=E5=A4=A7=E8=AF=BE?= =?UTF-8?q?=E7=A8=8B=E5=88=97=E8=A1=A8=E7=9A=84BUG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/welcome_controller.rb | 2 +- app/views/welcome/course.html.erb | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/controllers/welcome_controller.rb b/app/controllers/welcome_controller.rb index 3ae5538e9..db2f287eb 100644 --- a/app/controllers/welcome_controller.rb +++ b/app/controllers/welcome_controller.rb @@ -84,7 +84,7 @@ class WelcomeController < ApplicationController def course @course_page = FirstPage.find_by_page_type('course') - @school_id = params[:school_id] || User.current.user_extensions.school.try(:id) + @school_id = params[:school_id] || User.current.user_extensions.school.try(:id) || 117 @logoLink ||= logolink() ##3-8月份为查找春季课程,9-2月份为查找秋季课程 diff --git a/app/views/welcome/course.html.erb b/app/views/welcome/course.html.erb index 1d8a56060..95837cb6d 100644 --- a/app/views/welcome/course.html.erb +++ b/app/views/welcome/course.html.erb @@ -108,12 +108,13 @@
      - <% if User.current.logged? %> + <%# if User.current.logged? %>
    • <%= render :partial => 'no_course_title', :locals => {:course_title => l(:label_school_no_course)} %>
    • - <% end %> - <% User.current.logged? ? course_count = 9 : course_count = 10 %> + <%# end %> + <%# User.current.logged? ? course_count = 9 : course_count = 10 %> + <% course_count = 9 %> <% all_new_hot_course = find_all_new_hot_course(course_count, @school_id, year_now, course_term)%> <% while all_new_hot_course.count < course_count%> <% if course_term == "春季学期" From 451a022d5fe2855a5aa6f974128e9ab379e5f8b4 Mon Sep 17 00:00:00 2001 From: z9hang Date: Tue, 3 Mar 2015 17:36:05 +0800 Subject: [PATCH 48/68] =?UTF-8?q?=E4=BD=9C=E4=B8=9A=E8=AF=A6=E6=83=85?= =?UTF-8?q?=E3=80=81=E4=BD=9C=E4=B8=9A=E5=88=97=E8=A1=A8=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/mobile/entities/course.rb | 2 +- app/api/mobile/entities/homework.rb | 12 +++++++++++- app/services/courses_service.rb | 5 +++-- app/services/homework_service.rb | 26 +++++++++++++++++++++----- 4 files changed, 36 insertions(+), 9 deletions(-) diff --git a/app/api/mobile/entities/course.rb b/app/api/mobile/entities/course.rb index 512aab69a..4c91738c1 100644 --- a/app/api/mobile/entities/course.rb +++ b/app/api/mobile/entities/course.rb @@ -53,7 +53,7 @@ module Mobile c[:course].teacher end end - expose :my_homework,using: Mobile::Entities::HomeworkAttach do |f, opt| + expose :my_homework,using: Mobile::Entities::Homework do |f, opt| f[:my_homework] if f.is_a?(Hash) && f.key?(:my_homework) end course_expose :current_user_is_member diff --git a/app/api/mobile/entities/homework.rb b/app/api/mobile/entities/homework.rb index 5b996c1af..55883e14e 100644 --- a/app/api/mobile/entities/homework.rb +++ b/app/api/mobile/entities/homework.rb @@ -1,10 +1,15 @@ module Mobile module Entities class Homework < Grape::Entity + include Redmine::I18n def self.homework_expose(field) expose field do |f,opt| if f.is_a?(Hash) && f.key?(field) - f[field] + if field == :created_on + format_time(f[field]) + else + f[field] + end elsif f.is_a?(::Bid) if f.respond_to?(field) f.send(field) @@ -18,6 +23,8 @@ module Mobile homework_expose :id #课程名称 homework_expose :course_name + + homework_expose :course_id #作业发布者 expose :author,using: Mobile::Entities::User do |f, opt| f[:author] @@ -42,6 +49,9 @@ module Mobile #只有作业启用了匿评功能且当前用户是课程老师且已提交的作品数量大于或等于2才能开启匿评 homework_expose :homework_state + homework_expose :created_on + homework_expose :deadline + expose :homework_for_anonymous_comments,using: Mobile::Entities::HomeworkAttach do |f, opt| f[:homework_for_anonymous_comments] if f.is_a?(Hash) && f.key?(:homework_for_anonymous_comments) end diff --git a/app/services/courses_service.rb b/app/services/courses_service.rb index 7e80f415c..22b7f8cd3 100644 --- a/app/services/courses_service.rb +++ b/app/services/courses_service.rb @@ -383,8 +383,9 @@ class CoursesService end #end open_anonymous_evaluation = bid.open_anonymous_evaluation - {:course_name => course.name,:id => bid.id, :author => bid.author,:author_real_name => author_real_name, :homework_times => many_times, :homework_name => name, :homework_count => homework_count,:student_questions_count => student_questions_count, - :description => description, :homework_state => state,:open_anonymous_evaluation => open_anonymous_evaluation,:homework_for_anonymous_comments => homework_for_anonymous_comments} + {:course_name => course.name,:course_id => course.id,:id => bid.id, :author => bid.author,:author_real_name => author_real_name, :homework_times => many_times, :homework_name => name, :homework_count => homework_count,:student_questions_count => student_questions_count, + :description => description, :homework_state => state,:open_anonymous_evaluation => open_anonymous_evaluation,:homework_for_anonymous_comments => homework_for_anonymous_comments,:created_on => bid.created_on,:deadline => bid.deadline} + end #显示作业列表的同时显示分配给当前学生匿评的作业 diff --git a/app/services/homework_service.rb b/app/services/homework_service.rb index c5a333125..6287eeef5 100644 --- a/app/services/homework_service.rb +++ b/app/services/homework_service.rb @@ -26,8 +26,8 @@ class HomeworkService state = @bid.comment_status #end open_anonymous_evaluation = @bid.open_anonymous_evaluation - {:course_name => course.name,:id => @bid.id, :author => @bid.author,:author_real_name =>author, :homework_times => many_times, :homework_name => name, :homework_count => homework_count,:student_questions_count => student_questions_count, - :description => description, :homework_state => state,:open_anonymous_evaluation => open_anonymous_evaluation} + {:course_name => course.name,:course_id => course.id,:id => @bid.id, :author => @bid.author,:author_real_name =>author, :homework_times => many_times, :homework_name => name, :homework_count => homework_count,:student_questions_count => student_questions_count, + :description => description, :homework_state => state,:open_anonymous_evaluation => open_anonymous_evaluation,:created_on => @bid.created_on,:deadline => @bid.deadline} end # 启动作业匿评前提示信息 @@ -274,10 +274,26 @@ class HomeworkService membership.each do |mp| my_homeworks = [] mp.course.homeworks.each do |bid| - hw = bid.homeworks.where("user_id = #{current_user.id}") - my_homeworks << hw[0] unless (hw.nil? || hw[0].nil?) + #hw = bid.homeworks.where("user_id = #{current_user.id}") + @bid = bid + course = @bid.courses.first + author = @bid.author.lastname + @bid.author.firstname + many_times = course.homeworks.index(@bid) + 1 + name = @bid.name + homework_count = @bid.homeworks.count #已提交的作业数量 + student_questions_count = @bid.commit.nil? ? 0 : @bid.commit + description = @bid.description + #if is_course_teacher(User.current, course) && @bid.open_anonymous_evaluation == 1 && @bid.homeworks.count >= 2 + state = @bid.comment_status + #end + open_anonymous_evaluation = @bid.open_anonymous_evaluation + + my_homeworks << {:course_name => course.name,:id => @bid.id, :author => @bid.author,:author_real_name =>author, :homework_times => many_times, :homework_name => name, :homework_count => homework_count,:student_questions_count => student_questions_count, + :description => description, :homework_state => state,:open_anonymous_evaluation => open_anonymous_evaluation} + end + if mp.course.homeworks.count != 0 + course_list << {:course => mp.course,:img_url => url_to_avatar(mp.course),:my_homework => my_homeworks,:current_user_is_member => current_user.member_of_course?(mp.course),:current_user_is_teacher => is_course_teacher(current_user,mp.course)} end - course_list << {:course => mp.course,:img_url => url_to_avatar(mp.course),:my_homework => my_homeworks,:current_user_is_member => current_user.member_of_course?(mp.course),:current_user_is_teacher => is_course_teacher(current_user,mp.course)} end course_list end From 2c4e86408058c3634234aabfd21c6a2bf601fa23 Mon Sep 17 00:00:00 2001 From: sw <939547590@qq.com> Date: Tue, 3 Mar 2015 17:53:15 +0800 Subject: [PATCH 49/68] =?UTF-8?q?#1949=20=E5=AD=A6=E7=94=9F=E5=8F=AF?= =?UTF-8?q?=E4=BB=A5=E9=80=9A=E8=BF=87url=E8=AE=BF=E9=97=AE=E8=80=81?= =?UTF-8?q?=E5=B8=88=E5=B9=B6=E6=9C=AA=E5=8F=91=E5=B8=83=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E5=8D=B7=E5=B9=B6=E8=BF=9B=E8=A1=8C=E4=BD=9C=E7=AD=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/poll_controller.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/controllers/poll_controller.rb b/app/controllers/poll_controller.rb index 1b2794034..b897d039a 100644 --- a/app/controllers/poll_controller.rb +++ b/app/controllers/poll_controller.rb @@ -23,6 +23,10 @@ class PollController < ApplicationController def show @poll = Poll.find params[:id] + if @poll.polls_status != 2 && !User.current.allowed_to?(:as_teacher,@course) + render_403 + return + end #已提交问卷的用户不能再访问该界面 if has_commit_poll?(@poll.id,User.current.id) && (!User.current.admin?) render_403 From 5bba54432d47b9d89cf968800255e75615630365 Mon Sep 17 00:00:00 2001 From: sw <939547590@qq.com> Date: Wed, 4 Mar 2015 09:52:01 +0800 Subject: [PATCH 50/68] =?UTF-8?q?#1930=20=E9=A1=B9=E7=9B=AE=E7=BC=BA?= =?UTF-8?q?=E9=99=B7=E6=98=BE=E7=A4=BA=E4=B8=AA=E6=95=B0=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/layouts/base_projects.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/base_projects.html.erb b/app/views/layouts/base_projects.html.erb index b8cc2ddc9..d33a33a14 100644 --- a/app/views/layouts/base_projects.html.erb +++ b/app/views/layouts/base_projects.html.erb @@ -134,7 +134,7 @@ - <%=link_to "#{@project.issues.where('status_id in (1,2,4,6)').count}", project_issues_path(@project) %> + <%=link_to "#{@project.issues.where('status_id in (1,2,4,6)').count}/#{@project.issues.count}", project_issues_path(@project) %> From 33e9a3e326def600d594c81ee0824a2bd20d4ef7 Mon Sep 17 00:00:00 2001 From: sw <939547590@qq.com> Date: Wed, 4 Mar 2015 10:02:40 +0800 Subject: [PATCH 51/68] =?UTF-8?q?#1958=20=E5=85=B3=E6=B3=A8=E6=9C=89?= =?UTF-8?q?=E7=94=A8=E6=88=B70=E4=B8=AA=E5=90=8E=E8=BE=B9=E5=8F=96?= =?UTF-8?q?=E6=B6=88=E5=86=92=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/users/user_watchlist.html.erb | 100 ++++++++++++++---------- config/locales/zh.yml | 12 +-- 2 files changed, 64 insertions(+), 48 deletions(-) diff --git a/app/views/users/user_watchlist.html.erb b/app/views/users/user_watchlist.html.erb index e58e35d27..b6e889b91 100644 --- a/app/views/users/user_watchlist.html.erb +++ b/app/views/users/user_watchlist.html.erb @@ -1,47 +1,63 @@

      <%= l(:label_user_watcher)%>

      <% for user in User.watched_by(@user.id) %> -
      • - - - - - -
        <%= link_to image_tag(url_to_avatar(user), :class => "avatar"), user_path(user), :title => "#{user.name}" %> - - - - - - - - - - - -
        <%= content_tag "div", link_to(user.name,user_path(user)), :class => "project_avatar_name" %> -

        - <%# unless user.memberships.empty? %> - <% cond = Project.visible_condition(User.current) + " AND projects.project_type <> 1" %> - <% memberships = user.memberships.all(:conditions => cond) %> - <%= l(:label_x_contribute_to, :count => memberships.count) %> - <% for member in memberships %> - <%= link_to_project(member.project) %><%= (user.memberships.last == member) ? '' : ',' %> - <% end %> - <%# end %> -

        -

        - <%# unless user.memberships.empty? %> - <% user_courses = user_courses_list(user) %> - <%= l(:label_x_course_contribute_to, :count => user_courses.count) %> - <% for course in user_courses %> - <%# if course.name != nil %> - <%= link_to course.name,{:controller => 'courses',:action => 'show',id:course.id, host: Setting.course_domain} %><%= (user_courses.last == course) ? '' : ',' %> - <%# end %> - <% end %> - <%# end %> -

        -
        <%= l(:label_user_joinin) %><%= format_date(user.created_on) %> -
      +
        +
      • + + + + + +
        + <%= link_to image_tag(url_to_avatar(user), :class => "avatar"), user_path(user), :title => "#{user.name}" %> + + + + + + + + + + + + + +
        + + <%= content_tag "div", link_to(user.name,user_path(user)), :class => "project_avatar_name" %> + +
        +

        + <%# unless user.memberships.empty? %> + <% cond = Project.visible_condition(User.current) + " AND projects.project_type <> 1" %> + <% memberships = user.memberships.all(:conditions => cond) %> + <%= l(:label_x_contribute_to, :count => memberships.count) %> + <%= ":" unless memberships.empty? %> + <% for member in memberships %> + <%= link_to_project(member.project) %> + <%= (user.memberships.last == member) ? '' : ',' %> + <% end %> + <%# end %> +

        +

        + <%# unless user.memberships.empty? %> + <% user_courses = user_courses_list(user) %> + <%= l(:label_x_course_contribute_to, :count => user_courses.count) %> + <%= ":" unless user_courses.empty? %> + <% for course in user_courses %> + <%# if course.name != nil %> + <%= link_to course.name,{:controller => 'courses',:action => 'show',id:course.id, host: Setting.course_domain} %><%= (user_courses.last == course) ? '' : ',' %> + <%# end %> + <% end %> + <%# end %> +

        +
        + <%= l(:label_user_joinin) %> + <%= format_date(user.created_on) %> +
        +
        +
      • +
      <% end %>
      \ No newline at end of file diff --git a/config/locales/zh.yml b/config/locales/zh.yml index 9ef20756f..aa153cda6 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -1420,9 +1420,9 @@ zh: label_contribute_to: 参与了 %{project_count} 个项目: #modify by men label_x_contribute_to: - zero: 参与了 %{count} 个项目: - one: 参与了 %{count} 个项目: - other: 参与了 %{count} 个项目: + zero: 参与了 %{count} 个项目 + one: 参与了 %{count} 个项目 + other: 参与了 %{count} 个项目 #end label_total_commit: 共%{total_commit}次提交 label_question_number: 第%{question_number}题: @@ -1809,9 +1809,9 @@ zh: label_course_contribute_to: 参与了 %{project_count} 个项目: label_x_course_contribute_to: - zero: "参与了 %{count} 个课程:" - one: "参与了 %{count} 个课程:" - other: "参与了 %{count} 个课程:" + zero: "参与了 %{count} 个课程" + one: "参与了 %{count} 个课程" + other: "参与了 %{count} 个课程" label_join_contest: 加入竞赛 label_exit_contest: 退出竞赛 From 032e28067ddefe3f2bc3e9f8744c06d2214d3e4f Mon Sep 17 00:00:00 2001 From: sw <939547590@qq.com> Date: Wed, 4 Mar 2015 11:34:01 +0800 Subject: [PATCH 52/68] =?UTF-8?q?#1959=20=E9=A1=B9=E7=9B=AE=E8=AF=84?= =?UTF-8?q?=E5=88=86=E6=98=AF0=E7=9A=84=EF=BC=8C=E5=B0=B1=E4=B8=8D?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E4=BA=86=EF=BC=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/layouts/base_projects.html.erb | 30 ++++++++++++------------ 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/app/views/layouts/base_projects.html.erb b/app/views/layouts/base_projects.html.erb index d33a33a14..fc6713a94 100644 --- a/app/views/layouts/base_projects.html.erb +++ b/app/views/layouts/base_projects.html.erb @@ -96,21 +96,21 @@ <%= image_tag(url_to_avatar(@project), :class => 'avatar2') %> -
      - <%= link_to @project.name, project_path(@project)%> -
      -
      - - <% if @project.project_type == 0 %> - <%= l(:label_project_grade)%> : - <%= link_to(format("%.2f" ,project_scores(@project) ).to_i, {:controller => 'projects', - :action => 'show_projects_score', - :remote => true, - :id => @project.id - }, :style => "color: #EC6300;")%> - <% end %> - -
      + <% project_score = format("%.2f" ,project_scores(@project)).to_i%> +
      + <%= link_to @project.name, project_path(@project)%> +
      +
      + <% if @project.project_type == 0 && project_score != 0 %> + <%= l(:label_project_grade)%> : + <%= link_to(project_score, {:controller => 'projects', + :action => 'show_projects_score', + :remote => true, + :id => @project.id + }, :style => "color: #EC6300;")%> + <% end %> + +
      <%= render 'layouts/join_exit_project' %>
      From 7ee58d1d4419bb67757b05439f62c1f0d12bf81f Mon Sep 17 00:00:00 2001 From: lizanle <491823689@qq.com> Date: Wed, 4 Mar 2015 15:32:00 +0800 Subject: [PATCH 53/68] =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E5=8A=A8=E6=80=81?= =?UTF-8?q?=E9=87=8D=E5=86=99=EF=BC=8C=E5=B9=B6=E5=8E=BB=E6=8E=89=E9=A6=96?= =?UTF-8?q?=E9=A1=B5=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/attachments_controller.rb | 4 + app/controllers/issues_controller.rb | 2 + app/controllers/projects_controller.rb | 22 +- app/controllers/users_controller.rb | 16 +- app/helpers/expire_helper.rb | 16 - app/models/attachment.rb | 16 +- app/models/bid.rb | 7 - app/models/changeset.rb | 6 +- app/models/contest_notification.rb | 4 - app/models/document.rb | 17 +- app/models/forge_activity.rb | 22 + app/models/forum.rb | 5 - app/models/issue.rb | 24 +- app/models/journal.rb | 13 +- app/models/journals_for_message.rb | 5 +- app/models/memo.rb | 10 +- app/models/message.rb | 18 +- app/models/news.rb | 17 +- app/models/project.rb | 6 +- app/models/tracker.rb | 4 +- app/views/attachments/_form.html.erb | 3 +- app/views/files/_new.html.erb | 2 +- app/views/issues/show.html.erb | 5 +- app/views/kaminari/_paginator.html.erb | 2 +- app/views/projects/show.html.erb | 257 ++++-- app/views/welcome/index.html.erb | 10 - .../20150206023634_create_forge_activities.rb | 13 + ...2_import_issue_data_to_forge_activities.rb | 46 + ...36_add_journal_data_to_forge_activities.rb | 51 ++ ...import_message_data_to_forge_activities.rb | 41 + ...13_import_news_data_to_forge_activities.rb | 41 + ...dd_column_project_issue_index_to_issues.rb | 6 + ..._data_to_project_issues_indexs_in_issue.rb | 10 + ...mport_document_data_to_forge_activities.rb | 37 + ...rt_attachments_data_to_forge_activities.rb | 43 + db/schema.rb | 53 +- public/javascripts/application.js | 3 + public/javascripts/jquery.infinitescroll.js | 814 ++++++++++++++++++ public/javascripts/jquery.min.js | 2 + spec/models/forge_activity_spec.rb | 5 + 40 files changed, 1460 insertions(+), 218 deletions(-) delete mode 100644 app/helpers/expire_helper.rb create mode 100644 app/models/forge_activity.rb create mode 100644 db/migrate/20150206023634_create_forge_activities.rb create mode 100644 db/migrate/20150206060632_import_issue_data_to_forge_activities.rb create mode 100644 db/migrate/20150210062236_add_journal_data_to_forge_activities.rb create mode 100644 db/migrate/20150227061944_import_message_data_to_forge_activities.rb create mode 100644 db/migrate/20150227065713_import_news_data_to_forge_activities.rb create mode 100644 db/migrate/20150227083257_add_column_project_issue_index_to_issues.rb create mode 100644 db/migrate/20150227085333_add_data_to_project_issues_indexs_in_issue.rb create mode 100644 db/migrate/20150302061232_import_document_data_to_forge_activities.rb create mode 100644 db/migrate/20150302091345_import_attachments_data_to_forge_activities.rb create mode 100644 public/javascripts/jquery.infinitescroll.js create mode 100644 public/javascripts/jquery.min.js create mode 100644 spec/models/forge_activity_spec.rb diff --git a/app/controllers/attachments_controller.rb b/app/controllers/attachments_controller.rb index 2c6a002b4..252d823e9 100644 --- a/app/controllers/attachments_controller.rb +++ b/app/controllers/attachments_controller.rb @@ -186,6 +186,10 @@ class AttachmentsController < ApplicationController @attachment = Attachment.new(:file => request.raw_post) @attachment.author = User.current + if !params[:project].nil? + @attachment.container_type = 'Project' + @attachment.container_id = params[:project].split("?")[0] + end @attachment.filename = params[:filename].presence || Redmine::Utils.random_hex(16) saved = @attachment.save diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index c0d83fdcd..5dc945cc7 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -153,6 +153,8 @@ class IssuesController < ApplicationController def create call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue }) @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads])) + # 给该issue在它所在的项目中所有的issues中所在的位置给一个序号 + @issue.project_issues_index = @issue.project.issues.last.project_issues_index + 1 if @issue.save call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue}) respond_to do |format| diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 64706887b..9a9537e6d 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -247,6 +247,7 @@ class ProjectsController < ApplicationController # Author lizanle # Description 项目动态展示方法,删除了不必要的代码 def show +=begin # 试图跳转到请求的按钮 if params[:jump] && redirect_to_project_menu_item(@project, params[:jump]) return @@ -270,30 +271,37 @@ class ProjectsController < ApplicationController # 时间跨度不能太大,不然很慢,所以删掉了-1.years @date_from = @date_to - @days @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1') +=end + @author = params[:user_id].blank? ? nil : User.active.find(params[:user_id]) # 决定显示所用用户或单个用户活动 +=begin @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project, :with_subprojects => @with_subprojects, :author => @author) @activity.scope_select {|t| !has["show_#{t}"].nil?} +=end # 根据私密性,取出符合条件的所有数据 if User.current.member_of?(@project) || User.current.admin? - events = @activity.events(@date_from, @date_to) + @events_pages = ForgeActivity.where("project_id = ?",@project).order("created_at desc").page(params['page'|| 1]).per(20); + #events = @activity.events(@date_from, @date_to) else - events = @activity.events(@date_from, @date_to, :is_public => 1) + @events_pages = ForgeActivity.includes(:project).where("forge_activities.project_id = ? and projects.is_public + = ?",@project,1).order("created_at desc") + .page(params['page'|| 1]).per(10); + # @events = @activity.events(@date_from, @date_to, :is_public => 1) end - @offset, @limit = api_offset_and_limit({:limit => 10}) - @events_count = events.count - @events_pages = Paginator.new @events_count, @limit, params['page'] - @offset ||= @events_pages.offset +=begin + @events_pages = Paginator.new events.count, 10, params['page'] # 总的数据中取出某一页 - events = events.slice(@offset,@limit) + events = events.slice(@events_pages.offset,10) # 按天分组 @events_by_day = events.group_by {|event| User.current.time_to_date(event.event_datetime)} +=end # 根据对应的请求,返回对应的数据 respond_to do |format| diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index a2933c577..526ae227d 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -408,6 +408,9 @@ class UsersController < ApplicationController def show pre_count = 10 #limit + # Time 2015-02-04 11:46:34 + # Author lizanle + # Description type 1 :所有动态包括我关注的人 type 2:我的动态 type 3:关于我的回复 case params[:type] when "1" if @user == User.current @@ -428,26 +431,27 @@ class UsersController < ApplicationController messages = message.sort {|x,y| y.created_on <=> x.created_on } @message = messages[@info_pages.offset, @info_pages.per_page] @state = 2 - else + else + # Time 2015-02-04 10:50:49 + # Author lizanle + # Description 所有动态 where_condition = nil; # where_condition = "act_type <> 'JournalsForMessage'" if @user == User.current watcher = User.watched_by(@user) watcher.push(User.current) - activity = Activity.where(where_condition).where('user_id in (?)', watcher).order('id desc') + activity = Activity.where(where_condition).where('user_id in (?)', watcher).order('id desc') else activity = Activity.where(where_condition).where('user_id = ?', @user.id).order('id desc') end - @activity_count = activity.count @activity_pages = Paginator.new @activity_count, pre_count, params['page'] - activity_page = activity.slice(@activity_pages.offset,@activity_pages.per_page * 2) - activity_page = activity_page.reject { |e| + @activity = activity.slice(@activity_pages.offset,@activity_pages.per_page ) + @activity = @activity.reject { |e| ((e.act_type=="Issue") && ( !e.act.visible?(User.current))) || ((e.act_type == "Journal") && (!e.act.project.visible?(User.current))) || ((e.act_type == "Bid") && ((!User.current.member_of_course?(e.act.courses.first) || !User.current.admin?))) } - @activity = activity.slice(0,@activity_pages.per_page) @state = 0 end diff --git a/app/helpers/expire_helper.rb b/app/helpers/expire_helper.rb deleted file mode 100644 index 0a9cab69c..000000000 --- a/app/helpers/expire_helper.rb +++ /dev/null @@ -1,16 +0,0 @@ -module ExpireHelper - #index.html 中 “projects”塊 緩存過期 - def expire_project_cache - ActionController::Base.new.expire_fragment('projects') - end - - #index.html 中 “activities”塊 緩存過期 - def expire_activitie_cache - ActionController::Base.new.expire_fragment('activities') - end - - #welcome/index.html 中 “forums”塊 緩存過期 - def expire_forum_cache - ActionController::Base.new.expire_fragment('forums') - end -end diff --git a/app/models/attachment.rb b/app/models/attachment.rb index de7912667..7ac7d786d 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -25,7 +25,9 @@ class Attachment < ActiveRecord::Base belongs_to :softapplication, foreign_key: 'container_id', conditions: "attachments.container_type = 'Softapplication'" belongs_to :author, :class_name => "User", :foreign_key => "author_id" belongs_to :attachmentstype, :foreign_key => "attachtype",:primary_key => "id" - + # 被ForgeActivity虚拟关联 + has_many :forge_acts, :class_name => 'ForgeActivity',:as =>:forge_act ,:dependent => :destroy + # end include UserScoreHelper validates :filename, presence: true, length: {maximum: 254} @@ -70,7 +72,7 @@ class Attachment < ActiveRecord::Base @@thumbnails_storage_path = File.join(Rails.root, "tmp", "thumbnails") before_save :files_to_final_location - after_create :be_user_score # user_score + after_create :be_user_score ,:act_as_forge_activity# user_score after_update :be_user_score after_destroy :delete_from_disk,:down_user_score @@ -529,4 +531,14 @@ class Attachment < ActiveRecord::Base end end + # Time 2015-03-02 17:42:48 + # Author lizanle + # Description 上传该项目的文档资料也要保存一份在公共表中 + def act_as_forge_activity + if self.container_type == 'Project' + self.forge_acts << ForgeActivity.new(:user_id => self.author_id, + :project_id => self.container_id) + end + end + end diff --git a/app/models/bid.rb b/app/models/bid.rb index f423266b8..0fc46d24d 100644 --- a/app/models/bid.rb +++ b/app/models/bid.rb @@ -17,7 +17,6 @@ class Bid < ActiveRecord::Base HomeworkProject = 2 attr_accessible :author_id, :budget, :deadline, :name, :description, :homework_type, :password include Redmine::SafeAttributes - include ExpireHelper belongs_to :author, :class_name => 'User', :foreign_key => :author_id belongs_to :course @@ -33,12 +32,6 @@ class Bid < ActiveRecord::Base has_many :join_in_contests, :dependent => :destroy has_many :praise_tread, as: :praise_tread_object, dependent: :destroy # has_many :fork_homework, :class_name => 'Bid', :conditions => "#{Bid.table_name}.parent_id = #{id}" - - - after_create :expire_activitie_cache - after_update :expire_activitie_cache - before_destroy :expire_activitie_cache - acts_as_attachable NAME_LENGTH_LIMIT = 60 diff --git a/app/models/changeset.rb b/app/models/changeset.rb index 101647824..e05a7d2da 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class Changeset < ActiveRecord::Base - include ExpireHelper belongs_to :repository belongs_to :user include UserScoreHelper @@ -65,9 +64,8 @@ class Changeset < ActiveRecord::Base includes(:repository => :project).where(Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args)) } - after_create :scan_for_issues,:refresh_changests,:expire_activitie_cache#:be_user_score # user_score - after_update :be_user_score,:expire_activitie_cache - before_destroy :expire_activitie_cache + after_create :scan_for_issues,:refresh_changests#:be_user_score # user_score + after_update :be_user_score after_destroy :down_user_score before_create :before_create_cs diff --git a/app/models/contest_notification.rb b/app/models/contest_notification.rb index 0ccd0d5a7..71c448f65 100644 --- a/app/models/contest_notification.rb +++ b/app/models/contest_notification.rb @@ -1,10 +1,6 @@ class ContestNotification < ActiveRecord::Base - include ExpireHelper attr_accessible :content, :title validates :title, length: {maximum: 30} - after_create :expire_forum_cache - after_update :expire_forum_cache - before_destroy :expire_forum_cache end diff --git a/app/models/document.rb b/app/models/document.rb index 48a0151eb..33ffdaa2f 100644 --- a/app/models/document.rb +++ b/app/models/document.rb @@ -17,18 +17,16 @@ class Document < ActiveRecord::Base include Redmine::SafeAttributes - include ExpireHelper belongs_to :project belongs_to :user belongs_to :category, :class_name => "DocumentCategory", :foreign_key => "category_id" include UserScoreHelper after_save :be_user_score # user_score after_destroy :down_user_score - after_create :expire_activitie_cache - after_update :expire_activitie_cache - before_destroy :expire_activitie_cache acts_as_attachable :delete_permission => :delete_documents - + # 被ForgeActivity虚拟关联 + has_many :forge_acts, :class_name => 'ForgeActivity',:as =>:forge_act ,:dependent => :destroy + # end acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"}, #:author => Proc.new {|o| o.attachments.reorder("#{Attachment.table_name}.created_on ASC").first.try(:author) }, @@ -39,7 +37,7 @@ class Document < ActiveRecord::Base validates_presence_of :project, :title, :category validates_length_of :title, :maximum => 60 - + after_create :act_as_forge_activity scope :visible, lambda {|*args| includes(:project).where(Project.allowed_to_condition(args.shift || User.current, :view_documents, *args)) } @@ -81,5 +79,12 @@ class Document < ActiveRecord::Base update_document(self.user,2,self.project) end + # Time 2015-03-02 10:51:16 + # Author lizanle + # Description 新创建的document要在公共表ForgeActivity中记录 + def act_as_forge_activity + self.forge_acts << ForgeActivity.new(:user_id => self.user_id, + :project_id => self.project_id) + end end diff --git a/app/models/forge_activity.rb b/app/models/forge_activity.rb new file mode 100644 index 000000000..e94a29867 --- /dev/null +++ b/app/models/forge_activity.rb @@ -0,0 +1,22 @@ +# Time 2015-02-06 10:42:34 +# Author lizanle +# Description 这是保存Project相关的动态的公共表 +class ForgeActivity < ActiveRecord::Base + # 公共表中活动类型,命名规则:TYPE_OF_{类名}_ACT + TYPE_OF_ISSUE_ACT = "Issue" + TYPE_OF_MESSAGE_ACT = "Message" + TYPE_OF_ATTACHMENT_ACT = "Attachment" + TYPE_OF_DOCUMENT_ACT = "Document" + TYPE_OF_JOURNAL_ACT = "Journal" + TYPE_OF_WIKI_ACT = "Wiki" + TYPE_OF_NEWS_ACT = "News" + attr_accessible :forge_act_id, :forge_act_type,:project_id,:user_id,:org_id + # 虚拟关联 + belongs_to :forge_act ,:polymorphic => true + belongs_to :project + belongs_to :user + validates :user_id,presence: true + validates :project_id,presence: true + validates :forge_act_id,presence: true + validates :forge_act_type, presence: true +end diff --git a/app/models/forum.rb b/app/models/forum.rb index e47d18b02..6f834702b 100644 --- a/app/models/forum.rb +++ b/app/models/forum.rb @@ -1,13 +1,8 @@ class Forum < ActiveRecord::Base include Redmine::SafeAttributes - include ExpireHelper has_many :topics, :class_name => 'Memo', :conditions => "#{Memo.table_name}.parent_id IS NULL", :order => "#{Memo.table_name}.created_at DESC", :dependent => :destroy has_many :memos, :dependent => :destroy, conditions: "parent_id IS NULL" belongs_to :creator, :class_name => "User", :foreign_key => 'creator_id' - - after_create :expire_forum_cache - after_update :expire_forum_cache - before_destroy :expire_forum_cache safe_attributes 'name', 'description', 'topic_count', diff --git a/app/models/issue.rb b/app/models/issue.rb index 78d20dca1..c2670a0cc 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -19,7 +19,6 @@ class Issue < ActiveRecord::Base include Redmine::SafeAttributes include Redmine::Utils::DateCalculation include UserScoreHelper - include ExpireHelper belongs_to :project belongs_to :tracker belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id' @@ -46,6 +45,8 @@ class Issue < ActiveRecord::Base # added by fq has_many :acts, :class_name => 'Activity', :as => :act, :dependent => :destroy + # 被ForgeActivity虚拟关联 + has_many :forge_acts, :class_name => 'ForgeActivity',:as =>:forge_act ,:dependent => :destroy # end has_many :praise_tread, as: :praise_tread_object, dependent: :destroy @@ -79,12 +80,9 @@ class Issue < ActiveRecord::Base attr_reader :current_journal # fq - after_create :act_as_activity,:be_user_score_new_issue + after_create :act_as_activity,:be_user_score_new_issue,:act_as_forge_activity after_update :be_user_score after_destroy :down_user_score - after_create :expire_activitie_cache - after_update :expire_activitie_cache - before_destroy :expire_activitie_cache # after_create :be_user_score # end @@ -131,7 +129,15 @@ class Issue < ActiveRecord::Base self.acts << Activity.new(:user_id => self.author_id) end # end - + + # Time 2015-02-26 10:51:16 + # Author lizanle + # Description 新创建的issue要在公共表ForgeActivity中记录 + def act_as_forge_activity + self.forge_acts << ForgeActivity.new(:user_id => self.author_id, + :project_id => self.project_id) + end + # end # Returns a SQL conditions string used to find all issues visible by the specified user @@ -1170,13 +1176,13 @@ class Issue < ActiveRecord::Base # back string obj which is belong to project. def source_from - "" << self.project.name.to_s << - "#" << project_index + "" << self.project.name.to_s end def project_index if self.project.issues.include?(self) (self.project.issues.index(self).to_i + 1).to_s + else issue_index = 1 self.project.issues.each do |issue| @@ -1187,8 +1193,10 @@ class Issue < ActiveRecord::Base issue_index = issue_index+1 end end + issue_index.to_s end + end private diff --git a/app/models/journal.rb b/app/models/journal.rb index a386c2b80..3b660132e 100644 --- a/app/models/journal.rb +++ b/app/models/journal.rb @@ -27,6 +27,8 @@ class Journal < ActiveRecord::Base # added by fq has_one :journal_reply has_many :acts, :class_name => 'Activity', :as => :act, :dependent => :destroy + # 被ForgeActivity虚拟关联 + has_many :forge_acts, :class_name => 'ForgeActivity',:as =>:forge_act ,:dependent => :destroy # end attr_accessor :indice @@ -46,7 +48,7 @@ class Journal < ActiveRecord::Base before_create :split_private_notes # fq - after_save :act_as_activity,:be_user_score + after_save :act_as_activity,:be_user_score,:act_as_forge_activity # end #after_destroy :down_user_score #before_save :be_user_score @@ -156,6 +158,15 @@ class Journal < ActiveRecord::Base end # end + # Time 2015-02-27 13:30:19 + # Author lizanle + # Description 公共表中需要保存一份该记录 + def act_as_forge_activity + self.forge_acts << ForgeActivity.new(:user_id => self.user_id, + :project_id => self.issue.project.id) + + end + # 更新用户分数 -by zjc def be_user_score #新建了缺陷留言且留言不为空,不为空白 diff --git a/app/models/journals_for_message.rb b/app/models/journals_for_message.rb index c71fbaf47..4af9ec2f7 100644 --- a/app/models/journals_for_message.rb +++ b/app/models/journals_for_message.rb @@ -4,7 +4,6 @@ class JournalsForMessage < ActiveRecord::Base include Redmine::SafeAttributes include UserScoreHelper - include ExpireHelper safe_attributes "jour_type", # 留言所属类型 "jour_id", # 留言所属类型的id "notes", # 留言内容 @@ -55,9 +54,7 @@ class JournalsForMessage < ActiveRecord::Base has_many :acts, :class_name => 'Activity', :as => :act, :dependent => :destroy validates :notes, presence: true - after_create :act_as_activity ,:expire_activitie_cache#huang - after_update :expire_activitie_cache - before_destroy :expire_activitie_cache + after_create :act_as_activity #huang after_create :reset_counters! after_destroy :reset_counters! after_save :be_user_score diff --git a/app/models/memo.rb b/app/models/memo.rb index 165f8e144..4899a655a 100644 --- a/app/models/memo.rb +++ b/app/models/memo.rb @@ -1,13 +1,8 @@ class Memo < ActiveRecord::Base include Redmine::SafeAttributes include UserScoreHelper - include ExpireHelper belongs_to :forum belongs_to :author, :class_name => "User", :foreign_key => 'author_id' - - after_create :expire_cache - after_update :expire_cache - before_destroy :expire_cache validates_presence_of :author_id, :forum_id, :subject,:content # 若是主题帖,则内容可以是空 #validates :content, presence: true, if: Proc.new{|o| !o.parent_id.nil? } @@ -174,8 +169,5 @@ class Memo < ActiveRecord::Base update_memo_number(User.current,1) update_replay_for_memo(User.current,1) end - def expire_cache - expire_forum_cache - expire_activitie_cache - end + end diff --git a/app/models/message.rb b/app/models/message.rb index 9ce4d583a..413a4f91c 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -18,7 +18,6 @@ class Message < ActiveRecord::Base include Redmine::SafeAttributes include UserScoreHelper - include ExpireHelper belongs_to :board belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' has_many :praise_tread, as: :praise_tread_object, dependent: :destroy @@ -29,6 +28,8 @@ class Message < ActiveRecord::Base # added by fq has_many :acts, :class_name => 'Activity', :as => :act, :dependent => :destroy + # 被ForgeActivity虚拟关联 + has_many :forge_acts, :class_name => 'ForgeActivity',:as =>:forge_act ,:dependent => :destroy # end acts_as_searchable :columns => ['subject', 'content'], @@ -59,13 +60,12 @@ class Message < ActiveRecord::Base validates_length_of :subject, :maximum => 255 validate :cannot_reply_to_locked_topic, :on => :create - after_create :add_author_as_watcher, :reset_counters!,:expire_activitie_cache - after_update :update_messages_board,:expire_activitie_cache - before_destroy :expire_activitie_cache + after_create :add_author_as_watcher, :reset_counters! + after_update :update_messages_board after_destroy :reset_counters!,:down_user_score # fq - after_create :act_as_activity,:be_user_score + after_create :act_as_activity,:be_user_score,:act_as_forge_activity #before_save :be_user_score # end @@ -160,6 +160,14 @@ class Message < ActiveRecord::Base end # end + # Time 2015-02-27 14:32:25 + # Author lizanle + # Description + def act_as_forge_activity + self.forge_acts << ForgeActivity.new(:user_id => self.author_id, + :project_id => self.board.project.id) + end + #更新用户分数 -by zjc def be_user_score #新建message且无parent的为发帖 diff --git a/app/models/news.rb b/app/models/news.rb index 9c37719f3..fa9f31712 100644 --- a/app/models/news.rb +++ b/app/models/news.rb @@ -17,7 +17,6 @@ class News < ActiveRecord::Base include Redmine::SafeAttributes - include ExpireHelper belongs_to :project #added by nwb belongs_to :course @@ -25,7 +24,9 @@ class News < ActiveRecord::Base has_many :comments, :as => :commented, :dependent => :delete_all, :order => "created_on" # fq has_many :acts, :class_name => 'Activity', :as => :act, :dependent => :destroy - #end + # 被ForgeActivity虚拟关联 + has_many :forge_acts, :class_name => 'ForgeActivity',:as =>:forge_act ,:dependent => :destroy + # end validates_presence_of :title, :description validates_length_of :title, :maximum => 60 @@ -46,11 +47,8 @@ class News < ActiveRecord::Base after_create :add_author_as_watcher # fq - after_create :act_as_activity + after_create :act_as_activity,:act_as_forge_activity # end - after_create :expire_activitie_cache - after_update :expire_activitie_cache - before_destroy :expire_activitie_cache scope :visible, lambda {|*args| includes(:project).where(Project.allowed_to_condition(args.shift || User.current, :view_news, *args)) @@ -96,5 +94,12 @@ class News < ActiveRecord::Base self.acts << Activity.new(:user_id => self.author_id) end + # Time 2015-02-27 15:48:17 + # Author lizanle + # Description 公用表中也要记录 + def act_as_forge_activity + self.forge_acts << ForgeActivity.new(:user_id => self.author_id, + :project_id => self.project.id) + end end diff --git a/app/models/project.rb b/app/models/project.rb index df403bb5c..c8364f132 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -17,7 +17,6 @@ class Project < ActiveRecord::Base include Redmine::SafeAttributes - include ExpireHelper ProjectType_project = 0 ProjectType_course = 1 @@ -130,9 +129,8 @@ class Project < ActiveRecord::Base #ActiveModel::Dirty 这里有一个changed方法。对任何对象都可以用 after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?} # 创建project之后默认创建一个board,之后的board去掉了board的概念 - after_create :create_board_sync,:expire_project_cache - after_update :expire_project_cache - before_destroy :delete_all_members,:expire_project_cache + after_create :create_board_sync + before_destroy :delete_all_members def remove_references_before_destroy return if self.id.nil? Watcher.delete_all ['watchable_id = ?', id] diff --git a/app/models/tracker.rb b/app/models/tracker.rb index 6a0069975..af56d23bf 100644 --- a/app/models/tracker.rb +++ b/app/models/tracker.rb @@ -16,7 +16,9 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class Tracker < ActiveRecord::Base - + # Time 2015-02-6 09:34:44 + # Author lizanle + # Description freeze方法让字符串不可变 CORE_FIELDS_UNDISABLABLE = %w(project_id tracker_id subject description priority_id is_private).freeze # Fields that can be disabled # Other (future) fields should be appended, not inserted! diff --git a/app/views/attachments/_form.html.erb b/app/views/attachments/_form.html.erb index c97da6d66..1d637919c 100644 --- a/app/views/attachments/_form.html.erb +++ b/app/views/attachments/_form.html.erb @@ -27,6 +27,7 @@ // file.click(); // } +<% project = project %> <%#= button_tag "浏览", :type=>"button", :onclick=>"CompatibleSend();" %> @@ -41,7 +42,7 @@ :max_file_size => Setting.attachment_max_size.to_i.kilobytes, :max_file_size_message => l(:error_attachment_too_big, :max_size => number_to_human_size(Setting.attachment_max_size.to_i.kilobytes)), :max_concurrent_uploads => Redmine::Configuration['max_concurrent_ajax_uploads'].to_i, - :upload_path => uploads_path(:format => 'js'), + :upload_path => uploads_path(:format => 'js',:project =>project), :description_placeholder => l(:label_optional_description) } %> diff --git a/app/views/files/_new.html.erb b/app/views/files/_new.html.erb index bd66d93be..6e8e806cf 100644 --- a/app/views/files/_new.html.erb +++ b/app/views/files/_new.html.erb @@ -38,7 +38,7 @@

      -

      <%=l(:label_attachment_plural)%><%= render :partial => 'attachments/form' %>

      +

      <%=l(:label_attachment_plural)%><%= render :partial => 'attachments/form', locals: {project: project} %>

    <%= submit_tag l(:button_add) %> <% end %> diff --git a/app/views/issues/show.html.erb b/app/views/issues/show.html.erb index 204dd519a..270c950ae 100644 --- a/app/views/issues/show.html.erb +++ b/app/views/issues/show.html.erb @@ -1,6 +1,5 @@ <%# html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %> - -<% html_title "#{@issue.tracker.name} #{@issue.source_from}: #{@issue.subject}" %> +<% html_title "#{@issue.tracker.name} #{@issue.source_from}'#'#{@issue.project_index}: #{@issue.subject}" %> <%= render :partial => 'action_menu' %> @@ -87,7 +86,7 @@ end %> <%= render_custom_fields_rows(@issue) %> <%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %> - +woca <% if @issue.description? || @issue.attachments.any? -%>
    <% if @issue.description? %> diff --git a/app/views/kaminari/_paginator.html.erb b/app/views/kaminari/_paginator.html.erb index 5250cd1a0..ace425d08 100644 --- a/app/views/kaminari/_paginator.html.erb +++ b/app/views/kaminari/_paginator.html.erb @@ -7,7 +7,7 @@ paginator: the paginator that renders the pagination tags inside -%> <%= paginator.render do -%> -
  • <%= image_tag url_to_avatar(event.event_author), :class => "avatar-3" %> @@ -145,25 +143,20 @@
  • <% end %> - <% end %> - <% end %>
      -

      <%= l(:lable_bar_active)%> <%= link_to l(:label_my_question) , newbie_send_path, {:class => 'orangeButton idea_btn', :style => "color: #EEEEEE" }%> <%= link_to l(:label_my_feedback) , suggestion_send_path, {:class => 'orangeButton idea_btn', :style => "color: #EEEEEE" }%>

      <%= link_to l(:label_more), forums_path %>
      - <% cache("forums") do %> <% topics = find_new_forum_topics(6) %> <% topics.includes(:forum, :last_reply, :author).each do |topic|%> - <% cache topic do %>
    • <%= link_to '['+topic.forum.name + ']',forum_path(topic.forum),:class => 'memo_Bar_title' %> @@ -192,9 +185,6 @@
    • <%end %> - <% end %> - <% end %> -
    diff --git a/db/migrate/20150206023634_create_forge_activities.rb b/db/migrate/20150206023634_create_forge_activities.rb new file mode 100644 index 000000000..f5f6a56f0 --- /dev/null +++ b/db/migrate/20150206023634_create_forge_activities.rb @@ -0,0 +1,13 @@ +class CreateForgeActivities < ActiveRecord::Migration + def change + create_table :forge_activities do |t| + t.integer :user_id + t.integer :project_id + t.references :forge_act, polymorphic: true, index: true + t.integer :org_id + t.timestamps + end + + add_index :forge_activities ,:forge_act_id + end +end diff --git a/db/migrate/20150206060632_import_issue_data_to_forge_activities.rb b/db/migrate/20150206060632_import_issue_data_to_forge_activities.rb new file mode 100644 index 000000000..c47fdb5bd --- /dev/null +++ b/db/migrate/20150206060632_import_issue_data_to_forge_activities.rb @@ -0,0 +1,46 @@ +# Time 2015-02-37 15:03:42 +# Author lizanle +# Description 将Issue中的数据导入forge_activities表 +class ImportIssueDataToForgeActivities < ActiveRecord::Migration + def up + issue_arr = select_all("SELECT\n" + + " `issues`.`id` AS id,\n" + + " `issues`.`project_id` AS project_id,\n" + + " `issues`.`author_id` AS author_id,\n" + + " `issues`.`created_on` AS created_on,\n" + + " `issues`.`updated_on` AS updated_on\n" + + "FROM\n" + + " `issues`\n" + + "LEFT OUTER JOIN `projects` ON `projects`.`id` = `issues`.`project_id`\n" + + "LEFT OUTER JOIN `users` ON `users`.`id` = `issues`.`author_id`\n" + + "AND `users`.`type` IN ('User', 'AnonymousUser')\n" + + "LEFT OUTER JOIN `trackers` ON `trackers`.`id` = `issues`.`tracker_id`\n" + + "WHERE\n" + + " (\n" + + " projects. STATUS <> 9\n" + + " AND projects.id IN (\n" + + " SELECT\n" + + " em.project_id\n" + + " FROM\n" + + " enabled_modules em\n" + + " WHERE\n" + + " em. NAME = 'issue_tracking'\n" + + " )\n" + + " )"); + issue_arr.each do |e| + ForgeActivity.connection.execute("insert into forge_activities(forge_act_id, + forge_act_type, + project_id, + user_id, + created_at, + updated_at) + values(#{e["id"]},'#{Issue.to_s}',#{e["project_id"]},#{e["author_id"]}, + '#{e["created_on"].to_s.gsub("+0800","").to_datetime.strftime("%Y-%m-%d %H:%M:%S")}', + '#{e["updated_on"].to_s.gsub("+0800","").to_datetime.strftime("%Y-%m-%d %H:%M:%S")}')") + end + end + + def down + ForgeActivity.delete_all([" forge_act_type = ?",ForgeActivity::TYPE_OF_ISSUE_ACT]) + end +end diff --git a/db/migrate/20150210062236_add_journal_data_to_forge_activities.rb b/db/migrate/20150210062236_add_journal_data_to_forge_activities.rb new file mode 100644 index 000000000..11197efb3 --- /dev/null +++ b/db/migrate/20150210062236_add_journal_data_to_forge_activities.rb @@ -0,0 +1,51 @@ +class AddJournalDataToForgeActivities < ActiveRecord::Migration + def up + journal_arr = select_all("SELECT\n" + + " `journals`.`id` AS id,\n" + + " `journals`.`created_on` AS created_on,\n" + + " `projects`.`id` AS project_id,\n" + + " `issues`.`updated_on` AS updated_on,\n" + + " `users`.`id` AS author_id\n" + + "FROM\n" + + " `journals`\n" + + "LEFT OUTER JOIN `issues` ON `issues`.`id` = `journals`.`journalized_id`\n" + + "LEFT OUTER JOIN `projects` ON `projects`.`id` = `issues`.`project_id`\n" + + "LEFT OUTER JOIN `journal_details` ON `journal_details`.`journal_id` = `journals`.`id`\n" + + "LEFT OUTER JOIN `users` ON `users`.`id` = `journals`.`user_id`\n" + + "AND `users`.`type` IN ('User', 'AnonymousUser')\n" + + "WHERE\n" + + " (\n" + + " projects. STATUS <> 9\n" + + " AND projects.id IN (\n" + + " SELECT\n" + + " em.project_id\n" + + " FROM\n" + + " enabled_modules em\n" + + " WHERE\n" + + " em. NAME = 'issue_tracking'\n" + + " )\n" + + " )\n" + + " AND (\n" + + " journals.journalized_type = 'Issue'\n" + + " AND (\n" + + " journal_details.prop_key = 'status_id'\n" + + " OR journals.notes <> ''\n" + + " )\n" + + " )"); + journal_arr.each do |e| + ForgeActivity.connection.execute("insert into forge_activities(forge_act_id, + forge_act_type, + project_id, + user_id, + created_at, + updated_at) + values(#{e["id"]},'#{Journal.to_s}',#{e["project_id"]},#{e["author_id"]}, + '#{e["created_on"].to_s.gsub("+0800","").to_datetime.strftime("%Y-%m-%d %H:%M:%S")}', + '#{e["updated_on"].to_s.gsub("+0800","").to_datetime.strftime("%Y-%m-%d %H:%M:%S")}')") + end + end + + def down + ForgeActivity.delete_all([" forge_act_type = ?",ForgeActivity::TYPE_OF_JOURNAL_ACT]) + end +end diff --git a/db/migrate/20150227061944_import_message_data_to_forge_activities.rb b/db/migrate/20150227061944_import_message_data_to_forge_activities.rb new file mode 100644 index 000000000..5ce01a28e --- /dev/null +++ b/db/migrate/20150227061944_import_message_data_to_forge_activities.rb @@ -0,0 +1,41 @@ +class ImportMessageDataToForgeActivities < ActiveRecord::Migration + def up + issue_arr = select_all("SELECT\n" + + " `messages`.`id` AS id,\n" + + " `messages`.`author_id` AS author_id,\n" + + " `projects`.`id` AS project_id,\n" + + " `messages`.`created_on` AS created_on,\n" + + " `messages`.`updated_on` AS updated_on\n" + + "FROM\n" + + " `messages`\n" + + "LEFT OUTER JOIN `boards` ON `boards`.`id` = `messages`.`board_id`\n" + + "LEFT OUTER JOIN `projects` ON `projects`.`id` = `boards`.`project_id`\n" + + "LEFT OUTER JOIN `users` ON `users`.`id` = `messages`.`author_id`\n" + + "AND `users`.`type` IN ('User', 'AnonymousUser')\n" + + "WHERE\n" + + " projects. STATUS <> 9\n" + + " AND projects.id IN (\n" + + " SELECT\n" + + " em.project_id\n" + + " FROM\n" + + " enabled_modules em\n" + + " WHERE\n" + + " em. NAME = 'boards'\n" + + ")"); + issue_arr.each do |e| + ForgeActivity.connection.execute("insert into forge_activities(forge_act_id, + forge_act_type, + project_id, + user_id, + created_at, + updated_at) + values(#{e["id"]},'#{Message.to_s}',#{e["project_id"]},#{e["author_id"]}, + '#{e["created_on"].to_s.gsub("+0800","").to_datetime.strftime("%Y-%m-%d %H:%M:%S")}', + '#{e["updated_on"].to_s.gsub("+0800","").to_datetime.strftime("%Y-%m-%d %H:%M:%S")}')") + end + end + + def down + ForgeActivity.delete_all([" forge_act_type = ?",ForgeActivity::TYPE_OF_MESSAGE_ACT]) + end +end diff --git a/db/migrate/20150227065713_import_news_data_to_forge_activities.rb b/db/migrate/20150227065713_import_news_data_to_forge_activities.rb new file mode 100644 index 000000000..b6a68c033 --- /dev/null +++ b/db/migrate/20150227065713_import_news_data_to_forge_activities.rb @@ -0,0 +1,41 @@ +class ImportNewsDataToForgeActivities < ActiveRecord::Migration + def up + issue_arr = select_all("SELECT\n" + + " `messages`.`id` AS id,\n" + + " `messages`.`author_id` AS author_id,\n" + + " `projects`.`id` AS project_id,\n" + + " `messages`.`created_on` AS created_on,\n" + + " `messages`.`updated_on` AS updated_on\n" + + "FROM\n" + + " `messages`\n" + + "LEFT OUTER JOIN `boards` ON `boards`.`id` = `messages`.`board_id`\n" + + "LEFT OUTER JOIN `projects` ON `projects`.`id` = `boards`.`project_id`\n" + + "LEFT OUTER JOIN `users` ON `users`.`id` = `messages`.`author_id`\n" + + "AND `users`.`type` IN ('User', 'AnonymousUser')\n" + + "WHERE\n" + + " projects. STATUS <> 9\n" + + " AND projects.id IN (\n" + + " SELECT\n" + + " em.project_id\n" + + " FROM\n" + + " enabled_modules em\n" + + " WHERE\n" + + " em. NAME = 'boards'\n" + + ")"); + issue_arr.each do |e| + ForgeActivity.connection.execute("insert into forge_activities(forge_act_id, + forge_act_type, + project_id, + user_id, + created_at, + updated_at) + values(#{e["id"]},'#{News.to_s}',#{e["project_id"]},#{e["author_id"]}, + '#{e["created_on"].to_s.gsub("+0800","").to_datetime.strftime("%Y-%m-%d %H:%M:%S")}', + '#{e["created_on"].to_s.gsub("+0800","").to_datetime.strftime("%Y-%m-%d %H:%M:%S")}')") + end + end + + def down + ForgeActivity.delete_all([" forge_act_type = ?",ForgeActivity::TYPE_OF_NEWS_ACT]) + end +end diff --git a/db/migrate/20150227083257_add_column_project_issue_index_to_issues.rb b/db/migrate/20150227083257_add_column_project_issue_index_to_issues.rb new file mode 100644 index 000000000..b57969f01 --- /dev/null +++ b/db/migrate/20150227083257_add_column_project_issue_index_to_issues.rb @@ -0,0 +1,6 @@ +class AddColumnProjectIssueIndexToIssues < ActiveRecord::Migration + def change + add_column :issues, :project_issues_index, :integer + end + +end diff --git a/db/migrate/20150227085333_add_data_to_project_issues_indexs_in_issue.rb b/db/migrate/20150227085333_add_data_to_project_issues_indexs_in_issue.rb new file mode 100644 index 000000000..a5e227808 --- /dev/null +++ b/db/migrate/20150227085333_add_data_to_project_issues_indexs_in_issue.rb @@ -0,0 +1,10 @@ +class AddDataToProjectIssuesIndexsInIssue < ActiveRecord::Migration + def change + for i in 1 ... 1000 do i + Issue.page(i).per(10).each do |e| + index = e.project.issues.index(e).to_i + 1 + execute("update issues set project_issues_index = #{index} where id = #{e.id}") + end + end + end +end diff --git a/db/migrate/20150302061232_import_document_data_to_forge_activities.rb b/db/migrate/20150302061232_import_document_data_to_forge_activities.rb new file mode 100644 index 000000000..ac9e1af1e --- /dev/null +++ b/db/migrate/20150302061232_import_document_data_to_forge_activities.rb @@ -0,0 +1,37 @@ +class ImportDocumentDataToForgeActivities < ActiveRecord::Migration + def up + doc_arr = select_all("SELECT\n" + + " `documents`.`id` AS id,\n" + + " `documents`.`project_id` AS project_id,\n" + + " `documents`.`created_on` AS created_on,\n" + + " `documents`.`user_id` AS author_id\n" + + "FROM\n" + + " `documents`\n" + + "LEFT OUTER JOIN `projects` ON `projects`.`id` = `documents`.`project_id`\n" + + "WHERE\n" + + " projects. STATUS <> 9\n" + + " AND projects.id IN (\n" + + " SELECT\n" + + " em.project_id\n" + + " FROM\n" + + " enabled_modules em\n" + + " WHERE\n" + + " em. NAME = 'documents'\n" + + ")"); + doc_arr.each do |e| + ForgeActivity.connection.execute("insert into forge_activities(forge_act_id, + forge_act_type, + project_id, + user_id, + created_at, + updated_at) + values(#{e["id"]},'#{Document.to_s}',#{e["project_id"]},#{e["author_id"]}, + '#{e["created_on"].to_s.gsub("+0800","").to_datetime.strftime("%Y-%m-%d %H:%M:%S")}', + '#{e["created_on"].to_s.gsub("+0800","").to_datetime.strftime("%Y-%m-%d %H:%M:%S")}')") + end + end + + def down + ForgeActivity.delete_all([" forge_act_type = ?",ForgeActivity::TYPE_OF_DOCUMENT_ACT]) + end +end diff --git a/db/migrate/20150302091345_import_attachments_data_to_forge_activities.rb b/db/migrate/20150302091345_import_attachments_data_to_forge_activities.rb new file mode 100644 index 000000000..3cf8b50d6 --- /dev/null +++ b/db/migrate/20150302091345_import_attachments_data_to_forge_activities.rb @@ -0,0 +1,43 @@ +class ImportAttachmentsDataToForgeActivities < ActiveRecord::Migration + def up + attach_arr = select_all("SELECT\n" + + " attachments.author_id as author_id,\n" + + " attachments.created_on as created_on,\n" + + " attachments.id as id,\n" + + " projects.id as project_id\n" + + "FROM\n" + + " `attachments`\n" + + "LEFT JOIN versions ON attachments.container_type = 'Version'\n" + + "AND versions.id = attachments.container_id\n" + + "LEFT JOIN projects ON versions.project_id = projects.id\n" + + "OR (\n" + + " attachments.container_type = 'Project'\n" + + " AND attachments.container_id = projects.id\n" + + ")\n" + + "WHERE\n" + + " projects. STATUS <> 9\n" + + " AND projects.id IN (\n" + + " SELECT\n" + + " em.project_id\n" + + " FROM\n" + + " enabled_modules em\n" + + " WHERE\n" + + " em. NAME = 'files'\n" + + " )"); + attach_arr.each do |e| + ForgeActivity.connection.execute("insert into forge_activities(forge_act_id, + forge_act_type, + project_id, + user_id, + created_at, + updated_at) + values(#{e["id"]},'#{Attachment.to_s}',#{e["project_id"]},#{e["author_id"]}, + '#{e["created_on"].to_s.gsub("+0800","").to_datetime.strftime("%Y-%m-%d %H:%M:%S")}', + '#{e["created_on"].to_s.gsub("+0800","").to_datetime.strftime("%Y-%m-%d %H:%M:%S")}')") + end +end + +def down + ForgeActivity.delete_all([" forge_act_type = ?",ForgeActivity::TYPE_OF_ATTACHMENT_ACT]) +end +end diff --git a/db/schema.rb b/db/schema.rb index d7a1b18bc..4a3b14edc 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20150128032421) do +ActiveRecord::Schema.define(:version => 20150302091345) do create_table "activities", :force => true do |t| t.integer "act_id", :null => false @@ -472,6 +472,18 @@ ActiveRecord::Schema.define(:version => 20150128032421) do t.integer "show_contest", :default => 1 end + create_table "forge_activities", :force => true do |t| + t.integer "user_id" + t.integer "project_id" + t.integer "forge_act_id" + t.string "forge_act_type" + t.integer "org_id" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "forge_activities", ["forge_act_id"], :name => "index_forge_activities_on_forge_act_id" + create_table "forums", :force => true do |t| t.string "name", :null => false t.text "description" @@ -558,29 +570,30 @@ ActiveRecord::Schema.define(:version => 20150128032421) do add_index "issue_statuses", ["position"], :name => "index_issue_statuses_on_position" create_table "issues", :force => true do |t| - t.integer "tracker_id", :null => false - t.integer "project_id", :null => false - t.string "subject", :default => "", :null => false + t.integer "tracker_id", :null => false + t.integer "project_id", :null => false + t.string "subject", :default => "", :null => false t.text "description" t.date "due_date" t.integer "category_id" - t.integer "status_id", :null => false + t.integer "status_id", :null => false t.integer "assigned_to_id" - t.integer "priority_id", :null => false + t.integer "priority_id", :null => false t.integer "fixed_version_id" - t.integer "author_id", :null => false - t.integer "lock_version", :default => 0, :null => false + t.integer "author_id", :null => false + t.integer "lock_version", :default => 0, :null => false t.datetime "created_on" t.datetime "updated_on" t.date "start_date" - t.integer "done_ratio", :default => 0, :null => false + t.integer "done_ratio", :default => 0, :null => false t.float "estimated_hours" t.integer "parent_id" t.integer "root_id" t.integer "lft" t.integer "rgt" - t.boolean "is_private", :default => false, :null => false + t.boolean "is_private", :default => false, :null => false t.datetime "closed_on" + t.integer "project_issues_index" end add_index "issues", ["assigned_to_id"], :name => "index_issues_on_assigned_to_id" @@ -618,6 +631,16 @@ ActiveRecord::Schema.define(:version => 20150128032421) do add_index "journal_details", ["journal_id"], :name => "journal_details_journal_id" + create_table "journal_details_copy", :force => true do |t| + t.integer "journal_id", :default => 0, :null => false + t.string "property", :limit => 30, :default => "", :null => false + t.string "prop_key", :limit => 30, :default => "", :null => false + t.text "old_value" + t.text "value" + end + + add_index "journal_details_copy", ["journal_id"], :name => "journal_details_journal_id" + create_table "journal_replies", :id => false, :force => true do |t| t.integer "journal_id" t.integer "user_id" @@ -1013,12 +1036,12 @@ ActiveRecord::Schema.define(:version => 20150128032421) do end create_table "roles", :force => true do |t| - t.string "name", :limit => 30, :default => "", :null => false - t.integer "position", :default => 1 - t.boolean "assignable", :default => true - t.integer "builtin", :default => 0, :null => false + t.string "name", :limit => 90 + t.integer "position" + t.boolean "assignable" + t.integer "builtin" t.text "permissions" - t.string "issues_visibility", :limit => 30, :default => "default", :null => false + t.string "issues_visibility", :limit => 90 end create_table "schools", :force => true do |t| diff --git a/public/javascripts/application.js b/public/javascripts/application.js index 5692ebe83..4345f8c31 100644 --- a/public/javascripts/application.js +++ b/public/javascripts/application.js @@ -1,4 +1,7 @@ //= require_directory ./rateable +//= require jquery.min +//= require jquery.infinitescroll + /* Redmine - project management software Copyright (C) 2006-2013 Jean-Philippe Lang */ diff --git a/public/javascripts/jquery.infinitescroll.js b/public/javascripts/jquery.infinitescroll.js new file mode 100644 index 000000000..cc743b2b0 --- /dev/null +++ b/public/javascripts/jquery.infinitescroll.js @@ -0,0 +1,814 @@ +/*jshint undef: true */ +/*global jQuery: true */ + +/* + -------------------------------- + Infinite Scroll + -------------------------------- + + https://github.com/paulirish/infinite-scroll + + version 2.0b2.120519 + + Copyright 2011/12 Paul Irish & Luke Shumard + + Licensed under the MIT license + + + Documentation: http://infinite-scroll.com/ +*/ + +(function (window, $, undefined) { + "use strict"; + + $.infinitescroll = function infscr(options, callback, element) { + this.element = $(element); + + // Flag the object in the event of a failed creation + if (!this._create(options, callback)) { + this.failed = true; + } + }; + + $.infinitescroll.defaults = { + loading: { + finished: undefined, + finishedMsg: "貌似所有内容已经加载完毕了~~", + img: "data:image/gif;base64,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", + msg: null, + msgText: "正在努力加载,请稍后......", + selector: null, + speed: 'low', + start: undefined + }, + state: { + isDuringAjax: false, + isInvalidPage: false, + isDestroyed: false, + isDone: false, // For when it goes all the way through the archive. + isPaused: false, + isBeyondMaxPage: false, + currPage: 1 + }, + debug: false, + behavior: undefined, + binder: $(window), // used to cache the selector + nextSelector: "div.navigation a:first", + navSelector: "div.navigation", + contentSelector: null, // rename to pageFragment + extraScrollPx: 150, + itemSelector: "div.post", + animate: false, + pathParse: undefined, + dataType: 'html', + appendCallback: true, + bufferPx: 40, + errorCallback: function () { }, + infid: 0, //Instance ID + pixelsFromNavToBottom: undefined, + path: undefined, // Either parts of a URL as an array (e.g. ["/page/", "/"] or a function that takes in the page number and returns a URL + prefill: false, // When the document is smaller than the window, load data until the document is larger or links are exhausted + maxPage: undefined // to manually control maximum page (when maxPage is undefined, maximum page limitation is not work) + }; + + $.infinitescroll.prototype = { + + /* + ---------------------------- + Private methods + ---------------------------- + */ + + // Bind or unbind from scroll + _binding: function infscr_binding(binding) { + + var instance = this, + opts = instance.options; + + opts.v = '2.0b2.120520'; + + // if behavior is defined and this function is extended, call that instead of default + if (!!opts.behavior && this['_binding_'+opts.behavior] !== undefined) { + this['_binding_'+opts.behavior].call(this); + return; + } + + if (binding !== 'bind' && binding !== 'unbind') { + this._debug('Binding value ' + binding + ' not valid'); + return false; + } + + if (binding === 'unbind') { + (this.options.binder).unbind('smartscroll.infscr.' + instance.options.infid); + } else { + (this.options.binder)[binding]('smartscroll.infscr.' + instance.options.infid, function () { + instance.scroll(); + }); + } + + this._debug('Binding', binding); + }, + + // Fundamental aspects of the plugin are initialized + _create: function infscr_create(options, callback) { + + // Add custom options to defaults + var opts = $.extend(true, {}, $.infinitescroll.defaults, options); + this.options = opts; + var $window = $(window); + var instance = this; + + // Validate selectors + if (!instance._validate(options)) { + return false; + } + + // Validate page fragment path + var path = $(opts.nextSelector).attr('href'); + if (!path) { + this._debug('Navigation selector not found'); + return false; + } + + // Set the path to be a relative URL from root. + opts.path = opts.path || this._determinepath(path); + + // contentSelector is 'page fragment' option for .load() / .ajax() calls + opts.contentSelector = opts.contentSelector || this.element; + + // loading.selector - if we want to place the load message in a specific selector, defaulted to the contentSelector + opts.loading.selector = opts.loading.selector || opts.contentSelector; + + // Define loading.msg + opts.loading.msg = opts.loading.msg || $('
    Loading...
    ' + opts.loading.msgText + '
    '); + + // Preload loading.img + (new Image()).src = opts.loading.img; + + // distance from nav links to bottom + // computed as: height of the document + top offset of container - top offset of nav link + if(opts.pixelsFromNavToBottom === undefined) { + opts.pixelsFromNavToBottom = $(document).height() - $(opts.navSelector).offset().top; + this._debug("pixelsFromNavToBottom: " + opts.pixelsFromNavToBottom); + } + + var self = this; + + // determine loading.start actions + opts.loading.start = opts.loading.start || function() { + $(opts.navSelector).hide(); + opts.loading.msg + .appendTo(opts.loading.selector) + .show(opts.loading.speed, $.proxy(function() { + this.beginAjax(opts); + }, self)); + }; + + // determine loading.finished actions + opts.loading.finished = opts.loading.finished || function() { + if (!opts.state.isBeyondMaxPage) + opts.loading.msg.fadeOut(opts.loading.speed); + }; + + // callback loading + opts.callback = function(instance, data, url) { + if (!!opts.behavior && instance['_callback_'+opts.behavior] !== undefined) { + instance['_callback_'+opts.behavior].call($(opts.contentSelector)[0], data, url); + } + + if (callback) { + callback.call($(opts.contentSelector)[0], data, opts, url); + } + + if (opts.prefill) { + $window.bind("resize.infinite-scroll", instance._prefill); + } + }; + + if (options.debug) { + // Tell IE9 to use its built-in console + if (Function.prototype.bind && (typeof console === 'object' || typeof console === 'function') && typeof console.log === "object") { + ["log","info","warn","error","assert","dir","clear","profile","profileEnd"] + .forEach(function (method) { + console[method] = this.call(console[method], console); + }, Function.prototype.bind); + } + } + + this._setup(); + + // Setups the prefill method for use + if (opts.prefill) { + this._prefill(); + } + + // Return true to indicate successful creation + return true; + }, + + _prefill: function infscr_prefill() { + var instance = this; + var $window = $(window); + + function needsPrefill() { + return (instance.options.contentSelector.height() <= $window.height()); + } + + this._prefill = function() { + if (needsPrefill()) { + instance.scroll(); + } + + $window.bind("resize.infinite-scroll", function() { + if (needsPrefill()) { + $window.unbind("resize.infinite-scroll"); + instance.scroll(); + } + }); + }; + + // Call self after setting up the new function + this._prefill(); + }, + + // Console log wrapper + _debug: function infscr_debug() { + if (true !== this.options.debug) { + return; + } + + if (typeof console !== 'undefined' && typeof console.log === 'function') { + // Modern browsers + // Single argument, which is a string + if ((Array.prototype.slice.call(arguments)).length === 1 && typeof Array.prototype.slice.call(arguments)[0] === 'string') { + console.log( (Array.prototype.slice.call(arguments)).toString() ); + } else { + console.log( Array.prototype.slice.call(arguments) ); + } + } else if (!Function.prototype.bind && typeof console !== 'undefined' && typeof console.log === 'object') { + // IE8 + Function.prototype.call.call(console.log, console, Array.prototype.slice.call(arguments)); + } + }, + + // find the number to increment in the path. + _determinepath: function infscr_determinepath(path) { + + var opts = this.options; + + // if behavior is defined and this function is extended, call that instead of default + if (!!opts.behavior && this['_determinepath_'+opts.behavior] !== undefined) { + return this['_determinepath_'+opts.behavior].call(this,path); + } + + if (!!opts.pathParse) { + + this._debug('pathParse manual'); + return opts.pathParse(path, this.options.state.currPage+1); + + } else if (path.match(/^(.*?)\b2\b(.*?$)/)) { + path = path.match(/^(.*?)\b2\b(.*?$)/).slice(1); + + // if there is any 2 in the url at all. + } else if (path.match(/^(.*?)2(.*?$)/)) { + + // page= is used in django: + // http://www.infinite-scroll.com/changelog/comment-page-1/#comment-127 + if (path.match(/^(.*?page=)2(\/.*|$)/)) { + path = path.match(/^(.*?page=)2(\/.*|$)/).slice(1); + return path; + } + + path = path.match(/^(.*?)2(.*?$)/).slice(1); + + } else { + + // page= is used in drupal too but second page is page=1 not page=2: + // thx Jerod Fritz, vladikoff + if (path.match(/^(.*?page=)1(\/.*|$)/)) { + path = path.match(/^(.*?page=)1(\/.*|$)/).slice(1); + return path; + } else { + this._debug('Sorry, we couldn\'t parse your Next (Previous Posts) URL. Verify your the css selector points to the correct A tag. If you still get this error: yell, scream, and kindly ask for help at infinite-scroll.com.'); + // Get rid of isInvalidPage to allow permalink to state + opts.state.isInvalidPage = true; //prevent it from running on this page. + } + } + this._debug('determinePath', path); + return path; + + }, + + // Custom error + _error: function infscr_error(xhr) { + + var opts = this.options; + + // if behavior is defined and this function is extended, call that instead of default + if (!!opts.behavior && this['_error_'+opts.behavior] !== undefined) { + this['_error_'+opts.behavior].call(this,xhr); + return; + } + + if (xhr !== 'destroy' && xhr !== 'end') { + xhr = 'unknown'; + } + + this._debug('Error', xhr); + + if (xhr === 'end' || opts.state.isBeyondMaxPage) { + this._showdonemsg(); + } + + opts.state.isDone = true; + opts.state.currPage = 1; // if you need to go back to this instance + opts.state.isPaused = false; + opts.state.isBeyondMaxPage = false; + this._binding('unbind'); + + }, + + // Load Callback + _loadcallback: function infscr_loadcallback(box, data, url) { + var opts = this.options, + callback = this.options.callback, // GLOBAL OBJECT FOR CALLBACK + result = (opts.state.isDone) ? 'done' : (!opts.appendCallback) ? 'no-append' : 'append', + frag; + + // if behavior is defined and this function is extended, call that instead of default + if (!!opts.behavior && this['_loadcallback_'+opts.behavior] !== undefined) { + this['_loadcallback_'+opts.behavior].call(this,box,data); + return; + } + + switch (result) { + case 'done': + this._showdonemsg(); + return false; + + case 'no-append': + if (opts.dataType === 'html') { + data = '
    ' + data + '
    '; + data = $(data).find(opts.itemSelector); + } + break; + + case 'append': + var children = box.children(); + // if it didn't return anything + if (children.length === 0) { + return this._error('end'); + } + + // use a documentFragment because it works when content is going into a table or UL + frag = document.createDocumentFragment(); + while (box[0].firstChild) { + frag.appendChild(box[0].firstChild); + } + + this._debug('contentSelector', $(opts.contentSelector)[0]); + $(opts.contentSelector)[0].appendChild(frag); + // previously, we would pass in the new DOM element as context for the callback + // however we're now using a documentfragment, which doesn't have parents or children, + // so the context is the contentContainer guy, and we pass in an array + // of the elements collected as the first argument. + + data = children.get(); + break; + } + + // loadingEnd function + opts.loading.finished.call($(opts.contentSelector)[0],opts); + + // smooth scroll to ease in the new content + if (opts.animate) { + var scrollTo = $(window).scrollTop() + $(opts.loading.msg).height() + opts.extraScrollPx + 'px'; + $('html,body').animate({ scrollTop: scrollTo }, 800, function () { opts.state.isDuringAjax = false; }); + } + + if (!opts.animate) { + // once the call is done, we can allow it again. + opts.state.isDuringAjax = false; + } + + callback(this, data, url); + + if (opts.prefill) { + this._prefill(); + } + }, + + _nearbottom: function infscr_nearbottom() { + + var opts = this.options, + pixelsFromWindowBottomToBottom = 0 + $(document).height() - (opts.binder.scrollTop()) - $(window).height(); + + // if behavior is defined and this function is extended, call that instead of default + if (!!opts.behavior && this['_nearbottom_'+opts.behavior] !== undefined) { + return this['_nearbottom_'+opts.behavior].call(this); + } + + this._debug('math:', pixelsFromWindowBottomToBottom, opts.pixelsFromNavToBottom); + + // if distance remaining in the scroll (including buffer) is less than the orignal nav to bottom.... + return (pixelsFromWindowBottomToBottom - opts.bufferPx < opts.pixelsFromNavToBottom); + + }, + + // Pause / temporarily disable plugin from firing + _pausing: function infscr_pausing(pause) { + + var opts = this.options; + + // if behavior is defined and this function is extended, call that instead of default + if (!!opts.behavior && this['_pausing_'+opts.behavior] !== undefined) { + this['_pausing_'+opts.behavior].call(this,pause); + return; + } + + // If pause is not 'pause' or 'resume', toggle it's value + if (pause !== 'pause' && pause !== 'resume' && pause !== null) { + this._debug('Invalid argument. Toggling pause value instead'); + } + + pause = (pause && (pause === 'pause' || pause === 'resume')) ? pause : 'toggle'; + + switch (pause) { + case 'pause': + opts.state.isPaused = true; + break; + + case 'resume': + opts.state.isPaused = false; + break; + + case 'toggle': + opts.state.isPaused = !opts.state.isPaused; + break; + } + + this._debug('Paused', opts.state.isPaused); + return false; + + }, + + // Behavior is determined + // If the behavior option is undefined, it will set to default and bind to scroll + _setup: function infscr_setup() { + + var opts = this.options; + + // if behavior is defined and this function is extended, call that instead of default + if (!!opts.behavior && this['_setup_'+opts.behavior] !== undefined) { + this['_setup_'+opts.behavior].call(this); + return; + } + + this._binding('bind'); + + return false; + + }, + + // Show done message + _showdonemsg: function infscr_showdonemsg() { + + var opts = this.options; + + // if behavior is defined and this function is extended, call that instead of default + if (!!opts.behavior && this['_showdonemsg_'+opts.behavior] !== undefined) { + this['_showdonemsg_'+opts.behavior].call(this); + return; + } + + opts.loading.msg + .find('img') + .hide() + .parent() + .find('div').html(opts.loading.finishedMsg).animate({ opacity: 1 }, 2000, function () { + $(this).parent().fadeOut(opts.loading.speed); + }); + + // user provided callback when done + opts.errorCallback.call($(opts.contentSelector)[0],'done'); + }, + + // grab each selector option and see if any fail + _validate: function infscr_validate(opts) { + for (var key in opts) { + if (key.indexOf && key.indexOf('Selector') > -1 && $(opts[key]).length === 0) { + this._debug('Your ' + key + ' found no elements.'); + return false; + } + } + + return true; + }, + + /* + ---------------------------- + Public methods + ---------------------------- + */ + + // Bind to scroll + bind: function infscr_bind() { + this._binding('bind'); + }, + + // Destroy current instance of plugin + destroy: function infscr_destroy() { + this.options.state.isDestroyed = true; + this.options.loading.finished(); + return this._error('destroy'); + }, + + // Set pause value to false + pause: function infscr_pause() { + this._pausing('pause'); + }, + + // Set pause value to false + resume: function infscr_resume() { + this._pausing('resume'); + }, + + beginAjax: function infscr_ajax(opts) { + var instance = this, + path = opts.path, + box, desturl, method, condition; + + // increment the URL bit. e.g. /page/3/ + opts.state.currPage++; + + // Manually control maximum page + if ( opts.maxPage != undefined && opts.state.currPage > opts.maxPage ){ + opts.state.isBeyondMaxPage = true; + this.destroy(); + return; + } + + // if we're dealing with a table we can't use DIVs + box = $(opts.contentSelector).is('table, tbody') ? $('') : $('
    '); + + desturl = (typeof path === 'function') ? path(opts.state.currPage) : path.join(opts.state.currPage); + instance._debug('heading into ajax', desturl); + + method = (opts.dataType === 'html' || opts.dataType === 'json' ) ? opts.dataType : 'html+callback'; + if (opts.appendCallback && opts.dataType === 'html') { + method += '+callback'; + } + + switch (method) { + case 'html+callback': + instance._debug('Using HTML via .load() method'); + box.load(desturl + ' ' + opts.itemSelector, undefined, function infscr_ajax_callback(responseText) { + instance._loadcallback(box, responseText, desturl); + }); + + break; + + case 'html': + instance._debug('Using ' + (method.toUpperCase()) + ' via $.ajax() method'); + $.ajax({ + // params + url: desturl, + dataType: opts.dataType, + complete: function infscr_ajax_callback(jqXHR, textStatus) { + condition = (typeof (jqXHR.isResolved) !== 'undefined') ? (jqXHR.isResolved()) : (textStatus === "success" || textStatus === "notmodified"); + if (condition) { + instance._loadcallback(box, jqXHR.responseText, desturl); + } else { + instance._error('end'); + } + } + }); + + break; + case 'json': + instance._debug('Using ' + (method.toUpperCase()) + ' via $.ajax() method'); + $.ajax({ + dataType: 'json', + type: 'GET', + url: desturl, + success: function (data, textStatus, jqXHR) { + condition = (typeof (jqXHR.isResolved) !== 'undefined') ? (jqXHR.isResolved()) : (textStatus === "success" || textStatus === "notmodified"); + if (opts.appendCallback) { + // if appendCallback is true, you must defined template in options. + // note that data passed into _loadcallback is already an html (after processed in opts.template(data)). + if (opts.template !== undefined) { + var theData = opts.template(data); + box.append(theData); + if (condition) { + instance._loadcallback(box, theData); + } else { + instance._error('end'); + } + } else { + instance._debug("template must be defined."); + instance._error('end'); + } + } else { + // if appendCallback is false, we will pass in the JSON object. you should handle it yourself in your callback. + if (condition) { + instance._loadcallback(box, data, desturl); + } else { + instance._error('end'); + } + } + }, + error: function() { + instance._debug("JSON ajax request failed."); + instance._error('end'); + } + }); + + break; + } + }, + + // Retrieve next set of content items + retrieve: function infscr_retrieve(pageNum) { + pageNum = pageNum || null; + + var instance = this, + opts = instance.options; + + // if behavior is defined and this function is extended, call that instead of default + if (!!opts.behavior && this['retrieve_'+opts.behavior] !== undefined) { + this['retrieve_'+opts.behavior].call(this,pageNum); + return; + } + + // for manual triggers, if destroyed, get out of here + if (opts.state.isDestroyed) { + this._debug('Instance is destroyed'); + return false; + } + + // we dont want to fire the ajax multiple times + opts.state.isDuringAjax = true; + + opts.loading.start.call($(opts.contentSelector)[0],opts); + }, + + // Check to see next page is needed + scroll: function infscr_scroll() { + + var opts = this.options, + state = opts.state; + + // if behavior is defined and this function is extended, call that instead of default + if (!!opts.behavior && this['scroll_'+opts.behavior] !== undefined) { + this['scroll_'+opts.behavior].call(this); + return; + } + + if (state.isDuringAjax || state.isInvalidPage || state.isDone || state.isDestroyed || state.isPaused) { + return; + } + + if (!this._nearbottom()) { + return; + } + + this.retrieve(); + + }, + + // Toggle pause value + toggle: function infscr_toggle() { + this._pausing(); + }, + + // Unbind from scroll + unbind: function infscr_unbind() { + this._binding('unbind'); + }, + + // update options + update: function infscr_options(key) { + if ($.isPlainObject(key)) { + this.options = $.extend(true,this.options,key); + } + } + }; + + + /* + ---------------------------- + Infinite Scroll function + ---------------------------- + + Borrowed logic from the following... + + jQuery UI + - https://github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.widget.js + + jCarousel + - https://github.com/jsor/jcarousel/blob/master/lib/jquery.jcarousel.js + + Masonry + - https://github.com/desandro/masonry/blob/master/jquery.masonry.js + +*/ + + $.fn.infinitescroll = function infscr_init(options, callback) { + + + var thisCall = typeof options; + + switch (thisCall) { + + // method + case 'string': + var args = Array.prototype.slice.call(arguments, 1); + + this.each(function () { + var instance = $.data(this, 'infinitescroll'); + + if (!instance) { + // not setup yet + // return $.error('Method ' + options + ' cannot be called until Infinite Scroll is setup'); + return false; + } + + if (!$.isFunction(instance[options]) || options.charAt(0) === "_") { + // return $.error('No such method ' + options + ' for Infinite Scroll'); + return false; + } + + // no errors! + instance[options].apply(instance, args); + }); + + break; + + // creation + case 'object': + + this.each(function () { + + var instance = $.data(this, 'infinitescroll'); + + if (instance) { + + // update options of current instance + instance.update(options); + + } else { + + // initialize new instance + instance = new $.infinitescroll(options, callback, this); + + // don't attach if instantiation failed + if (!instance.failed) { + $.data(this, 'infinitescroll', instance); + } + + } + + }); + + break; + + } + + return this; + }; + + + + /* + * smartscroll: debounced scroll event for jQuery * + * https://github.com/lukeshumard/smartscroll + * Based on smartresize by @louis_remi: https://github.com/lrbabe/jquery.smartresize.js * + * Copyright 2011 Louis-Remi & Luke Shumard * Licensed under the MIT license. * + */ + + var event = $.event, + scrollTimeout; + + event.special.smartscroll = { + setup: function () { + $(this).bind("scroll", event.special.smartscroll.handler); + }, + teardown: function () { + $(this).unbind("scroll", event.special.smartscroll.handler); + }, + handler: function (event, execAsap) { + // Save the context + var context = this, + args = arguments; + + // set correct event type + event.type = "smartscroll"; + + if (scrollTimeout) { clearTimeout(scrollTimeout); } + scrollTimeout = setTimeout(function () { + $(context).trigger('smartscroll', args); + }, execAsap === "execAsap" ? 0 : 100); + } + }; + + $.fn.smartscroll = function (fn) { + return fn ? this.bind("smartscroll", fn) : this.trigger("smartscroll", ["execAsap"]); + }; + + +})(window, jQuery); diff --git a/public/javascripts/jquery.min.js b/public/javascripts/jquery.min.js new file mode 100644 index 000000000..388377952 --- /dev/null +++ b/public/javascripts/jquery.min.js @@ -0,0 +1,2 @@ +/*! jQuery v1.8.3 jquery.com | jquery.org/license */ +(function(e,t){function _(e){var t=M[e]={};return v.each(e.split(y),function(e,n){t[n]=!0}),t}function H(e,n,r){if(r===t&&e.nodeType===1){var i="data-"+n.replace(P,"-$1").toLowerCase();r=e.getAttribute(i);if(typeof r=="string"){try{r=r==="true"?!0:r==="false"?!1:r==="null"?null:+r+""===r?+r:D.test(r)?v.parseJSON(r):r}catch(s){}v.data(e,n,r)}else r=t}return r}function B(e){var t;for(t in e){if(t==="data"&&v.isEmptyObject(e[t]))continue;if(t!=="toJSON")return!1}return!0}function et(){return!1}function tt(){return!0}function ut(e){return!e||!e.parentNode||e.parentNode.nodeType===11}function at(e,t){do e=e[t];while(e&&e.nodeType!==1);return e}function ft(e,t,n){t=t||0;if(v.isFunction(t))return v.grep(e,function(e,r){var i=!!t.call(e,r,e);return i===n});if(t.nodeType)return v.grep(e,function(e,r){return e===t===n});if(typeof t=="string"){var r=v.grep(e,function(e){return e.nodeType===1});if(it.test(t))return v.filter(t,r,!n);t=v.filter(t,r)}return v.grep(e,function(e,r){return v.inArray(e,t)>=0===n})}function lt(e){var t=ct.split("|"),n=e.createDocumentFragment();if(n.createElement)while(t.length)n.createElement(t.pop());return n}function Lt(e,t){return e.getElementsByTagName(t)[0]||e.appendChild(e.ownerDocument.createElement(t))}function At(e,t){if(t.nodeType!==1||!v.hasData(e))return;var n,r,i,s=v._data(e),o=v._data(t,s),u=s.events;if(u){delete o.handle,o.events={};for(n in u)for(r=0,i=u[n].length;r").appendTo(i.body),n=t.css("display");t.remove();if(n==="none"||n===""){Pt=i.body.appendChild(Pt||v.extend(i.createElement("iframe"),{frameBorder:0,width:0,height:0}));if(!Ht||!Pt.createElement)Ht=(Pt.contentWindow||Pt.contentDocument).document,Ht.write(""),Ht.close();t=Ht.body.appendChild(Ht.createElement(e)),n=Dt(t,"display"),i.body.removeChild(Pt)}return Wt[e]=n,n}function fn(e,t,n,r){var i;if(v.isArray(t))v.each(t,function(t,i){n||sn.test(e)?r(e,i):fn(e+"["+(typeof i=="object"?t:"")+"]",i,n,r)});else if(!n&&v.type(t)==="object")for(i in t)fn(e+"["+i+"]",t[i],n,r);else r(e,t)}function Cn(e){return function(t,n){typeof t!="string"&&(n=t,t="*");var r,i,s,o=t.toLowerCase().split(y),u=0,a=o.length;if(v.isFunction(n))for(;u)[^>]*$|#([\w\-]*)$)/,E=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,S=/^[\],:{}\s]*$/,x=/(?:^|:|,)(?:\s*\[)+/g,T=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,N=/"[^"\\\r\n]*"|true|false|null|-?(?:\d\d*\.|)\d+(?:[eE][\-+]?\d+|)/g,C=/^-ms-/,k=/-([\da-z])/gi,L=function(e,t){return(t+"").toUpperCase()},A=function(){i.addEventListener?(i.removeEventListener("DOMContentLoaded",A,!1),v.ready()):i.readyState==="complete"&&(i.detachEvent("onreadystatechange",A),v.ready())},O={};v.fn=v.prototype={constructor:v,init:function(e,n,r){var s,o,u,a;if(!e)return this;if(e.nodeType)return this.context=this[0]=e,this.length=1,this;if(typeof e=="string"){e.charAt(0)==="<"&&e.charAt(e.length-1)===">"&&e.length>=3?s=[null,e,null]:s=w.exec(e);if(s&&(s[1]||!n)){if(s[1])return n=n instanceof v?n[0]:n,a=n&&n.nodeType?n.ownerDocument||n:i,e=v.parseHTML(s[1],a,!0),E.test(s[1])&&v.isPlainObject(n)&&this.attr.call(e,n,!0),v.merge(this,e);o=i.getElementById(s[2]);if(o&&o.parentNode){if(o.id!==s[2])return r.find(e);this.length=1,this[0]=o}return this.context=i,this.selector=e,this}return!n||n.jquery?(n||r).find(e):this.constructor(n).find(e)}return v.isFunction(e)?r.ready(e):(e.selector!==t&&(this.selector=e.selector,this.context=e.context),v.makeArray(e,this))},selector:"",jquery:"1.8.3",length:0,size:function(){return this.length},toArray:function(){return l.call(this)},get:function(e){return e==null?this.toArray():e<0?this[this.length+e]:this[e]},pushStack:function(e,t,n){var r=v.merge(this.constructor(),e);return r.prevObject=this,r.context=this.context,t==="find"?r.selector=this.selector+(this.selector?" ":"")+n:t&&(r.selector=this.selector+"."+t+"("+n+")"),r},each:function(e,t){return v.each(this,e,t)},ready:function(e){return v.ready.promise().done(e),this},eq:function(e){return e=+e,e===-1?this.slice(e):this.slice(e,e+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(l.apply(this,arguments),"slice",l.call(arguments).join(","))},map:function(e){return this.pushStack(v.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:[].sort,splice:[].splice},v.fn.init.prototype=v.fn,v.extend=v.fn.extend=function(){var e,n,r,i,s,o,u=arguments[0]||{},a=1,f=arguments.length,l=!1;typeof u=="boolean"&&(l=u,u=arguments[1]||{},a=2),typeof u!="object"&&!v.isFunction(u)&&(u={}),f===a&&(u=this,--a);for(;a0)return;r.resolveWith(i,[v]),v.fn.trigger&&v(i).trigger("ready").off("ready")},isFunction:function(e){return v.type(e)==="function"},isArray:Array.isArray||function(e){return v.type(e)==="array"},isWindow:function(e){return e!=null&&e==e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return e==null?String(e):O[h.call(e)]||"object"},isPlainObject:function(e){if(!e||v.type(e)!=="object"||e.nodeType||v.isWindow(e))return!1;try{if(e.constructor&&!p.call(e,"constructor")&&!p.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(n){return!1}var r;for(r in e);return r===t||p.call(e,r)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw new Error(e)},parseHTML:function(e,t,n){var r;return!e||typeof e!="string"?null:(typeof t=="boolean"&&(n=t,t=0),t=t||i,(r=E.exec(e))?[t.createElement(r[1])]:(r=v.buildFragment([e],t,n?null:[]),v.merge([],(r.cacheable?v.clone(r.fragment):r.fragment).childNodes)))},parseJSON:function(t){if(!t||typeof t!="string")return null;t=v.trim(t);if(e.JSON&&e.JSON.parse)return e.JSON.parse(t);if(S.test(t.replace(T,"@").replace(N,"]").replace(x,"")))return(new Function("return "+t))();v.error("Invalid JSON: "+t)},parseXML:function(n){var r,i;if(!n||typeof n!="string")return null;try{e.DOMParser?(i=new DOMParser,r=i.parseFromString(n,"text/xml")):(r=new ActiveXObject("Microsoft.XMLDOM"),r.async="false",r.loadXML(n))}catch(s){r=t}return(!r||!r.documentElement||r.getElementsByTagName("parsererror").length)&&v.error("Invalid XML: "+n),r},noop:function(){},globalEval:function(t){t&&g.test(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(C,"ms-").replace(k,L)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,n,r){var i,s=0,o=e.length,u=o===t||v.isFunction(e);if(r){if(u){for(i in e)if(n.apply(e[i],r)===!1)break}else for(;s0&&e[0]&&e[a-1]||a===0||v.isArray(e));if(f)for(;u-1)a.splice(n,1),i&&(n<=o&&o--,n<=u&&u--)}),this},has:function(e){return v.inArray(e,a)>-1},empty:function(){return a=[],this},disable:function(){return a=f=n=t,this},disabled:function(){return!a},lock:function(){return f=t,n||c.disable(),this},locked:function(){return!f},fireWith:function(e,t){return t=t||[],t=[e,t.slice?t.slice():t],a&&(!r||f)&&(i?f.push(t):l(t)),this},fire:function(){return c.fireWith(this,arguments),this},fired:function(){return!!r}};return c},v.extend({Deferred:function(e){var t=[["resolve","done",v.Callbacks("once memory"),"resolved"],["reject","fail",v.Callbacks("once memory"),"rejected"],["notify","progress",v.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return v.Deferred(function(n){v.each(t,function(t,r){var s=r[0],o=e[t];i[r[1]](v.isFunction(o)?function(){var e=o.apply(this,arguments);e&&v.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[s+"With"](this===i?n:this,[e])}:n[s])}),e=null}).promise()},promise:function(e){return e!=null?v.extend(e,r):r}},i={};return r.pipe=r.then,v.each(t,function(e,s){var o=s[2],u=s[3];r[s[1]]=o.add,u&&o.add(function(){n=u},t[e^1][2].disable,t[2][2].lock),i[s[0]]=o.fire,i[s[0]+"With"]=o.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=l.call(arguments),r=n.length,i=r!==1||e&&v.isFunction(e.promise)?r:0,s=i===1?e:v.Deferred(),o=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?l.call(arguments):r,n===u?s.notifyWith(t,n):--i||s.resolveWith(t,n)}},u,a,f;if(r>1){u=new Array(r),a=new Array(r),f=new Array(r);for(;t
    a",n=p.getElementsByTagName("*"),r=p.getElementsByTagName("a")[0];if(!n||!r||!n.length)return{};s=i.createElement("select"),o=s.appendChild(i.createElement("option")),u=p.getElementsByTagName("input")[0],r.style.cssText="top:1px;float:left;opacity:.5",t={leadingWhitespace:p.firstChild.nodeType===3,tbody:!p.getElementsByTagName("tbody").length,htmlSerialize:!!p.getElementsByTagName("link").length,style:/top/.test(r.getAttribute("style")),hrefNormalized:r.getAttribute("href")==="/a",opacity:/^0.5/.test(r.style.opacity),cssFloat:!!r.style.cssFloat,checkOn:u.value==="on",optSelected:o.selected,getSetAttribute:p.className!=="t",enctype:!!i.createElement("form").enctype,html5Clone:i.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",boxModel:i.compatMode==="CSS1Compat",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,boxSizingReliable:!0,pixelPosition:!1},u.checked=!0,t.noCloneChecked=u.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!o.disabled;try{delete p.test}catch(d){t.deleteExpando=!1}!p.addEventListener&&p.attachEvent&&p.fireEvent&&(p.attachEvent("onclick",h=function(){t.noCloneEvent=!1}),p.cloneNode(!0).fireEvent("onclick"),p.detachEvent("onclick",h)),u=i.createElement("input"),u.value="t",u.setAttribute("type","radio"),t.radioValue=u.value==="t",u.setAttribute("checked","checked"),u.setAttribute("name","t"),p.appendChild(u),a=i.createDocumentFragment(),a.appendChild(p.lastChild),t.checkClone=a.cloneNode(!0).cloneNode(!0).lastChild.checked,t.appendChecked=u.checked,a.removeChild(u),a.appendChild(p);if(p.attachEvent)for(l in{submit:!0,change:!0,focusin:!0})f="on"+l,c=f in p,c||(p.setAttribute(f,"return;"),c=typeof p[f]=="function"),t[l+"Bubbles"]=c;return v(function(){var n,r,s,o,u="padding:0;margin:0;border:0;display:block;overflow:hidden;",a=i.getElementsByTagName("body")[0];if(!a)return;n=i.createElement("div"),n.style.cssText="visibility:hidden;border:0;width:0;height:0;position:static;top:0;margin-top:1px",a.insertBefore(n,a.firstChild),r=i.createElement("div"),n.appendChild(r),r.innerHTML="
    t
    ",s=r.getElementsByTagName("td"),s[0].style.cssText="padding:0;margin:0;border:0;display:none",c=s[0].offsetHeight===0,s[0].style.display="",s[1].style.display="none",t.reliableHiddenOffsets=c&&s[0].offsetHeight===0,r.innerHTML="",r.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",t.boxSizing=r.offsetWidth===4,t.doesNotIncludeMarginInBodyOffset=a.offsetTop!==1,e.getComputedStyle&&(t.pixelPosition=(e.getComputedStyle(r,null)||{}).top!=="1%",t.boxSizingReliable=(e.getComputedStyle(r,null)||{width:"4px"}).width==="4px",o=i.createElement("div"),o.style.cssText=r.style.cssText=u,o.style.marginRight=o.style.width="0",r.style.width="1px",r.appendChild(o),t.reliableMarginRight=!parseFloat((e.getComputedStyle(o,null)||{}).marginRight)),typeof r.style.zoom!="undefined"&&(r.innerHTML="",r.style.cssText=u+"width:1px;padding:1px;display:inline;zoom:1",t.inlineBlockNeedsLayout=r.offsetWidth===3,r.style.display="block",r.style.overflow="visible",r.innerHTML="
    ",r.firstChild.style.width="5px",t.shrinkWrapBlocks=r.offsetWidth!==3,n.style.zoom=1),a.removeChild(n),n=r=s=o=null}),a.removeChild(p),n=r=s=o=u=a=p=null,t}();var D=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,P=/([A-Z])/g;v.extend({cache:{},deletedIds:[],uuid:0,expando:"jQuery"+(v.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(e){return e=e.nodeType?v.cache[e[v.expando]]:e[v.expando],!!e&&!B(e)},data:function(e,n,r,i){if(!v.acceptData(e))return;var s,o,u=v.expando,a=typeof n=="string",f=e.nodeType,l=f?v.cache:e,c=f?e[u]:e[u]&&u;if((!c||!l[c]||!i&&!l[c].data)&&a&&r===t)return;c||(f?e[u]=c=v.deletedIds.pop()||v.guid++:c=u),l[c]||(l[c]={},f||(l[c].toJSON=v.noop));if(typeof n=="object"||typeof n=="function")i?l[c]=v.extend(l[c],n):l[c].data=v.extend(l[c].data,n);return s=l[c],i||(s.data||(s.data={}),s=s.data),r!==t&&(s[v.camelCase(n)]=r),a?(o=s[n],o==null&&(o=s[v.camelCase(n)])):o=s,o},removeData:function(e,t,n){if(!v.acceptData(e))return;var r,i,s,o=e.nodeType,u=o?v.cache:e,a=o?e[v.expando]:v.expando;if(!u[a])return;if(t){r=n?u[a]:u[a].data;if(r){v.isArray(t)||(t in r?t=[t]:(t=v.camelCase(t),t in r?t=[t]:t=t.split(" ")));for(i=0,s=t.length;i1,null,!1))},removeData:function(e){return this.each(function(){v.removeData(this,e)})}}),v.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=v._data(e,t),n&&(!r||v.isArray(n)?r=v._data(e,t,v.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=v.queue(e,t),r=n.length,i=n.shift(),s=v._queueHooks(e,t),o=function(){v.dequeue(e,t)};i==="inprogress"&&(i=n.shift(),r--),i&&(t==="fx"&&n.unshift("inprogress"),delete s.stop,i.call(e,o,s)),!r&&s&&s.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return v._data(e,n)||v._data(e,n,{empty:v.Callbacks("once memory").add(function(){v.removeData(e,t+"queue",!0),v.removeData(e,n,!0)})})}}),v.fn.extend({queue:function(e,n){var r=2;return typeof e!="string"&&(n=e,e="fx",r--),arguments.length1)},removeAttr:function(e){return this.each(function(){v.removeAttr(this,e)})},prop:function(e,t){return v.access(this,v.prop,e,t,arguments.length>1)},removeProp:function(e){return e=v.propFix[e]||e,this.each(function(){try{this[e]=t,delete this[e]}catch(n){}})},addClass:function(e){var t,n,r,i,s,o,u;if(v.isFunction(e))return this.each(function(t){v(this).addClass(e.call(this,t,this.className))});if(e&&typeof e=="string"){t=e.split(y);for(n=0,r=this.length;n=0)r=r.replace(" "+n[s]+" "," ");i.className=e?v.trim(r):""}}}return this},toggleClass:function(e,t){var n=typeof e,r=typeof t=="boolean";return v.isFunction(e)?this.each(function(n){v(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if(n==="string"){var i,s=0,o=v(this),u=t,a=e.split(y);while(i=a[s++])u=r?u:!o.hasClass(i),o[u?"addClass":"removeClass"](i)}else if(n==="undefined"||n==="boolean")this.className&&v._data(this,"__className__",this.className),this.className=this.className||e===!1?"":v._data(this,"__className__")||""})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;n=0)return!0;return!1},val:function(e){var n,r,i,s=this[0];if(!arguments.length){if(s)return n=v.valHooks[s.type]||v.valHooks[s.nodeName.toLowerCase()],n&&"get"in n&&(r=n.get(s,"value"))!==t?r:(r=s.value,typeof r=="string"?r.replace(R,""):r==null?"":r);return}return i=v.isFunction(e),this.each(function(r){var s,o=v(this);if(this.nodeType!==1)return;i?s=e.call(this,r,o.val()):s=e,s==null?s="":typeof s=="number"?s+="":v.isArray(s)&&(s=v.map(s,function(e){return e==null?"":e+""})),n=v.valHooks[this.type]||v.valHooks[this.nodeName.toLowerCase()];if(!n||!("set"in n)||n.set(this,s,"value")===t)this.value=s})}}),v.extend({valHooks:{option:{get:function(e){var t=e.attributes.value;return!t||t.specified?e.value:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,s=e.type==="select-one"||i<0,o=s?null:[],u=s?i+1:r.length,a=i<0?u:s?i:0;for(;a=0}),n.length||(e.selectedIndex=-1),n}}},attrFn:{},attr:function(e,n,r,i){var s,o,u,a=e.nodeType;if(!e||a===3||a===8||a===2)return;if(i&&v.isFunction(v.fn[n]))return v(e)[n](r);if(typeof e.getAttribute=="undefined")return v.prop(e,n,r);u=a!==1||!v.isXMLDoc(e),u&&(n=n.toLowerCase(),o=v.attrHooks[n]||(X.test(n)?F:j));if(r!==t){if(r===null){v.removeAttr(e,n);return}return o&&"set"in o&&u&&(s=o.set(e,r,n))!==t?s:(e.setAttribute(n,r+""),r)}return o&&"get"in o&&u&&(s=o.get(e,n))!==null?s:(s=e.getAttribute(n),s===null?t:s)},removeAttr:function(e,t){var n,r,i,s,o=0;if(t&&e.nodeType===1){r=t.split(y);for(;o=0}})});var $=/^(?:textarea|input|select)$/i,J=/^([^\.]*|)(?:\.(.+)|)$/,K=/(?:^|\s)hover(\.\S+|)\b/,Q=/^key/,G=/^(?:mouse|contextmenu)|click/,Y=/^(?:focusinfocus|focusoutblur)$/,Z=function(e){return v.event.special.hover?e:e.replace(K,"mouseenter$1 mouseleave$1")};v.event={add:function(e,n,r,i,s){var o,u,a,f,l,c,h,p,d,m,g;if(e.nodeType===3||e.nodeType===8||!n||!r||!(o=v._data(e)))return;r.handler&&(d=r,r=d.handler,s=d.selector),r.guid||(r.guid=v.guid++),a=o.events,a||(o.events=a={}),u=o.handle,u||(o.handle=u=function(e){return typeof v=="undefined"||!!e&&v.event.triggered===e.type?t:v.event.dispatch.apply(u.elem,arguments)},u.elem=e),n=v.trim(Z(n)).split(" ");for(f=0;f=0&&(y=y.slice(0,-1),a=!0),y.indexOf(".")>=0&&(b=y.split("."),y=b.shift(),b.sort());if((!s||v.event.customEvent[y])&&!v.event.global[y])return;n=typeof n=="object"?n[v.expando]?n:new v.Event(y,n):new v.Event(y),n.type=y,n.isTrigger=!0,n.exclusive=a,n.namespace=b.join("."),n.namespace_re=n.namespace?new RegExp("(^|\\.)"+b.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,h=y.indexOf(":")<0?"on"+y:"";if(!s){u=v.cache;for(f in u)u[f].events&&u[f].events[y]&&v.event.trigger(n,r,u[f].handle.elem,!0);return}n.result=t,n.target||(n.target=s),r=r!=null?v.makeArray(r):[],r.unshift(n),p=v.event.special[y]||{};if(p.trigger&&p.trigger.apply(s,r)===!1)return;m=[[s,p.bindType||y]];if(!o&&!p.noBubble&&!v.isWindow(s)){g=p.delegateType||y,l=Y.test(g+y)?s:s.parentNode;for(c=s;l;l=l.parentNode)m.push([l,g]),c=l;c===(s.ownerDocument||i)&&m.push([c.defaultView||c.parentWindow||e,g])}for(f=0;f=0:v.find(h,this,null,[s]).length),u[h]&&f.push(c);f.length&&w.push({elem:s,matches:f})}d.length>m&&w.push({elem:this,matches:d.slice(m)});for(r=0;r0?this.on(t,null,e,n):this.trigger(t)},Q.test(t)&&(v.event.fixHooks[t]=v.event.keyHooks),G.test(t)&&(v.event.fixHooks[t]=v.event.mouseHooks)}),function(e,t){function nt(e,t,n,r){n=n||[],t=t||g;var i,s,a,f,l=t.nodeType;if(!e||typeof e!="string")return n;if(l!==1&&l!==9)return[];a=o(t);if(!a&&!r)if(i=R.exec(e))if(f=i[1]){if(l===9){s=t.getElementById(f);if(!s||!s.parentNode)return n;if(s.id===f)return n.push(s),n}else if(t.ownerDocument&&(s=t.ownerDocument.getElementById(f))&&u(t,s)&&s.id===f)return n.push(s),n}else{if(i[2])return S.apply(n,x.call(t.getElementsByTagName(e),0)),n;if((f=i[3])&&Z&&t.getElementsByClassName)return S.apply(n,x.call(t.getElementsByClassName(f),0)),n}return vt(e.replace(j,"$1"),t,n,r,a)}function rt(e){return function(t){var n=t.nodeName.toLowerCase();return n==="input"&&t.type===e}}function it(e){return function(t){var n=t.nodeName.toLowerCase();return(n==="input"||n==="button")&&t.type===e}}function st(e){return N(function(t){return t=+t,N(function(n,r){var i,s=e([],n.length,t),o=s.length;while(o--)n[i=s[o]]&&(n[i]=!(r[i]=n[i]))})})}function ot(e,t,n){if(e===t)return n;var r=e.nextSibling;while(r){if(r===t)return-1;r=r.nextSibling}return 1}function ut(e,t){var n,r,s,o,u,a,f,l=L[d][e+" "];if(l)return t?0:l.slice(0);u=e,a=[],f=i.preFilter;while(u){if(!n||(r=F.exec(u)))r&&(u=u.slice(r[0].length)||u),a.push(s=[]);n=!1;if(r=I.exec(u))s.push(n=new m(r.shift())),u=u.slice(n.length),n.type=r[0].replace(j," ");for(o in i.filter)(r=J[o].exec(u))&&(!f[o]||(r=f[o](r)))&&(s.push(n=new m(r.shift())),u=u.slice(n.length),n.type=o,n.matches=r);if(!n)break}return t?u.length:u?nt.error(e):L(e,a).slice(0)}function at(e,t,r){var i=t.dir,s=r&&t.dir==="parentNode",o=w++;return t.first?function(t,n,r){while(t=t[i])if(s||t.nodeType===1)return e(t,n,r)}:function(t,r,u){if(!u){var a,f=b+" "+o+" ",l=f+n;while(t=t[i])if(s||t.nodeType===1){if((a=t[d])===l)return t.sizset;if(typeof a=="string"&&a.indexOf(f)===0){if(t.sizset)return t}else{t[d]=l;if(e(t,r,u))return t.sizset=!0,t;t.sizset=!1}}}else while(t=t[i])if(s||t.nodeType===1)if(e(t,r,u))return t}}function ft(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function lt(e,t,n,r,i){var s,o=[],u=0,a=e.length,f=t!=null;for(;u-1&&(s[f]=!(o[f]=c))}}else g=lt(g===o?g.splice(d,g.length):g),i?i(null,o,g,a):S.apply(o,g)})}function ht(e){var t,n,r,s=e.length,o=i.relative[e[0].type],u=o||i.relative[" "],a=o?1:0,f=at(function(e){return e===t},u,!0),l=at(function(e){return T.call(t,e)>-1},u,!0),h=[function(e,n,r){return!o&&(r||n!==c)||((t=n).nodeType?f(e,n,r):l(e,n,r))}];for(;a1&&ft(h),a>1&&e.slice(0,a-1).join("").replace(j,"$1"),n,a0,s=e.length>0,o=function(u,a,f,l,h){var p,d,v,m=[],y=0,w="0",x=u&&[],T=h!=null,N=c,C=u||s&&i.find.TAG("*",h&&a.parentNode||a),k=b+=N==null?1:Math.E;T&&(c=a!==g&&a,n=o.el);for(;(p=C[w])!=null;w++){if(s&&p){for(d=0;v=e[d];d++)if(v(p,a,f)){l.push(p);break}T&&(b=k,n=++o.el)}r&&((p=!v&&p)&&y--,u&&x.push(p))}y+=w;if(r&&w!==y){for(d=0;v=t[d];d++)v(x,m,a,f);if(u){if(y>0)while(w--)!x[w]&&!m[w]&&(m[w]=E.call(l));m=lt(m)}S.apply(l,m),T&&!u&&m.length>0&&y+t.length>1&&nt.uniqueSort(l)}return T&&(b=k,c=N),x};return o.el=0,r?N(o):o}function dt(e,t,n){var r=0,i=t.length;for(;r2&&(f=u[0]).type==="ID"&&t.nodeType===9&&!s&&i.relative[u[1].type]){t=i.find.ID(f.matches[0].replace($,""),t,s)[0];if(!t)return n;e=e.slice(u.shift().length)}for(o=J.POS.test(e)?-1:u.length-1;o>=0;o--){f=u[o];if(i.relative[l=f.type])break;if(c=i.find[l])if(r=c(f.matches[0].replace($,""),z.test(u[0].type)&&t.parentNode||t,s)){u.splice(o,1),e=r.length&&u.join("");if(!e)return S.apply(n,x.call(r,0)),n;break}}}return a(e,h)(r,t,s,n,z.test(e)),n}function mt(){}var n,r,i,s,o,u,a,f,l,c,h=!0,p="undefined",d=("sizcache"+Math.random()).replace(".",""),m=String,g=e.document,y=g.documentElement,b=0,w=0,E=[].pop,S=[].push,x=[].slice,T=[].indexOf||function(e){var t=0,n=this.length;for(;ti.cacheLength&&delete e[t.shift()],e[n+" "]=r},e)},k=C(),L=C(),A=C(),O="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[-\\w]|[^\\x00-\\xa0])+",_=M.replace("w","w#"),D="([*^$|!~]?=)",P="\\["+O+"*("+M+")"+O+"*(?:"+D+O+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+_+")|)|)"+O+"*\\]",H=":("+M+")(?:\\((?:(['\"])((?:\\\\.|[^\\\\])*?)\\2|([^()[\\]]*|(?:(?:"+P+")|[^:]|\\\\.)*|.*))\\)|)",B=":(even|odd|eq|gt|lt|nth|first|last)(?:\\("+O+"*((?:-\\d)?\\d*)"+O+"*\\)|)(?=[^-]|$)",j=new RegExp("^"+O+"+|((?:^|[^\\\\])(?:\\\\.)*)"+O+"+$","g"),F=new RegExp("^"+O+"*,"+O+"*"),I=new RegExp("^"+O+"*([\\x20\\t\\r\\n\\f>+~])"+O+"*"),q=new RegExp(H),R=/^(?:#([\w\-]+)|(\w+)|\.([\w\-]+))$/,U=/^:not/,z=/[\x20\t\r\n\f]*[+~]/,W=/:not\($/,X=/h\d/i,V=/input|select|textarea|button/i,$=/\\(?!\\)/g,J={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),NAME:new RegExp("^\\[name=['\"]?("+M+")['\"]?\\]"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+H),POS:new RegExp(B,"i"),CHILD:new RegExp("^:(only|nth|first|last)-child(?:\\("+O+"*(even|odd|(([+-]|)(\\d*)n|)"+O+"*(?:([+-]|)"+O+"*(\\d+)|))"+O+"*\\)|)","i"),needsContext:new RegExp("^"+O+"*[>+~]|"+B,"i")},K=function(e){var t=g.createElement("div");try{return e(t)}catch(n){return!1}finally{t=null}},Q=K(function(e){return e.appendChild(g.createComment("")),!e.getElementsByTagName("*").length}),G=K(function(e){return e.innerHTML="",e.firstChild&&typeof e.firstChild.getAttribute!==p&&e.firstChild.getAttribute("href")==="#"}),Y=K(function(e){e.innerHTML="";var t=typeof e.lastChild.getAttribute("multiple");return t!=="boolean"&&t!=="string"}),Z=K(function(e){return e.innerHTML="",!e.getElementsByClassName||!e.getElementsByClassName("e").length?!1:(e.lastChild.className="e",e.getElementsByClassName("e").length===2)}),et=K(function(e){e.id=d+0,e.innerHTML="
    ",y.insertBefore(e,y.firstChild);var t=g.getElementsByName&&g.getElementsByName(d).length===2+g.getElementsByName(d+0).length;return r=!g.getElementById(d),y.removeChild(e),t});try{x.call(y.childNodes,0)[0].nodeType}catch(tt){x=function(e){var t,n=[];for(;t=this[e];e++)n.push(t);return n}}nt.matches=function(e,t){return nt(e,null,null,t)},nt.matchesSelector=function(e,t){return nt(t,null,null,[e]).length>0},s=nt.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(i===1||i===9||i===11){if(typeof e.textContent=="string")return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=s(e)}else if(i===3||i===4)return e.nodeValue}else for(;t=e[r];r++)n+=s(t);return n},o=nt.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?t.nodeName!=="HTML":!1},u=nt.contains=y.contains?function(e,t){var n=e.nodeType===9?e.documentElement:e,r=t&&t.parentNode;return e===r||!!(r&&r.nodeType===1&&n.contains&&n.contains(r))}:y.compareDocumentPosition?function(e,t){return t&&!!(e.compareDocumentPosition(t)&16)}:function(e,t){while(t=t.parentNode)if(t===e)return!0;return!1},nt.attr=function(e,t){var n,r=o(e);return r||(t=t.toLowerCase()),(n=i.attrHandle[t])?n(e):r||Y?e.getAttribute(t):(n=e.getAttributeNode(t),n?typeof e[t]=="boolean"?e[t]?t:null:n.specified?n.value:null:null)},i=nt.selectors={cacheLength:50,createPseudo:N,match:J,attrHandle:G?{}:{href:function(e){return e.getAttribute("href",2)},type:function(e){return e.getAttribute("type")}},find:{ID:r?function(e,t,n){if(typeof t.getElementById!==p&&!n){var r=t.getElementById(e);return r&&r.parentNode?[r]:[]}}:function(e,n,r){if(typeof n.getElementById!==p&&!r){var i=n.getElementById(e);return i?i.id===e||typeof i.getAttributeNode!==p&&i.getAttributeNode("id").value===e?[i]:t:[]}},TAG:Q?function(e,t){if(typeof t.getElementsByTagName!==p)return t.getElementsByTagName(e)}:function(e,t){var n=t.getElementsByTagName(e);if(e==="*"){var r,i=[],s=0;for(;r=n[s];s++)r.nodeType===1&&i.push(r);return i}return n},NAME:et&&function(e,t){if(typeof t.getElementsByName!==p)return t.getElementsByName(name)},CLASS:Z&&function(e,t,n){if(typeof t.getElementsByClassName!==p&&!n)return t.getElementsByClassName(e)}},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace($,""),e[3]=(e[4]||e[5]||"").replace($,""),e[2]==="~="&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),e[1]==="nth"?(e[2]||nt.error(e[0]),e[3]=+(e[3]?e[4]+(e[5]||1):2*(e[2]==="even"||e[2]==="odd")),e[4]=+(e[6]+e[7]||e[2]==="odd")):e[2]&&nt.error(e[0]),e},PSEUDO:function(e){var t,n;if(J.CHILD.test(e[0]))return null;if(e[3])e[2]=e[3];else if(t=e[4])q.test(t)&&(n=ut(t,!0))&&(n=t.indexOf(")",t.length-n)-t.length)&&(t=t.slice(0,n),e[0]=e[0].slice(0,n)),e[2]=t;return e.slice(0,3)}},filter:{ID:r?function(e){return e=e.replace($,""),function(t){return t.getAttribute("id")===e}}:function(e){return e=e.replace($,""),function(t){var n=typeof t.getAttributeNode!==p&&t.getAttributeNode("id");return n&&n.value===e}},TAG:function(e){return e==="*"?function(){return!0}:(e=e.replace($,"").toLowerCase(),function(t){return t.nodeName&&t.nodeName.toLowerCase()===e})},CLASS:function(e){var t=k[d][e+" "];return t||(t=new RegExp("(^|"+O+")"+e+"("+O+"|$)"))&&k(e,function(e){return t.test(e.className||typeof e.getAttribute!==p&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r,i){var s=nt.attr(r,e);return s==null?t==="!=":t?(s+="",t==="="?s===n:t==="!="?s!==n:t==="^="?n&&s.indexOf(n)===0:t==="*="?n&&s.indexOf(n)>-1:t==="$="?n&&s.substr(s.length-n.length)===n:t==="~="?(" "+s+" ").indexOf(n)>-1:t==="|="?s===n||s.substr(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r){return e==="nth"?function(e){var t,i,s=e.parentNode;if(n===1&&r===0)return!0;if(s){i=0;for(t=s.firstChild;t;t=t.nextSibling)if(t.nodeType===1){i++;if(e===t)break}}return i-=r,i===n||i%n===0&&i/n>=0}:function(t){var n=t;switch(e){case"only":case"first":while(n=n.previousSibling)if(n.nodeType===1)return!1;if(e==="first")return!0;n=t;case"last":while(n=n.nextSibling)if(n.nodeType===1)return!1;return!0}}},PSEUDO:function(e,t){var n,r=i.pseudos[e]||i.setFilters[e.toLowerCase()]||nt.error("unsupported pseudo: "+e);return r[d]?r(t):r.length>1?(n=[e,e,"",t],i.setFilters.hasOwnProperty(e.toLowerCase())?N(function(e,n){var i,s=r(e,t),o=s.length;while(o--)i=T.call(e,s[o]),e[i]=!(n[i]=s[o])}):function(e){return r(e,0,n)}):r}},pseudos:{not:N(function(e){var t=[],n=[],r=a(e.replace(j,"$1"));return r[d]?N(function(e,t,n,i){var s,o=r(e,null,i,[]),u=e.length;while(u--)if(s=o[u])e[u]=!(t[u]=s)}):function(e,i,s){return t[0]=e,r(t,null,s,n),!n.pop()}}),has:N(function(e){return function(t){return nt(e,t).length>0}}),contains:N(function(e){return function(t){return(t.textContent||t.innerText||s(t)).indexOf(e)>-1}}),enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return t==="input"&&!!e.checked||t==="option"&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},parent:function(e){return!i.pseudos.empty(e)},empty:function(e){var t;e=e.firstChild;while(e){if(e.nodeName>"@"||(t=e.nodeType)===3||t===4)return!1;e=e.nextSibling}return!0},header:function(e){return X.test(e.nodeName)},text:function(e){var t,n;return e.nodeName.toLowerCase()==="input"&&(t=e.type)==="text"&&((n=e.getAttribute("type"))==null||n.toLowerCase()===t)},radio:rt("radio"),checkbox:rt("checkbox"),file:rt("file"),password:rt("password"),image:rt("image"),submit:it("submit"),reset:it("reset"),button:function(e){var t=e.nodeName.toLowerCase();return t==="input"&&e.type==="button"||t==="button"},input:function(e){return V.test(e.nodeName)},focus:function(e){var t=e.ownerDocument;return e===t.activeElement&&(!t.hasFocus||t.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},active:function(e){return e===e.ownerDocument.activeElement},first:st(function(){return[0]}),last:st(function(e,t){return[t-1]}),eq:st(function(e,t,n){return[n<0?n+t:n]}),even:st(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:st(function(e,t,n){for(var r=n<0?n+t:n;++r",e.querySelectorAll("[selected]").length||i.push("\\["+O+"*(?:checked|disabled|ismap|multiple|readonly|selected|value)"),e.querySelectorAll(":checked").length||i.push(":checked")}),K(function(e){e.innerHTML="

    ",e.querySelectorAll("[test^='']").length&&i.push("[*^$]="+O+"*(?:\"\"|'')"),e.innerHTML="",e.querySelectorAll(":enabled").length||i.push(":enabled",":disabled")}),i=new RegExp(i.join("|")),vt=function(e,r,s,o,u){if(!o&&!u&&!i.test(e)){var a,f,l=!0,c=d,h=r,p=r.nodeType===9&&e;if(r.nodeType===1&&r.nodeName.toLowerCase()!=="object"){a=ut(e),(l=r.getAttribute("id"))?c=l.replace(n,"\\$&"):r.setAttribute("id",c),c="[id='"+c+"'] ",f=a.length;while(f--)a[f]=c+a[f].join("");h=z.test(e)&&r.parentNode||r,p=a.join(",")}if(p)try{return S.apply(s,x.call(h.querySelectorAll(p),0)),s}catch(v){}finally{l||r.removeAttribute("id")}}return t(e,r,s,o,u)},u&&(K(function(t){e=u.call(t,"div");try{u.call(t,"[test!='']:sizzle"),s.push("!=",H)}catch(n){}}),s=new RegExp(s.join("|")),nt.matchesSelector=function(t,n){n=n.replace(r,"='$1']");if(!o(t)&&!s.test(n)&&!i.test(n))try{var a=u.call(t,n);if(a||e||t.document&&t.document.nodeType!==11)return a}catch(f){}return nt(n,null,null,[t]).length>0})}(),i.pseudos.nth=i.pseudos.eq,i.filters=mt.prototype=i.pseudos,i.setFilters=new mt,nt.attr=v.attr,v.find=nt,v.expr=nt.selectors,v.expr[":"]=v.expr.pseudos,v.unique=nt.uniqueSort,v.text=nt.getText,v.isXMLDoc=nt.isXML,v.contains=nt.contains}(e);var nt=/Until$/,rt=/^(?:parents|prev(?:Until|All))/,it=/^.[^:#\[\.,]*$/,st=v.expr.match.needsContext,ot={children:!0,contents:!0,next:!0,prev:!0};v.fn.extend({find:function(e){var t,n,r,i,s,o,u=this;if(typeof e!="string")return v(e).filter(function(){for(t=0,n=u.length;t0)for(i=r;i=0:v.filter(e,this).length>0:this.filter(e).length>0)},closest:function(e,t){var n,r=0,i=this.length,s=[],o=st.test(e)||typeof e!="string"?v(e,t||this.context):0;for(;r-1:v.find.matchesSelector(n,e)){s.push(n);break}n=n.parentNode}}return s=s.length>1?v.unique(s):s,this.pushStack(s,"closest",e)},index:function(e){return e?typeof e=="string"?v.inArray(this[0],v(e)):v.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.prevAll().length:-1},add:function(e,t){var n=typeof e=="string"?v(e,t):v.makeArray(e&&e.nodeType?[e]:e),r=v.merge(this.get(),n);return this.pushStack(ut(n[0])||ut(r[0])?r:v.unique(r))},addBack:function(e){return this.add(e==null?this.prevObject:this.prevObject.filter(e))}}),v.fn.andSelf=v.fn.addBack,v.each({parent:function(e){var t=e.parentNode;return t&&t.nodeType!==11?t:null},parents:function(e){return v.dir(e,"parentNode")},parentsUntil:function(e,t,n){return v.dir(e,"parentNode",n)},next:function(e){return at(e,"nextSibling")},prev:function(e){return at(e,"previousSibling")},nextAll:function(e){return v.dir(e,"nextSibling")},prevAll:function(e){return v.dir(e,"previousSibling")},nextUntil:function(e,t,n){return v.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return v.dir(e,"previousSibling",n)},siblings:function(e){return v.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return v.sibling(e.firstChild)},contents:function(e){return v.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:v.merge([],e.childNodes)}},function(e,t){v.fn[e]=function(n,r){var i=v.map(this,t,n);return nt.test(e)||(r=n),r&&typeof r=="string"&&(i=v.filter(r,i)),i=this.length>1&&!ot[e]?v.unique(i):i,this.length>1&&rt.test(e)&&(i=i.reverse()),this.pushStack(i,e,l.call(arguments).join(","))}}),v.extend({filter:function(e,t,n){return n&&(e=":not("+e+")"),t.length===1?v.find.matchesSelector(t[0],e)?[t[0]]:[]:v.find.matches(e,t)},dir:function(e,n,r){var i=[],s=e[n];while(s&&s.nodeType!==9&&(r===t||s.nodeType!==1||!v(s).is(r)))s.nodeType===1&&i.push(s),s=s[n];return i},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)e.nodeType===1&&e!==t&&n.push(e);return n}});var ct="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",ht=/ jQuery\d+="(?:null|\d+)"/g,pt=/^\s+/,dt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,vt=/<([\w:]+)/,mt=/]","i"),Et=/^(?:checkbox|radio)$/,St=/checked\s*(?:[^=]|=\s*.checked.)/i,xt=/\/(java|ecma)script/i,Tt=/^\s*\s*$/g,Nt={option:[1,""],legend:[1,"
    ","
    "],thead:[1,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],col:[2,"","
    "],area:[1,"",""],_default:[0,"",""]},Ct=lt(i),kt=Ct.appendChild(i.createElement("div"));Nt.optgroup=Nt.option,Nt.tbody=Nt.tfoot=Nt.colgroup=Nt.caption=Nt.thead,Nt.th=Nt.td,v.support.htmlSerialize||(Nt._default=[1,"X
    ","
    "]),v.fn.extend({text:function(e){return v.access(this,function(e){return e===t?v.text(this):this.empty().append((this[0]&&this[0].ownerDocument||i).createTextNode(e))},null,e,arguments.length)},wrapAll:function(e){if(v.isFunction(e))return this.each(function(t){v(this).wrapAll(e.call(this,t))});if(this[0]){var t=v(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstChild&&e.firstChild.nodeType===1)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return v.isFunction(e)?this.each(function(t){v(this).wrapInner(e.call(this,t))}):this.each(function(){var t=v(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=v.isFunction(e);return this.each(function(n){v(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){v.nodeName(this,"body")||v(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(e){(this.nodeType===1||this.nodeType===11)&&this.appendChild(e)})},prepend:function(){return this.domManip(arguments,!0,function(e){(this.nodeType===1||this.nodeType===11)&&this.insertBefore(e,this.firstChild)})},before:function(){if(!ut(this[0]))return this.domManip(arguments,!1,function(e){this.parentNode.insertBefore(e,this)});if(arguments.length){var e=v.clean(arguments);return this.pushStack(v.merge(e,this),"before",this.selector)}},after:function(){if(!ut(this[0]))return this.domManip(arguments,!1,function(e){this.parentNode.insertBefore(e,this.nextSibling)});if(arguments.length){var e=v.clean(arguments);return this.pushStack(v.merge(this,e),"after",this.selector)}},remove:function(e,t){var n,r=0;for(;(n=this[r])!=null;r++)if(!e||v.filter(e,[n]).length)!t&&n.nodeType===1&&(v.cleanData(n.getElementsByTagName("*")),v.cleanData([n])),n.parentNode&&n.parentNode.removeChild(n);return this},empty:function(){var e,t=0;for(;(e=this[t])!=null;t++){e.nodeType===1&&v.cleanData(e.getElementsByTagName("*"));while(e.firstChild)e.removeChild(e.firstChild)}return this},clone:function(e,t){return e=e==null?!1:e,t=t==null?e:t,this.map(function(){return v.clone(this,e,t)})},html:function(e){return v.access(this,function(e){var n=this[0]||{},r=0,i=this.length;if(e===t)return n.nodeType===1?n.innerHTML.replace(ht,""):t;if(typeof e=="string"&&!yt.test(e)&&(v.support.htmlSerialize||!wt.test(e))&&(v.support.leadingWhitespace||!pt.test(e))&&!Nt[(vt.exec(e)||["",""])[1].toLowerCase()]){e=e.replace(dt,"<$1>");try{for(;r1&&typeof f=="string"&&St.test(f))return this.each(function(){v(this).domManip(e,n,r)});if(v.isFunction(f))return this.each(function(i){var s=v(this);e[0]=f.call(this,i,n?s.html():t),s.domManip(e,n,r)});if(this[0]){i=v.buildFragment(e,this,l),o=i.fragment,s=o.firstChild,o.childNodes.length===1&&(o=s);if(s){n=n&&v.nodeName(s,"tr");for(u=i.cacheable||c-1;a0?this.clone(!0):this).get(),v(o[i])[t](r),s=s.concat(r);return this.pushStack(s,e,o.selector)}}),v.extend({clone:function(e,t,n){var r,i,s,o;v.support.html5Clone||v.isXMLDoc(e)||!wt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(kt.innerHTML=e.outerHTML,kt.removeChild(o=kt.firstChild));if((!v.support.noCloneEvent||!v.support.noCloneChecked)&&(e.nodeType===1||e.nodeType===11)&&!v.isXMLDoc(e)){Ot(e,o),r=Mt(e),i=Mt(o);for(s=0;r[s];++s)i[s]&&Ot(r[s],i[s])}if(t){At(e,o);if(n){r=Mt(e),i=Mt(o);for(s=0;r[s];++s)At(r[s],i[s])}}return r=i=null,o},clean:function(e,t,n,r){var s,o,u,a,f,l,c,h,p,d,m,g,y=t===i&&Ct,b=[];if(!t||typeof t.createDocumentFragment=="undefined")t=i;for(s=0;(u=e[s])!=null;s++){typeof u=="number"&&(u+="");if(!u)continue;if(typeof u=="string")if(!gt.test(u))u=t.createTextNode(u);else{y=y||lt(t),c=t.createElement("div"),y.appendChild(c),u=u.replace(dt,"<$1>"),a=(vt.exec(u)||["",""])[1].toLowerCase(),f=Nt[a]||Nt._default,l=f[0],c.innerHTML=f[1]+u+f[2];while(l--)c=c.lastChild;if(!v.support.tbody){h=mt.test(u),p=a==="table"&&!h?c.firstChild&&c.firstChild.childNodes:f[1]===""&&!h?c.childNodes:[];for(o=p.length-1;o>=0;--o)v.nodeName(p[o],"tbody")&&!p[o].childNodes.length&&p[o].parentNode.removeChild(p[o])}!v.support.leadingWhitespace&&pt.test(u)&&c.insertBefore(t.createTextNode(pt.exec(u)[0]),c.firstChild),u=c.childNodes,c.parentNode.removeChild(c)}u.nodeType?b.push(u):v.merge(b,u)}c&&(u=c=y=null);if(!v.support.appendChecked)for(s=0;(u=b[s])!=null;s++)v.nodeName(u,"input")?_t(u):typeof u.getElementsByTagName!="undefined"&&v.grep(u.getElementsByTagName("input"),_t);if(n){m=function(e){if(!e.type||xt.test(e.type))return r?r.push(e.parentNode?e.parentNode.removeChild(e):e):n.appendChild(e)};for(s=0;(u=b[s])!=null;s++)if(!v.nodeName(u,"script")||!m(u))n.appendChild(u),typeof u.getElementsByTagName!="undefined"&&(g=v.grep(v.merge([],u.getElementsByTagName("script")),m),b.splice.apply(b,[s+1,0].concat(g)),s+=g.length)}return b},cleanData:function(e,t){var n,r,i,s,o=0,u=v.expando,a=v.cache,f=v.support.deleteExpando,l=v.event.special;for(;(i=e[o])!=null;o++)if(t||v.acceptData(i)){r=i[u],n=r&&a[r];if(n){if(n.events)for(s in n.events)l[s]?v.event.remove(i,s):v.removeEvent(i,s,n.handle);a[r]&&(delete a[r],f?delete i[u]:i.removeAttribute?i.removeAttribute(u):i[u]=null,v.deletedIds.push(r))}}}}),function(){var e,t;v.uaMatch=function(e){e=e.toLowerCase();var t=/(chrome)[ \/]([\w.]+)/.exec(e)||/(webkit)[ \/]([\w.]+)/.exec(e)||/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(e)||/(msie) ([\w.]+)/.exec(e)||e.indexOf("compatible")<0&&/(mozilla)(?:.*? rv:([\w.]+)|)/.exec(e)||[];return{browser:t[1]||"",version:t[2]||"0"}},e=v.uaMatch(o.userAgent),t={},e.browser&&(t[e.browser]=!0,t.version=e.version),t.chrome?t.webkit=!0:t.webkit&&(t.safari=!0),v.browser=t,v.sub=function(){function e(t,n){return new e.fn.init(t,n)}v.extend(!0,e,this),e.superclass=this,e.fn=e.prototype=this(),e.fn.constructor=e,e.sub=this.sub,e.fn.init=function(r,i){return i&&i instanceof v&&!(i instanceof e)&&(i=e(i)),v.fn.init.call(this,r,i,t)},e.fn.init.prototype=e.fn;var t=e(i);return e}}();var Dt,Pt,Ht,Bt=/alpha\([^)]*\)/i,jt=/opacity=([^)]*)/,Ft=/^(top|right|bottom|left)$/,It=/^(none|table(?!-c[ea]).+)/,qt=/^margin/,Rt=new RegExp("^("+m+")(.*)$","i"),Ut=new RegExp("^("+m+")(?!px)[a-z%]+$","i"),zt=new RegExp("^([-+])=("+m+")","i"),Wt={BODY:"block"},Xt={position:"absolute",visibility:"hidden",display:"block"},Vt={letterSpacing:0,fontWeight:400},$t=["Top","Right","Bottom","Left"],Jt=["Webkit","O","Moz","ms"],Kt=v.fn.toggle;v.fn.extend({css:function(e,n){return v.access(this,function(e,n,r){return r!==t?v.style(e,n,r):v.css(e,n)},e,n,arguments.length>1)},show:function(){return Yt(this,!0)},hide:function(){return Yt(this)},toggle:function(e,t){var n=typeof e=="boolean";return v.isFunction(e)&&v.isFunction(t)?Kt.apply(this,arguments):this.each(function(){(n?e:Gt(this))?v(this).show():v(this).hide()})}}),v.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Dt(e,"opacity");return n===""?"1":n}}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":v.support.cssFloat?"cssFloat":"styleFloat"},style:function(e,n,r,i){if(!e||e.nodeType===3||e.nodeType===8||!e.style)return;var s,o,u,a=v.camelCase(n),f=e.style;n=v.cssProps[a]||(v.cssProps[a]=Qt(f,a)),u=v.cssHooks[n]||v.cssHooks[a];if(r===t)return u&&"get"in u&&(s=u.get(e,!1,i))!==t?s:f[n];o=typeof r,o==="string"&&(s=zt.exec(r))&&(r=(s[1]+1)*s[2]+parseFloat(v.css(e,n)),o="number");if(r==null||o==="number"&&isNaN(r))return;o==="number"&&!v.cssNumber[a]&&(r+="px");if(!u||!("set"in u)||(r=u.set(e,r,i))!==t)try{f[n]=r}catch(l){}},css:function(e,n,r,i){var s,o,u,a=v.camelCase(n);return n=v.cssProps[a]||(v.cssProps[a]=Qt(e.style,a)),u=v.cssHooks[n]||v.cssHooks[a],u&&"get"in u&&(s=u.get(e,!0,i)),s===t&&(s=Dt(e,n)),s==="normal"&&n in Vt&&(s=Vt[n]),r||i!==t?(o=parseFloat(s),r||v.isNumeric(o)?o||0:s):s},swap:function(e,t,n){var r,i,s={};for(i in t)s[i]=e.style[i],e.style[i]=t[i];r=n.call(e);for(i in t)e.style[i]=s[i];return r}}),e.getComputedStyle?Dt=function(t,n){var r,i,s,o,u=e.getComputedStyle(t,null),a=t.style;return u&&(r=u.getPropertyValue(n)||u[n],r===""&&!v.contains(t.ownerDocument,t)&&(r=v.style(t,n)),Ut.test(r)&&qt.test(n)&&(i=a.width,s=a.minWidth,o=a.maxWidth,a.minWidth=a.maxWidth=a.width=r,r=u.width,a.width=i,a.minWidth=s,a.maxWidth=o)),r}:i.documentElement.currentStyle&&(Dt=function(e,t){var n,r,i=e.currentStyle&&e.currentStyle[t],s=e.style;return i==null&&s&&s[t]&&(i=s[t]),Ut.test(i)&&!Ft.test(t)&&(n=s.left,r=e.runtimeStyle&&e.runtimeStyle.left,r&&(e.runtimeStyle.left=e.currentStyle.left),s.left=t==="fontSize"?"1em":i,i=s.pixelLeft+"px",s.left=n,r&&(e.runtimeStyle.left=r)),i===""?"auto":i}),v.each(["height","width"],function(e,t){v.cssHooks[t]={get:function(e,n,r){if(n)return e.offsetWidth===0&&It.test(Dt(e,"display"))?v.swap(e,Xt,function(){return tn(e,t,r)}):tn(e,t,r)},set:function(e,n,r){return Zt(e,n,r?en(e,t,r,v.support.boxSizing&&v.css(e,"boxSizing")==="border-box"):0)}}}),v.support.opacity||(v.cssHooks.opacity={get:function(e,t){return jt.test((t&&e.currentStyle?e.currentStyle.filter:e.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":t?"1":""},set:function(e,t){var n=e.style,r=e.currentStyle,i=v.isNumeric(t)?"alpha(opacity="+t*100+")":"",s=r&&r.filter||n.filter||"";n.zoom=1;if(t>=1&&v.trim(s.replace(Bt,""))===""&&n.removeAttribute){n.removeAttribute("filter");if(r&&!r.filter)return}n.filter=Bt.test(s)?s.replace(Bt,i):s+" "+i}}),v(function(){v.support.reliableMarginRight||(v.cssHooks.marginRight={get:function(e,t){return v.swap(e,{display:"inline-block"},function(){if(t)return Dt(e,"marginRight")})}}),!v.support.pixelPosition&&v.fn.position&&v.each(["top","left"],function(e,t){v.cssHooks[t]={get:function(e,n){if(n){var r=Dt(e,t);return Ut.test(r)?v(e).position()[t]+"px":r}}}})}),v.expr&&v.expr.filters&&(v.expr.filters.hidden=function(e){return e.offsetWidth===0&&e.offsetHeight===0||!v.support.reliableHiddenOffsets&&(e.style&&e.style.display||Dt(e,"display"))==="none"},v.expr.filters.visible=function(e){return!v.expr.filters.hidden(e)}),v.each({margin:"",padding:"",border:"Width"},function(e,t){v.cssHooks[e+t]={expand:function(n){var r,i=typeof n=="string"?n.split(" "):[n],s={};for(r=0;r<4;r++)s[e+$t[r]+t]=i[r]||i[r-2]||i[0];return s}},qt.test(e)||(v.cssHooks[e+t].set=Zt)});var rn=/%20/g,sn=/\[\]$/,on=/\r?\n/g,un=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,an=/^(?:select|textarea)/i;v.fn.extend({serialize:function(){return v.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?v.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||an.test(this.nodeName)||un.test(this.type))}).map(function(e,t){var n=v(this).val();return n==null?null:v.isArray(n)?v.map(n,function(e,n){return{name:t.name,value:e.replace(on,"\r\n")}}):{name:t.name,value:n.replace(on,"\r\n")}}).get()}}),v.param=function(e,n){var r,i=[],s=function(e,t){t=v.isFunction(t)?t():t==null?"":t,i[i.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};n===t&&(n=v.ajaxSettings&&v.ajaxSettings.traditional);if(v.isArray(e)||e.jquery&&!v.isPlainObject(e))v.each(e,function(){s(this.name,this.value)});else for(r in e)fn(r,e[r],n,s);return i.join("&").replace(rn,"+")};var ln,cn,hn=/#.*$/,pn=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,dn=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,vn=/^(?:GET|HEAD)$/,mn=/^\/\//,gn=/\?/,yn=/)<[^<]*)*<\/script>/gi,bn=/([?&])_=[^&]*/,wn=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,En=v.fn.load,Sn={},xn={},Tn=["*/"]+["*"];try{cn=s.href}catch(Nn){cn=i.createElement("a"),cn.href="",cn=cn.href}ln=wn.exec(cn.toLowerCase())||[],v.fn.load=function(e,n,r){if(typeof e!="string"&&En)return En.apply(this,arguments);if(!this.length)return this;var i,s,o,u=this,a=e.indexOf(" ");return a>=0&&(i=e.slice(a,e.length),e=e.slice(0,a)),v.isFunction(n)?(r=n,n=t):n&&typeof n=="object"&&(s="POST"),v.ajax({url:e,type:s,dataType:"html",data:n,complete:function(e,t){r&&u.each(r,o||[e.responseText,t,e])}}).done(function(e){o=arguments,u.html(i?v("
    ").append(e.replace(yn,"")).find(i):e)}),this},v.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(e,t){v.fn[t]=function(e){return this.on(t,e)}}),v.each(["get","post"],function(e,n){v[n]=function(e,r,i,s){return v.isFunction(r)&&(s=s||i,i=r,r=t),v.ajax({type:n,url:e,data:r,success:i,dataType:s})}}),v.extend({getScript:function(e,n){return v.get(e,t,n,"script")},getJSON:function(e,t,n){return v.get(e,t,n,"json")},ajaxSetup:function(e,t){return t?Ln(e,v.ajaxSettings):(t=e,e=v.ajaxSettings),Ln(e,t),e},ajaxSettings:{url:cn,isLocal:dn.test(ln[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":Tn},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":e.String,"text html":!0,"text json":v.parseJSON,"text xml":v.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:Cn(Sn),ajaxTransport:Cn(xn),ajax:function(e,n){function T(e,n,s,a){var l,y,b,w,S,T=n;if(E===2)return;E=2,u&&clearTimeout(u),o=t,i=a||"",x.readyState=e>0?4:0,s&&(w=An(c,x,s));if(e>=200&&e<300||e===304)c.ifModified&&(S=x.getResponseHeader("Last-Modified"),S&&(v.lastModified[r]=S),S=x.getResponseHeader("Etag"),S&&(v.etag[r]=S)),e===304?(T="notmodified",l=!0):(l=On(c,w),T=l.state,y=l.data,b=l.error,l=!b);else{b=T;if(!T||e)T="error",e<0&&(e=0)}x.status=e,x.statusText=(n||T)+"",l?d.resolveWith(h,[y,T,x]):d.rejectWith(h,[x,T,b]),x.statusCode(g),g=t,f&&p.trigger("ajax"+(l?"Success":"Error"),[x,c,l?y:b]),m.fireWith(h,[x,T]),f&&(p.trigger("ajaxComplete",[x,c]),--v.active||v.event.trigger("ajaxStop"))}typeof e=="object"&&(n=e,e=t),n=n||{};var r,i,s,o,u,a,f,l,c=v.ajaxSetup({},n),h=c.context||c,p=h!==c&&(h.nodeType||h instanceof v)?v(h):v.event,d=v.Deferred(),m=v.Callbacks("once memory"),g=c.statusCode||{},b={},w={},E=0,S="canceled",x={readyState:0,setRequestHeader:function(e,t){if(!E){var n=e.toLowerCase();e=w[n]=w[n]||e,b[e]=t}return this},getAllResponseHeaders:function(){return E===2?i:null},getResponseHeader:function(e){var n;if(E===2){if(!s){s={};while(n=pn.exec(i))s[n[1].toLowerCase()]=n[2]}n=s[e.toLowerCase()]}return n===t?null:n},overrideMimeType:function(e){return E||(c.mimeType=e),this},abort:function(e){return e=e||S,o&&o.abort(e),T(0,e),this}};d.promise(x),x.success=x.done,x.error=x.fail,x.complete=m.add,x.statusCode=function(e){if(e){var t;if(E<2)for(t in e)g[t]=[g[t],e[t]];else t=e[x.status],x.always(t)}return this},c.url=((e||c.url)+"").replace(hn,"").replace(mn,ln[1]+"//"),c.dataTypes=v.trim(c.dataType||"*").toLowerCase().split(y),c.crossDomain==null&&(a=wn.exec(c.url.toLowerCase()),c.crossDomain=!(!a||a[1]===ln[1]&&a[2]===ln[2]&&(a[3]||(a[1]==="http:"?80:443))==(ln[3]||(ln[1]==="http:"?80:443)))),c.data&&c.processData&&typeof c.data!="string"&&(c.data=v.param(c.data,c.traditional)),kn(Sn,c,n,x);if(E===2)return x;f=c.global,c.type=c.type.toUpperCase(),c.hasContent=!vn.test(c.type),f&&v.active++===0&&v.event.trigger("ajaxStart");if(!c.hasContent){c.data&&(c.url+=(gn.test(c.url)?"&":"?")+c.data,delete c.data),r=c.url;if(c.cache===!1){var N=v.now(),C=c.url.replace(bn,"$1_="+N);c.url=C+(C===c.url?(gn.test(c.url)?"&":"?")+"_="+N:"")}}(c.data&&c.hasContent&&c.contentType!==!1||n.contentType)&&x.setRequestHeader("Content-Type",c.contentType),c.ifModified&&(r=r||c.url,v.lastModified[r]&&x.setRequestHeader("If-Modified-Since",v.lastModified[r]),v.etag[r]&&x.setRequestHeader("If-None-Match",v.etag[r])),x.setRequestHeader("Accept",c.dataTypes[0]&&c.accepts[c.dataTypes[0]]?c.accepts[c.dataTypes[0]]+(c.dataTypes[0]!=="*"?", "+Tn+"; q=0.01":""):c.accepts["*"]);for(l in c.headers)x.setRequestHeader(l,c.headers[l]);if(!c.beforeSend||c.beforeSend.call(h,x,c)!==!1&&E!==2){S="abort";for(l in{success:1,error:1,complete:1})x[l](c[l]);o=kn(xn,c,n,x);if(!o)T(-1,"No Transport");else{x.readyState=1,f&&p.trigger("ajaxSend",[x,c]),c.async&&c.timeout>0&&(u=setTimeout(function(){x.abort("timeout")},c.timeout));try{E=1,o.send(b,T)}catch(k){if(!(E<2))throw k;T(-1,k)}}return x}return x.abort()},active:0,lastModified:{},etag:{}});var Mn=[],_n=/\?/,Dn=/(=)\?(?=&|$)|\?\?/,Pn=v.now();v.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Mn.pop()||v.expando+"_"+Pn++;return this[e]=!0,e}}),v.ajaxPrefilter("json jsonp",function(n,r,i){var s,o,u,a=n.data,f=n.url,l=n.jsonp!==!1,c=l&&Dn.test(f),h=l&&!c&&typeof a=="string"&&!(n.contentType||"").indexOf("application/x-www-form-urlencoded")&&Dn.test(a);if(n.dataTypes[0]==="jsonp"||c||h)return s=n.jsonpCallback=v.isFunction(n.jsonpCallback)?n.jsonpCallback():n.jsonpCallback,o=e[s],c?n.url=f.replace(Dn,"$1"+s):h?n.data=a.replace(Dn,"$1"+s):l&&(n.url+=(_n.test(f)?"&":"?")+n.jsonp+"="+s),n.converters["script json"]=function(){return u||v.error(s+" was not called"),u[0]},n.dataTypes[0]="json",e[s]=function(){u=arguments},i.always(function(){e[s]=o,n[s]&&(n.jsonpCallback=r.jsonpCallback,Mn.push(s)),u&&v.isFunction(o)&&o(u[0]),u=o=t}),"script"}),v.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(e){return v.globalEval(e),e}}}),v.ajaxPrefilter("script",function(e){e.cache===t&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),v.ajaxTransport("script",function(e){if(e.crossDomain){var n,r=i.head||i.getElementsByTagName("head")[0]||i.documentElement;return{send:function(s,o){n=i.createElement("script"),n.async="async",e.scriptCharset&&(n.charset=e.scriptCharset),n.src=e.url,n.onload=n.onreadystatechange=function(e,i){if(i||!n.readyState||/loaded|complete/.test(n.readyState))n.onload=n.onreadystatechange=null,r&&n.parentNode&&r.removeChild(n),n=t,i||o(200,"success")},r.insertBefore(n,r.firstChild)},abort:function(){n&&n.onload(0,1)}}}});var Hn,Bn=e.ActiveXObject?function(){for(var e in Hn)Hn[e](0,1)}:!1,jn=0;v.ajaxSettings.xhr=e.ActiveXObject?function(){return!this.isLocal&&Fn()||In()}:Fn,function(e){v.extend(v.support,{ajax:!!e,cors:!!e&&"withCredentials"in e})}(v.ajaxSettings.xhr()),v.support.ajax&&v.ajaxTransport(function(n){if(!n.crossDomain||v.support.cors){var r;return{send:function(i,s){var o,u,a=n.xhr();n.username?a.open(n.type,n.url,n.async,n.username,n.password):a.open(n.type,n.url,n.async);if(n.xhrFields)for(u in n.xhrFields)a[u]=n.xhrFields[u];n.mimeType&&a.overrideMimeType&&a.overrideMimeType(n.mimeType),!n.crossDomain&&!i["X-Requested-With"]&&(i["X-Requested-With"]="XMLHttpRequest");try{for(u in i)a.setRequestHeader(u,i[u])}catch(f){}a.send(n.hasContent&&n.data||null),r=function(e,i){var u,f,l,c,h;try{if(r&&(i||a.readyState===4)){r=t,o&&(a.onreadystatechange=v.noop,Bn&&delete Hn[o]);if(i)a.readyState!==4&&a.abort();else{u=a.status,l=a.getAllResponseHeaders(),c={},h=a.responseXML,h&&h.documentElement&&(c.xml=h);try{c.text=a.responseText}catch(p){}try{f=a.statusText}catch(p){f=""}!u&&n.isLocal&&!n.crossDomain?u=c.text?200:404:u===1223&&(u=204)}}}catch(d){i||s(-1,d)}c&&s(u,f,c,l)},n.async?a.readyState===4?setTimeout(r,0):(o=++jn,Bn&&(Hn||(Hn={},v(e).unload(Bn)),Hn[o]=r),a.onreadystatechange=r):r()},abort:function(){r&&r(0,1)}}}});var qn,Rn,Un=/^(?:toggle|show|hide)$/,zn=new RegExp("^(?:([-+])=|)("+m+")([a-z%]*)$","i"),Wn=/queueHooks$/,Xn=[Gn],Vn={"*":[function(e,t){var n,r,i=this.createTween(e,t),s=zn.exec(t),o=i.cur(),u=+o||0,a=1,f=20;if(s){n=+s[2],r=s[3]||(v.cssNumber[e]?"":"px");if(r!=="px"&&u){u=v.css(i.elem,e,!0)||n||1;do a=a||".5",u/=a,v.style(i.elem,e,u+r);while(a!==(a=i.cur()/o)&&a!==1&&--f)}i.unit=r,i.start=u,i.end=s[1]?u+(s[1]+1)*n:n}return i}]};v.Animation=v.extend(Kn,{tweener:function(e,t){v.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");var n,r=0,i=e.length;for(;r-1,f={},l={},c,h;a?(l=i.position(),c=l.top,h=l.left):(c=parseFloat(o)||0,h=parseFloat(u)||0),v.isFunction(t)&&(t=t.call(e,n,s)),t.top!=null&&(f.top=t.top-s.top+c),t.left!=null&&(f.left=t.left-s.left+h),"using"in t?t.using.call(e,f):i.css(f)}},v.fn.extend({position:function(){if(!this[0])return;var e=this[0],t=this.offsetParent(),n=this.offset(),r=er.test(t[0].nodeName)?{top:0,left:0}:t.offset();return n.top-=parseFloat(v.css(e,"marginTop"))||0,n.left-=parseFloat(v.css(e,"marginLeft"))||0,r.top+=parseFloat(v.css(t[0],"borderTopWidth"))||0,r.left+=parseFloat(v.css(t[0],"borderLeftWidth"))||0,{top:n.top-r.top,left:n.left-r.left}},offsetParent:function(){return this.map(function(){var e=this.offsetParent||i.body;while(e&&!er.test(e.nodeName)&&v.css(e,"position")==="static")e=e.offsetParent;return e||i.body})}}),v.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,n){var r=/Y/.test(n);v.fn[e]=function(i){return v.access(this,function(e,i,s){var o=tr(e);if(s===t)return o?n in o?o[n]:o.document.documentElement[i]:e[i];o?o.scrollTo(r?v(o).scrollLeft():s,r?s:v(o).scrollTop()):e[i]=s},e,i,arguments.length,null)}}),v.each({Height:"height",Width:"width"},function(e,n){v.each({padding:"inner"+e,content:n,"":"outer"+e},function(r,i){v.fn[i]=function(i,s){var o=arguments.length&&(r||typeof i!="boolean"),u=r||(i===!0||s===!0?"margin":"border");return v.access(this,function(n,r,i){var s;return v.isWindow(n)?n.document.documentElement["client"+e]:n.nodeType===9?(s=n.documentElement,Math.max(n.body["scroll"+e],s["scroll"+e],n.body["offset"+e],s["offset"+e],s["client"+e])):i===t?v.css(n,r,i,u):v.style(n,r,i,u)},n,o?i:t,o,null)}})}),e.jQuery=e.$=v,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return v})})(window); \ No newline at end of file diff --git a/spec/models/forge_activity_spec.rb b/spec/models/forge_activity_spec.rb new file mode 100644 index 000000000..8e19f2014 --- /dev/null +++ b/spec/models/forge_activity_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe ForgeActivity do + pending "add some examples to (or delete) #{__FILE__}" +end From a2d986f70c73376a00a114130af6a8436cd172f6 Mon Sep 17 00:00:00 2001 From: sw <939547590@qq.com> Date: Wed, 4 Mar 2015 16:55:25 +0800 Subject: [PATCH 54/68] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/layouts/base_projects.html.erb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/views/layouts/base_projects.html.erb b/app/views/layouts/base_projects.html.erb index fc6713a94..9007e4c3c 100644 --- a/app/views/layouts/base_projects.html.erb +++ b/app/views/layouts/base_projects.html.erb @@ -104,10 +104,9 @@ <% if @project.project_type == 0 && project_score != 0 %> <%= l(:label_project_grade)%> : <%= link_to(project_score, {:controller => 'projects', - :action => 'show_projects_score', - :remote => true, - :id => @project.id - }, :style => "color: #EC6300;")%> + :action => 'show_projects_score', + :remote => true, + :id => @project.id}, :style => "color: #EC6300;")%> <% end %>
    From 411d201fc3694b200ca5e931cde945ee6678ce98 Mon Sep 17 00:00:00 2001 From: z9hang Date: Wed, 4 Mar 2015 17:04:07 +0800 Subject: [PATCH 55/68] =?UTF-8?q?=E4=BD=9C=E4=B8=9A=E8=AF=A6=E6=83=85?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E6=B7=BB=E5=8A=A0=E7=95=99=E8=A8=80=E5=88=97?= =?UTF-8?q?=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/mobile/entities/homework.rb | 4 ++++ app/services/homework_service.rb | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/api/mobile/entities/homework.rb b/app/api/mobile/entities/homework.rb index 55883e14e..d4c34ffec 100644 --- a/app/api/mobile/entities/homework.rb +++ b/app/api/mobile/entities/homework.rb @@ -52,6 +52,10 @@ module Mobile homework_expose :created_on homework_expose :deadline + expose :jours,using: Mobile::Entities::Jours do |f, opt| + f[:jours] if f.is_a?(Hash) && f.key?(:jours) + end + expose :homework_for_anonymous_comments,using: Mobile::Entities::HomeworkAttach do |f, opt| f[:homework_for_anonymous_comments] if f.is_a?(Hash) && f.key?(:homework_for_anonymous_comments) end diff --git a/app/services/homework_service.rb b/app/services/homework_service.rb index 6287eeef5..d69617eb5 100644 --- a/app/services/homework_service.rb +++ b/app/services/homework_service.rb @@ -26,8 +26,9 @@ class HomeworkService state = @bid.comment_status #end open_anonymous_evaluation = @bid.open_anonymous_evaluation + jours = @bid.journals_for_messages.where('m_parent_id IS NULL').order('created_on DESC') {:course_name => course.name,:course_id => course.id,:id => @bid.id, :author => @bid.author,:author_real_name =>author, :homework_times => many_times, :homework_name => name, :homework_count => homework_count,:student_questions_count => student_questions_count, - :description => description, :homework_state => state,:open_anonymous_evaluation => open_anonymous_evaluation,:created_on => @bid.created_on,:deadline => @bid.deadline} + :description => description, :homework_state => state,:open_anonymous_evaluation => open_anonymous_evaluation,:created_on => @bid.created_on,:deadline => @bid.deadline,:jours => jours} end # 启动作业匿评前提示信息 From 199ef4ebe689ec593018b49f2df60b20eee20225 Mon Sep 17 00:00:00 2001 From: z9hang Date: Wed, 4 Mar 2015 17:14:09 +0800 Subject: [PATCH 56/68] =?UTF-8?q?=E5=8C=BF=E8=AF=84=E4=BD=9C=E5=93=81?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E5=88=97=E8=A1=A8=E8=A1=A5=E4=B8=8Acur=5Fpag?= =?UTF-8?q?e=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/mobile/entities/anonymous_works_params.rb | 1 + app/services/homework_service.rb | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/api/mobile/entities/anonymous_works_params.rb b/app/api/mobile/entities/anonymous_works_params.rb index 95cf9215d..e6bc8f346 100644 --- a/app/api/mobile/entities/anonymous_works_params.rb +++ b/app/api/mobile/entities/anonymous_works_params.rb @@ -15,6 +15,7 @@ module Mobile anonymous_works_params_expose :m_score anonymous_works_params_expose :is_anonymous_comments anonymous_works_params_expose :cur_type + anonymous_works_params_expose :cur_page expose :jours ,using: Mobile::Entities::Jours do |f, opt| if f.is_a?(Hash) && f.key?(:jours) f[:jours] diff --git a/app/services/homework_service.rb b/app/services/homework_service.rb index d69617eb5..f9be6f79d 100644 --- a/app/services/homework_service.rb +++ b/app/services/homework_service.rb @@ -117,7 +117,7 @@ class HomeworkService @is_anonymous_comments = @bid.comment_status == 1 && !@homework.users.include?(current_user) && @homework.user != current_user && !@is_teacher #判断是不是匿评(开启匿评,当前用户不是作业的创建者或者参与者,不是老师) jours = @homework.journals_for_messages.where("is_comprehensive_evaluation = 3 or is_comprehensive_evaluation is null").order("created_on DESC")#jours留言 is null条件用以兼容历史数据 #@jour = paginateHelper jours,5 #留言 - #@cur_page = params[:cur_page] || 1 + @cur_page = params[:cur_page] || 1 @cur_type = params[:cur_type] || 5 teacher_stars_json_like = stars_to_json_like(@teacher_stars,true,@homework,true) student_stars_json_like = stars_to_json_like(@student_stars,false,@homework,(false || @is_teacher)) @@ -126,7 +126,7 @@ class HomeworkService end [@homework,{:is_teacher => @is_teacher,:m_score => @m_score,:jours => jours,:teacher_stars => teacher_stars_json_like, - :student_stars => student_stars_json_like,:is_anonymous_comments => @is_anonymous_comments,:cur_type => @cur_type}] + :student_stars => student_stars_json_like,:is_anonymous_comments => @is_anonymous_comments,:cur_type => @cur_type,:cur_page => @cur_page}] #name = @homework.name #desc = @homework.description #datetime = @homework.created_at From 7f249d29f41d0a36bcbd4956b7c6f3f0a77eb82d Mon Sep 17 00:00:00 2001 From: sw <939547590@qq.com> Date: Thu, 5 Mar 2015 08:45:32 +0800 Subject: [PATCH 57/68] =?UTF-8?q?=E4=BF=AE=E6=95=B4=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/projects/show.html.erb | 63 ++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 23 deletions(-) diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb index 7a06c57cd..84b3768a2 100644 --- a/app/views/projects/show.html.erb +++ b/app/views/projects/show.html.erb @@ -1,7 +1,7 @@ <% if @events_by_day.size >0 %>

    - <% # 暂时隐藏时间的显示%> + <%#= l(:label_date_from_to, :start => format_date(@date_to - @days), :end => format_date(@date_to-1)) %>

    @@ -12,7 +12,9 @@ <%= image_tag(url_to_avatar(e.event_author), :class => "avatar") %>
    - <%= h(e.project) if @project.nil? || @project.id != e.project.id %> + + <%= h(e.project) if @project.nil? || @project.id != e.project.id %> + <% if @canShowRealName %> <%= link_to_user(e.event_author) if e.respond_to?(:event_author) %> @@ -30,10 +32,18 @@
    <%= l :label_activity_time %> - :  <%= format_activity_day(day) %> <%= format_time(e.event_datetime, false) %> + :  + <%= format_activity_day(day) %> + <%= format_time(e.event_datetime, false) %> + <% if e.event_type == "issue" %>
    - <%= link_to l(:label_find_all_comments), issue_path(e) %> <%= l(:label_comments_count, :count => e.journals.count) %> + + <%= link_to l(:label_find_all_comments), issue_path(e) %> + + + <%= l(:label_comments_count, :count => e.journals.count) %> +
    <% end %>
    @@ -48,21 +58,24 @@ <%= image_tag(url_to_avatar(@user), :class => "avatar") %>
    - - <% if @canShowRealName %> - (<%= link_to_user(@user, @canShowRealName) %> - ) - <% else %> - <%= link_to_user(@user) %> - <% end %> - <%#= l(:label_new_activity) %> - - <%= l(:label_user_create_project) %> <%= link_to @project.name %> + + <% if @canShowRealName %> + (<%= link_to_user(@user, @canShowRealName) %>) + <% else %> + <%= link_to_user(@user) %> + <% end %> + <%#= l(:label_new_activity) %> + + <%= l(:label_user_create_project) %> + <%= link_to @project.name %> !
    - <%= l :label_activity_time %>: <%= format_time(@project.created_on) %> + + <%= l :label_activity_time %>: + <%= format_time(@project.created_on) %> +
    @@ -75,20 +88,24 @@
    - <% if @canShowRealName %> - (<%= link_to_user(@user, @canShowRealName) %> - ) - <% else %> - <%= link_to_user(@user) %> - <% end %> + <% if @canShowRealName %> + (<%= link_to_user(@user, @canShowRealName) %>) + <% else %> + <%= link_to_user(@user) %> + <% end %> <%#= l(:label_new_activity) %> - <%= l(:label_user_create_project) %> <%= link_to @project.name %> + <%= @user.to_s %> + <%= l(:label_user_create_project) %> + <%= link_to @project.name %> !
    - <%= l :label_activity_time %>: <%= format_time(@project.created_on) %> + + <%= l :label_activity_time %>: + <%= format_time(@project.created_on) %> +
    From d01aa5f15c4ba445d3394a2922d34fd770405033 Mon Sep 17 00:00:00 2001 From: z9hang Date: Thu, 5 Mar 2015 09:26:13 +0800 Subject: [PATCH 58/68] =?UTF-8?q?=E5=85=B3=E6=B3=A8=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E9=99=90=E5=88=B6=E4=B8=8D=E8=83=BD=E5=85=B3=E6=B3=A8=E8=87=AA?= =?UTF-8?q?=E5=B7=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/watches_service.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/services/watches_service.rb b/app/services/watches_service.rb index 2b9bed3cd..2aab7de81 100644 --- a/app/services/watches_service.rb +++ b/app/services/watches_service.rb @@ -1,7 +1,12 @@ +#coding=utf-8 class WatchesService def watch params @current_user = User.find(params[:current_user_id]) + if params[:object_type] == 'user' && params[:current_user_id] == params[:object_id] + raise '不能关注自己!' + end @watchables = find_watchables params + if @watchables.nil? raise '404' end From bcc8abdee9e514878f91a2f6b56b34e7963457a5 Mon Sep 17 00:00:00 2001 From: sw <939547590@qq.com> Date: Thu, 5 Mar 2015 09:27:13 +0800 Subject: [PATCH 59/68] =?UTF-8?q?1=E3=80=81=E5=A2=9E=E5=8A=A0=E7=BB=84?= =?UTF-8?q?=E7=BB=87=E7=9B=B8=E5=85=B3=E6=95=B0=E6=8D=AE=E8=A1=A8=202?= =?UTF-8?q?=E3=80=81=E5=A2=9E=E5=8A=A0=E7=BB=84=E7=BB=87=E5=92=8C=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E7=9A=84=E5=85=B3=E8=81=94=E5=85=B3=E7=B3=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/organization.rb | 5 +++++ app/models/project.rb | 4 +++- db/migrate/20150305011023_create_organizations.rb | 10 ++++++++++ .../20150305011359_add_organization_to_project.rb | 9 +++++++++ db/schema.rb | 10 +++++++++- spec/models/organization_spec.rb | 5 +++++ 6 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 app/models/organization.rb create mode 100644 db/migrate/20150305011023_create_organizations.rb create mode 100644 db/migrate/20150305011359_add_organization_to_project.rb create mode 100644 spec/models/organization_spec.rb diff --git a/app/models/organization.rb b/app/models/organization.rb new file mode 100644 index 000000000..5f52dee98 --- /dev/null +++ b/app/models/organization.rb @@ -0,0 +1,5 @@ +class Organization < ActiveRecord::Base + attr_accessible :logo_link, :name + + has_many :projects +end diff --git a/app/models/project.rb b/app/models/project.rb index df403bb5c..3ac29249f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -90,7 +90,9 @@ class Project < ActiveRecord::Base :association_foreign_key => 'custom_field_id' has_many :tags, :through => :project_tags, :class_name => 'Tag' - has_many :project_tags, :class_name => 'ProjectTags' + has_many :project_tags, :class_name => 'ProjectTags' + + belongs_to :organization # has_many :journals diff --git a/db/migrate/20150305011023_create_organizations.rb b/db/migrate/20150305011023_create_organizations.rb new file mode 100644 index 000000000..798c526c5 --- /dev/null +++ b/db/migrate/20150305011023_create_organizations.rb @@ -0,0 +1,10 @@ +class CreateOrganizations < ActiveRecord::Migration + def change + create_table :organizations do |t| + t.string :name + t.string :logo_link + + t.timestamps + end + end +end diff --git a/db/migrate/20150305011359_add_organization_to_project.rb b/db/migrate/20150305011359_add_organization_to_project.rb new file mode 100644 index 000000000..4bf8c4c88 --- /dev/null +++ b/db/migrate/20150305011359_add_organization_to_project.rb @@ -0,0 +1,9 @@ +class AddOrganizationToProject < ActiveRecord::Migration + def up + add_column :projects, :organization_id, :integer + end + + def down + remove_column :projects, :organization_id + end +end diff --git a/db/schema.rb b/db/schema.rb index d7a1b18bc..d0bee8db6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20150128032421) do +ActiveRecord::Schema.define(:version => 20150305011359) do create_table "activities", :force => true do |t| t.integer "act_id", :null => false @@ -802,6 +802,13 @@ ActiveRecord::Schema.define(:version => 20150128032421) do t.integer "project_id" end + create_table "organizations", :force => true do |t| + t.string "name" + t.string "logo_link" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + create_table "poll_answers", :force => true do |t| t.integer "poll_question_id" t.text "answer_text" @@ -926,6 +933,7 @@ ActiveRecord::Schema.define(:version => 20150128032421) do t.integer "user_id" t.integer "dts_test", :default => 0 t.string "enterprise_name" + t.integer "organization_id" end add_index "projects", ["lft"], :name => "index_projects_on_lft" diff --git a/spec/models/organization_spec.rb b/spec/models/organization_spec.rb new file mode 100644 index 000000000..6f6f99fb4 --- /dev/null +++ b/spec/models/organization_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe Organization do + pending "add some examples to (or delete) #{__FILE__}" +end From 04dea372c573f41ed53ab46f24353bb1b9580c0a Mon Sep 17 00:00:00 2001 From: z9hang Date: Thu, 5 Mar 2015 10:09:19 +0800 Subject: [PATCH 60/68] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=84=8F=E8=A7=81?= =?UTF-8?q?=E5=8F=8D=E9=A6=88=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/mobile/apis/comments.rb | 14 ++++++++++++++ app/controllers/forums_controller.rb | 10 ++++++---- app/services/comment_service.rb | 9 +++++++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/app/api/mobile/apis/comments.rb b/app/api/mobile/apis/comments.rb index a7eea735d..7a7f17d8f 100644 --- a/app/api/mobile/apis/comments.rb +++ b/app/api/mobile/apis/comments.rb @@ -72,6 +72,20 @@ module Mobile present :status, 0 end + desc ' 意见反馈' + params do + requires :token, type: String + requires :subject,type: String,desc: '意见' + end + post do + cs_params = { + memo: {:subject => params[:subject],:content => '该贴来自手机App意见反馈'}, + } + cs = CommentService.new + memo = cs.create_feedback cs_params, current_user + raise "commit failed #{memo.errors.full_messages}" if memo.new_record? + present :status, 0 + end end end diff --git a/app/controllers/forums_controller.rb b/app/controllers/forums_controller.rb index 54b8c6305..2f7092d9a 100644 --- a/app/controllers/forums_controller.rb +++ b/app/controllers/forums_controller.rb @@ -15,12 +15,14 @@ class ForumsController < ApplicationController PageLimit = 20 def create_feedback if User.current.logged? - @memo = Memo.new(params[:memo]) - @memo.forum_id = "1" - @memo.author_id = User.current.id + #@memo = Memo.new(params[:memo]) + #@memo.forum_id = "1" + #@memo.author_id = User.current.id #@forum = @memo.forum + cs = CommentService.new + @memo = cs.create_feedback params,User.current respond_to do |format| - if @memo.save + if !@memo.new_record? format.html { redirect_to forum_path(@memo.forum) } else sort_init 'updated_at', 'desc' diff --git a/app/services/comment_service.rb b/app/services/comment_service.rb index 42dc8c415..876caaaf3 100644 --- a/app/services/comment_service.rb +++ b/app/services/comment_service.rb @@ -78,4 +78,13 @@ class CommentService @jfm end + #发贴,用于意见反馈 + def create_feedback params,current_user + @memo = Memo.new(params[:memo]) + @memo.forum_id = "1" + @memo.author_id = current_user.id + @memo.save + @memo + end + end \ No newline at end of file From d1edd86e11108629c6b4cbfaccea4c0614b6e95b Mon Sep 17 00:00:00 2001 From: sw <939547590@qq.com> Date: Thu, 5 Mar 2015 10:40:10 +0800 Subject: [PATCH 61/68] =?UTF-8?q?1=E3=80=81=E4=BF=AE=E6=94=B9=E7=BB=84?= =?UTF-8?q?=E7=BB=87=E9=A6=96=E9=A1=B5=E9=A1=B9=E7=9B=AE=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=202=E3=80=81=E4=BF=AE=E6=94=B9=E6=AD=A4=E7=BB=84=E7=BB=87?= =?UTF-8?q?=E6=B2=A1=E6=9C=89=E9=A1=B9=E7=9B=AE=E6=97=B6=E7=9A=84=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/welcome_controller.rb | 36 ++++++++-------- app/views/welcome/_hot_projects_list.html.erb | 4 +- app/views/welcome/index.html.erb | 41 +++++++++---------- 3 files changed, 42 insertions(+), 39 deletions(-) diff --git a/app/controllers/welcome_controller.rb b/app/controllers/welcome_controller.rb index db2f287eb..4ee5ddbf1 100644 --- a/app/controllers/welcome_controller.rb +++ b/app/controllers/welcome_controller.rb @@ -27,28 +27,31 @@ class WelcomeController < ApplicationController def index # 企业版定制: params[:project]为传过来的参数 unless params[:organization].nil? - @cur_projects = Project.find(params[:organization]) - @organization = @cur_projects.enterprise_name - @organization_projects = (current_user.admin? || User.current.member_of?(@cur_projects)) ? Project.where("enterprise_name =? ", @organization) : Project.all_public.where("enterprise_name =? ", @organization) - @e_count = @organization_projects.count - @part_projects = [] - # 取十个 - @organization_projects.each do |obj| - break if(@organization_projects[10] == obj) - @part_projects << Project.visible.find_by_id("#{obj.id}") unless obj.id.nil? - end - # 不够十个的用最火项目替代 - @e_count < 9 ? @part_projects = find_miracle_project( 9 - @e_count, 3,"score desc") : @part_projects - # 配置文件首页定制 + @organization = Organization.find params[:organization] + @organization_projects = Project.visible.joins(:project_status).joins("LEFT JOIN project_scores ON projects.id = project_scores.project_id").where("projects.organization_id = ?", 1).order("score DESC").limit(10).all + @part_projects = @organization_projects.count < 9 ? find_miracle_project( 9 - @organization_projects.count, 3,"score desc") : [] + # @cur_projects = Project.find(params[:organization]) + # @organization = @cur_projects.enterprise_name + # @organization_projects = (current_user.admin? || User.current.member_of?(@cur_projects)) ? Project.where("enterprise_name =? ", @organization) : Project.all_public.where("enterprise_name =? ", @organization) + # @e_count = @organization_projects.count + # @part_projects = [] + # # 取十个 + # @organization_projects.each do |obj| + # break if(@organization_projects[10] == obj) + # @part_projects << Project.visible.find_by_id("#{obj.id}") unless obj.id.nil? + # end + # # 不够十个的用最火项目替代 + # @e_count < 9 ? @part_projects = find_miracle_project( 9 - @e_count, 3,"score desc") : @part_projects + # # 配置文件首页定制 @enterprise_page = FirstPage.find_by_page_type('enterprise') if @enterprise_page.nil? @enterprise_page = FirstPage.new @enterprise_page.page_type = 'enterprise' end - # 主页配置部分结束 - + # 主页配置部分结束 end # end 企业版定制结束 + if @first_page.nil? || @first_page.sort_type.nil? @projects = find_miracle_project(10, 3,"score desc") else @@ -74,7 +77,8 @@ class WelcomeController < ApplicationController @projects = @projects_all.order("score desc") end end - + rescue Exception => e + render_404 end def robots diff --git a/app/views/welcome/_hot_projects_list.html.erb b/app/views/welcome/_hot_projects_list.html.erb index 92e1469cd..d14a6901b 100644 --- a/app/views/welcome/_hot_projects_list.html.erb +++ b/app/views/welcome/_hot_projects_list.html.erb @@ -5,7 +5,9 @@
    <% unless project.is_public %> - <%= l(:label_private) %> + + <%= l(:label_private) %> + <% end %> <%= link_to( project.name, project_path(project.id), :class => "d-g-blue d-p-project-name",:title => "#{project.name}" )%> (<%= link_to l(:label_project_member_amount, :count=>projectCount(project)), project_member_path(project) ,:course =>'0' %>) diff --git a/app/views/welcome/index.html.erb b/app/views/welcome/index.html.erb index 3e0a50ceb..76ca25743 100644 --- a/app/views/welcome/index.html.erb +++ b/app/views/welcome/index.html.erb @@ -35,7 +35,7 @@ <% if get_avatar?(@first_page) %> <%= image_tag(url_to_avatar(@first_page), width:@first_page.image_width,height: @first_page.image_height) %> <% else %> - <%= image_tag '/images/transparent.png', width:@first_page.image_width,height: @first_page.image_height %> + <%= image_tag @organization.logo_link, width:@first_page.image_width,height: @first_page.image_height %> <% end %> <% else %> <%= image_tag(url_to_avatar(@enterprise_page), width:@first_page.image_width,height: @first_page.image_height) %> @@ -47,9 +47,9 @@ <%= @first_page.description.html_safe %> <% end %> <% else %> - - <%= @organization %> - + + <%= @organization.name %> +
    <%= @enterprise_page.title %> @@ -86,24 +86,21 @@ <% end; reset_cycle %> <% else %> - <% if @e_count == 0 %> -
    <%= l(:label_enterprise_tips) %>
    - <% @projects.map do |project| %> - <%= render :partial => 'hot_projects_list', :locals => {:project => project} %> - <% end %> - <% elsif @e_count < 10 %> - <% @organization_projects.map do |project| %> - <%= render :partial => 'hot_projects_list', :locals => {:project => project} %> - <% end %> -
    <%= l(:label_part_enterprise_tips) %>
    - <% @part_projects.map do |project| %> - <%= render :partial => 'hot_projects_list', :locals => {:project => project} %> - <% end %> - <% else %> - <% @part_projects.map do |project| %> - <%= render :partial => 'hot_projects_list', :locals => {:project => project} %> - <% end %> - <% end %> + <% if @part_projects.empty? %> + <% @projects.map do |project| %> + <%= render :partial => 'hot_projects_list', :locals => {:project => project} %> + <% end %> + <% else %> + <% @organization_projects.map do |project| %> + <%= render :partial => 'hot_projects_list', :locals => {:project => project} %> + <% end %> +
    + <%= l(:label_part_enterprise_tips) %> +
    + <% @part_projects.map do |project| %> + <%= render :partial => 'hot_projects_list', :locals => {:project => project} %> + <% end %> + <% end %> <% end %>
    From dead08a0d6a220ed96516d880de481109ed0f2d7 Mon Sep 17 00:00:00 2001 From: sw <939547590@qq.com> Date: Thu, 5 Mar 2015 11:53:28 +0800 Subject: [PATCH 62/68] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E3=80=81=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E9=A1=B9=E7=9B=AE=E5=A2=9E=E5=8A=A0=E6=89=80=E5=B1=9E?= =?UTF-8?q?=E7=BB=84=E7=BB=87=E7=9A=84=E9=80=89=E6=8B=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/projects_controller.rb | 2 ++ app/helpers/projects_helper.rb | 11 +++++++ app/views/projects/_form.html.erb | 45 ++++++++++++++++++++------ app/views/projects/new.html.erb | 4 ++- config/locales/zh.yml | 2 +- 5 files changed, 53 insertions(+), 11 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 41ec5fbf4..dea9c21ec 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -179,6 +179,7 @@ class ProjectsController < ApplicationController @trackers = Tracker.sorted.all @project = Project.new @project.safe_attributes = params[:project] + @project.organization_id = params[:organization_id] if validate_parent_id && @project.save @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id') # Add current user as a project member if he is not admin @@ -404,6 +405,7 @@ class ProjectsController < ApplicationController def update @project.safe_attributes = params[:project] + @project.organization_id = params[:organization_id] #@project.dts_test = params[:project][:dts_test] if validate_parent_id && @project.save @course = Course.find_by_extra(@project.identifier) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index fe6714186..ccc750462 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -371,4 +371,15 @@ module ProjectsHelper return projects end + + def project_organizations_id_option + type = [] + Organization.all.each do |org| + option = [] + option << org.name + option << org.id + type << option + end + type + end end diff --git a/app/views/projects/_form.html.erb b/app/views/projects/_form.html.erb index 636aadf43..d0544cadb 100644 --- a/app/views/projects/_form.html.erb +++ b/app/views/projects/_form.html.erb @@ -1,35 +1,62 @@ <%= error_messages_for 'project' %> <% unless @project.new_record? %> -

    <%= render :partial=>"avatar/avatar_form",:locals=> {source:@project} %>

    +

    + <%= render :partial=>"avatar/avatar_form",:locals=> {source:@project} %> +

    <% end %> -

    <%= f.text_field :name, :required => true, :size => 60, :style => "width:490px;" %>

    +

    + <%= f.text_field :name, :required => true, :size => 60, :style => "width:490px;" %> +

    <%= f.text_area :description, :rows => 8, :class => 'wiki-edit', :style => "font-size:small;width:490px;margin-left:10px;" %>

    -

    <%= f.text_field :enterprise_name, :size => 60, :style => "width:490px;" %>

    -

    <%= f.text_field :identifier, :required => true, :size => 60, :style => "width:488px;", :disabled => @project.identifier_frozen?, :maxlength => Project::IDENTIFIER_MAX_LENGTH, +

    + <%#= f.text_field :enterprise_name, :size => 60, :style => "width:490px;" %> + + <%= select_tag :organization_id,options_for_select(project_organizations_id_option,@project.organization_id),{} %> +

    +

    + <%= f.text_field :identifier, :required => true, :size => 60, :style => "width:488px;", :disabled => @project.identifier_frozen?, :maxlength => Project::IDENTIFIER_MAX_LENGTH, value:"#{User.current.id.to_s + '_' +Time.now.to_s.gsub(' ','_').gsub(':','').gsub('+','')}" %> <% unless @project.identifier_frozen? %> - <%= l(:text_length_between, :min => 1, :max => Project::IDENTIFIER_MAX_LENGTH) %> <%= l(:text_project_identifier_info).html_safe %> + + <%= l(:text_length_between, :min => 1, :max => Project::IDENTIFIER_MAX_LENGTH) %> + <%= l(:text_project_identifier_info).html_safe %> + <% end %>

    -

    <%= f.check_box :is_public, :style => "margin-left:10px;" %>

    -

    <%= f.check_box :hidden_repo, :style => "margin-left:10px;" %>

    +

    + + <%= f.check_box :is_public, :style => "margin-left:10px;" %> + +

    +

    + + <%= f.check_box :hidden_repo, :style => "margin-left:10px;" %> + +

    -

    <%= f.text_field :project_type, :value => 0 %>

    +

    + <%= f.text_field :project_type, :value => 0 %> +

    <%= wikitoolbar_for 'project_description' %> <% @project.custom_field_values.each do |value| %> -

    <%= custom_field_tag_with_label :project, value %>

    +

    + <%= custom_field_tag_with_label :project, value %> +

    <% end %> <%= call_hook(:view_projects_form, :project => @project, :form => f) %> diff --git a/app/views/projects/new.html.erb b/app/views/projects/new.html.erb index b68fcb5bc..57afc6c08 100644 --- a/app/views/projects/new.html.erb +++ b/app/views/projects/new.html.erb @@ -4,7 +4,9 @@ <%= labelled_form_for @project do |f| %>

    <%=l(:label_project_new)%>

    -

    <%=raw l(:label_project_new_description)%>

    +

    + <%=raw l(:label_project_new_description)%> +

    <%= render :partial => 'form', :locals => { :f => f } %> <%= submit_tag l(:button_create), :class => "enterprise"%> diff --git a/config/locales/zh.yml b/config/locales/zh.yml index aa153cda6..e9efa7fba 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -247,7 +247,7 @@ zh: label_course_closed_tips: "确定要%{desc}课程?" # end field_name: 名称 - field_enterprise_name: 组织名称 + field_enterprise_name: 组织 label_week_mail: 一周动态 label_day_mail: 一日动态 From 3b4bf8b99d26cb870ce2e7edb0c5aa7c96471a46 Mon Sep 17 00:00:00 2001 From: sw <939547590@qq.com> Date: Thu, 5 Mar 2015 14:15:07 +0800 Subject: [PATCH 63/68] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=BB=84=E7=BB=87?= =?UTF-8?q?=E7=9A=84index=E7=95=8C=E9=9D=A2=EF=BC=8C=E4=BB=A5=E5=8F=8Ainde?= =?UTF-8?q?x=E7=95=8C=E9=9D=A2=E5=88=B0=E7=BB=84=E7=BB=87=E9=A6=96?= =?UTF-8?q?=E9=A1=B5=E7=9A=84=E8=B7=B3=E8=BD=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/organization_controller.rb | 10 +++++++ app/controllers/organizations_controller.rb | 6 ---- app/views/organization/index.html.erb | 31 +++++++++++++++++++++ app/views/organizations/index.html.erb | 23 --------------- config/routes.rb | 8 +++--- 5 files changed, 45 insertions(+), 33 deletions(-) create mode 100644 app/controllers/organization_controller.rb delete mode 100644 app/controllers/organizations_controller.rb create mode 100644 app/views/organization/index.html.erb delete mode 100644 app/views/organizations/index.html.erb diff --git a/app/controllers/organization_controller.rb b/app/controllers/organization_controller.rb new file mode 100644 index 000000000..ef0919ced --- /dev/null +++ b/app/controllers/organization_controller.rb @@ -0,0 +1,10 @@ +class OrganizationController < ApplicationController + layout 'project_base' + def index + #@projects = Project.find_by_sql("SELECT * FROM projects WHERE id IN (select MAX(id) from projects GROUP BY enterprise_name)") + @organizations = Organization.all + respond_to do |format| + format.html + end + end +end diff --git a/app/controllers/organizations_controller.rb b/app/controllers/organizations_controller.rb deleted file mode 100644 index c9cce5752..000000000 --- a/app/controllers/organizations_controller.rb +++ /dev/null @@ -1,6 +0,0 @@ -class OrganizationsController < ApplicationController - layout 'project_base' - def index - @projects = Project.find_by_sql("SELECT * FROM projects WHERE id IN (select MAX(id) from projects GROUP BY enterprise_name)") - end -end diff --git a/app/views/organization/index.html.erb b/app/views/organization/index.html.erb new file mode 100644 index 000000000..2f657831a --- /dev/null +++ b/app/views/organization/index.html.erb @@ -0,0 +1,31 @@ + + <%= l(:label_all_enterprises) %> + +
    +
    + <%= l(:label_all_enterprises) %> +
    +
    + <% if @organizations.empty? %> +

    + <%= l(:label_enterprise_nil) %> +

    + <% else %> + <% @organizations.each do |organization| %> + <% unless organization.name.blank? %> +
      +
    • + #{project.enterprise_name} + <%= link_to organization.name, home_path(:organization => organization.id) %> +
    • +
    + <% end %> + <% end %> + <% end %> +
    +
    +
    +
    +
      +
      +<% html_title(l(:label_enterprise_all)) -%> diff --git a/app/views/organizations/index.html.erb b/app/views/organizations/index.html.erb deleted file mode 100644 index 45813ced5..000000000 --- a/app/views/organizations/index.html.erb +++ /dev/null @@ -1,23 +0,0 @@ -<%= l(:label_all_enterprises) %> -
      -
      <%= l(:label_all_enterprises) %>
      -
      - <% if @projects.count == 0 %> -

      <%= l(:label_enterprise_nil) %>

      - <% else %> - <% @projects.each do |organization| %> - <% unless organization.enterprise_name.blank? %> -
        -
      • #{project.enterprise_name} - <%= link_to organization.enterprise_name, home_path(:organization => organization) %>
      • -
      - <% end %> - <% end %> - <% end %> -
      -
      -
      -
      -
        -
        -<% html_title(l(:label_enterprise_all)) -%> diff --git a/config/routes.rb b/config/routes.rb index bf0b73e8d..6756a713d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -26,9 +26,6 @@ # Example: :via => :get ====> :via => :get RedmineApp::Application.routes.draw do - get "organizations/index" - - #match '/contests/:id/contestnotifications', :controller => 'contestnotifications', :action => 'index' mount Mobile::API => '/api' @@ -39,6 +36,10 @@ RedmineApp::Application.routes.draw do resources :apply_project_masters delete 'apply_project_masters', :to => 'apply_project_masters#delete' + resources :organization, :only => [:index] do + + end + resources :homework_attach do collection do get 'get_homework_member_list' @@ -390,7 +391,6 @@ RedmineApp::Application.routes.draw do match '/statistics', :to => 'projects#statistics', :as => 'statistics', :via => :get # match '/investor', :controller => 'projects', :action => 'investor', :as => 'investor', :via => :get match '/homework', :to => 'projects#homework', :as => 'homework', :via => :get - match 'organizations', :to => 'organizations#index', :as => 'index', :via => :get # match '/activity', :controller => 'activities', :action => 'index', :as => 'activity', :via => :get # match '/repository', :controller => 'repositories', :action => 'show', :repository_id => nil, :path => nil, :rev => nil, :as => 'repository', :via => :get From 45fe622f26a933ed3a6c1208a94cd22d31c1c1b8 Mon Sep 17 00:00:00 2001 From: sw <939547590@qq.com> Date: Thu, 5 Mar 2015 14:42:46 +0800 Subject: [PATCH 64/68] =?UTF-8?q?1=E3=80=81=E4=BF=AE=E6=94=B9=E8=BF=9B?= =?UTF-8?q?=E5=85=A5=E7=B3=BB=E7=BB=9F=E9=A6=96=E9=A1=B5=E6=8A=A5=E9=94=99?= =?UTF-8?q?=202=E3=80=81=E5=A2=9E=E5=8A=A0=E7=BB=84=E7=BB=87=E7=9A=84?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=9B=BE=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/organization/index.html.erb | 2 +- app/views/welcome/index.html.erb | 4 ++-- public/images/avatars/Organization/0 | Bin 0 -> 56368 bytes 3 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 public/images/avatars/Organization/0 diff --git a/app/views/organization/index.html.erb b/app/views/organization/index.html.erb index 2f657831a..f1c50d6ab 100644 --- a/app/views/organization/index.html.erb +++ b/app/views/organization/index.html.erb @@ -15,7 +15,7 @@ <% unless organization.name.blank? %>
        • - #{project.enterprise_name} + <%= organization.name%> <%= link_to organization.name, home_path(:organization => organization.id) %>
        diff --git a/app/views/welcome/index.html.erb b/app/views/welcome/index.html.erb index 76ca25743..eca3e4c1d 100644 --- a/app/views/welcome/index.html.erb +++ b/app/views/welcome/index.html.erb @@ -35,10 +35,10 @@ <% if get_avatar?(@first_page) %> <%= image_tag(url_to_avatar(@first_page), width:@first_page.image_width,height: @first_page.image_height) %> <% else %> - <%= image_tag @organization.logo_link, width:@first_page.image_width,height: @first_page.image_height %> + <%= image_tag '/images/transparent.png', width:@first_page.image_width,height: @first_page.image_height %> <% end %> <% else %> - <%= image_tag(url_to_avatar(@enterprise_page), width:@first_page.image_width,height: @first_page.image_height) %> + <%= image_tag(url_to_avatar(@organization), width:@first_page.image_width,height: @first_page.image_height) %> <% end %>
        diff --git a/public/images/avatars/Organization/0 b/public/images/avatars/Organization/0 new file mode 100644 index 0000000000000000000000000000000000000000..2ae1d749446d88f0516c7c1807e93a426f85264a GIT binary patch literal 56368 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 literal 0 HcmV?d00001 From 5f8595d874a4186aba2dc1be77ccd5334f481ff2 Mon Sep 17 00:00:00 2001 From: sw <939547590@qq.com> Date: Thu, 5 Mar 2015 15:27:22 +0800 Subject: [PATCH 65/68] =?UTF-8?q?admin=E9=A1=B5=E9=9D=A2=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E7=BB=84=E7=BB=87=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/admin_controller.rb | 7 +++++++ app/views/admin/organization.html.erb | 1 + config/locales/zh.yml | 1 + config/routes.rb | 1 + lib/redmine.rb | 1 + 5 files changed, 11 insertions(+) create mode 100644 app/views/admin/organization.html.erb diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index aab9d7b69..1efbb665f 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -322,4 +322,11 @@ class AdminController < ApplicationController end end + #组织 + def organization + @organizations = Organization.all + respond_to do |format| + format.html + end + end end diff --git a/app/views/admin/organization.html.erb b/app/views/admin/organization.html.erb new file mode 100644 index 000000000..9d07aa0df --- /dev/null +++ b/app/views/admin/organization.html.erb @@ -0,0 +1 @@ +111 \ No newline at end of file diff --git a/config/locales/zh.yml b/config/locales/zh.yml index e9efa7fba..362838800 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -538,6 +538,7 @@ zh: label_project: 项目 label_activity_project: '项目: ' #added by bai + label_organization_list: 组织列表 label_project_plural: 项目列表 label_first_page_made: 首页定制 label_project_first_page: 项目托管平台首页 diff --git a/config/routes.rb b/config/routes.rb index 6756a713d..fb4e40e49 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -646,6 +646,7 @@ RedmineApp::Application.routes.draw do match 'admin/info', :via => :get match 'admin/test_email', :via => :get match 'admin/default_configuration', :via => :post + get 'admin/organization' resources :auth_sources do member do diff --git a/lib/redmine.rb b/lib/redmine.rb index cd855e01a..5cfd8bae8 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -360,6 +360,7 @@ Redmine::MenuManager.map :homework_menu do |menu| end ########end Redmine::MenuManager.map :admin_menu do |menu| + menu.push :organization, {:controller => 'admin', :action => 'organization'}, :caption => :label_organization_list menu.push :projects, {:controller => 'admin', :action => 'projects'}, :caption => :label_project_plural menu.push :users, {:controller => 'admin', :action => 'users'}, :caption => :label_user_plural menu.push :first_page_made, {:controller => 'admin',:action => 'first_page_made'},:caption => :label_first_page_made From 3c4b502e970e4950191bd164d958daa2658fad03 Mon Sep 17 00:00:00 2001 From: sw <939547590@qq.com> Date: Thu, 5 Mar 2015 17:03:44 +0800 Subject: [PATCH 66/68] =?UTF-8?q?1=E3=80=81=E4=BF=AE=E6=94=B9=E8=BF=9B?= =?UTF-8?q?=E5=85=A5=E7=BB=84=E7=BB=87=E4=B8=BB=E9=A1=B5=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E6=98=BE=E7=A4=BA=E4=B8=8D=E6=AD=A3=E7=A1=AE?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98=202=E3=80=81admin=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=BB=84=E7=BB=87=E5=88=97=E8=A1=A8=E6=98=BE?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/welcome_controller.rb | 2 +- app/views/admin/organization.html.erb | 44 ++++++++++++++++++++++- app/views/admin/projects.html.erb | 51 +++++++++++++++++++-------- config/locales/zh.yml | 2 ++ config/routes.rb | 2 +- 5 files changed, 84 insertions(+), 17 deletions(-) diff --git a/app/controllers/welcome_controller.rb b/app/controllers/welcome_controller.rb index 4ee5ddbf1..290af0068 100644 --- a/app/controllers/welcome_controller.rb +++ b/app/controllers/welcome_controller.rb @@ -28,7 +28,7 @@ class WelcomeController < ApplicationController # 企业版定制: params[:project]为传过来的参数 unless params[:organization].nil? @organization = Organization.find params[:organization] - @organization_projects = Project.visible.joins(:project_status).joins("LEFT JOIN project_scores ON projects.id = project_scores.project_id").where("projects.organization_id = ?", 1).order("score DESC").limit(10).all + @organization_projects = Project.visible.joins(:project_status).joins("LEFT JOIN project_scores ON projects.id = project_scores.project_id").where("projects.organization_id = ?", @organization.id).order("score DESC").limit(10).all @part_projects = @organization_projects.count < 9 ? find_miracle_project( 9 - @organization_projects.count, 3,"score desc") : [] # @cur_projects = Project.find(params[:organization]) # @organization = @cur_projects.enterprise_name diff --git a/app/views/admin/organization.html.erb b/app/views/admin/organization.html.erb index 9d07aa0df..582b8b44a 100644 --- a/app/views/admin/organization.html.erb +++ b/app/views/admin/organization.html.erb @@ -1 +1,43 @@ -111 \ No newline at end of file +
        + <%= link_to l(:label_organization_new), new_organization_path, :class => 'icon icon-add' %> +
        + +

        + <%=l(:label_organization_list)%> +

        + +
        +
        + + + + + + + + + <% @organizations.each do |org|%> + "> + + + + + <% end%> + +
        + <%=l(:label_organization)%> + + <%=l(:field_created_on)%> +
        + + <%= link_to org.name,home_path(:organization => org.id) %> + + + <%= format_date(org.created_at) %> + + <%= link_to(l(:button_change), edit_organization_path(org.id), :class => 'icon icon-copy') %> + <%= link_to(l(:button_delete), organization_path(org.id), :method => :delete,:confirm => l(:text_are_you_sure), :class => 'icon icon-del') %> +
        +
        + +<% html_title(l(:label_project_plural)) -%> diff --git a/app/views/admin/projects.html.erb b/app/views/admin/projects.html.erb index 762ceae9e..4c9479a6d 100644 --- a/app/views/admin/projects.html.erb +++ b/app/views/admin/projects.html.erb @@ -2,16 +2,25 @@ <%= link_to l(:label_project_new), {:controller => 'projects', :action => 'new'}, :class => 'icon icon-add' %> -

        <%=l(:label_project_plural)%>

        +

        + <%=l(:label_project_plural)%> +

        <%= form_tag({}, :method => :get) do %> -
        <%= l(:label_filter_plural) %> - -<%= select_tag 'status', project_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %> - -<%= text_field_tag 'name', params[:name], :size => 30 %> -<%= submit_tag l(:button_apply), :class => "small", :name => nil %> -<%= link_to l(:button_clear), {:controller => 'admin', :action => 'projects'}, :class => 'icon icon-reload' %> +
        + + <%= l(:label_filter_plural) %> + + + <%= select_tag 'status', project_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %> + + <%= text_field_tag 'name', params[:name], :size => 30 %> + <%= submit_tag l(:button_apply), :class => "small", :name => nil %> + <%= link_to l(:button_clear), {:controller => 'admin', :action => 'projects'}, :class => 'icon icon-reload' %>
        <% end %>   @@ -19,17 +28,31 @@
        - - - + + + <% project_tree(@projects) do |project, level| %> <%= project.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>"> - - - + + +
        <%=l(:label_project)%><%=l(:field_is_public)%><%=l(:field_created_on)%> + <%=l(:label_project)%> + + <%=l(:field_is_public)%> + + <%=l(:field_created_on)%> +
        <%= link_to_project_settings(project, {}) %><%= checked_image project.is_public? %><%= format_date(project.created_on) %> + + <%= link_to_project_settings(project, {}) %> + + + <%= checked_image project.is_public? %> + + <%= format_date(project.created_on) %> + <%= link_to(l(:button_archive), { :controller => 'projects', :action => 'archive', :id => project, :status => params[:status] }, :data => {:confirm => l(:text_are_you_sure)}, :method => :post, :class => 'icon icon-lock') unless project.archived? %> <%= link_to(l(:button_unarchive), { :controller => 'projects', :action => 'unarchive', :id => project, :status => params[:status] }, :method => :post, :class => 'icon icon-unlock') if project.archived? && (project.parent.nil? || !project.parent.archived?) %> diff --git a/config/locales/zh.yml b/config/locales/zh.yml index 362838800..231339a8e 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -538,7 +538,9 @@ zh: label_project: 项目 label_activity_project: '项目: ' #added by bai + label_organization: 组织 label_organization_list: 组织列表 + label_organization_new: 新建组织 label_project_plural: 项目列表 label_first_page_made: 首页定制 label_project_first_page: 项目托管平台首页 diff --git a/config/routes.rb b/config/routes.rb index fb4e40e49..53f6f6a4b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -36,7 +36,7 @@ RedmineApp::Application.routes.draw do resources :apply_project_masters delete 'apply_project_masters', :to => 'apply_project_masters#delete' - resources :organization, :only => [:index] do + resources :organization, :except => [:show] do end From e2307366f63e00961bcc1f8942044194976f743f Mon Sep 17 00:00:00 2001 From: sw <939547590@qq.com> Date: Thu, 5 Mar 2015 17:42:31 +0800 Subject: [PATCH 67/68] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E7=BB=84=E7=BB=87?= =?UTF-8?q?=E7=9A=84=E5=A2=9E=E3=80=81=E5=88=A0=E3=80=81=E6=94=B9=E3=80=81?= =?UTF-8?q?=E6=9F=A5=E5=8A=9F=E8=83=BD=E5=8F=8A=E7=9B=B8=E5=85=B3=E7=95=8C?= =?UTF-8?q?=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/organization_controller.rb | 45 ++++++++++++++++++++++ app/views/organization/_form.html.erb | 21 ++++++++++ app/views/organization/edit.html.erb | 25 ++++++++++++ app/views/organization/new.html.erb | 18 +++++++++ config/locales/zh.yml | 2 + 5 files changed, 111 insertions(+) create mode 100644 app/views/organization/_form.html.erb create mode 100644 app/views/organization/edit.html.erb create mode 100644 app/views/organization/new.html.erb diff --git a/app/controllers/organization_controller.rb b/app/controllers/organization_controller.rb index ef0919ced..db8295e44 100644 --- a/app/controllers/organization_controller.rb +++ b/app/controllers/organization_controller.rb @@ -1,5 +1,7 @@ class OrganizationController < ApplicationController layout 'project_base' + before_filter :require_admin, :except => [:index] + def index #@projects = Project.find_by_sql("SELECT * FROM projects WHERE id IN (select MAX(id) from projects GROUP BY enterprise_name)") @organizations = Organization.all @@ -7,4 +9,47 @@ class OrganizationController < ApplicationController format.html end end + + def new + @organizations = Organization.new + respond_to do |format| + format.html + end + end + + def create + @organizations = Organization.new + @organizations.name = params[:organization][:name] + if @organizations.save + redirect_to admin_organization_url + end + end + + def edit + @organization = Organization.find params[:id] + respond_to do |format| + format.html + end + rescue Exception => e + render_404 + end + + def update + @organization = Organization.find params[:id] + @organization.name = params[:organization][:name] + if @organization.save + redirect_to admin_organization_url + end + rescue Exception => e + render_404 + end + + def destroy + @organization = Organization.find params[:id] + if @organization.destroy + redirect_to admin_organization_url + end + rescue Exception => e + render_404 + end end diff --git a/app/views/organization/_form.html.erb b/app/views/organization/_form.html.erb new file mode 100644 index 000000000..80cc76850 --- /dev/null +++ b/app/views/organization/_form.html.erb @@ -0,0 +1,21 @@ +<%= error_messages_for 'project' %> + +<% unless @organizations.new_record? %> +

        + <%= render :partial=>"avatar/avatar_form",:locals=> {source:@organizations} %> +

        +<% end %> +

        + + <%= f.text_field :name, :required => true, :size => 60, :style => "width:290px;" %> +

        + + + <%#= l(:field_description)%> + + + + diff --git a/app/views/organization/edit.html.erb b/app/views/organization/edit.html.erb new file mode 100644 index 000000000..60b7c06a0 --- /dev/null +++ b/app/views/organization/edit.html.erb @@ -0,0 +1,25 @@ +<%= form_for(@organization) do |f|%> +

        + <%=l(:label_organization_edit)%> +

        +
        + <%= error_messages_for 'project' %> +

        + <%= render :partial=>"avatar/avatar_form",:locals=> {source:@organization} %> +

        +

        + + <%= f.text_field :name, :required => true, :size => 60, :style => "width:290px;" %> +

        + + <%= submit_tag l(:button_create), :class => "enterprise"%> + +
        + <%#= submit_tag l(:button_create_and_continue), :name => 'continue' %> + <%= javascript_tag "$('#project_name').focus();" %> +<% end %> + +<% html_title(l(:label_organization_edit)) -%> \ No newline at end of file diff --git a/app/views/organization/new.html.erb b/app/views/organization/new.html.erb new file mode 100644 index 000000000..163f4a5f5 --- /dev/null +++ b/app/views/organization/new.html.erb @@ -0,0 +1,18 @@ +<%= form_for(@organizations, :method => :post, + :name => 'new_form', + :url => {:controller => 'organization', + :action => 'create'}) do |f|%> +

        + <%=l(:label_organization_new)%> +

        +
        + <%= render :partial => 'form', :locals => { :f => f } %> + + <%= submit_tag l(:button_create), :class => "enterprise"%> + +
        + <%#= submit_tag l(:button_create_and_continue), :name => 'continue' %> + <%= javascript_tag "$('#project_name').focus();" %> +<% end %> + +<% html_title(l(:label_organization_new)) -%> \ No newline at end of file diff --git a/config/locales/zh.yml b/config/locales/zh.yml index 231339a8e..72634bd2b 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -539,8 +539,10 @@ zh: label_activity_project: '项目: ' #added by bai label_organization: 组织 + label_organization_name: 组织名称 label_organization_list: 组织列表 label_organization_new: 新建组织 + label_organization_edit: 修改组织 label_project_plural: 项目列表 label_first_page_made: 首页定制 label_project_first_page: 项目托管平台首页 From 26081c61205d549ffadb52195bd1ddb5f7d87a2e Mon Sep 17 00:00:00 2001 From: sw <939547590@qq.com> Date: Fri, 6 Mar 2015 09:27:43 +0800 Subject: [PATCH 68/68] =?UTF-8?q?=E6=96=B0=E5=BB=BA=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E6=97=B6=E5=A2=9E=E5=8A=A0=E4=B8=8D=E9=80=89=E6=8B=A9=E6=89=80?= =?UTF-8?q?=E5=B1=9E=E7=BB=84=E7=BB=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/helpers/projects_helper.rb | 4 ++++ config/locales/zh.yml | 1 + db/schema.rb | 30 ++++++++++++++---------------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index ccc750462..3c1663fcb 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -374,6 +374,10 @@ module ProjectsHelper def project_organizations_id_option type = [] + option1 = [] + option1 << l(:label_organization_choose) + option1 << 0 + type << option1 Organization.all.each do |org| option = [] option << org.name diff --git a/config/locales/zh.yml b/config/locales/zh.yml index 72634bd2b..edfea2cb7 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -539,6 +539,7 @@ zh: label_activity_project: '项目: ' #added by bai label_organization: 组织 + label_organization_choose: --请选择组织-- label_organization_name: 组织名称 label_organization_list: 组织列表 label_organization_new: 新建组织 diff --git a/db/schema.rb b/db/schema.rb index 4a3b14edc..b863dbaed 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20150302091345) do +ActiveRecord::Schema.define(:version => 20150305011359) do create_table "activities", :force => true do |t| t.integer "act_id", :null => false @@ -631,16 +631,6 @@ ActiveRecord::Schema.define(:version => 20150302091345) do add_index "journal_details", ["journal_id"], :name => "journal_details_journal_id" - create_table "journal_details_copy", :force => true do |t| - t.integer "journal_id", :default => 0, :null => false - t.string "property", :limit => 30, :default => "", :null => false - t.string "prop_key", :limit => 30, :default => "", :null => false - t.text "old_value" - t.text "value" - end - - add_index "journal_details_copy", ["journal_id"], :name => "journal_details_journal_id" - create_table "journal_replies", :id => false, :force => true do |t| t.integer "journal_id" t.integer "user_id" @@ -825,6 +815,13 @@ ActiveRecord::Schema.define(:version => 20150302091345) do t.integer "project_id" end + create_table "organizations", :force => true do |t| + t.string "name" + t.string "logo_link" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + create_table "poll_answers", :force => true do |t| t.integer "poll_question_id" t.text "answer_text" @@ -949,6 +946,7 @@ ActiveRecord::Schema.define(:version => 20150302091345) do t.integer "user_id" t.integer "dts_test", :default => 0 t.string "enterprise_name" + t.integer "organization_id" end add_index "projects", ["lft"], :name => "index_projects_on_lft" @@ -1036,12 +1034,12 @@ ActiveRecord::Schema.define(:version => 20150302091345) do end create_table "roles", :force => true do |t| - t.string "name", :limit => 90 - t.integer "position" - t.boolean "assignable" - t.integer "builtin" + t.string "name", :limit => 30, :default => "", :null => false + t.integer "position", :default => 1 + t.boolean "assignable", :default => true + t.integer "builtin", :default => 0, :null => false t.text "permissions" - t.string "issues_visibility", :limit => 90 + t.string "issues_visibility", :limit => 30, :default => "default", :null => false end create_table "schools", :force => true do |t|