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'