{"id":28381776,"url":"https://github.com/opf/openproject-proto_plugin","last_synced_at":"2025-06-25T04:31:14.414Z","repository":{"id":45944540,"uuid":"78435776","full_name":"opf/openproject-proto_plugin","owner":"opf","description":"Exemplary OpenProject plugin demonstrating different ways to extend OpenProject.","archived":false,"fork":false,"pushed_at":"2024-12-09T08:29:17.000Z","size":325,"stargazers_count":21,"open_issues_count":3,"forks_count":19,"subscribers_count":12,"default_branch":"dev","last_synced_at":"2025-06-05T21:43:00.730Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/opf.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-01-09T14:24:14.000Z","updated_at":"2024-12-09T08:29:21.000Z","dependencies_parsed_at":"2023-01-24T17:17:10.212Z","dependency_job_id":null,"html_url":"https://github.com/opf/openproject-proto_plugin","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/opf%2Fopenproject-proto_plugin","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/opf%2Fopenproject-proto_plugin/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/opf%2Fopenproject-proto_plugin/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/opf%2Fopenproject-proto_plugin/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/opf","download_url":"https://codeload.github.com/opf/openproject-proto_plugin/tar.gz/refs/heads/dev","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/opf%2Fopenproject-proto_plugin/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":258342525,"owners_count":22685868,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-05-30T04:06:25.939Z","updated_at":"2025-06-25T04:31:14.404Z","avatar_url":"https://github.com/opf.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# OpenProject Prototype Plugin\n\nIn this plugin we try to give you an idea on how to write an OpenProject plugin. Examples of doing the most common things a plugin may want to do are included.\n\nTo get started quickly you may just copy this plugin, remove the bits you don't need and modify/add the features you want.\n\n\n## Pre-requisites\n\nIn order to be able to continue, you will first have to have a working OpenProject core development environment. Please follow these guides to set that up:\n\n* [Development environment Ubuntu/Debian](https://www.openproject.org/docs/development/development-environment-ubuntu/)\n* [Development environment Mac OS X](https://www.openproject.org/docs/development/development-environment-osx/)\n* [Development environment Docker](https://www.openproject.org/docs/development/development-environment-docker/)\n\nWe are assuming that you understand how to develop Ruby on Rails applications and are familiar with controllers, views, asset management, hooks and engines.\n\nTo get started with a development environment of the OpenProject core, we recommend you follow our development guides at https://docs.openproject.org/development/ as well as the [guide for plugin development](https://www.openproject.org/docs/development/create-openproject-plugin).\n\nThe frontend can be written using plain-vanilla JavaScript, but if you choose to integrate directly with the OpenProject frontend then you will have to understand the Angular framework.\n\n\n## Getting started\n\nTo include this plugin, you need to create a file called `Gemfile.plugins` in your OpenProject core directory with the following contents:\n\n```\ngroup :opf_plugins do\n  gem \"openproject-proto_plugin\", git: \"https://github.com/opf/openproject-proto_plugin.git\", branch: \"dev\"\nend\n```\n\nAs you may want to play around with and modify the plugin locally, you may want to check it out first and use the following instead to reference a local path:\n\n```\ngroup :opf_plugins do\n  gem \"openproject-proto_plugin\", path: \"/path/to/openproject-proto_plugin\"\nend\n```\n\nIf you already have a `Gemfile.plugins` just add the line \"gem\" line to it inside the `:opf_plugins` group.\n\nOnce you've done that, **switch to the OpenProject core directory** and run:\n\n```bash\n./bin/setup_dev\n```\n\nWhile you're in the root of the OpenProject core, we recommend you export the OpenProject core path as `$OPENPROJECT_ROOT`.\n\n```bash\nexport OPENPROJECT_ROOT=$(pwd)\n```\n\nThis will make the plugin known to the OpenProject core with bundler and optionally link a frontend directory into the core (more on that later).\n\nOptionally, you might want to run plugin seeds, if there are any:\n\n```bash\nbundle exec rails db:seed # creates default data from the plugin's seeder (`app/seeders`)\n```\n\nYou can then start the core server as described in the above guides. For example, you can then start the rails server with:\n\n```bash\nRAILS_ENV=development ./bin/rails server\n```\n\nAs well as the Angular CLI in development mode with:\n\n```bash\nRAILS_ENV=development npm run serve\n```\n\nIn order to verify that the plugin has been installed correctly, go to the Administration Plugins Page at http://localhost:3000/admin/plugins and you should be able to find your plugin in the list.\n\n![](images/admin-plugins-page.png?raw=true)\n\nIn the following sections we will explain some common features that you may want to use in your own plugin. This plugin has already been setup with the basic framework to illustrate all these features.\n\nEach section will list the relevant files you may want to look at and explain the features. Beyond that there are also code comments in the respective files which provide further details.\n\n### Frontend linking\nThis proto plugin contains an Angular frontend part. The way the Angular CLI works, it needs to build the project from a common root folder. That is located at `$OPENPROJECT_ROOT/frontend`.\n\nTo make your plugin's frontend available to the OpenProject core, it is being symlinked into `$OPENPROJECT_CORE/frontend/src/app/features/plugins/linked/your-plugin-name`. This is being done by the `bin/setup_dev` script, which needs to run whenever you add or remove a plugin from your Gemfile.\n\nWorking with this symlinked frontend is a bit tricky. What we recommend is that you develop your Ruby backend in the plugin folder, while you develop the Angular frontend in the OpenProject core. This way, you will get all benefits from the CLI and language services such as auto-completion, angular generations, correct paths being looked up by Typescript, etc.\n\nJS files in `/frontend` import other modules in the core app with the `core-app/` prefix which is an alias pointing to `\u003ccore-app-root\u003e/frontend/src/app` defined in the `tsconfig.base.json` file, be careful to update import path when configurations change. You will get error outputs from your angular CLI however.\n\n### Rails generators\n\nThe plugin comes with an executable `bin/rails` which you can use when calling rails generators for generating everything. You will have to define `OPENPROJECT_ROOT` in your environment for it to work unfortunately, because the plugin requires the core to load.\n\nBy `core` we mean the directory under which you originally checked out the OpenProject repository:\n\n```\n$ git clone https://github.com/opf/openproject.git ~/dev/openproject/core\n$ git checkout dev\n```\n\nSo for example, should the core be located under under `~/dev/openproject/core` you can set it like this, for instance in your `.bashrc`:\n\n```\nexport OPENPROJECT_ROOT=~/dev/openproject/core\n```\n\nor you can just prepend the relevant rails commands like this:\n\n```\n$ OPENPROJECT_ROOT=~/dev/openproject/core rails generate ...\n```\n\nOnce you've set that up you can use the rails generators as usual.\n\nFor instance this is how you could **generate a model**:\n\n```\n$ bundle exec rails generate model Kitten name:string --no-test-framework\n      invoke  active_record\n      create    db/migrate/20170116125942_create_kittens.rb\n      create    app/models/kitten.rb\n```\n\nFinally, don't forget to run the migration from the core directory. Please note that you cannot run `db:migrate` or other commands with rails from the engine. You'll have to execute those from the core.\n\n```\n$ cd $OPENPROJECT_ROOT\n$ bundle exec rails db:migrate\n```\n\nNow let's double-check that our Kittens table as been seeded:\n\n```\n$ rails c\n...\n[1] pry(main)\u003e Kitten.pluck(:name)\n   (0.3ms)  SELECT `kittens`.`name` FROM `kittens`\n=\u003e [\"Klaus\", \"Herbert\", \"Felix\"]\n```\n\nMake sure that the application is running (`bundle exec rails s`) and go to `http://localhost:3000/kittens`. You should see something like this:\n\n![](images/kittens-main-page.png?raw=true)\n\nGreat, we're on our way.\n\n\n\n### Specs\n\nThe relevant files for the specs are:\n\n* `spec/controllers/kittens_controller_spec.rb`\n\nYou have to run the specs from within the core. For instance:\n\n```\n$ cd $OPENPROJECT_ROOT\n$ RAILS_ENV=test bundle exec rspec `bundle show openproject-proto_plugin`/spec/controllers/kittens_controller_spec.rb\n```\n\n\n\n## Seeders\n\nThe relevant files for the seeders are:\n\n* `app/seeders/kittens_seeder.rb` - Creates example records.\n\nYou can define so called \"Seeders\" for your plugin which get called when `rake db:seed` is run in the core. For example:\n\n```\n$ cd OPENPROJECT_ROOT\n$ bundle exec rails db:seed\n```\n\nThe plugin defines a `KittenSeeder` which creates a few example rows to be displayed in the `KittensController`.\n\nA plugin's seeders have to be defined under its namespace within the `BasicData` module, for instance `BasicData::ProtoPlugin::KittensSeeder`.\nThey will be discovered and invoked by the core automatically.\n\n\n\n## Models\n\nThe relevant files for the models are:\n\n* `app/models/kitten.rb` - the code for the model where you can add validations etc.\n* `app/models/application_record.rb` - auto-generated base record\n* `db/migrate/20170116125942_create_kittens.rb` - database migration\n\nThe models work as usual in Rails applications. For the sake of completeness, the model validates the name attribute:\n\n```ruby\nclass Kitten \u003c ApplicationRecord\n  validates :name, uniqueness: true, length: { minimum: 5 }\nend\n```\n\n\n\n## Controllers\n\nThe relevant files for the controllers are:\n\n* `app/controllers/kittens_controller.rb` - main controller with `:index` entry point\n* `app/views/kittens/index.html.erb` - main template for kittens index view\n\nThe controllers work as expected for Rails applications. In preparation for the following example, we create a basic minimal controller which only supports creation of new kittens:\n\n```ruby\nclass KittensController \u003c ApplicationController\n  def index\n    @kittens = Kitten.all\n    render layout: true\n  end\n\n  def new\n    @kitten = Kitten.new\n  end\n\n  def create\n    @kitten = Kitten.new(kitten_params)\n    ...\n  end\n\n  private\n\n  def kitten_params\n    params.require(:kitten).permit(:name)\n  end\nend\n```\n\n\n\n## Create kitten example\n\nAs a simple example, let's enable the create kitten button on the kittens homepage block so that it brings the user to a create kitten page. It's already linked to `new_kitten_path` so all we need to do now with the controller already in place is to create `views/kittens/new.html.erb` template:\n\n```\n\u003ch1\u003e\u003c%= t(:label_kitten_new) %\u003e\u003c/h1\u003e\n\n\u003c%= render \"form\", kitten: @kitten %\u003e\n```\n\nThe partial `views/kittens/_form,html.erb` is a basic form for inputting the name:\n\n```\n\u003c%= form_for(kitten) do |f| %\u003e\n\n    \u003cp\u003e\n      \u003c%= f.label :name %\u003e\n      \u003c%= f.text_field :name %\u003e\n    \u003c/p\u003e\n\n    \u003c%= f.submit %\u003e\n\n\u003c% end %\u003e\n```\n\nwhich should end up looking something like this.\n\n![](images/create-new-kitten.png?raw=true)\n\nWe leave it up as an exercise for the reader to complete the CRUD with edit and delete actions. Good luck!\n\n\n\n## I18n\n\nOpenProject uses Rails I18n helpers as well as `I18n-js` to provide translations for the backend and frontend.\n\nYou can add your strings to `config/locales/en.yml` and `config/locales/js-en.yml` for backend and frontend translations, respectively.\n\nThe translations can then be called with:\n\n- `I18n.t('your_namespace.your_key')` and\n- `I18n.t('js.your_js_translation_key')`\n\n\n\n## Static assets\n\nThe relevant files for the assets are:\n\n* `lib/open_project/proto_plugin/engine.rb` - assets statement at the end of the engine.\n* `app/assets/javascripts/proto_plugin/main.js` - main entry point for plain JavaScript and document ready hook.\n* `app/assets/stylesheets/proto_plugin/main.scss` - good ol' Sass stuff.\n* `app/assets/images/kitty.png` - a nice kitty image.\n\nAny additional assets you want to use have to be registered for pre-compilation in the engine like this:\n\n```\nassets %w(kitty.png)\n```\n\nYou don't technically have to put the assets into a subfolder with the same name as your plugin. But it's highly recommended to do so in order to avoid naming conflicts. For example, if the image `kitty.png` is not scoped, it might conflict with the core if it were also to include another asset named `kitty.png` too.\n\nPlease note that OpenProject no longer uses the Rails asset pipeline for JavaScript and CSS. While you could still serve both through the asset pipeline, they are not being transformed anymore (SCSS to CSS, JS minification, etc.). For those, use the Angular frontend module instead.\n\n## Angular frontend\n\nThe plugin can create its own Angular module and also hook into parts of the core frontend. The relevant files for the frontend are:\n\n* `frontend/app/module/main.ts`\n\n\nThis file defines the Angular module for this plugin that gets linked into core `frontend/app/src/modules/plugins/linked`\n\nAny changes made to the frontend require running Angular CLI to update. To do that go to the OpenProject folder (NOT the plugin directory) and execute the following command with the plugin contained in the Gemfile.plugins.\n\n```bash\n$ ./bin/setup_dev\n$ npm run serve\n```\n\nThis will compile and output all changes on the fly as you change it using the Angular CLI.\n\n### Global SASS styles\n\nWith the Angular frontend, you have the option to generate component-based styles (for example, `kitten.component.sass`) which will be available only within that component using Angular style isolation.\n\nIf you want to define global styles or override core styles, you can create or extend the file `frontend/module/global_styles.scss` for styles that will be applied locally.\n\n\n\n### Reloading the application in development\n\nFor the backend part, Rails will autoload and reload dependencies in all `app/` folders. If you change something in your plugin under `lib/` , especially changes to the engine.rb, menu system, or plugin registration, you will probably have to restart your Rails server.\n\nFor the frontend part, automatic reloading is automatically active when you run `npm run serve` using a file watcher. If you don't see changes in your files resulting in a new compilation cycle, please ensure you're working within the linked core, as that will ensure that the symlink is modified.\n\n\n\n## Menu Items\n\nThe relevant files for the menu items are:\n\n* `lib/open_project/proto_plugin/engine.rb` - register block in the beginning\n* `app/controllers/kittens_controller.rb`\n\nRegistering new user-defined menu items is easy. For instance, let's assume that you want to add a new item to the project menu. Just add the following to the `engine.rb` file:\n\n```ruby\nmenu :project_menu, # Which menu to add an item to (compare the core config/initializers/menus.rb for options)\n     :kittens, # The name of the new item to add\n     { controller: '/kittens', action: 'index' }, # The Rails route definition or path to define\n     after: :overview, # use before: or after: to move the menu item next to an existing definition\n     param: :project_id, # Leave it at :project_id if you're adding a project menu item\n     caption: :\"proto_plugin_name\", # The caption, use a symbol for I18n lookup, or a string for plain text\n     icon: 'icon2 icon-bug', # The icon classes to add, see http://localhost:3000/styleguide for options\n     html: { id: \"kittens-menu-item\" }, # Additional Rails tag_helper html to add\n     if: -\u003e(project) { true } # A condition, such as permissions when to show the menu\n```\n\nYou are then free to enable the \"Kittens module\" for a given project by going to that \"Project settings\" page, for example `/projects/demo-project/settings/modules` and checking the checkbox.\n\n\n\n![](images/enable-kittens-module.png?raw=true)\n\nThe menu item will now appear on the top level project page as well as all sub-levels `/projects/demo-project/*`.\n\n![](images/kittens-menu-item.png?raw=true)\n\nYou can add nested menu items by passing a `parent` option to the following items. For instance you could add a child menu item to the menu item shown above by adding `parent: :kittens` as another option.\n\nThere are a number of menus available from which to choose:\n\n* top_menu\n* account_menu\n* application_menu\n* my_menu\n* admin_menu\n* project_menu\n\n\n## Homescreen Blocks\n\nBy default the homepage contains a number of blocks (widget boxes), namely: \"Projects\", \"Users\", \"My account\", \"OpenProject community\" and \"Administration\".\n\nYou can easily add your own user-defined block so that it will also appears on the homepage.\n\nThe relevant files for homescreen blocks are:\n\n* `lib/open_project/proto_plugin/engine.rb` - `proto_plugin.homescreen_blocks` initializer\n* `app/views/homescreen/blocks/_homescreen_block.html.erb`\n\nIn the file `engine.rb` you can register additional blocks in OpenProject's homescreen like this:\n\n```\ninitializer 'proto_plugin.homescreen_blocks' do\n  OpenProject::Static::Homescreen.manage :blocks do |blocks|\n    blocks.push(\n      { partial: 'homescreen_block', if: Proc.new { true } }\n    )\n  end\nend\n```\n\nWhere the `if` option is optional.\n\nThe partial file `_homescreen_block.html.erb` provides the template from which the contents of the block will be generated. Have a look at this file to get a better idea of the possibilities.\n\nThis is what you should now see on the homepage:\n\n![](images/kitten-homescreen-block.png?raw=true)\n\n\n## OpenProject::Notification listeners\n\nThe relevant files for notification listeners are:\n\n* `lib/open_project/proto_plugin/engine.rb` - `proto_plugin.notifications` initializer\n\nAlthough OpenProject has inherited hooks (see next section) from Redmine, it also employs its own mechanism for simple event callbacks. Their return values are ignored.\n\nFor example, you can be notified whenever a user has been invited to OpenProject by subscribing to the `user_invited` event. Add the following to the `engine.rb` file:\n\n```\ninitializer 'proto_plugin.notifications' do\n  OpenProject::Notifications.subscribe 'user_invited' do |token|\n    user = token.user\n\n    Rails.logger.debug \"#{user.email} invited to OpenProject\"\n  end\nend\n```\n\n\n### Events\n\nCurrently the supported events (_block parameters in parenthesis_) to which you can subscribe are:\n\n* user_invited (token)\n* user_reinvited (token)\n* project_updated (project)\n* project_renamed (project)\n* project_deletion_imminent (project)\n* member_updated (member)\n* member_removed (member)\n* journal_created (payload)\n* watcher_added (payload)\n\n\n### Setting Events\n\nWhenever a given setting changes, an event is triggered passing the previous and new values. For instance:\n\n* `setting.host_name.changed` (value, old_value)\n\nWhere `host_name` is the name of the setting. You can find out all setting names simply by inspecting the relevant setting input field in the admin area in your browser or by listing them all on the rails console through `Setting.pluck(:name)`. Also have a look at `config/settings.yml` where all the default values for settings are defined by their name.\n\n\n## Hooks\n\nThe relevant files for hooks are:\n\n* `lib/open_project/engine.rb` - `proto_plugin.register_hooks` initializer\n* `lib/open_project/hooks.rb`\n* `app/views/hooks/proto_plugin/_homescreen_after_links.html.erb`\n* `app/views/hooks/proto_plugin/_view_layouts_base_sidebar.html.erb`\n\nHooks can be used to extend views, controllers and models at certain predefined places. Each hook has a name for which a method has to be defined in your hook class, see `lib/open_project/proto_plugin/hooks.rb` for more details.\n\nFor example:\n\n```\nrender_on :homescreen_after_links, partial: '/hooks/homescreen_after_links'\n```\n\nBy using `render_on`, the given variables are made available as locals to the partial for that defined hook. Otherwise they will be available through the defined hook method's first and only parameter named `context`.\n\nAdditionally the following context information is put into context if available:\n\n* project - current project\n* request - Request instance\n* controller - current Controller instance\n* hook_caller - object that called the hook\n\n\n### View Hooks\n\n_Note: context variables placed within (parenthesis)_\n\nHooks in the base template:\n\n* :view_layouts_base_html_head\n* :view_layouts_base_sidebar\n* :view_layouts_base_breadcrumb\n* :view_layouts_base_content\n* :view_layouts_base_body_bottom\n\nMore hooks:\n\n* :view_account_login_auth_provider\n* :view_account_login_top\n* :view_account_login_bottom\n* :view_account_register_after_basic_information (f) - f being a form helper\n* :activity_index_head\n* :view_admin_info_top\n* :view_admin_info_bottom\n* :view_common_error_details (params, project)\n* :homescreen_administration_links\n* :view_work_package_overview_attributes\n\nCustom field form hooks:\n\n* :view_custom_fields_form_upper_box (custom_field, form)\n* :view_custom_fields_form_work_package_custom_field (custom_field, form)\n* :view_custom_fields_form_user_custom_field (custom_field, form)\n* :view_custom_fields_form_group_custom_field (custom_field, form)\n* :view_custom_fields_form_project_custom_field (custom_field, form)\n* :view_custom_fields_form_time_entry_activity_custom_field (custom_field, form)\n* :view_custom_fields_form_version_custom_field (custom_field, form)\n* :view_custom_fields_form_issue_priority_custom_field (custom_field, form)\n\n\n### Controller Hooks\n\n_Note: context variables placed within (parenthesis)_\n\n* :controller_account_success_authentication_after (user)\n* :controller_custom_fields_new_after_save (custom_field)\n* :controller_custom_fields_edit_after_save (custom_field)\n* :controller_messages_new_after_save (params, message)\n* :controller_messages_reply_after_save (params, message)\n* :controller_timelog_available_criterias (available_criterias, project)\n* :controller_timelog_time_report_joins (sql)\n* :controller_timelog_edit_before_save (params, time_entry)\n* :controller_wiki_edit_after_save (params, page)\n* :controller_work_packages_bulk_edit_before_save (params, work_package)\n* :controller_work_packages_move_before_save (params, work_package, target_project, copy)\n\n\n### More Hooks\n\n_Note: context variables placed within (parenthesis)_\n\n* :model_changeset_scan_commit_for_issue_ids_pre_issue_update (changeset, issue)\n* :copy_project_add_member (new_member, member)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fopf%2Fopenproject-proto_plugin","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fopf%2Fopenproject-proto_plugin","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fopf%2Fopenproject-proto_plugin/lists"}