diff --git a/.openproject-token.pub b/.openproject-token.pub new file mode 100644 index 0000000000..7aaca5c835 --- /dev/null +++ b/.openproject-token.pub @@ -0,0 +1,24 @@ +-----BEGIN PUBLIC KEY----- +MIIEFjANBgkqhkiG9w0BAQEFAAOCBAMAMIID/gKCA/UAquIZchoog2ffcr9J2KSl +mlum6sN3smTVNsp9JGd1q4fr/kUFGch6q1cFEX3x5BGDXx7wPPI4ppKzeQHaxWmx +wxqs3eevcTFUEF9A2MPX7p5Ia0TbH4d7e7D9YMWvDXoQLggrxMFdUHY3ppUnBPgB ++EJG1Pv0FlBAdxYX0em7kLwhcp9PBP/zXso/qkkKK/pncyKizOLC3zv3E0ixcQ7o +Nq0aolTJFMHcqEquKaQN1jicDdzU6ks+YKh7kByZvVChe/InlroVXKrUa34hAZDM +acEkURJma3meN0IyPFA7fHRe1AhiNYF2MatNKysPrbOffYLOjamlaqmHTeJAec6e +vMHd+LlIz4xXivR0lY2wDawqp0waSLJaW8lZetOf0iwbqQkzZhz4sWDZopyGiqAU +v9/zS4OjUBr7JQbVcV3LIkzGWwNysSvTMrlvzCesYVsCwpLjP6gFxdclYJuTwEeL +o+T+AgoNyuj6ixhwHTJxIVhuBpebX44/YTYyUGMgItekDCH2Dxvtv2DaCL7YIqNG +ibvCyzCyLak7Tz97CMvCUf1EIRVfolbGpphi2Zzpoeqhfheh+0LQ3gmMBJpuLnJU +VXEtrOPunTkOrdUqL5rD/+mfd8yufsJ16Uk5j7gNUcIJsCcGWZ3Nhfidi3tvmJPF +H8HgNZ6W6smj9k7+TdZbRsH1LZp1LL/stLch3ffFHHcJye7d2t75uKiOoz1/1JMY +fl/wfaEaKvTGBKr/NFKcVDSBXHgx6VMA3oWV2AyLnTaY98XVPY1zzbyNKlNQkSaD +p/VCl334+YMwR9/5aJYJg58lljw6aBu+cozNkydKjCmqEpZNdR1tFusAY7jd/M2G +yXPBTdUJD6GrLx0Hot/wImJ30gOMgpeoetNUfM9/FimySLy65DRzCtLm1hthAlWM +Is5vFwMmFFSb3ozTsnkj9W16jHk9HdodKJfezzcPqu3TW2EMMNbbtXa3OPaHkht2 +huYJGnGu0hNhAj+x+KCxkpLveS0Ajw322qmtAqnwJLvfwShnz//cptNX5kXtrNGy ++o6I4jHibIATfaMKMt8gHmCZ381zAwrAOU7c0FQna+IkU8dgZDx7T+Xo1Q/GXuaO +1b2aT6geT60A3VgF+OBnoHe5Ext7vfNL9v0wcN5NLR5KgjexwEhcBcA2FauCTrVt +FUNgir+5XALd/wBlvxkvPKTJnQld/aK7xF0ui3c3/ryPX5cKzpfm9APK/hOFzkJ7 +ieARHqrQGqOYdClIUJIH0b/92dFq49Eqn1cKpztVzsU9xzdI/4w5JUSw/kbguVf7 +Yd0Rdc9KF/9WMwjzrWSti4meNBUO6/18cAognx0Pf5qsrSzOewIDAQAB +-----END PUBLIC KEY----- diff --git a/Gemfile b/Gemfile index d281629361..b260c9b7cd 100644 --- a/Gemfile +++ b/Gemfile @@ -153,6 +153,8 @@ gem 'nokogiri', '~> 1.6.8' gem 'fog-aws' gem 'carrierwave', git: 'https://github.com/carrierwaveuploader/carrierwave', branch: 'master' +gem 'openproject-token', '~> 1.0.0' + group :test do gem 'rack-test', '~> 0.6.3' gem 'shoulda-context', '~> 1.2' @@ -261,7 +263,7 @@ platforms :jruby do end group :opf_plugins do - gem 'openproject-translations', git: 'https://github.com/opf/openproject-translations.git', branch: 'release/6.1' + gem 'openproject-translations', git: 'https://github.com/opf/openproject-translations.git', branch: 'dev' end # TODO: Make this group :optional when bundler v10.x diff --git a/Gemfile.lock b/Gemfile.lock index 9a029bdfe8..36e63c2414 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -60,10 +60,10 @@ GIT GIT remote: https://github.com/opf/openproject-translations.git - revision: 1e8c4e52119f0ea690bf7d015798ae65b1af0f7a - branch: release/6.1 + revision: bbc216af7f0133fd191fbe7ea121a2c962a8491f + branch: dev specs: - openproject-translations (6.1.4) + openproject-translations (6.2.0) crowdin-api (~> 0.4.1) mixlib-shellout (~> 2.1.0) rails (~> 5.0.0) @@ -361,6 +361,8 @@ GEM nokogiri (1.6.8.1) mini_portile2 (~> 2.1.0) oj (2.17.4) + openproject-token (1.0.0) + activemodel (~> 5.0) parallel (1.9.0) parallel_tests (2.4.1) parallel @@ -648,6 +650,7 @@ DEPENDENCIES nokogiri (~> 1.6.8) oj (~> 2.17.4) omniauth! + openproject-token (~> 1.0.0) openproject-translations! parallel_tests (~> 2.4.1) passenger diff --git a/app/assets/images/enterprise_edition.png b/app/assets/images/enterprise_edition.png new file mode 100644 index 0000000000..abc36e1ee7 Binary files /dev/null and b/app/assets/images/enterprise_edition.png differ diff --git a/app/assets/stylesheets/content/_enterprise.sass b/app/assets/stylesheets/content/_enterprise.sass new file mode 100644 index 0000000000..f1bd7a7202 --- /dev/null +++ b/app/assets/stylesheets/content/_enterprise.sass @@ -0,0 +1,13 @@ +.upsale-notification + max-width: 560px + margin-bottom: 20px + padding-bottom: 20px + + .widget-box--teaser-image + width: 30% + float: right + margin-top: -20px + +.token-form textarea + font-family: "Lucida Console", Monaco, monospace + max-width: 560px diff --git a/app/assets/stylesheets/content/_ng_dialog.sass b/app/assets/stylesheets/content/_ng_dialog.sass index aff012d697..1f42e12c01 100644 --- a/app/assets/stylesheets/content/_ng_dialog.sass +++ b/app/assets/stylesheets/content/_ng_dialog.sass @@ -46,6 +46,10 @@ // Required for close icon position: relative + .ngdialog-body + margin-top: 2rem + min-height: 50px + .ngdialog-close cursor: pointer position: absolute diff --git a/app/assets/stylesheets/content/_widget_box.sass b/app/assets/stylesheets/content/_widget_box.sass index bad7979f4d..27f1d975e6 100644 --- a/app/assets/stylesheets/content/_widget_box.sass +++ b/app/assets/stylesheets/content/_widget_box.sass @@ -49,7 +49,8 @@ $widget-box--enumeration-width: 20px flex-direction: column .widget-box--enumeration, - .widget-box--arrow-links + .widget-box--arrow-links, + .widget-box--feature-list flex-grow: 2 .icon-context:before @@ -125,6 +126,28 @@ $widget-box--enumeration-width: 20px //necessary for correct alignment even with long texts width: calc(100% - #{$widget-box--enumeration-width}) + .widget-box--feature-list + list-style: none + margin: 0.5rem 0 1rem 0 + + &:last-child + margin-bottom: 0 + + li:before + @include icon-common + @extend .icon-notice + @extend .icon-notice:before + display: inline-block + font-size: 0.6rem + color: $alternative-color + width: $widget-box--enumeration-width + + + .widget-box--teaser-image + height: 100px + margin-left: auto + margin-right: auto + @include breakpoint(680px down) .widget-boxes &.-flex diff --git a/app/assets/stylesheets/default.css.sass b/app/assets/stylesheets/default.css.sass index 0a14da1327..77eaa4e51f 100644 --- a/app/assets/stylesheets/default.css.sass +++ b/app/assets/stylesheets/default.css.sass @@ -60,6 +60,7 @@ @import content/links @import content/action_menu_main @import content/legacy_actions +@import content/enterprise @import content/my_page @import content/project_overview @import content/news diff --git a/app/assets/stylesheets/specific/onboarding.sass b/app/assets/stylesheets/specific/onboarding.sass index d690a3a673..4e4944151c 100644 --- a/app/assets/stylesheets/specific/onboarding.sass +++ b/app/assets/stylesheets/specific/onboarding.sass @@ -27,8 +27,10 @@ //++ // Align ngDialog's close button -.onboarding-modal .ngdialog-close +.onboarding-modal .ngdialog-close, +.ngdialog-theme-openproject .ngdialog-close line-height: $header-height + color: $header-item-font-color .onboarding--top-menu display: flex diff --git a/app/controllers/enterprises_controller.rb b/app/controllers/enterprises_controller.rb new file mode 100644 index 0000000000..ddd1a5e6dc --- /dev/null +++ b/app/controllers/enterprises_controller.rb @@ -0,0 +1,67 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2017 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public token version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public token +# as published by the Free Software Foundation; either version 2 +# of the token, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public token for more details. +# +# You should have received a copy of the GNU General Public token +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See doc/COPYRIGHT.rdoc for more details. +#++ +class EnterprisesController < ApplicationController + layout 'admin' + menu_item :enterprise + + before_action :require_admin + + def show + @current_token = EnterpriseToken.current + @token = @current_token || EnterpriseToken.new + end + + def create + @token = EnterpriseToken.current || EnterpriseToken.new + @token.encoded_token = params[:enterprise_token][:encoded_token] + + if @token.save + flash[:notice] = t(:notice_successful_update) + redirect_to action: :show + else + render action: :show + end + end + + def destroy + token = EnterpriseToken.current + if token + token.destroy + flash[:notice] = t(:notice_successful_delete) + redirect_to action: :show + else + render_404 + end + end + + private + + def default_breadcrumb + t(:label_enterprise) + end +end diff --git a/app/models/enterprise_token.rb b/app/models/enterprise_token.rb new file mode 100644 index 0000000000..acefe8c899 --- /dev/null +++ b/app/models/enterprise_token.rb @@ -0,0 +1,107 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2017 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See doc/COPYRIGHT.rdoc for more details. +#++ +class EnterpriseToken < ActiveRecord::Base + class << self + def current + RequestStore.fetch(cache_key) do + set_current_token + end + end + + def cache_key + RequestStore.fetch(:current_ee_token_updated_at) { EnterpriseToken.maximum(:updated_at) } + most_recent_update = (RequestStore[:current_ee_token_updated_at] || Time.now.utc).to_i + "/openproject/ee/#{most_recent_update}" + end + + def clear_cache(key = cache_key) + Rails.cache.delete(key) + RequestStore.delete key + RequestStore.delete :current_ee_token_updated_at + end + + def allows_to?(action) + Authorization::EnterpriseService.new(current).call(action).result + end + + def show_banners + !current || current.expired? + end + + def set_current_token + token = EnterpriseToken.order('created_at DESC').first + + if token && token.token_object + token + end + end + end + + validates_presence_of :encoded_token + validate :valid_token_object + + before_save :unset_current_token + before_destroy :unset_current_token + + delegate :will_expire?, + :expired?, + :subscriber, + :mail, + :issued_at, + :starts_at, + :expires_at, + :restrictions, + to: :token_object + + def token_object + @token_object = load_token unless defined?(@token_object) + @token_object + end + + def allows_to?(action) + Authorization::EnterpriseService.new(self).call(action).result + end + + def unset_current_token + # Clear current cache + self.class.clear_cache + end + + private + + def load_token + OpenProject::Token.import(encoded_token) + rescue OpenProject::Token::ImportError => error + Rails.logger.error "Failed to load EE token: #{error}" + nil + end + + def valid_token_object + errors.add(:encoded_token, :unreadable) unless token_object + end +end diff --git a/app/models/queries/relations/relation_query.rb b/app/models/queries/relations/relation_query.rb index ea8b51db59..bc9a2a67bb 100644 --- a/app/models/queries/relations/relation_query.rb +++ b/app/models/queries/relations/relation_query.rb @@ -33,7 +33,7 @@ module Queries Relation end - def self.default_scope + def default_scope Relation.all end end diff --git a/app/services/authorization/enterprise_service.rb b/app/services/authorization/enterprise_service.rb new file mode 100644 index 0000000000..2be318513c --- /dev/null +++ b/app/services/authorization/enterprise_service.rb @@ -0,0 +1,64 @@ +#-- encoding: UTF-8 +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2017 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See doc/COPYRIGHT.rdoc for more details. +#++ + +class Authorization::EnterpriseService + attr_accessor :token + + def initialize(token) + self.token = token + end + + # Return a true ServiceResult if the token contains this particular action. + def call(action) + + allowed = + if token.nil? || token.expired? + false + else + process(action) + end + + result(allowed) + end + + private + + def process(action) + case action + when :define_custom_style + true # Every non-expired token + else + false + end + end + + def result(bool) + ServiceResult.new(success: bool, result: bool) + end +end diff --git a/app/views/enterprises/_current.html.erb b/app/views/enterprises/_current.html.erb new file mode 100644 index 0000000000..ecbaae9439 --- /dev/null +++ b/app/views/enterprises/_current.html.erb @@ -0,0 +1,38 @@ +
+
+
+
<%= EnterpriseToken.human_attribute_name(:subscriber) %>
+
+
+ <%= @current_token.subscriber %> +
+
+
<%= EnterpriseToken.human_attribute_name(:mail) %>
+
+
+ <%= @current_token.mail %> +
+
+
<%= EnterpriseToken.human_attribute_name(:starts_at) %>
+
+
+ <%= format_date @current_token.starts_at %> +
+
+ <% if @current_token.will_expire? %> +
<%= EnterpriseToken.human_attribute_name(:expires_at) %>
+
+
+ <%= format_date @current_token.expires_at %> +
+
+ <% end %> +
+
+
+ +<%= form_tag({ action: :destroy }, + method: :delete, + class: 'confirm-form-submit') do %> + <%= styled_button_tag l(:button_delete), type: 'submit', class: '-with-icon icon-delete' %> +<% end %> diff --git a/app/views/enterprises/_form.html.erb b/app/views/enterprises/_form.html.erb new file mode 100644 index 0000000000..9c7f2637f0 --- /dev/null +++ b/app/views/enterprises/_form.html.erb @@ -0,0 +1,21 @@ +<% if @current_token.present? %> + +<% else %> + +<% end %> +<%= labelled_tabular_form_for @token, + url: { action: :create }, + html: { class: 'token-form' }, + method: :post do |f|%> + +
+ <%= f.text_area :encoded_token, :cols => 60, :rows => 15, placeholder: t('admin.enterprise.paste'), autocomplete: "off", autocorrect: "off", autocapitalize: "off", spellcheck: false %> +
+ +
+ <%= styled_button_tag t(:button_create), id: 'token-submit-button', class: '-highlight -with-icon icon-checkmark' %> + <%= link_to t(:button_cancel), { action: :show }, class: 'button' %> +
+<% end %> +
\ No newline at end of file diff --git a/app/views/enterprises/show.html.erb b/app/views/enterprises/show.html.erb new file mode 100644 index 0000000000..05422b8d4b --- /dev/null +++ b/app/views/enterprises/show.html.erb @@ -0,0 +1,48 @@ +<% html_title t(:label_administration), t(:label_enterprise_edition) %> + +<%= toolbar title: t(:label_enterprise_edition) do %> +
  • + <%= link_to( "#{OpenProject::Static::Links.links[:upsale][:href]}?utm_source=ce-token-admin", + { class: 'button -alt-highlight', + title: t('admin.enterprise.order')}) do %> + + <%= t('admin.enterprise.order') %> + <% end %> +
  • +<% end %> + +<%= error_messages_for 'token' %> + +<% if @current_token.present? %> + <%= render partial: "current" %> +<% else %> +
    +
    +

    <%= t('admin.enterprise.upgrade_to_ee') %>

    + <%= image_tag "enterprise_edition.png", class: "widget-box--teaser-image" %> + +

    <%= t('homescreen.blocks.upsale.description') %>

    + +
      +
    • + <%= t('homescreen.blocks.upsale.additional_features') %> +
    • +
    • + <%= t('homescreen.blocks.upsale.professional_support') %> +
    • +
    +

    + <%= t('homescreen.blocks.upsale.become_hero') %> <%= t('homescreen.blocks.upsale.you_contribute') %> +

    + <%= link_to( "#{OpenProject::Static::Links.links[:upsale][:href]}?utm_source=ce-enterprise-admin", + { class: 'button -alt-highlight', + aria: {label: t('admin.enterprise.order')}, + title: t('admin.enterprise.order')}) do %> + + <%= t('admin.enterprise.order') %> + <% end %> +
    +
    +<% end %> + +<%= render partial: "form" %> diff --git a/app/views/homescreen/blocks/_upsale.html.erb b/app/views/homescreen/blocks/_upsale.html.erb new file mode 100644 index 0000000000..2d98ca26f8 --- /dev/null +++ b/app/views/homescreen/blocks/_upsale.html.erb @@ -0,0 +1,30 @@ +

    + + <%= t('homescreen.blocks.upsale.title') %> +

    + +<%= image_tag "enterprise_edition.png", class: "widget-box--teaser-image" %> + +

    <%= t('homescreen.blocks.upsale.description') %>

    + + +

    + <%= t('homescreen.blocks.upsale.become_hero') %> <%= t('homescreen.blocks.upsale.you_contribute') %> +

    + +
    + <%= link_to "#{OpenProject::Static::Links.links[:upsale][:href]}?utm_source=ce-homescreen", + { class: 'button -alt-highlight', + aria: {label: t('homescreen.blocks.upsale.more_info')}, + title: t('homescreen.blocks.upsale.more_info')} do %> + + <%= t('homescreen.blocks.upsale.more_info') %> + <% end %> +
    diff --git a/app/views/timelines/show.html.erb b/app/views/timelines/show.html.erb index 4645c694e0..bf2ed6b9da 100644 --- a/app/views/timelines/show.html.erb +++ b/app/views/timelines/show.html.erb @@ -27,13 +27,14 @@ See doc/COPYRIGHT.rdoc for more details. ++#%> -<% html_title "#{l('timelines.project_menu.timelines')}: #{h @timeline.name}" %> +<% html_title "#{t('timelines.project_menu.timelines')}: #{h @timeline.name}" %> + <%= toolbar title: @timeline.name do %> <% if timeline_action_authorized?(:new) %>
  • <%= new_timeline_link @project do %> - <%= l('timelines.timeline') %> + <%= t('timelines.timeline') %> <% end %>
  • <% end %> @@ -42,7 +43,7 @@ See doc/COPYRIGHT.rdoc for more details.
  • <%= edit_timeline_link @project, @timeline do %> - <%= l(:button_edit) %> + <%= t(:button_edit) %> <%= h(@timeline.name) %> <% end %>
  • @@ -52,7 +53,7 @@ See doc/COPYRIGHT.rdoc for more details.
  • <%= destroy_timeline_link @project, @timeline do %> - <%= l(:button_delete) %> + <%= t(:button_delete) %> <%= h(@timeline.name) %> <% end %>
  • @@ -61,7 +62,7 @@ See doc/COPYRIGHT.rdoc for more details.
  • <%= link_to url_for(controller: '/reportings', action: 'index', project_id: @project.id), class: 'button' do %> - <%= l('timelines.project_menu.reportings') %> + <%= t('timelines.project_menu.reportings') %> <% end %>
  • <% end %> @@ -69,7 +70,7 @@ See doc/COPYRIGHT.rdoc for more details.
    <%= form_tag '', {id: "specialForm", 'ng-controller' => 'TimelineSelectionController'} do %>
    - <%= label_tag 'timeline_select', l("timelines.timeline"), class: "form--label -transparent" %> + <%= label_tag 'timeline_select', t("timelines.timeline"), class: "form--label -transparent" %>