diff --git a/app/assets/stylesheets/design/custom.scss b/app/assets/stylesheets/design/custom.scss index d5ef96d57..dbd61fe82 100644 --- a/app/assets/stylesheets/design/custom.scss +++ b/app/assets/stylesheets/design/custom.scss @@ -781,4 +781,36 @@ div#git_help_data { div#git_help_data p { padding-bottom: 5px; -} \ No newline at end of file +} + +// for bootstrap +.close { + float: right; + font-size: 20px; + font-weight: bold; + line-height: 18px; + color: #000000; + text-shadow: 0 1px 0 #ffffff; + opacity: 0.2; + filter: alpha(opacity=20); +} + +.close:hover { + color: #000000; + text-decoration: none; + opacity: 0.4; + filter: alpha(opacity=40); + cursor: pointer; +} + +.modal { + margin: -150px 0 0 -280px; +} + +#forkModal.modal .btn.btn-primary { + width: 100%; +} + +.center { + text-align: center; +} diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index bd3b6b3a6..8f9a949ad 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -26,9 +26,10 @@ class ProjectsController < ApplicationController @project = Project.new params[:project] @project.owner = choose_owner @who_owns = (@project.owner_type == 'User' ? :me : :group) + authorize! :update, @project.owner if @project.owner.class == Group if @project.save - flash[:notice] = t('flash.project.saved') + flash[:notice] = t('flash.project.saved') redirect_to @project else flash[:error] = t('flash.project.save_error') @@ -56,7 +57,9 @@ class ProjectsController < ApplicationController end def fork - if forked = @project.fork(current_user) and forked.valid? + owner = (Group.find params[:group] if params[:group].present?) || current_user + authorize! :update, owner if owner.class == Group + if forked = @project.fork(owner) and forked.valid? redirect_to forked, :notice => t("flash.project.forked") else flash[:warning] = t("flash.project.fork_error") diff --git a/app/models/ability.rb b/app/models/ability.rb index 6820d6d97..3c9db3a8d 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -57,6 +57,7 @@ class Ability can(:write, Project) {|project| local_writer? project} # for grack can([:update, :sections, :manage_collaborators], Project) {|project| local_admin? project} can(:fork, Project) {|project| can? :read, project} + can(:fork_to_group, Project) {|project| project.owner_type == 'Group' and can? :update, project.owner} can(:destroy, Project) {|project| owner? project} can(:destroy, Project) {|project| project.owner_type == 'Group' and project.owner.objects.exists?(:object_type => 'User', :object_id => user.id, :role => 'admin')} can :remove_user, Project diff --git a/app/views/git/shared/_fork.html.haml b/app/views/git/shared/_fork.html.haml index 899a711d0..b491b3dfe 100644 --- a/app/views/git/shared/_fork.html.haml +++ b/app/views/git/shared/_fork.html.haml @@ -1,4 +1,29 @@ -- if can? :fork, @project - .r#fork-and-edit= link_to t('layout.projects.fork_and_edit'), fork_project_path(@project), :method => :post, :confirm => t("layout.confirm"), :class => 'button' +- if Group.can_own_project(current_user).present? + .r#fork-and-edit= link_to t('layout.projects.fork_and_edit'), '#forkModal', :class => 'button', 'data-toggle' => 'modal' - if can? :create, @project.build_lists.new .r{:style => "display: block"}= link_to t('layout.projects.new_build_list'), new_project_build_list_path(@project), :class => 'button' + + #forkModal.modal{:style => 'display: none;'} + .modal-header + %a.close{"data-dismiss" => "modal"} × + %h3=t 'layout.projects.fork_modal_header' + .modal-footer + - if current_user.projects.exists? :name => @project.name + %p.center + =t 'layout.projects.already_exists' + =link_to "#{current_user.uname}/#{@project.name}", project_path(current_user.projects.by_name(@project.name).first.id) + - else + = form_for @project, :url => fork_project_path(@project), :html => { :class => :form, :multipart => true, :method => :post } do |f| + =f.submit t('layout.projects.fork_to', :to => current_user.uname), :class => 'btn btn-primary' + - Group.can_own_project(current_user).each do |group| + .modal-footer + - if group.projects.exists? :name => @project.name + %p.center + =t 'layout.projects.already_exists' + =link_to "#{group.uname}/#{@project.name} (#{t 'activerecord.models.group'})", project_path(group.projects.by_name(@project.name).first.id) + - else + = form_for @project, :url => fork_project_path(@project), :html => { :class => :form, :multipart => true, :method => :post } do |f| + = hidden_field_tag :group, group.id + =f.submit t('layout.projects.fork_to', :to => "#{group.uname} (#{t 'activerecord.models.group'})"), :class => 'btn btn-primary' +- else + .r#fork-and-edit= link_to t('layout.projects.fork_and_edit'), fork_project_path(@project), :method => :post, :confirm => t("layout.confirm"), :class => 'button' diff --git a/config/locales/models/project.en.yml b/config/locales/models/project.en.yml index 11f4df34b..1d38c6b45 100644 --- a/config/locales/models/project.en.yml +++ b/config/locales/models/project.en.yml @@ -4,6 +4,9 @@ en: add: Add edit: Settings fork_and_edit: Fork + fork_to: Fork to %{to} + fork_modal_header: Where do you want to fork this project? + already_exists: Project already exists list: List list_header: Projects edit_header: Edit project diff --git a/config/locales/models/project.ru.yml b/config/locales/models/project.ru.yml index ae9a75c7a..219437767 100644 --- a/config/locales/models/project.ru.yml +++ b/config/locales/models/project.ru.yml @@ -4,6 +4,9 @@ ru: add: Добавить edit: Настройки fork_and_edit: Клонировать + fork_to: Клонировать в %{to} + fork_modal_header: Куда Вы хотите клонировать проект? + already_exists: Проект уже существует list: Список list_header: Проекты edit_header: Редактировать проект diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index de19f9852..eceef48a5 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -2,17 +2,17 @@ require 'spec_helper' describe ProjectsController do - - before(:each) do + + before(:each) do stub_rsync_methods @project = FactoryGirl.create(:project) @another_user = FactoryGirl.create(:user) @create_params = {:project => {:name => 'pro'}} @update_params = {:project => {:name => 'pro2'}} - end + end - context 'for guest' do + context 'for guest' do it 'should not be able to perform index action' do get :index response.should redirect_to(new_user_session_path) @@ -25,10 +25,10 @@ describe ProjectsController do end context 'for admin' do - before(:each) do - @admin = FactoryGirl.create(:admin) - set_session_for(@admin) - end + before(:each) do + @admin = FactoryGirl.create(:admin) + set_session_for(@admin) + end it_should_behave_like 'projects user with admin rights' it_should_behave_like 'projects user with reader rights' @@ -44,12 +44,12 @@ describe ProjectsController do end context 'for owner user' do - before(:each) do - @user = FactoryGirl.create(:user) - set_session_for(@user) - @project.update_attribute(:owner, @user) - @project.relations.create!(:object_type => 'User', :object_id => @user.id, :role => 'admin') - end + before(:each) do + @user = FactoryGirl.create(:user) + set_session_for(@user) + @project.update_attribute(:owner, @user) + @project.relations.create!(:object_type => 'User', :object_id => @user.id, :role => 'admin') + end it_should_behave_like 'projects user with admin rights' it_should_behave_like 'user with rights to view projects' @@ -67,30 +67,49 @@ describe ProjectsController do post :fork, :id => @project.id response.should redirect_to(forbidden_path) end + end context 'for reader user' do - before(:each) do - @user = FactoryGirl.create(:user) - set_session_for(@user) - @project.relations.create!(:object_type => 'User', :object_id => @user.id, :role => 'reader') - end + before(:each) do + @user = FactoryGirl.create(:user) + set_session_for(@user) + @project.relations.create!(:object_type => 'User', :object_id => @user.id, :role => 'reader') + end it_should_behave_like 'projects user with reader rights' end context 'for writer user' do - before(:each) do - @user = FactoryGirl.create(:user) - set_session_for(@user) - @project.relations.create!(:object_type => 'User', :object_id => @user.id, :role => 'writer') - end + before(:each) do + @user = FactoryGirl.create(:user) + set_session_for(@user) + @project.relations.create!(:object_type => 'User', :object_id => @user.id, :role => 'writer') + end it_should_behave_like 'projects user with reader rights' + + it 'should not be able to create project to other group' do + group = FactoryGirl.create(:group) + post :create, @create_params.merge({:who_owns => 'group', :owner_id => group.id}) + response.should redirect_to(forbidden_path) + end + + it 'should not be able to fork project to other group' do + group = FactoryGirl.create(:group) + post :fork, :id => @project.id, :group => group.id + response.should redirect_to(forbidden_path) + end + + it 'should be able to fork project to group' do + group = FactoryGirl.create(:group) + group.objects.create(:object_type => 'User', :object_id => @user.id, :role => 'admin') + post :fork, :id => @project.id, :group => group.id + response.should redirect_to(project_path(group.projects.first.id)) + end end context 'search projects' do - before(:each) do @admin = FactoryGirl.create(:admin) @project1 = FactoryGirl.create(:project, :name => 'perl-debug') @@ -103,4 +122,14 @@ describe ProjectsController do assigns(:projects).should eq([@project2, @project1]) end end + + context 'for other user' do + it 'should not be able to fork hidden project' do + @user = FactoryGirl.create(:user) + set_session_for(@user) + @project.update_attribute(:visibility, 'hidden') + post :fork, :id => @project.id + response.should redirect_to(forbidden_path) + end + end end diff --git a/vendor/assets/javascripts/bootstrap-modal.js b/vendor/assets/javascripts/bootstrap-modal.js new file mode 100644 index 000000000..e92970627 --- /dev/null +++ b/vendor/assets/javascripts/bootstrap-modal.js @@ -0,0 +1,210 @@ +/* ========================================================= + * bootstrap-modal.js v2.0.2 + * http://twitter.github.com/bootstrap/javascript.html#modals + * ========================================================= + * 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" + + /* MODAL CLASS DEFINITION + * ====================== */ + + var Modal = function ( content, options ) { + this.options = options + this.$element = $(content) + .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this)) + } + + Modal.prototype = { + + constructor: Modal + + , toggle: function () { + return this[!this.isShown ? 'show' : 'hide']() + } + + , show: function () { + var that = this + + if (this.isShown) return + + $('body').addClass('modal-open') + + this.isShown = true + this.$element.trigger('show') + + escape.call(this) + backdrop.call(this, function () { + var transition = $.support.transition && that.$element.hasClass('fade') + + !that.$element.parent().length && that.$element.appendTo(document.body) //don't move modals dom position + + that.$element + .show() + + if (transition) { + that.$element[0].offsetWidth // force reflow + } + + that.$element.addClass('in') + + transition ? + that.$element.one($.support.transition.end, function () { that.$element.trigger('shown') }) : + that.$element.trigger('shown') + + }) + } + + , hide: function ( e ) { + e && e.preventDefault() + + if (!this.isShown) return + + var that = this + this.isShown = false + + $('body').removeClass('modal-open') + + escape.call(this) + + this.$element + .trigger('hide') + .removeClass('in') + + $.support.transition && this.$element.hasClass('fade') ? + hideWithTransition.call(this) : + hideModal.call(this) + } + + } + + + /* MODAL PRIVATE METHODS + * ===================== */ + + function hideWithTransition() { + var that = this + , timeout = setTimeout(function () { + that.$element.off($.support.transition.end) + hideModal.call(that) + }, 500) + + this.$element.one($.support.transition.end, function () { + clearTimeout(timeout) + hideModal.call(that) + }) + } + + function hideModal( that ) { + this.$element + .hide() + .trigger('hidden') + + backdrop.call(this) + } + + function backdrop( callback ) { + var that = this + , animate = this.$element.hasClass('fade') ? 'fade' : '' + + if (this.isShown && this.options.backdrop) { + var doAnimate = $.support.transition && animate + + this.$backdrop = $('
') + .appendTo(document.body) + + if (this.options.backdrop != 'static') { + this.$backdrop.click($.proxy(this.hide, this)) + } + + if (doAnimate) this.$backdrop[0].offsetWidth // force reflow + + this.$backdrop.addClass('in') + + doAnimate ? + this.$backdrop.one($.support.transition.end, callback) : + callback() + + } else if (!this.isShown && this.$backdrop) { + this.$backdrop.removeClass('in') + + $.support.transition && this.$element.hasClass('fade')? + this.$backdrop.one($.support.transition.end, $.proxy(removeBackdrop, this)) : + removeBackdrop.call(this) + + } else if (callback) { + callback() + } + } + + function removeBackdrop() { + this.$backdrop.remove() + this.$backdrop = null + } + + function escape() { + var that = this + if (this.isShown && this.options.keyboard) { + $(document).on('keyup.dismiss.modal', function ( e ) { + e.which == 27 && that.hide() + }) + } else if (!this.isShown) { + $(document).off('keyup.dismiss.modal') + } + } + + + /* MODAL PLUGIN DEFINITION + * ======================= */ + + $.fn.modal = function ( option ) { + return this.each(function () { + var $this = $(this) + , data = $this.data('modal') + , options = $.extend({}, $.fn.modal.defaults, $this.data(), typeof option == 'object' && option) + if (!data) $this.data('modal', (data = new Modal(this, options))) + if (typeof option == 'string') data[option]() + else if (options.show) data.show() + }) + } + + $.fn.modal.defaults = { + backdrop: true + , keyboard: true + , show: true + } + + $.fn.modal.Constructor = Modal + + + /* MODAL DATA-API + * ============== */ + + $(function () { + $('body').on('click.modal.data-api', '[data-toggle="modal"]', function ( e ) { + var $this = $(this), href + , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 + , option = $target.data('modal') ? 'toggle' : $.extend({}, $target.data(), $this.data()) + + e.preventDefault() + $target.modal(option) + }) + }) + +}( window.jQuery ); \ No newline at end of file diff --git a/vendor/assets/javascripts/vendor.js b/vendor/assets/javascripts/vendor.js index f08079bfe..a879f7abe 100644 --- a/vendor/assets/javascripts/vendor.js +++ b/vendor/assets/javascripts/vendor.js @@ -7,5 +7,6 @@ //= require codemirror/runmode //= require_tree ./codemirror/modes //= require cusel +//= require bootstrap-modal // require html5shiv // require_tree . diff --git a/vendor/assets/stylesheets/bootstrap.css b/vendor/assets/stylesheets/bootstrap.css new file mode 100644 index 000000000..b24ff2ae1 --- /dev/null +++ b/vendor/assets/stylesheets/bootstrap.css @@ -0,0 +1,301 @@ +/*! + * Bootstrap v2.0.2 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */ +.clearfix { + *zoom: 1; +} +.clearfix:before, +.clearfix:after { + display: table; + content: ""; +} +.clearfix:after { + clear: both; +} +.hide-text { + overflow: hidden; + text-indent: 100%; + white-space: nowrap; +} +.input-block-level { + display: block; + width: 100%; + min-height: 28px; + /* Make inputs at least the height of their button counterpart */ + + /* Makes inputs behave like true block-level elements */ + + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; +} +.btn-group { + position: relative; + *zoom: 1; + *margin-left: .3em; +} +.btn-group:before, +.btn-group:after { + display: table; + content: ""; +} +.btn-group:after { + clear: both; +} +.btn-group:first-child { + *margin-left: 0; +} +.btn-group + .btn-group { + margin-left: 5px; +} +.btn-toolbar { + margin-top: 9px; + margin-bottom: 9px; +} +.btn-toolbar .btn-group { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; +} +.btn-group .btn { + position: relative; + float: left; + margin-left: -1px; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} +.btn-group .btn:first-child { + margin-left: 0; + -webkit-border-top-left-radius: 4px; + -moz-border-radius-topleft: 4px; + border-top-left-radius: 4px; + -webkit-border-bottom-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; + border-bottom-left-radius: 4px; +} +.btn-group .btn:last-child, +.btn-group .dropdown-toggle { + -webkit-border-top-right-radius: 4px; + -moz-border-radius-topright: 4px; + border-top-right-radius: 4px; + -webkit-border-bottom-right-radius: 4px; + -moz-border-radius-bottomright: 4px; + border-bottom-right-radius: 4px; +} +.btn-group .btn.large:first-child { + margin-left: 0; + -webkit-border-top-left-radius: 6px; + -moz-border-radius-topleft: 6px; + border-top-left-radius: 6px; + -webkit-border-bottom-left-radius: 6px; + -moz-border-radius-bottomleft: 6px; + border-bottom-left-radius: 6px; +} +.btn-group .btn.large:last-child, +.btn-group .large.dropdown-toggle { + -webkit-border-top-right-radius: 6px; + -moz-border-radius-topright: 6px; + border-top-right-radius: 6px; + -webkit-border-bottom-right-radius: 6px; + -moz-border-radius-bottomright: 6px; + border-bottom-right-radius: 6px; +} +.btn-group .btn:hover, +.btn-group .btn:focus, +.btn-group .btn:active, +.btn-group .btn.active { + z-index: 2; +} +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} +.btn-group .dropdown-toggle { + padding-left: 8px; + padding-right: 8px; + -webkit-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + *padding-top: 3px; + *padding-bottom: 3px; +} +.btn-group .btn-mini.dropdown-toggle { + padding-left: 5px; + padding-right: 5px; + *padding-top: 1px; + *padding-bottom: 1px; +} +.btn-group .btn-small.dropdown-toggle { + *padding-top: 4px; + *padding-bottom: 4px; +} +.btn-group .btn-large.dropdown-toggle { + padding-left: 12px; + padding-right: 12px; +} +.btn-group.open { + *z-index: 1000; +} +.btn-group.open .dropdown-menu { + display: block; + margin-top: 1px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} +.btn-group.open .dropdown-toggle { + background-image: none; + -webkit-box-shadow: inset 0 1px 6px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 6px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 6px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} +.btn .caret { + margin-top: 7px; + margin-left: 0; +} +.btn:hover .caret, +.open.btn-group .caret { + opacity: 1; + filter: alpha(opacity=100); +} +.btn-mini .caret { + margin-top: 5px; +} +.btn-small .caret { + margin-top: 6px; +} +.btn-large .caret { + margin-top: 6px; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid #000000; +} +.btn-primary .caret, +.btn-warning .caret, +.btn-danger .caret, +.btn-info .caret, +.btn-success .caret, +.btn-inverse .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; + opacity: 0.75; + filter: alpha(opacity=75); +} +.modal-open .dropdown-menu { + z-index: 2050; +} +.modal-open .dropdown.open { + *z-index: 2050; +} +.modal-open .popover { + z-index: 2060; +} +.modal-open .tooltip { + z-index: 2070; +} +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: #000000; +} +.modal-backdrop.fade { + opacity: 0; +} +.modal-backdrop, +.modal-backdrop.fade.in { + opacity: 0.8; + filter: alpha(opacity=80); +} +.modal { + position: fixed; + top: 50%; + left: 50%; + z-index: 1050; + overflow: auto; + width: 560px; + margin: -250px 0 0 -280px; + background-color: #ffffff; + border: 1px solid #999; + border: 1px solid rgba(0, 0, 0, 0.3); + *border: 1px solid #999; + /* IE6-7 */ + + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -webkit-background-clip: padding-box; + -moz-background-clip: padding-box; + background-clip: padding-box; +} +.modal.fade { + -webkit-transition: opacity .3s linear, top .3s ease-out; + -moz-transition: opacity .3s linear, top .3s ease-out; + -ms-transition: opacity .3s linear, top .3s ease-out; + -o-transition: opacity .3s linear, top .3s ease-out; + transition: opacity .3s linear, top .3s ease-out; + top: -25%; +} +.modal.fade.in { + top: 50%; +} +.modal-header { + padding: 9px 15px; + border-bottom: 1px solid #eee; +} +.modal-header .close { + margin-top: 2px; +} +.modal-body { + overflow-y: auto; + max-height: 400px; + padding: 15px; +} +.modal-form { + margin-bottom: 0; +} +.modal-footer { + padding: 14px 15px 15px; + margin-bottom: 0; + text-align: right; + background-color: #f5f5f5; + border-top: 1px solid #ddd; + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; + -webkit-box-shadow: inset 0 1px 0 #ffffff; + -moz-box-shadow: inset 0 1px 0 #ffffff; + box-shadow: inset 0 1px 0 #ffffff; + *zoom: 1; +} +.modal-footer:before, +.modal-footer:after { + display: table; + content: ""; +} +.modal-footer:after { + clear: both; +} +.modal-footer .btn + .btn { + margin-left: 5px; + margin-bottom: 0; +} +.modal-footer .btn-group .btn + .btn { + margin-left: -1px; +} diff --git a/vendor/assets/stylesheets/vendor.scss b/vendor/assets/stylesheets/vendor.scss index b1a585e0a..9bac4e7b0 100644 --- a/vendor/assets/stylesheets/vendor.scss +++ b/vendor/assets/stylesheets/vendor.scss @@ -12,3 +12,5 @@ @import "codemirror/modes/diff"; @import "codemirror/modes/rpm-spec"; @import "codemirror/modes/tiddlywiki"; + +@import "bootstrap"