From abe2b8a30d45f9ea1b8d7e4f58319f3a9a576a2b Mon Sep 17 00:00:00 2001 From: George Vinogradov Date: Mon, 4 Jun 2012 23:49:20 +0400 Subject: [PATCH] [issue #428] Work with existing advisories. * Added advisories page to platform * Added attacing of existing advisories to BuildList. --- app/assets/javascripts/application.js | 2 +- .../javascripts/backbone/models/advisory.js | 12 + .../routers/build_lists_advisories_router.js | 10 + .../views/build_list_advisories_view.js | 87 ++++++ .../javascripts/lib/bootstrap-popover.js | 98 +++++++ .../javascripts/lib/bootstrap-tooltip.js | 275 ++++++++++++++++++ app/assets/javascripts/lib/lib.js | 3 + app/assets/stylesheets/design/custom.scss | 114 ++++++++ app/controllers/advisories_controller.rb | 1 + .../platforms/platforms_controller.rb | 4 + .../projects/build_lists_controller.rb | 31 +- app/helpers/advisories_helper.rb | 6 + app/models/ability.rb | 4 +- app/models/advisory.rb | 14 + .../advisories/_advisories.json.jbuilder | 8 + app/views/advisories/_form.html.haml | 4 + app/views/platforms/base/_sidebar.html.haml | 3 + .../platforms/platforms/_advisories.html.haml | 7 + .../platforms/platforms/_advisory.html.haml | 3 + .../platforms/platforms/advisories.html.haml | 6 + app/views/projects/build_lists/show.html.haml | 31 +- config/locales/layout.en.yml | 2 +- config/locales/models/advisory.en.yml | 2 + config/locales/models/advisory.ru.yml | 2 + config/routes.rb | 2 + 25 files changed, 717 insertions(+), 14 deletions(-) create mode 100644 app/assets/javascripts/backbone/models/advisory.js create mode 100644 app/assets/javascripts/backbone/routers/build_lists_advisories_router.js create mode 100644 app/assets/javascripts/backbone/views/build_list_advisories_view.js create mode 100644 app/assets/javascripts/lib/bootstrap-popover.js create mode 100644 app/assets/javascripts/lib/bootstrap-tooltip.js create mode 100644 app/assets/javascripts/lib/lib.js create mode 100644 app/views/advisories/_advisories.json.jbuilder create mode 100644 app/views/platforms/platforms/_advisories.html.haml create mode 100644 app/views/platforms/platforms/_advisory.html.haml create mode 100644 app/views/platforms/platforms/advisories.html.haml diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index f2389da4d..9d8340566 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -4,7 +4,7 @@ //= require autocomplete-rails //= require vendor //= require jquery.dataTables_ext -//= require_tree ./lib +//= require lib/lib //= require_tree ./design //= require_tree ./extra diff --git a/app/assets/javascripts/backbone/models/advisory.js b/app/assets/javascripts/backbone/models/advisory.js new file mode 100644 index 000000000..654ac09c6 --- /dev/null +++ b/app/assets/javascripts/backbone/models/advisory.js @@ -0,0 +1,12 @@ +Rosa.Models.Advisory = Backbone.Model.extend({ + defaults: { + id: null, + description: null, + references: null, + update_type: null + } +}); + +Rosa.Collections.AdvisoriesCollection = Backbone.Collection.extend({ + model: Rosa.Models.Advisory +}); diff --git a/app/assets/javascripts/backbone/routers/build_lists_advisories_router.js b/app/assets/javascripts/backbone/routers/build_lists_advisories_router.js new file mode 100644 index 000000000..bf20ecfa0 --- /dev/null +++ b/app/assets/javascripts/backbone/routers/build_lists_advisories_router.js @@ -0,0 +1,10 @@ +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.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 new file mode 100644 index 000000000..7a47f0797 --- /dev/null +++ b/app/assets/javascripts/backbone/views/build_list_advisories_view.js @@ -0,0 +1,87 @@ +Rosa.Views.BuildListAdvisoriesView = Backbone.View.extend({ + initialize: function() { + _.bindAll(this, 'popoverTitle', 'popoverDesc', 'showAdvisory', + 'changeAdvisoryList', 'showPreview', 'showForm', 'hideAll'); + 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'); + + this._$selector.on('change', this.showAdvisory); + this._$type_select.on('change', this.changeAdvisoryList); + }, + + changeAdvisoryList: function() { + this._$selector.children('.popoverable').hide(); + this._$selector.children('.popoverable.' + this._$type_select.val()).show(); + this._$selector.val('no').trigger('change'); + }, + + popoverTitle: function(el) { + return el.val(); + }, + + popoverDesc: function(el) { + return this.collection.get(el.val()).get('popover_desc'); + }, + + showAdvisory: function(el) { + var adv_id = this._$selector.val(); + switch (adv_id) { + case 'no': + this.hideAll(); + break + case 'new': + this.showForm(); + break + default: + this.showPreview(adv_id); + } + }, + + showPreview: function(id) { + if (this._$form.is(':visible')) { + this._$form.slideUp(); + } + var adv = this.collection.get(id); + var prev = this._$preview; + prev.children('h3').html(prev.children('h3').html() + ' ' + 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(); + } + }, + + showForm: function() { + if (this._$preview.is(':visible')) { + this._$preview.slideUp(); + } + if (!this._$form.is(':visible')) { + this._$form.slideDown(); + } + }, + + hideAll: function() { + if (this._$preview.is(':visible')) { + this._$preview.slideUp(); + } + if (this._$form.is(':visible')) { + this._$form.slideUp(); + } + }, + + render: function() { + var title = this.popoverTitle; + var description = this.popoverDesc; + this.changeAdvisoryList(); + this.$('#attach_advisory > .popoverable').popover({ + title: function() { return title($(this)); }, + content: function() { return description($(this)); } + }); + this.showAdvisory(); + return this; + } + +}); diff --git a/app/assets/javascripts/lib/bootstrap-popover.js b/app/assets/javascripts/lib/bootstrap-popover.js new file mode 100644 index 000000000..39fbe358e --- /dev/null +++ b/app/assets/javascripts/lib/bootstrap-popover.js @@ -0,0 +1,98 @@ +/* =========================================================== + * bootstrap-popover.js v2.0.4 + * http://twitter.github.com/bootstrap/javascript.html#popovers + * =========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* POPOVER PUBLIC CLASS DEFINITION + * =============================== */ + + var Popover = function ( element, options ) { + this.init('popover', element, options) + } + + + /* NOTE: POPOVER EXTENDS BOOTSTRAP-TOOLTIP.js + ========================================== */ + + Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype, { + + constructor: Popover + + , setContent: function () { + var $tip = this.tip() + , title = this.getTitle() + , content = this.getContent() + + $tip.find('.popover-title')[this.isHTML(title) ? 'html' : 'text'](title) + $tip.find('.popover-content > *')[this.isHTML(content) ? 'html' : 'text'](content) + + $tip.removeClass('fade top bottom left right in') + } + + , hasContent: function () { + return this.getTitle() || this.getContent() + } + + , getContent: function () { + var content + , $e = this.$element + , o = this.options + + content = $e.attr('data-content') + || (typeof o.content == 'function' ? o.content.call($e[0]) : o.content) + + return content + } + + , tip: function () { + if (!this.$tip) { + this.$tip = $(this.options.template) + } + return this.$tip + } + + }) + + + /* POPOVER PLUGIN DEFINITION + * ======================= */ + + $.fn.popover = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('popover') + , options = typeof option == 'object' && option + if (!data) $this.data('popover', (data = new Popover(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.popover.Constructor = Popover + + $.fn.popover.defaults = $.extend({} , $.fn.tooltip.defaults, { + placement: 'right' + , content: '' + , template: '

' + }) + +}(window.jQuery); \ No newline at end of file diff --git a/app/assets/javascripts/lib/bootstrap-tooltip.js b/app/assets/javascripts/lib/bootstrap-tooltip.js new file mode 100644 index 000000000..b476f1c4e --- /dev/null +++ b/app/assets/javascripts/lib/bootstrap-tooltip.js @@ -0,0 +1,275 @@ +/* =========================================================== + * bootstrap-tooltip.js v2.0.4 + * http://twitter.github.com/bootstrap/javascript.html#tooltips + * Inspired by the original jQuery.tipsy by Jason Frame + * =========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* TOOLTIP PUBLIC CLASS DEFINITION + * =============================== */ + + var Tooltip = function (element, options) { + this.init('tooltip', element, options) + } + + Tooltip.prototype = { + + constructor: Tooltip + + , init: function (type, element, options) { + var eventIn + , eventOut + + this.type = type + this.$element = $(element) + this.options = this.getOptions(options) + this.enabled = true + + if (this.options.trigger != 'manual') { + eventIn = this.options.trigger == 'hover' ? 'mouseenter' : 'focus' + eventOut = this.options.trigger == 'hover' ? 'mouseleave' : 'blur' + this.$element.on(eventIn, this.options.selector, $.proxy(this.enter, this)) + this.$element.on(eventOut, this.options.selector, $.proxy(this.leave, this)) + } + + this.options.selector ? + (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : + this.fixTitle() + } + + , getOptions: function (options) { + options = $.extend({}, $.fn[this.type].defaults, options, this.$element.data()) + + if (options.delay && typeof options.delay == 'number') { + options.delay = { + show: options.delay + , hide: options.delay + } + } + + return options + } + + , enter: function (e) { + var self = $(e.currentTarget)[this.type](this._options).data(this.type) + + if (!self.options.delay || !self.options.delay.show) return self.show() + + clearTimeout(this.timeout) + self.hoverState = 'in' + this.timeout = setTimeout(function() { + if (self.hoverState == 'in') self.show() + }, self.options.delay.show) + } + + , leave: function (e) { + var self = $(e.currentTarget)[this.type](this._options).data(this.type) + + if (this.timeout) clearTimeout(this.timeout) + if (!self.options.delay || !self.options.delay.hide) return self.hide() + + self.hoverState = 'out' + this.timeout = setTimeout(function() { + if (self.hoverState == 'out') self.hide() + }, self.options.delay.hide) + } + + , show: function () { + var $tip + , inside + , pos + , actualWidth + , actualHeight + , placement + , tp + + if (this.hasContent() && this.enabled) { + $tip = this.tip() + this.setContent() + + if (this.options.animation) { + $tip.addClass('fade') + } + + placement = typeof this.options.placement == 'function' ? + this.options.placement.call(this, $tip[0], this.$element[0]) : + this.options.placement + + inside = /in/.test(placement) + + $tip + .remove() + .css({ top: 0, left: 0, display: 'block' }) + .appendTo(inside ? this.$element : document.body) + + pos = this.getPosition(inside) + + actualWidth = $tip[0].offsetWidth + actualHeight = $tip[0].offsetHeight + + switch (inside ? placement.split(' ')[1] : placement) { + case 'bottom': + tp = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2} + break + case 'top': + tp = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2} + break + case 'left': + tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth} + break + case 'right': + tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width} + break + } + + $tip + .css(tp) + .addClass(placement) + .addClass('in') + } + } + + , isHTML: function(text) { + // html string detection logic adapted from jQuery + return typeof text != 'string' + || ( text.charAt(0) === "<" + && text.charAt( text.length - 1 ) === ">" + && text.length >= 3 + ) || /^(?:[^<]*<[\w\W]+>[^>]*$)/.exec(text) + } + + , setContent: function () { + var $tip = this.tip() + , title = this.getTitle() + + $tip.find('.tooltip-inner')[this.isHTML(title) ? 'html' : 'text'](title) + $tip.removeClass('fade in top bottom left right') + } + + , hide: function () { + var that = this + , $tip = this.tip() + + $tip.removeClass('in') + + function removeWithAnimation() { + var timeout = setTimeout(function () { + $tip.off($.support.transition.end).remove() + }, 500) + + $tip.one($.support.transition.end, function () { + clearTimeout(timeout) + $tip.remove() + }) + } + + $.support.transition && this.$tip.hasClass('fade') ? + removeWithAnimation() : + $tip.remove() + } + + , fixTitle: function () { + var $e = this.$element + if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') { + $e.attr('data-original-title', $e.attr('title') || '').removeAttr('title') + } + } + + , hasContent: function () { + return this.getTitle() + } + + , getPosition: function (inside) { + return $.extend({}, (inside ? {top: 0, left: 0} : this.$element.offset()), { + width: this.$element[0].offsetWidth + , height: this.$element[0].offsetHeight + }) + } + + , getTitle: function () { + var title + , $e = this.$element + , o = this.options + + title = $e.attr('data-original-title') + || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) + + return title + } + + , tip: function () { + return this.$tip = this.$tip || $(this.options.template) + } + + , validate: function () { + if (!this.$element[0].parentNode) { + this.hide() + this.$element = null + this.options = null + } + } + + , enable: function () { + this.enabled = true + } + + , disable: function () { + this.enabled = false + } + + , toggleEnabled: function () { + this.enabled = !this.enabled + } + + , toggle: function () { + this[this.tip().hasClass('in') ? 'hide' : 'show']() + } + + } + + + /* TOOLTIP PLUGIN DEFINITION + * ========================= */ + + $.fn.tooltip = function ( option ) { + return this.each(function () { + var $this = $(this) + , data = $this.data('tooltip') + , options = typeof option == 'object' && option + if (!data) $this.data('tooltip', (data = new Tooltip(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.tooltip.Constructor = Tooltip + + $.fn.tooltip.defaults = { + animation: true + , placement: 'top' + , selector: false + , template: '
' + , trigger: 'hover' + , title: '' + , delay: 0 + } + +}(window.jQuery); diff --git a/app/assets/javascripts/lib/lib.js b/app/assets/javascripts/lib/lib.js new file mode 100644 index 000000000..2d346835e --- /dev/null +++ b/app/assets/javascripts/lib/lib.js @@ -0,0 +1,3 @@ +//= require ./jquery.placeholder +//= require ./bootstrap-tooltip +//= require ./bootstrap-popover diff --git a/app/assets/stylesheets/design/custom.scss b/app/assets/stylesheets/design/custom.scss index a8f27ec09..993836353 100644 --- a/app/assets/stylesheets/design/custom.scss +++ b/app/assets/stylesheets/design/custom.scss @@ -953,3 +953,117 @@ form.mass_build input[type="checkbox"] { width: 10px; height: 11px; } + +div#new_advisory_form, div#advisory_preview { + display: none; +} + +/*=============== popovers ===============*/ + +.popover { + display: none; + left: 0; + padding: 5px; + position: absolute; + top: 0; + z-index: 1010; +} + +.popover.top { + margin-top: -5px; +} + +.popover.right { + margin-left: 5px; +} + +.popover.bottom { + margin-top: 5px; +} + +.popover.left { + margin-left: -5px; +} + +.popover.top .arrow { + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid #000000; + bottom: 0; + left: 50%; + margin-left: -5px; +} + +.popover.right .arrow { + border-bottom: 5px solid transparent; + border-right: 5px solid #000000; + border-top: 5px solid transparent; + left: 0; + margin-top: -5px; + top: 50%; +} + +.popover.bottom .arrow { + border-bottom: 5px solid #000000; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + left: 50%; + margin-left: -5px; + top: 0; +} + +.popover.left .arrow { + border-bottom: 5px solid transparent; + border-left: 5px solid #000000; + border-top: 5px solid transparent; + margin-top: -5px; + right: 0; + top: 50%; +} + +.popover .arrow { + height: 0; + position: absolute; + width: 0; +} + +.popover-inner { + background: none repeat scroll 0 0 rgba(0, 0, 0, 0.8); + border-radius: 6px 6px 6px 6px; + box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + overflow: hidden; + padding: 3px; + width: 280px; + text-align: justify; +} + +.popover-title { + background-color: #F5F5F5; + border-bottom: 1px solid #EEEEEE; + border-radius: 3px 3px 0 0; + line-height: 1; + padding: 9px 15px; + margin: 0; +} + +.popover-content { + background-clip: padding-box; + background-color: #FFFFFF; + border-radius: 0 0 3px 3px; + padding: 14px; + font-size: 13px; +} + +.popover-content p, .popover-content ul, .popover-content ol { + margin-bottom: 0; + margin: 0; +} + +.fade { + -moz-transition: opacity 0.15s linear 0s; + opacity: 0; +} + +.fade.in { + opacity: 1; +} diff --git a/app/controllers/advisories_controller.rb b/app/controllers/advisories_controller.rb index 447a6bb5a..979712412 100644 --- a/app/controllers/advisories_controller.rb +++ b/app/controllers/advisories_controller.rb @@ -2,6 +2,7 @@ class AdvisoriesController < ApplicationController before_filter :authenticate_user! before_filter :find_advisory, :only => [:show] + skip_before_filter :authenticate_user! if APP_CONFIG['anonymous_access'] load_and_authorize_resource def index diff --git a/app/controllers/platforms/platforms_controller.rb b/app/controllers/platforms/platforms_controller.rb index aeb805269..17b541a76 100644 --- a/app/controllers/platforms/platforms_controller.rb +++ b/app/controllers/platforms/platforms_controller.rb @@ -2,6 +2,7 @@ class Platforms::PlatformsController < Platforms::BaseController before_filter :authenticate_user! + skip_before_filter :authenticate_user!, :only => [:advisories] if APP_CONFIG['anonymous_access'] load_and_authorize_resource autocomplete :user, :uname @@ -134,4 +135,7 @@ class Platforms::PlatformsController < Platforms::BaseController redirect_to members_platform_url(@platform) end + def advisories + @advisories = @platform.advisories.paginate(:page => params[:page]) + end end diff --git a/app/controllers/projects/build_lists_controller.rb b/app/controllers/projects/build_lists_controller.rb index 4c8980e64..0fcf3720e 100644 --- a/app/controllers/projects/build_lists_controller.rb +++ b/app/controllers/projects/build_lists_controller.rb @@ -70,9 +70,11 @@ class Projects::BuildListsController < Projects::BaseController def show @item_groups = @build_list.items.group_by_level + @advisories = @build_list.project.advisories end def update +# raise params.inspect if params[:publish].present? and can?(:publish, @build_list) publish elsif params[:reject_publish].present? and can?(:reject_publish, @build_list) @@ -172,12 +174,29 @@ class Projects::BuildListsController < Projects::BaseController def publish @build_list.update_type = params[:build_list][:update_type] if params[:build_list][:update_type].present? - if params[:create_advisory].present? and !@build_list.build_advisory(params[:build_list][:advisory]) do |a| - a.update_type = @build_list.update_type - a.project = @build_list.project - a.platforms << @build_list.save_to_platform unless a.platforms.include? @build_list.save_to_platform - end.save - redirect_to :back, :notice => t('layout.build_lists.publish_fail') and return + + if params[:attach_advisory].present? and params[:attach_advisory] != 'no' and !@build_list.advisory + if params[:attach_advisory] == 'new' + # create new advisory + if !@build_list.build_advisory(params[:build_list][:advisory]) do |a| + a.update_type = @build_list.update_type + a.project = @build_list.project + a.platforms << @build_list.save_to_platform unless a.platforms.include? @build_list.save_to_platform + end.save + redirect_to :back, :notice => t('layout.build_lists.publish_fail') and return + end + else + # attach existing advisory + a = Advisory.where(:advisory_id => params[:attach_advisory]).limit(1).first + if a.update_type != @build_list.update_type + redirect_to :back, :notice => t('layout.build_lists.publish_fail') and return + end + a.platforms << @build_list.save_to_platform unless a.platforms.include? @build_list.save_to_platform + @build_list.advisory = a + if !a.save + redirect_to :back, :notice => t('layout.build_lists.publish_fail') and return + end + end end if @build_list.save and @build_list.publish redirect_to :back, :notice => t('layout.build_lists.publish_success') diff --git a/app/helpers/advisories_helper.rb b/app/helpers/advisories_helper.rb index 30d3085e8..62a940bb7 100644 --- a/app/helpers/advisories_helper.rb +++ b/app/helpers/advisories_helper.rb @@ -1,5 +1,11 @@ # -*- 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}"] }) + end + def construct_ref_link(ref) ref = sanitize(ref) url = if ref =~ %r[^http(s?)://*] diff --git a/app/models/ability.rb b/app/models/ability.rb index 03124c51e..743572488 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -19,6 +19,8 @@ class Ability can :search, BuildList can :read, BuildList, :project => {:visibility => 'open'} can :read, ProductBuildList, :product => {:platform => {:visibility => 'open'}} + can :read, Advisory + can(:advisories, Platform) {APP_CONFIG['anonymous_access']} # Core callbacks can [:publish_build, :status_build, :pre_build, :post_build, :circle_build, :new_bbdt], BuildList @@ -82,7 +84,7 @@ class Ability can([:read, :related, :members], Platform, read_relations_for('platforms')) {|platform| local_reader? platform} can([:update, :members], Platform) {|platform| local_admin? platform} can([:destroy, :members, :add_member, :remove_member, :remove_members, :build_all, :mass_builds] , Platform) {|platform| owner? platform} - can :autocomplete_user_uname, Platform + can [:autocomplete_user_uname, :read_advisories, :advisories], Platform can [:read, :projects_list], Repository, :platform => {:visibility => 'open'} can [:read, :projects_list], Repository, :platform => {:owner_type => 'User', :owner_id => user.id} diff --git a/app/models/advisory.rb b/app/models/advisory.rb index 859d5c127..c643f333b 100644 --- a/app/models/advisory.rb +++ b/app/models/advisory.rb @@ -6,10 +6,13 @@ class Advisory < ActiveRecord::Base validates :description, :update_type, :presence => true after_create :generate_advisory_id + before_save :normalize_references, :if => :references_changed? ID_TEMPLATE = 'ROSA-%s-%d:%04d' TYPES = {'security' => 'SA', 'bugfix' => 'A'} + scope :by_project, lambda {|p| where('project_id' => p.try(:id) || p)} + def to_param advisory_id end @@ -20,4 +23,15 @@ class Advisory < ActiveRecord::Base self.advisory_id = sprintf(ID_TEMPLATE, :type => TYPES[self.update_type], :year => Time.now.utc.year, :id => self.id) self.save end + + def normalize_references + self.references.gsub!(/\r| /, '') + self.references = self.references.split('\n').map do |ref| + ref = CGI::escapeHTML(ref) + ref = "http://#{ref}" unless ref =~ %r[^http(s?)://*] + ref + end.join("\n") + end + end +Advisory.include_root_in_json = false diff --git a/app/views/advisories/_advisories.json.jbuilder b/app/views/advisories/_advisories.json.jbuilder new file mode 100644 index 000000000..2719429c1 --- /dev/null +++ b/app/views/advisories/_advisories.json.jbuilder @@ -0,0 +1,8 @@ +json.array!(advisories) do |json, a| + json.id a.advisory_id + json.advisory_id a.advisory_id + json.description simple_format(a.description) + json.popover_desc truncate(a.description, :length => 500); + json.references a.references.split("\n").map{|ref| construct_ref_link(ref)}.join('
') + json.update_type a.update_type +end diff --git a/app/views/advisories/_form.html.haml b/app/views/advisories/_form.html.haml index a21ff2acb..64f1f4320 100644 --- a/app/views/advisories/_form.html.haml +++ b/app/views/advisories/_form.html.haml @@ -13,3 +13,7 @@ .rightlist = f.text_area :references, :class => 'text_field', :cols => 80 .both + +:javascript + $(function() { + }); diff --git a/app/views/platforms/base/_sidebar.html.haml b/app/views/platforms/base/_sidebar.html.haml index eaa4c6eeb..7c987f075 100644 --- a/app/views/platforms/base/_sidebar.html.haml +++ b/app/views/platforms/base/_sidebar.html.haml @@ -15,6 +15,9 @@ - if can? :read, @platform.products.build %li{:class => (contr == :products) ? 'active' : ''} = link_to t("layout.products.list_header"), platform_products_path(@platform) + - if can? :read_advisories, @platform + %li{:class => (contr == :platforms and act == :advisories) ? 'active' : ''} + = link_to t("layout.advisories.list_header"), advisories_platform_path(@platform) - if can? :update, @platform %li{:class => (act == :edit && contr == :platforms) ? 'active' : nil} = link_to t("platform_menu.settings"), edit_platform_path(@platform) diff --git a/app/views/platforms/platforms/_advisories.html.haml b/app/views/platforms/platforms/_advisories.html.haml new file mode 100644 index 000000000..910a9d643 --- /dev/null +++ b/app/views/platforms/platforms/_advisories.html.haml @@ -0,0 +1,7 @@ +%table#myTable.tablesorter.advisories{:cellspacing => "0", :cellpadding => "0"} + %thead + %tr + %th.th1= t("activerecord.attributes.advisory.advisory_id") + %th.th2= t("activerecord.attributes.advisory.description") + %tbody + = render :partial => 'advisory', :collection => @advisories, :as => :advisory diff --git a/app/views/platforms/platforms/_advisory.html.haml b/app/views/platforms/platforms/_advisory.html.haml new file mode 100644 index 000000000..071f86d48 --- /dev/null +++ b/app/views/platforms/platforms/_advisory.html.haml @@ -0,0 +1,3 @@ +%tr{:class => cycle("odd", "even")} + %td= link_to advisory.advisory_id, advisory_path(advisory) + %td= truncate(advisory.description, :length => 50) diff --git a/app/views/platforms/platforms/advisories.html.haml b/app/views/platforms/platforms/advisories.html.haml new file mode 100644 index 000000000..caf2cc666 --- /dev/null +++ b/app/views/platforms/platforms/advisories.html.haml @@ -0,0 +1,6 @@ +- set_meta_tags :title => [title_object(@platform), t('layout.advisories.list_header')] += render 'submenu' += render 'sidebar' + += render :partial => 'advisories', :object => @advisories += will_paginate @advisories diff --git a/app/views/projects/build_lists/show.html.haml b/app/views/projects/build_lists/show.html.haml index 46b7b761f..4e4f68ad7 100644 --- a/app/views/projects/build_lists/show.html.haml +++ b/app/views/projects/build_lists/show.html.haml @@ -72,11 +72,32 @@ .both - if @build_list.can_publish? and @build_list.save_to_platform.released and @build_list.advisory.nil? - .leftlist= label_tag :create_advisory, t("layout.build_lists.create_advisory") - .rightlist= check_box_tag :create_advisory, 1, false - .both - = f.fields_for @build_list.build_advisory do |f| - = render :partial => 'advisories/form', :locals => {:f => f} + #advisory_block + .leftlist= label_tag :attach_advisory, t("layout.build_lists.attached_advisory") + .rightlist= select_tag :attach_advisory, advisories_select_options(@advisories) + .both + + #new_advisory_form + = f.fields_for @build_list.build_advisory do |f| + = render :partial => 'advisories/form', :locals => {:f => f} + + #advisory_preview + %h3= t("activerecord.models.advisory") << ' ' + + .leftlist= t("activerecord.attributes.advisory.description") + .rightlist.descr   + .both + + .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(); + }); = submit_tag t("layout.publish"), :confirm => t("layout.confirm"), :name => 'publish' if @build_list.can_publish? and can?(:publish, @build_list) = submit_tag t("layout.reject_publish"), :confirm => t("layout.confirm"), :name => 'reject_publish' if @build_list.can_reject_publish? and can?(:reject_publish, @build_list) diff --git a/config/locales/layout.en.yml b/config/locales/layout.en.yml index ac1790dd0..3e1625958 100644 --- a/config/locales/layout.en.yml +++ b/config/locales/layout.en.yml @@ -16,7 +16,7 @@ en: by: by remove: Remove - + find_project: Find project... filter_by_name: Filter by name diff --git a/config/locales/models/advisory.en.yml b/config/locales/models/advisory.en.yml index 027dd62bf..dd9ee79b5 100644 --- a/config/locales/models/advisory.en.yml +++ b/config/locales/models/advisory.en.yml @@ -6,6 +6,8 @@ en: project_name: Project affected_versions: Affected versions ref_comment: Add links one by row + no_: No + new: New flash: advisories: diff --git a/config/locales/models/advisory.ru.yml b/config/locales/models/advisory.ru.yml index 169bf5f70..f0914a12d 100644 --- a/config/locales/models/advisory.ru.yml +++ b/config/locales/models/advisory.ru.yml @@ -6,6 +6,8 @@ ru: project_name: Проект affected_versions: Применен в версиях ref_comment: Вставляйте ссылки по одной на строку + no_: Нет + new: Новый flash: advisories: diff --git a/config/routes.rb b/config/routes.rb index 74129a0fb..30e9f4a22 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -49,6 +49,7 @@ Rosa::Application.routes.draw do post :make_clone post :build_all get :mass_builds + get :advisories end get :autocomplete_user_uname, :on => :collection resources :repositories do @@ -61,6 +62,7 @@ Rosa::Application.routes.draw do resources :products do resources :product_build_lists, :only => [:create, :destroy] end + end match '/private/:platform_name/*file_path' => 'privates#show'