diff --git a/.circleci/config.yml b/.circleci/config.yml index 367e622..fe106e9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,10 @@ # # Check https://circleci.com/docs/2.0/language-ruby/ for more details # -version: 2 +orbs: + aws-ecr: circleci/aws-ecr@6.2.0 + +version: 2.1 general: artifacts: @@ -55,12 +58,37 @@ jobs: cp -r coverage/${CIRCLE_PROJECT_REPONAME}_test /tmp/coverage/ - store_artifacts: path: /tmp/coverage + deploy_heroku: + machine: + image: circleci/classic:edge + steps: + - checkout + - run: + name: 'Install Heroku CLI, if necessary' + command: | + curl https://cli-assets.heroku.com/install.sh | sh + - run: + name: 'Build Docker Image' + command: | + heroku -v + docker build --build-arg=COMMIT=${CIRCLE_SHA1} \ + --build-arg=BRANCH=${CIRCLE_BRANCH} -t registry.heroku.com/${HEROKU_APP_NAME}/web . + - run: + name: 'Release Docker Image' + command: | + docker login --username=_ --password=$HEROKU_AUTH_TOKEN registry.heroku.com + docker push registry.heroku.com/${HEROKU_APP_NAME}/web + #heroku container:push web --app ${HEROKU_APP_NAME} + heroku container:release web --app ${HEROKU_APP_NAME} workflows: - version: 2 - build_and_test: + build_test_push: jobs: - - build + - build: + filters: + branches: + ignore: + - /ignore-build-.*/ - test: requires: - build @@ -68,4 +96,28 @@ workflows: branches: ignore: - /v0.1.x-support-Redmine3.*/ + - aws-ecr/build-and-push-image: + account-url: AWS_ECR_ACCOUNT_URL + aws-access-key-id: AWS_ACCESS_KEY_ID + aws-secret-access-key: AWS_SECRET_ACCESS_KEY + create-repo: true + dockerfile: Dockerfile + path: . + region: AWS_REGION + repo: redmine_banner + extra-build-args: '--build-arg COMMIT=$CIRCLE_SHA1 --build-arg=BRANCH=$CIRCLE_BRANCH' + requires: + - test + filters: + branches: + only: + - master + - deploy_heroku: + requires: + - build + filters: + branches: + ignore: + - master + diff --git a/.rubocop.yml b/.rubocop.yml index bbce20b..7cd3c92 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,7 +1,6 @@ inherit_from: .rubocop_todo.yml AllCops: - # TODO: Change TargetRubyVersion to 2.0 or higher, when Redmine cloded to support ruby 1.9x. - TargetRubyVersion: 2.2 + TargetRubyVersion: 2.3 Style/IfUnlessModifier: Enabled: false diff --git a/Dockerfile b/Dockerfile index f42f4d9..00699fc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,20 @@ +# +# docker build --build-arg=COMMIT=$(git rev-parse --short HEAD) \ +# --build-arg=BRANCH=$(git name-rev --name-only HEAD) -t akiko/redmine_banner:latest . +# +# FROM ruby:2.5 LABEL maintainer="AKIKO TAKANO / (Twitter: @akiko_pusu)" \ description="Image to run Redmine simply with sqlite to try/review plugin." +ARG BRANCH="master" +ARG COMMIT="commit_sha" + +ENV COMMIT_SHA=${COMMIT} +ENV COMMIT_BRANCH=${BRANCH} + +RUN mkdir /app + ### get Redmine source ### Replace shell with bash so we can source files ### RUN rm /bin/sh && ln -s /bin/bash /bin/sh @@ -9,33 +22,38 @@ RUN rm /bin/sh && ln -s /bin/bash /bin/sh ### install default sys packeges ### RUN apt-get update -RUN apt-get install -qq -y \ +RUN apt-get install -qq -y --no-install-recommends \ git vim subversion \ - sqlite3 default-libmysqlclient-dev -RUN apt-get install -qq -y build-essential libc6-dev + sqlite3 && rm -rf /var/lib/apt/lists/* -RUN cd /tmp && svn co http://svn.redmine.org/redmine/branches/4.0-stable/ redmine -WORKDIR /tmp/redmine +RUN cd /app && svn co http://svn.redmine.org/redmine/branches/4.0-stable/ redmine +WORKDIR /app/redmine +COPY . /app/redmine/plugins/redmine_banner/ # add database.yml (for development, development with mysql, test) RUN echo $'test:\n\ adapter: sqlite3\n\ - database: /tmp/data/redmine_test.sqlite3\n\ + database: /app/data/redmine_test.sqlite3\n\ encoding: utf8mb4\n\ development:\n\ adapter: sqlite3\n\ - database: /tmp/data/redmine_development.sqlite3\n\ - encoding: utf8mb4\n\ -development_mysql:\n\ - adapter: mysql2\n\ - host: mysql\n\ - password: pasword\n\ - database: redemine_development\n\ - username: root\n'\ + database: /app/data/redmine_development.sqlite3\n\ + encoding: utf8mb4\n'\ >> config/database.yml RUN gem update bundler -RUN bundle install --without postgresql rmagick -RUN bundle exec rake db:migrate - +RUN bundle install --without postgresql rmagick mysql +RUN bundle exec rake db:migrate && bundle exec rake redmine:plugins:migrate \ + && bundle exec rake generate_secret_token +RUN bundle exec rails runner \ + "Setting.send('plugin_redmine_banner=', {enable: 'true', type: 'info', display_part: 'both', banner_description: 'This is a test message for Global Banner. (${COMMIT_BRANCH}:${COMMIT_SHA})'}.stringify_keys)" + +# Change Admin's password to 'redmine_banner_${COMMIT_SHA}' +# Default is 'redmine_banner_commit_sha' +RUN bundle exec rails runner \ + "User.find_by_login('admin').update!(password: 'redmine_banner_${COMMIT_SHA}', must_change_passwd: false)" + +EXPOSE 3000 +RUN ls /app/redmine +CMD ["rails", "server", "-b", "0.0.0.0"] diff --git a/README.md b/README.md index 186e005..6b84ade 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,19 @@ Please use ver **0.1.x** or ``v0.1.x-support-Redmine3`` branch in case using Red ## Changelog +### 0.2.1 + +* Fix: Prevent conflict with CKEditor. (GitHub: #111) +* Code refactoring. +* Add feature to update Global Banner via API. (Alpha / Related: #86 #113) + * Not only Redmine admin but also user who assigned group named **GlobalBanner_Admin** can also update Global banner via API. + * Even prptotype version. + * Please see [swagger.yml](script/swagger.yml) to try update global banner via API. +* Update CI Setting + * Add step to build and push image to AWS ECR. + * Add steps to build and deploy to Heroku Container registry as release container service. +* Add how to try banner via Docker in README. + ### 0.2.0 * Support Redmine 4.x. @@ -129,6 +142,44 @@ NOTE: Mainly, maintenance, bugfix and refactoring only. There is no additional f * First release +### Quick try with using Docker + +You can try quickly this plugin with Docker environment. +Please try: + +```bash +# Admin password is 'redmine_banner_commit_sha' +$ https://github.com/akiko-pusu/redmine_banner +$ docker-compose up web -d + +# or +# +# Admin password is 'redmine_banner_{COMMIT}' +$ docker build --build-arg=COMMIT=$(git rev-parse --short HEAD) \ + --build-arg=BRANCH=$(git name-rev --name-only HEAD) -t akiko/redmine_banner:latest . + +$ docker run -p 3000:3000 akiko/redmine_banner:latest +``` + +### Run test + +Please see wercker.yml for more details. + +```bash +% cd REDMINE_ROOT_DIR +% cp plugins/redmine_banner/Gemfile.local plugins/redmine_banner/Gemfile +% bundle install --with test +% export RAILS_ENV=test +% bundle exec ruby -I"lib:test" -I plugins/redmine_banner/test plugins/ \ + redmine_banner/test/controller/global_banner_controller_test.rb +``` + +or + +```bash +% bundle exec rails redmine_banner:test +``` + ### Repository * diff --git a/_config.yml b/_config.yml deleted file mode 100644 index 2f7efbe..0000000 --- a/_config.yml +++ /dev/null @@ -1 +0,0 @@ -theme: jekyll-theme-minimal \ No newline at end of file diff --git a/_layouts/default.html b/_layouts/default.html deleted file mode 100644 index c2a5ecf..0000000 --- a/_layouts/default.html +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - -{% seo %} - - - - -
-
-

