diff --git a/Gemfile b/Gemfile index ac2778c59..7a37b7c44 100644 --- a/Gemfile +++ b/Gemfile @@ -98,3 +98,7 @@ gem 'aasm' gem 'enumerize' gem 'diffy' + +# oauth2 +gem 'omniauth', '~> 1.9.0' +gem 'omniauth-oauth2', '~> 1.6.0' diff --git a/Gemfile.lock b/Gemfile.lock index def9dba9b..13cf669d0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -121,7 +121,7 @@ GEM grape-entity (0.7.1) activesupport (>= 4.0) multi_json (>= 1.3.2) - hashie (3.6.0) + hashie (3.5.7) htmlentities (4.3.4) httparty (0.16.2) multi_xml (>= 0.5.2) @@ -179,6 +179,12 @@ GEM multi_json (~> 1.3) multi_xml (~> 0.5) rack (>= 1.2, < 3) + omniauth (1.9.0) + hashie (>= 3.4.6, < 3.7.0) + rack (>= 1.6.2, < 3) + omniauth-oauth2 (1.6.0) + oauth2 (~> 1.1) + omniauth (~> 1.9) pdfkit (0.8.4.1) popper_js (1.14.5) public_suffix (4.0.1) @@ -381,6 +387,8 @@ DEPENDENCIES listen (>= 3.0.5, < 3.2) mysql2 (>= 0.4.4, < 0.6.0) oauth2 + omniauth (~> 1.9.0) + omniauth-oauth2 (~> 1.6.0) pdfkit puma (~> 3.11) rack-cors @@ -415,4 +423,4 @@ RUBY VERSION ruby 2.3.7p456 BUNDLED WITH - 1.17.3 + 2.0.2 diff --git a/LocalReadMe.md b/LocalReadMe.md index ea6285ea3..ff885677e 100644 --- a/LocalReadMe.md +++ b/LocalReadMe.md @@ -43,7 +43,7 @@ *(前端)1、educoder/public/images/educoder/headNavLogo.png 三 替换后端管理页面图标 *(前端)1、educoder/app/assets/images/logo.png - 改动文件后还要执行rake assets:precompile + 改动文件后还要执行 rake assets:precompile 15、标签企业名称修改(****需要重新build) 一、 全局搜索EduCoder 替换掉 替换的公司名称 改动文件后还要执行rake assets:precompile \ No newline at end of file diff --git a/README.md b/README.md index 8dcb6d131..90f519de4 100644 --- a/README.md +++ b/README.md @@ -1,69 +1,70 @@ -# README - -This README would normally document whatever steps are necessary to get the -application up and running. - -Things you may want to cover: - -* Ruby version - -* System dependencies - -* Configuration - -* Database creation - -* Database initialization - -* How to run the test suite - -* Services (job queues, cache servers, search engines, etc.) - -* Deployment instructions - -* ... -#### Jbuilder介绍 -Jbuilder: https://github.com/rails/jbuilder - -#### Rails5 介绍 -rails guide: https://ruby-china.github.io/rails-guides/v5.0/ - -#### API设计文档 -doc for api: https://www.showdoc.cc/web/#/127895880302646?page_id=729221359592009 -user:Hjqreturn PW:12345678 - -#### 测试版访问地址:https://testeduplus2.educoder.net - -#### 实训平台繁忙 - 仓库异常:繁忙等级(81) - -#### 新版域名跳转规则 -新版域名要求总结:testeduplus2.educoder.net/(主要提供实训、实训课堂等业务) -目前有两个域名testbdweb.educoder.net(老版:主要提供课堂、项目、个人主页、后台等服务) - -要求: -1、两服务域名都应该启动‘提供服务 - -2、如果请求链接包含以下的形式,则域名跳至testeduplus2.educoder.net -testeduplus2.educoder.net/shixuns -testeduplus2.educoder.net/shixuns/* -testeduplus2.educoder.net/paths -testeduplus2.educoder.net/paths/* -testeduplus2.educoder.net/myshixuns/ -testeduplus2.educoder.net/tasks/* -testeduplus2.educoder.net/games/* - -如果不满足上述需求的,域名全部跳转至testbdweb.educoder.net -比如:门户首页,如果访问:testeduplus2.educoder.net 应为没包含上述链接。则调制testbdweb.educoder.net -在比如:testeduplus2.educoder.net /users/Hjqreturn没包含上述规则,则跳转到testbdweb.educoder.net/users/Hjqreturn - - -# 需要重构user_extensions 相关sql语句的地方标记 REDO:Extention - -# 文件上传:ActiveStorage -# 新能:bootsnap - -# 注意事项: -# 第一次部署需要执行一些rake任务 -# 配置redis地址 +# README +https://www.trustie.net/issues/24719 +[云上实验室] Logo、导航、底部备案信息定制化 +This README would normally document whatever steps are necessary to get the +application up and running. + +Things you may want to cover: + +* Ruby version + +* System dependencies + +* Configuration + +* Database creation + +* Database initialization + +* How to run the test suite + +* Services (job queues, cache servers, search engines, etc.) + +* Deployment instructions + +* ... +#### Jbuilder介绍 +Jbuilder: https://github.com/rails/jbuilder + +#### Rails5 介绍 +rails guide: https://ruby-china.github.io/rails-guides/v5.0/ + +#### API设计文档 +doc for api: https://www.showdoc.cc/web/#/127895880302646?page_id=729221359592009 +user:Hjqreturn PW:12345678 + +#### 测试版访问地址:https://testeduplus2.educoder.net + +#### 实训平台繁忙 + 仓库异常:繁忙等级(81) + +#### 新版域名跳转规则 +新版域名要求总结:testeduplus2.educoder.net/(主要提供实训、实训课堂等业务) +目前有两个域名testbdweb.educoder.net(老版:主要提供课堂、项目、个人主页、后台等服务) + +要求: +1、两服务域名都应该启动‘提供服务 + +2、如果请求链接包含以下的形式,则域名跳至testeduplus2.educoder.net +testeduplus2.educoder.net/shixuns +testeduplus2.educoder.net/shixuns/* +testeduplus2.educoder.net/paths +testeduplus2.educoder.net/paths/* +testeduplus2.educoder.net/myshixuns/ +testeduplus2.educoder.net/tasks/* +testeduplus2.educoder.net/games/* + +如果不满足上述需求的,域名全部跳转至testbdweb.educoder.net +比如:门户首页,如果访问:testeduplus2.educoder.net 应为没包含上述链接。则调制testbdweb.educoder.net +在比如:testeduplus2.educoder.net /users/Hjqreturn没包含上述规则,则跳转到testbdweb.educoder.net/users/Hjqreturn + + +# 需要重构user_extensions 相关sql语句的地方标记 REDO:Extention + +# 文件上传:ActiveStorage +# 新能:bootsnap + +# 注意事项: +# 第一次部署需要执行一些rake任务 +# 配置redis地址 # 配置gitlab/intializers/gitlab_config.yml \ No newline at end of file diff --git a/app/assets/javascripts/admins/carousels/index.js b/app/assets/javascripts/admins/carousels/index.js index 0f279f17e..fe665a35f 100644 --- a/app/assets/javascripts/admins/carousels/index.js +++ b/app/assets/javascripts/admins/carousels/index.js @@ -1,5 +1,7 @@ $(document).on('turbolinks:load', function() { if ($('body.admins-carousels-index-page').length > 0) { + var laboratoryId = $('#carousels-container').data('laboratoryId'); + // ------------ 保存链接 ----------- $('.carousels-card').on('click', '.save-data-btn', function(){ var $link = $(this); @@ -13,7 +15,7 @@ $(document).on('turbolinks:load', function() { $link.attr('disabled', true); $.ajax({ - url: '/admins/carousels/' + id, + url: '/admins/laboratories/' + laboratoryId + '/carousels/' + id, method: 'PATCH', dataType: 'json', data: { link: link, name: name }, @@ -34,7 +36,7 @@ $(document).on('turbolinks:load', function() { $checkbox.attr('disabled', true); $.ajax({ - url: '/admins/carousels/' + id, + url: '/admins/laboratories/' + laboratoryId + '/carousels/' + id, method: 'PATCH', dataType: 'json', data: { status: checked }, @@ -60,7 +62,7 @@ $(document).on('turbolinks:load', function() { var insertId = $(sibling).data('id') || ''; $.ajax({ - url: '/admins/carousels/drag', + url: '/admins/laboratories/' + laboratoryId + '/carousels/drag', method: 'POST', dataType: 'json', data: { move_id: moveId, after_id: insertId }, diff --git a/app/assets/javascripts/common.js b/app/assets/javascripts/common.js index 8ff5c7bb7..66158b17e 100644 --- a/app/assets/javascripts/common.js +++ b/app/assets/javascripts/common.js @@ -64,4 +64,12 @@ function customConfirm(opts){ } } return $.confirm($.extend({}, defaultOpts, opts)) +} + +function show_success_flash(){ + $.notify({ + message: '操作成功' + },{ + type: 'success' + }); } \ No newline at end of file diff --git a/app/assets/javascripts/cooperative.js b/app/assets/javascripts/cooperative.js new file mode 100644 index 000000000..77e0477eb --- /dev/null +++ b/app/assets/javascripts/cooperative.js @@ -0,0 +1,91 @@ +//= require rails-ujs +//= require activestorage +//= require turbolinks +//= require jquery3 +//= require popper +//= require bootstrap-sprockets +//= require jquery.validate.min +//= require additional-methods.min +//= require bootstrap-notify +//= require jquery.cookie.min +//= require select2 +//= require jquery.cxselect +//= require bootstrap-datepicker +//= require bootstrap.viewer +//= require jquery.mloading +//= require jquery-confirm.min +//= require common + +//= require echarts +//= require codemirror/lib/codemirror +//= require codemirror/mode/shell/shell +//= require editormd/editormd +//= require editormd/languages/zh-tw +//= require dragula/dragula + +//= require_tree ./i18n +//= require_tree ./cooperative + + +$.ajaxSetup({ + beforeSend: function(xhr) { + xhr.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content')); + } +}); + +// ******** select2 global config ******** +$.fn.select2.defaults.set('theme', 'bootstrap4'); +$.fn.select2.defaults.set('language', 'zh-CN'); + +Turbolinks.setProgressBarDelay(200); + +$.notifyDefaults({ + type: 'success', + z_index: 9999, + delay: 2000 +}); + +$(document).on('turbolinks:load', function(){ + $('[data-toggle="tooltip"]').tooltip({ trigger : 'hover' }); + $('[data-toggle="popover"]').popover(); + + // 图片查看大图 + $('img.preview-image').bootstrapViewer(); + + // flash alert提示框自动关闭 + if($('.cooperative-alert-container .alert').length > 0){ + setTimeout(function(){ + $('.cooperative-alert-container .alert:not(.alert-danger)').alert('close'); + }, 2000); + setTimeout(function(){ + $('.cooperative-alert-container .alert.alert-danger').alert('close'); + }, 5000); + } +}); + +$(document).on("turbolinks:before-cache", function () { + $('[data-toggle="tooltip"]').tooltip('hide'); + $('[data-toggle="popover"]').popover('hide'); +}); +// var progressBar = new Turbolinks.ProgressBar(); + +// $(document).on('ajax:send', function(event){ +// console.log('ajax send', event); +// progressBar.setValue(0) +// progressBar.show() +// }); +// +// $(document).on('ajax:complete', function(event){ +// console.log('ajax complete', event); +// progressBar.setValue(1) +// progressBar.hide() // 分页时不触发,奇怪 +// }); +// $(document).on('ajax:success', function(event){ +// console.log('ajax success', event); +// }); +// $(document).on('ajax:error', function(event){ +// console.log('ajax error', event); +// }); + +$(function () { +}); diff --git a/app/assets/javascripts/cooperative/carousels/index.js b/app/assets/javascripts/cooperative/carousels/index.js new file mode 100644 index 000000000..4d8fe1b2c --- /dev/null +++ b/app/assets/javascripts/cooperative/carousels/index.js @@ -0,0 +1,125 @@ +$(document).on('turbolinks:load', function() { + if ($('body.cooperative-carousels-index-page').length > 0) { + // ------------ 保存链接 ----------- + $('.carousels-card').on('click', '.save-data-btn', function(){ + var $link = $(this); + var id = $link.data('id'); + var link = $('.custom-carousel-item-' + id).find('.link-input').val(); + var name = $('.custom-carousel-item-' + id).find('.name-input').val(); + if(!name || name.length == 0){ + $.notify({ message: '名称不能为空' },{ type: 'danger' }); + return; + } + $link.attr('disabled', true); + + $.ajax({ + url: '/cooperative/carousels/' + id, + method: 'PATCH', + dataType: 'json', + data: { link: link, name: name }, + success: function(data){ + $.notify({ message: '操作成功' }); + }, + error: ajaxErrorNotifyHandler, + complete: function(){ + $link.removeAttr('disabled'); + } + }) + }); + // -------------- 是否在首页展示 -------------- + $('.carousels-card').on('change', '.online-check-box', function(){ + var $checkbox = $(this); + var id = $checkbox.data('id'); + var checked = $checkbox.is(':checked'); + $checkbox.attr('disabled', true); + + $.ajax({ + url: '/cooperative/carousels/' + id, + method: 'PATCH', + dataType: 'json', + data: { status: checked }, + success: function(data){ + $.notify({ message: '保存成功' }); + var box = $('.custom-carousel-item-' + id).find('.drag'); + if(checked){ + box.removeClass('not_active'); + }else{ + box.addClass('not_active'); + } + }, + error: ajaxErrorNotifyHandler, + complete: function(){ + $checkbox.removeAttr('disabled'); + } + }) + }); + + // ------------ 拖拽 ------------- + var onDropFunc = function(el, _target, _source, sibling){ + var moveId = $(el).data('id'); + var insertId = $(sibling).data('id') || ''; + + $.ajax({ + url: '/cooperative/carousels/drag', + method: 'POST', + dataType: 'json', + data: { move_id: moveId, after_id: insertId }, + success: function(data){ + $('#carousels-container .custom-carousel-item-no').each(function(index, ele){ + $(ele).html(index + 1); + }) + }, + error: function(res){ + var data = res.responseJSON; + $.notify({message: '移动失败,原因:' + data.message}, {type: 'danger'}); + } + }) + }; + var ele1 = document.getElementById('carousels-container'); + dragula([ele1], { mirrorContainer: ele1 }).on('drop', onDropFunc); + + + // ----------- 新增 -------------- + var $createModal = $('.modal.cooperative-add-carousel-modal'); + var $createForm = $createModal.find('form.cooperative-add-carousel-form'); + + $createForm.validate({ + errorElement: 'span', + errorClass: 'danger text-danger', + rules: { + "portal_image[image]": { + required: true + }, + "portal_image[name]": { + required: true + }, + } + }); + + $createModal.on('show.bs.modal', function(event){ + resetFileInputFunc($createModal.find('.img-file-input')); + $createModal.find('.file-names').html('选择文件'); + }); + + $createModal.on('click', '.submit-btn', function() { + $createForm.find('.error').html(''); + + if ($createForm.valid()) { + $createForm.submit(); + } else { + $createForm.find('.error').html('请选择图片'); + } + }); + $createModal.on('change', '.img-file-input', function(){ + var file = $(this)[0].files[0]; + $createModal.find('.file-names').html(file ? file.name : '请选择文件'); + }) + + // -------------- 重新上传图片 -------------- + //replace_image_url + $('.modal.cooperative-upload-file-modal').on('upload:success', function(e, data){ + var $carouselItem = $('.custom-carousel-item-' + data.source_id); + $carouselItem.find('.custom-carousel-item-img img').attr('src', data.url); + }) + } +}) \ No newline at end of file diff --git a/app/assets/javascripts/cooperative/laboratory_settings/edit.js b/app/assets/javascripts/cooperative/laboratory_settings/edit.js new file mode 100644 index 000000000..50bb96ac9 --- /dev/null +++ b/app/assets/javascripts/cooperative/laboratory_settings/edit.js @@ -0,0 +1,86 @@ +$(document).on('turbolinks:load', function() { + if ($('body.cooperative-laboratory-settings-edit-page, body.cooperative-laboratory-settings-update-page').length > 0) { + var $container = $('.edit-laboratory-setting-container'); + var $form = $container.find('.edit_laboratory'); + + $('.logo-item-left').on("change", 'input[type="file"]', function () { + var $fileInput = $(this); + var file = this.files[0]; + var imageType = /image.*/; + if (file && file.type.match(imageType)) { + var reader = new FileReader(); + reader.onload = function () { + var $box = $fileInput.parent(); + $box.find('img').attr('src', reader.result).css('display', 'block'); + $box.addClass('has-img'); + }; + reader.readAsDataURL(file); + } else { + } + }); + + createMDEditor('laboratory-footer-editor', { height: 200, placeholder: '请输入备案信息' }); + + $form.validate({ + errorElement: 'span', + errorClass: 'danger text-danger', + errorPlacement:function(error,element){ + if(element.parent().hasClass("input-group")){ + element.parent().after(error); + }else{ + element.after(error) + } + }, + rules: { + identifier: { + required: true, + checkSite: true + }, + name: { + required: true + } + } + }); + $.validator.addMethod("checkSite",function(value,element,params){ + var checkSite = /^(?=^.{3,255}$)[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+$/; + return this.optional(element)||(checkSite.test(value + '.educoder.com')); + },"域名不合法!"); + + $form.on('click', '.submit-btn', function(){ + $form.find('.submit-btn').attr('disabled', 'disabled'); + $form.find('.error').html(''); + var valid = $form.valid(); + + $('input[name="navbar[][name]"]').each(function(_, e){ + var $ele = $(e); + if($ele.val() === undefined || $ele.val().length === 0){ + $ele.addClass('danger text-danger'); + valid = false; + } else { + $ele.removeClass('danger text-danger'); + } + }); + + if(!valid) return; + $.ajax({ + method: 'PATCH', + dataType: 'json', + url: $form.attr('action'), + data: new FormData($form[0]), + processData: false, + contentType: false, + success: function(data){ + $.notify({ message: '保存成功' }); + window.location.reload(); + }, + error: function(res){ + var data = res.responseJSON; + $form.find('.error').html(data.message); + }, + complete: function(){ + $form.find('.submit-btn').attr('disabled', false); + } + }); + }) + } +}); \ No newline at end of file diff --git a/app/assets/javascripts/cooperative/laboratory_users/index.js b/app/assets/javascripts/cooperative/laboratory_users/index.js new file mode 100644 index 000000000..cd705d8d0 --- /dev/null +++ b/app/assets/javascripts/cooperative/laboratory_users/index.js @@ -0,0 +1,62 @@ +$(document).on('turbolinks:load', function() { + if ($('body.cooperative-laboratory-users-index-page').length > 0) { + // ============= 添加管理员 ============== + var $addMemberModal = $('.cooperative-add-laboratory-user-modal'); + var $addMemberForm = $addMemberModal.find('.cooperative-add-laboratory-user-form'); + var $memberSelect = $addMemberModal.find('.laboratory-user-select'); + + $addMemberModal.on('show.bs.modal', function(event){ + $memberSelect.select2('val', ' '); + }); + + $memberSelect.select2({ + theme: 'bootstrap4', + placeholder: '请输入要添加的管理员姓名', + multiple: true, + minimumInputLength: 1, + ajax: { + delay: 500, + url: '/cooperative/users', + dataType: 'json', + data: function(params){ + return { name: params.term }; + }, + processResults: function(data){ + return { results: data.users } + } + }, + templateResult: function (item) { + if(!item.id || item.id === '') return item.text; + return item.real_name + "--" + item.identity; + }, + templateSelection: function(item){ + if (item.id) { + } + return item.real_name || item.text; + } + }); + + $addMemberModal.on('click', '.submit-btn', function(){ + $addMemberForm.find('.error').html(''); + + var memberIds = $memberSelect.val(); + if (memberIds && memberIds.length > 0) { + $.ajax({ + method: 'POST', + dataType: 'json', + url: '/cooperative/laboratory_users', + data: { user_ids: memberIds }, + success: function(data){ + if(data && data.status == 0){ + show_success_flash(); + $addMemberModal.modal('hide'); + window.location.reload(); + } + } + }); + } else { + $addMemberModal.modal('hide'); + } + }); + } +}); diff --git a/app/assets/javascripts/cooperative/modals/upload-file-modal.js b/app/assets/javascripts/cooperative/modals/upload-file-modal.js new file mode 100644 index 000000000..835ccd383 --- /dev/null +++ b/app/assets/javascripts/cooperative/modals/upload-file-modal.js @@ -0,0 +1,62 @@ +$(document).on('turbolinks:load', function() { + var $modal = $('.modal.cooperative-upload-file-modal'); + if ($modal.length > 0) { + var $form = $modal.find('form.cooperative-upload-file-form') + var $sourceIdInput = $modal.find('input[name="source_id"]'); + var $sourceTypeInput = $modal.find('input[name="source_type"]'); + + $modal.on('show.bs.modal', function(event){ + var $link = $(event.relatedTarget); + var sourceId = $link.data('sourceId'); + var sourceType = $link.data('sourceType'); + + $sourceIdInput.val(sourceId); + $sourceTypeInput.val(sourceType); + + $modal.find('.upload-file-input').trigger('click'); + }); + + $modal.find('.upload-file-input').on('change', function(e){ + var file = $(this)[0].files[0]; + + if(file){ + $modal.find('.file-names').html(file.name); + $modal.find('.submit-btn').trigger('click'); + } + }) + + var formValid = function(){ + if($form.find('input[name="file"]').val() == undefined || $form.find('input[name="file"]').val().length == 0){ + $form.find('.error').html('请选择文件'); + return false; + } + + return true; + }; + + $modal.on('click', '.submit-btn', function(){ + $form.find('.error').html(''); + + if (formValid()) { + var formDataString = $form.serialize(); + $.ajax({ + method: 'POST', + dataType: 'json', + url: '/cooperatives/files?' + formDataString, + data: new FormData($form[0]), + processData: false, + contentType: false, + success: function(data){ + $.notify({ message: '上传成功' }); + $modal.trigger('upload:success', data); + $modal.modal('hide'); + }, + error: function(res){ + var data = res.responseJSON; + $form.find('.error').html(data.message); + } + }); + } + }); + } +}); \ No newline at end of file diff --git a/app/assets/javascripts/cooperative/sidebar.js b/app/assets/javascripts/cooperative/sidebar.js new file mode 100644 index 000000000..dea677d54 --- /dev/null +++ b/app/assets/javascripts/cooperative/sidebar.js @@ -0,0 +1,16 @@ +$(document).on('turbolinks:load', function(){ + $('#sidebarCollapse').on('click', function () { + $(this).toggleClass('active'); + $('#sidebar').toggleClass('active'); + $.cookie('cooperative_sidebar_collapse', $(this).hasClass('active'), {path: '/cooperative'}); + }); + + var sidebarController = $('#sidebar').data('current-controller'); + if (sidebarController.length > 0) { + $('#sidebar a.active').removeClass('active'); + $('#sidebar ul.collapse.show').removeClass('show'); + var activeLi = $('#sidebar a[data-controller="' + sidebarController + '"]'); + activeLi.addClass('active'); + activeLi.parent().parent('ul.collapse').addClass('show'); + } +}); \ No newline at end of file diff --git a/app/assets/stylesheets/admins/common.scss b/app/assets/stylesheets/admins/common.scss index adad72997..aabe6085c 100644 --- a/app/assets/stylesheets/admins/common.scss +++ b/app/assets/stylesheets/admins/common.scss @@ -48,9 +48,15 @@ } .action-container { - .action { + & > .action { padding: 0 3px; } + + .more-action-dropdown { + .dropdown-item { + font-size: 14px; + } + } } /* 分页 */ diff --git a/app/assets/stylesheets/admins/laboratories.scss b/app/assets/stylesheets/admins/laboratories.scss index ad5c8c5a8..69f4586d1 100644 --- a/app/assets/stylesheets/admins/laboratories.scss +++ b/app/assets/stylesheets/admins/laboratories.scss @@ -31,6 +31,7 @@ display: block; width: 80px; height: 80px; + background: #f0f0f0; } &-upload { diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css deleted file mode 100644 index fad1eb25d..000000000 --- a/app/assets/stylesheets/application.css +++ /dev/null @@ -1,16 +0,0 @@ -/* - * This is a manifest file that'll be compiled into application.css, which will include all the files - * listed below. - * - * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's - * vendor/assets/stylesheets directory can be referenced here using a relative path. - * - * You're free to add application-wide styles to this file and they'll appear at the bottom of the - * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS - * files in this directory. Styles in this file should be added after the last require_* statement. - * It is generally better to create a new file per style scope. - * - *= require_tree . - *= require_self - - */ diff --git a/app/assets/stylesheets/cooperative.scss b/app/assets/stylesheets/cooperative.scss new file mode 100644 index 000000000..56984b96b --- /dev/null +++ b/app/assets/stylesheets/cooperative.scss @@ -0,0 +1,54 @@ +@import "bootstrap"; +@import "font-awesome-sprockets"; +@import "font-awesome"; +@import "select2.min"; +@import "select2-bootstrap4.min"; +@import "bootstrap-datepicker"; +@import "bootstrap-datepicker.standalone"; +@import "jquery.mloading"; +@import "jquery-confirm.min"; + +@import "codemirror/lib/codemirror"; +@import "editormd/css/editormd.min"; +@import "dragula/dragula"; + +@import "common"; +@import "cooperative/*"; + +body { + width: 100vw; + height: 100vh; + max-width: 100vw; + max-height: 100vh; + display: flex; + align-items: stretch; + font-size: 14px; + background: #efefef; + overflow: hidden; +} + +.simple_form { + .form-group { + .collection_radio_buttons { + margin-bottom: 0px; + } + + .form-check-inline { + height: calc(1.5em + 0.75rem + 2px) + } + } +} + +input.form-control { + font-size: 14px; +} + +.btn-default{ + color: #666; + background: #e1e1e1!important; +} +.export-absolute{ + right:20px; + position: absolute; +} + diff --git a/app/assets/stylesheets/cooperative/carousels.scss b/app/assets/stylesheets/cooperative/carousels.scss new file mode 100644 index 000000000..092e02fe8 --- /dev/null +++ b/app/assets/stylesheets/cooperative/carousels.scss @@ -0,0 +1,60 @@ +.cooperative-carousels-index-page { + .carousels-card { + .custom-carousel-item { + & > .drag { + cursor: move; + background: #fff; + box-shadow: 1px 2px 5px 3px #f0f0f0; + } + + &-no { + font-size: 28px; + text-align: center; + } + + &-img { + cursor: pointer; + width: 100%; + height: 60px; + + & > img { + display: block; + width: 100%; + height: 60px; + background: #F5F5F5; + } + } + + .not_active { + background: #F0F0F0; + } + + .delete-btn { + font-size: 20px; + color: red; + cursor: pointer; + } + + .save-url-btn { + cursor: pointer; + } + + .operate-box { + display: flex; + justify-content: space-between; + align-items: center; + } + + .online-check-box { + font-size: 20px; + } + + .name-input { + flex: 1; + } + .link-input { + flex: 3; + } + } + } +} \ No newline at end of file diff --git a/app/assets/stylesheets/cooperative/common.scss b/app/assets/stylesheets/cooperative/common.scss new file mode 100644 index 000000000..488dd4caa --- /dev/null +++ b/app/assets/stylesheets/cooperative/common.scss @@ -0,0 +1,126 @@ +.cooperative-body-container { + padding: 20px; + flex: 1; + min-height: 100vh; + display: flex; + flex-direction: column; + overflow-y: scroll; + + & > .content { + flex: 1; + font-size: 14px; + + .box { + padding: 20px; + border-radius: 5px; + background: #fff; + } + } + + /* 面包屑 */ + .breadcrumb { + padding-left: 5px; + font-size: 20px; + background: unset; + } + + /* 内容表格 */ + table { + table-layout: fixed; + + td { + vertical-align: middle; + } + + tr { + &.no-data { + &:hover { + color: darkgrey; + background: unset; + } + + & > td { + text-align: center; + height: 300px; + } + } + } + } + + .action-container { + & > .action { + padding: 0 3px; + } + + .more-action-dropdown { + .dropdown-item { + font-size: 14px; + } + } + } + + /* 分页 */ + .paginate-container { + margin-top: 20px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + .paginate-total { + margin-bottom: 10px; + color: darkgrey; + } + + .pagination { + margin-bottom: 0px; + } + } + + /* 搜索表单 */ + .search-form-container { + display: flex; + margin-bottom: 20px; + + .search-form { + flex: 1; + + * { font-size: 14px; } + + select, input { + margin-right: 10px; + font-size: 14px; + } + } + } + + .global-error { + color: grey; + min-height: 300px; + + &-code { + font-size: 80px; + } + + &-text { + font-size: 24px; + } + } + + .nav-tabs { + .nav-link { + padding: 0.5rem 2rem; + } + } + + .CodeMirror { + border: 1px solid #ced4da; + } + + .batch-action-container { + margin-bottom: -15px; + padding: 10px 20px 0; + background: #fff; + } +} + diff --git a/app/assets/stylesheets/cooperative/laboratory_settings.scss b/app/assets/stylesheets/cooperative/laboratory_settings.scss new file mode 100644 index 000000000..fde0cddc3 --- /dev/null +++ b/app/assets/stylesheets/cooperative/laboratory_settings.scss @@ -0,0 +1,76 @@ +.cooperative-laboratory-settings-edit-page, .cooperative-laboratory-settings-update-page { + .edit-laboratory-setting-container { + .logo-item { + display: flex; + + &-img { + display: block; + width: 80px; + height: 80px; + background: #f0f0f0; + } + + &-upload { + cursor: pointer; + position: absolute; + top: 0; + width: 80px; + height: 80px; + background: #F5F5F5; + border: 1px solid #E5E5E5; + + &::before { + content: ''; + position: absolute; + top: 27px; + left: 39px; + width: 2px; + height: 26px; + background: #E5E5E5; + } + + &::after { + content: ''; + position: absolute; + top: 39px; + left: 27px; + width: 26px; + height: 2px; + background: #E5E5E5; + } + } + + &-left { + position: relative; + width: 80px; + height: 80px; + + &.has-img { + .logo-item-upload { + display: none; + } + + &:hover { + .logo-item-upload { + display: block; + background: rgba(145, 145, 145, 0.8); + } + } + } + } + + &-right { + display: flex; + flex-direction: column; + justify-content: space-between; + color: #777777; + font-size: 12px; + } + + &-title { + color: #23272B; + font-size: 14px; + } + } + } +} \ No newline at end of file diff --git a/app/assets/stylesheets/cooperative/sidebar.scss b/app/assets/stylesheets/cooperative/sidebar.scss new file mode 100644 index 000000000..e943acd90 --- /dev/null +++ b/app/assets/stylesheets/cooperative/sidebar.scss @@ -0,0 +1,235 @@ +#sidebar { + min-width: 200px; + max-width: 200px; + background: #272822; + color: #fff; + transition: all 0.5s; + overflow-y: scroll; + + &::-webkit-scrollbar { + display:none + } + + &.active { + min-width: 60px; + max-width: 60px; + text-align: center; + + .sidebar-header { + padding: 10px; + display: flex; + flex-direction: column; + + &-logo { + overflow: hidden; + margin-bottom: 10px; + + & > .logo-label { + display: none; + } + } + } + + ul li a { + padding: 10px; + text-align: center; + font-size: 0.85em; + display: flex; + justify-content: center; + + span { display: none } + + i { + margin-right: 0; + display: block; + font-size: 1.8em; + margin-bottom: 5px; + width: 30px; + height: 20px; + } + } + + .dropdown-toggle::after { + top: auto; + bottom: 10px; + right: 50%; + -webkit-transform: translateX(50%); + -ms-transform: translateX(50%); + transform: translateX(50%); + } + + ul ul a { + padding: 10px !important; + + span { display: none } + + i { + margin-left: 0px; + display: block; + font-size: 0.8em; + width: 30px; + height: 10px; + } + } + } + + .sidebar-header { + padding: 20px; + background: #272822; + display: flex; + flex-direction: row; + justify-content: space-between; + + &-logo { + display: flex; + justify-content: space-between; + align-items: center; + + & > img { + width: 40px; + height: auto; + } + + & > .logo-label { + font-size: 18px; + color: darkgrey; + margin-left: 10px; + } + } + } + + #sidebarCollapse { + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + text-align: right; + + &.active { + width: 40px; + height: 30px; + background: #3f3f3f; + border: 1px solid grey; + border-radius: 3px; + + i.fold { display: none; } + i.unfold { display: block; } + } + + i.fold { + display: block; + } + i.unfold { display: none; } + } + + a, a:hover, a:focus { + color: inherit; + text-decoration: none; + transition: all 0.3s; + } + + & > ul > li > a > i { + width: 14px; + height: 14px; + } + + ul { + &.components { + padding: 20px 0; + border-bottom: 1px solid #3f3f3f; + } + + p { + color: #fff; + padding: 10px; + } + + li > a { + padding: 10px; + font-size: 1em; + display: block; + text-align: left; + + i { + margin-right: 10px; + font-size: 1em; + margin-bottom: 5px; + } + } + + li a { + &:hover, &.active { + color: #fff; + background: #276891; + } + } + + li.active > a, a[aria-expanded="true"] { + color: #fff; + //background: #276891; + } + + ul a { + font-size: 0.9em !important; + padding-left: 30px !important; + background: #3f3f3f; + } + } +} + +@media (max-width: 768px) { + #sidebar { + &.active { + padding: 10px 5px; + min-width: 40px; + max-width: 40px; + text-align: center; + margin-left: 0; + transform: none; + + .sidebar-header { + padding: 0px; + + .sidebar-header-logo { + display: none; + } + + #sidebarCollapse { + width: 30px; + height: 20px; + } + } + + ul li a { + padding: 10px; + font-size: 0.85em; + + i { + margin-right: 0; + display: block; + margin-bottom: 5px; + } + } + + & > ul > li > a > i { + font-size: 1.8em; + } + + ul ul a { + padding: 10px !important; + } + } + + .sidebar-header { + } + } + + .dropdown-toggle::after { + top: auto; + bottom: 10px; + right: 50%; + -webkit-transform: translateX(50%); + -ms-transform: translateX(50%); + transform: translateX(50%); + } +} diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 7c27b3c83..49f833e67 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -183,20 +183,6 @@ class AccountsController < ApplicationController end private - def autologin_cookie_name - edu_setting('autologin_cookie_name') || 'autologin' - end - - def logout_user - if User.current.logged? - if autologin = cookies.delete(autologin_cookie_name) - User.current.delete_autologin_token(autologin) - end - User.current.delete_session_token(session[:tk]) - self.logged_user = nil - end - session[:user_id] = nil - end # type 事件类型 1:用户注册 2:忘记密码 3: 绑定手机 4: 绑定邮箱, 5: 验证手机号是否有效 # 如果有新的继续后面加 # login_type 1:手机类型 2:邮箱类型 diff --git a/app/controllers/admins/base_controller.rb b/app/controllers/admins/base_controller.rb index 345df3e31..e2e3babae 100644 --- a/app/controllers/admins/base_controller.rb +++ b/app/controllers/admins/base_controller.rb @@ -1,7 +1,7 @@ class Admins::BaseController < ApplicationController - include Admins::PaginateHelper + include Base::PaginateHelper include Admins::RenderHelper - include Admins::ErrorRescueHandler + include Base::ErrorRescueHandler layout 'admin' diff --git a/app/controllers/admins/carousels_controller.rb b/app/controllers/admins/carousels_controller.rb index cd693d2fd..ad6af7649 100644 --- a/app/controllers/admins/carousels_controller.rb +++ b/app/controllers/admins/carousels_controller.rb @@ -1,15 +1,17 @@ class Admins::CarouselsController < Admins::BaseController before_action :convert_file!, only: [:create] + helper_method :current_laboratory + def index - @images = PortalImage.order(position: :asc) + @images = current_laboratory.portal_images.order(position: :asc) end def create - position = PortalImage.count + 1 + position = current_laboratory.portal_images.count + 1 ActiveRecord::Base.transaction do - image = PortalImage.create!(create_params.merge(position: position)) + image = current_laboratory.portal_images.create!(create_params.merge(position: position)) file_path = Util::FileManage.disk_filename('PortalImage', image.id) File.delete(file_path) if File.exist?(file_path) # 删除之前的文件 @@ -17,7 +19,7 @@ class Admins::CarouselsController < Admins::BaseController end flash[:success] = '保存成功' - redirect_to admins_carousels_path + redirect_to admins_laboratory_carousels_path(current_laboratory) end def update @@ -29,7 +31,7 @@ class Admins::CarouselsController < Admins::BaseController ActiveRecord::Base.transaction do current_image.destroy! # 前移 - PortalImage.where('position > ?', current_image.position) + current_laboratory.portal_images.where('position > ?', current_image.position) .update_all('position = position - 1') file_path = Util::FileManage.disk_filename('PortalImage', current_image.id) @@ -39,10 +41,10 @@ class Admins::CarouselsController < Admins::BaseController end def drag - move = PortalImage.find_by(id: params[:move_id]) - after = PortalImage.find_by(id: params[:after_id]) + move = current_laboratory.portal_images.find_by(id: params[:move_id]) + after = current_laboratory.portal_images.find_by(id: params[:after_id]) - Admins::DragPortalImageService.call(move, after) + Admins::DragPortalImageService.call(current_laboratory, move, after) render_ok rescue Admins::DragPortalImageService::Error => e render_error(e.message) @@ -50,8 +52,12 @@ class Admins::CarouselsController < Admins::BaseController private + def current_laboratory + @_current_laboratory ||= Laboratory.find(params[:laboratory_id]) + end + def current_image - @_current_image ||= PortalImage.find(params[:id]) + @_current_image ||= current_laboratory.portal_images.find(params[:id]) end def create_params diff --git a/app/controllers/admins/competition_settings_controller.rb b/app/controllers/admins/competition_settings_controller.rb new file mode 100644 index 000000000..390ad17e8 --- /dev/null +++ b/app/controllers/admins/competition_settings_controller.rb @@ -0,0 +1,20 @@ +class Admins::CompetitionSettingsController < Admins::BaseController + def show + @competition = current_competition + end + + def update + Admins::SaveLaboratorySettingService.call(current_competition, form_params) + render_ok + end + + private + + def current_competition + @_current_competition ||= Competition.find(params[:competition_id]) + end + + def form_params + params.permit(:identifier, :name, :nav_logo, :login_logo, :tab_logo, :footer, navbar: %i[name link hidden]) + end +end \ No newline at end of file diff --git a/app/controllers/admins/competitions_controller.rb b/app/controllers/admins/competitions_controller.rb new file mode 100644 index 000000000..3b9b63243 --- /dev/null +++ b/app/controllers/admins/competitions_controller.rb @@ -0,0 +1,46 @@ +class Admins::CompetitionsController < Admins::BaseController + include CustomSortable + before_action :find_competition, except: [:index] + + def index + params[:sort_by] = params[:sort_by].presence || 'created_on' + params[:sort_direction] = params[:sort_direction].presence || 'desc' + @competitions = custom_sort Competition.all, params[:sort_by], params[:sort_direction] + @params_page = params[:page] || 1 + @competitions = paginate @competitions + ids = @competitions.map(&:id) + @member_count_map = TeamMember.where(competition_id: ids).group(:competition_id).count + + respond_to do |format| + format.js + format.html + end + end + + def publish + @competition.update_attributes!(:published_at, Time.now) + end + + def unpublish + @competition.update_attributes!(:published_at, nil) + end + + def online_switch + if @competition.status + @competition.update_attributes!(status: false) + else + @competition.update_attributes!(status: true, online_time: Time.now) + end + end + + def enroll_list + + end + + private + + def find_competition + @competition = Competition.find_by(id: params[:id]) + end + +end \ No newline at end of file diff --git a/app/controllers/admins/shixun_settings_controller.rb b/app/controllers/admins/shixun_settings_controller.rb index 9202ccce6..9919c7daa 100644 --- a/app/controllers/admins/shixun_settings_controller.rb +++ b/app/controllers/admins/shixun_settings_controller.rb @@ -46,7 +46,7 @@ class Admins::ShixunSettingsController < Admins::BaseController tag_ids.each do |id| unless tag_repertoire_ids.include?(id) tag_repertoire = @shixun.shixun_tag_repertoires.new(shixun_id:@shixun.id,tag_repertoire_id:id) - tag_repertoire.save + tag_repertoire.save! end end else diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 53e1be6e7..e6b58bc3b 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -8,6 +8,7 @@ class ApplicationController < ActionController::Base include GitHelper include LoggerHelper include LaboratoryHelper + include LoginHelper protect_from_forgery prepend: true, unless: -> { request.format.json? } @@ -235,12 +236,6 @@ class ApplicationController < ActionController::Base # end end - def start_user_session(user) - session[:user_id] = user.id - session[:ctime] = Time.now.utc.to_i - session[:atime] = Time.now.utc.to_i - end - def user_setup # reacct静态资源加载不需要走这一步 return if params[:controller] == "main" @@ -281,17 +276,6 @@ class ApplicationController < ActionController::Base # User.current = User.find 81403 end - # Sets the logged in user - def logged_user=(user) - reset_session - if user && user.is_a?(User) - User.current = user - start_user_session(user) - else - User.current = User.anonymous - end - end - # Returns the current user or nil if no user is logged in # and starts a session if needed def find_current_user @@ -307,10 +291,6 @@ class ApplicationController < ActionController::Base end end - def autologin_cookie_name - edu_setting('autologin_cookie_name').presence || 'autologin' - end - def try_to_autologin if cookies[autologin_cookie_name] # auto-login feature starts a new session @@ -358,9 +338,9 @@ class ApplicationController < ActionController::Base # 如果代码窗口是隐藏的,则不用保存代码 return if myshixun.shixun.hide_code || myshixun.shixun.vnc file_content = git_fle_content myshixun.repo_path, path - unless file_content.present? - raise("获取文件代码异常") - end + #unless file_content.present? + # raise("获取文件代码异常") + #end logger.info("#######game_id:#{game_id}, file_content:#{file_content}") game_code = GameCode.where(:game_id => game_id, :path => path).first if game_code.nil? @@ -621,22 +601,6 @@ class ApplicationController < ActionController::Base cookies[:fileDownload] = true end - def set_autologin_cookie(user) - token = Token.get_or_create_permanent_login_token(user, "autologin") - cookie_options = { - :value => token.value, - :expires => 1.month.from_now, - :path => '/', - :secure => false, - :httponly => true - } - if edu_setting('cookie_domain').present? - cookie_options = cookie_options.merge(domain: edu_setting('cookie_domain')) - end - cookies[autologin_cookie_name] = cookie_options - logger.info("cookies is #{cookies}") - end - # 149课程的评审用户数据创建(包含创建课堂学生) def open_class_user user = User.find_by(login: "OpenClassUser") diff --git a/app/controllers/attachments_controller.rb b/app/controllers/attachments_controller.rb index df74cf737..258ab9d83 100644 --- a/app/controllers/attachments_controller.rb +++ b/app/controllers/attachments_controller.rb @@ -83,6 +83,7 @@ class AttachmentsController < ApplicationController @file.destroy! delete_file(@file_path) + normal_status("删除成功") rescue Exception => e uid_logger_error(e.message) tip_exception(e.message) diff --git a/app/controllers/bind_users_controller.rb b/app/controllers/bind_users_controller.rb new file mode 100644 index 000000000..354b2993b --- /dev/null +++ b/app/controllers/bind_users_controller.rb @@ -0,0 +1,22 @@ +class BindUsersController < ApplicationController + before_action :require_login + + def create + user = CreateBindUserService.call(current_user, create_params) + successful_authentication(user) if user.id != current_user.id + + render_ok + rescue ApplicationService::Error => ex + render_error(ex.message) + end + + def new_user + current_user + end + + private + + def create_params + params.permit(:username, :password, :type, :not_bind) + end +end \ No newline at end of file diff --git a/app/controllers/boards_controller.rb b/app/controllers/boards_controller.rb index 1081a82ce..9d3afc80c 100644 --- a/app/controllers/boards_controller.rb +++ b/app/controllers/boards_controller.rb @@ -2,7 +2,7 @@ class BoardsController < ApplicationController before_action :require_login, :check_auth before_action :find_course, only: [:create] before_action :set_board, except: [:create] - before_action :teacher_or_admin_allowed + before_action :teacher_allowed def index @boards = @course.boards.includes(messages: [:last_reply, :author]) @@ -20,9 +20,8 @@ class BoardsController < ApplicationController new_board.parent_id = board.try(:id) new_board.position = board.children.count + 1 new_board.save! + render :json => {category_id: new_board.id, status: 0, message: "添加成功"} end - - normal_status(0, "添加成功") end # 子目录的拖动 diff --git a/app/controllers/competitions/competitions_controller.rb b/app/controllers/competitions/competitions_controller.rb index 34dac7350..77159fdaf 100644 --- a/app/controllers/competitions/competitions_controller.rb +++ b/app/controllers/competitions/competitions_controller.rb @@ -1,5 +1,6 @@ class Competitions::CompetitionsController < Competitions::BaseController skip_before_action :require_login + before_action :allow_visit, except: [:index] def index # 已上架 或者 即将上架 @@ -24,10 +25,10 @@ class Competitions::CompetitionsController < Competitions::BaseController end def show - unless current_competition.published? || admin_or_business? - render_forbidden - return - end + end + + def common_header + end private @@ -35,4 +36,11 @@ class Competitions::CompetitionsController < Competitions::BaseController def current_competition @_current_competition ||= Competition.find_by!(identifier: params[:id]) end + + def allow_visit + unless current_competition.published? || admin_or_business? + render_forbidden + return + end + end end \ No newline at end of file diff --git a/app/controllers/concerns/admins/render_helper.rb b/app/controllers/concerns/admins/render_helper.rb index 0f136b62d..a43a094d3 100644 --- a/app/controllers/concerns/admins/render_helper.rb +++ b/app/controllers/concerns/admins/render_helper.rb @@ -1,39 +1,7 @@ module Admins::RenderHelper extend ActiveSupport::Concern - def render_by_format(hash) - format = request.format.symbol - hash.key?(format) ? hash[format].call : hash[:html].call - end - - def render_forbidden - render_by_format(html: -> { current_user&.business? ? render('admins/shared/403') : redirect_to('/403') }, - json: -> { render status: 403, json: { messages: I18n.t('error.forbidden') } } ) - end - - def render_not_found - render_by_format(html: -> { render 'admins/shared/404' }, - js: -> { render_js_error('资源未找到') }, - json: -> { render status: 404, json: { message: '资源未找到' } }) - end - - def render_unprocessable_entity(message, type: :alert) - render_by_format(html: -> { render 'admins/shared/422' }, - js: -> { render_js_error(message, type: type) }, - json: -> { render status: 422, json: { message: message } }) - end - alias_method :render_error, :render_unprocessable_entity - - def internal_server_error(message = '系统错误') - @message = message - render_by_format(html: -> { render 'admins/shared/500' }, - js: -> { render_js_error(message) }, - json: -> { render status: 500, json: { message: message } }) - end - - def render_js_template(template, **opts) - render({ template: template, formats: :js }.merge(opts)) - end + include Base::RenderHelper def render_delete_success render_js_template 'admins/shared/delete' diff --git a/app/controllers/concerns/admins/error_rescue_handler.rb b/app/controllers/concerns/base/error_rescue_handler.rb similarity index 94% rename from app/controllers/concerns/admins/error_rescue_handler.rb rename to app/controllers/concerns/base/error_rescue_handler.rb index ceb810f36..d64ce4356 100644 --- a/app/controllers/concerns/admins/error_rescue_handler.rb +++ b/app/controllers/concerns/base/error_rescue_handler.rb @@ -1,4 +1,4 @@ -module Admins::ErrorRescueHandler +module Base::ErrorRescueHandler extend ActiveSupport::Concern included do diff --git a/app/controllers/concerns/admins/paginate_helper.rb b/app/controllers/concerns/base/paginate_helper.rb similarity index 94% rename from app/controllers/concerns/admins/paginate_helper.rb rename to app/controllers/concerns/base/paginate_helper.rb index da7652584..a10d7eb2e 100644 --- a/app/controllers/concerns/admins/paginate_helper.rb +++ b/app/controllers/concerns/base/paginate_helper.rb @@ -1,4 +1,4 @@ -module Admins::PaginateHelper +module Base::PaginateHelper extend ActiveSupport::Concern def offset diff --git a/app/controllers/concerns/base/render_helper.rb b/app/controllers/concerns/base/render_helper.rb new file mode 100644 index 000000000..e0aa49ac1 --- /dev/null +++ b/app/controllers/concerns/base/render_helper.rb @@ -0,0 +1,37 @@ +module Base::RenderHelper + extend ActiveSupport::Concern + + def render_by_format(hash) + format = request.format.symbol + hash.key?(format) ? hash[format].call : hash[:html].call + end + + def render_forbidden + render_by_format(html: -> { current_user&.business? ? render('shared/403') : redirect_to('/403') }, + json: -> { render status: 403, json: { messages: I18n.t('error.forbidden') } } ) + end + + def render_not_found + render_by_format(html: -> { render 'shared/404' }, + js: -> { render_js_error('资源未找到') }, + json: -> { render status: 404, json: { message: '资源未找到' } }) + end + + def render_unprocessable_entity(message, type: :alert) + render_by_format(html: -> { render 'shared/422' }, + js: -> { render_js_error(message, type: type) }, + json: -> { render status: 422, json: { message: message } }) + end + alias_method :render_error, :render_unprocessable_entity + + def internal_server_error(message = '系统错误') + @message = message + render_by_format(html: -> { render 'shared/500' }, + js: -> { render_js_error(message) }, + json: -> { render status: 500, json: { message: message } }) + end + + def render_js_template(template, **opts) + render({ template: template, formats: :js }.merge(opts)) + end +end \ No newline at end of file diff --git a/app/controllers/concerns/cooperative/render_helper.rb b/app/controllers/concerns/cooperative/render_helper.rb new file mode 100644 index 000000000..3110ca304 --- /dev/null +++ b/app/controllers/concerns/cooperative/render_helper.rb @@ -0,0 +1,16 @@ +module Cooperative::RenderHelper + include Base::RenderHelper + + def render_delete_success + render_js_template 'cooperative/shared/delete' + end + alias_method :render_success_js, :render_delete_success + + def render_js_error(message, type: :alert) + if type == :notify + render js: "$.notify({ message: '#{message}' },{ type: 'danger', delay: 5000 });" + else + render_js_template 'cooperative/shared/error', locals: { message: message } + end + end +end \ No newline at end of file diff --git a/app/controllers/concerns/laboratory_helper.rb b/app/controllers/concerns/laboratory_helper.rb index fbb18b36d..157957b03 100644 --- a/app/controllers/concerns/laboratory_helper.rb +++ b/app/controllers/concerns/laboratory_helper.rb @@ -2,6 +2,7 @@ module LaboratoryHelper extend ActiveSupport::Concern included do + helper_method :current_laboratory helper_method :default_setting end @@ -9,6 +10,10 @@ module LaboratoryHelper @_current_laboratory ||= (Laboratory.find_by_subdomain(request.subdomain) || Laboratory.find(1)) end + def default_laboratory + @_default_laboratory ||= Laboratory.find(1) + end + def default_setting @_default_setting ||= LaboratorySetting.find_by(laboratory_id: 1) end diff --git a/app/controllers/concerns/login_helper.rb b/app/controllers/concerns/login_helper.rb new file mode 100644 index 000000000..e94cf8a21 --- /dev/null +++ b/app/controllers/concerns/login_helper.rb @@ -0,0 +1,69 @@ +module LoginHelper + extend ActiveSupport::Concern + + def edu_setting(name) + EduSetting.get(name) + end + + def autologin_cookie_name + edu_setting('autologin_cookie_name').presence || 'autologin' + end + + def set_autologin_cookie(user) + token = Token.get_or_create_permanent_login_token(user, "autologin") + cookie_options = { + :value => token.value, + :expires => 1.month.from_now, + :path => '/', + :secure => false, + :httponly => true + } + if edu_setting('cookie_domain').present? + cookie_options = cookie_options.merge(domain: edu_setting('cookie_domain')) + end + cookies[autologin_cookie_name] = cookie_options + Rails.logger.info("cookies is #{cookies}") + end + + def successful_authentication(user) + Rails.logger.info("id: #{user&.id} Successful authentication start: '#{user.login}' from #{request.remote_ip} at #{Time.now.utc}") + # Valid user + self.logged_user = user + + # generate a key and set cookie if autologin + set_autologin_cookie(user) + + UserAction.create(action_id: user&.id, action_type: 'Login', user_id: user&.id, ip: request.remote_ip) + user.update_column(:last_login_on, Time.now) + # 注册完成后有一天的试用申请(先去掉) + # UserDayCertification.create(user_id: user.id, status: 1) + end + + def logout_user + if User.current.logged? + if autologin = cookies.delete(autologin_cookie_name) + User.current.delete_autologin_token(autologin) + end + User.current.delete_session_token(session[:tk]) + self.logged_user = nil + end + session[:user_id] = nil + end + + # Sets the logged in user + def logged_user=(user) + reset_session + if user && user.is_a?(User) + User.current = user + start_user_session(user) + else + User.current = User.anonymous + end + end + + def start_user_session(user) + session[:user_id] = user.id + session[:ctime] = Time.now.utc.to_i + session[:atime] = Time.now.utc.to_i + end +end \ No newline at end of file diff --git a/app/controllers/cooperative/base_controller.rb b/app/controllers/cooperative/base_controller.rb new file mode 100644 index 000000000..463880e87 --- /dev/null +++ b/app/controllers/cooperative/base_controller.rb @@ -0,0 +1,58 @@ +class Cooperative::BaseController < ApplicationController + include Base::PaginateHelper + include Cooperative::RenderHelper + include Base::ErrorRescueHandler + + layout 'cooperative' + + skip_before_action :verify_authenticity_token + + before_action :laboratory_exist!, :require_login, :require_cooperative_manager! + + after_action :rebind_event_if_ajax_render_partial + + helper_method :current_laboratory, :current_setting_or_default + + private + + def current_laboratory + @_current_laboratory ||= Laboratory.find_by_subdomain(request.subdomain) + end + + def current_setting_or_default(name) + current_laboratory.public_send(name) || default_setting.public_send(name) + end + + def laboratory_exist! + return if current_laboratory.present? + + redirect_to '/403' + end + + def require_login + return if User.current.logged? + + redirect_to "/login?back_url=#{CGI::escape(request.fullpath)}" + end + + def require_cooperative_manager! + return if current_user.blank? || !current_user.logged? + return if current_user.admin_or_business? + return if current_laboratory.laboratory_users.exists?(user_id: current_user.id) + + render_forbidden + end + + # 触发after ajax render partial hooks,执行一些因为局部刷新后失效的绑定事件 + def rebind_event_if_ajax_render_partial + return if request.format.symbol != :js + return if response.content_type != 'text/javascript' + + path = Rails.root.join('app/views/shared/after_render_js_hook.js.erb') + return unless File.exists?(path) + + append_js = ERB.new(File.open(path).read).result + response.body += append_js + end + +end \ No newline at end of file diff --git a/app/controllers/cooperative/carousels_controller.rb b/app/controllers/cooperative/carousels_controller.rb new file mode 100644 index 000000000..c958f9859 --- /dev/null +++ b/app/controllers/cooperative/carousels_controller.rb @@ -0,0 +1,80 @@ +class Cooperative::CarouselsController < Cooperative::BaseController + before_action :convert_file!, only: [:create] + + def index + @images = current_laboratory.portal_images.order(position: :asc) + end + + def create + position = current_laboratory.portal_images.count + 1 + + ActiveRecord::Base.transaction do + image = current_laboratory.portal_images.create!(create_params.merge(position: position)) + + file_path = Util::FileManage.disk_filename('PortalImage', image.id) + File.delete(file_path) if File.exist?(file_path) # 删除之前的文件 + Util.write_file(@file, file_path) + end + + flash[:success] = '保存成功' + redirect_to cooperative_carousels_path(current_laboratory) + end + + def update + current_image.update!(update_params) + render_ok + end + + def destroy + ActiveRecord::Base.transaction do + current_image.destroy! + # 前移 + current_laboratory.portal_images.where('position > ?', current_image.position) + .update_all('position = position - 1') + + file_path = Util::FileManage.disk_filename('PortalImage', current_image.id) + File.delete(file_path) if File.exist?(file_path) + end + render_delete_success + end + + def drag + move = current_laboratory.portal_images.find_by(id: params[:move_id]) + after = current_laboratory.portal_images.find_by(id: params[:after_id]) + + Admins::DragPortalImageService.call(current_laboratory, move, after) + render_ok + rescue Admins::DragPortalImageService::Error => e + render_error(e.message) + end + + private + + def current_image + @_current_image ||= current_laboratory.portal_images.find(params[:id]) + end + + def create_params + params.require(:portal_image).permit(:name, :link) + end + + def update_params + params.permit(:name, :link, :status) + end + + def convert_file! + max_size = 10 * 1024 * 1024 # 10M + file = params.dig('portal_image', 'image') + if file.class == ActionDispatch::Http::UploadedFile + @file = file + render_error('请上传文件') if @file.size.zero? + render_error('文件大小超过限制') if @file.size > max_size + else + file = file.to_s.strip + return render_error('请上传正确的图片') if file.blank? + @file = Util.convert_base64_image(file, max_size: max_size) + end + rescue Base64ImageConverter::Error => ex + render_error(ex.message) + end +end \ No newline at end of file diff --git a/app/controllers/cooperative/dashboards_controller.rb b/app/controllers/cooperative/dashboards_controller.rb new file mode 100644 index 000000000..4a9a11330 --- /dev/null +++ b/app/controllers/cooperative/dashboards_controller.rb @@ -0,0 +1,4 @@ +class Cooperative::DashboardsController < Cooperative::BaseController + def show + end +end \ No newline at end of file diff --git a/app/controllers/cooperative/files_controller.rb b/app/controllers/cooperative/files_controller.rb new file mode 100644 index 000000000..56710a968 --- /dev/null +++ b/app/controllers/cooperative/files_controller.rb @@ -0,0 +1,46 @@ +class Cooperative::FilesController < Cooperative::BaseController + before_action :convert_file!, only: [:create] + + def create + File.delete(file_path) if File.exist?(file_path) # 删除之前的文件 + + Util.write_file(@file, file_path) + + render_ok(source_id: params[:source_id], source_type: params[:source_type].to_s, url: file_url + "?t=#{Random.rand}") + rescue StandardError => ex + logger_error(ex) + render_error('上传失败') + end + + private + + def convert_file! + max_size = 10 * 1024 * 1024 # 10M + if params[:file].class == ActionDispatch::Http::UploadedFile + @file = params[:file] + render_error('请上传文件') if @file.size.zero? + render_error('文件大小超过限制') if @file.size > max_size + else + file = params[:file].to_s.strip + return render_error('请上传正确的图片') if file.blank? + @file = Util.convert_base64_image(file, max_size: max_size) + end + rescue Base64ImageConverter::Error => ex + render_error(ex.message) + end + + def file_path + @_file_path ||= begin + case params[:source_type].to_s + when 'Shixun' then + Util::FileManage.disk_filename('Shixun', params[:source_id]) + else + Util::FileManage.disk_filename(params[:source_type].to_s, params[:source_id].to_s) + end + end + end + + def file_url + Util::FileManage.disk_file_url(params[:source_type].to_s, params[:source_id].to_s) + end +end \ No newline at end of file diff --git a/app/controllers/cooperative/laboratory_settings_controller.rb b/app/controllers/cooperative/laboratory_settings_controller.rb new file mode 100644 index 000000000..d9ba70d9d --- /dev/null +++ b/app/controllers/cooperative/laboratory_settings_controller.rb @@ -0,0 +1,14 @@ +class Cooperative::LaboratorySettingsController < Cooperative::BaseController + def edit + @laboratory = current_laboratory + end + + def update + Admins::SaveLaboratorySettingService.call(current_laboratory, form_params) + render_ok + end + + def form_params + params.permit(:identifier, :name, :nav_logo, :login_logo, :tab_logo, :footer, navbar: %i[name link hidden]) + end +end \ No newline at end of file diff --git a/app/controllers/cooperative/laboratory_users_controller.rb b/app/controllers/cooperative/laboratory_users_controller.rb new file mode 100644 index 000000000..390d1f2b1 --- /dev/null +++ b/app/controllers/cooperative/laboratory_users_controller.rb @@ -0,0 +1,23 @@ +class Cooperative::LaboratoryUsersController < Cooperative::BaseController + def index + laboratory_users = current_laboratory.laboratory_users + @laboratory_users = paginate laboratory_users.includes(user: { user_extension: [:school, :department] }) + end + + def create + Admins::AddLaboratoryUserService.call(current_laboratory, params.permit(user_ids: [])) + render_ok + end + + def destroy + current_laboratory_user.destroy! + + render_delete_success + end + + private + + def current_laboratory_user + @_current_laboratory_user ||= current_laboratory.laboratory_users.find(params[:id]) + end +end \ No newline at end of file diff --git a/app/controllers/cooperative/users_controller.rb b/app/controllers/cooperative/users_controller.rb new file mode 100644 index 000000000..8d6e5f52a --- /dev/null +++ b/app/controllers/cooperative/users_controller.rb @@ -0,0 +1,10 @@ +class Cooperative::UsersController < Cooperative::BaseController + def index + params[:sort_by] = params[:sort_by].presence || 'created_on' + params[:sort_direction] = params[:sort_direction].presence || 'desc' + params[:school_id] = current_laboratory.school_id + + users = Admins::UserQuery.call(params) + @users = paginate users.includes(user_extension: :school) + end +end \ No newline at end of file diff --git a/app/controllers/course_groups_controller.rb b/app/controllers/course_groups_controller.rb index 0e16d1bac..42a070734 100644 --- a/app/controllers/course_groups_controller.rb +++ b/app/controllers/course_groups_controller.rb @@ -2,15 +2,15 @@ class CourseGroupsController < ApplicationController before_action :require_login, :check_auth before_action :set_group, except: [:create] before_action :find_course, only: [:create] - before_action :teacher_or_admin_allowed + before_action :teacher_allowed def create tip_exception("分班名称不能为空") if params[:name].blank? if @course.course_groups.where(name: params[:name]).count > 0 normal_status(-1, "已存在同名分班") else - @course.course_groups.create!(name: params[:name], position: @course.course_groups.count + 1) - normal_status(0, "创建成功") + course_group = @course.course_groups.create!(name: params[:name], position: @course.course_groups.count + 1) + render :json => {group_id: course_group.id, status: 0, message: "创建成功"} end end diff --git a/app/controllers/course_modules_controller.rb b/app/controllers/course_modules_controller.rb index 6e8afd525..0bef519fd 100644 --- a/app/controllers/course_modules_controller.rb +++ b/app/controllers/course_modules_controller.rb @@ -2,7 +2,8 @@ class CourseModulesController < ApplicationController before_action :require_login, :check_auth before_action :set_module, except: [:unhidden_modules] before_action :find_course, only: [:unhidden_modules] - before_action :teacher_or_admin_allowed + before_action :teacher_or_admin_allowed, except: [:add_second_category] + before_action :teacher_allowed, only: [:add_second_category] # 模块置顶 def sticky_module @@ -48,9 +49,9 @@ class CourseModulesController < ApplicationController tip_exception("已存在同名子目录") if @course_module.course_second_categories.exists?(name: params[:name].strip) ActiveRecord::Base.transaction do begin - @course_module.course_second_categories.create!(name: params[:name].strip, category_type: @course_module.module_type, + category = @course_module.course_second_categories.create!(name: params[:name].strip, category_type: @course_module.module_type, course_id: @course.id, position: @course_module.course_second_categories.count + 1) - normal_status(0, "添加成功") + render :json => {category_id: category.id, status: 0, message: "添加成功"} rescue Exception => e uid_logger_error(e.message) tip_exception("添加子目录失败") diff --git a/app/controllers/course_second_categories_controller.rb b/app/controllers/course_second_categories_controller.rb index e5c3366cd..2de1637f2 100644 --- a/app/controllers/course_second_categories_controller.rb +++ b/app/controllers/course_second_categories_controller.rb @@ -1,7 +1,7 @@ class CourseSecondCategoriesController < ApplicationController before_action :require_login, :check_auth before_action :set_category - before_action :teacher_or_admin_allowed + before_action :teacher_allowed # 目录重命名 def rename_category diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb index 0c643d15c..a1bd16e94 100644 --- a/app/controllers/courses_controller.rb +++ b/app/controllers/courses_controller.rb @@ -35,7 +35,7 @@ class CoursesController < ApplicationController :transfer_to_course_group, :delete_from_course, :export_member_scores_excel, :search_users, :add_students_by_search, :get_historical_courses, :add_teacher_popup, :add_teacher, :export_couser_info, :export_member_act_score, - :update_informs, :new_informs, :delete_informs] + :update_informs, :new_informs, :delete_informs, :switch_to_student] before_action :admin_allowed, only: [:set_invite_code_halt, :set_public_or_private, :change_course_admin, :set_course_group, :create_group_by_importing_file, :update_task_position, :tasks_list] @@ -583,6 +583,8 @@ class CoursesController < ApplicationController # 学生身份的处理 student_member = course_members.where(role: %i[STUDENT]).take + + # 不存在则创建学生身份 if params[:roles].include?("STUDENT") && student_member.blank? correspond_teacher_exist = CourseMember.exists?(user_id: params[:user_id], is_active: 1, course_id: @course.id, role: %i[CREATOR PROFESSOR ASSISTANT_PROFESSOR]) new_student = CourseMember.new(user_id: params[:user_id], course_id: @course.id, role: 4) @@ -597,6 +599,9 @@ class CoursesController < ApplicationController student_member.destroy! CourseDeleteStudentDeleteWorksJob.perform_later(@course.id, [params[:user_id]]) # CourseDeleteStudentNotifyJob.perform_later(@course.id, [params[:user_id]], current_user.id) + elsif params[:roles].include?("STUDENT") && student_member.present? && !params[:roles].include?("PROFESSOR") && !params[:roles].include?("ASSISTANT_PROFESSOR") + # 学生身份存在且学生没有教师身份时更新is_active + student_member.update_attributes!(is_active: 1) end normal_status(0, "修改成功") @@ -681,13 +686,19 @@ class CoursesController < ApplicationController course_member = @course.course_members.find_by!(user_id: current_user.id, is_active: 1) tip_exception("切换失败") if course_member.STUDENT? - course_student = CourseMember.find_by!(user_id: current_user.id, role: %i[STUDENT], course_id: @course.id) - course_member.update_attributes(is_active: 0) - course_student.update_attributes(is_active: 1) + course_student = CourseMember.find_by(user_id: current_user.id, role: %i[STUDENT], course_id: @course.id) + course_member.update_attributes!(is_active: 0) + if course_student + course_student.update_attributes!(is_active: 1) + else + # 学生身份不存在则创建 + CourseMember.create!(user_id: current_user.id, role: 4, course_id: @course.id) + CourseAddStudentCreateWorksJob.perform_later(@course.id, [current_user.id]) + end normal_status(0, "切换成功") rescue => e uid_logger_error("switch_to_student error: #{e.message}") - tip_exception("切换失败") + tip_exception(e.message) raise ActiveRecord::Rollback end end @@ -1127,7 +1138,7 @@ class CoursesController < ApplicationController def top_banner @user = current_user - @is_teacher = @user_course_identity < Course::STUDENT + @switch_student = Course::BUSINESS < @user_course_identity && @user_course_identity < Course::STUDENT @is_student = @user_course_identity == Course::STUDENT @course.increment!(:visits) end diff --git a/app/controllers/exercise_questions_controller.rb b/app/controllers/exercise_questions_controller.rb index 9eeba6adc..aacef6bc7 100644 --- a/app/controllers/exercise_questions_controller.rb +++ b/app/controllers/exercise_questions_controller.rb @@ -619,7 +619,7 @@ class ExerciseQuestionsController < ApplicationController :status => 0 } ExerciseShixunAnswer.create(ex_shixun_option) - new_obj_score = @c_score + new_obj_score = ex_obj_score + @c_score end total_scores = new_obj_score + ex_subj_score if total_scores < 0.0 diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 191825cc3..d71b72618 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -518,27 +518,27 @@ class ExercisesController < ApplicationController common_group = exercise_groups_ids & course_id #传入的班级与问卷已存在的班级的交集,即表示已有分班的 new_group_ids = course_id - common_group #新传入的班级id if common_group.count > 0 #判断试卷的分班设置是否存在,存在则更新,负责则新建 - ex_group_params = { + exercise_group_sets = exercise_groups.find_in_exercise_group("course_group_id",common_group) + exercise_group_sets.each do |the_group_setting| + ex_group_params = { :publish_time => exercise_publish_time, :end_time => exercise_end_time - } - exercise_group_sets = exercise_groups.find_in_exercise_group("course_group_id",common_group) - the_group_setting = exercise_group_sets.first - if the_group_setting.present? + } + the_group_setting_status = set_exercise_status(the_group_setting.publish_time,the_group_setting.end_time) if the_group_setting_status == 2 ex_group_params = { - :publish_time => the_group_setting.publish_time, - :end_time => exercise_end_time + :publish_time => the_group_setting.publish_time, + :end_time => exercise_end_time } elsif the_group_setting_status == 3 ex_group_params = { - :publish_time => the_group_setting.publish_time, - :end_time => the_group_setting.end_time + :publish_time => the_group_setting.publish_time, + :end_time => the_group_setting.end_time } end + the_group_setting.update_attributes!(ex_group_params) end - exercise_group_sets.update_all(ex_group_params) end if new_group_ids.size > 0 new_group_ids.each do |c| @@ -560,8 +560,9 @@ class ExercisesController < ApplicationController error_count == 0 normal_status(-1,"已发布/已截止的试卷不允许修改时间") else + # 未发布的分班设置才能删除 if old_exercise_groups.size > 0 - old_all_ex_groups = exercise_groups.find_in_exercise_group("course_group_id",old_exercise_groups) + old_all_ex_groups = exercise_groups.find_in_exercise_group("course_group_id",old_exercise_groups).exercise_group_not_published old_all_ex_groups.destroy_all end #试卷更新为exercise_group_setting的发布时间最小,截止时间最大 @@ -720,8 +721,8 @@ class ExercisesController < ApplicationController if exercise.unified_setting ex_status = exercise.exercise_status #则为试卷的状态 else - ex_status = exercise.exercise_group_settings.find_in_exercise_group("course_group_id",params[:group_ids]) - .exercise_group_not_published.present? ? 1 : 0 + ex_status = @course.course_groups.where(id: params[:group_ids]).size != + exercise.exercise_group_settings.where(course_group_id: params[:group_ids]).exercise_group_published.size ? 1 : 0 end if ex_status == 1 #如果试卷存在已发布的,或者是已截止的,那么则直接跳过 g_course = group_ids #表示是否传入分班参数,如果传入分班的参数,那么试卷的统一设置需修改 @@ -739,8 +740,8 @@ class ExercisesController < ApplicationController g_course.each_with_index do |i, index| exercise_group_setting = exercise.exercise_group_settings.find_in_exercise_group("course_group_id",i).first #根据课堂分班的id,寻找试卷所在的班级 group_end_time = params[:detail] ? group_end_times[index] : ex_end_time - if exercise_group_setting #如果该试卷分组存在,则更新,否则新建 - exercise_group_setting.update_attributes(publish_time: Time.now, end_time: group_end_time) + if exercise_group_setting.present? #如果该试卷分组存在,则更新,否则新建 + exercise_group_setting.update_attributes!(publish_time: Time.now, end_time: group_end_time) else p_course_group = { :exercise_id => exercise.id, @@ -1152,7 +1153,7 @@ class ExercisesController < ApplicationController # 1 老师权限,0 学生权限 @is_teacher_or = (@user_course_identity < Course::STUDENT) ? 1 : 0 @student_status = 2 - @exercise_questions = @exercise.exercise_questions.includes(:exercise_shixun_challenges,:exercise_standard_answers,:exercise_answers,:exercise_shixun_answers).order("question_number ASC") + @exercise_questions = @exercise.exercise_questions.includes(:exercise_shixun_challenges,:exercise_standard_answers,:exercise_answers,:exercise_shixun_answers,:exercise_answer_comments).order("question_number ASC") @question_status = [] get_exercise_status = @exercise.get_exercise_status(current_user) #当前用户的试卷状态 @ex_answer_status = @exercise.get_exercise_status(@ex_user&.user) #当前试卷用户的试卷状态 @@ -1323,7 +1324,7 @@ class ExercisesController < ApplicationController end rescue Exception => e uid_logger_error(e.message) - tip_exception("页面调用失败!") + tip_exception(e.message) raise ActiveRecord::Rollback end end @@ -1425,18 +1426,24 @@ class ExercisesController < ApplicationController @exercise_questions = @exercise.exercise_questions&.includes(:exercise_choices,:exercise_answers,:exercise_standard_answers,:exercise_shixun_challenges,:exercise_shixun_answers) - @paging_type = "percent" - # 按题型排序 + percent_sort = "desc" + if params[:sort].present? - @paging_type = params[:sort].to_s + percent_sort = params[:sort] end + # @paging_type = "percent" + # # 按题型排序 + # if params[:sort].present? + # @paging_type = params[:sort].to_s + # end ques_result_all = exercise_commit_result(@exercise_questions,@exercise_commit_user_ids) - if @paging_type == "percent" - @question_result_hash = ques_result_all.sort_by{|s| s[:percent]} + #默认降序排列 + if percent_sort == "desc" + @question_result_hash = ques_result_all.sort_by{|s| s[:percent]}.reverse else - @question_result_hash = ques_result_all.sort_by{|s| s[:"#{@paging_type}"]} + @question_result_hash = ques_result_all.sort_by{|s| s[:percent]} end @exercise_questions_count = @exercise_questions.size @@ -1709,9 +1716,9 @@ class ExercisesController < ApplicationController ques_number = q.question_number end if q.question_type != Exercise::PRACTICAL - ques_vote = q.exercise_answers.search_exercise_answer("user_id",user_id) + ques_vote = q.exercise_answers.select{|answer| answer.user_id == user_id} else - ques_vote = q.exercise_shixun_answers.search_shixun_answers("user_id",user_id) + ques_vote = q.exercise_shixun_answers.select{|answer| answer.user_id == user_id} end ques_status = 0 if ques_vote.present? diff --git a/app/controllers/files_controller.rb b/app/controllers/files_controller.rb index 791d145c2..117475894 100644 --- a/app/controllers/files_controller.rb +++ b/app/controllers/files_controller.rb @@ -9,19 +9,20 @@ class FilesController < ApplicationController before_action :set_pagination, only: %i[index public_with_course_and_project mine_with_course_and_project] before_action :validate_upload_params, only: %i[upload import] before_action :find_file, only: %i[show setting update] + before_action :publish_params, only: %i[upload import update] SORT_TYPE = %w[created_on downloads quotes] def index sort = params[:sort] || 0 # 0: 降序;1: 升序 sort_type = params[:sort_type] || 'created_on' # created_on:时间排序, downloads:下载次数排序; quotes: 引用次数排序 - course_second_category_id = params[:course_second_category_id] || 0 # 0: 为主目录, 其他为次目录id + @course_second_category_id = params[:course_second_category_id] || 0 # 0: 为主目录, 其他为次目录id @user = current_user - @attachments = course_second_category_id.to_i == 0 ? @course.attachments : @course.attachments.by_course_second_category_id(course_second_category_id) - @attachments = @attachments.includes(attachment_group_settings: :course_group, author: [:user_extension, :course_members]) + @attachments = @course_second_category_id.to_i == 0 ? @course.attachments.includes(:course_second_category) : @course.attachments.by_course_second_category_id(@course_second_category_id) + @attachments = @attachments.includes(author: [:user_extension, :course_members]) .ordered(sort: sort.to_i, sort_type: sort_type.strip) - get_category(@course, course_second_category_id) + get_category(@course, @course_second_category_id) @total_count = @attachments.size @publish_count = @attachments.published.size @unpublish_count = @total_count - @publish_count @@ -137,9 +138,9 @@ class FilesController < ApplicationController def upload attachment_ids = params[:attachment_ids] course_second_category_id = params[:course_second_category_id] || 0 # 0: 为主目录, 其他为次目录id - is_unified_setting = params.has_key?(:is_unified_setting) ? params[:is_unified_setting] : true - publish_time = params[:publish_time] - course_group_publish_times = params[:course_group_publish_times] || [] + # is_unified_setting = params.has_key?(:is_unified_setting) ? params[:is_unified_setting] : true + # publish_time = params[:publish_time] + # course_group_publish_times = params[:course_group_publish_times] || [] begin attachment_ids.each do |attchment_id| @@ -148,9 +149,12 @@ class FilesController < ApplicationController attachment.container = @course attachment.course_second_category_id = course_second_category_id attachment.description = params[:description] - attachment.is_public = params[:is_public] ? 1 : 0 - attachment.set_publish_time(publish_time) if is_unified_setting - attachment.set_course_group_publish_time(@course, course_group_publish_times) if @course.course_groups.size > 0 && !is_unified_setting && publish_time.blank? + attachment.is_public = params[:is_public] && @course.is_public == 1 ? 1 : 0 + attachment.is_publish = @atta_is_publish + attachment.delay_publish = @atta_delay_publish + attachment.publish_time = @atta_publish_time + # attachment.set_publish_time(publish_time) if is_unified_setting + # attachment.set_course_group_publish_time(@course, course_group_publish_times) if @course.course_groups.size > 0 && !is_unified_setting && publish_time.blank? attachment.save! end end @@ -188,8 +192,9 @@ class FilesController < ApplicationController attach_copied_obj.created_on = Time.now attach_copied_obj.author = current_user attach_copied_obj.is_public = 0 - attach_copied_obj.is_publish = 1 - attach_copied_obj.publish_time = Time.now + attach_copied_obj.is_publish = @atta_is_publish + attach_copied_obj.delay_publish = @atta_delay_publish + attach_copied_obj.publish_time = @atta_publish_time attach_copied_obj.course_second_category_id = course_second_category_id attach_copied_obj.copy_from = ori.copy_from.nil? ? ori.id : ori.copy_from if attach_copied_obj.attachtype == nil @@ -209,11 +214,7 @@ class FilesController < ApplicationController def update return normal_status(403, "您没有权限进行该操作") if current_user.course_identity(@course) >= 5 && @file.author != current_user - is_unified_setting = params[:is_unified_setting] - publish_time = params[:publish_time] - publish_time = format_time(Time.parse(publish_time)) unless publish_time.blank? is_public = params[:is_public] - course_group_publish_times = params[:course_group_publish_times] || [] @old_attachment = @file @new_attachment = Attachment.find_by_id params[:new_attachment_id] @@ -225,25 +226,29 @@ class FilesController < ApplicationController old_course_second_category_id = @old_attachment.course_second_category_id @old_attachment.copy_attributes_from_new_attachment(@new_attachment) - @old_attachment.is_public = is_public == true ? 1 : 0 if is_public @old_attachment.course_second_category_id = old_course_second_category_id @old_attachment.save! @new_attachment.delete end + @old_attachment.is_public = is_public == true && @course.is_public == 1 ? 1 : 0 + @old_attachment.is_publish = @atta_is_publish + @old_attachment.delay_publish = @atta_delay_publish + @old_attachment.publish_time = @atta_publish_time + if params[:description] && !params[:description].strip.blank? && params[:description] != @old_attachment.description @old_attachment.description = params[:description] end - @old_attachment.set_public(is_public) + # @old_attachment.set_public(is_public) - if is_unified_setting - @old_attachment.set_publish_time(publish_time) - @old_attachment.attachment_group_settings.destroy_all - end + # if is_unified_setting + # @old_attachment.set_publish_time(publish_time) + # @old_attachment.attachment_group_settings.destroy_all + # end - if publish_time.blank? && @course.course_groups.size > 0 && !is_unified_setting - @old_attachment.set_course_group_publish_time(@course, course_group_publish_times) - end + # if publish_time.blank? && @course.course_groups.size > 0 && !is_unified_setting + # @old_attachment.set_course_group_publish_time(@course, course_group_publish_times) + # end @old_attachment.save! rescue Exception => e @@ -304,11 +309,19 @@ class FilesController < ApplicationController end def file_validate_sort_type - normal_status(-2, "参数sort_tyope暂时只支持 'created_on', 'quotes', 'downloads'") if params.has_key?(:sort_type) && !SORT_TYPE.include?(params[:sort_type].strip) + normal_status(-2, "参数sort_type暂时只支持 'created_on', 'quotes', 'downloads'") if params.has_key?(:sort_type) && !SORT_TYPE.include?(params[:sort_type].strip) end def validate_upload_params find_attachment_ids find_course_second_category_id end + + def publish_params + tip_exception("缺少发布参数") if params[:delay_publish].blank? + tip_exception("缺少延期发布的时间参数") if params[:delay_publish].to_i == 1 && params[:publish_time].blank? + @atta_is_publish = params[:delay_publish].to_i == 1 && params[:publish_time].to_time > Time.now ? 0 : 1 + @atta_delay_publish = params[:delay_publish].to_i + @atta_publish_time = params[:delay_publish].to_i == 1 && params[:publish_time] ? params[:publish_time] : Time.now + end end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 2ed82f1b5..48c942e65 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -2,10 +2,12 @@ class HomeController < ApplicationController def index # banner图 - images = PortalImage.where(status: true).order("position asc") + images = current_laboratory.portal_images.only_online.order(position: :asc) + images = default_laboratory.portal_images.only_online.order(position: :asc) if images.blank? # 未设置时使用EduCoder的轮播图 + @images_url = [] images.each do |image| - @images_url << {path: image.link, image_url: Util::FileManage.disk_file_url('PortalImage', image.id)} + @images_url << {path: image.link, image_url: Util::FileManage.source_disk_file_url(image)} end # 目录分级 diff --git a/app/controllers/homework_commons_controller.rb b/app/controllers/homework_commons_controller.rb index 83b80bec4..60cf2d6c5 100644 --- a/app/controllers/homework_commons_controller.rb +++ b/app/controllers/homework_commons_controller.rb @@ -160,7 +160,8 @@ class HomeworkCommonsController < ApplicationController # 作品状态 0: 未提交, 1 按时提交, 2 延迟提交 if params[:work_status].present? - work_status = params[:work_status].map{|status| status.to_i} + params_work_status = request.get? ? params[:work_status].split(",") : params[:work_status] + work_status = params_work_status.map{|status| status.to_i} all_student_works = @student_works.left_joins(:myshixun) @student_works = all_student_works.where(work_status: work_status) @@ -170,7 +171,8 @@ class HomeworkCommonsController < ApplicationController # 分班情况 unless params[:course_group].blank? - group_user_ids = @course.students.where(course_group_id: params[:course_group]).pluck(:user_id) + group_ids = request.get? ? params[:course_group].split(",") : params[:course_group] + group_user_ids = @course.students.where(course_group_id: group_ids).pluck(:user_id) # 有分组只可能是老师身份查看列表 @student_works = @student_works.where(user_id: group_user_ids) end @@ -482,7 +484,7 @@ class HomeworkCommonsController < ApplicationController publish_time = setting[:publish_time] == "" ? Time.now : setting[:publish_time] # 截止时间为空时取发布时间后一个月 - end_time = setting[:end_time] == "" ? Time.at(publish_time.to_time.to_i+30*24*3600) : setting[:end_time] + end_time = setting[:end_time] HomeworkGroupSetting.where(homework_common_id: @homework.id, course_group_id: setting[:group_id]). update_all(publish_time: publish_time, end_time: end_time) setting_group_ids << setting[:group_id] @@ -1170,7 +1172,7 @@ class HomeworkCommonsController < ApplicationController # 可立即截止的分班:统一设置则是用户管理的所有分班,否则是当前用户管理的分班中已发布且未截止的 charge_group_ids = @course.charge_group_ids(@current_user) # 当前用户管理的分班 group_ids = @homework.unified_setting ? charge_group_ids : - @homework.homework_group_settings.where(course_group_id: charge_group_ids).none_end.pluck(:course_group_id) + @homework.homework_group_settings.where(course_group_id: charge_group_ids).published_no_end.pluck(:course_group_id) @course_groups = @course.course_groups.where(id: group_ids) else tip_exception("没有可截止的分班") diff --git a/app/controllers/oauth/base_controller.rb b/app/controllers/oauth/base_controller.rb new file mode 100644 index 000000000..e2eb26a2a --- /dev/null +++ b/app/controllers/oauth/base_controller.rb @@ -0,0 +1,20 @@ +class Oauth::BaseController < ActionController::Base + include RenderHelper + include LoginHelper + + skip_before_action :verify_authenticity_token + + private + + def session_user_id + session[:user_id] + end + + def current_user + @_current_user ||= User.find_by(id: session_user_id) + end + + def auth_hash + request.env['omniauth.auth'] + end +end \ No newline at end of file diff --git a/app/controllers/oauth/qq_controller.rb b/app/controllers/oauth/qq_controller.rb new file mode 100644 index 000000000..4b9a46443 --- /dev/null +++ b/app/controllers/oauth/qq_controller.rb @@ -0,0 +1,9 @@ +class Oauth::QQController < Oauth::BaseController + def create + user, new_user = Oauth::CreateOrFindQqAccountService.call(current_user, auth_hash) + + successful_authentication(user) + + render_ok(new_user: new_user) + end +end \ No newline at end of file diff --git a/app/controllers/oauth/wechat_controller.rb b/app/controllers/oauth/wechat_controller.rb new file mode 100644 index 000000000..6c0c53eb6 --- /dev/null +++ b/app/controllers/oauth/wechat_controller.rb @@ -0,0 +1,11 @@ +class Oauth::WechatController < Oauth::BaseController + def create + user, new_user = Oauth::CreateOrFindWechatAccountService.call(current_user ,params) + + successful_authentication(user) + + render_ok(new_user: new_user) + rescue Oauth::CreateOrFindWechatAccountService::Error => ex + render_error(ex.message) + end +end \ No newline at end of file diff --git a/app/controllers/polls_controller.rb b/app/controllers/polls_controller.rb index da5917e1b..cb507fdcb 100644 --- a/app/controllers/polls_controller.rb +++ b/app/controllers/polls_controller.rb @@ -279,7 +279,8 @@ class PollsController < ApplicationController if poll.unified_setting pl_status = poll.polls_status #则为试卷的状态 else - pl_status = poll.poll_group_settings.find_in_poll_group("course_group_id",params[:group_ids]).poll_group_not_published.present? ? 1 : 0 #立即发布针对分组设置的全部未发布的班级才生效 + pl_status = @course.course_groups.where(id: group_ids).size != + poll.poll_group_settings.where(course_group_id: group_ids).poll_group_published.size ? 1 : 0 #立即发布针对分组设置的全部未发布的班级才生效 end if pl_status == 1 #如果问卷存在已发布的,或者是已截止的,那么则直接跳过 g_course = group_ids #表示是否传入分班参数,如果传入分班的参数,那么poll的统一设置需修改 @@ -295,8 +296,8 @@ class PollsController < ApplicationController g_course.each_with_index do |i, index| poll_group_setting = poll.poll_group_settings.find_in_poll_group("course_group_id",i).first #根据课堂分班的id,寻找问卷所在的班级 group_end_time = params[:detail] ? group_end_times[index] : ex_end_time - if poll_group_setting #如果该问卷分组存在,则更新,否则新建 - poll_group_setting.update_attributes(publish_time: Time.now, end_time: group_end_time) + if poll_group_setting.present? #如果该问卷分组存在,则更新,否则新建 + poll_group_setting.update_attributes!(publish_time: Time.now, end_time: group_end_time) else p_course_group = { :poll_id => poll.id, @@ -306,7 +307,7 @@ class PollsController < ApplicationController :end_time => group_end_time, } new_poll_group = poll.poll_group_settings.new p_course_group - new_poll_group.save + new_poll_group.save! end end e_time = poll.poll_group_settings.end_time_present.map(&:end_time).max @@ -782,27 +783,27 @@ class PollsController < ApplicationController common_group = poll_groups_ids & course_id #传入的班级与问卷已存在的班级的交集,即表示已有分班的 new_group_ids = course_id - common_group #新传入的班级id if common_group.size > 0 #传入的参数存在已发布的分班 - poll_group_params = { + poll_group = poll_groups.find_in_poll_group("course_group_id",common_group) + poll_group.each do |the_group_setting| + poll_group_params = { :publish_time => poll_publish_time, :end_time => poll_end_time - } - poll_group = poll_groups.find_in_poll_group("course_group_id",common_group) - the_group_setting = poll_group.first - if the_group_setting.present? + } + the_group_setting_status = set_poll_status(the_group_setting.publish_time,the_group_setting.end_time) if the_group_setting_status == 2 poll_group_params = { - :publish_time => the_group_setting.publish_time, - :end_time => poll_end_time + :publish_time => the_group_setting.publish_time, + :end_time => poll_end_time } elsif the_group_setting_status == 3 poll_group_params = { - :publish_time => the_group_setting.publish_time, - :end_time => the_group_setting.end_time + :publish_time => the_group_setting.publish_time, + :end_time => the_group_setting.end_time } end + the_group_setting.update_attributes(poll_group_params) end - poll_group.update_all(poll_group_params) end if new_group_ids.size > 0 new_group_ids.each do |c| @@ -824,8 +825,9 @@ class PollsController < ApplicationController error_count == 0 normal_status(-1,"已发布/已截止的问卷不允许修改时间") else + # 未发布的分班设置才能删除 if old_poll_groups.size > 0 - old_all_poll_groups = poll_groups.find_in_poll_group("course_group_id",old_poll_groups) + old_all_poll_groups = poll_groups.find_in_poll_group("course_group_id",old_poll_groups).poll_group_not_published old_all_poll_groups.destroy_all end #问卷更新为poll_group_setting的发布时间最小,截止时间最大 diff --git a/app/controllers/question_banks_controller.rb b/app/controllers/question_banks_controller.rb index f6b321e1b..e26982dfd 100644 --- a/app/controllers/question_banks_controller.rb +++ b/app/controllers/question_banks_controller.rb @@ -91,12 +91,16 @@ class QuestionBanksController < ApplicationController banks = @object_type.classify.constantize.where(id: params[:object_id]) course = current_user.manage_courses.find_by!(id: params[:course_id]) task_ids = [] + homework_type = "" + container_type = "" banks.each do |bank| case @object_type when 'HomeworkBank' # 作业 task = quote_homework_bank bank, course + homework_type = task.homework_type when 'ExerciseBank' - if bank.container_type == 'Exercise' # 试卷 + container_type = bank.container_type + if container_type == 'Exercise' # 试卷 task = quote_exercise_bank bank, course else # 问卷 task = quote_poll_bank bank, course @@ -108,7 +112,23 @@ class QuestionBanksController < ApplicationController end task_ids << task.id if task end - render :json => {task_ids: task_ids, status: 0, message: "发送成功"} + + case @object_type + when 'HomeworkBank' # 作业 + category_id = course.course_modules.find_by(module_type: homework_type == "normal" ? "common_homework" : "group_homework")&.id + when 'ExerciseBank' + if container_type == 'Exercise' # 试卷 + category_id = course.course_modules.find_by(module_type: "exercise")&.id + else # 问卷 + category_id = course.course_modules.find_by(module_type: "poll")&.id + end + when 'GtaskBank' + category_id = course.course_modules.find_by(module_type: "graduation")&.id + when 'GtopicBank' + category_id = course.course_modules.find_by(module_type: "graduation")&.id + end + + render :json => {task_ids: task_ids, category_id: category_id, status: 0, message: "发送成功"} end def destroy diff --git a/app/controllers/users/open_users_controller.rb b/app/controllers/users/open_users_controller.rb new file mode 100644 index 000000000..49bf27a72 --- /dev/null +++ b/app/controllers/users/open_users_controller.rb @@ -0,0 +1,13 @@ +class Users::OpenUsersController < Users::BaseAccountController + def destroy + current_open_users.destroy! + + render_ok + end + + private + + def current_open_users + @_current_third_party ||= observed_user.open_users.find(params[:id]) + end +end \ No newline at end of file diff --git a/app/controllers/weapps/base_controller.rb b/app/controllers/weapps/base_controller.rb new file mode 100644 index 000000000..fadf10fb6 --- /dev/null +++ b/app/controllers/weapps/base_controller.rb @@ -0,0 +1,34 @@ +class Weapps::BaseController < ApplicationController + + private + + def require_wechat_login! + return if session_unionid.present? + + render_error('请先进行微信授权') + end + + def weapp_session_key + Wechat::Weapp.session_key(session_openid) + end + + def set_weapp_session_key(session_key) + Wechat::Weapp.write_session_key(session_openid, session_key) + end + + def session_openid + session[:openid] + end + + def set_session_openid(openid) + session[:openid] = openid + end + + def session_unionid + session[:unionid] + end + + def set_session_unionid(unionid) + session[:unionid] = unionid + end +end \ No newline at end of file diff --git a/app/controllers/weapps/code_sessions_controller.rb b/app/controllers/weapps/code_sessions_controller.rb new file mode 100644 index 000000000..f215da8e3 --- /dev/null +++ b/app/controllers/weapps/code_sessions_controller.rb @@ -0,0 +1,26 @@ +class Weapps::CodeSessionsController < Weapps::BaseController + def create + logged = false + return render_error('code不能为空') if params[:code].blank? + + result = Wechat::Weapp.jscode2session(params[:code]) + + set_session_openid(result['openid']) + set_weapp_session_key(result['session_key']) # weapp session_key写入缓存 后续解密需要 + + # 已授权,绑定过账号 + open_user = OpenUsers::Wechat.find_by(uid: result['unionid']) + if open_user.present? && open_user.user + set_session_unionid(result['unionid']) + successful_authentication(open_user.user) + logged = true + else + # 新用户 + user_info = Wechat::Weapp.decrypt(result['session_key'], params[:encrypted_data], params[:iv]) + + set_session_unionid(user_info['unionId']) + end + + render_ok(openid: result['openid'], logged: logged) + end +end \ No newline at end of file diff --git a/app/controllers/weapps/homes_controller.rb b/app/controllers/weapps/homes_controller.rb new file mode 100644 index 000000000..efb80b898 --- /dev/null +++ b/app/controllers/weapps/homes_controller.rb @@ -0,0 +1,12 @@ +class Weapps::HomesController < Weapps::BaseController + def show + # banner图 + @images = PortalImage.where(status: true).order(position: :asc) + + # 热门实训 + @shixuns = Shixun.where(homepage_show: true).includes(:tag_repertoires, :challenges).limit(4) + + # 热门实践课程 + @subjects = Subject.where(homepage_show: true).includes(:shixuns, :repertoire).limit(4) + end +end \ No newline at end of file diff --git a/app/controllers/weapps/registers_controller.rb b/app/controllers/weapps/registers_controller.rb new file mode 100644 index 000000000..0cbab7fd4 --- /dev/null +++ b/app/controllers/weapps/registers_controller.rb @@ -0,0 +1,63 @@ +class Weapps::RegistersController < Weapps::BaseController + before_action :require_wechat_login! + + def create + # 查询验证码是否正确;type只可能是1或者8 + type = phone_mail_type(params[:login].strip) + code = params[:code].strip + + if type == 1 + uid_logger("start register by phone: type is #{type}") + pre = 'p' + email = nil + phone = params[:login] + verifi_code = VerificationCode.where(phone: phone, code: code, code_type: 1).last + else + uid_logger("start register by email: type is #{type}") + pre = 'm' + email = params[:login] + phone = nil + verifi_code = VerificationCode.where(email: email, code: code, code_type: 8).last + end + uid_logger("start register: verifi_code is #{verifi_code}, code is #{code}, time is #{Time.now.to_i - verifi_code.try(:created_at).to_i}") + # check_code = (verifi_code.try(:code) == code.strip && (Time.now.to_i - verifi_code.created_at.to_i) <= 10*60) + # todo 上线前请删除万能验证码"513231" + unless code == "513231" && request.subdomain == "test-newweb" + return render_error('验证码不正确') if verifi_code.try(:code) != code.strip + return render_error('验证码已失效') if !verifi_code&.effective? + end + + login = User.generate_login(pre) + @user = User.new(admin: false, login: login, mail: email, phone: phone, type: 'User') + @user.password = params[:password] + # 现在因为是验证码,所以在注册的时候就可以激活 + @user.activate + # 必须要用save操作,密码的保存是在users中 + ActiveRecord::Base.transaction do + @user.save! + UserExtension.create!(user_id: @user.id) + # 绑定微信号 + OpenUsers::Wechat.create!(user: @user, uid: session_unionid) + + # 注册完成,手机号或邮箱想可以奖励500金币 + RewardGradeService.call( + @user, + container_id: @user.id, + container_type: pre == 'p' ? 'Phone' : 'Mail', + score: 500 + ) + end + successful_authentication(@user) + session[:user_id] = @user.id + + render_ok + end + + private + + # 1 手机类型;0 邮箱类型 + # 注意新版的login是自动名生成的 + def phone_mail_type value + value =~ /^1\d{10}$/ ? 1 : 0 + end +end \ No newline at end of file diff --git a/app/controllers/weapps/sessions_controller.rb b/app/controllers/weapps/sessions_controller.rb new file mode 100644 index 000000000..f65111399 --- /dev/null +++ b/app/controllers/weapps/sessions_controller.rb @@ -0,0 +1,24 @@ +class Weapps::SessionsController < Weapps::BaseController + before_action :require_wechat_login! + + def create + return render_error('重复登录') if current_user.present? && current_user.logged? + + user = User.try_to_login(params[:login], params[:password]) + + return render_error('错误的账号或密码') if user.blank? + return render_error('违反平台使用规范,账号已被锁定') if user.locked? + return render_error('错误的账号或密码') unless user.check_password?(params[:password].to_s) + + if user.wechat_open_user && user.wechat_open_user.uid != session_unionid + render_error('该账号已被其它微信号绑定') + return + end + + # 绑定微信号 + OpenUsers::Wechat.create!(user: user, uid: session_unionid) if user.wechat_open_user.blank? + + successful_authentication(user) + render_ok + end +end \ No newline at end of file diff --git a/app/controllers/weapps/verifies_controller.rb b/app/controllers/weapps/verifies_controller.rb new file mode 100644 index 000000000..5d8f4056e --- /dev/null +++ b/app/controllers/weapps/verifies_controller.rb @@ -0,0 +1,8 @@ +class Weapps::VerifiesController < Weapps::BaseController + before_action :require_wechat_login! + + def create + valid = Wechat::Weapp.verify?(session_openid, params[:verify_string], params[:signature]) + render_ok(valid: valid) + end +end \ No newline at end of file diff --git a/app/forms/users/update_password_form.rb b/app/forms/users/update_password_form.rb index 023caa40f..4da341839 100644 --- a/app/forms/users/update_password_form.rb +++ b/app/forms/users/update_password_form.rb @@ -4,5 +4,4 @@ class Users::UpdatePasswordForm attr_accessor :password, :old_password validates :password, presence: true - validates :old_password, presence: true end \ No newline at end of file diff --git a/app/helpers/admins/base_helper.rb b/app/helpers/admins/base_helper.rb index d79456ac8..95e54db46 100644 --- a/app/helpers/admins/base_helper.rb +++ b/app/helpers/admins/base_helper.rb @@ -1,110 +1,3 @@ module Admins::BaseHelper - def sidebar_item_group(url, text, **opts) - link_opts = url.start_with?('/') ? {} : { 'data-toggle': 'collapse', 'aria-expanded': false } - content = - link_to url, link_opts do - content_tag(:i, '', class: "fa fa-#{opts[:icon]}", 'data-toggle': 'tooltip', 'data-placement': 'right', 'data-boundary': 'window', title: text) + - content_tag(:span, text) - end - - content += - content_tag(:ul, id: url[1..-1], class: 'collapse list-unstyled', "data-parent": '#sidebar') do - yield - end - - raw content - end - - def sidebar_item(url, text, **opts) - content = - link_to url, 'data-controller': opts[:controller] do - content_tag(:i, '', class: "fa fa-#{opts[:icon]} fa-fw", 'data-toggle': 'tooltip', 'data-placement': 'right', 'data-boundary': 'window', title: text) + - content_tag(:span, text) - end - - raw content - end - - def admin_sidebar_controller - key = params[:controller].to_s.gsub(/\//, '-') - SidebarUtil.controller_name(key) || key - end - - def define_admin_breadcrumbs(&block) - content_for(:setup_admin_breadcrumb, &block) - end - - def add_admin_breadcrumb(text, url = nil) - @_admin_breadcrumbs ||= [] - @_admin_breadcrumbs << OpenStruct.new(text: text, url: url) - end - - def display_text(str, default = '--') - str.presence || default - end - - def overflow_hidden_span(text, width: 300) - opts = { class: 'd-inline-block text-truncate', style: "max-width: #{width}px" } - opts.merge!('data-toggle': 'tooltip', title: text) if text != '--' - - content_tag(:span, text, opts) - end - - def sort_tag(content = '', **opts) - options = {} - options[:sort_by] = opts.delete(:name) - is_current_sort = params[:sort_by].to_s == options[:sort_by] - options[:sort_direction] = is_current_sort && params[:sort_direction].to_s == 'desc' ? 'asc' : 'desc' - - path = opts.delete(:path) + "?" + unsafe_params.merge(options).to_query - arrow_class = case params[:sort_direction].to_s - when 'desc' then 'fa-sort-amount-desc' - when 'asc' then 'fa-sort-amount-asc' - else '' - end - opts[:style] = "#{opts[:style]} ;position: relative;" - - content_tag(:span, opts) do - link_to path, remote: true do - content = content_tag(:span) { yield } if block_given? - - content += content_tag(:i, '', class: "fa color-light-green ml-1 #{arrow_class}", style: 'position: absolute;top:0;') if is_current_sort - raw content - end - end - end - - def javascript_void_link(name, **opts) - raw link_to(name, 'javascript:void(0)', opts) - end - - def agree_link(name, url, **opts) - klass = ['action agree-action', opts.delete(:class)].compact.join(' ') - - refresh_url_data = "refresh_url=#{CGI::escape(request.fullpath)}" - url = url + (url.index('?') ? '&' : '?') + refresh_url_data - - raw link_to(name, url, { method: :post, remote: true, class: klass, 'data-confirm': '确认审核通过?'}.merge(opts)) - end - - def delete_link(name, url, **opts, &block) - klass = ['action delete-action', opts.delete(:class)].compact.join(' ') - - refresh_url_data = "refresh_url=#{CGI::escape(request.fullpath)}" - url = url + (url.index('?') ? '&' : '?') + refresh_url_data - - if block_given? - raw link_to(url, { method: :delete, remote: true, class: klass, 'data-confirm': '确认删除?'}.merge(opts), &block) - else - raw link_to(name, url, { method: :delete, remote: true, class: klass, 'data-confirm': '确认删除?'}.merge(opts)) - end - end - - def unsafe_params - params.except(:controller, :action).to_unsafe_h - end - - def list_index_no(page,index) - (page - 1) * 20 + index + 1 - end + include ManageBackHelper end \ No newline at end of file diff --git a/app/helpers/cooperative/base_helper.rb b/app/helpers/cooperative/base_helper.rb new file mode 100644 index 000000000..de95813f1 --- /dev/null +++ b/app/helpers/cooperative/base_helper.rb @@ -0,0 +1,3 @@ +module Cooperative::BaseHelper + include ManageBackHelper +end \ No newline at end of file diff --git a/app/helpers/courses_helper.rb b/app/helpers/courses_helper.rb index fb7bd1a88..9afbdd3af 100644 --- a/app/helpers/courses_helper.rb +++ b/app/helpers/courses_helper.rb @@ -62,7 +62,7 @@ module CoursesHelper course_board = course.course_board "/courses/#{course.id}/boards/#{course_board.id}" when "course_group" - "/courses/#{course.id}/students" + "/courses/#{course.id}/course_groups" end end diff --git a/app/helpers/exercises_helper.rb b/app/helpers/exercises_helper.rb index ef9261990..395d67913 100644 --- a/app/helpers/exercises_helper.rb +++ b/app/helpers/exercises_helper.rb @@ -10,9 +10,9 @@ module ExercisesHelper exercise_obj_status.each do |q| q_type = q.question_type if q_type == Exercise::PRACTICAL - answers_content = q.exercise_shixun_answers.search_shixun_answers("user_id",user_id) + answers_content = q.exercise_shixun_answers.select{|answer| answer.user_id == user_id} else - answers_content = q.exercise_answers.search_answer_users("user_id",user_id) + answers_content = q.exercise_answers.select{|answer| answer.user_id == user_id} end if q_type <= Exercise::JUDGMENT @@ -39,8 +39,13 @@ module ExercisesHelper else ques_score = 0.0 end + elsif q_type == Exercise::COMPLETION + ques_score = answers_content.select{|answer| answer.score >= 0.0}.pluck(:score).sum + if ques_score.to_s.split(".").last == "9" + ques_score = ques_score.to_f + 0.1 + end else - ques_score = answers_content.score_reviewed.select(:score).pluck(:score).sum + ques_score = answers_content.select{|answer| answer.score >= 0.0}.pluck(:score).sum end if ques_score >= q.question_score #满分作答为正确 @@ -64,7 +69,7 @@ module ExercisesHelper exercise_sub_status = exercise_questions.find_by_custom("question_type",Exercise::SUBJECTIVE) #主观题 @ex_sub_array = [] #主观题的已答/未答 exercise_sub_status.each do |s| - sub_answer = s.exercise_answers.search_answer_users("user_id",user_id) #主观题只有一个回答 + sub_answer = s.exercise_answers.select{|answer| answer.user_id == user_id} #主观题只有一个回答 if sub_answer.present? && sub_answer.first.score >= 0.0 if s.question_score <= sub_answer.first.score stand_status = 1 @@ -115,14 +120,27 @@ module ExercisesHelper if ex.question_type > Exercise::COMPLETION #当为主观题和实训题时, ex_answered_scores = effictive_users.score_reviewed.pluck(:score).sum #该问题的全部得分 percent = (ex_total_score == 0.0 ? 0.0 : (ex_answered_scores / ex_total_score.to_f).round(3) * 100) #正确率 + end + # if ex.question_type != Exercise::MULTIPLE + # ex_answered_scores = effictive_users.score_reviewed.pluck(:score).sum #该问题的全部得分 + # percent = (ex_total_score == 0.0 ? 0.0 : (ex_answered_scores / ex_total_score.to_f).round(3) * 100) #正确率 + # else + # multiple_score = 0 + # user_ids.each do |user_id| + # ex_answer_score = ex_answers.select{|answer| answer.user_id == user_id}&.first&.score.to_f + # multiple_score += ex_answer_score + # end + # percent = (ex_total_score == 0.0 ? 0.0 : (multiple_score / ex_total_score.to_f).round(3) * 100) #正确率 + # end + question_answer_infos = [] if ex.question_type <= Exercise::JUDGMENT #选择题和判断题 ex_choices = ex.exercise_choices standard_answer = ex.exercise_standard_answers.pluck(:exercise_choice_id).sort #标准答案的位置 - # right_users_count = 0 - # 该问题的正确率 + right_users_count = 0 + #该问题的正确率 if ex.question_type == Exercise::MULTIPLE #多选题 right_user_ids = user_ids standard_answer.each do |choice_position| @@ -130,10 +148,13 @@ module ExercisesHelper right_user_ids = right_user_ids & effictive_users.select{|answer| answer.exercise_choice_id == standard_answer_choice_id}.pluck(:user_id) end right_users_count = right_user_ids.size + # right_users_scores = right_users_count * ex&.question_score.to_f else #单选题和判断题 standard_answer_choice_id = ex_choices.select{|ec| ec.choice_position == standard_answer.first}.first&.id right_users_count = effictive_users.select{|answer| answer.exercise_choice_id == standard_answer_choice_id}.size + # right_users_scores = right_users_count * ex&.question_score.to_f end + # percent = (ex_total_score == 0.0 ? 0.0 : (right_users_scores / ex_total_score.to_f).round(3) * 100) #正确率 percent = commit_user_ids > 0 ? (right_users_count / commit_user_ids.to_f).round(3)*100 : 0.0 @@ -159,6 +180,7 @@ module ExercisesHelper null_stand_choice = null_standard_answer.pluck(:exercise_choice_id) #一个exercise_choice_id可能对应多个answer_text null_stand_text = null_standard_answer.pluck(:answer_text) standard_answer_count = 0 + each_null_score = null_stand_choice.size > 0 ? (ex&.question_score.to_f / null_stand_choice.uniq.size).round(3) : 0.0 all_user_count = 0 null_stand_choice.each_with_index do |s,index| user_count = 0 @@ -181,7 +203,10 @@ module ExercisesHelper all_user_count += user_count standard_answer_count += 1 end - percent = commit_user_ids > 0 ? (all_user_count / commit_user_ids.to_f).round(3)*100 : 0.0 + answer_user_score = all_user_count * each_null_score + percent = (ex_total_score == 0.0 ? 0.0 : (answer_user_score / ex_total_score.to_f).round(3) * 100) #正确率 + + # percent = commit_user_ids > 0 ? (all_user_count / commit_user_ids.to_f).round(3)*100 : 0.0 user_wrong_count = (effictive_users_count - all_user_count ) @@ -772,16 +797,24 @@ module ExercisesHelper question_comment = [] # user_score_pre = nil if ques_type == 5 - exercise_answers = q.exercise_shixun_answers.search_shixun_answers("user_id",ex_answerer_id) + exercise_answers = q.exercise_shixun_answers.select{|answer| answer.user_id == ex_answerer_id} else - exercise_answers = q.exercise_answers.search_exercise_answer("user_id",ex_answerer_id) #试卷用户的回答 + exercise_answers = q.exercise_answers.select{|answer| answer.user_id == ex_answerer_id} #试卷用户的回答 end if student_status == 2 #当前为老师,或为学生且已提交 - user_score_pre = exercise_answers.score_reviewed + user_score_pre = exercise_answers.select{|answer| answer.score >= 0.0} if ques_type == 4 #主观题时,且没有大于0的分数时,为空 user_score = user_score_pre.present? ? user_score_pre.pluck(:score).sum : nil - elsif ques_type == 5 || ques_type == 3 + elsif ques_type == 5 user_score = user_score_pre.present? ? user_score_pre.pluck(:score).sum : 0.0 + elsif ques_type == 3 #填空题时,需小心出现9.9分 + user_score = user_score_pre.present? ? user_score_pre.pluck(:score).sum : 0.0 + if user_score > 0.0 + if user_score.to_s.split(".").last == "9" + user_score = user_score.to_f + 0.1 + end + end + # user_score = user_score_pre.present? ? user_score_pre.pluck(:score).sum : 0.0 else #选择题,判断题根据第一个记录查分 user_score = user_score_pre.present? ? user_score_pre.first.score : 0.0 @@ -829,7 +862,7 @@ module ExercisesHelper if ex_type == 4 #填空题/主观题/实训题有评论的 q_answer_id = exercise_answers.present? ? exercise_answers.first.id : nil - question_comment = q.exercise_answer_comments.search_answer_comments("exercise_answer_id",q_answer_id) + question_comment = q.exercise_answer_comments.select{|comment| comment.exercise_answer_id == q_answer_id} end { "user_score": (user_score.present? ? user_score.round(1).to_s : nil), diff --git a/app/helpers/export_helper.rb b/app/helpers/export_helper.rb index 05b1b2f8b..534a78dc1 100644 --- a/app/helpers/export_helper.rb +++ b/app/helpers/export_helper.rb @@ -286,10 +286,18 @@ module ExportHelper @user_columns = [] ques_type_boolean = question_types.include?(4) if ques_type_boolean #仅存在主观题或客观题的时候 - @table_columns = @table_columns + %w(客观题成绩 主观题成绩 最终成绩 开始答题时间 提交时间) + @table_columns = @table_columns + %w(客观题成绩 主观题成绩 最终成绩) else - @table_columns = @table_columns + %w(最终成绩 开始答题时间 提交时间) + @table_columns = @table_columns + %w(最终成绩) end + for i in 1 .. exercise.exercise_questions.size + @table_columns = @table_columns + ["第#{i}题"] + end + + @table_columns = @table_columns + %w(开始答题时间 提交时间) + + questions = exercise.exercise_questions.includes(:exercise_answers,:exercise_shixun_answers).order("question_number ASC") + export_ex_users.includes(user: :user_extension).each_with_index do |e_user,index| user_info = e_user.user member = course.students.find_by_user_id(e_user.user_id) @@ -312,11 +320,36 @@ module ExportHelper user_option = [index+1,user_login,user_real_name, user_mail, user_student_id,user_course,user_commit_stu] if ques_type_boolean - other_user_option = [user_obj_score,user_suj_score,user_score,user_start_time,user_end_time] + other_user_option = [user_obj_score,user_suj_score,user_score] else - other_user_option = [user_score,user_start_time,user_end_time] + other_user_option = [user_score] end - user_option = user_option + other_user_option + + time_option = [user_start_time,user_end_time] + + score_option = [] + questions.each do |q| + q_type = q.question_type + if q_type == Exercise::PRACTICAL + answers_content = q.exercise_shixun_answers.select{|answer| answer.user_id == e_user.user_id} + else + answers_content = q.exercise_answers.select{|answer| answer.user_id == e_user.user_id} + end + + if q_type <= Exercise::JUDGMENT || q_type == Exercise::SUBJECTIVE + if answers_content.present? #学生有回答时,分数已经全部存到exercise_answer 表,所以可以直接取第一个值 + ques_score = answers_content.first.score + ques_score = ques_score.nil? || ques_score < 0 ? 0.0 : ques_score + else + ques_score = 0.0 + end + else + ques_score = answers_content.select{|answer| answer.score >= 0.0}.pluck(:score).sum + end + score_option << ques_score + end + + user_option = user_option + other_user_option + score_option + time_option @user_columns.push(user_option) end end @@ -419,7 +452,7 @@ module ExportHelper end end - out_file_name = "作品附件_#{homework_common&.course&.name}_#{homework_common.name}_#{Time.now.strftime('%Y%m%d_%H%M%S')}.zip" + out_file_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){ diff --git a/app/helpers/manage_back_helper.rb b/app/helpers/manage_back_helper.rb new file mode 100644 index 000000000..cfc67d714 --- /dev/null +++ b/app/helpers/manage_back_helper.rb @@ -0,0 +1,115 @@ +module ManageBackHelper + extend ActiveSupport::Concern + + def sidebar_item_group(url, text, **opts) + link_opts = url.start_with?('/') ? {} : { 'data-toggle': 'collapse', 'aria-expanded': false } + content = + link_to url, link_opts do + content_tag(:i, '', class: "fa fa-#{opts[:icon]}", 'data-toggle': 'tooltip', 'data-placement': 'right', 'data-boundary': 'window', title: text) + + content_tag(:span, text) + end + + content += + content_tag(:ul, id: url[1..-1], class: 'collapse list-unstyled', "data-parent": '#sidebar') do + yield + end + + raw content + end + + def sidebar_item(url, text, **opts) + content = + link_to url, 'data-controller': opts[:controller] do + content_tag(:i, '', class: "fa fa-#{opts[:icon]} fa-fw", 'data-toggle': 'tooltip', 'data-placement': 'right', 'data-boundary': 'window', title: text) + + content_tag(:span, text) + end + + raw content + end + + def admin_sidebar_controller + key = params[:controller].to_s.gsub(/\//, '-') + SidebarUtil.controller_name(key) || key + end + alias_method :sidebar_controller, :admin_sidebar_controller + + def define_admin_breadcrumbs(&block) + content_for(:setup_admin_breadcrumb, &block) + end + alias_method :define_breadcrumbs, :define_admin_breadcrumbs + + def add_admin_breadcrumb(text, url = nil) + @_breadcrumbs ||= [] + @_breadcrumbs << OpenStruct.new(text: text, url: url) + end + alias_method :add_breadcrumb, :add_admin_breadcrumb + + def display_text(str, default = '--') + str.presence || default + end + + def overflow_hidden_span(text, width: 300) + opts = { class: 'd-inline-block text-truncate', style: "max-width: #{width}px" } + opts.merge!('data-toggle': 'tooltip', title: text) if text != '--' + + content_tag(:span, text, opts) + end + + def sort_tag(content = '', **opts) + options = {} + options[:sort_by] = opts.delete(:name) + is_current_sort = params[:sort_by].to_s == options[:sort_by] + options[:sort_direction] = is_current_sort && params[:sort_direction].to_s == 'desc' ? 'asc' : 'desc' + + path = opts.delete(:path) + "?" + unsafe_params.merge(options).to_query + arrow_class = case params[:sort_direction].to_s + when 'desc' then 'fa-sort-amount-desc' + when 'asc' then 'fa-sort-amount-asc' + else '' + end + opts[:style] = "#{opts[:style]} ;position: relative;" + + content_tag(:span, opts) do + link_to path, remote: true do + content = content_tag(:span) { yield } if block_given? + + content += content_tag(:i, '', class: "fa color-light-green ml-1 #{arrow_class}", style: 'position: absolute;top:0;') if is_current_sort + raw content + end + end + end + + def javascript_void_link(name, **opts) + raw link_to(name, 'javascript:void(0)', opts) + end + + def agree_link(name, url, **opts) + klass = ['action agree-action', opts.delete(:class)].compact.join(' ') + + refresh_url_data = "refresh_url=#{CGI::escape(request.fullpath)}" + url = url + (url.index('?') ? '&' : '?') + refresh_url_data + + raw link_to(name, url, { method: :post, remote: true, class: klass, 'data-confirm': '确认审核通过?'}.merge(opts)) + end + + def delete_link(name, url, **opts, &block) + klass = ['action delete-action', opts.delete(:class)].compact.join(' ') + + refresh_url_data = "refresh_url=#{CGI::escape(request.fullpath)}" + url = url + (url.index('?') ? '&' : '?') + refresh_url_data + + if block_given? + raw link_to(url, { method: :delete, remote: true, class: klass, 'data-confirm': '确认删除?'}.merge(opts), &block) + else + raw link_to(name, url, { method: :delete, remote: true, class: klass, 'data-confirm': '确认删除?'}.merge(opts)) + end + end + + def unsafe_params + params.except(:controller, :action).to_unsafe_h + end + + def list_index_no(page,index) + (page - 1) * 20 + index + 1 + end +end \ No newline at end of file diff --git a/app/libs/hot_search_keyword.rb b/app/libs/hot_search_keyword.rb new file mode 100644 index 000000000..f026142cb --- /dev/null +++ b/app/libs/hot_search_keyword.rb @@ -0,0 +1,23 @@ +# 基于Redis实现热门搜索关键字 +class HotSearchKeyword + class << self + def add(keyword) + return if keyword.blank? + Rails.cache.data.zincrby(redis_key, 1, keyword) + end + + def hot(limit = 5) + Rails.cache.data.zrevrange(redis_key, 0, limit - 1) + end + + def available? + Rails.cache.is_a?(ActiveSupport::Cache::RedisStore) + end + + private + + def redis_key + 'educoder:es:hot_keyword' + end + end +end \ No newline at end of file diff --git a/app/libs/omniauth/strategies/qq.rb b/app/libs/omniauth/strategies/qq.rb new file mode 100644 index 000000000..513257e3c --- /dev/null +++ b/app/libs/omniauth/strategies/qq.rb @@ -0,0 +1,50 @@ +module OmniAuth + module Strategies + class QQ < OmniAuth::Strategies::OAuth2 + option :client_options, { + site: 'https://graph.qq.com', + authorize_url: '/oauth2.0/authorize', + token_url: '/oauth2.0/token' + } + + def request_phase + super + end + + def authorize_params + super.tap do |params| + %w[scope client_options].each do |v| + if request.params[v] + params[v.to_sym] = request.params[v] + end + end + end + end + + uid { raw_info['openid'].to_s } + + info do + { + name: user_info['nickname'], + nickname: user_info['nickname'], + image: user_info['figureurl_qq_1'] + } + end + + extra do + { raw_info: user_info } + end + + def raw_info + access_token.options[:mode] = :query + @raw_info ||= access_token.get('/oauth2.0/me').parsed + end + + def user_info + access_token.options[:mode] = :query + params = { oauth_consumer_key: options.client_id, openid: raw_info['openid'], format: 'json' } + @user_info ||= access_token.get('/user/get_user_info', params: params) + end + end + end +end diff --git a/app/libs/util.rb b/app/libs/util.rb index 72e728ab9..84f14a6c0 100644 --- a/app/libs/util.rb +++ b/app/libs/util.rb @@ -1,3 +1,5 @@ +require 'open-uri' + module Util module_function @@ -29,6 +31,16 @@ module Util end end + def download_file(url, save_path) + data = open(url, &:read) + file = File.new(save_path, 'w+') + file.binmode + file << data + file.flush + file.close + file + end + def logger_error(exception) Rails.logger.error(exception.message) exception.backtrace.each { |message| Rails.logger.error(message) } diff --git a/app/libs/wechat/app.rb b/app/libs/wechat/app.rb deleted file mode 100644 index 54f60fa2a..000000000 --- a/app/libs/wechat/app.rb +++ /dev/null @@ -1,11 +0,0 @@ -class Wechat::App - class << self - attr_accessor :appid, :secret - - delegate :access_token, :jscode2session, to: :client - - def client - @_client ||= Wechat::Client.new(appid, secret) - end - end -end \ No newline at end of file diff --git a/app/libs/wechat/weapp.rb b/app/libs/wechat/weapp.rb new file mode 100644 index 000000000..755c9351a --- /dev/null +++ b/app/libs/wechat/weapp.rb @@ -0,0 +1,47 @@ +class Wechat::Weapp + class << self + attr_accessor :appid, :secret + + delegate :access_token, :jscode2session, to: :client + + def client + @_client ||= Wechat::Client.new(appid, secret) + end + + def session_key(openid) + Rails.cache.read(session_key_cache_key(openid)) + end + + def write_session_key(openid, session_key) + Rails.cache.write(session_key_cache_key(openid), session_key) + end + + def verify?(openid, str, signature) + session_key = session_key(openid) + Digest::SHA1.hexdigest("#{str}#{session_key}") == signature + end + + def decrypt(session_key, encrypted_data, iv) + session_key = Base64.decode64(session_key) + encrypted_data = Base64.decode64(encrypted_data) + iv = Base64.decode64(iv) + + cipher = OpenSSL::Cipher::AES.new(128, :CBC) + cipher.decrypt + cipher.padding = 0 + cipher.key = session_key + cipher.iv = iv + data = cipher.update(encrypted_data) << cipher.final + result = JSON.parse(data[0...-data.last.ord]) + + raise Wechat::Error, '解密错误' if result.dig('watermark', 'appid') != appid + result + end + + private + + def session_key_cache_key(openid) + "weapp:#{appid}:#{openid}:session_key" + end + end +end \ No newline at end of file diff --git a/app/libs/wechat_oauth.rb b/app/libs/wechat_oauth.rb new file mode 100644 index 000000000..ba4baee30 --- /dev/null +++ b/app/libs/wechat_oauth.rb @@ -0,0 +1,13 @@ +module WechatOauth + class << self + attr_accessor :appid, :secret, :scope, :base_url + + def logger + @_logger ||= STDOUT + end + + def logger=(l) + @_logger = l + end + end +end \ No newline at end of file diff --git a/app/libs/wechat_oauth/error.rb b/app/libs/wechat_oauth/error.rb new file mode 100644 index 000000000..ac7f5fddc --- /dev/null +++ b/app/libs/wechat_oauth/error.rb @@ -0,0 +1,14 @@ +class WechatOauth::Error < StandardError + attr_reader :code + + def initialize(code, msg) + super(msg) + @code = code + end + + def message + I18n.t("oauth.wechat.#{code}") + rescue I18n::MissingTranslationData + super + end +end \ No newline at end of file diff --git a/app/libs/wechat_oauth/service.rb b/app/libs/wechat_oauth/service.rb new file mode 100644 index 000000000..35ef8f455 --- /dev/null +++ b/app/libs/wechat_oauth/service.rb @@ -0,0 +1,61 @@ +module WechatOauth::Service + module_function + + def request(method, url, params) + WechatOauth.logger.info("[WechatOauth] [#{method.to_s.upcase}] #{url} || #{params}") + + client = Faraday.new(url: WechatOauth.base_url) + response = client.public_send(method, url, params) + result = JSON.parse(response.body) + + WechatOauth.logger.info("[WechatOauth] [#{response.status}] #{result}") + + if result['errcode'].present? && result['errcode'].to_s != '0' + raise WechatOauth::Error.new(result['errcode'], result['errmsg']) + end + + result + end + + # https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html + # response: + # { + # "access_token":"ACCESS_TOKEN", + # "expires_in":7200, + # "refresh_token":"REFRESH_TOKEN", + # "openid":"OPENID", + # "scope":"SCOPE", + # "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL" + # } + def access_token(code) + params = { + appid: WechatOauth.appid, + secret: WechatOauth.secret, + code: code, + grant_type: 'authorization_code' + } + + request(:get, '/sns/oauth2/access_token', params) + end + + # https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Authorized_Interface_Calling_UnionID.html + # response: + # { + # "openid":"OPENID", + # "nickname":"NICKNAME", + # "sex":1, + # "province":"PROVINCE", + # "city":"CITY", + # "country":"COUNTRY", + # "headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0", + # "privilege":[ + # "PRIVILEGE1", + # "PRIVILEGE2" + # ], + # "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL" + # + # } + def user_info(access_token, openid) + request(:get, '/sns/userinfo', access_token: access_token, openid: openid) + end +end \ No newline at end of file diff --git a/app/models/attachment.rb b/app/models/attachment.rb index ae28e7d52..8b7034ab9 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -9,6 +9,8 @@ class Attachment < ApplicationRecord belongs_to :course, foreign_key: :container_id, optional: true has_many :attachment_group_settings, :dependent => :destroy has_many :attachment_histories, -> { order(version: :desc) }, :dependent => :destroy + # 二级目录 + belongs_to :course_second_category, optional: true scope :by_filename_or_user_name, -> (keywords) { joins(:author).where("filename like :search or LOWER(concat(users.lastname, users.firstname)) LIKE :search", :search => "%#{keywords.split(" ").join('|')}%") unless keywords.blank? } @@ -96,7 +98,7 @@ class Attachment < ApplicationRecord def become_history history = self.attachment_histories.first - new_attachment_history = AttachmentHistory.new(self.attributes.except("id", "resource_bank_id", "unified_setting", "course_second_category_id").merge( + new_attachment_history = AttachmentHistory.new(self.attributes.except("id", "resource_bank_id", "unified_setting", "course_second_category_id", "delay_publish").merge( attachment_id: self.id, version: history.nil? ? 1 : history.version + 1, )) @@ -104,7 +106,7 @@ class Attachment < ApplicationRecord end def copy_attributes_from_new_attachment(new_attachment) - self.attributes = new_attachment.attributes.dup.except("id","container_id","container_type","is_public","downloads", "quotes",'is_publish','publish_time') + self.attributes = new_attachment.attributes.dup.except("id","container_id","container_type","is_public","downloads", "quotes",'is_publish','publish_time', "delay_publish") end def set_public(is_public) diff --git a/app/models/competition.rb b/app/models/competition.rb index 024478ad6..9055adefc 100644 --- a/app/models/competition.rb +++ b/app/models/competition.rb @@ -18,6 +18,29 @@ class Competition < ApplicationRecord after_create :create_competition_modules + def mode_type + case mode + when 1 + "课堂" + when 2 + "实训" + when 3 + "教学" + when 4 + "托管" + else + "--" + end + end + + def teacher_staff_num + teacher_staff ? "#{teacher_staff.minimum}~#{teacher_staff.maximum}" : "--" + end + + def member_staff_num + member_staff ? "#{member_staff.minimum}~#{member_staff.maximum}" : "--" + end + # 是否上架 def published? status? diff --git a/app/models/competition_mode_setting.rb b/app/models/competition_mode_setting.rb new file mode 100644 index 000000000..b6bafa7c3 --- /dev/null +++ b/app/models/competition_mode_setting.rb @@ -0,0 +1,3 @@ +class CompetitionModeSetting < ApplicationRecord + belongs_to :course +end diff --git a/app/models/homework_common.rb b/app/models/homework_common.rb index fc2dd3ea4..def3b6b0d 100644 --- a/app/models/homework_common.rb +++ b/app/models/homework_common.rb @@ -37,6 +37,7 @@ class HomeworkCommon < ApplicationRecord validates :name, length: { maximum: 60 } validates :description, length: { maximum: 15000 } + validates :explanation, length: { maximum: 5000 } validates :reference_answer, length: { maximum: 15000 } # after_update :update_activity diff --git a/app/models/homework_group_setting.rb b/app/models/homework_group_setting.rb index ae9491cb3..7a06d5a7a 100644 --- a/app/models/homework_group_setting.rb +++ b/app/models/homework_group_setting.rb @@ -6,6 +6,6 @@ class HomeworkGroupSetting < ApplicationRecord scope :none_published, -> {where("homework_group_settings.publish_time IS NULL OR homework_group_settings.publish_time > ?", Time.now)} scope :published_no_end, -> {where("homework_group_settings.publish_time IS NOT NULL AND homework_group_settings.publish_time < ? and homework_group_settings.end_time > ?", Time.now, Time.now)} - scope :none_end, -> {where("homework_group_settings.end_time IS NOT NULL AND homework_group_settings.end_time > ?", Time.now)} + scope :none_end, -> {where("homework_group_settings.end_time IS NULL or homework_group_settings.end_time > ?", Time.now)} end diff --git a/app/models/laboratory.rb b/app/models/laboratory.rb index 53e66ece0..35965dc0d 100644 --- a/app/models/laboratory.rb +++ b/app/models/laboratory.rb @@ -6,8 +6,12 @@ class Laboratory < ApplicationRecord has_one :laboratory_setting, dependent: :destroy + has_many :portal_images, dependent: :destroy + validates :identifier, uniqueness: { case_sensitive: false }, allow_nil: true + delegate :name, :navbar, :footer, :login_logo_url, :nav_logo_url, :tab_logo_url, :default_navbar, to: :laboratory_setting + def site rails_env = EduSetting.get('rails_env') suffix = rails_env && rails_env != 'production' ? ".#{rails_env}.educoder.net" : '.educoder.net' @@ -19,7 +23,7 @@ class Laboratory < ApplicationRecord return if subdomain.blank? rails_env = EduSetting.get('rails_env') - subdomain = subdomain.slice(0, subdomain.size - rails_env.size - 1) if subdomain.end_with?(rails_env) # winse.dev => winse + subdomain = subdomain.slice(0, subdomain.size - rails_env.size - 1) if rails_env && subdomain.end_with?(rails_env) # winse.dev => winse find_by_identifier(subdomain) end diff --git a/app/models/open_user.rb b/app/models/open_user.rb new file mode 100644 index 000000000..45c4ee6ba --- /dev/null +++ b/app/models/open_user.rb @@ -0,0 +1,11 @@ +class OpenUser < ApplicationRecord + belongs_to :user + + validates :uid, presence: true, uniqueness: { scope: :type } + + serialize :extra, JSON + + def can_bind_cache_key + "open_user:#{type}:#{uid}:can_bind" + end +end \ No newline at end of file diff --git a/app/models/open_users/qq.rb b/app/models/open_users/qq.rb new file mode 100644 index 000000000..9e7827128 --- /dev/null +++ b/app/models/open_users/qq.rb @@ -0,0 +1,9 @@ +class OpenUsers::QQ < OpenUser + def nickname + extra&.[]('nickname') + end + + def en_type + 'qq' + end +end \ No newline at end of file diff --git a/app/models/open_users/wechat.rb b/app/models/open_users/wechat.rb new file mode 100644 index 000000000..aadc9e78c --- /dev/null +++ b/app/models/open_users/wechat.rb @@ -0,0 +1,9 @@ +class OpenUsers::Wechat < OpenUser + def nickname + extra&.[]('nickname') + end + + def en_type + 'qq' + end +end \ No newline at end of file diff --git a/app/models/portal_image.rb b/app/models/portal_image.rb index a3fd71bb5..b07214c77 100644 --- a/app/models/portal_image.rb +++ b/app/models/portal_image.rb @@ -1,4 +1,8 @@ class PortalImage < ApplicationRecord + belongs_to :laboratory + + scope :only_online, -> { where(status: true) } + def online? status? end diff --git a/app/models/user.rb b/app/models/user.rb index 0f169b5a4..103f9e0b5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -28,9 +28,12 @@ class User < ApplicationRecord MIX_PASSWORD_LIMIT = 8 - CHARS = %W(2 3 4 5 6 7 8 9 a b c f e f g h i j k l m n o p q r s t u v w x y z) + LOGIN_CHARS = %W(2 3 4 5 6 7 8 9 a b c f e f g h i j k l m n o p q r s t u v w x y z).freeze has_one :user_extension, dependent: :destroy + has_many :open_users, dependent: :destroy + has_one :wechat_open_user, class_name: 'OpenUsers::Wechat' + has_one :qq_open_user, class_name: 'OpenUsers::QQ' accepts_nested_attributes_for :user_extension, update_only: true has_many :memos, foreign_key: 'author_id' @@ -40,7 +43,7 @@ class User < ApplicationRecord has_many :myshixuns, :dependent => :destroy has_many :study_shixuns, through: :myshixuns, source: :shixun # 已学习的实训 has_many :course_messages - has_many :courses, dependent: :destroy + has_many :courses, foreign_key: 'tea_id', dependent: :destroy #试卷 has_many :exercise_banks, :dependent => :destroy @@ -166,6 +169,8 @@ class User < ApplicationRecord # validates_format_of :mail, with: VALID_EMAIL_REGEX, multiline: true # validates_format_of :phone, with: VALID_PHONE_REGEX, multiline: true validate :validate_password_length + #validate :validate_ID_number + #validates_format_of :ID_number, with: VALID_NUMBER_REGEX, multiline: true, message: "身份证号格式不对" # validates :nickname, presence: true, length: { maximum: 10 } # validates :lastname, presence: true @@ -242,6 +247,11 @@ class User < ApplicationRecord user_extension&.department&.name || '' end + # 课堂的所有身份 + def course_role course + course.course_members.where(user_id: id).pluck(:role) + end + # 课堂的老师(创建者、老师、助教) def teacher_of_course?(course) course.course_members.exists?(user_id: id, role: [1,2,3], is_active: 1) || admin? || business? @@ -264,7 +274,7 @@ class User < ApplicationRecord # 课堂的学生 def student_of_course?(course) - course.course_members.exists?(user_id: id, role: %i[STUDENT]) + course.course_members.exists?(user_id: id, role: %i[STUDENT], is_active: 1) end # 课堂成员 @@ -583,6 +593,16 @@ class User < ApplicationRecord mail.present? end + # 手机号:123***123 + def hidden_phone + Util.conceal(phone, :phone).to_s + end + + # 邮箱:w***l@qq.com + def hidden_mail + Util.conceal(mail, :email).to_s + end + # 学院的url标识 def college_identifier Department.find_by_id(department_members.pluck(:department_id).first)&.identifier @@ -613,13 +633,21 @@ class User < ApplicationRecord admin? || business? end - def self.generate_login(prefix = 'p') - code = CHARS.sample(8).join - while User.exists?(login: prefix + code) do - code = CHARS.sample(8).join + def self.generate_login(prefix) + login = prefix + LOGIN_CHARS.sample(8).join('') + while User.exists?(login: login) + login = prefix + LOGIN_CHARS.sample(8).join('') end - prefix + code + login + end + + def bind_open_user?(type) + case type + when 'wechat' then wechat_open_user.present? + when 'qq' then qq_open_user.present? + else false + end end protected diff --git a/app/queries/admins/laboratory_query.rb b/app/queries/admins/laboratory_query.rb index 21020216b..8667bb8ed 100644 --- a/app/queries/admins/laboratory_query.rb +++ b/app/queries/admins/laboratory_query.rb @@ -15,7 +15,7 @@ class Admins::LaboratoryQuery < ApplicationQuery keyword = strip_param(:keyword) if keyword.present? like_sql = 'schools.name LIKE :keyword OR laboratories.identifier LIKE :keyword' - laboratories = laboratories.joins(:school).where(like_sql, keyword: "%#{keyword}%") + laboratories = laboratories.left_joins(:school).where(like_sql, keyword: "%#{keyword}%") end custom_sort laboratories, params[:sort_by], params[:sort_direction] diff --git a/app/queries/admins/user_query.rb b/app/queries/admins/user_query.rb index 75e50fc1b..749ba2110 100644 --- a/app/queries/admins/user_query.rb +++ b/app/queries/admins/user_query.rb @@ -37,6 +37,9 @@ class Admins::UserQuery < ApplicationQuery users = users.where('CONCAT(lastname, firstname) LIKE :name', name: "%#{name}%") end + # 单位ID + users = users.joins(:user_extension).where(user_extensions: { school_id: params[:school_id] }) if params[:school_id].present? + # 学校名称 school_name = params[:school_name].to_s.strip.presence users = users.joins(user_extension: :school).where('schools.name LIKE ?', "%#{school_name}%") if school_name diff --git a/app/services/admins/drag_portal_image_service.rb b/app/services/admins/drag_portal_image_service.rb index 9f8adea38..5555c08b2 100644 --- a/app/services/admins/drag_portal_image_service.rb +++ b/app/services/admins/drag_portal_image_service.rb @@ -1,9 +1,10 @@ class Admins::DragPortalImageService < ApplicationService Error = Class.new(StandardError) - attr_reader :move, :after + attr_reader :laboratory, :move, :after - def initialize(move, after) + def initialize(laboratory, move, after) + @laboratory = laboratory @move = move @after = after # 移动后下一个位置的元素 end @@ -11,7 +12,7 @@ class Admins::DragPortalImageService < ApplicationService def call return if move.position + 1 == after&.position # 未移动 - images = PortalImage.all + images = laboratory.portal_images ActiveRecord::Base.transaction do if after.blank? || move.id == after.id # 移动至末尾 @@ -31,5 +32,4 @@ class Admins::DragPortalImageService < ApplicationService end end end - end \ No newline at end of file diff --git a/app/services/admins/save_laboratory_setting_service.rb b/app/services/admins/save_laboratory_setting_service.rb index 00e202cd9..f2a5c151e 100644 --- a/app/services/admins/save_laboratory_setting_service.rb +++ b/app/services/admins/save_laboratory_setting_service.rb @@ -30,7 +30,7 @@ class Admins::SaveLaboratorySettingService < ApplicationService hash = {} hash[:name] = strip nav[:name] hash[:link] = strip nav[:link] - hash[:hidden] = nav[:hidden].to_s == 0 + hash[:hidden] = nav[:hidden].to_s != '0' hash end end diff --git a/app/services/application_service.rb b/app/services/application_service.rb index 8b3819017..1be6896eb 100644 --- a/app/services/application_service.rb +++ b/app/services/application_service.rb @@ -1,6 +1,8 @@ class ApplicationService include Callable + Error = Class.new(StandardError) + private def strip(str) diff --git a/app/services/create_bind_user_service.rb b/app/services/create_bind_user_service.rb new file mode 100644 index 000000000..f7fc5b6f6 --- /dev/null +++ b/app/services/create_bind_user_service.rb @@ -0,0 +1,53 @@ +class CreateBindUserService < ApplicationService + attr_reader :user, :params + + def initialize(user, params) + @user = user + @params = params + end + + def call + raise Error, '系统错误' if open_user.blank? + raise Error, '系统错误' unless can_bind_user? + + if params[:not_bind].to_s == 'true' + clear_can_bind_user_flag + return user + end + + bind_user = User.try_to_login(params[:username], params[:password]) + raise Error, '用户名或者密码错误' if bind_user.blank? + raise Error, '该账号已被其他微信号绑定,请更换其他账号进行绑定' if bind_user.bind_open_user?(params[:type].to_s) + + ActiveRecord::Base.transaction do + open_user.user_id = bind_user.id + open_user.save! + + user.user_extension.delete + user.delete + end + + clear_can_bind_user_flag + + bind_user + end + + private + + def open_user + @_open_user ||= begin + case params[:type].to_s + when 'wechat' then user.wechat_open_user + when 'qq' then user.qq_open_user + end + end + end + + def can_bind_user? + Rails.cache.read(open_user.can_bind_cache_key).present? + end + + def clear_can_bind_user_flag + Rails.cache.delete(open_user.can_bind_cache_key) + end +end \ No newline at end of file diff --git a/app/services/oauth/create_or_find_qq_account_service.rb b/app/services/oauth/create_or_find_qq_account_service.rb new file mode 100644 index 000000000..200d436ef --- /dev/null +++ b/app/services/oauth/create_or_find_qq_account_service.rb @@ -0,0 +1,38 @@ +class Oauth::CreateOrFindQqAccountService < ApplicationService + + attr_reader :user, :params + + def initialize(user, params) + @user = user + @params = params + end + + def call + new_user = false + # 存在该用户 + open_user = OpenUsers::QQ.find_by(uid: params['uid']) + return [open_user.user, new_user] if open_user.present? + + if user.blank? || !user.logged? + new_user = true + # 新用户 + login = User.generate_login('q') + @user = User.new(login: login, nickname: params.dig('info', 'nickname'), type: 'User', status: User::STATUS_ACTIVE) + end + + ActiveRecord::Base.transaction do + if user.new_record? + user.save! + + gender = params.dig('extra', 'raw_info', 'gender') == '女' ? 1 : 0 + user.create_user_extension!(gender: gender) + end + + new_open_user = OpenUsers::QQ.create!(user: user, uid: params['uid'], extra: params.dig('extra', 'raw_info')) + + Rails.cache.write(new_open_user.can_bind_cache_key, 1, expires_in: 1.hours) if new_user # 方便后面进行账号绑定 + end + + [user, new_user] + end +end \ No newline at end of file diff --git a/app/services/oauth/create_or_find_wechat_account_service.rb b/app/services/oauth/create_or_find_wechat_account_service.rb new file mode 100644 index 000000000..372e55900 --- /dev/null +++ b/app/services/oauth/create_or_find_wechat_account_service.rb @@ -0,0 +1,57 @@ +class Oauth::CreateOrFindWechatAccountService < ApplicationService + Error = Class.new(StandardError) + + attr_reader :user, :params + + def initialize(user, params) + @user = user + @params = params + end + + def call + code = params['code'].to_s.strip + raise Error, 'Code不能为空' if code.blank? + new_user = false + + result = WechatOauth::Service.access_token(code) + result = WechatOauth::Service.user_info(result['access_token'], result['openid']) + + # 存在该用户 + open_user = OpenUsers::Wechat.find_by(uid: result['unionid']) + return [open_user.user, new_user] if open_user.present? + + if user.blank? || !user.logged? + new_user = true + # 新用户 + login = User.generate_login('w') + @user = User.new(login: login, nickname: result['nickname'], type: 'User', status: User::STATUS_ACTIVE) + end + + ActiveRecord::Base.transaction do + if new_user + user.save! + + gender = result['sex'].to_i == 1 ? 0 : 1 + user.create_user_extension!(gender: gender) + + # 下载头像 + avatar_path = Util::FileManage.source_disk_filename(user) + Util.download_file(result['headimgurl'], avatar_path) + end + + new_open_user= OpenUsers::Wechat.create!(user: user, uid: result['unionid'], extra: result) + + Rails.cache.write(new_open_user.can_bind_cache_key, 1, expires_in: 1.hours) if new_user # 方便后面进行账号绑定 + end + + [user, new_user] + rescue WechatOauth::Error => ex + raise Error, ex.message + end + + private + + def code + params[:code].to_s.strip + end +end \ No newline at end of file diff --git a/app/services/users/update_password_service.rb b/app/services/users/update_password_service.rb index 0df32eb76..53c6f74c8 100644 --- a/app/services/users/update_password_service.rb +++ b/app/services/users/update_password_service.rb @@ -11,7 +11,7 @@ class Users::UpdatePasswordService < ApplicationService def call Users::UpdatePasswordForm.new(params).validate! - raise Error, '旧密码不匹配' unless user.check_password?(params[:old_password]) + raise Error, '旧密码不匹配' unless user.check_password?(params[:old_password]) || user.hashed_password.blank? ActiveRecord::Base.transaction do user.update!(password: params[:password]) diff --git a/app/views/admins/carousels/index.html.erb b/app/views/admins/carousels/index.html.erb index 89c224784..299d1dcfd 100644 --- a/app/views/admins/carousels/index.html.erb +++ b/app/views/admins/carousels/index.html.erb @@ -1,5 +1,6 @@ <% define_admin_breadcrumbs do + add_admin_breadcrumb('云上实验室', admins_laboratories_path) add_admin_breadcrumb('轮播图') end %> @@ -9,7 +10,7 @@ 首页轮播图(拖动排序) <%= javascript_void_link '添加', class: 'btn btn-primary btn-sm add-btn', data: { toggle: 'modal', target: '.admin-add-carousel-modal' } %> -
+
<% @images.each_with_index do |image, index| %> -<%= render partial: 'admins/carousels/shared/add_carousel_modal' %> +<%= render partial: 'admins/carousels/shared/add_carousel_modal', locals: { laboratory_id: current_laboratory } %> <%= render partial: 'admins/shared/modal/upload_file_modal' %> \ No newline at end of file diff --git a/app/views/admins/carousels/shared/_add_carousel_modal.html.erb b/app/views/admins/carousels/shared/_add_carousel_modal.html.erb index d1e75e29e..c3055e59e 100644 --- a/app/views/admins/carousels/shared/_add_carousel_modal.html.erb +++ b/app/views/admins/carousels/shared/_add_carousel_modal.html.erb @@ -8,7 +8,7 @@