diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js index 72a2ff118..196404300 100644 --- a/app/assets/javascripts/admin.js +++ b/app/assets/javascripts/admin.js @@ -32,8 +32,11 @@ $(document).on('turbolinks:load', function(){ // flash alert提示框自动关闭 if($('.admin-alert-container .alert').length > 0){ setTimeout(function(){ - $('.admin-alert-container .alert').alert('close'); + $('.admin-alert-container .alert:not(.alert-danger)').alert('close'); }, 2000); + setTimeout(function(){ + $('.admin-alert-container .alert.alert-danger').alert('close'); + }, 5000); } }); diff --git a/app/assets/javascripts/admins/mirror_repositories/edit.js b/app/assets/javascripts/admins/mirror_repositories/edit.js new file mode 100644 index 000000000..7fb3ad10d --- /dev/null +++ b/app/assets/javascripts/admins/mirror_repositories/edit.js @@ -0,0 +1,19 @@ +$(document).on('turbolinks:load', function() { + if ($('body.admins-mirror-repositories-edit-page, body.admins-mirror-repositories-update-page').length > 0) { + var $form = $('form.edit-mirror'); + + $form.validate({ + errorElement: 'span', + errorClass: 'danger text-danger', + rules: { + "mirror_repository[type_name]": { + required: true + } + } + }); + + $form.submit(function(e){ + if(!$form.valid()){ e.preventDefault(); } + }); + } +}); \ No newline at end of file diff --git a/app/assets/javascripts/admins/mirror_repositories/index.js b/app/assets/javascripts/admins/mirror_repositories/index.js new file mode 100644 index 000000000..2e30bdd94 --- /dev/null +++ b/app/assets/javascripts/admins/mirror_repositories/index.js @@ -0,0 +1,4 @@ +$(document).on('turbolinks:load', function() { + if ($('body.admins-mirror-repositories-index-page').length > 0) { + } +}); \ No newline at end of file diff --git a/app/assets/javascripts/admins/modals/admin-choose-mirror-modal.js b/app/assets/javascripts/admins/modals/admin-choose-mirror-modal.js new file mode 100644 index 000000000..6111b2401 --- /dev/null +++ b/app/assets/javascripts/admins/modals/admin-choose-mirror-modal.js @@ -0,0 +1,32 @@ +$(document).on('turbolinks:load', function() { + $('.admin-modal-container').on('show.bs.modal', '.modal.admin-choose-mirror-modal', function(){ + var $modal = $('.modal.admin-choose-mirror-modal'); + var $form = $modal.find('form.admin-choose-mirror-form'); + + var validateForm = function(){ + var checkedValue = $form.find('input[name="mirror_number"]:checked').val(); + + if(checkedValue == undefined){ + $modal.find('.error').html('必须选择一种镜像保存!'); + return false; + } + return true; + } + + $modal.on('click', '.submit-btn', function(){ + $form.find('.error').html(''); + var url = $form.attr('action'); + + if (validateForm()) { + $.ajax({ + method: 'POST', + dataType: 'script', + url: url, + data: $form.serialize(), + }).done(function(){ + $modal.modal('hide'); + }); + } + }); + }) +}); \ No newline at end of file diff --git a/app/assets/javascripts/admins/modals/admin-replace-mirror-modal.js b/app/assets/javascripts/admins/modals/admin-replace-mirror-modal.js new file mode 100644 index 000000000..8b7a129a6 --- /dev/null +++ b/app/assets/javascripts/admins/modals/admin-replace-mirror-modal.js @@ -0,0 +1,89 @@ +$(document).on('turbolinks:load', function() { + var $modal = $('.modal.admin-replace-mirror-modal'); + if ($modal.length > 0) { + var $form = $modal.find('form.admin-replace-mirror-form'); + var $mirrorIdInput = $modal.find('.modal-body input[name="mirror_id"]'); + var $mirrorSelect = $modal.find('.new-mirror-select'); + + var setMirror = function(id, name){ + $mirrorIdInput.val(id); + $form.find('.mirror-id-container').html(id); + $form.find('.mirror-name-container').html(name); + } + + $form.validate({ + errorElement: 'span', + errorClass: 'danger text-danger', + rules: { + new_mirror_id: { + required: true + }, + }, + messages: { + new_mirror_id: { + required: '请选择新镜像' + } + } + }); + + // modal ready fire + $modal.on('show.bs.modal', function (event) { + var $link = $(event.relatedTarget); + + var mirrorId = $link.data('id'); + var mirrorName = $link.data('name'); + + setMirror(mirrorId, mirrorName); + $mirrorSelect.select2('val', ' '); + }); + $modal.on('hide.bs.modal', function () { + setMirror('', ''); + $mirrorSelect.select2('val', ' '); + $('#new_mirror_id-error').remove(); + }); + + $mirrorSelect.select2({ + theme: 'bootstrap4', + placeholder: '输入要合并的镜像名', + minimumInputLength: 1, + ajax: { + url: '/admins/mirror_repositories/for_select', + dataType: 'json', + data: function(params){ + return { keyword: params.term }; + }, + processResults: function(data){ + return { results: data.mirrors } + } + }, + templateResult: function (item) { + if(!item.id || item.id === '') return item.text; + return item.name; + }, + templateSelection: function(item){ + if (item.id) { + $('#new_mirror_id-error').remove(); + $('#new_mirror_id').val(item.id); + } + return item.name || item.text; + } + }); + + $modal.on('click', '.submit-btn', function(){ + $form.find('.error').html(''); + + if ($form.valid()) { + var url = $form.data('url'); + + $.ajax({ + method: 'POST', + dataType: 'script', + url: url, + data: $form.serialize(), + }).done(function(){ + $modal.modal('hide'); + }); + } + }); + } +}); \ No newline at end of file diff --git a/app/assets/stylesheets/admin.scss b/app/assets/stylesheets/admin.scss index 0aa1329ca..36f1e537f 100644 --- a/app/assets/stylesheets/admin.scss +++ b/app/assets/stylesheets/admin.scss @@ -47,4 +47,9 @@ label.error { .flex-1 { flex: 1; -} \ No newline at end of file +} + +.font-12 { font-size: 12px !important; } +.font-14 { font-size: 14px !important; } +.font-16 { font-size: 16px !important; } +.font-18 { font-size: 18px !important; } \ No newline at end of file diff --git a/app/controllers/admins/choose_mirror_repositories_controller.rb b/app/controllers/admins/choose_mirror_repositories_controller.rb new file mode 100644 index 000000000..c178e0d76 --- /dev/null +++ b/app/controllers/admins/choose_mirror_repositories_controller.rb @@ -0,0 +1,11 @@ +class Admins::ChooseMirrorRepositoriesController < Admins::BaseController + def new + @mirror = MirrorRepository.find(params[:mirror_id]) + @new_mirror = MirrorOperationRecord.where(mirror_repository_id: @mirror.id, status: 1, user_id: -1).first + end + + def create + mirror = MirrorRepository.find(params[:mirror_id]) + Admins::ChooseMirrorService.call(mirror, current_user, params[:mirror_number]) + end +end \ No newline at end of file diff --git a/app/controllers/admins/mirror_repositories_controller.rb b/app/controllers/admins/mirror_repositories_controller.rb new file mode 100644 index 000000000..63e4667d1 --- /dev/null +++ b/app/controllers/admins/mirror_repositories_controller.rb @@ -0,0 +1,96 @@ +class Admins::MirrorRepositoriesController < Admins::BaseController + before_action :check_shixun_mirrors!, only: [:index] + + def index + mirrors = MirrorRepository.all + mirrors = mirrors.reorder(status: :desc, main_type: :desc, type_name: :asc) + + @mirrors = paginate mirrors.includes(:mirror_scripts) + @error_mirror_names = MirrorRepository.where(status: 5).pluck(:name) + end + + def new + @mirror = MirrorRepository.new + end + + def create + @mirror = MirrorRepository.new + Admins::SaveMirrorRepositoryService.call(@mirror, current_user, form_params) + + flash[:success] = '保存成功' + redirect_to edit_admins_mirror_repository_path(@mirror) + rescue ActiveRecord::RecordInvalid + flash.now[:danger] = '保存失败' + render 'new' + rescue Admins::SaveMirrorRepositoryService::Error => ex + flash.now[:danger] = ex.message + render 'new' + end + + def edit + @mirror = current_mirror + end + + def update + @mirror = current_mirror + + Admins::SaveMirrorRepositoryService.call(current_mirror, current_user, form_params) + + flash[:success] = '保存成功' + redirect_to edit_admins_mirror_repository_path(current_mirror) + rescue ActiveRecord::RecordInvalid + flash.now[:danger] = '保存失败' + render 'edit' + rescue Admins::SaveMirrorRepositoryService::Error => ex + flash.now[:danger] = ex.message + render 'edit' + end + + def destroy + return render_js_error('该状态下不允许删除') unless current_mirror.deletable? + + current_mirror.destroy! + + render_delete_success + end + + def for_select + mirrors = MirrorRepository.all + + keyword = params[:keyword].to_s.strip + mirrors = mirrors.where('name LIKE ?', "%#{keyword}%") if keyword.present? + + @mirrors = paginate mirrors + + render_ok(count: @mirrors.total_count, mirrors: @mirrors.as_json(only: %i[id name])) + end + + def merge + origin_mirror = MirrorRepository.find(params[:mirror_id]) + mirror = MirrorRepository.find(params[:new_mirror_id]) + + ActiveRecord::Base.transaction do + origin_mirror.update!(name: mirror.name, mirrorID: mirror.mirrorID) + mirror.destroy! + end + end + + private + + def current_mirror + @_current_mirror ||= MirrorRepository.find(params[:id]) + end + + def form_params + columns = %i[type_name main_type time_limit resource_limit cpu_limit memory_limit description status] + params.require(:mirror_repository).permit(*columns) + end + + def check_shixun_mirrors! + return unless request.format.html? + + Admins::CheckShixunMirrorsService.call + rescue Admins::CheckShixunMirrorsService::Error => e + internal_server_error(e.message) + end +end diff --git a/app/controllers/concerns/admins/render_helper.rb b/app/controllers/concerns/admins/render_helper.rb index 0ccc16a09..80f008b08 100644 --- a/app/controllers/concerns/admins/render_helper.rb +++ b/app/controllers/concerns/admins/render_helper.rb @@ -1,36 +1,34 @@ 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 - respond_to do |format| - format.html { redirect_to '/403' } - format.json { super } - end + render_by_format(html: -> { redirect_to '/403' }, + json: -> { render status: 403, json: { messages: I18n.t('error.forbidden') } } ) end def render_not_found - respond_to do |format| - format.html { render 'admins/shared/404' } - format.js { render_js_error('资源未找到') } - format.json { render status: 404, json: { message: '资源未找到' } } - end + render_by_format(html: -> { render 'admins/shared/404' }, + js: -> { render_js_error('资源未找到') }, + json: -> { render status: 404, json: { message: '资源未找到' } }) end def render_unprocessable_entity(message) - respond_to do |format| - format.html { render 'admins/shared/422' } - format.js { render_js_error(message) } - format.json { render status: 422, json: { message: message } } - end + render_by_format(html: -> { render 'admins/shared/422' }, + js: -> { render_js_error(message) }, + json: -> { render status: 422, json: { message: message } }) end alias_method :render_error, :render_unprocessable_entity - def internal_server_error - respond_to do |format| - format.html { render 'admins/shared/500' } - format.js { render_js_error('系统错误') } - format.json { render status: 500, json: { message: '系统错误' } } - end + 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) diff --git a/app/helpers/admins/mirror_repositories_helper.rb b/app/helpers/admins/mirror_repositories_helper.rb new file mode 100644 index 000000000..d62494e24 --- /dev/null +++ b/app/helpers/admins/mirror_repositories_helper.rb @@ -0,0 +1,23 @@ +module Admins::MirrorRepositoriesHelper + def mirror_type_tag(mirror) + case mirror.main_type + when '1' then ''.html_safe + when '0' then ''.html_safe + end + end + + def mirror_status_tag(mirror) + case mirror.status + when 0 + ''.html_safe + when 1 + ''.html_safe + when 2, 3 + ''.html_safe + when 4 + ''.html_safe + when 5 + ''.html_safe + end + end +end \ No newline at end of file diff --git a/app/models/mirror_operation_record.rb b/app/models/mirror_operation_record.rb new file mode 100644 index 000000000..39e352a0f --- /dev/null +++ b/app/models/mirror_operation_record.rb @@ -0,0 +1,7 @@ +# status: 0 创建镜像; 1 修改镜像ID; 2 修改镜像name 3 删除镜像 4.从主节点同步镜像到子节点(子节点发生异常), 5. 修改镜像别名, 6. 修改镜像的状态 +# user_id: -1时,证明是非人为因素造成,中间层异常导致 +class MirrorOperationRecord < ActiveRecord::Base + default_scope { order(created_at: :desc) } + + belongs_to :mirror_repository +end diff --git a/app/models/mirror_repository.rb b/app/models/mirror_repository.rb index be26b5ad1..e29b008ad 100644 --- a/app/models/mirror_repository.rb +++ b/app/models/mirror_repository.rb @@ -8,4 +8,8 @@ class MirrorRepository < ApplicationRecord scope :published_mirror, -> { where(status: 1) } scope :published_main_mirror, -> { published_mirror.where(main_type: 1) } scope :published_small_mirror, -> { published_mirror.where(main_type: 0) } + + def deletable? + status != 1 && !shixun_mirror_repositories.exists? + end end diff --git a/app/services/admins/check_shixun_mirrors_service.rb b/app/services/admins/check_shixun_mirrors_service.rb new file mode 100644 index 000000000..868fab042 --- /dev/null +++ b/app/services/admins/check_shixun_mirrors_service.rb @@ -0,0 +1,89 @@ +class Admins::CheckShixunMirrorsService < ApplicationService + Error = Class.new(StandardError) + + def call + bridge_images + + ActiveRecord::Base.transaction do + check_sync_mirrors! + + check_mirrors! + end + end + + private + + def mirrors + bridge_images['images'] + end + + def sync_mirrors + bridge_images['imagesNotSync'] + end + + def check_mirrors! + return if mirrors.blank? + image_names = [] + + mirrors.each do |data| + mirror = JSON.parse(data) + + name_repository = MirrorRepository.find_by(name: mirror['imageName']) + id_repository = MirrorRepository.find_by(mirrorID: mirror['imageID']) + + image_names << mirror['imageName'] + + if name_repository.blank? && id_repository.present? # 镜像名称被修改 + id_repository.update_column(:status, 2) + MirrorOperationRecord.create!(mirror_repository_id: id_repository.id, mirror_id: mirror['imageID'], + mirror_name: mirror['imageName'], status: 2, user_id: -1) + elsif name_repository.blank? # 镜像不存在、创建镜像 + new_repository = MirrorRepository.create!(mirrorID: mirror['imageID'], name: mirror['imageName']) + MirrorOperationRecord.create!(mirror_repository_id: new_repository.id, mirror_id: mirror['imageID'], + mirror_name: mirror['imageName'], status: 0, user_id: -1) + elsif name_repository.mirrorID != mirror['imageID'] # 镜像ID被修改 + name_repository.update_column(:status, 2) + MirrorOperationRecord.create!(mirror_repository_id: name_repository.id, mirror_id: mirror['imageID'], + mirror_name: mirror['imageName'], status: 1, user_id: -1) + end + end + + # 判断中间层镜像是否被删除 + MirrorRepository.find_each do |mirror| + next if mirror&.name.blank? || image_names.index(mirror.name) + + mirror.update_column(:status, 4) + MirrorOperationRecord.create!(mirror_repository_id: mirror.id, mirror_id: mirror&.mirrorID, + mirror_name: mirror.name, status: 3, user_id: -1) + end + end + + def check_sync_mirrors! + return if sync_mirrors.blank? + + sync_mirrors.each do |data| + mirror = JSON.parse(data) + + repository = MirrorRepository.find_by(name: mirror['imageName']) + next if repository.blank? || repository.status != 1 + + repository.update_column(:status, 5) + MirrorOperationRecord.create!(mirror_repository_id: repository.id, mirror_id: mirror['imageID'], + mirror_name: mirror['imageName'], status: 4, user_id: -1) + end + end + + def bridge_images + @_bridge_images ||= begin + url = EduSetting.get('cloud_bridge') + res = Faraday.get(url) + + raise Error, '拉取镜像信息异常' if res && res['code'].nonzero? + + res + rescue => e + Rails.logger.error("get response failed ! #{e.message}") + raise Error, '实训云平台繁忙(繁忙等级:84)' + end + end +end \ No newline at end of file diff --git a/app/services/admins/choose_mirror_service.rb b/app/services/admins/choose_mirror_service.rb new file mode 100644 index 000000000..77d187884 --- /dev/null +++ b/app/services/admins/choose_mirror_service.rb @@ -0,0 +1,21 @@ +class Admins::ChooseMirrorService < ApplicationService + attr_reader :mirror, :user, :number + + def initialize(mirror, user, mirror_number) + @mirror = mirror + @user = user + @number = mirror_number + end + + def call + if mirror.mirrorID == number + mirror.update_column(:status, 1) + return + end + + old_number = mirror.mirrorID + mirror.update!(mirrorID: number, status: 1) + MirrorOperationRecord.create!(mirror_repository_id: mirror.id, mirror_id: number, mirror_name: mirror.name, + status: 1, user_id: user.id, old_tag: old_number, new_tag: mirror.mirrorID) + end +end \ No newline at end of file diff --git a/app/services/admins/save_mirror_repository_service.rb b/app/services/admins/save_mirror_repository_service.rb new file mode 100644 index 000000000..4aff64f66 --- /dev/null +++ b/app/services/admins/save_mirror_repository_service.rb @@ -0,0 +1,37 @@ +class Admins::SaveMirrorRepositoryService < ApplicationService + Error = Class.new(StandardError) + + attr_reader :mirror, :user, :params + + def initialize(mirror, user, params) + @mirror = mirror + @user = user + @params = params + end + + def call + mirror.assign_attributes(params) + + raise Error, '镜像别名重复' if MirrorRepository.where.not(id: mirror.id).exists?(type_name: params[:type_name]) + + ActiveRecord::Base.transaction do + record_operation! if mirror.persisted? + + mirror.save! + end + end + + private + + def record_operation! + if mirror.type_name_changed? + MirrorOperationRecord.create!(mirror_repository_id: mirror.id, status: 5, + user_id: user.id, old_tag: mirror.type_name_in_database, + new_tag: mirror.type_name) + elsif mirror.status_changed? + MirrorOperationRecord.create!(mirror_repository_id: mirror.id, status: 5, + user_id: user.id, old_tag: mirror.status_in_database, + new_tag: mirror.status) + end + end +end \ No newline at end of file diff --git a/app/views/admins/choose_mirror_repositories/create.js.erb b/app/views/admins/choose_mirror_repositories/create.js.erb new file mode 100644 index 000000000..585ecb1af --- /dev/null +++ b/app/views/admins/choose_mirror_repositories/create.js.erb @@ -0,0 +1,5 @@ +$.notify({ message: '操作成功' },{ type: 'success' }); + +setTimeout(function(){ + window.location.reload(); +}, 500) \ No newline at end of file diff --git a/app/views/admins/choose_mirror_repositories/new.js.erb b/app/views/admins/choose_mirror_repositories/new.js.erb new file mode 100644 index 000000000..8603011ab --- /dev/null +++ b/app/views/admins/choose_mirror_repositories/new.js.erb @@ -0,0 +1,2 @@ +$('.admin-modal-container').html("<%= j( render partial: 'admins/mirror_repositories/shared/choose_mirror_modal', locals: { mirror: @mirror, new_mirror: @new_mirror } ) %>"); +$('.modal.admin-choose-mirror-modal').modal('show'); \ No newline at end of file diff --git a/app/views/admins/mirror_repositories/edit.html.erb b/app/views/admins/mirror_repositories/edit.html.erb new file mode 100644 index 000000000..7df580c96 --- /dev/null +++ b/app/views/admins/mirror_repositories/edit.html.erb @@ -0,0 +1,8 @@ +<% + define_admin_breadcrumbs do + add_admin_breadcrumb('镜像管理', admins_mirror_repositories_path) + add_admin_breadcrumb('镜像详情') + end +%> + +<%= render partial: 'admins/mirror_repositories/shared/form', locals: { mirror: @mirror, form_action: 'update' } %> \ No newline at end of file diff --git a/app/views/admins/mirror_repositories/index.html.erb b/app/views/admins/mirror_repositories/index.html.erb new file mode 100644 index 000000000..ac384408f --- /dev/null +++ b/app/views/admins/mirror_repositories/index.html.erb @@ -0,0 +1,23 @@ +<% define_admin_breadcrumbs do %> + <% add_admin_breadcrumb('镜像管理') %> +<% end %> + +
+
+ <%= link_to '新建', new_admins_mirror_repository_path, class: 'btn btn-primary' %> +
+ +<% if @error_mirror_names.present? %> +
+ 以下镜像异常: + <% @error_mirror_names.each do |mirror_name| %> + <%= mirror_name %> + <% end %> +
+<% end %> + +
+ <%= render partial: 'admins/mirror_repositories/shared/list', locals: { mirrors: @mirrors } %> +
+ +<%= render 'admins/mirror_repositories/shared/replace_mirror_modal' %> \ No newline at end of file diff --git a/app/views/admins/mirror_repositories/index.js.erb b/app/views/admins/mirror_repositories/index.js.erb new file mode 100644 index 000000000..58ccb1ef8 --- /dev/null +++ b/app/views/admins/mirror_repositories/index.js.erb @@ -0,0 +1 @@ +$('.mirror-repository-list-container').html("<%= j( render partial: 'admins/mirror_repositories/shared/list', locals: { mirrors: @mirrors } ) %>"); \ No newline at end of file diff --git a/app/views/admins/mirror_repositories/merge.js.erb b/app/views/admins/mirror_repositories/merge.js.erb new file mode 100644 index 000000000..585ecb1af --- /dev/null +++ b/app/views/admins/mirror_repositories/merge.js.erb @@ -0,0 +1,5 @@ +$.notify({ message: '操作成功' },{ type: 'success' }); + +setTimeout(function(){ + window.location.reload(); +}, 500) \ No newline at end of file diff --git a/app/views/admins/mirror_repositories/new.html.erb b/app/views/admins/mirror_repositories/new.html.erb new file mode 100644 index 000000000..792fe0857 --- /dev/null +++ b/app/views/admins/mirror_repositories/new.html.erb @@ -0,0 +1,8 @@ +<% + define_admin_breadcrumbs do + add_admin_breadcrumb('镜像管理', admins_mirror_repositories_path) + add_admin_breadcrumb('新建镜像') + end +%> + +<%= render partial: 'admins/mirror_repositories/shared/form', locals: { mirror: @mirror, form_action: 'create' } %> \ No newline at end of file diff --git a/app/views/admins/mirror_repositories/shared/_choose_mirror_modal.html.erb b/app/views/admins/mirror_repositories/shared/_choose_mirror_modal.html.erb new file mode 100644 index 000000000..99c846c70 --- /dev/null +++ b/app/views/admins/mirror_repositories/shared/_choose_mirror_modal.html.erb @@ -0,0 +1,42 @@ + \ No newline at end of file diff --git a/app/views/admins/mirror_repositories/shared/_form.html.erb b/app/views/admins/mirror_repositories/shared/_form.html.erb new file mode 100644 index 000000000..c8ab2a186 --- /dev/null +++ b/app/views/admins/mirror_repositories/shared/_form.html.erb @@ -0,0 +1,42 @@ +
+ <%= simple_form_for([:admins, mirror], url: { action: form_action }, html: { class: 'edit-mirror col-md-12' }, defaults: { wrapper_html: { class: 'col-md-4' } }) do |f| %> + <% unless mirror.new_record? %> +
+ <%= f.input :mirrorID, label: '镜像ID', input_html: { readonly: true, class: 'form-control-plaintext' } %> + <%= f.input :name, label: '镜像名称', input_html: { readonly: true, class: 'form-control-plaintext' } %> +
+ <% end %> + +
+ <%= f.input :type_name, as: :string, label: '镜像别名 *' %> + +
+ <%= f.label :main_type, label: '类别' %> + <%= f.select :main_type, [['主类别', 1],['小类别', 0]], {}, class: 'form-control optional' %> +
+
+ +
+ <%= f.input :time_limit, as: :integer, label: '评测时限(S)' %> + <%= f.input :resource_limit, as: :integer, label: '磁盘限制(K)' %> +
+ +
+ <%= f.input :cpu_limit, as: :integer, label: 'CPU限制(核)' %> + <%= f.input :memory_limit, as: :integer, label: '内存限制(M)' %> +
+ +
+ <%= f.input :description, as: :text, label: '描述', wrapper_html: { class: 'col-md-8' } %> +
+ +
+ <%= f.input :status, as: :radio_buttons, label: '状态', collection: [%w(未发布 0), %w(已发布 1)], wrapper_html: { class: 'col-md-4' } %> +
+ +
+ <%= f.button :submit, value: '保存', class: 'btn-primary mr-3 px-4', 'data-disable-with': '保存中...' %> + <%= link_to '取消', admins_mirror_repositories_path, class: 'btn btn-secondary px-4' %> +
+ <% end %> +
\ No newline at end of file diff --git a/app/views/admins/mirror_repositories/shared/_list.html.erb b/app/views/admins/mirror_repositories/shared/_list.html.erb new file mode 100644 index 000000000..b4c2df70e --- /dev/null +++ b/app/views/admins/mirror_repositories/shared/_list.html.erb @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + <% if mirrors.present? %> + <% mirrors.each do |mirror| %> + + + + + + + + + + + + <% end %> + <% else %> + <%= render 'admins/shared/no_data_for_table' %> + <% end %> + +
ID类别镜像别名镜像名称镜像描述修改时间脚本状态操作
<%= mirror.id %><%= mirror_type_tag(mirror) %><%= display_text(mirror.type_name) %><%= overflow_hidden_span mirror.name, width: 150 %><%= overflow_hidden_span mirror.description, width: 240 %><%= mirror.updated_at.strftime('%Y-%m-%d %H:%M') %> + <% if mirror.main_type == "1" %> + <%= link_to "/users/modify_script?mirror_id=#{mirror.id}", target: '_blank' do %> + + <% end %> + <% end %> + <%= mirror_status_tag mirror %> + <%= link_to '编辑', edit_admins_mirror_repository_path(mirror), class: 'action edit-action' %> + + <% if mirror.status == 2 %> + <%= link_to '同步', new_admins_choose_mirror_repository_path(mirror_id: mirror.id), remote: true, class: 'action sync-action' %> + <% end %> + + <%= javascript_void_link '替换', class: 'action replace-action', data: { toggle: 'modal', target: '.admin-replace-mirror-modal', id: mirror.id, name: mirror.name } %> + + <% if mirror.deletable? %> + <%= delete_link '删除', admins_mirror_repository_path(mirror, element: ".mirror-repository-item-#{mirror.id}"), class: 'delete-mirror-repository-action' %> + <% end %> +
+ +<%= render partial: 'admins/shared/paginate', locals: { objects: mirrors } %> \ No newline at end of file diff --git a/app/views/admins/mirror_repositories/shared/_replace_mirror_modal.html.erb b/app/views/admins/mirror_repositories/shared/_replace_mirror_modal.html.erb new file mode 100644 index 000000000..f2b2d20c8 --- /dev/null +++ b/app/views/admins/mirror_repositories/shared/_replace_mirror_modal.html.erb @@ -0,0 +1,33 @@ + \ No newline at end of file diff --git a/app/views/admins/shared/500.html.erb b/app/views/admins/shared/500.html.erb index f053f58ec..b1488d6ff 100644 --- a/app/views/admins/shared/500.html.erb +++ b/app/views/admins/shared/500.html.erb @@ -2,5 +2,5 @@
500
-
系统错误
+
<%= @message %>
\ No newline at end of file diff --git a/app/views/admins/shared/_paginate.html.erb b/app/views/admins/shared/_paginate.html.erb index b7e40e879..5c15f762c 100644 --- a/app/views/admins/shared/_paginate.html.erb +++ b/app/views/admins/shared/_paginate.html.erb @@ -1,5 +1,5 @@
- <% if objects.size.nonzero? %> + <% if objects && objects.size.nonzero? %>
<%= page_entries_info objects %>
<% end %> <%= paginate objects, views_prefix: 'admins', remote: true %> diff --git a/app/views/admins/shared/_sidebar.html.erb b/app/views/admins/shared/_sidebar.html.erb index ab30e8bd3..8c995ec45 100644 --- a/app/views/admins/shared/_sidebar.html.erb +++ b/app/views/admins/shared/_sidebar.html.erb @@ -20,6 +20,12 @@ <% end %> +
  • + <%= sidebar_item_group('#shixun-submenu', '实训管理', icon: 'window-restore') do %> +
  • <%= sidebar_item(admins_mirror_repositories_path, '镜像管理', icon: 'cubes', controller: 'admins-mirror_repositories') %>
  • + <% end %> + + <%#= sidebar_item_group('#course-submenu', '课堂+', icon: 'mortar-board') do %> diff --git a/app/views/admins/users/edit.html.erb b/app/views/admins/users/edit.html.erb index 3f9d71b96..22ac1f18e 100644 --- a/app/views/admins/users/edit.html.erb +++ b/app/views/admins/users/edit.html.erb @@ -131,7 +131,7 @@
    <%= f.button :submit, value: '保存', class: 'btn-primary mr-3 px-4' %> - <%= link_to '取消', 'javascript:history.go(-1)', class: 'btn btn-secondary px-4' %> + <%= link_to '取消', admins_users_path, class: 'btn btn-secondary px-4' %>
    <% end %>
    \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 7e2cf72f3..285bb7e1a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -786,6 +786,13 @@ Rails.application.routes.draw do post :refuse end end + resources :mirror_repositories, only: [:index, :new, :create, :edit, :update, :destroy] do + collection do + post :merge + get :for_select + end + end + resources :choose_mirror_repositories, only: [:new, :create] end #git 认证回调 diff --git a/public/react/src/common/UrlTool.js b/public/react/src/common/UrlTool.js index e1c9f0c74..12505186a 100644 --- a/public/react/src/common/UrlTool.js +++ b/public/react/src/common/UrlTool.js @@ -1,10 +1,10 @@ const isDev = window.location.port == 3007; -export const TEST_HOST = "http://pre-newweb.educoder.net" +export const TEST_HOST = "https://pre-newweb.educoder.net" export function getImageUrl(path) { // https://www.educoder.net // https://testbdweb.trustie.net // const local = 'http://localhost:3000' - const local = 'http://pre-newweb.educoder.net' + const local = 'https://pre-newweb.educoder.net' if (isDev) { return `${local}/${path}` } @@ -12,7 +12,7 @@ export function getImageUrl(path) { } export function setImagesUrl(path){ - const local = 'http://pre-newweb.educoder.net' + const local = 'https://pre-newweb.educoder.net' let firstStr=path.substr(0,1); // console.log(firstStr); if(firstStr=="/"){ @@ -31,7 +31,7 @@ export function getUrl(path, goTest) { // testbdweb.educoder.net testbdweb.trustie.net // const local = goTest ? 'https://testeduplus2.educoder.net' : 'http://localhost:3000' // const local = 'https://testeduplus2.educoder.net' - const local = 'http://pre-newweb.educoder.net' + const local = 'https://pre-newweb.educoder.net' if (isDev) { return `${local}${path?path:''}` } diff --git a/public/react/src/modules/courses/busyWork/CommonWorkAppraise.js b/public/react/src/modules/courses/busyWork/CommonWorkAppraise.js index ae40fedda..e24892680 100644 --- a/public/react/src/modules/courses/busyWork/CommonWorkAppraise.js +++ b/public/react/src/modules/courses/busyWork/CommonWorkAppraise.js @@ -271,11 +271,11 @@ class CommonWorkAppraise extends Component{ {work_members.map((item, index) => { return {isAdmin ? - this.props.toWorkDetailPage(this.props.match.params, null, item.work_id)} > {item.user_name} - : {item.user_name}} + : {item.user_name}} {item.is_leader && } })} diff --git a/public/react/src/modules/courses/busyWork/CommonWorkList.js b/public/react/src/modules/courses/busyWork/CommonWorkList.js index 28e54fff3..ca550a965 100644 --- a/public/react/src/modules/courses/busyWork/CommonWorkList.js +++ b/public/react/src/modules/courses/busyWork/CommonWorkList.js @@ -430,7 +430,7 @@ class CommonWorkList extends Component{ } _getRequestParams() { - const { search, arg_work_status, arg_teacher_comment, arg_course_group, order, page } = this.state + const { search, arg_work_status, arg_teacher_comment, arg_course_group, order, page, arg_member_work } = this.state return { page, search, @@ -441,6 +441,7 @@ class CommonWorkList extends Component{ limit: PAGE_SIZE, b_order: orderMap[order], group_id:arg_course_group, + member_work: arg_member_work } } fetchData = () => { @@ -488,6 +489,11 @@ class CommonWorkList extends Component{ this.fetchList() }) } + memberWorkChange = (values, isAllChecked) => { + this.setState({arg_member_work: isAllChecked ? '' : values[0], page: 1}, () => { + this.fetchList() + }) + } funorder = (order) => { this.setState({ order }, () => { this.fetchList() @@ -581,10 +587,21 @@ class CommonWorkList extends Component{ return { label: `${item.name}(${item.count})`, value: item.id } }) + // 1:组长, 0:组员,“” 不限 + const member_works = [{ + name: '组长', id: 1 + }, { + name: '组员', id: 0 + }] + const options_member_work = member_works.map((item) => { + return { label: `${item.name}`, value: item.id } + }) + const isAdmin = this.props.isAdmin() const isStudent = this.props.isStudent() const isAdminOrStudent = this.props.isAdminOrStudent() + const isGroup = this.props.isGroup(); // work_group let StudentData; @@ -679,6 +696,7 @@ class CommonWorkList extends Component{ {options_course_group.length > 1 && } + {isGroup && } {/* value={search} */} diff --git a/public/react/src/modules/courses/busyWork/CommonWorkPost.js b/public/react/src/modules/courses/busyWork/CommonWorkPost.js index 7e916e4cd..d2c1ca01e 100644 --- a/public/react/src/modules/courses/busyWork/CommonWorkPost.js +++ b/public/react/src/modules/courses/busyWork/CommonWorkPost.js @@ -1,12 +1,14 @@ import React,{Component} from "React"; import { Form, Select, Input, Button,Checkbox,Upload,Icon,message,Modal,Tooltip} from "antd"; import {Link} from 'react-router-dom'; -import TPMMDEditor from '../../tpm/challengesnew/TPMMDEditor'; -import { WordsBtn, getUploadActionUrl, appendFileSizeToUploadFile, appendFileSizeToUploadFileAll } from 'educoder'; import axios from 'axios'; -import Modals from '../../modals/Modals'; import _ from 'lodash' +import { WordsBtn, getUploadActionUrl, appendFileSizeToUploadFile, appendFileSizeToUploadFileAll } from 'educoder'; +import Modals from '../../modals/Modals'; +import TPMMDEditor from '../../tpm/challengesnew/TPMMDEditor'; +import LeaderIcon from './common/LeaderIcon' + const Search = Input.Search; const CheckboxGroup = Checkbox.Group; @@ -757,6 +759,8 @@ render(){ text-overflow:ellipsis; white-space:nowrap } + .members .leaderIcon { + } `}
    @@ -768,15 +772,21 @@ render(){ display:item.user_name===undefined?"none":"" }}> -
    {item.user_name}
    +
    + {item.user_name}{ (item.is_leader || !this.isEdit && key==0) && } +
    +
    9 ? item.group_name : ''} >{item.group_name}
    12 ? item.student_id : ''} >{item.student_id}
    - {item.user_id != this.props.current_user.user_id ?
    this.delecttask_status(item.user_id)}>
    :""} + {item.user_id != this.props.current_user.user_id ? +
    this.delecttask_status(item.user_id)}>
    :""} +
    ) })} diff --git a/public/react/src/modules/courses/busyWork/common/LeaderIcon.js b/public/react/src/modules/courses/busyWork/common/LeaderIcon.js index 526e165c5..20ecce43f 100644 --- a/public/react/src/modules/courses/busyWork/common/LeaderIcon.js +++ b/public/react/src/modules/courses/busyWork/common/LeaderIcon.js @@ -2,8 +2,10 @@ import React,{Component} from "React"; export default function LeaderIcon(props = {}) { let icon = null; + const { className, style } = props; + const _className = `font-8 blueFull Actionbtn ${className}` if (props.small) { - icon =
    组长
    } else { - icon =
    组长
    + icon =
    组长
    } return icon diff --git a/public/react/src/modules/courses/members/modal/CreateGroupByImportModal.js b/public/react/src/modules/courses/members/modal/CreateGroupByImportModal.js index d5e77da9d..9b3d72e00 100644 --- a/public/react/src/modules/courses/members/modal/CreateGroupByImportModal.js +++ b/public/react/src/modules/courses/members/modal/CreateGroupByImportModal.js @@ -45,6 +45,7 @@ class CreateGroupByImportModal extends Component{ .then((response) => { if (response.data.status == 0) { this.props.showNotification(response.data.message) + this.props.createGroupImportSuccess && this.props.createGroupImportSuccess() this.setVisible(false) } else { diff --git a/public/react/src/modules/courses/members/studentsList.js b/public/react/src/modules/courses/members/studentsList.js index cc7485101..3462438f8 100644 --- a/public/react/src/modules/courses/members/studentsList.js +++ b/public/react/src/modules/courses/members/studentsList.js @@ -234,6 +234,9 @@ class studentsList extends Component{ off('updateNavSuccess', this.updateNavSuccess) } } + createGroupImportSuccess = () => { + this.props.updataleftNavfun() + } updateNavSuccess = () => { this.fetchCourseGroups() } @@ -556,7 +559,9 @@ class studentsList extends Component{ firstRowRight={ { isSuperAdmin && - + this.refs['createGroupByImportModal'].setVisible(true)}>导入创建分班 } { isAdmin && isParent && this.addDir()}>添加分班 } diff --git a/public/react/src/modules/courses/members/teacherList.js b/public/react/src/modules/courses/members/teacherList.js index 06dbe74a8..e3fd4a98d 100644 --- a/public/react/src/modules/courses/members/teacherList.js +++ b/public/react/src/modules/courses/members/teacherList.js @@ -42,6 +42,7 @@ function buildColumns(that) { title: '序号', dataIndex: 'name', key: 'index', + width: 78, render: (content, item, index) => { return index + 1 // return item.isApply == true ? '' : {(that.state.page - 1) * 20 + index + 1 @@ -74,6 +75,7 @@ function buildColumns(that) { dataIndex: 'role', key: 'role', sorter: showSorter, + width: 86, // 'ascend' | 'descend' defaultSortOrder: 'ascend', sortDirections: sortDirections, @@ -160,16 +162,16 @@ function buildColumns(that) { }, }) } - if(isAdminOrTeacher && hasGraduationModule) { - columns.unshift({ - title: '', - dataIndex: 'course_member_id', - key: 'course_member_id', - render: (content, item, index) => { - return content ? : '' - } - }) - } + // if(isAdminOrTeacher && hasGraduationModule) { + // columns.unshift({ + // title: '', + // dataIndex: 'course_member_id', + // key: 'course_member_id', + // render: (content, item, index) => { + // return content ? : '' + // } + // }) + // } return columns } diff --git a/public/react/src/modules/courses/new/CoursesNew.js b/public/react/src/modules/courses/new/CoursesNew.js index edee26a27..cbcb33fc2 100644 --- a/public/react/src/modules/courses/new/CoursesNew.js +++ b/public/react/src/modules/courses/new/CoursesNew.js @@ -45,10 +45,11 @@ class CoursesNew extends Component { fetching:false, boolxinjian:false, checkboxgroup:undefined, + addonAfteronelenone:0, + addonAfteronelentwo:0, checkbofrup:[{module_type:"shixun_homework",module_name:"实训作业"},{module_type:"common_homework",module_name:"普通作业"},{module_type:"group_homework",module_name:"分组作业"} ,{module_type:"exercise",module_name:"试卷"},{module_type:"poll",module_name:"问卷"},{module_type:"graduation",module_name:"毕业设计"} ,{module_type:"board",module_name:"讨论"},{module_type:"attachment",module_name:"资源"},{module_type:"course_group",module_name:"分班"}], - checkbofrups:[], } } componentDidMount() { @@ -66,8 +67,8 @@ class CoursesNew extends Component { this.props.form.setFieldsValue({ course: data.course_list_name, classroom: data.name, - period: data.class_period, - credit: data.credit, + period: data.class_period===undefined?'':data.class_period===null?'':data.class_period===null?'':data.class_period==="null"?'':data.class_period+"", + credit: data.credit===undefined?'':data.credit===null?'':data.credit===null?'':data.credit==="null"?'':data.credit+"", checkboxgroup: data.course_module_types, Realnamecertification: data.authentication, Professionalcertification:data.professional_certification, @@ -81,7 +82,9 @@ class CoursesNew extends Component { is_public: data.is_public === 1 ? true : false, Realnamecertification: data.authentication, Professionalcertification:data.professional_certification, - // checkbofrups:data.course_modules, + addonAfteronelenone: data.class_period===undefined?'':data.class_period===null?'':data.class_period===null?'':data.class_period==="null"?'':data.class_period, + addonAfteronelentwo:data.credit===undefined?'':data.credit===null?'':data.credit===null?'':data.credit==="null"?'':data.credit, + }); // try { // if(data.course_modules===undefined||data.course_modules.length===0){ @@ -94,6 +97,9 @@ class CoursesNew extends Component { // checkbofrups:this.state.checkbofrup, // }); // } + + + this.handleSearchschool(data.school); }).catch((error) => { console.log(error); @@ -469,10 +475,20 @@ class CoursesNew extends Component { const optionschool = this.state.searchlistscholl===undefined?"":this.state.searchlistscholl===null?"":this.state.searchlistscholl==="[]"?"":this.state.searchlistscholl.map(z => ); // console.log(this.props.current_user.user_school) // form合并了 - console.log("获取到的数据"); - console.log(this.state); - console.log(this.props); - console.log(this.props.current_user); + // console.log("获取到的数据"); + // console.log(this.state); + // console.log(this.props); + // console.log(this.props.current_user); + var addonAfterone=this.props.form&&this.props.form.getFieldValue('period'); + var addonAfteronelen=0; + if(addonAfterone){ + addonAfteronelen=String(addonAfterone).length; + } + var addonAftertwo=this.props.form&&this.props.form.getFieldValue('credit'); + var addonAfteronelens=0; + if(addonAftertwo){ + addonAfteronelens=String(addonAftertwo).length; + } return ( @@ -630,6 +646,23 @@ class CoursesNew extends Component { } `} + { - return event.target.value.replace(/\D/g,'') - }} + pattern: new RegExp(/^[0-9]+([.]{1}[0-9]+){0,1}$/, "g"), + message: '必须是数值' + }, + { + max:5, + message: '不能超过5个字符', + }]} )( - + )} @@ -656,15 +689,16 @@ class CoursesNew extends Component { {getFieldDecorator("credit", { rules:[{ - required:false, - pattern: new RegExp(/^[0-9]\d*$/, "g"), - message: '' - }], - getValueFromEvent: (event) => { - return event.target.value.replace(/\D/g,'') - }} + pattern: new RegExp(/^[0-9]+([.]{1}[0-9]+){0,1}$/, "g"), + message: '必须是数值' + }, + { + max:5, + message: '不能超过5个字符', + } + ]} )( - + )} - {getFieldDecorator("checkboxgroup", { initialValue: [ "shixun_homework", "common_homework", "group_homework", "exercise", "attachment", "course_group", diff --git a/public/react/src/modules/courses/new/Goldsubject.js b/public/react/src/modules/courses/new/Goldsubject.js index 80530c7ed..5149d3e5a 100644 --- a/public/react/src/modules/courses/new/Goldsubject.js +++ b/public/react/src/modules/courses/new/Goldsubject.js @@ -27,7 +27,9 @@ function disabledDateTime() { disabledMinutes: () => range(1, 30).concat(range(31, 60)), }; } - +function disabledDate(current) { + return current && current < moment().endOf('day').subtract(1, 'days'); +} // function disabledDate(current) { // console.log(current); // return current && current < moment().endOf('day').subtract(1, 'days'); @@ -49,13 +51,15 @@ class Goldsubject extends Component { fetching:false, subject_id:"", start_date:"", + addonAfteronelenone:"", + addonAfteronelentwo:"", Whethertocreateanewclassroom:true, checkbofrup:[ {module_type:"announcement",module_name:"公告栏"},{module_type:"online_learning",module_name:"在线学习"} ,{module_type:"shixun_homework",module_name:"实训作业"},{module_type:"common_homework",module_name:"普通作业"} ,{module_type:"exercise",module_name:"试卷"},{module_type:"poll",module_name:"问卷"} ,{module_type:"attachment",module_name:"资源"},{module_type:"board",module_name:"讨论"},{module_type:"course_group",module_name:"分班"},], - checkbofrups:[], + } } // disabledEndDate= endValue => { @@ -121,8 +125,8 @@ class Goldsubject extends Component { this.props.form.setFieldsValue({ course: data.course_list_name, classroom: data.name, - period: data.class_period, - credit: data.credit, + period: data.class_period===undefined?'':data.class_period===null?'':data.class_period===null?'':data.class_period==="null"?'':data.class_period+"", + credit: data.credit===undefined?'':data.credit===null?'':data.credit===null?'':data.credit==="null"?'':data.credit+"", checkboxgroup: data.course_module_types, Realnamecertification: data.authentication, Professionalcertification:data.professional_certification, @@ -140,22 +144,13 @@ class Goldsubject extends Component { Professionalcertification:data.professional_certification, name: data.name, class_period: data.class_period, - credit: parseFloat(data.credit), + addonAfteronelenone: data.class_period===undefined?'':data.class_period===null?'':data.class_period===null?'':data.class_period==="null"?'':data.class_period, + credit: parseFloat(data.credit), + addonAfteronelentwo:data.credit===undefined?'':data.credit===null?'':data.credit===null?'':data.credit==="null"?'':data.credit, course_module_types: data.course_module_types, school:data.school, Whethertocreateanewclassroom:false, }); - // try { - // if(data.course_modules===undefined||data.course_modules.length===0){ - // this.setState({ - // checkbofrups:this.state.checkbofrup, - // }); - // } - // }catch (e) { - // this.setState({ - // checkbofrups:this.state.checkbofrup, - // }); - // } this.handleSearchschool(data.school); }).catch((error) => { console.log(error); @@ -607,7 +602,7 @@ class Goldsubject extends Component { this.applyForAddOrgForm.setVisible(true) } render() { - let {datatime,datatimetwo,school,searchlistscholl,Whethertocreateanewclassroom} = this.state; + let {datatime,datatimetwo,school,searchlistscholl,Whethertocreateanewclassroom,addonAfteronelenone,addonAfteronelentwo} = this.state; const {getFieldDecorator} = this.props.form; const propsWithoutForm = Object.assign({}, this.props) delete propsWithoutForm.form @@ -619,6 +614,18 @@ class Goldsubject extends Component { // console.log(this.state); // console.log(this.props); // console.log(this.props.current_user); + var addonAfterone=this.props.form&&this.props.form.getFieldValue('period'); + var addonAfteronelen=0; + if(addonAfterone){ + addonAfteronelen=String(addonAfterone).length; + } + var addonAftertwo=this.props.form&&this.props.form.getFieldValue('credit'); + var addonAfteronelens=0; + if(addonAftertwo){ + addonAfteronelens=String(addonAftertwo).length; + } + console.log(addonAfteronelenone); + console.log(addonAfteronelentwo); return ( @@ -786,6 +793,23 @@ class Goldsubject extends Component { } `} + { - return event.target.value.replace(/\D/g,'') - }} + pattern: new RegExp(/^[0-9]+([.]{1}[0-9]+){0,1}$/, "g"), + message: '必须是数值' + }, + { + max:5, + message: '不能超过5个字符', + }]} )( - + )} - { - return event.target.value.replace(/\D/g,'') - }} + pattern: new RegExp(/^[0-9]+([.]{1}[0-9]+){0,1}$/, "g"), + message: '必须是数值' + }, + { + max:5, + message: '不能超过5个字符', + } + ]} )( - + )} {getFieldDecorator("endtime", { - rules: [{type: 'object',required: true, message: "结束时间不能为空"}], + rules: [{type: 'object', + required: true, message: "结束时间不能为空"}], })(