{{ site.title | default: site.github.repository_name }}

- - {% if site.logo %} - Logo - {% endif %} - -

{{ site.description | default: site.github.project_tagline }}

- - {% if site.github.is_user_page %} -

View My GitHub Profile

- {% endif %} - - {% if site.show_downloads %} - - {% endif %} -
-
- - {{ content }} - -
- -
- - {% if site.google_analytics %} - - {% endif %} - - diff --git a/app/controllers/banners/api/global_banner_controller.rb b/app/controllers/banners/api/global_banner_controller.rb new file mode 100644 index 0000000..c6bf9dc --- /dev/null +++ b/app/controllers/banners/api/global_banner_controller.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +module Banners + module Api + class GlobalBannerController < ApplicationController + # TODO: This group should be customize. + GLOBAL_BANNER_ADMIN_GROUP = 'GlobalBanner_Admin' + + before_action :require_login, :require_banner_admin + accept_api_auth :show, :register_banner + + def show + render json: global_banner_json + end + + def register_banner + begin + global_banner_params = build_params + rescue ActionController::ParameterMissing, ActionController::UnpermittedParameters => e + response_bad_request(e.message) && (return) + end + + retval = Setting.send('plugin_redmine_banner=', global_banner_params.stringify_keys) + + if retval + render status: 200, json: { status: 'OK', message: 'updating the global banner' } + else + response_bad_request("Can't save data to settings table.") + end + rescue StandardError => e + response_internal_server_error(e.message) + end + + private + + # TODO: Private methods should be refactoring. + # TODO: Validation is required + def build_params + valid_params(params) + end + + def global_banner_json + { global_banner: Setting['plugin_redmine_banner'] } + end + + # 400 Bad Request + def response_bad_request(error_message = nil) + json_data = { status: 400, message: 'Bad Request' } + json_data.merge!(reason: error_message) if error_message.present? + logger.warn("Global Banner Update failed. Caused: #{json_data}") + render status: 400, json: json_data + end + + # 401 Unauthorized + def response_unauthorized + render status: 401, json: { status: 401, message: 'Unauthorized' } + end + + # 500 Internal Server Error + def response_internal_server_error(error_message = nil) + json_data = { status: 500, message: 'Internal Server Error' } + json_data.merge!(reason: error_message) if error_message.present? + logger.warn("Global Banner Update failed. Caused: #{json_data}") + render status: 500, json: json_data + end + + def require_banner_admin + return if User.current.admin? || banner_admin?(User.current) + + response_unauthorized + end + + def banner_admin?(user) + banner_admin_group = Group.find_by_lastname(GLOBAL_BANNER_ADMIN_GROUP) + return false if banner_admin_group.blank? + + banner_admin_group.users.include?(user) + end + + def valid_params(params) + params.require(:global_banner).permit( + :banner_description, + :display_part, + :enable, + :end_hour, :end_min, :end_ymd, + :related_link, + :start_hour, :start_min, :start_ymd, + :type, + :use_timer + ) + end + end + end +end diff --git a/config/routes.rb b/config/routes.rb index d695a50..ad2a00b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + Rails.application.routes.draw do concern :previewable do post 'preview', on: :collection @@ -17,4 +19,13 @@ post 'off', on: :collection get 'off', on: :collection end + + namespace 'banners' do + namespace 'api' do + resource :global_banner, only: %i[register_banner] do + put '/', to: 'global_banner#register_banner', on: :member + get '/', to: 'global_banner#show', on: :member + end + end + end end diff --git a/docker-compose.yml b/docker-compose.yml index bc86cd4..b2f64bb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,5 +21,6 @@ services: volumes: - .:/tmp/redmine/plugins/redmine_banner ports: - - 3000:3000 + - "3000:3000" + diff --git a/init.rb b/init.rb index fbb1c22..579e832 100644 --- a/init.rb +++ b/init.rb @@ -4,7 +4,7 @@ require 'banners/projects_helper_patch' # NOTE: Keep error message for a while to support Redmine3.x users. -def issue_template_version_message(original_message = nil) +def banner_version_message(original_message = nil) <<-"USAGE" ========================== #{original_message} @@ -21,7 +21,7 @@ def issue_template_version_message(original_message = nil) author 'Akiko Takano' author_url 'http://twitter.com/akiko_pusu' description 'Plugin to show site-wide message, such as maintenacne informations or notifications.' - version '0.2.0' + version '0.2.1' requires_redmine version_or_higher: '4.0' url 'https://github.com/akiko-pusu/redmine_banner' @@ -44,7 +44,13 @@ def issue_template_version_message(original_message = nil) project_module :banner do permission :manage_banner, { banner: %I[show edit project_banner_off] }, require: :member end + + Rails.configuration.to_prepare do + unless SettingsController.included_modules.include?(Banners::SettingsControllerPatch) + SettingsController.send(:prepend, Banners::SettingsControllerPatch) + end + end rescue ::Redmine::PluginRequirementError => e - raise ::Redmine::PluginRequirementError.new(issue_template_version_message(e.message)) # rubocop:disable Style/RaiseArgs + raise ::Redmine::PluginRequirementError.new(banner_version_message(e.message)) # rubocop:disable Style/RaiseArgs end end diff --git a/lib/banners/banner_helper.rb b/lib/banners/banner_helper.rb index e9343fb..9bee245 100644 --- a/lib/banners/banner_helper.rb +++ b/lib/banners/banner_helper.rb @@ -1,8 +1,15 @@ +# frozen_string_literal: true + module Banners module BannerHelper - def get_time(ymd, h, m) - d = Date.strptime(ymd, '%Y-%m-%d') - Time.mktime(d.year, d.month, d.day, h.to_i, m.to_i) + def get_time(ymd, hour, minute) + begin + d = Date.strptime(ymd, '%Y-%m-%d') + rescue StandardError => e + logger.warn("Passed value #{ymd} for Banner has wrong format. #{e.message}") + d = Date.current + end + Time.mktime(d.year, d.month, d.day, hour.to_i, minute.to_i) end def enabled?(project) diff --git a/lib/banners/settings_controller_patch.rb b/lib/banners/settings_controller_patch.rb index 0b5aefb..9e074c4 100644 --- a/lib/banners/settings_controller_patch.rb +++ b/lib/banners/settings_controller_patch.rb @@ -91,4 +91,3 @@ def validate_date_range?(param_settings) end end -SettingsController.prepend Banners::SettingsControllerPatch diff --git a/redmine_banner_docker/Dockerfile b/redmine_banner_docker/Dockerfile new file mode 100644 index 0000000..190c329 --- /dev/null +++ b/redmine_banner_docker/Dockerfile @@ -0,0 +1,57 @@ +# +# docker build --build-arg=COMMIT=$(git rev-parse --short HEAD) \ +# --build-arg=BRANCH=$(git name-rev --name-only HEAD) -t akiko/redmine_banner:latest . +# +# +FROM ruby:2.5 +LABEL maintainer="AKIKO TAKANO / (Twitter: @akiko_pusu)" \ + description="Image to run Redmine simply with sqlite to try/review plugin." + +ARG BRANCH="master" +ARG COMMIT="commit_sha" + +ENV COMMIT_SHA=${COMMIT} +ENV COMMIT_BRANCH=${BRANCH} + + +### get Redmine source +### Replace shell with bash so we can source files ### +RUN rm /bin/sh && ln -s /bin/bash /bin/sh + +### install default sys packeges ### + +RUN apt-get update +RUN apt-get install -qq -y --no-install-recommends \ + git vim subversion \ + sqlite3 && rm -rf /var/lib/apt/lists/* + +RUN cd /tmp && svn co http://svn.redmine.org/redmine/branches/4.0-stable/ redmine +WORKDIR /tmp/redmine + +COPY . /tmp/redmine/plugins/redmine_banner/ + +# add database.yml (for development, development with mysql, test) +RUN echo $'test:\n\ + adapter: sqlite3\n\ + database: /tmp/data/redmine_test.sqlite3\n\ + encoding: utf8mb4\n\ +development:\n\ + adapter: sqlite3\n\ + database: /tmp/data/redmine_development.sqlite3\n\ + encoding: utf8mb4\n'\ +>> config/database.yml + +RUN gem update bundler +RUN bundle install --without postgresql rmagick mysql +RUN bundle exec rake db:migrate && bundle exec rake redmine:plugins:migrate \ + && bundle exec rake generate_secret_token +RUN bundle exec rails runner \ + "Setting.send('plugin_redmine_banner=', {enable: 'true', type: 'info', display_part: 'both', banner_description: 'This is a test message for Global Banner. (${COMMIT_BRANCH}:${COMMIT_SHA})'}.stringify_keys)" + +# Change Admin's password to 'redmine_banner_${COMMIT_SHA}' +# Default is 'redmine_banner_commit_sha' +RUN bundle exec rails runner \ + "User.find_by_login('admin').update!(password: 'redmine_banner_${COMMIT_SHA}', must_change_passwd: false)" + +EXPOSE 3000 +CMD ["rails", "server", "-b", "0.0.0.0"] diff --git a/script/swagger.yml b/script/swagger.yml new file mode 100644 index 0000000..5767f4a --- /dev/null +++ b/script/swagger.yml @@ -0,0 +1,173 @@ +swagger: "2.0" +info: + description: | + Here is a API to handle Redmine's site wide banner. + Now prototype version. + version: "1.0.0-α" + title: "GlobalBanner API" + termsOfService: "http://swagger.io/terms/" + contact: + email: "akiko.pusu@gmail.com" +schemes: + - "http" + - "https" +host: "localhost:3000" +paths: + /banners/api/global_banner.json: + get: + summary: "Show Global Banner setting" + description: | + Show Global Banner setting as JSON format stored in a settings table. + + Only Redmine Administrator or user assigned to group named **"GlobalBanner_Admin"** can use this api. + The way to authenticate follows Redmine itself. + - Exp. User parameter ?key=YOUR_API_KEY or HTTP Header: X-Redmine-API-Key and so on. + - Please see: http://www.redmine.org/projects/redmine/wiki/Rest_api#Authentication + parameters: + - name: "X-Redmine-API-Key" + in: "header" + description: "Your API KEY" + required: true + type: "string" + responses: + 200: + description: "Response in success" + schema: + $ref: '#/definitions/global_banner_body' + 401: + description: "Response in Unauthorized." + schema: + type: "object" + properties: + status: + type: "string" + example: "401" + message: + type: "string" + example: "Unauthorized" + put: + summary: "Update Global Banner setting" + description: | + Update Global Banner setting as JSON format stored in a settings table. + + Only Redmine Administrator or user assigned to group named **"GlobalBanner_Admin"** can use this api. + The way to authenticate follows Redmine itself. + - Exp. User parameter ?key=YOUR_API_KEY or HTTP Header: X-Redmine-API-Key and so on. + - Please see: http://www.redmine.org/projects/redmine/wiki/Rest_api#Authentication + consumes: + - application/json + parameters: + - name: "X-Redmine-API-Key" + in: "header" + description: "Your API KEY" + required: true + type: "string" + - in: body + name: global_banner + description: Data to update Global Banner. + required: true + schema: + $ref: '#/definitions/global_banner_body' + responses: + 200: + description: "Response in success." + schema: + type: "object" + properties: + status: + type: "string" + example: "OK" + message: + type: "string" + example: "updatig the global banner" + 401: + description: "Response in Unauthorized." + schema: + type: "object" + properties: + status: + type: "string" + example: "401" + message: + type: "string" + example: "Unauthorized" + 500: + description: "Response in internal error." + schema: + type: "object" + properties: + status: + type: "string" + example: "500" + message: + type: "string" + example: "Internal Server Error" + +definitions: + global_banner_body: + type: "object" + properties: + global_banner: + $ref: "#/definitions/Globel_banner" # Storeを呼び出す + Globel_banner: + type: "object" + required: + - banner_description + - display_part + - type + properties: + banner_description: + type: string + example: "Message for Global Banner" + display_part: + type: "string" + example: "both" + enum: [header, footer, both] + description: > + Display part: + * `header` - Display banner on the top + * `footer` - Display banner on the bottom + * `both` - Display banner both header and footer + enable: + type: "string" + example: "true" + enum: [true, false] + end_hour: + type: integer + example: 16 + maximum: 23 + end_min: + type: integer + example: 31 + maximum: 59 + end_ymd: + type: "string" + example: "2019-08-21" + related_link: + type: "string" + example: "http://localhost:3000/news" + start_hour: + type: integer + example: 16 + maximum: 23 + start_min: + type: integer + example: 31 + maximum: 59 + start_ymd: + type: "string" + example: "2019-08-20" + type: + type: "string" + example: "info" + enum: [info, warn, alert, normal, nodata] + description: > + type: + * `info` - Info style. (Pale blue) + * `warn` - Warning style. (Yellow) + * `alert` - Alert style. (Pale red) + * `normal` - White and without status icon. + * `nodata` - Redmine's style. + use_timer: + type: "string" + diff --git a/test/controller/global_banner_controller_test.rb b/test/controller/global_banner_controller_test.rb new file mode 100644 index 0000000..1912307 --- /dev/null +++ b/test/controller/global_banner_controller_test.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require File.expand_path('../test_helper', __dir__) +class GlobalBannerControllerTest < Redmine::IntegrationTest + fixtures :users + + def setup + @user = User.find_by_login('admin') + + @banner_admin_group = Group.create(name: 'GlobalBanner_Admin') + @non_admin_user = User.find(2) + end + + def test_routing + assert_routing({ path: '/banners/api/global_banner', method: :get }, controller: 'banners/api/global_banner', action: 'show') + assert_routing({ path: '/banners/api/global_banner', method: :put }, controller: 'banners/api/global_banner', action: 'register_banner') + end + + # Case Admin + def test_get_global_banner_when_api_disable + Setting.rest_api_enabled = '0' + get '/banners/api/global_banner.json', headers: { 'X-Redmine-API-Key' => @user.api_key } + assert_response 403 + end + + def test_get_global_banner_when_api_enable + Setting.rest_api_enabled = '1' + get '/banners/api/global_banner.json', headers: { 'X-Redmine-API-Key' => @user.api_key } + assert_response :success + end + + # Case non-admin user (jsmith) + def test_get_global_banner_when_api_enable_and_non_banner_admin_group + Setting.rest_api_enabled = '1' + get '/banners/api/global_banner.json', headers: { 'X-Redmine-API-Key' => @non_admin_user.api_key } + assert_response 401 + end + + def test_get_global_banner_when_api_enable_and_banner_admin_group + Setting.rest_api_enabled = '1' + + @banner_admin_group.user_ids = [@non_admin_user.id] + @banner_admin_group.save! + + get '/banners/api/global_banner.json', headers: { 'X-Redmine-API-Key' => @non_admin_user.api_key } + assert_response :success + end + + def test_put_global_banner_when_api_enable_and_banner_admin_group_and_empty_data + Setting.rest_api_enabled = '1' + + @banner_admin_group.user_ids = [@non_admin_user.id] + @banner_admin_group.save! + + put '/banners/api/global_banner.json', params: { "global_banner": {} }, as: :json, + headers: { 'X-Redmine-API-Key' => @non_admin_user.api_key } + assert_response 400 + end + + def test_put_global_banner_when_api_enable_and_banner_admin_group_and_valid_data + Setting.rest_api_enabled = '1' + + @banner_admin_group.user_ids = [@non_admin_user.id] + @banner_admin_group.save! + + global_banner = { + "banner_description": 'Test message', + "display_part": 'both', + "enable": 'true' + } + + put '/banners/api/global_banner.json', params: { "global_banner": global_banner }, as: :json, + headers: { 'X-Redmine-API-Key' => @non_admin_user.api_key } + assert_response :success + end +end