diff --git a/app/assets/javascripts/backbone/models/advisory.js b/app/assets/javascripts/backbone/models/advisory.js index 654ac09c6..74e15fb21 100644 --- a/app/assets/javascripts/backbone/models/advisory.js +++ b/app/assets/javascripts/backbone/models/advisory.js @@ -3,7 +3,91 @@ Rosa.Models.Advisory = Backbone.Model.extend({ id: null, description: null, references: null, - update_type: null + update_type: null, + found: false + }, + + initialize: function() { + _.bindAll(this, 'findByAdvisoryID'); + + this.url = '/advisories'; + }, + + findByAdvisoryID: function(id, bl_type, options) { + var self = this; + + var urlError = function() { + throw new Error("A 'url' property or function must be specified"); + }; + + var typeError = function() { + throw new Error("A 'bl_type' must be 'security' or 'bugfix'"); + }; + + var idError = function() { + throw new Error("A 'id' must be a string at least 4 characters long"); + }; + + if ( (typeof(id) != "string") || (id.length < 4) ) { + idError(); + } + + if ( (bl_type == undefined) || (bl_type == null) || ((bl_type != 'security') && (bl_type != 'bugfix')) ) { + typeError(); + } + + options |= {}; + var data = _.extend({ + query: id, + bl_type: bl_type + }, {}); + + var params = _.extend({ + type: 'GET', + dataType: 'json', + beforeSend: function( xhr ) { + var token = $('meta[name="csrf-token"]').attr('content'); + if (token) xhr.setRequestHeader('X-CSRF-Token', token); + + self.trigger('search:start'); + } + }, options); + + if (!params.url) { + params.url = ((_.isFunction(this.url) ? this.url() : this.url) + '/search') || urlError(); + } + + params.data = data; + + var complete = options.complete; + params.complete = function(jqXHR, textStatus) { + //console.log(jqXHR); + + switch (jqXHR.status) { + case 200: + self.set(_.extend({ + found: true + }, JSON.parse(jqXHR.responseText)), {silent: true}); + self.trigger('search:end'); + break + + case 404: + self.set(self.defaults, {silent: true}); + self.trigger('search:end'); + break + + default: + self.set(self.defaults, {silent: true}); + self.trigger('search:failed'); + } + + if (complete) complete(jqXHR, textStatus); + } + + $.ajax(params); + + return this; + } }); diff --git a/app/assets/javascripts/backbone/routers/build_lists_advisories_router.js b/app/assets/javascripts/backbone/routers/build_lists_advisories_router.js index bf20ecfa0..950db332d 100644 --- a/app/assets/javascripts/backbone/routers/build_lists_advisories_router.js +++ b/app/assets/javascripts/backbone/routers/build_lists_advisories_router.js @@ -2,8 +2,7 @@ Rosa.Routers.BuildListsAdvisoriesRouter = Backbone.Router.extend({ routes: {}, initialize: function() { - this.advisoriesCollection = new Rosa.Collections.AdvisoriesCollection(Rosa.bootstrapedData.advisories); - this.advisoriesView = new Rosa.Views.BuildListAdvisoriesView({ collection: this.advisoriesCollection }); + this.advisoriesView = new Rosa.Views.BuildListAdvisoriesView({ model: new Rosa.Models.Advisory() }); this.advisoriesView.render(); } diff --git a/app/assets/javascripts/backbone/views/build_list_advisories_view.js b/app/assets/javascripts/backbone/views/build_list_advisories_view.js index 13cd34d56..260c8e60d 100644 --- a/app/assets/javascripts/backbone/views/build_list_advisories_view.js +++ b/app/assets/javascripts/backbone/views/build_list_advisories_view.js @@ -1,37 +1,42 @@ Rosa.Views.BuildListAdvisoriesView = Backbone.View.extend({ initialize: function() { - _.bindAll(this, 'popoverTitle', 'popoverDesc', 'showAdvisory', - 'changeAdvisoryList', 'showPreview', 'showForm', 'hideAll'); - $('.chzn-select').chosen(); - this.$el = $('#advisory_block'); - this._$form = this.$('#new_advisory_form'); - this._$preview = this.$('#advisory_preview'); - this._$type_select = $('#build_list_update_type'); - this._$selector = this.$('#attach_advisory'); + _.bindAll(this, 'showAdvisory', 'showPreview', 'showForm', + 'showSearch', 'hideAll', 'displayStatus', 'processSearch'); + this.$el = $('#advisory_block'); + this._$type_select = $('#build_list_update_type'); + this._$publish_button = $('input[type="submit"][name="publish"]'); + + this._$form = this.$('#new_advisory_form'); + this._$preview = this.$('#advisory_preview'); + + this._$search = this.$('#advisory_search_block'); + this._$search_field = this.$('#advisory_search'); + this._$not_found = this.$('#advisory_search_block > .advisory_not_found'); + this._$server_error = this.$('#advisory_search_block > .server_error'); + this._$continue_input = this.$('#advisory_search_block > .continue_input'); + this._search_timer = null; + + this._$selector = this.$('#attach_advisory'); + this._header_text = this._$preview.children('h3').html(); this._$selector.on('change', this.showAdvisory); - this._$type_select.on('change', this.changeAdvisoryList); + this._$search_field.on('input keyup', this.processSearch); + var self = this; + this._$type_select.on('change', function() { + self._$search_field.trigger('input'); + }); + + this.model.on('search:start', function() { + this._$publish_button.prop({disabled: true}); + }, this); + this.model.on('search:end', this.showPreview, this); + this.model.on('search:failed', this.handleSearchError, this); }, - changeAdvisoryList: function() { - this.$('.popoverable').hide(); - this.$('.popoverable.' + this._$type_select.val()).show(); - this._$selector.val('no').trigger("liszd:updated").trigger('change'); - }, - - popoverTitle: function(el) { - console.log(el); - console.log(el.html()); - return el.html(); - }, - - popoverDesc: function(el) { - return this.collection.get(el.html()).get('popover_desc'); - }, - - showAdvisory: function(el) { + showAdvisory: function(ev) { var adv_id = this._$selector.val(); + this._$publish_button.prop({disabled: false}); switch (adv_id) { case 'no': this.hideAll(); @@ -40,21 +45,70 @@ Rosa.Views.BuildListAdvisoriesView = Backbone.View.extend({ this.showForm(); break default: - this.showPreview(adv_id); + this.showSearch(); + this._$publish_button.prop({disabled: true}); } }, + processSearch: function(ev) { + if (ev.type == "keyup") { + if (ev.keyCode != 13) { + return + } else { + ev.preventDefault(); + } + } + + var TIMER_INTERVAL = 500; + + var self = this; + + var timerCallback = function() { + if (self._$search_field.val().length > 3) { + // real search + self.model.findByAdvisoryID(self._$search_field.val(), self._$type_select.val()); + } else { + // hide preview if nothing to show + if (self._$preview.is(':visible')) { + self._$preview.slideUp(); + } + self.displayStatus('found'); + } + }; + + if (this.model.get('advisory_id') == this._$search_field.val()) { + this.showPreview(); + return; + } + // timeout before real AJAX request + clearTimeout(this._search_timer); + this._search_timer = setTimeout(timerCallback, TIMER_INTERVAL); + }, + showPreview: function(id) { + this._$publish_button.prop({disabled: false}); if (this._$form.is(':visible')) { this._$form.slideUp(); } - var adv = this.collection.get(id); var prev = this._$preview; - prev.children('h3').html(this._header_text + ' ' + adv.get('advisory_id')); - prev.children('.descr').html(adv.get('description')); - prev.children('.refs').html(adv.get('references')); - if (!this._$preview.is(':visible')) { - this._$preview.slideDown(); + var adv = this.model; + if (adv.get('found')) { + this._$selector.children('option.advisory_id').val(adv.get('advisory_id')); + + prev.children('h3').html(this._header_text + ' ' + adv.get('advisory_id')); + prev.children('.descr').html(adv.get('description')); + prev.children('.refs').html(adv.get('references')); + if (!this._$preview.is(':visible')) { + this._$preview.slideDown(); + } + this.displayStatus('found'); + } else { + if (this._$preview.is(':visible')) { + this._$preview.slideUp(); + } + this._$publish_button.prop({disabled: true}); + this.displayStatus('not_found'); + this._$selector.children('option.advisory_id').val(''); } }, @@ -62,12 +116,27 @@ Rosa.Views.BuildListAdvisoriesView = Backbone.View.extend({ if (this._$preview.is(':visible')) { this._$preview.slideUp(); } + if (this._$search.is(':visible')) { + this._$search.slideUp(); + } if (!this._$form.is(':visible')) { this._$form.slideDown(); } }, - hideAll: function() { + showSearch: function() { + if (this._$form.is(':visible')) { + this._$form.slideUp(); + } + if (!this._$search.is(':visible')) { + this._$search.slideDown(); + this._$search_field.trigger('input'); + } + }, + + handleSearchError: function() { + this._$publish_button.prop({disabled: true}); + this.displayStatus('error'); if (this._$preview.is(':visible')) { this._$preview.slideUp(); } @@ -76,14 +145,33 @@ Rosa.Views.BuildListAdvisoriesView = Backbone.View.extend({ } }, + hideAll: function() { + if (this._$preview.is(':visible')) { + this._$preview.slideUp(); + } + if (this._$search.is(':visible')) { + this._$search.slideUp(); + } + if (this._$form.is(':visible')) { + this._$form.slideUp(); + } + }, + + displayStatus: function(st) { + var ELEMS = { + 'found': this._$continue_input, + 'not_found': this._$not_found, + 'error': this._$server_error + }; + + this._$continue_input.hide(); + this._$not_found.hide(); + this._$server_error.hide(); + + ELEMS[st].show(); + }, + render: function() { - var title = this.popoverTitle; - var description = this.popoverDesc; - this.changeAdvisoryList(); - this.$('.popoverable').popover({ - title: function() { return title($(this)); }, - content: function() { return description($(this)); } - }); this.showAdvisory(); return this; } diff --git a/app/assets/stylesheets/design/custom.scss b/app/assets/stylesheets/design/custom.scss index 9746f9b30..65fd753f7 100644 --- a/app/assets/stylesheets/design/custom.scss +++ b/app/assets/stylesheets/design/custom.scss @@ -954,10 +954,55 @@ form.mass_build input[type="checkbox"] { height: 11px; } -div#new_advisory_form, div#advisory_preview { +div#new_advisory_form, +div#advisory_preview, +div#advisory_search_block, +div#advisory_search_block div.info { display: none; } +div#advisory_search_block { + padding-bottom: 15px; +} + +p.hint_text { + color: #666666; + font-size: 0.9em; + padding: 0; +} + +div#advisory_search_block p.hint_text { + display: block; + width: 350px; +} + +div#advisory_search_block div.info { + width: 565px; + border: solid 1px; + border-radius: 5px; +} + +div#advisory_search_block div.info p { + text-align: center; + margin: 0.5em 2em 0.7em; +} + +div#advisory_search_block div.advisory_not_found { + background-color: #B7CFFF; + border-color: #6666FF; +} + +div#advisory_search_block div.server_error { + background-color: #FACFCF; + border-color: #FF7777; +} + +div#advisory_search_block div.continue_input { + background-color: #CFFACF; + border-color: #00CF00; + display: block; +} + /*=============== popovers ===============*/ .popover { diff --git a/app/controllers/advisories_controller.rb b/app/controllers/advisories_controller.rb index c0a6dbe23..3440fb3d0 100644 --- a/app/controllers/advisories_controller.rb +++ b/app/controllers/advisories_controller.rb @@ -25,4 +25,13 @@ class AdvisoriesController < ApplicationController end end + def search + puts params[:bl_type] + @advisory = Advisory.by_update_type(params[:bl_type]).search_by_id(params[:query]).limit(1).first + raise ActionController::RoutingError.new('Not Found') if @advisory.nil? + respond_to do |format| + format.json { render :json => @advisory } + end + end + end diff --git a/app/helpers/advisories_helper.rb b/app/helpers/advisories_helper.rb index 62a940bb7..42634b590 100644 --- a/app/helpers/advisories_helper.rb +++ b/app/helpers/advisories_helper.rb @@ -1,9 +1,13 @@ # -*- encoding : utf-8 -*- module AdvisoriesHelper def advisories_select_options(advisories, opts = {:class => 'popoverable'}) - def_values = [[t("layout.advisories.no_"), 'no'], [t("layout.advisories.new"), 'new']] - options_for_select(def_values, def_values.first) + - options_for_select(advisories.map { |a| [a.advisory_id, :class => "#{opts[:class]} #{a.update_type}"] }) + def_values = [[t("layout.advisories.no_"), 'no'], [t("layout.advisories.new"), 'new'], [t("layout.advisories.existing"), 'existing', {:class => 'advisory_id'}]] + options_for_select(def_values, def_values.first) + end + + def advisory_id_for_hint + sprintf(Advisory::ID_STRING_TEMPLATE, :type => "{#{Advisory::TYPES.values.join(',')}}", + :year => 'YYYY', :id => 'XXXX') end def construct_ref_link(ref) diff --git a/app/models/advisory.rb b/app/models/advisory.rb index facedbb91..636ba519e 100644 --- a/app/models/advisory.rb +++ b/app/models/advisory.rb @@ -8,11 +8,12 @@ class Advisory < ActiveRecord::Base after_create :generate_advisory_id before_save :normalize_references, :if => :references_changed? - ID_TEMPLATE = 'ROSA-%s-%d:%04d' + ID_TEMPLATE = 'ROSA-%s-%d:%04d' + ID_STRING_TEMPLATE = 'ROSA-%s-%04s:%04s' TYPES = {'security' => 'SA', 'bugfix' => 'A'} - scope :by_project, lambda {|p| where('project_id' => p.try(:id) || p)} scope :search_by_id, lambda { |aid| where('advisory_id ILIKE ?', "%#{aid.to_s.strip}%") } + scope :by_update_type, lambda { |ut| where(:update_type => ut) } default_scope order('created_at DESC') def to_param diff --git a/app/views/projects/build_lists/show.html.haml b/app/views/projects/build_lists/show.html.haml index afdc6524f..2861b8ae7 100644 --- a/app/views/projects/build_lists/show.html.haml +++ b/app/views/projects/build_lists/show.html.haml @@ -32,7 +32,7 @@ .leftlist= t("activerecord.attributes.build_list.update_type") .rightlist - if @build_list.can_publish? and can?(:publish, @build_list) - = f.select :update_type, options_for_select(BuildList::RELEASE_UPDATE_TYPES, @build_list.update_type), {}, :class => 'chzn-select' + = f.select :update_type, options_for_select(BuildList::RELEASE_UPDATE_TYPES, @build_list.update_type) - else = @build_list.update_type .both @@ -75,9 +75,23 @@ #advisory_block .leftlist= label_tag :attach_advisory, t("layout.build_lists.attached_advisory") .rightlist - = select_tag :attach_advisory, advisories_select_options(@advisories), :class => 'chzn-select' + = select_tag :attach_advisory, advisories_select_options(@advisories) .both + #advisory_search_block + %h3= t("layout.advisories.search_by_id") + .leftlist= label_tag :advisory_search, t("layout.advisories.search_hint") + .rightlist + %input#advisory_search{:type => 'text'} + %p.hint_text= t("layout.advisories.advisory_id_info", :advisory_format => advisory_id_for_hint) + .both + .info.advisory_not_found + %p= t("layout.advisories.banners.advisory_not_found") + .info.server_error + %p= t("layout.advisories.banners.server_error") + .info.continue_input + %p= t("layout.advisories.banners.continue_input") + #new_advisory_form = f.fields_for @build_list.build_advisory do |f| = render :partial => 'advisories/form', :locals => {:f => f} @@ -92,11 +106,8 @@ .leftlist= t("activerecord.attributes.advisory.references") .rightlist.refs   .both - :javascript $(function() { - Rosa.bootstrapedData.advisories = #{ render 'advisories/advisories.json.jbuilder', - :advisories => @advisories }; var r = new Rosa.Routers.BuildListsAdvisoriesRouter(); }); @@ -108,7 +119,6 @@ - if @item_groups.blank? %h4.nomargin= t("layout.build_lists.no_items_data") - @item_groups.each_with_index do |group, level| - -#%h4.nomargin= "#{group} ##{level}" - group.each do |item| %h4.nomargin= "#{item.name} ##{level}" %table.tablesorter.width565{:cellpadding => "0", :cellspacing => "0"} diff --git a/config/locales/models/advisory.en.yml b/config/locales/models/advisory.en.yml index 40e2228a0..55e3befbe 100644 --- a/config/locales/models/advisory.en.yml +++ b/config/locales/models/advisory.en.yml @@ -10,7 +10,15 @@ en: ref_comment: Add links one by row no_: No new: New + existing: Existing search_by_id: Search advisory by it's ID + search_hint: Paste full AdvisoryID into text field or enter there its uniq part. + advisory_id_info: AdvisoryID is a string %{advisory_format}, where 'XXXX' (at least 4 symbols) is a uniq part. + + banners: + advisory_not_found: Couldn't find advisory with given ID for this type of Build List. + server_error: Server problem. Please try again later. + continue_input: Continue input while needed Advisory appears in preview. flash: advisories: diff --git a/config/locales/models/advisory.ru.yml b/config/locales/models/advisory.ru.yml index 1cb0e5a4b..e9a12b74d 100644 --- a/config/locales/models/advisory.ru.yml +++ b/config/locales/models/advisory.ru.yml @@ -10,7 +10,15 @@ ru: ref_comment: Вставляйте ссылки по одной на строку no_: Нет new: Новый + existing: Существующий search_by_id: Искать бюллетень по его ID + search_hint: Скопируйте в поле ввода полный AdvisoryID или введите его уникальную часть + advisory_id_info: AdvisoryID имеет формат %{advisory_format}, где 'XXXX' (минимум 4 символа) - это уникальная часть. + + banners: + advisory_not_found: Не удалось найти запрашиваемый бюллетень для сборочного листа этого типа. + server_error: Произошла ошибка сервера. Попробуйте позже. + continue_input: Продолжайте вводить ID до тех пор, пока не найдется нужный бюллетень. flash: advisories: diff --git a/config/routes.rb b/config/routes.rb index 569a0f939..385e002e0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -38,7 +38,9 @@ Rosa::Application.routes.draw do end end - resources :advisories, :only => [:index, :show] + resources :advisories, :only => [:index, :show, :search] do + get :search, :on => :collection + end scope :module => 'platforms' do resources :platforms do diff --git a/vendor/assets/javascripts/vendor.js b/vendor/assets/javascripts/vendor.js index aafbbfbed..9d833de06 100644 --- a/vendor/assets/javascripts/vendor.js +++ b/vendor/assets/javascripts/vendor.js @@ -10,8 +10,8 @@ //= require bootstrap-modal //= require bootstrap-button //= require bootstrap-dropdown -//= require bootstrap-tooltip -//= require bootstrap-popover +// require bootstrap-tooltip +// require bootstrap-popover //= require chosen.jquery // require html5shiv // require_tree .