diff --git a/app/assets/javascripts/memos.js b/app/assets/javascripts/memos.js new file mode 100644 index 000000000..dee720fac --- /dev/null +++ b/app/assets/javascripts/memos.js @@ -0,0 +1,2 @@ +// Place all the behaviors and hooks related to the matching controller here. +// All this logic will automatically be available in application.js. diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index d1357d43f..e60e37bce 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -160,7 +160,7 @@ class AccountsController < ApplicationController # 发送验证码 # params[:login] 手机号或者邮箱号 - # params[:type]为事件通知类型 1:用户注册注册 2:忘记密码 3: 绑定手机 4: 绑定邮箱 # 如果有新的继续后面加 + # params[:type]为事件通知类型 1:用户注册注册 2:忘记密码 3: 绑定手机 4: 绑定邮箱, 5: 验收手机号有效 # 如果有新的继续后面加 # 发送验证码:send_type 1:注册手机验证码 2:找回密码手机验证码 3:找回密码邮箱验证码 4:绑定手机 5:绑定邮箱 # 6:手机验证码登录 7:邮箱验证码登录 8:邮箱注册验证码 9: 验收手机号有效 def get_verification_code @@ -200,7 +200,7 @@ class AccountsController < ApplicationController session[:user_id] = nil end - # type 事件类型 1:用户注册 2:忘记密码 3: 绑定手机 4: 绑定邮箱 # 如果有新的继续后面加 + # type 事件类型 1:用户注册 2:忘记密码 3: 绑定手机 4: 绑定邮箱, 5: 验证手机号是否有效 # 如果有新的继续后面加 # login_type 1:手机类型 2:邮箱类型 def verify_type login_type, type case type @@ -212,6 +212,8 @@ class AccountsController < ApplicationController login_type == 1 ? 4 : tip_exception('请填写正确的手机号') when 4 login_type == 1 ? tip_exception('请填写正确的邮箱') : 5 + when 5 + login_type == 1 ? 9 : tip_exception('请填写正确的手机号') end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index f01ad4b6b..7aa89dbd7 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -38,6 +38,7 @@ class ApplicationController < ActionController::Base def user_course_identity @user_course_identity = current_user.course_identity(@course) if @user_course_identity > Course::STUDENT && @course.is_public == 0 + tip_exception(401, "..") unless User.current.logged? tip_exception(409, "您没有权限进入") end uid_logger("###############user_course_identity:#{@user_course_identity}") @@ -63,10 +64,10 @@ class ApplicationController < ActionController::Base # 发送及记录激活码 # 发送验证码:type 1:注册手机验证码 2:找回密码手机验证码 3:找回密码邮箱验证码 4:绑定手机 5:绑定邮箱 - # 6:手机验证码登录 7:邮箱验证码登录 8:邮箱注册验证码 + # 6:手机验证码登录 7:邮箱验证码登录 8:邮箱注册验证码 9:验证手机号有效 def check_verification_code(code, send_type, value) case send_type - when 1, 2, 4 + when 1, 2, 4, 9 # 手机类型的发送 sigle_para = {phone: value} status = Educoder::Sms.send(mobile: value, code: code) @@ -240,14 +241,17 @@ class ApplicationController < ActionController::Base User.current = User.find 57703 end - if params[:debug] == 'teacher' #todo 为了测试,记得讲debug删除 - User.current = User.find 81403 - elsif params[:debug] == 'student' - User.current = User.find 8686 - elsif params[:debug] == 'admin' - User.current = User.find 1 - end + # 测试版前端需求 + if request.host == "47.96.87.25" + if params[:debug] == 'teacher' #todo 为了测试,记得讲debug删除 + User.current = User.find 81403 + elsif params[:debug] == 'student' + User.current = User.find 8686 + elsif params[:debug] == 'admin' + User.current = User.find 1 + end + end # User.current = User.find 81403 end @@ -582,4 +586,8 @@ class ApplicationController < ActionController::Base def render_parameter_missing render json: { status: -1, message: '参数缺失' } end + + def set_export_cookies + cookies[:fileDownload] = true + end end diff --git a/app/controllers/concerns/paginate_helper.rb b/app/controllers/concerns/paginate_helper.rb index 34740eb5d..bbe84a348 100644 --- a/app/controllers/concerns/paginate_helper.rb +++ b/app/controllers/concerns/paginate_helper.rb @@ -1,7 +1,7 @@ module PaginateHelper def paginate(objs, **opts) page = params[:page].to_i <= 0 ? 1 : params[:page].to_i - per_page = params[:per_page].to_i > 0 ? params[:per_page].to_i : opts[:per_page] || 20 + per_page = params[:per_page].to_i > 0 && params[:per_page].to_i < 50 ? params[:per_page].to_i : opts[:per_page] || 20 Kaminari.paginate_array(objs).page(page).per(per_page) end diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb index bdb367111..abc3e4856 100644 --- a/app/controllers/courses_controller.rb +++ b/app/controllers/courses_controller.rb @@ -1027,10 +1027,13 @@ class CoursesController < ApplicationController tip_exception(403,"无权限操作") elsif @all_members.size == 0 normal_status(-1,"课堂暂时没有学生") + elsif params[:export].present? && params[:export] + normal_status(0,"正在下载中") else + set_export_cookies member_to_xlsx(@course, @all_members, @c_homeworks, @c_exercises, @c_tasks) - filename_ = "#{current_user.real_name}_#{@course.name}_全部成绩" - render xlsx: "#{format_sheet_name filename_.strip.first(30)}",template: "courses/export_member_scores_excel.xlsx.axlsx", + filename_ = "#{current_user.real_name}_#{@course.name}_全部成绩_#{Time.now.strftime('%Y%m%d_%H%M%S')}" + render xlsx: "#{format_sheet_name filename_.strip}",template: "courses/export_member_scores_excel.xlsx.axlsx", locals: {course_info:@course_info, activity_level:@user_activity_level, course_scores:@course_user_scores,shixun_works:@shixun_work_arrays, common_works:@common_work_arrays,group_works:@group_work_arrays,task_works:@task_work_arrays, diff --git a/app/controllers/discusses_controller.rb b/app/controllers/discusses_controller.rb index 3b987be51..6a270f619 100644 --- a/app/controllers/discusses_controller.rb +++ b/app/controllers/discusses_controller.rb @@ -1,7 +1,7 @@ class DiscussesController < ApplicationController LIMIT = 10 before_action :find_container, only: [:index, :hidden] - before_action :find_discuss, except: [:create, :index, :new_message, :reward_code] + before_action :find_discuss, except: [:create, :index, :new_message, :reward_code, :forum_discusses] def index page = params[:page].to_i @@ -28,6 +28,48 @@ class DiscussesController < ApplicationController @current_user = current_user end + def forum_discusses + page = params[:page] || 1 + limit = params[:limit] || 15 + offset = (page.to_i-1) * limit + search = params[:search] + tag = params[:tag_repertoire_id] + sql, sql1, sql2 = '', '', '' + sql1 = + unless search.blank? + "and d.content like '%#{search}%'" + end + + sql2 = + if tag + shixun_ids = ShixunTagRepertoire.where(:tag_repertoire_id => tag).pluck(:shixun_id) + "and d.dis_id in(#{shixun_ids.join(",")})" + end + + sql = "select d.id from discusses d join shixuns s on d.dis_id = s.id where s.status = 2 and s.hidden = false and d.root_id is null + and d.hidden = false #{sql1} #{sql2} order by d.created_at desc" + + memo_ids = Discuss.find_by_sql(sql).pluck(:id) + @memo_count = memo_ids.size + memo_ids = memo_ids[offset, limit] + order_ids = memo_ids.size > 0 ? memo_ids.join(',') : -1 + @memos = Discuss.where(id: memo_ids).order("field(id,#{order_ids})").includes(:praise_treads, dis: :tag_repertoires, user: :user_extension) + # @memos = memos.includes(:praise_treads, user: :user_extension).page(page).per(limit) + # 实训标签使用最多的9个 + # @hot_tags = TagRepertoire.find_by_sql("select distinct(a.name), a.id from + # (select tr.id, tr.name, count(d.dis_id) cnt + # from tag_repertoires tr join (shixun_tag_repertoires str + # left join (shixuns s join discusses d on d.dis_id = s.id) + # on s.id = str.shixun_id) on tr.id = str.tag_repertoire_id + # group by d.dis_id order by cnt desc) a limit 9").map{|ht| ht.attributes.dup} + tag_id = ShixunTagRepertoire.joins(:shixun).order("myshixuns_count desc").pluck(:tag_repertoire_id).uniq.first(9) + @hot_tags = TagRepertoire.select([:id, :name]).where(id: tag_id).order("FIELD(id, #{tag_id.join(",")})").map{|ht| ht.attributes.dup} if tag_id + + @memos = DiscussesService.new.memo_list @memos + @hot_memos = Memo.field_for_recommend.posts.hot.includes(:tag_repertoires).limit(4) + @recommend_shixuns = DiscussesService.new.recommends + end + def new_message onclick_time = Myshixun.find(params[:myshixun_id]).try(:onclick_time) ids = Discuss.where(user_id: User.current.id, dis_id: params[:container_id], dis_type: params[:container_type]). diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 03c943f8d..6f121db5f 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -1256,13 +1256,16 @@ class ExercisesController < ApplicationController normal_status(-1,"试卷未发布") elsif (@exercise_users_size == 0) || ( @export_ex_users&.exercise_user_committed.size == 0) normal_status(-1,"暂无用户提交") + elsif params[:export].present? && params[:export] + normal_status(0,"正在下载中") else respond_to do |format| format.xlsx{ + set_export_cookies get_export_users(@exercise,@course,@export_ex_users) exercise_export_name_ = "#{current_user.real_name}_#{@course.name}_#{@exercise.exercise_name}_#{Time.now.strftime('%Y%m%d_%H%M%S')}" - render xlsx: "#{exercise_export_name_.strip.first(30)}",template: "exercises/exercise_lists.xlsx.axlsx",locals: {table_columns:@table_columns,exercise_users:@user_columns} + render xlsx: "#{exercise_export_name_.strip}",template: "exercises/exercise_lists.xlsx.axlsx",locals: {table_columns:@table_columns,exercise_users:@user_columns} } end end @@ -1281,7 +1284,12 @@ class ExercisesController < ApplicationController @exercise_questions = @exercise.exercise_questions.includes(:exercise_choices).order("question_number ASC") filename_ = "#{@exercise.user.real_name}_#{@course.name}_#{Time.now.strftime('%Y%m%d_%H%M%S')}.pdf" stylesheets = "#{Rails.root}/app/templates/exercise_export/exercise_export.css" - render pdf: 'exercise_export/blank_exercise', filename: filename_, stylesheets: stylesheets + if params[:export].present? && params[:export] + normal_status(0,"正在下载中") + else + set_export_cookies + render pdf: 'exercise_export/blank_exercise', filename: filename_, stylesheets: stylesheets + end end #空白试卷预览页面,仅供测试使用,无其他任何用途 diff --git a/app/controllers/games_controller.rb b/app/controllers/games_controller.rb index b2de6dcad..b3b062c1a 100644 --- a/app/controllers/games_controller.rb +++ b/app/controllers/games_controller.rb @@ -116,10 +116,11 @@ class GamesController < ApplicationController @qrcode_str = Base64.encode64( qr.to_img.resize(400,400).to_s ) else - @type = "image" #conv = Iconv.new("GBK", "utf-8") @game_challenge = @game.challenge type = @game_challenge.show_type + @type = shixun_show_type type + workspace_path = @game.try(:picture_path) @answer_path = "#{Rails.root}/#{workspace_path}/#{@game_challenge.expect_picture_path}" @user_path = "#{Rails.root}/#{workspace_path}/#{@game_challenge.picture_path}" diff --git a/app/controllers/graduation_tasks_controller.rb b/app/controllers/graduation_tasks_controller.rb index ab99e8d86..0885eeae2 100644 --- a/app/controllers/graduation_tasks_controller.rb +++ b/app/controllers/graduation_tasks_controller.rb @@ -132,12 +132,14 @@ class GraduationTasksController < ApplicationController tip_exception(403, "无权限操作") elsif complete_works == 0 normal_status(-1,"暂无用户提交") + elsif params[:export].present? && params[:export] + normal_status(0,"正在下载中") else respond_to do |format| format.xlsx{ graduation_work_to_xlsx(@work_excel,@task,current_user) task_export_name_ = "#{current_user.real_name}_#{@course.name}_#{@task.name}_#{Time.now.strftime('%Y%m%d_%H%M%S')}" - render xlsx: "#{task_export_name_.strip.first(30)}",template: "graduation_tasks/tasks_list.xlsx.axlsx",locals: {table_columns:@head_cells_column, task_users:@task_cells_column} + render xlsx: "#{task_export_name_.strip}",template: "graduation_tasks/tasks_list.xlsx.axlsx",locals: {table_columns:@head_cells_column, task_users:@task_cells_column} } end end @@ -148,12 +150,17 @@ class GraduationTasksController < ApplicationController zip_works = @work_excel.where("work_status > 0") status = checkfileSize(zip_works) if status == 0 - respond_to do |format| - format.zip{ - zipfile = zip_homework_common @task, zip_works - file = decode64(zipfile[0][:base64file]) - send_file "#{OUTPUT_FOLDER}/#{file}", filename: filename_for_content_disposition(file), type: 'application/zip' - } + if params[:export].present? && params[:export] + normal_status(0,"正在下载中") + else + respond_to do |format| + format.zip{ + set_export_cookies + zipfile = zip_homework_common @task, zip_works + file = decode64(zipfile[0][:base64file]) + send_file "#{OUTPUT_FOLDER}/#{file}", filename: filename_for_content_disposition(file), type: 'application/zip' + } + end end else normal_status(status,status == -2 ? "500M" : "无附件可下载") diff --git a/app/controllers/graduation_topics_controller.rb b/app/controllers/graduation_topics_controller.rb index 754b472c5..bd93401f3 100644 --- a/app/controllers/graduation_topics_controller.rb +++ b/app/controllers/graduation_topics_controller.rb @@ -271,7 +271,12 @@ class GraduationTopicsController < ApplicationController students = course.students.joins(user: :user_extension).order("user_extensions.student_id") graduation_topic_to_xlsx(students,course) topic_export_name_ = "#{current_user.real_name}_#{course.name}_毕设选题_#{Time.now.strftime('%Y%m%d_%H%M%S')}" - render xlsx: "#{topic_export_name_.strip.first(30)}",template: "graduation_topics/export.xlsx.axlsx",locals: {table_columns:@topic_head_cells,topic_users:@topic_body_cells} + if params[:export].present? && params[:export] + normal_status(0,"正在下载中") + else + set_export_cookies + render xlsx: "#{topic_export_name_.strip}",template: "graduation_topics/export.xlsx.axlsx",locals: {table_columns:@topic_head_cells,topic_users:@topic_body_cells} + end rescue Exception => e uid_logger(e.message) missing_template diff --git a/app/controllers/homework_commons_controller.rb b/app/controllers/homework_commons_controller.rb index 5cccb3ee7..603a59920 100644 --- a/app/controllers/homework_commons_controller.rb +++ b/app/controllers/homework_commons_controller.rb @@ -207,9 +207,12 @@ class HomeworkCommonsController < ApplicationController tip_exception(403, "无权限操作") elsif @work_excel.blank? || @work_excel.size == 0 normal_status(-1,"暂无用户提交!") + elsif params[:export].present? && params[:export] + normal_status(0,"正在下载中") else respond_to do |format| format.xlsx{ + set_export_cookies student_work_to_xlsx(@work_excel,@homework) exercise_export_name = "#{current_user.real_name}_#{@course.name}_#{@homework.name}_#{Time.now.strftime('%Y%m%d_%H%M%S')}" render xlsx: "#{exercise_export_name.strip}",template: "homework_commons/works_list.xlsx.axlsx",locals: @@ -229,12 +232,17 @@ class HomeworkCommonsController < ApplicationController end if status == 0 - respond_to do |format| - format.zip{ - zipfile = zip_homework_common @homework, zip_works - file = decode64(zipfile[0][:base64file]) - send_file "#{OUTPUT_FOLDER}/#{file}", filename: filename_for_content_disposition(file), type: 'application/zip' - } + if params[:export].present? && params[:export] + normal_status(0,"正在下载中") + else + respond_to do |format| + format.zip{ + set_export_cookies + zipfile = zip_homework_common @homework, zip_works + file = decode64(zipfile[0][:base64file]) + send_file "#{OUTPUT_FOLDER}/#{file}", filename: filename_for_content_disposition(file), type: 'application/zip' + } + end end else normal_status(status, status == -2 ? "500M" : "无附件可下载") @@ -328,7 +336,7 @@ class HomeworkCommonsController < ApplicationController @messages = @messages.parent_comment end - @messages = @messages.page(@page).per(@limit).order("created_on desc") + @messages = @messages.includes(:praise_treads).page(@page).per(@limit).order("created_on desc") end def reference_answer diff --git a/app/controllers/memos_controller.rb b/app/controllers/memos_controller.rb index 723f2e9a4..66adcc46b 100644 --- a/app/controllers/memos_controller.rb +++ b/app/controllers/memos_controller.rb @@ -1,5 +1,9 @@ class MemosController < ApplicationController - before_action :set_memo, only: [:show, :edit, :update, :destroy] + before_action :require_login, except: [:show, :index] + before_action :set_memo, only: [:show, :edit, :update, :destroy, :sticky_or_cancel, :hidden, :more_reply] + before_action :validate_memo_params, only: [:create, :update] + before_action :owner_or_admin, only: [:edit, :update, :destroy] + before_action :is_admin, only: [:sticky_or_cancel, :hidden] include ApplicationHelper # GET /memos @@ -8,27 +12,21 @@ class MemosController < ApplicationController @user = current_user @memos = Memo.all s_order = (params[:order] == "replies_count" ? "all_replies_count" : params[:order]) || "updated_at" - #@tidding_count = unviewed_tiddings(current_user) if current_user.present? - page = params[:page].to_i + # @tidding_count = unviewed_tiddings(current_user) if current_user.present? + page = params[:page] || 1 + limit = params[:limit] || 15 search = params[:search] - offset = page * 15 forum_id = params[:forum] - user_id = params[:user_id] - if user_id == -1 - user_id = current_user.try(:id) - end tag_repertoire_id = params[:tag_repertoire_id] sql = if forum_id - search ? "forum_id = #{forum_id} and root_id is null and subject like '%#{search}%'" : + !search.blank? ? "forum_id = #{forum_id} and root_id is null and subject like '%#{search}%'" : "forum_id = #{forum_id} and root_id is null" - elsif search - user_id ? "author_id = #{user_id.to_i} and forum_id in(3, 5) and root_id is null and subject like '%#{search}%'" : - "forum_id in(3, 5) and root_id is null and subject like '%#{search}%'" + elsif !search.blank? + "forum_id in(3, 5) and root_id is null and subject like '%#{search}%'" else - user_id ? "author_id = #{user_id.to_i} and forum_id in(3, 5) and root_id is null" : - "forum_id in(3, 5) and root_id is null" + "forum_id in(3, 5) and root_id is null" end if tag_repertoire_id @@ -41,75 +39,74 @@ class MemosController < ApplicationController sql += " and all_replies_count != 0" end - memos = Memo.field_for_list.includes(:praise_tread, :author).where("#{sql}") + memos = Memo.field_for_list.where("#{sql}") @memos_count = memos.length - @memos = memos.order("sticky = 1 desc, #{Memo.table_name}.#{s_order} desc").offset(offset).limit(15) - @my_memos_count = Memo.user_posts(current_user.try(:id)).count + @memos = memos.order("sticky = 1 desc, #{Memo.table_name}.#{s_order} desc").page(page).per(limit) + @memos = @memos.includes(:praise_treads, :tag_repertoires, author: :user_extension) + # @my_memos_count = Memo.user_posts(current_user.try(:id)).count @tags_info = MemoTagRepertoire.find_by_sql("SELECT tag_repertoire_id, tr.name, count(*) cnt FROM memo_tag_repertoires mtr join tag_repertoires tr on tr.id = mtr.tag_repertoire_id group by tag_repertoire_id order by cnt desc, tag_repertoire_id desc limit 9") - @hot_memos = Memo.field_for_recommend.posts.hot.limit(4) + @hot_memos = Memo.field_for_recommend.posts.hot.includes(:tag_repertoires).limit(4) + @recommend_shixuns = DiscussesService.new.recommends end - # GET /memos/1 # GET /memos/1.json def show # tidding_count = unviewed_tiddings(current_user) if current_user @user = current_user - # TODO 附件最后再做 - # attachments_list = @memo.update_column(:viewed_count, @memo.viewed_count+1) - @memos = @memo.reply_for_memo.includes(:praise_tread, :author).order("created_at desc").limit(10) - + @memos = @memo.reply_for_memo.includes(:praise_treads, author: :user_extension).order("created_at desc").limit(10) + @attachments = @memo.attachments + @recommend_shixuns = DiscussesService.new.recommends end # GET /memos/new def new - @csrf_token = session[:_csrf_toke] ||= SecureRandom.base64(32) @tag_list = TagRepertoire.field_for_list.order("name asc") - end # GET /memos/1/edit def edit + @tag_list = TagRepertoire.field_for_list.order("name asc") + @memo_tags = @memo.tag_repertoires.field_for_list + @attachments = @memo.attachments end - # POST /memos # POST /memos.json def create ActiveRecord::Base.transaction do begin @memo = Memo.new(memo_params) @memo.author = current_user - # TODO 保存附件 - # @memo.save_attachments(params[:attachments]) if params[:attachments] @memo.save! + Attachment.associate_container(params[:attachment_ids], @memo.id, @memo.class.name) params[:tags].each do |tag| - MemoTagRepertoire.create(:memo_id => @memo.id, :tag_repertoire_id => tag) + MemoTagRepertoire.create!(memo_id: @memo.id, tag_repertoire_id: tag) end - @status = 0 - @message = "帖子创建成功!" + normal_status("帖子创建成功") rescue Exception => e - @status = -1 - @message = "帖子创建失败,原因:#{e}" + tip_exception("帖子创建失败,原因:#{e}") raise ActiveRecord::Rollback end end - - end - # PATCH/PUT /memos/1 # PATCH/PUT /memos/1.json def update - respond_to do |format| - if @memo.update(memo_params) - format.html { redirect_to @memo, notice: 'Memo was successfully updated.' } - format.json { render :show, status: :ok, location: @memo } - else - format.html { render :edit } - format.json { render json: @memo.errors, status: :unprocessable_entity } + ActiveRecord::Base.transaction do + begin + @memo.update_attributes!(memo_params) + Attachment.associate_container(params[:attachment_ids], @memo.id, @memo.class.name) + @memo.memo_tag_repertoires.destroy_all + params[:tags].each do |tag| + MemoTagRepertoire.create!(memo_id: @memo.id, tag_repertoire_id: tag) + end + normal_status("帖子更新成功") + rescue Exception => e + tip_exception("帖子更新失败,原因:#{e}") + raise ActiveRecord::Rollback end end end @@ -118,21 +115,88 @@ class MemosController < ApplicationController # DELETE /memos/1.json def destroy @memo.destroy - respond_to do |format| - format.html { redirect_to memos_url, notice: 'Memo was successfully destroyed.' } - format.json { head :no_content } + normal_status("删除成功") + end + + def sticky_or_cancel + tip_exception("只能对主贴进行置顶操作") unless @memo.parent_id.nil? + begin + @memo.update_attributes!(sticky: !@memo.sticky) + normal_status("更新成功") + rescue Exception => e + tip_exception("更新失败,原因:#{e}") + raise ActiveRecord::Rollback end end - private - # Use callbacks to share common setup or constraints between actions. - def set_memo - @memo = Memo.find(params[:id]) + def hidden + tip_exception("不能对主贴进行隐藏操作") if @memo.parent_id.nil? + begin + @memo.update_attributes!(hidden: !@memo.hidden) + normal_status("更新成功") + rescue Exception => e + tip_exception("更新失败,原因:#{e}") + raise ActiveRecord::Rollback end + end + + def reply + tip_exception("parent_id不能为空") if params[:parent_id].blank? + tip_exception("content不能为空") if params[:content].blank? - # Never trust parameters from the scary internet, only allow the white list through. - def memo_params - params.fetch(:memo, {}) + ActiveRecord::Base.transaction do + begin + memo = Memo.find_by!(id: params[:parent_id]) + reply = Memo.new + reply.content = params[:content] + reply.author = current_user + reply.forum_id = memo.forum_id + reply.subject = memo.subject + reply.root_id = memo.root_id || memo.id + memo.children << reply + m = Memo.find_by!(id: reply.root_id) + m.increment!(:all_replies_count) + normal_status("回复成功") + rescue Exception => e + tip_exception("回复失败,原因:#{e}") + raise ActiveRecord::Rollback + end end + end + + def more_reply + @user = current_user + page = params[:page] || 2 + limit = params[:limit] || 10 + offset = (page.to_i - 1) * limit + @memos_count = Memo.where(parent_id: @memo.id).count + @memos = Memo.limit(limit).where(parent_id: @memo.id).includes(:author, :praise_treads).order("created_at desc").offset(offset) + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_memo + @memo = Memo.find(params[:id]) + end + + def owner_or_admin + tip_exception(403, "无权限操作") unless @memo.author == current_user || current_user.admin? || current_user.business? + end + + def is_admin + tip_exception(403, "无权限操作") unless current_user.admin? || current_user.business? + end + + # Never trust parameters from the scary internet, only allow the white list through. + def memo_params + params.require(:memo).permit(:subject, :content, :forum_id) + end + + def validate_memo_params + tip_exception("话题名称不能为空") if params[:subject].blank? + tip_exception("话题内容不能为空") if params[:content].blank? + tip_exception("话题类型不能为空") if params[:forum_id].blank? + tip_exception("技术标签不能为空") if params[:tags].blank? + end end diff --git a/app/controllers/myshixuns_controller.rb b/app/controllers/myshixuns_controller.rb index fa47fd7f6..1d6b15969 100644 --- a/app/controllers/myshixuns_controller.rb +++ b/app/controllers/myshixuns_controller.rb @@ -24,7 +24,7 @@ class MyshixunsController < ApplicationController ActiveRecord::Base.transaction do begin @shixun = Shixun.select(:id, :identifier).find(@myshixun.shixun_id) - @myshixun.destroy + @myshixun.destroy! StudentWork.where(:myshixun_id => @myshixun.id).update_all(:myshixun_id => 0, :work_status => 0) diff --git a/app/controllers/polls_controller.rb b/app/controllers/polls_controller.rb index 4348b3bfc..7d978d722 100644 --- a/app/controllers/polls_controller.rb +++ b/app/controllers/polls_controller.rb @@ -945,12 +945,15 @@ class PollsController < ApplicationController tip_exception(403,"无权限操作") elsif (@poll.polls_status == 1) || (@poll_export_questions.size == 0) || (@poll_commit_ids.size == 0) normal_status(-1,"暂无用户提交") + elsif params[:export].present? && params[:export] + normal_status(0,"正在下载中") else respond_to do |format| format.xlsx{ + set_export_cookies polls_export_name_ = "#{current_user.real_name}_#{@course.name}_#{@poll.polls_name}_#{Time.now.strftime('%Y%m%d_%H%M%S')}" polls_user_commit = poll_commit_result(@poll,@poll_export_questions,@poll_users,@poll_commit_ids) - render xlsx: "#{polls_export_name_.strip.first(30)}",template: "polls/commit_result.xlsx.axlsx",locals: {polls_user_commit:polls_user_commit} + render xlsx: "#{polls_export_name_.strip}",template: "polls/commit_result.xlsx.axlsx",locals: {polls_user_commit:polls_user_commit} } end end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index c9ed6c597..672477790 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -1,10 +1,2 @@ class ProjectsController < ApplicationController - def search - query_params = { keyword: params[:keyword], category: 'manage' } - projects = Users::ProjectService.new(current_user, query_params).call - - params[:limit] = params[:per_page].to_i.zero? ? 20 : params[:per_page].to_i - @count = projects.count - @projects = paginate projects - end end \ No newline at end of file diff --git a/app/controllers/shixuns_controller.rb b/app/controllers/shixuns_controller.rb index 6c3f34a60..26effd12e 100644 --- a/app/controllers/shixuns_controller.rb +++ b/app/controllers/shixuns_controller.rb @@ -714,6 +714,7 @@ class ShixunsController < ApplicationController end end rescue Exception => e + logger.info("shixun_exec error: #{e.message}") if e.message != "ActiveRecord::RecordInvalid" logger.error("##########project_fork error #{e.message}") @myshixun.destroy! diff --git a/app/controllers/student_works_controller.rb b/app/controllers/student_works_controller.rb index f9f4717a6..a5d46e46a 100644 --- a/app/controllers/student_works_controller.rb +++ b/app/controllers/student_works_controller.rb @@ -457,8 +457,8 @@ class StudentWorksController < ApplicationController @myself_eff = @echart_data[:efficiency_list].find { |item| item.last == @user.id } @myself_consume = @echart_data[:consume_list].find { |item| item.last == @user.id } - filename_ = "实训报告_#{@shixun&.name}_#{@use&.real_name}" - filename = Base64.urlsafe_encode64(filename_.strip.first(30)) + filename_ = "#{@use&.student_id}_#{@use&.real_name}_#{@shixun&.name}_#{Time.now.strftime('%Y%m%d_%H%M%S')}" + filename = Base64.urlsafe_encode64(filename_.strip) stylesheets = %w(shixun_work/shixun_work.css shared/codemirror.css) render pdf: 'shixun_work/shixun_work', filename: filename, stylesheets: stylesheets end diff --git a/app/controllers/users/base_controller.rb b/app/controllers/users/base_controller.rb index afc03ee13..969aca320 100644 --- a/app/controllers/users/base_controller.rb +++ b/app/controllers/users/base_controller.rb @@ -26,11 +26,24 @@ class Users::BaseController < ApplicationController render_forbidden end + def page_value + params[:page].to_i <= 0 ? 1 : params[:page].to_i + end + + def per_page_value + params[:per_page].to_i > 0 && params[:per_page].to_i < 50 ? params[:per_page].to_i : 20 + end + alias_method :limit_value, :per_page_value + + def offset_value + (page_value - 1) * limit_value + end + def paginate(objs, **opts) - page = params[:page].to_i <= 0 ? 1 : params[:page].to_i - per_page = params[:per_page].to_i > 0 ? params[:per_page].to_i : 20 + page = page_value + per_page = per_page_value - return Kaminari.paginate_array(objs).page(page).per(per_page) unless observed_logged_user? && opts[:special] + return Kaminari.paginate_array(objs).page(page).per(per_page) unless opts[:special] && observed_logged_user? # note: 为实现第一页少一条记录,让前端放置新建入口 if page == 1 diff --git a/app/controllers/users/private_message_details_controller.rb b/app/controllers/users/private_message_details_controller.rb new file mode 100644 index 000000000..486d23d7f --- /dev/null +++ b/app/controllers/users/private_message_details_controller.rb @@ -0,0 +1,23 @@ +class Users::PrivateMessageDetailsController < Users::BaseController + before_action :private_user_resources! + + after_action :update_message_status, only: [:show] + + def show + messages = observed_user.private_messages.without_deleted.where(target: target_user) + + @count = messages.count + @messages = messages.order(send_time: :asc).includes(sender: :user_extension) + end + + private + + def target_user + @_target_user ||= User.find(params[:target_id]) + end + + # 置为已读 + def update_message_status + observed_user.private_messages.only_unread.where(target: target_user).update_all(status: 1) + end +end \ No newline at end of file diff --git a/app/controllers/users/private_messages_controller.rb b/app/controllers/users/private_messages_controller.rb new file mode 100644 index 000000000..b80b6152b --- /dev/null +++ b/app/controllers/users/private_messages_controller.rb @@ -0,0 +1,39 @@ +class Users::PrivateMessagesController < Users::BaseController + before_action :private_user_resources! + after_action :update_onclick_time!, only: [:index] + + def index + @count = observed_user.private_messages.without_deleted.group(:target_id).count.count + + subquery = observed_user.private_messages.without_deleted.order(send_time: :desc).to_sql + query = "SELECT subquery.*, COUNT(*) message_count FROM (#{subquery}) subquery "\ + "GROUP BY subquery.target_id ORDER BY subquery.send_time desc LIMIT #{limit_value} OFFSET #{offset_value}" + @messages = PrivateMessage.select('*').from("(#{query}) AS query").includes(target: :user_extension) + end + + def create + receiver = User.find_by(id: params[:target_id]) + return render_error('用户未找到') if receiver.blank? + + @message = PrivateMessages::CreateService.call(observed_user, receiver, create_params) + rescue PrivateMessages::CreateService::Error => ex + render_error(ex.message) + end + + def destroy + message = observed_user.private_messages.without_deleted.find(params[:id]) + message.destroy! + + render_ok + end + + private + + def update_onclick_time! + current_user.onclick_time.touch(:onclick_time) + end + + def create_params + params.permit(:content) + end +end \ No newline at end of file diff --git a/app/controllers/users/projects_controller.rb b/app/controllers/users/projects_controller.rb index 863b99b37..07f4d5cac 100644 --- a/app/controllers/users/projects_controller.rb +++ b/app/controllers/users/projects_controller.rb @@ -1,4 +1,6 @@ class Users::ProjectsController < Users::BaseController + skip_before_action :check_observed_user_exists!, only: [:search] + def index projects = Users::ProjectService.new(observed_user, query_params).call @@ -6,6 +8,15 @@ class Users::ProjectsController < Users::BaseController @projects = paginate(projects.includes(:project_score, owner: { user_extension: :school }), special: true) end + def search + query_params = { keyword: params[:keyword], category: 'manage' } + projects = Users::ProjectService.new(current_user, query_params).call + + params[:limit] = params[:per_page].to_i.zero? ? 20 : params[:per_page].to_i + @count = projects.count + @projects = paginate projects + end + private def query_params diff --git a/app/controllers/users/recent_contacts_controller.rb b/app/controllers/users/recent_contacts_controller.rb new file mode 100644 index 000000000..bc4b8ea8f --- /dev/null +++ b/app/controllers/users/recent_contacts_controller.rb @@ -0,0 +1,8 @@ +class Users::RecentContactsController < Users::BaseController + before_action :private_user_resources! + + def index + contacts = observed_user.recent_contacts.distinct + @contacts = contacts.order('private_messages.created_at DESC').limit(10).includes(:user_extension) + end +end \ No newline at end of file diff --git a/app/controllers/users/unread_message_infos_controller.rb b/app/controllers/users/unread_message_infos_controller.rb new file mode 100644 index 000000000..7abd36304 --- /dev/null +++ b/app/controllers/users/unread_message_infos_controller.rb @@ -0,0 +1,12 @@ +class Users::UnreadMessageInfosController < Users::BaseController + before_action :private_user_resources! + + def show + click_time = observed_user.click_time + + unread_tiding_count = observed_user.tidings.where('created_at > ?', click_time).count + unread_message_count = observed_user.private_messages.only_unread.group(:target_id).count.count + + render_ok(unread_tiding_count: unread_tiding_count, unread_message_count: unread_message_count) + end +end \ No newline at end of file diff --git a/app/controllers/users_for_private_messages_controller.rb b/app/controllers/users_for_private_messages_controller.rb new file mode 100644 index 000000000..bbd5682a1 --- /dev/null +++ b/app/controllers/users_for_private_messages_controller.rb @@ -0,0 +1,17 @@ +class UsersForPrivateMessagesController < ApplicationController + before_action :require_login, :check_auth + + def index + users = User.active.where.not(id: current_user.id) + + keyword = params[:keyword].to_s.strip + if keyword.blank? + @users = [] + return + end + + users = users.where('LOWER(concat(lastname, firstname, nickname)) LIKE ?', "%#{keyword}%") + + @users = users.limit(10).includes(:user_extension) + end +end \ No newline at end of file diff --git a/app/controllers/zips_controller.rb b/app/controllers/zips_controller.rb index f4822b96b..3ed1eb8b3 100644 --- a/app/controllers/zips_controller.rb +++ b/app/controllers/zips_controller.rb @@ -8,7 +8,13 @@ class ZipsController < ApplicationController def shixun_report service = BatchExportShixunReportService.new(@homework, @all_student_works) filename_ = filename_for_content_disposition(service.filename) - send_file service.zip, filename: filename_, type: 'application/zip' + if params[:export].present? && params[:export] + normal_status(0,"正在下载中") + else + set_export_cookies + send_file service.zip, filename: filename_, type: 'application/zip' + end + rescue BatchExportShixunReportService::Error => ex normal_status(-1, ex.message) end @@ -18,7 +24,12 @@ class ZipsController < ApplicationController exercises = ExportExercisesService.new(@exercise,@ex_users,@request_url) file_name_ = filename_for_content_disposition(exercises.filename) - send_file exercises.ex_zip, filename: file_name_, type: 'application/zip' + if params[:export].present? && params[:export] + normal_status(0,"正在下载中") + else + set_export_cookies + send_file exercises.ex_zip, filename: file_name_, type: 'application/zip' + end rescue Exception => e normal_status(-1, e.message) end diff --git a/app/decorators/private_message_decorator.rb b/app/decorators/private_message_decorator.rb new file mode 100644 index 000000000..6db17acf3 --- /dev/null +++ b/app/decorators/private_message_decorator.rb @@ -0,0 +1,9 @@ +module PrivateMessageDecorator + extend ApplicationDecorator + + display_time_method :send_time + + def unread? + status.zero? + end +end \ No newline at end of file diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 5df86ef54..7e9fd652b 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -329,6 +329,23 @@ module ApplicationHelper content end + def strip_html(text, len=0, endss="...") + ss = "" + if !text.nil? && text.length>0 + ss=text.gsub(/<\/?.*?>/, '').strip + ss = ss.gsub(/ */, '') + ss = ss.gsub(/\r\n/,'') #新增 + ss = ss.gsub(/\n/,'') #新增 + if len > 0 && ss.length > len + ss = ss[0, len] + endss + elsif len > 0 && ss.length <= len + ss = ss + #ss = truncate(ss, :length => len) + end + end + ss + end + def strip_export_title(content) con_ = "" if content.length > 0 diff --git a/app/helpers/export_helper.rb b/app/helpers/export_helper.rb index 742deb85b..5d36c465f 100644 --- a/app/helpers/export_helper.rb +++ b/app/helpers/export_helper.rb @@ -418,7 +418,7 @@ module ExportHelper end end - out_file_name = "#{Time.now.to_i}_#{homework_common.name}.zip" + out_file_name = "作品附件_#{homework_common&.course&.name}_#{homework_common.name}_#{Time.now.strftime('%Y%m%d_%H%M%S')}.zip" out_file_name.gsub!(" ", "-") out_file_name.gsub!("/", "_") out_file = find_or_pack(homework_common, homework_common.user_id, digests.sort){ @@ -496,8 +496,8 @@ module ExportHelper def make_zip_name(work, file_name="") Rails.logger.info("######################file_name: #{file_name}") - name = file_name === "" ? "" : (file_name[0, file_name.rindex('.')]+"_") - "#{name}#{work.user.real_name}_#{((work.user.student_id.nil?) ? "" : work.user.student_id)}" + # name = file_name === "" ? "" : (file_name[0, file_name.rindex('.')]+"_") + "#{work&.user&.student_id}_#{work&.user&.real_name}_#{Time.now.strftime('%Y%m%d_%H%M%S')}" end def zipping(zip_name_refer, files_paths, output_path, is_attachment=false, not_exist_file=[]) diff --git a/app/helpers/games_helper.rb b/app/helpers/games_helper.rb index aff05904f..5a9871673 100644 --- a/app/helpers/games_helper.rb +++ b/app/helpers/games_helper.rb @@ -6,7 +6,7 @@ module GamesHelper end # 获取目录下所有文件,返回一个文件名的数组 type是查看文件的类型image表示图片 - # type [[1, "图片"], [2, "apk/exe"], [3, "txt"], [4, "html"]] + # type [[1, "图片"], [2, "apk/exe"], [3, "txt"], [4, "html"], [5, "mp3"], [6, "mp4"]] def get_dir_filename(path, type, game_id) answer_picture = [] return answer_picture unless File.directory?(path) @@ -39,6 +39,12 @@ module GamesHelper end f.close @type = 'txt' + elsif extension == 'mp3' && type == 5 + answer_picture << file + @type = 'mp3' + elsif extension == 'mp4' && type == 6 + answer_picture << file + @type = 'mp4' end end @@ -51,4 +57,21 @@ module GamesHelper "编译失败,请在测试结果中查看具体的错误信息" : test_set.try(:actual_output) end end + + def shixun_show_type type + case type.to_i + when 1 + "image" + when 2 + "apk/exe" + when 3 + "txt" + when 4 + "html" + when 5 + "mp3" + when 6 + "mp4" + end + end end diff --git a/app/helpers/memos_helper.rb b/app/helpers/memos_helper.rb index 7df887a4b..434d9b66a 100644 --- a/app/helpers/memos_helper.rb +++ b/app/helpers/memos_helper.rb @@ -1,2 +1,6 @@ module MemosHelper + + def forum_list + [{id: 5, name: "技术分享"}, {id: 3, name: "操作指南"}] + end end diff --git a/app/models/challenge.rb b/app/models/challenge.rb index 0bea54eb4..8e4d2ae42 100644 --- a/app/models/challenge.rb +++ b/app/models/challenge.rb @@ -1,5 +1,6 @@ class Challenge < ApplicationRecord # difficulty: 关卡难度: 1.简单 2.中等 3.困难 + # show_type: 效果展示:-1.无效果 1.图片 2.apk/exe 3.txt 4.html 5.mp3 6.mp4 default_scope { order("challenges.position asc") } belongs_to :shixun, :touch => true, counter_cache: true diff --git a/app/models/discuss.rb b/app/models/discuss.rb index 2d9c00110..a50b18a6f 100644 --- a/app/models/discuss.rb +++ b/app/models/discuss.rb @@ -8,6 +8,7 @@ class Discuss < ApplicationRecord has_many :praise_treads, as: :praise_tread_object, dependent: :destroy has_many :tidings, as: :container, dependent: :destroy has_one :praise_tread_cache, as: :object, dependent: :destroy + belongs_to :dis, polymorphic: true belongs_to :challenge after_create :send_tiding @@ -44,6 +45,10 @@ class Discuss < ApplicationRecord Discuss.where(parent_id: self.id).includes(:user).reorder(created_at: :asc) end + def child_discuss_count + Discuss.where(root_id: id).count + end + private def send_tiding diff --git a/app/models/forum.rb b/app/models/forum.rb new file mode 100644 index 000000000..88aafa676 --- /dev/null +++ b/app/models/forum.rb @@ -0,0 +1,2 @@ +class Forum < ApplicationRecord +end diff --git a/app/models/memo.rb b/app/models/memo.rb index 610a7684d..e70a7cec9 100644 --- a/app/models/memo.rb +++ b/app/models/memo.rb @@ -1,10 +1,12 @@ class Memo < ApplicationRecord include Searchable::Memo - has_many :memo_tag_repertoires, :dependent => :destroy + belongs_to :forum, touch: true + + has_many :memo_tag_repertoires, dependent: :destroy has_many :tag_repertoires, :through => :memo_tag_repertoires - has_many :praise_tread, as: :praise_tread_object, dependent: :destroy + has_many :praise_treads, as: :praise_tread_object, dependent: :destroy has_one :praise_tread_cache, as: :object, dependent: :destroy belongs_to :author, class_name: 'User', foreign_key: 'author_id' @@ -12,6 +14,7 @@ class Memo < ApplicationRecord has_many :descendants, foreign_key: :root_id, class_name: 'Memo' has_many :children, foreign_key: :parent_id, class_name: 'Memo' + has_many :attachments, as: :container, dependent: :destroy scope :field_for_list, lambda{ select([:id, :subject, :author_id, :sticky, :updated_at, :language, :reward, :all_replies_count, :viewed_count, :forum_id]) diff --git a/app/models/private_message.rb b/app/models/private_message.rb index 1db4c9f66..640e48db7 100644 --- a/app/models/private_message.rb +++ b/app/models/private_message.rb @@ -1,3 +1,9 @@ class PrivateMessage < ApplicationRecord belongs_to :user + belongs_to :target, class_name: "User" + belongs_to :sender, class_name: "User" + belongs_to :receiver, class_name: "User" + + scope :without_deleted, -> { where.not(status: 2) } + scope :only_unread, -> { where(status: 0) } end diff --git a/app/models/tiding.rb b/app/models/tiding.rb index 3ef625c57..90abdf809 100644 --- a/app/models/tiding.rb +++ b/app/models/tiding.rb @@ -13,9 +13,14 @@ class Tiding < ApplicationRecord value = container.try(:identifier) end + if value.blank? && parent_container_type && Object.const_defined?(parent_container_type) + value = parent_container_type.try(:identifier) + end + if value.blank? && belong_container_type && Object.const_defined?(belong_container_type) value = belong_container.try(:identifier) end + value end end \ No newline at end of file diff --git a/app/models/user.rb b/app/models/user.rb index 14d7b2697..b453f9998 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -54,7 +54,8 @@ class User < ApplicationRecord has_one :onclick_time, :dependent => :destroy # 新版私信 - has_many :private_messages, :dependent => :destroy + has_many :private_messages, dependent: :destroy + has_many :recent_contacts, through: :private_messages, source: :target has_many :tidings, :dependent => :destroy has_many :games, :dependent => :destroy @@ -439,7 +440,7 @@ class User < ApplicationRecord end def manager_of_memo?(memo) - id == memo.author_id || admin? + id == memo.author_id || admin? || business? end # 是否是项目管理者 diff --git a/app/services/batch_export_shixun_report_service.rb b/app/services/batch_export_shixun_report_service.rb index d71235790..253eb4480 100644 --- a/app/services/batch_export_shixun_report_service.rb +++ b/app/services/batch_export_shixun_report_service.rb @@ -14,7 +14,7 @@ class BatchExportShixunReportService end def filename - @_filename ||= "#{homework.name}_#{Time.now.strftime('%Y%m%d_%H%M%S')}" + @_filename ||= "实训报告_#{homework&.course&.name}_#{homework.name}_#{Time.now.strftime('%Y%m%d_%H%M%S')}" end def zip diff --git a/app/services/discusses_service.rb b/app/services/discusses_service.rb index 91fa523c9..c14850a4c 100644 --- a/app/services/discusses_service.rb +++ b/app/services/discusses_service.rb @@ -148,30 +148,6 @@ class DiscussesService @discuss = Discuss.select([:id, :hidden, :reward, :dis_type, :dis_id, :position, :challenge_id, :root_id]).find(id) end - protected - def memo_list memos - memos.map do |m| - user = User.find(m.user_id) - praise_count = m.praise_tread.where(:praise_or_tread => 1).count - replies_count = Discuss.where(:root_id => m.id).count - shixun_tag = m.dis.tag_repertoires.map(&:name) - m.attributes.dup.except("user_id", "dis_id", "dis_type", "root_id").merge({ - subject: (message_content m.content), - username: user.show_name, - login: user.login, - praise_count: praise_count, - replies_count: replies_count, - image_url: url_to_avatar(user), - shixun_tag: shixun_tag, - tpm_url: "/shixuns/#{m.dis.identifier}/shixun_discuss" - }) - end - end - - def format_for_current_user current_user - {username: current_user.show_name, login: current_user.login, user_id: current_user.id, image_url: url_to_avatar(current_user), admin: current_user.admin?} - end - def recommends hot_shixuns = Shixun.field_for_recommend.published.order("myshixuns_count desc").limit(2) newest_shixuns = Shixun.field_for_recommend.published.order("created_at desc").limit(2) @@ -185,6 +161,30 @@ class DiscussesService return recommend_shixuns end + def memo_list memos + memos.map do |m| + user = m.user + # praise_count = m.praise_treads.select{|pt| pt.praise_or_tread == 1}.count + replies_count = m.child_discuss_count + shixun_tag = m.dis.tag_repertoires.map(&:name) + m.attributes.dup.except("user_id", "dis_id", "dis_type", "root_id", "praise_count", "content").merge({ + subject: (message_content m.content), + username: user.full_name, + login: user.login, + replies_count: replies_count, + image_url: url_to_avatar(user), + shixun_tag: shixun_tag, + tpm_url: "/shixuns/#{m.dis.identifier}/shixun_discuss" + }) + end + end + + protected + + def format_for_current_user current_user + {username: current_user.show_name, login: current_user.login, user_id: current_user.id, image_url: url_to_avatar(current_user), admin: current_user.admin?} + end + # 将数据库对象转换成哈希对象 def object_to_hash objects objects.map{|o| o.attributes.dup} diff --git a/app/services/exercise_user_pdf_service.rb b/app/services/exercise_user_pdf_service.rb index 73711192f..6891e53ea 100644 --- a/app/services/exercise_user_pdf_service.rb +++ b/app/services/exercise_user_pdf_service.rb @@ -15,8 +15,8 @@ class ExerciseUserPdfService end def filename - user_course = @course.course_members.find_by(user_id:@ex_user_user.id)&.course_group_name - exercise_user_name = "#{@ex_user_user.real_name}_#{user_course.present? ? user_course : "未分班"}_#{exercise.exercise_name}_#{Time.now.strftime('%Y%m%d_%H%M')}" + # user_course = @course.course_members.find_by(user_id:@ex_user_user.id)&.course_group_name + exercise_user_name = "#{@ex_user_user&.student_id}_#{@ex_user_user.real_name}_#{exercise.exercise_name}_#{Time.now.strftime('%Y%m%d_%H%M')}" "#{exercise_user_name.strip}.pdf" end diff --git a/app/services/export_exercises_service.rb b/app/services/export_exercises_service.rb index ca2d347a6..1f1e15326 100644 --- a/app/services/export_exercises_service.rb +++ b/app/services/export_exercises_service.rb @@ -10,7 +10,7 @@ class ExportExercisesService end def filename - exercise_export_name = "#{exercise.user.real_name}_#{exercise.exercise_name}_#{Time.now.strftime('%Y%m%d_%H%M%S')}" + exercise_export_name = "学生答题_#{exercise&.course&.name}_#{exercise.exercise_name}_#{Time.now.strftime('%Y%m%d_%H%M%S')}" "#{exercise_export_name.strip}.zip" end diff --git a/app/services/export_shixun_report_service.rb b/app/services/export_shixun_report_service.rb index f9da3d6f2..57b955762 100644 --- a/app/services/export_shixun_report_service.rb +++ b/app/services/export_shixun_report_service.rb @@ -10,7 +10,7 @@ class ExportShixunReportService end def filename - @_filename ||= "#{homework.name}-#{work.user&.student_id}-#{work.user.real_name}.pdf".gsub(' ', '-').gsub('/', '_') + @_filename ||= "#{work.user&.student_id}_#{work.user.real_name}_#{homework.name}_#{Time.now.strftime('%Y%m%d_%H%M%S')}.pdf".gsub(' ', '-').gsub('/', '_') end def prepare_binding diff --git a/app/services/private_messages/create_service.rb b/app/services/private_messages/create_service.rb new file mode 100644 index 000000000..88f3a084f --- /dev/null +++ b/app/services/private_messages/create_service.rb @@ -0,0 +1,35 @@ +class PrivateMessages::CreateService < ApplicationService + Error = Class.new(StandardError) + + attr_reader :sender, :receiver, :params + + def initialize(sender, receiver, params) + @sender = sender + @receiver = receiver + @params = params + end + + def call + validate! + + same_attr = { sender: sender, receiver: receiver, content: content, send_time: Time.now } + + message = nil + ActiveRecord::Base.transaction do + message = sender.private_messages.create!(same_attr.merge(target: receiver, status: 1)) + receiver.private_messages.create!(same_attr.merge(target: sender, status: 0)) + end + message + end + + private + + def content + @_content ||= params[:content].to_s.strip + end + + def validate! + raise Error, '内容不能为空' if content.blank? + raise Error, '内容太长' if content.size > 255 + end +end \ No newline at end of file diff --git a/app/services/project_packages/save_service.rb b/app/services/project_packages/save_service.rb index bcfc19a10..a876f56b3 100644 --- a/app/services/project_packages/save_service.rb +++ b/app/services/project_packages/save_service.rb @@ -15,7 +15,7 @@ class ProjectPackages::SaveService < ApplicationService is_create = package.new_record? raise Error, '类型不存在' unless ProjectPackageCategory.where(id: params[:category_id]).exists? - params[:project_package_category_id] = params[:category_id].to_i + params[:project_package_category_id] = params.delete(:category_id).to_i raise Error, '竞标截止时间不能小于当前时间' if params[:deadline_at].present? && params[:deadline_at].to_time < Time.now @@ -25,7 +25,9 @@ class ProjectPackages::SaveService < ApplicationService end ActiveRecord::Base.transaction do - package.assign_attributes(params) + columns = %i[project_package_category_id title content deadline_at + min_price max_price contact_name contact_phone] + package.assign_attributes(params.slice(*columns)) package.save! # 处理附件 diff --git a/app/services/projects/apply_join_service.rb b/app/services/projects/apply_join_service.rb index a177de930..d14b3dc52 100644 --- a/app/services/projects/apply_join_service.rb +++ b/app/services/projects/apply_join_service.rb @@ -22,7 +22,7 @@ class Projects::ApplyJoinService < ApplicationService apply.forge_activities.find_or_create_by!(user: user, project: project) - notify_project_manager! + notify_project_manager!(apply) end # notify_project_owner @@ -47,10 +47,13 @@ class Projects::ApplyJoinService < ApplicationService end end - def notify_project_manager! + def notify_project_manager!(apply) columns = %i[user_id applied_id applied_type status viewed applied_user_id role project_id created_at updated_at] AppliedMessage.bulk_insert(*columns) do |worker| - base_attr = { status: false, viewed: false, applied_user_id: user.id, role: role_value, project_id: project.id } + base_attr = { + applied_id: apply.id, applied_type: 'AppliedProject', status: false, viewed: false, + applied_user_id: user.id, role: role_value, project_id: project.id + } project.manager_members.each do |manager| worker.add(base_attr.merge(user_id: manager.user_id)) diff --git a/app/views/courses/search_users.json.jbuilder b/app/views/courses/search_users.json.jbuilder index ac826fdf2..61362de66 100644 --- a/app/views/courses/search_users.json.jbuilder +++ b/app/views/courses/search_users.json.jbuilder @@ -1,6 +1,7 @@ json.users do json.array! @users do |user| json.id user.id + json.login user.login json.name user.real_name json.student_id user&.student_id json.school_name user&.school_name diff --git a/app/views/discusses/forum_discusses.json.jbuilder b/app/views/discusses/forum_discusses.json.jbuilder new file mode 100644 index 000000000..3b1aefd89 --- /dev/null +++ b/app/views/discusses/forum_discusses.json.jbuilder @@ -0,0 +1,27 @@ +json.memo_list @memos +# do |memo| +# json.(memo, :id, :updated_at, :reward) +# json.subject message_content(memo.content) +# json.praise_count memo.praises_count +# json.replies_count memo.child_discuss_count +# json.shixun_tag memo.dis.tag_repertoires.map(&:name) +# json.username memo.user.full_name +# json.login memo.user.login +# json.image_url url_to_avatar(memo.user) +# json.tpm_url "/shixuns/#{memo.dis.identifier}/shixun_discuss" +# end + +json.memo_count @memo_count + +json.hot_memos do + json.array! @hot_memos do |hm| + json.(hm, :id, :subject, :language, :forum_id) + json.replies_count hm.all_replies_count + # json.praise_count hm.praise_tread.praise_count + json.tag hm.tag_repertoires.map(&:name) + end +end + +json.hot_tags @hot_tags + +json.recommend_shixuns @recommend_shixuns \ No newline at end of file diff --git a/app/views/discusses/reward_code.json.jbuilder b/app/views/discusses/reward_code.json.jbuilder index e5e0e9152..e2a98192b 100644 --- a/app/views/discusses/reward_code.json.jbuilder +++ b/app/views/discusses/reward_code.json.jbuilder @@ -1 +1,2 @@ +json.status 0 json.code @code \ No newline at end of file diff --git a/app/views/games/picture_display.json.jbuilder b/app/views/games/picture_display.json.jbuilder index 541c478a9..770f0d71b 100644 --- a/app/views/games/picture_display.json.jbuilder +++ b/app/views/games/picture_display.json.jbuilder @@ -24,5 +24,29 @@ elsif @type == "txt" json.contents @contents.html_safe elsif @type =="qrcode" json.qrcode_str @qrcode_str - +elsif @type == "mp3" || @type == "mp4" + # if @type == "mp4" + # json.orignal_file [{"file_url": "http://120.27.231.56:48080/attachments/download/378171/123.mp4"}] + # json.user_file [{"file_url": "http://120.27.231.56:48080/attachments/download/378172/456.mp4"}] + # json.answer_file [{"file_url": "http://120.27.231.56:48080/attachments/download/378173/789.mp4"}] + # else + # json.orignal_file [{"file_url": "http://120.27.231.56:48080/attachments/download/378174/58099.mp3"}] + # json.user_file [{"file_url": "http://120.27.231.56:48080/attachments/download/378175/654058514.mp3"}] + # json.answer_file [{"file_url": "http://120.27.231.56:48080/attachments/download/378175/654058514.mp3"}] + # end + json.orignal_file do + json.array! @orignal_picture do |file| + json.file_url attachment_show_users_path(:file_name => file, :path => @original_path) + end + end + json.user_file do + json.array! @user_picture do |file| + json.file_url attachment_show_users_path(:file_name => file, :path => @user_path, :time => Time.now.to_i) + end + end + json.answer_file do + json.array! @answer_picture do |file| + json.file_url attachment_show_users_path(:file_name => file, :path => @answer_path) + end + end end \ No newline at end of file diff --git a/app/views/graduation_topics/_graduation_comments.json.jbuilder b/app/views/graduation_topics/_graduation_comments.json.jbuilder index 1d49d450f..ee9ed7529 100644 --- a/app/views/graduation_topics/_graduation_comments.json.jbuilder +++ b/app/views/graduation_topics/_graduation_comments.json.jbuilder @@ -10,7 +10,7 @@ json.hidden message.hidden if message.m_parent_id json.can_delete message.can_delete(identity) else - json.praise_count message.praise_treads.liker.count - json.user_praise message.praise_treads.user_liker(current_user).count + json.praise_count message.praise_treads.select{|pt| pt.praise_or_tread == 1}.count + json.user_praise message.praise_treads.select{|pt| pt.praise_or_tread == 1 && user_id == current_user.id}.count json.child_message_count message.m_reply_count end diff --git a/app/views/memos/_memo.json.jbuilder b/app/views/memos/_memo.json.jbuilder index 22adba5fc..bc3744d1f 100644 --- a/app/views/memos/_memo.json.jbuilder +++ b/app/views/memos/_memo.json.jbuilder @@ -1,8 +1,14 @@ -json.(memo, :id, :subject, :is_md, :content, :sticky, :reward, :viewed_count) - -json.tag memo.tag_repertoires.map(&:name) -json.time memo.created_at -json.replies_count memo.all_replies_count -json.attachments_list [] -json.user_praise memo.praise_tread.user_liker(@user.try(:id)) ? true : false -json.memo_praise_count = memo.praise_tread.liker.count +json.memo do + json.id memo.id + json.subject memo.subject + json.is_md memo.is_md + json.content memo.content + json.sticky memo.sticky + json.reward memo.reward + json.viewed_count memo.viewed_count + json.tag memo.tag_repertoires.map(&:name) + json.time memo.created_at + json.replies_count memo.all_replies_count + json.user_praise memo.praise_treads.user_liker(@user.try(:id)) ? true : false + json.memo_praise_count memo.praise_treads.liker.count +end diff --git a/app/views/memos/_memo_list.json.jbuilder b/app/views/memos/_memo_list.json.jbuilder index 8da056e92..1f45070a1 100644 --- a/app/views/memos/_memo_list.json.jbuilder +++ b/app/views/memos/_memo_list.json.jbuilder @@ -1,7 +1,7 @@ json.(memo, :id, :subject, :author_id, :sticky, :updated_at, :language, :reward, :all_replies_count, :viewed_count, :forum_id) -json.praise_count memo.praise_tread.praise_count +json.praise_count memo.praise_treads.liker.count json.replies_count memo.all_replies_count json.tag memo.tag_repertoires.map(&:name) json.user_name memo.author.full_name diff --git a/app/views/memos/_replies_list.json.jbuilder b/app/views/memos/_replies_list.json.jbuilder index 3dae61813..9ec6976c2 100644 --- a/app/views/memos/_replies_list.json.jbuilder +++ b/app/views/memos/_replies_list.json.jbuilder @@ -7,10 +7,10 @@ json.username memo.author.full_name json.reward memo.reward json.hidden memo.hidden json.permission @user.manager_of_memo?(memo) -json.praise_count memo.praise_tread.liker.count -json.user_praise memo.praise_tread.select{|pt| pt.user_id == @user.id}.length > 0 +json.praise_count memo.praise_treads.select{|pt| pt.praise_or_tread == 1}.count +json.user_praise memo.praise_treads.select{|pt| pt.praise_or_tread == 1 && pt.user_id == @user.id}.length > 0 json.user_login memo.author.login -json.admin @user.admin +json.admin @user.admin? || @user.business? json.children do json.array! memo.children_of_reply do |child| diff --git a/app/views/memos/edit.json.jbuilder b/app/views/memos/edit.json.jbuilder new file mode 100644 index 000000000..f3d3f3724 --- /dev/null +++ b/app/views/memos/edit.json.jbuilder @@ -0,0 +1,7 @@ +json.(@memo, :subject, :content, :forum_id) +json.memo_tags @memo_tags +json.attachments @attachments do |attachment| + json.partial! "attachments/attachment_simple", locals: {attachment: attachment} +end +json.tag_list @tag_list +json.forums forum_list diff --git a/app/views/memos/index.json.jbuilder b/app/views/memos/index.json.jbuilder index 8a90e374a..e004f7ea7 100644 --- a/app/views/memos/index.json.jbuilder +++ b/app/views/memos/index.json.jbuilder @@ -8,22 +8,30 @@ # tidding_count: 消息数 # # -json.memo_list do - json.array! @memos do |memo| - json.partial! "memos/memo_list", locals: {memo: memo} - end +json.memo_list @memos do |memo| + json.(memo, :id, :subject, :sticky, + :updated_at, :language, :reward, + :viewed_count, :forum_id) + json.praise_count memo.praise_treads.select{|pt| pt.praise_or_tread == 1}.count + json.replies_count memo.all_replies_count + json.tag memo.tag_repertoires.map(&:name) + json.user_name memo.author.full_name + json.login memo.author.login + json.image_url url_to_avatar(memo.author) end json.memo_count @memos_count json.hot_memos do json.array! @hot_memos do |hm| - json.(hm, :id, :subject, :language, :forum_id, :all_replies_count) + json.(hm, :id, :subject, :language, :forum_id) json.replies_count hm.all_replies_count - json.praise_count hm.praise_tread.praise_count + # json.praise_count hm.praise_tread.praise_count json.tag hm.tag_repertoires.map(&:name) end end json.hot_tags @tags_info.map{|o| o.attributes.dup.except("cnt", "id")} +json.recommend_shixuns @recommend_shixuns + diff --git a/app/views/memos/more_reply.json.jbuilder b/app/views/memos/more_reply.json.jbuilder new file mode 100644 index 000000000..af1a9f1ac --- /dev/null +++ b/app/views/memos/more_reply.json.jbuilder @@ -0,0 +1,7 @@ +json.memo_replies do + json.array! @memos do |memo| + json.partial! "memos/replies_list", memo: memo + end +end + +json.memos_count @memos_count \ No newline at end of file diff --git a/app/views/memos/new.json.jbuilder b/app/views/memos/new.json.jbuilder index 14313b2ac..c35066023 100644 --- a/app/views/memos/new.json.jbuilder +++ b/app/views/memos/new.json.jbuilder @@ -1,3 +1,2 @@ json.tag_list @tag_list -json.csrf_token @csrf_token - +json.forums forum_list \ No newline at end of file diff --git a/app/views/memos/show.json.jbuilder b/app/views/memos/show.json.jbuilder index 7acd6ba4a..1f7840983 100644 --- a/app/views/memos/show.json.jbuilder +++ b/app/views/memos/show.json.jbuilder @@ -1,5 +1,9 @@ json.partial! "memos/memo", memo: @memo +json.attachments_list @attachments do |attachment| + json.partial! "attachments/attachment_simple", locals: {attachment: attachment} +end + json.memo_replies do json.array! @memos do |memo| json.partial! "memos/replies_list", memo: memo @@ -9,10 +13,11 @@ end json.author_info do json.username @memo.author.full_name # TODO watched_by 插件没法用,等把lib文件载入后在打开代码 - #json.watched @memo.author.watched_by?(@user) + json.watched @user.watched?(@memo.author) json.image_url url_to_avatar(@memo.author) json.identity @memo.author.identity json.login @memo.author.login json.user_id @memo.author.id end +json.recommend_shixuns @recommend_shixuns \ No newline at end of file diff --git a/app/views/users/get_user_info.json.jbuilder b/app/views/users/get_user_info.json.jbuilder index d9eb736ad..fc2eca762 100644 --- a/app/views/users/get_user_info.json.jbuilder +++ b/app/views/users/get_user_info.json.jbuilder @@ -1,4 +1,5 @@ json.username @user.full_name +json.real_name @user.real_name json.login @user.login json.user_id @user.id json.image_url url_to_avatar(@user) @@ -7,6 +8,7 @@ json.is_teacher @user.user_extension&.teacher? json.user_identity @user.identity json.tidding_count 0 json.user_phone_binded @user.phone.present? +json.phone @user.phone json.profile_completed @user.profile_completed? if @course json.course_identity @course_identity diff --git a/app/views/users/private_message_details/show.json.jbuilder b/app/views/users/private_message_details/show.json.jbuilder new file mode 100644 index 000000000..065767fb4 --- /dev/null +++ b/app/views/users/private_message_details/show.json.jbuilder @@ -0,0 +1,11 @@ +json.count @count +json.messages do + json.array! @messages.each do |message| + json.extract! message, :id, :user_id, :receiver_id, :sender_id, :content + + json.send_time message.display_send_time + json.sender do + json.partial! 'users/user_simple', user: message.sender + end + end +end \ No newline at end of file diff --git a/app/views/users/private_messages/create.json.jbuilder b/app/views/users/private_messages/create.json.jbuilder new file mode 100644 index 000000000..888cfeff6 --- /dev/null +++ b/app/views/users/private_messages/create.json.jbuilder @@ -0,0 +1,10 @@ +json.status 0 +json.message 'success' +json.private_message do + json.extract! @message, :id, :user_id, :receiver_id, :sender_id, :content + + json.send_time @message.display_send_time + json.sender do + json.partial! 'users/user_simple', user: @message.sender + end +end \ No newline at end of file diff --git a/app/views/users/private_messages/index.json.jbuilder b/app/views/users/private_messages/index.json.jbuilder new file mode 100644 index 000000000..c18a7d209 --- /dev/null +++ b/app/views/users/private_messages/index.json.jbuilder @@ -0,0 +1,13 @@ +json.count @count +json.private_messages do + json.array! @messages.each do |message| + json.extract! message, :id, :content, :message_count + + json.unread message.unread? + json.send_time message.display_send_time + + json.target do + json.partial! 'users/user_simple', user: message.target + end + end +end \ No newline at end of file diff --git a/app/views/projects/search.json.jbuilder b/app/views/users/projects/search.json.jbuilder similarity index 100% rename from app/views/projects/search.json.jbuilder rename to app/views/users/projects/search.json.jbuilder diff --git a/app/views/users/recent_contacts/index.json.jbuilder b/app/views/users/recent_contacts/index.json.jbuilder new file mode 100644 index 000000000..f2f7f025a --- /dev/null +++ b/app/views/users/recent_contacts/index.json.jbuilder @@ -0,0 +1,2 @@ +json.users @contacts, partial: 'users/user_simple', as: :user +json.count @contacts.size \ No newline at end of file diff --git a/app/views/users_for_private_messages/index.json.jbuilder b/app/views/users_for_private_messages/index.json.jbuilder new file mode 100644 index 000000000..be040e368 --- /dev/null +++ b/app/views/users_for_private_messages/index.json.jbuilder @@ -0,0 +1,2 @@ +json.users @users, partial: 'users/user_simple', as: :user +json.count @users.size \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 6623edd99..79542817f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -20,7 +20,18 @@ Rails.application.routes.draw do put 'commons/unhidden', to: 'commons#unhidden' delete 'commons/delete', to: 'commons#delete' - resources :memos + resources :memos do + member do + post :sticky_or_cancel + post :hidden + get :more_reply + end + + collection do + post :reply + end + end + resources :tem_tests # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html # @@ -53,6 +64,11 @@ Rails.application.routes.draw do resource :grade_records, only: [:show] resource :watch, only: [:create, :destroy] resources :project_packages, only: [:index] + # 私信 + resources :private_messages, only: [:index, :create, :destroy] + resources :recent_contacts, only: [:index] + resource :private_message_details, only: [:show] + resource :unread_message_info, only: [:show] end @@ -70,7 +86,7 @@ Rails.application.routes.draw do get :system_update resource :trial_apply, only: [:create] - resources :projects, only: [] do + resources :projects, module: :users, only: [] do get :search, on: :collection end @@ -91,6 +107,7 @@ Rails.application.routes.draw do end end end + resources :users_for_private_messages, only: [:index] resources :myshixuns, param: :identifier, shallow: true do member do @@ -216,6 +233,7 @@ Rails.application.routes.draw do resources :discusses do collection do get :new_message + get :forum_discusses end member do @@ -697,7 +715,7 @@ Rails.application.routes.draw do resources :libraries, only: [:index, :show, :create, :update, :destroy] scope module: :projects do - resources :applied_projects, only: [:create] + resources :project_applies, only: [:create] end end diff --git a/db/migrate/20190730024112_add_index_to_user.rb b/db/migrate/20190730024112_add_index_to_user.rb new file mode 100644 index 000000000..fa198cffa --- /dev/null +++ b/db/migrate/20190730024112_add_index_to_user.rb @@ -0,0 +1,7 @@ +class AddIndexToUser < ActiveRecord::Migration[5.2] + def change + # add_index :users, :login, unique: true + # add_index :users, :mail, unique: true + # add_index :users, :phone, unique: true + end +end diff --git a/public/compatibility.html b/public/compatibility.html index f13c673f5..f28bad2c8 100644 --- a/public/compatibility.html +++ b/public/compatibility.html @@ -5,9 +5,9 @@ EduCoder - - - + + + diff --git a/public/images/educoder/path.png b/public/images/educoder/path.png new file mode 100644 index 000000000..d3d816012 Binary files /dev/null and b/public/images/educoder/path.png differ diff --git a/public/images/educoder/project_packages/ai.png b/public/images/educoder/project_packages/ai.png new file mode 100755 index 000000000..825e75165 Binary files /dev/null and b/public/images/educoder/project_packages/ai.png differ diff --git a/public/images/educoder/project_packages/backend.png b/public/images/educoder/project_packages/backend.png new file mode 100755 index 000000000..359bca746 Binary files /dev/null and b/public/images/educoder/project_packages/backend.png differ diff --git a/public/images/educoder/project_packages/cloud_compute_and_big_data.png b/public/images/educoder/project_packages/cloud_compute_and_big_data.png new file mode 100755 index 000000000..42e64da3b Binary files /dev/null and b/public/images/educoder/project_packages/cloud_compute_and_big_data.png differ diff --git a/public/images/educoder/project_packages/database.png b/public/images/educoder/project_packages/database.png new file mode 100755 index 000000000..2b668e774 Binary files /dev/null and b/public/images/educoder/project_packages/database.png differ diff --git a/public/images/educoder/project_packages/devops_and_test.png b/public/images/educoder/project_packages/devops_and_test.png new file mode 100755 index 000000000..77b8e6f3b Binary files /dev/null and b/public/images/educoder/project_packages/devops_and_test.png differ diff --git a/public/images/educoder/project_packages/front.png b/public/images/educoder/project_packages/front.png new file mode 100755 index 000000000..23ba2a755 Binary files /dev/null and b/public/images/educoder/project_packages/front.png differ diff --git a/public/images/educoder/project_packages/mobile.png b/public/images/educoder/project_packages/mobile.png new file mode 100755 index 000000000..cd73520c7 Binary files /dev/null and b/public/images/educoder/project_packages/mobile.png differ diff --git a/public/images/educoder/project_packages/other.png b/public/images/educoder/project_packages/other.png new file mode 100755 index 000000000..a700cd5de Binary files /dev/null and b/public/images/educoder/project_packages/other.png differ diff --git a/public/images/educoder/project_packagesHead.jpg b/public/images/educoder/project_packagesHead.jpg new file mode 100644 index 000000000..181045e03 Binary files /dev/null and b/public/images/educoder/project_packagesHead.jpg differ diff --git a/public/javascripts/download/jquery.fileDownload.js b/public/javascripts/download/jquery.fileDownload.js new file mode 100644 index 000000000..0d0301c23 --- /dev/null +++ b/public/javascripts/download/jquery.fileDownload.js @@ -0,0 +1,490 @@ +/* +* jQuery File Download Plugin v1.4.5 +* +* http://www.johnculviner.com +* +* Copyright (c) 2013 - John Culviner +* +* Licensed under the MIT license: +* http://www.opensource.org/licenses/mit-license.php +* +* !!!!NOTE!!!! +* You must also write a cookie in conjunction with using this plugin in the server's response headers containing the file download: +* Set-Cookie: fileDownload=true; path=/" +* !!!!NOTE!!!! +*/ + +(function($, window){ + // i'll just put them here to get evaluated on script load + var htmlSpecialCharsRegEx = /[<>&\r\n"']/gm; + var htmlSpecialCharsPlaceHolders = { + '<': 'lt;', + '>': 'gt;', + '&': 'amp;', + '\r': "#13;", + '\n': "#10;", + '"': 'quot;', + "'": '#39;' /*single quotes just to be safe, IE8 doesn't support ', so use ' instead */ + }; + +$.extend({ + // + //$.fileDownload('/path/to/url/', options) + // see directly below for possible 'options' + fileDownload: function (fileUrl, options) { + + //provide some reasonable defaults to any unspecified options below + var settings = $.extend({ + + // + //Requires jQuery UI: provide a message to display to the user when the file download is being prepared before the browser's dialog appears + // + preparingMessageHtml: null, + + // + //Requires jQuery UI: provide a message to display to the user when a file download fails + // + failMessageHtml: null, + + // + //the stock android browser straight up doesn't support file downloads initiated by a non GET: http://code.google.com/p/android/issues/detail?id=1780 + //specify a message here to display if a user tries with an android browser + //if jQuery UI is installed this will be a dialog, otherwise it will be an alert + //Set to null to disable the message and attempt to download anyway + // + androidPostUnsupportedMessageHtml: "Unfortunately your Android browser doesn't support this type of file download. Please try again with a different browser.", + + // + //Requires jQuery UI: options to pass into jQuery UI Dialog + // + dialogOptions: { modal: true }, + + // + //a function to call while the dowload is being prepared before the browser's dialog appears + //Args: + // url - the original url attempted + // + prepareCallback: function (url) { }, + + // + //a function to call after a file download successfully completed + //Args: + // url - the original url attempted + // + successCallback: function (url) { }, + + // + //a function to call after a file download request was canceled + //Args: + // url - the original url attempted + // + abortCallback: function (url) { }, + + // + //a function to call after a file download failed + //Args: + // responseHtml - the html that came back in response to the file download. this won't necessarily come back depending on the browser. + // in less than IE9 a cross domain error occurs because 500+ errors cause a cross domain issue due to IE subbing out the + // server's error message with a "helpful" IE built in message + // url - the original url attempted + // error - original error cautch from exception + // + failCallback: function (responseHtml, url, error) { }, + + // + // the HTTP method to use. Defaults to "GET". + // + httpMethod: "GET", + + // + // if specified will perform a "httpMethod" request to the specified 'fileUrl' using the specified data. + // data must be an object (which will be $.param serialized) or already a key=value param string + // + data: null, + + // + //a period in milliseconds to poll to determine if a successful file download has occured or not + // + checkInterval: 100, + + // + //the cookie name to indicate if a file download has occured + // + cookieName: "fileDownload", + + // + //the cookie value for the above name to indicate that a file download has occured + // + cookieValue: "true", + + // + //the cookie path for above name value pair + // + cookiePath: "/", + + // + //if specified it will be used when attempting to clear the above name value pair + //useful for when downloads are being served on a subdomain (e.g. downloads.example.com) + // + cookieDomain: null, + + // + //the title for the popup second window as a download is processing in the case of a mobile browser + // + popupWindowTitle: "Initiating file download...", + + // + //Functionality to encode HTML entities for a POST, need this if data is an object with properties whose values contains strings with quotation marks. + //HTML entity encoding is done by replacing all &,<,>,',",\r,\n characters. + //Note that some browsers will POST the string htmlentity-encoded whilst others will decode it before POSTing. + //It is recommended that on the server, htmlentity decoding is done irrespective. + // + encodeHTMLEntities: true + + }, options); + + var deferred = new $.Deferred(); + + //Setup mobile browser detection: Partial credit: http://detectmobilebrowser.com/ + var userAgent = (navigator.userAgent || navigator.vendor || window.opera).toLowerCase(); + + var isIos; //has full support of features in iOS 4.0+, uses a new window to accomplish this. + var isAndroid; //has full support of GET features in 4.0+ by using a new window. Non-GET is completely unsupported by the browser. See above for specifying a message. + var isOtherMobileBrowser; //there is no way to reliably guess here so all other mobile devices will GET and POST to the current window. + + if (/ip(ad|hone|od)/.test(userAgent)) { + + isIos = true; + + } else if (userAgent.indexOf('android') !== -1) { + + isAndroid = true; + + } else { + + isOtherMobileBrowser = /avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|playbook|silk|iemobile|iris|kindle|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(userAgent) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|e\-|e\/|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(di|rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|xda(\-|2|g)|yas\-|your|zeto|zte\-/i.test(userAgent.substr(0, 4)); + + } + + var httpMethodUpper = settings.httpMethod.toUpperCase(); + + if (isAndroid && httpMethodUpper !== "GET" && settings.androidPostUnsupportedMessageHtml) { + //the stock android browser straight up doesn't support file downloads initiated by non GET requests: http://code.google.com/p/android/issues/detail?id=1780 + + if ($().dialog) { + $("
").html(settings.androidPostUnsupportedMessageHtml).dialog(settings.dialogOptions); + } else { + alert(settings.androidPostUnsupportedMessageHtml); + } + + return deferred.reject(); + } + + var $preparingDialog = null; + + var internalCallbacks = { + + onPrepare: function (url) { + + //wire up a jquery dialog to display the preparing message if specified + if (settings.preparingMessageHtml) { + + $preparingDialog = $("
").html(settings.preparingMessageHtml).dialog(settings.dialogOptions); + + } else if (settings.prepareCallback) { + + settings.prepareCallback(url); + + } + + }, + + onSuccess: function (url) { + + //remove the perparing message if it was specified + if ($preparingDialog) { + $preparingDialog.dialog('close'); + } + + settings.successCallback(url); + + deferred.resolve(url); + }, + + onAbort: function (url) { + + //remove the perparing message if it was specified + if ($preparingDialog) { + $preparingDialog.dialog('close'); + }; + + settings.abortCallback(url); + + deferred.reject(url); + }, + + onFail: function (responseHtml, url, error) { + + //remove the perparing message if it was specified + if ($preparingDialog) { + $preparingDialog.dialog('close'); + } + + //wire up a jquery dialog to display the fail message if specified + if (settings.failMessageHtml) { + $("
").html(settings.failMessageHtml).dialog(settings.dialogOptions); + } + + settings.failCallback(responseHtml, url, error); + + deferred.reject(responseHtml, url); + } + }; + + internalCallbacks.onPrepare(fileUrl); + + //make settings.data a param string if it exists and isn't already + if (settings.data !== null && typeof settings.data !== "string") { + settings.data = $.param(settings.data); + } + + + var $iframe, + downloadWindow, + formDoc, + $form; + + if (httpMethodUpper === "GET") { + + if (settings.data !== null) { + //need to merge any fileUrl params with the data object + + var qsStart = fileUrl.indexOf('?'); + + if (qsStart !== -1) { + //we have a querystring in the url + + if (fileUrl.substring(fileUrl.length - 1) !== "&") { + fileUrl = fileUrl + "&"; + } + } else { + + fileUrl = fileUrl + "?"; + } + + fileUrl = fileUrl + settings.data; + } + + if (isIos || isAndroid) { + + downloadWindow = window.open(fileUrl); + downloadWindow.document.title = settings.popupWindowTitle; + window.focus(); + + } else if (isOtherMobileBrowser) { + + window.location(fileUrl); + + } else { + + //create a temporary iframe that is used to request the fileUrl as a GET request + $iframe = $("").appendTo("body"); + } + + } else { + + var formInnerHtml = ""; + + if (settings.data !== null) { + + $.each(settings.data.replace(/\+/g, ' ').split("&"), function () { + + var kvp = this.split("="); + + //Issue: When value contains sign '=' then the kvp array does have more than 2 items. We have to join value back + var k = kvp[0]; + kvp.shift(); + var v = kvp.join("="); + kvp = [k, v]; + + var key = settings.encodeHTMLEntities ? htmlSpecialCharsEntityEncode(decodeURIComponent(kvp[0])) : decodeURIComponent(kvp[0]); + if (key) { + var value = settings.encodeHTMLEntities ? htmlSpecialCharsEntityEncode(decodeURIComponent(kvp[1])) : decodeURIComponent(kvp[1]); + formInnerHtml += ''; + } + }); + } + + if (isOtherMobileBrowser) { + + $form = $("
").appendTo("body"); + $form.hide() + .prop('method', settings.httpMethod) + .prop('action', fileUrl) + .html(formInnerHtml); + + } else { + + if (isIos) { + + downloadWindow = window.open("about:blank"); + downloadWindow.document.title = settings.popupWindowTitle; + formDoc = downloadWindow.document; + window.focus(); + + } else { + + $iframe = $("").appendTo("body"); + formDoc = getiframeDocument($iframe); + } + + formDoc.write("" + formInnerHtml + "
" + settings.popupWindowTitle + ""); + $form = $(formDoc).find('form'); + } + + $form.submit(); + } + + + //check if the file download has completed every checkInterval ms + setTimeout(checkFileDownloadComplete, settings.checkInterval); + + + function checkFileDownloadComplete() { + //has the cookie been written due to a file download occuring? + + var cookieValue = settings.cookieValue; + if(typeof cookieValue == 'string') { + cookieValue = cookieValue.toLowerCase(); + } + + var lowerCaseCookie = settings.cookieName.toLowerCase() + "=" + cookieValue; + + if (document.cookie.toLowerCase().indexOf(lowerCaseCookie) > -1) { + + //execute specified callback + internalCallbacks.onSuccess(fileUrl); + + //remove cookie + var cookieData = settings.cookieName + "=; path=" + settings.cookiePath + "; expires=" + new Date(0).toUTCString() + ";"; + if (settings.cookieDomain) cookieData += " domain=" + settings.cookieDomain + ";"; + document.cookie = cookieData; + + //remove iframe + cleanUp(false); + + return; + } + + //has an error occured? + //if neither containers exist below then the file download is occuring on the current window + if (downloadWindow || $iframe) { + + //has an error occured? + try { + + var formDoc = downloadWindow ? downloadWindow.document : getiframeDocument($iframe); + + if (formDoc && formDoc.body !== null && formDoc.body.innerHTML.length) { + + var isFailure = true; + + if ($form && $form.length) { + var $contents = $(formDoc.body).contents().first(); + + try { + if ($contents.length && $contents[0] === $form[0]) { + isFailure = false; + } + } catch (e) { + if (e && e.number == -2146828218) { + // IE 8-10 throw a permission denied after the form reloads on the "$contents[0] === $form[0]" comparison + isFailure = true; + } else { + throw e; + } + } + } + + if (isFailure) { + // IE 8-10 don't always have the full content available right away, they need a litle bit to finish + setTimeout(function () { + internalCallbacks.onFail(formDoc.body.innerHTML, fileUrl); + cleanUp(true); + }, 100); + + return; + } + } + } + catch (err) { + + //500 error less than IE9 + internalCallbacks.onFail('', fileUrl, err); + + cleanUp(true); + + return; + } + } + + + //keep checking... + setTimeout(checkFileDownloadComplete, settings.checkInterval); + } + + //gets an iframes document in a cross browser compatible manner + function getiframeDocument($iframe) { + var iframeDoc = $iframe[0].contentWindow || $iframe[0].contentDocument; + if (iframeDoc.document) { + iframeDoc = iframeDoc.document; + } + return iframeDoc; + } + + function cleanUp(isFailure) { + + setTimeout(function() { + + if (downloadWindow) { + + if (isAndroid) { + downloadWindow.close(); + } + + if (isIos) { + if (downloadWindow.focus) { + downloadWindow.focus(); //ios safari bug doesn't allow a window to be closed unless it is focused + if (isFailure) { + downloadWindow.close(); + } + } + } + } + + //iframe cleanup appears to randomly cause the download to fail + //not doing it seems better than failure... + //if ($iframe) { + // $iframe.remove(); + //} + + }, 0); + } + + + function htmlSpecialCharsEntityEncode(str) { + return str.replace(htmlSpecialCharsRegEx, function(match) { + return '&' + htmlSpecialCharsPlaceHolders[match]; + }); + } + var promise = deferred.promise(); + promise.abort = function() { + cleanUp(); + $iframe.attr('src', '').html(''); + internalCallbacks.onAbort(fileUrl); + }; + return promise; + } +}); + +})(jQuery, this || window); \ No newline at end of file diff --git a/public/javascripts/download/jquery.fileDownload.min.js b/public/javascripts/download/jquery.fileDownload.min.js new file mode 100644 index 000000000..c4ae8b923 --- /dev/null +++ b/public/javascripts/download/jquery.fileDownload.min.js @@ -0,0 +1 @@ +!function(e,o){var t=/[<>&\r\n"']/gm,a={"<":"lt;",">":"gt;","&":"amp;","\r":"#13;","\n":"#10;",'"':"quot;","'":"#39;"};e.extend({fileDownload:function(i,n){var r,l,c,d=e.extend({preparingMessageHtml:null,failMessageHtml:null,androidPostUnsupportedMessageHtml:"Unfortunately your Android browser doesn't support this type of file download. Please try again with a different browser.",dialogOptions:{modal:!0},prepareCallback:function(e){},successCallback:function(e){},abortCallback:function(e){},failCallback:function(e,o,t){},httpMethod:"GET",data:null,checkInterval:100,cookieName:"fileDownload",cookieValue:"true",cookiePath:"/",cookieDomain:null,popupWindowTitle:"Initiating file download...",encodeHTMLEntities:!0},n),s=new e.Deferred,p=(navigator.userAgent||navigator.vendor||o.opera).toLowerCase();/ip(ad|hone|od)/.test(p)?r=!0:-1!==p.indexOf("android")?l=!0:c=/avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|playbook|silk|iemobile|iris|kindle|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(p)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|e\-|e\/|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(di|rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|xda(\-|2|g)|yas\-|your|zeto|zte\-/i.test(p.substr(0,4));var m=d.httpMethod.toUpperCase();if(l&&"GET"!==m&&d.androidPostUnsupportedMessageHtml)return e().dialog?e("
").html(d.androidPostUnsupportedMessageHtml).dialog(d.dialogOptions):alert(d.androidPostUnsupportedMessageHtml),s.reject();var u,f,g,h,b=null,k={onPrepare:function(o){d.preparingMessageHtml?b=e("
").html(d.preparingMessageHtml).dialog(d.dialogOptions):d.prepareCallback&&d.prepareCallback(o)},onSuccess:function(e){b&&b.dialog("close"),d.successCallback(e),s.resolve(e)},onAbort:function(e){b&&b.dialog("close"),d.abortCallback(e),s.reject(e)},onFail:function(o,t,a){b&&b.dialog("close"),d.failMessageHtml&&e("
").html(d.failMessageHtml).dialog(d.dialogOptions),d.failCallback(o,t,a),s.reject(o,t)}};if(k.onPrepare(i),null!==d.data&&"string"!=typeof d.data&&(d.data=e.param(d.data)),"GET"===m){if(null!==d.data)-1!==i.indexOf("?")?"&"!==i.substring(i.length-1)&&(i+="&"):i+="?",i+=d.data;r||l?((f=o.open(i)).document.title=d.popupWindowTitle,o.focus()):c?o.location(i):u=e("").appendTo("body")}else{var v="";null!==d.data&&e.each(d.data.replace(/\+/g," ").split("&"),function(){var e=this.split("="),o=e[0];e.shift(),e=[o,e.join("=")];var t=d.encodeHTMLEntities?T(decodeURIComponent(e[0])):decodeURIComponent(e[0]);if(t){var a=d.encodeHTMLEntities?T(decodeURIComponent(e[1])):decodeURIComponent(e[1]);v+=''}}),c?(h=e("
").appendTo("body")).hide().prop("method",d.httpMethod).prop("action",i).html(v):(r?((f=o.open("about:blank")).document.title=d.popupWindowTitle,g=f.document,o.focus()):g=w(u=e("").appendTo("body")),g.write(""+v+"
"+d.popupWindowTitle+""),h=e(g).find("form")),h.submit()}function w(e){var o=e[0].contentWindow||e[0].contentDocument;return o.document&&(o=o.document),o}function y(e){setTimeout(function(){f&&(l&&f.close(),r&&f.focus&&(f.focus(),e&&f.close()))},0)}function T(e){return e.replace(t,function(e){return"&"+a[e]})}setTimeout(function o(){var t=d.cookieValue;"string"==typeof t&&(t=t.toLowerCase());var a=d.cookieName.toLowerCase()+"="+t;if(document.cookie.toLowerCase().indexOf(a)>-1){k.onSuccess(i);var n=d.cookieName+"=; path="+d.cookiePath+"; expires="+new Date(0).toUTCString()+";";return d.cookieDomain&&(n+=" domain="+d.cookieDomain+";"),document.cookie=n,void y(!1)}if(f||u)try{var r=f?f.document:w(u);if(r&&null!==r.body&&r.body.innerHTML.length){var l=!0;if(h&&h.length){var c=e(r.body).contents().first();try{c.length&&c[0]===h[0]&&(l=!1)}catch(e){if(!e||-2146828218!=e.number)throw e;l=!0}}if(l)return void setTimeout(function(){k.onFail(r.body.innerHTML,i),y(!0)},100)}}catch(e){return k.onFail("",i,e),void y(!0)}setTimeout(o,d.checkInterval)},d.checkInterval);var x=s.promise();return x.abort=function(){y(),u.attr("src","").html(""),k.onAbort(i)},x}})}(jQuery,this||window); \ No newline at end of file diff --git a/public/javascripts/media/clappr-playback-rate-plugin.min.js b/public/javascripts/media/clappr-playback-rate-plugin.min.js new file mode 100644 index 000000000..8bbcbfaca --- /dev/null +++ b/public/javascripts/media/clappr-playback-rate-plugin.min.js @@ -0,0 +1 @@ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e(require("clappr")):"function"==typeof define&&define.amd?define("clappr-playback-rate-plugin",["clappr"],e):"object"==typeof exports?exports["clappr-playback-rate-plugin"]=e(require("clappr")):t["clappr-playback-rate-plugin"]=e(t.clappr)}("undefined"!=typeof self?self:this,function(t){return function(t){function e(n){if(a[n])return a[n].exports;var o=a[n]={i:n,l:!1,exports:{}};return t[n].call(o.exports,o,o.exports,e),o.l=!0,o.exports}var a={};return e.m=t,e.c=a,e.d=function(t,a,n){e.o(t,a)||Object.defineProperty(t,a,{configurable:!1,enumerable:!0,get:n})},e.n=function(t){var a=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(a,"a",a),a},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=0)}([function(t,e,a){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function o(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}function r(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}Object.defineProperty(e,"__esModule",{value:!0});var i=a(1),l=(a.n(i),a(2)),c=a.n(l),s=a(3),u=a.n(s),p=function(){function t(t,e){for(var a=0;a\n <%= title %>\n\n\n'},function(t,e,a){e=t.exports=a(4)(void 0),e.push([t.i,".media-control[data-media-control] .media-control-layer[data-controls] .playback_rate[data-playback-rate-select] {\n float: right;\n margin-top: 5px;\n position: relative; }\n .media-control[data-media-control] .media-control-layer[data-controls] .playback_rate[data-playback-rate-select] button.media-control-button.media-control-icon {\n font-family: Roboto,\"Open Sans\",Arial,sans-serif;\n -webkit-font-smoothing: antialiased;\n font-size: 12px;\n cursor: pointer;\n padding: 10px; }\n .media-control[data-media-control] .media-control-layer[data-controls] .playback_rate[data-playback-rate-select] button.media-control-button.media-control-icon:hover {\n color: #c9c9c9; }\n .media-control[data-media-control] .media-control-layer[data-controls] .playback_rate[data-playback-rate-select] button.media-control-button.media-control-icon.changing {\n -webkit-animation: pulse 0.5s infinite alternate; }\n .media-control[data-media-control] .media-control-layer[data-controls] .playback_rate[data-playback-rate-select] > ul {\n display: none;\n list-style-type: none;\n position: absolute;\n bottom: 25px;\n border: 1px solid black;\n border-radius: 4px;\n background-color: rgba(0, 0, 0, 0.7); }\n .media-control[data-media-control] .media-control-layer[data-controls] .playback_rate[data-playback-rate-select] li {\n position: relative;\n font-size: 12px; }\n .media-control[data-media-control] .media-control-layer[data-controls] .playback_rate[data-playback-rate-select] li[data-title] {\n padding: 5px; }\n .media-control[data-media-control] .media-control-layer[data-controls] .playback_rate[data-playback-rate-select] li a {\n color: #aaa;\n padding: 2px 10px 2px 15px;\n display: block;\n text-decoration: none; }\n .media-control[data-media-control] .media-control-layer[data-controls] .playback_rate[data-playback-rate-select] li a.active {\n background-color: black;\n font-weight: bold;\n color: #fff; }\n .media-control[data-media-control] .media-control-layer[data-controls] .playback_rate[data-playback-rate-select] li a.active:before {\n content: '\\2713';\n position: absolute;\n top: 2px;\n left: 4px; }\n .media-control[data-media-control] .media-control-layer[data-controls] .playback_rate[data-playback-rate-select] li a:hover {\n color: #fff;\n text-decoration: none; }\n\n@-webkit-keyframes pulse {\n 0% {\n color: #fff; }\n 50% {\n color: #ff0101; }\n 100% {\n color: #B80000; } }\n",""])},function(t,e){function a(t,e){var a=t[1]||"",o=t[3];if(!o)return a;if(e&&"function"==typeof btoa){var r=n(o);return[a].concat(o.sources.map(function(t){return"/*# sourceURL="+o.sourceRoot+t+" */"})).concat([r]).join("\n")}return[a].join("\n")}function n(t){return"/*# sourceMappingURL=data:application/json;charset=utf-8;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(t))))+" */"}t.exports=function(t){var e=[];return e.toString=function(){return this.map(function(e){var n=a(e,t);return e[2]?"@media "+e[2]+"{"+n+"}":n}).join("")},e.i=function(t,a){"string"==typeof t&&(t=[[null,t,""]]);for(var n={},o=0;o0&&e._renderPlugin()})})}},{key:"removeThumbnail",value:function(t){var e=this,n=t.constructor===Array?t:[t];return this._onThumbsLoaded.then(function(){var t=!0,o=!1;return n.forEach(function(n){var i=e._thumbs.some(function(t,o){return t.src===n&&(e._thumbs.splice(o,1),e._getOptions().backdropHeight&&(e._$backdropCarouselImgs[o].remove(),e._$backdropCarouselImgs.splice(o,1)),!0)});i?o=!0:t=!1}),o&&e._renderPlugin(),c.Promise.resolve(t)})}},{key:"_init",value:function(){this._thumbsLoaded&&(this._$backdropCarouselImgs=[],this._createElements(),this._loadBackdrop(),this._renderPlugin())}},{key:"_getOptions",value:function(){if(!("scrubThumbnails"in this.core.options))throw"'scrubThumbnails property missing from options object.";return this.core.options.scrubThumbnails}},{key:"_appendElToMediaControl",value:function(){this.core.mediaControl.$el.find(".media-control-background").first().after(this.el)}},{key:"_onMouseMove",value:function(t){this._calculateHoverPosition(t),this._show=!0,this._renderPlugin()}},{key:"_onMouseLeave",value:function(){this._show=!1,this._renderPlugin()}},{key:"_calculateHoverPosition",value:function(t){var e=t.pageX-this.core.mediaControl.$seekBarContainer.offset().left;this._hoverPosition=Math.min(1,Math.max(e/this.core.mediaControl.$seekBarContainer.width(),0))}},{key:"_buildThumbsFromOptions",value:function(){var t=this,e=this._getOptions().thumbs,n=e.map(function(e){return t._addThumbFromSrc(e)});return c.Promise.all(n)}},{key:"_addThumbFromSrc",value:function(t){var e=this;return new c.Promise(function(e,n){var o=new Image;o.onload=function(){e(o)},o.onerror=n,o.src=t.url}).then(function(n){var o=t.time,i=null;e._thumbs.some(function(t,e){return o0?e._thumbs[i-1]:null;u&&(u.duration=o-u.time);var s=r?r.time-t.time:null,a=n.width,c=n.height,l={imageW:a,imageH:c,x:t.x||0,y:t.y||0,w:t.w||a,h:t.h||c,url:t.url,time:o,duration:s,src:t};return e._thumbs.splice(i,0,l),l})}},{key:"_buildImg",value:function(t,e){var n=e/t.h,o=(0,a.$)("").addClass("thumbnail-img").attr("src",t.url),i=(0,a.$)("
").addClass("thumbnail-container");return i.css("width",t.w*n),i.css("height",e),o.css({height:t.imageH*n,left:-1*t.x*n,top:-1*t.y*n}),i.append(o),i}},{key:"_loadBackdrop",value:function(){if(this._getOptions().backdropHeight)for(var t=this._$carousel,e=0;e=0;n--){var o=e[n];if(o.time<=t)return n}return 0}},{key:"_renderPlugin",value:function(){this._thumbsLoaded&&(this._show&&this._thumbs.length>0?(this.$el.removeClass("hidden"),this._updateCarousel(),this._updateSpotlightThumb()):this.$el.addClass("hidden"))}},{key:"_createElements",value:function(){this.$el.html(this.template({backdropHeight:this._getOptions().backdropHeight,spotlightHeight:this._getOptions().spotlightHeight})),this.$el.append(a.Styler.getStyleFor(d["default"])),this._$spotlight=this.$el.find(".spotlight"),this._$backdrop=this.$el.find(".backdrop"),this._$carousel=this._$backdrop.find(".carousel"),this.$el.addClass("hidden"),this._appendElToMediaControl()}}]),e}(a.UICorePlugin);e["default"]=p,t.exports=e["default"]},function(t,e,n){(function(t,o){"use strict";function i(t,e){this._id=t,this._clearFn=e}var r=n(4).nextTick,u=Function.prototype.apply,s=Array.prototype.slice,a={},c=0;e.setTimeout=function(){return new i(u.call(setTimeout,window,arguments),clearTimeout)},e.setInterval=function(){return new i(u.call(setInterval,window,arguments),clearInterval)},e.clearTimeout=e.clearInterval=function(t){t.close()},i.prototype.unref=i.prototype.ref=function(){},i.prototype.close=function(){this._clearFn.call(window,this._id)},e.enroll=function(t,e){clearTimeout(t._idleTimeoutId),t._idleTimeout=e},e.unenroll=function(t){clearTimeout(t._idleTimeoutId),t._idleTimeout=-1},e._unrefActive=e.active=function(t){clearTimeout(t._idleTimeoutId);var e=t._idleTimeout;e>=0&&(t._idleTimeoutId=setTimeout(function(){t._onTimeout&&t._onTimeout()},e))},e.setImmediate="function"==typeof t?t:function(t){var n=c++,o=!(arguments.length<2)&&s.call(arguments,1);return a[n]=!0,r(function(){a[n]&&(o?t.apply(null,o):t.call(null),e.clearImmediate(n))}),n},e.clearImmediate="function"==typeof o?o:function(t){delete a[t]}}).call(e,n(1).setImmediate,n(1).clearImmediate)},function(t,e){"use strict";t.exports=function(){var t=[];return t.toString=function(){for(var t=[],e=0;e1)for(var n=1;n")},M.garbage=function(t){this.options.recycleVideo&&"VIDEO"===t[0].tagName.toUpperCase()&&(t.children().remove(),D.push(t))},M);function M(){(0,s.default)(this,M)}x.options={recycleVideo:!1};var N=e.DoubleEventHandler=(F.prototype.handle=function(t,e,r){var i=!(2]*>/,Xe=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,Ye=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,Ze=/^(?:body|html)$/i,$e=/([A-Z])/g,_e=["val","css","html","text","data","width","height","offset"],bf=Se.createElement("table"),cf=Se.createElement("tr"),df={tr:Se.createElement("tbody"),tbody:bf,thead:bf,tfoot:bf,td:cf,th:cf,"*":Se.createElement("div")},ef=/complete|loaded|interactive/,ff=/^[\w-]*$/,hf=(gf={}).toString,jf={},mf=Se.createElement("div"),nf={tabindex:"tabIndex",readonly:"readOnly",for:"htmlFor",class:"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},of=Array.isArray||function(t){return t instanceof Array},jf.matches=function(t,e){if(!e||!t||1!==t.nodeType)return!1;var r=t.matches||t.webkitMatchesSelector||t.mozMatchesSelector||t.oMatchesSelector||t.matchesSelector;if(r)return r.call(t,e);var i,n=t.parentNode,a=!n;return a&&(n=mf).appendChild(t),i=~jf.qsa(n,e).indexOf(t),a&&mf.removeChild(t),i},kf=function(t){return t.replace(/-+(.)?/g,function(t,e){return e?e.toUpperCase():""})},lf=function(r){return Qe.call(r,function(t,e){return r.indexOf(t)==e})},jf.fragment=function(t,e,r){var i,n,a;return Xe.test(t)&&(i=Me(Se.createElement(RegExp.$1))),i||(t.replace&&(t=t.replace(Ye,"<$1>")),e===Ke&&(e=We.test(t)&&RegExp.$1),e in df||(e="*"),(a=df[e]).innerHTML=""+t,i=Me.each(Re.call(a.childNodes),function(){a.removeChild(this)})),uf(r)&&(n=Me(i),Me.each(r,function(t,e){-1<_e.indexOf(t)?n[t](e):n.attr(t,e)})),i},jf.Z=function(t,e){return new Df(t,e)},jf.isZ=function(t){return t instanceof jf.Z},jf.init=function(t,e){var r;if(!t)return jf.Z();if("string"==typeof t)if("<"==(t=t.trim())[0]&&We.test(t))r=jf.fragment(t,RegExp.$1,e),t=null;else{if(e!==Ke)return Me(e).find(t);r=jf.qsa(Se,t)}else{if(qf(t))return Me(Se).ready(t);if(jf.isZ(t))return t;if(of(t))r=function(t){return Qe.call(t,function(t){return null!=t})}(t);else if(tf(t))r=[t],t=null;else if(We.test(t))r=jf.fragment(t.trim(),RegExp.$1,e),t=null;else{if(e!==Ke)return Me(e).find(t);r=jf.qsa(Se,t)}}return jf.Z(r,t)},(Me=function(t,e){return jf.init(t,e)}).extend=function(e){var r,t=Re.call(arguments,1);return"boolean"==typeof e&&(r=e,e=t.shift()),t.forEach(function(t){!function t(e,r,i){for(Le in r)i&&(uf(r[Le])||of(r[Le]))?(uf(r[Le])&&!uf(e[Le])&&(e[Le]={}),of(r[Le])&&!of(e[Le])&&(e[Le]=[]),t(e[Le],r[Le],i)):r[Le]!==Ke&&(e[Le]=r[Le])}(e,t,r)}),e},jf.qsa=function(t,e){var r,i="#"==e[0],n=!i&&"."==e[0],a=i||n?e.slice(1):e,o=ff.test(a);return t.getElementById&&o&&i?(r=t.getElementById(a))?[r]:[]:1!==t.nodeType&&9!==t.nodeType&&11!==t.nodeType?[]:Re.call(o&&!i&&t.getElementsByClassName?n?t.getElementsByClassName(a):t.getElementsByTagName(e):t.querySelectorAll(e))},Me.contains=Se.documentElement.contains?function(t,e){return t!==e&&t.contains(e)}:function(t,e){for(;e&&(e=e.parentNode);)if(e===t)return!0;return!1},Me.type=pf,Me.isFunction=qf,Me.isWindow=rf,Me.isArray=of,Me.isPlainObject=uf,Me.isEmptyObject=function(t){var e;for(e in t)return!1;return!0},Me.isNumeric=function(t){var e=Number(t),r=typeof t;return null!=t&&"boolean"!=r&&("string"!=r||t.length)&&!isNaN(e)&&isFinite(e)||!1},Me.inArray=function(t,e,r){return Oe.indexOf.call(e,t,r)},Me.camelCase=kf,Me.trim=function(t){return null==t?"":String.prototype.trim.call(t)},Me.uuid=0,Me.support={},Me.expr={},Me.noop=function(){},Me.map=function(t,e){var r,i,n,a=[];if(vf(t))for(i=0;i)<[^<]*)*<\/script>/gi,Ok=/^(?:text|application)\/javascript/i,Pk=/^(?:text|application)\/xml/i,Qk="application/json",Rk="text/html",Sk=/^\s*$/,Tk=Kk.createElement("a");function Vk(t,e,r,i){if(t.global)return function(t,e,r){var i=Ik.Event(e);return Ik(t).trigger(i,r),!i.isDefaultPrevented()}(e||Kk,r,i)}function Yk(t,e){var r=e.context;if(!1===e.beforeSend.call(r,t,e)||!1===Vk(e,r,"ajaxBeforeSend",[t,e]))return!1;Vk(e,r,"ajaxSend",[t,e])}function Zk(t,e,r,i){var n=r.context,a="success";r.success.call(n,t,a,e),i&&i.resolveWith(n,[t,a,e]),Vk(r,n,"ajaxSuccess",[e,r,t]),_k(a,e,r)}function $k(t,e,r,i,n){var a=i.context;i.error.call(a,r,e,t),n&&n.rejectWith(a,[r,e,t]),Vk(i,a,"ajaxError",[r,i,t||e]),_k(e,r,i)}function _k(t,e,r){var i=r.context;r.complete.call(i,e,t),Vk(r,i,"ajaxComplete",[e,r]),function(t){t.global&&!--Ik.active&&Vk(t,null,"ajaxStop")}(r)}function bl(){}function dl(t,e){return""==e?t:(t+"&"+e).replace(/[&?]{1,2}/,"?")}function fl(t,e,r,i){return Ik.isFunction(e)&&(i=r,r=e,e=void 0),Ik.isFunction(r)||(i=r,r=void 0),{url:t,data:e,success:r,dataType:i}}Tk.href=window.location.href,Ik.active=0,Ik.ajaxJSONP=function(r,i){if(!("type"in r))return Ik.ajax(r);function t(t){Ik(s).triggerHandler("error",t||"abort")}var n,a,e=r.jsonpCallback,o=(Ik.isFunction(e)?e():e)||"Zepto"+Jk++,s=Kk.createElement("script"),l=window[o],u={abort:t};return i&&i.promise(u),Ik(s).on("load error",function(t,e){clearTimeout(a),Ik(s).off().remove(),"error"!=t.type&&n?Zk(n[0],u,r,i):$k(null,e||"error",u,r,i),window[o]=l,n&&Ik.isFunction(l)&&l(n[0]),l=n=void 0}),!1===Yk(u,r)?t("abort"):(window[o]=function(){n=arguments},s.src=r.url.replace(/\?(.+)=\?/,"?$1="+o),Kk.head.appendChild(s),0").html(t.replace(Nk,"")).find(i):t),s&&s.apply(n,arguments)},Ik.ajax(o),this};var gl=encodeURIComponent;Ik.param=function(t,e){var r=[];return r.add=function(t,e){Ik.isFunction(e)&&(e=e()),null==e&&(e=""),this.push(gl(t)+"="+gl(e))},function r(i,t,n,a){var o,s=Ik.isArray(t),l=Ik.isPlainObject(t);Ik.each(t,function(t,e){o=Ik.type(e),a&&(t=n?a:a+"["+(l||"object"==o||"array"==o?t:"")+"]"),!a&&s?i.add(e.name,e.value):"array"==o||!n&&"object"==o?r(i,e,n,t):i.add(t,e)})}(r,t,e),r.join("&").replace(/%20/g,"+")}}(Je),(_m=Je).Callbacks=function(i){i=_m.extend({},i);var e,r,n,a,o,s,l=[],u=!i.once&&[],d=function(t){for(e=i.memory&&t,r=!0,s=a||0,a=0,o=l.length,n=!0;l&&s/,Sq="Zepto"+ +new Date,Lq.qsa=function(a,o){return Tq(o,function(t,r,i){try{var e;!t&&r?t="*":Rq.test(t)&&(e=Kq(a).addClass(Sq),t="."+Sq+" "+t);var n=Mq(a,t)}catch(t){throw console.error("error performing selector: %o",o),t}finally{e&&e.removeClass(Sq)}return r?Lq.uniq(Kq.map(n,function(t,e){return r.call(t,e,n,i)})):n})},Lq.matches=function(i,t){return Tq(t,function(t,e,r){return(!t||Nq(i,t))&&(!e||e.call(i,null,r)===i)})},He.exports=Je},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0});function n(t){return null===t?"":(""+t).replace(o,function(t){return a[t]})}function i(a,t){var e,r=new RegExp([(l.escape||u).source,(l.interpolate||u).source,(l.evaluate||u).source].join("|")+"|$","g"),o=0,s="__p+='";a.replace(r,function(t,e,r,i,n){return s+=a.slice(o,n).replace(c,function(t){return"\\"+d[t]}),e&&(s+="'+\n((__t=("+e+"))==null?'':escapeExpr(__t))+\n'"),r&&(s+="'+\n((__t=("+r+"))==null?'':__t)+\n'"),i&&(s+="';\n"+i+"\n__p+='"),o=n+t.length,t}),s+="';\n",l.variable||(s="with(obj||{}){\n"+s+"}\n"),s="var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');};\n"+s+"return __p;\n//# sourceURL=/microtemplates/source["+f+++"]";try{e=new Function(l.variable||"obj","escapeExpr",s)}catch(t){throw t.source=s,t}if(t)return e(t,n);function i(t){return e.call(this,t,n)}return i.source="function("+(l.variable||"obj")+"){\n"+s+"}",i}var l={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g},u=/(.)^/,d={"'":"'","\\":"\\","\r":"r","\n":"n","\t":"t","\u2028":"u2028","\u2029":"u2029"},c=/\\|'|\r|\n|\t|\u2028|\u2029/g,a={"&":"&","<":"<",">":">",'"':""","'":"'"},o=new RegExp("[&<>\"']","g"),f=0;i.settings=l,e.default=i,t.exports=e.default},function(t,e){t.exports=function(r){var o=[];return o.toString=function(){return this.map(function(t){var e=function(t,e){var r=t[1]||"",i=t[3];if(!i)return r;if(e&&"function"==typeof btoa){var n=function(t){return"/*# sourceMappingURL=data:application/json;charset=utf-8;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(t))))+" */"}(i),a=i.sources.map(function(t){return"/*# sourceURL="+i.sourceRoot+t+" */"});return[r].concat(a).concat([n]).join("\n")}return[r].join("\n")}(t,r);return t[2]?"@media "+t[2]+"{"+e+"}":e}).join("")},o.i=function(t,e){"string"==typeof t&&(t=[[null,t,""]]);for(var r={},i=0;i=e.length?{value:void 0,done:!0}:(t=i(e,r),this._i+=t.length,{value:t,done:!1})})},function(t,e){t.exports=!0},function(t,e,i){function n(){}var a=i(21),o=i(116),s=i(51),l=i(49)("IE_PROTO"),u="prototype",d=function(){var t,e=i(67)("iframe"),r=s.length;for(e.style.display="none",i(117).appendChild(e),e.src="javascript:",(t=e.contentWindow.document).open(),t.write("