From c4809defa10ba9b83a26708d2625090862cd9d86 Mon Sep 17 00:00:00 2001 From: Lodewiges Date: Wed, 17 Dec 2025 00:51:07 +0100 Subject: [PATCH 1/6] asked claude to make the rough changes --- GRAPHITI_MIGRATION.md | 639 ++++++++++++++++++ Gemfile | 5 +- Gemfile.lock | 59 +- app/controllers/application_controller.rb | 18 +- app/controllers/concerns/graphiti_crud.rb | 72 ++ app/controllers/v1/activities_controller.rb | 5 + app/controllers/v1/application_controller.rb | 17 +- .../v1/article_comments_controller.rb | 8 +- app/controllers/v1/articles_controller.rb | 6 + .../v1/board_room_presences_controller.rb | 5 + app/controllers/v1/books_controller.rb | 6 + .../v1/debit/collections_controller.rb | 16 +- .../v1/debit/mandates_controller.rb | 5 + .../v1/debit/transactions_controller.rb | 5 + .../closed_question_answers_controller.rb | 5 + .../closed_question_options_controller.rb | 5 + .../v1/form/closed_questions_controller.rb | 5 + app/controllers/v1/form/forms_controller.rb | 5 + .../form/open_question_answers_controller.rb | 5 + .../v1/form/open_questions_controller.rb | 5 + .../v1/form/responses_controller.rb | 8 +- .../v1/forum/categories_controller.rb | 5 + app/controllers/v1/forum/posts_controller.rb | 5 + .../v1/forum/threads_controller.rb | 6 + app/controllers/v1/groups_controller.rb | 11 +- app/controllers/v1/mail_aliases_controller.rb | 5 + app/controllers/v1/memberships_controller.rb | 5 + app/controllers/v1/permissions_controller.rb | 5 + app/controllers/v1/photo_albums_controller.rb | 6 + .../v1/photo_comments_controller.rb | 5 + app/controllers/v1/photo_tags_controller.rb | 5 + app/controllers/v1/photos_controller.rb | 13 +- app/controllers/v1/polls_controller.rb | 5 + app/controllers/v1/room_adverts_controller.rb | 6 + app/controllers/v1/static_pages_controller.rb | 6 + app/controllers/v1/stored_mails_controller.rb | 8 +- .../v1/study_room_presences_controller.rb | 5 + app/controllers/v1/users_controller.rb | 30 +- app/controllers/v1/vacancies_controller.rb | 5 + app/resources/v1/activity_resource.rb | 76 ++- app/resources/v1/application_resource.rb | 118 ++-- app/resources/v1/article_comment_resource.rb | 16 +- app/resources/v1/article_resource.rb | 75 +- .../v1/board_room_presence_resource.rb | 28 +- app/resources/v1/book_resource.rb | 25 +- app/resources/v1/debit/collection_resource.rb | 32 +- app/resources/v1/debit/mandate_resource.rb | 18 +- .../v1/debit/transaction_resource.rb | 20 +- app/resources/v1/debit/user_resource.rb | 2 + .../form/closed_question_answer_resource.rb | 12 +- .../form/closed_question_option_resource.rb | 23 +- .../v1/form/closed_question_resource.rb | 25 +- app/resources/v1/form/form_resource.rb | 51 +- .../v1/form/open_question_answer_resource.rb | 13 +- .../v1/form/open_question_resource.rb | 25 +- app/resources/v1/form/response_resource.rb | 30 +- app/resources/v1/form/user_resource.rb | 2 + app/resources/v1/forum/category_resource.rb | 29 +- app/resources/v1/forum/post_resource.rb | 28 +- app/resources/v1/forum/thread_resource.rb | 41 +- app/resources/v1/forum/user_resource.rb | 2 + app/resources/v1/group_resource.rb | 71 +- .../v1/groups_permissions_resource.rb | 4 + app/resources/v1/mail_alias_resource.rb | 29 +- app/resources/v1/membership_resource.rb | 18 +- app/resources/v1/permission_resource.rb | 11 +- .../v1/permissions_users_resource.rb | 6 +- app/resources/v1/photo_album_resource.rb | 40 +- app/resources/v1/photo_comment_resource.rb | 16 +- app/resources/v1/photo_resource.rb | 51 +- app/resources/v1/photo_tag_resource.rb | 20 +- app/resources/v1/poll_resource.rb | 23 +- app/resources/v1/room_advert_resource.rb | 41 +- app/resources/v1/static_page_resource.rb | 36 +- app/resources/v1/stored_mail_resource.rb | 31 +- .../v1/study_room_presence_resource.rb | 28 +- app/resources/v1/user_resource.rb | 312 ++++++--- app/resources/v1/vacancy_resource.rb | 60 +- config/initializers/graphiti.rb | 10 + config/initializers/jsonapi_resources.rb | 20 - config/routes.rb | 73 +- 81 files changed, 1841 insertions(+), 790 deletions(-) create mode 100644 GRAPHITI_MIGRATION.md create mode 100644 app/controllers/concerns/graphiti_crud.rb create mode 100644 config/initializers/graphiti.rb delete mode 100644 config/initializers/jsonapi_resources.rb diff --git a/GRAPHITI_MIGRATION.md b/GRAPHITI_MIGRATION.md new file mode 100644 index 00000000..4d35a09c --- /dev/null +++ b/GRAPHITI_MIGRATION.md @@ -0,0 +1,639 @@ +# JSONAPI::Resources → Graphiti Migration Guide + +This document describes all changes made to migrate from `jsonapi-resources` to `graphiti`. + +## Table of Contents +- [Why Graphiti?](#why-graphiti) +- [Gemfile Changes](#gemfile-changes) +- [Configuration Changes](#configuration-changes) +- [Resource Changes](#resource-changes) +- [Controller Changes](#controller-changes) +- [Route Changes](#route-changes) +- [Frontend Changes Required](#frontend-changes-required) +- [Filter System](#filter-system) +- [Testing](#testing) + +--- + +## Why Graphiti? + +| Feature | JSONAPI::Resources | Graphiti | +|---------|-------------------|----------| +| Maintained | ❌ No longer maintained | ✅ Actively maintained | +| Rack 3.x support | ❌ | ✅ | +| JSON:API compliant | ✅ | ✅ | +| Pundit support | Via separate gem | ✅ Built-in | +| Similar DSL | - | ✅ Easy migration | + +--- + +## Gemfile Changes + +### Removed +```ruby +gem 'jsonapi-authorization', '~> 3.0', '>= 3.0.2' +gem 'jsonapi-resources', '~> 0.9.1' +``` + +### Added +```ruby +gem 'graphiti', '~> 1.7' +gem 'graphiti-rails', '~> 0.4' +gem 'kaminari', '~> 1.2' # Pagination for Graphiti +``` + +### Installation +```bash +bundle install +``` + +--- + +## Configuration Changes + +### Removed +- `config/initializers/jsonapi_resources.rb` + +### Added +- `config/initializers/graphiti.rb` + +```ruby +# config/initializers/graphiti.rb +Graphiti.configure do |config| + config.pagination_links = true +end + +Graphiti::Errors::InvalidRequest +Graphiti::Serializer.config do |config| + config.default_key_transform = :underscore +end + +Rails.application.config.after_initialize do + Rails.application.eager_load! if Rails.env.development? +end +``` + +--- + +## Resource Changes + +### Base Resource Class + +**Before (JSONAPI::Resources):** +```ruby +class V1::ApplicationResource < JSONAPI::Resource + include JSONAPI::Authorization::PunditScopedResource + abstract + + attributes :created_at, :updated_at + + def self.creatable_fields(_context) + [] + end + + def self.records(options = {}) + # ... + end +end +``` + +**After (Graphiti):** +```ruby +class V1::ApplicationResource < Graphiti::Resource + self.adapter = Graphiti::Adapters::ActiveRecord + self.abstract_class = true + + attribute :created_at, :datetime, writable: false + attribute :updated_at, :datetime, writable: false + + def base_scope + if context&.dig(:action) == 'index' && current_user_or_application + Pundit.policy_scope!(current_user_or_application, self.class.model) + else + self.class.model.all + end + end +end +``` + +### Key Differences + +| Feature | JSONAPI::Resources | Graphiti | +|---------|-------------------|----------| +| Attributes | `attributes :name, :email` | `attribute :name, :string` (typed) | +| Model reference | `@model` | `@object` | +| Computed attributes | Method with same name | Block in attribute definition | +| Field visibility | `fetchable_fields` method | `readable` guard on attribute | +| Write permissions | `creatable_fields(context)` | `writable` guard on attribute | +| Abstract class | `abstract` | `self.abstract_class = true` | +| Model class | Auto-inferred | `self.model = User` | + +### Attribute Syntax + +**Before:** +```ruby +attributes :name, :email, :avatar_url + +def avatar_url + @model.avatar.url +end + +def fetchable_fields + fields = super + fields -= [:email] unless can_read_email? + fields +end + +def self.creatable_fields(context) + %i[name email] +end +``` + +**After:** +```ruby +attribute :name, :string +attribute :email, :string do + readable { can_read_email? } +end +attribute :avatar_url, :string, writable: false do + @object.avatar.url +end +``` + +### Relationship Syntax + +**Before:** +```ruby +has_many :groups +has_one :author, always_include_linkage_data: true +``` + +**After:** +```ruby +has_many :groups +has_one :author, resource: V1::UserResource +``` + +### Callbacks + +**Before:** +```ruby +before_create do + @model.author_id = current_user.id +end + +after_create do + UserMailer.welcome(@model).deliver_later +end +``` + +**After:** +```ruby +before_save only: [:create] do |model| + model.author_id = current_user.id +end + +after_commit only: [:create] do |model| + UserMailer.welcome(model).deliver_later +end +``` + +--- + +## Controller Changes + +### Base Controller + +**Before:** +```ruby +class ApplicationController < JSONAPI::ResourceController + include Pundit::Authorization + + def verify_content_type_header + true + end +end +``` + +**After:** +```ruby +class ApplicationController < ActionController::API + include Pundit::Authorization + include Graphiti::Rails + include Graphiti::Responders + + register_exception Graphiti::Errors::RecordNotFound, status: 404 + register_exception Graphiti::Errors::InvalidRequest, status: 400 +end +``` + +### Resource Controllers + +A new concern `GraphitiCrud` was created to provide standard CRUD operations: + +```ruby +# app/controllers/concerns/graphiti_crud.rb +module GraphitiCrud + extend ActiveSupport::Concern + + included do + class_attribute :resource_class + end + + class_methods do + def graphiti_resource(klass) + self.resource_class = klass + end + end + + def index + resources = resource_class.all(params, context) + render jsonapi: resources + end + + def show + resource = resource_class.find(params, context) + render jsonapi: resource + end + + def create + resource = resource_class.build(params, context) + if resource.save + render jsonapi: resource, status: :created + else + render jsonapi_errors: resource + end + end + + def update + resource = resource_class.find(params, context) + if resource.update_attributes + render jsonapi: resource + else + render jsonapi_errors: resource + end + end + + def destroy + resource = resource_class.find(params, context) + if resource.destroy + head :no_content + else + render jsonapi_errors: resource + end + end +end +``` + +**Controller usage:** +```ruby +class V1::UsersController < V1::ApplicationController + include GraphitiCrud + + graphiti_resource V1::UserResource + + # Custom actions still work normally + def custom_action + # ... + end +end +``` + +--- + +## Route Changes + +**Before:** +```ruby +namespace :v1 do + jsonapi_resources :users do + jsonapi_relationships + member do + post :activate + end + end +end +``` + +**After:** +```ruby +namespace :v1 do + resources :users do + member do + post :activate + end + end +end +``` + +--- + +## Frontend Changes Required + +### ✅ No Changes Needed + +These JSON:API standard patterns work identically: + +```javascript +// Filtering +GET /v1/users?filter[search]=john + +// Including relationships +GET /v1/users?include=groups,memberships + +// Sparse fieldsets +GET /v1/users?fields[users]=first_name,last_name + +// Sorting +GET /v1/users?sort=-created_at,first_name + +// Pagination +GET /v1/users?page[number]=2&page[size]=25 +``` + +### ⚠️ Changes Required + +#### 1. Pagination Response Format + +**Before (JSONAPI::Resources):** +```json +{ + "data": [...], + "meta": { + "page_count": 5 + } +} +``` + +**After (Graphiti):** +```json +{ + "data": [...], + "meta": { + "page": { + "current_page": 1, + "per_page": 25, + "total_pages": 5, + "total_count": 125 + } + } +} +``` + +**Frontend fix:** +```javascript +// Before +const totalPages = response.meta.page_count; + +// After +const totalPages = response.meta.page.total_pages; +const totalRecords = response.meta.page.total_count; +``` + +#### 2. Boolean Filters + +**Before:** Any truthy value worked +```javascript +?filter[upcoming]=1 +?filter[upcoming]=yes +``` + +**After:** Use actual boolean values +```javascript +?filter[upcoming]=true +?filter[upcoming]=false +``` + +#### 3. Error Response Format + +Both follow JSON:API spec, but Graphiti provides more metadata: + +```json +{ + "errors": [{ + "code": "unprocessable_entity", + "status": "422", + "title": "Validation Error", + "detail": "Name can't be blank", + "meta": { + "attribute": "name", + "message": "can't be blank" + } + }] +} +``` + +--- + +## Filter System + +### Basic Filter Syntax + +**Before:** +```ruby +filter :upcoming, apply: ->(records, _value, _options) { records.upcoming } + +filter :group, apply: lambda { |records, value, _options| + records.where(group_id: value) +} +``` + +**After:** +```ruby +filter :upcoming, :boolean do + eq do |scope, value| + value ? scope.upcoming : scope + end +end + +filter :group, :integer do + eq do |scope, value| + scope.where(group_id: value) + end +end +``` + +### Filter Types + +| Type | Description | Example | +|------|-------------|---------| +| `:string` | Text filters | `filter :name, :string` | +| `:integer` | Number filters | `filter :age, :integer` | +| `:boolean` | True/false | `filter :active, :boolean` | +| `:datetime` | Date/time with operators | `filter :created_at, :datetime` | +| `:date` | Date only | `filter :birthday, :date` | +| `:float` | Decimal numbers | `filter :price, :float` | +| `:array` | Array of values | `filter :ids, :array` | + +### Filter Operators (New Feature!) + +Graphiti supports multiple operators per filter: + +```ruby +filter :created_at, :datetime do + eq { |scope, val| scope.where(created_at: val) } + gt { |scope, val| scope.where('created_at > ?', val) } + lt { |scope, val| scope.where('created_at < ?', val) } + gte { |scope, val| scope.where('created_at >= ?', val) } + lte { |scope, val| scope.where('created_at <= ?', val) } +end +``` + +**Frontend usage:** +```javascript +// Equals (default) +?filter[created_at]=2024-01-01 + +// Greater than +?filter[created_at][gt]=2024-01-01 + +// Less than +?filter[created_at][lt]=2024-12-31 + +// Range (between) +?filter[created_at][gte]=2024-01-01&filter[created_at][lte]=2024-12-31 +``` + +### Search Filter + +The application-wide search filter was converted to: + +```ruby +# In ApplicationResource +filter :search, :string do + eq do |scope, value| + searchable = self.class.config[:searchable_fields] || [] + return scope if searchable.empty? + + arel = scope.model.arel_table + value.to_s.split.each do |word| + conditions = searchable.map { |field| + arel[field].lower.matches("%#{word.downcase}%") + }.inject(:or) + scope = scope.where(conditions) + end + scope + end +end + +# Define searchable fields in child resources +def self.searchable_fields(*fields) + if fields.any? + config[:searchable_fields] = fields + else + config[:searchable_fields] || [] + end +end +``` + +**In child resources:** +```ruby +class V1::UserResource < V1::ApplicationResource + searchable_fields :email, :first_name, :last_name, :nickname +end +``` + +--- + +## Testing + +### RSpec Request Specs + +The request specs should mostly work unchanged since the API contract is the same. + +**Key differences to test:** + +1. **Pagination meta format:** +```ruby +expect(json['meta']['page']['total_pages']).to eq(5) +``` + +2. **Error responses:** +```ruby +expect(json['errors'].first['meta']['attribute']).to eq('name') +``` + +### Running Tests + +```bash +# Run all tests +bundle exec rspec + +# Run specific resource specs +bundle exec rspec spec/resources/ + +# Run request specs +bundle exec rspec spec/requests/ +``` + +--- + +## Files Changed + +### New Files +- `config/initializers/graphiti.rb` +- `app/controllers/concerns/graphiti_crud.rb` +- `GRAPHITI_MIGRATION.md` (this file) + +### Removed Files +- `config/initializers/jsonapi_resources.rb` + +### Modified Files + +#### Gemfile +- Removed jsonapi-resources gems +- Added graphiti gems + +#### Controllers (30+ files) +- `app/controllers/application_controller.rb` +- `app/controllers/v1/application_controller.rb` +- `app/controllers/v1/*_controller.rb` (all resource controllers) +- `app/controllers/v1/debit/*_controller.rb` +- `app/controllers/v1/form/*_controller.rb` +- `app/controllers/v1/forum/*_controller.rb` + +#### Resources (26 files) +- `app/resources/v1/application_resource.rb` +- `app/resources/v1/*_resource.rb` (all resources) +- `app/resources/v1/debit/*_resource.rb` +- `app/resources/v1/form/*_resource.rb` +- `app/resources/v1/forum/*_resource.rb` + +#### Routes +- `config/routes.rb` + +--- + +## Troubleshooting + +### Common Issues + +#### 1. "undefined method `model`" +Make sure to set `self.model = ModelClass` in your resource. + +#### 2. Filter not working +Check that: +- Filter type is correct (`:boolean`, `:string`, etc.) +- You're using `eq do |scope, value|` block syntax + +#### 3. Relationships not loading +Ensure you specify the resource class: +```ruby +has_one :author, resource: V1::UserResource +``` + +#### 4. Context not available +Access context via `context` method, not instance variable: +```ruby +def current_user + context&.dig(:user) +end +``` + +--- + +## Resources + +- [Graphiti Documentation](https://www.graphiti.dev/guides/) +- [Graphiti GitHub](https://github.com/graphiti-api/graphiti) +- [JSON:API Specification](https://jsonapi.org/) diff --git a/Gemfile b/Gemfile index ff322343..7f263b30 100644 --- a/Gemfile +++ b/Gemfile @@ -18,8 +18,9 @@ gem 'iban-tools', '~> 1.2.1' gem 'icalendar', '~> 2.11', '>= 2.11.2' gem 'improvmx', '~> 0.2.1' gem 'isbn_validation', '~> 1.2', '>= 1.2.2' -gem 'jsonapi-authorization', '~> 3.0', '>= 3.0.2' -gem 'jsonapi-resources', '~> 0.9.1' +gem 'graphiti', '~> 1.7' +gem 'graphiti-rails', '~> 0.4' +gem 'kaminari', '~> 1.2' # Pagination for Graphiti gem 'message_bus', '~> 4.4', '>= 4.4.1' gem 'mini_magick', '~> 5.3' gem 'paper_trail', '~> 16.0' diff --git a/Gemfile.lock b/Gemfile.lock index a790f37a..170739db 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -145,6 +145,23 @@ GEM dotenv (= 3.1.8) railties (>= 6.1) drb (2.2.1) + dry-core (1.1.0) + concurrent-ruby (~> 1.0) + logger + zeitwerk (~> 2.6) + dry-inflector (1.2.0) + dry-logic (1.6.0) + bigdecimal + concurrent-ruby (~> 1.0) + dry-core (~> 1.1) + zeitwerk (~> 2.6) + dry-types (1.8.3) + bigdecimal (~> 3.0) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0) + dry-inflector (~> 1.0) + dry-logic (~> 1.4) + zeitwerk (~> 2.6) erubi (1.13.1) et-orbi (1.2.11) tzinfo @@ -172,6 +189,19 @@ GEM ruby-progressbar (~> 1.4) globalid (1.2.1) activesupport (>= 6.1) + graphiti (1.8.2) + activesupport (>= 5.2) + concurrent-ruby (>= 1.2, < 2.0) + dry-types (>= 0.15.0, < 2.0) + graphiti_errors (~> 1.1.0) + jsonapi-renderer (~> 0.2, >= 0.2.2) + jsonapi-serializable (~> 0.3.0) + graphiti-rails (0.4.1) + graphiti (~> 1.2) + railties (>= 5.0) + rescue_registry (~> 1.0) + graphiti_errors (1.1.2) + jsonapi-serializable (~> 0.1) guard (2.19.1) formatador (>= 0.2.4) listen (>= 2.7, < 4.0) @@ -223,13 +253,21 @@ GEM jar-dependencies (0.5.5) json (2.10.2) json (2.10.2-java) - jsonapi-authorization (3.0.2) - jsonapi-resources (~> 0.9.0) - pundit (>= 1.0.0, < 3.0.0) - jsonapi-resources (0.9.12) - activerecord (>= 4.1) - concurrent-ruby - railties (>= 4.1) + jsonapi-renderer (0.2.2) + jsonapi-serializable (0.3.1) + jsonapi-renderer (~> 0.2.0) + kaminari (1.2.2) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.2.2) + kaminari-activerecord (= 1.2.2) + kaminari-core (= 1.2.2) + kaminari-actionview (1.2.2) + actionview + kaminari-core (= 1.2.2) + kaminari-activerecord (1.2.2) + activerecord + kaminari-core (= 1.2.2) + kaminari-core (1.2.2) language_server-protocol (3.17.0.4) lint_roller (1.1.0) listen (3.9.0) @@ -396,6 +434,8 @@ GEM io-console (~> 0.5) request_store (1.7.0) rack (>= 1.4) + rescue_registry (1.0.0) + activesupport (>= 5.0) rest-client (2.1.0) http-accept (>= 1.7.0, < 2.0) http-cookie (>= 1.0.2, < 2.0) @@ -594,14 +634,15 @@ DEPENDENCIES faker (~> 3.5, >= 3.5.2) friendly_id (~> 5.5, >= 5.5.1) fuubar (~> 2.5, >= 2.5.1) + graphiti (~> 1.7) + graphiti-rails (~> 0.4) guard-rspec (~> 4.7, >= 4.7.3) http (~> 5.3, >= 5.3.1) iban-tools (~> 1.2.1) icalendar (~> 2.11, >= 2.11.2) improvmx (~> 0.2.1) isbn_validation (~> 1.2, >= 1.2.2) - jsonapi-authorization (~> 3.0, >= 3.0.2) - jsonapi-resources (~> 0.9.1) + kaminari (~> 1.2) listen (~> 3.9) message_bus (~> 4.4, >= 4.4.1) mina (~> 1.2, >= 1.2.5) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 21c8bcb4..52ca7179 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,18 +1,18 @@ -class ApplicationController < JSONAPI::ResourceController +# frozen_string_literal: true + +class ApplicationController < ActionController::API include Pundit::Authorization + include Graphiti::Rails + include Graphiti::Responders + + # Register JSON:API MIME type + register_exception Graphiti::Errors::RecordNotFound, status: 404 + register_exception Graphiti::Errors::InvalidRequest, status: 400 before_action :set_paper_trail_whodunnit before_action :set_sentry_context - protect_from_forgery with: :null_session rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized - # Disable content_type check, since it makes testing practically impossible - # and does not add any layers of usefulness or security. - # TODO: Remove when JR docs include instructions on rspec with this header - def verify_content_type_header - true - end - def pundit_user current_user || current_application end diff --git a/app/controllers/concerns/graphiti_crud.rb b/app/controllers/concerns/graphiti_crud.rb new file mode 100644 index 00000000..ff67a77e --- /dev/null +++ b/app/controllers/concerns/graphiti_crud.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +# Provides standard Graphiti CRUD actions for controllers +# Include this module to get index, show, create, update, destroy actions +module GraphitiCrud + extend ActiveSupport::Concern + + included do + class_attribute :resource_class + end + + class_methods do + def graphiti_resource(klass) + self.resource_class = klass + end + end + + def index + resources = resource_class.all(params, context) + render jsonapi: resources + end + + def show + resource = resource_class.find(params, context) + render jsonapi: resource + end + + def create + resource = resource_class.build(params, context) + + if resource.save + render jsonapi: resource, status: :created + else + render jsonapi_errors: resource + end + end + + def update + resource = resource_class.find(params, context) + + if resource.update_attributes + render jsonapi: resource + else + render jsonapi_errors: resource + end + end + + def destroy + resource = resource_class.find(params, context) + + if resource.destroy + head :no_content + else + render jsonapi_errors: resource + end + end + + private + + def resource_class + self.class.resource_class || infer_resource_class + end + + def infer_resource_class + # Infer from controller name: V1::UsersController -> V1::UserResource + controller_name = self.class.name + resource_name = controller_name + .sub(/Controller$/, '') + .singularize + 'Resource' + resource_name.constantize + end +end diff --git a/app/controllers/v1/activities_controller.rb b/app/controllers/v1/activities_controller.rb index b51904d5..b55acd01 100644 --- a/app/controllers/v1/activities_controller.rb +++ b/app/controllers/v1/activities_controller.rb @@ -1,9 +1,13 @@ require 'icalendar/tzinfo' class V1::ActivitiesController < V1::ApplicationController + include GraphitiCrud + before_action :doorkeeper_authorize!, except: %i[index show ical] before_action :set_model, only: %i[generate_alias] + graphiti_resource V1::ActivityResource + def generate_alias authorize @model @@ -98,6 +102,7 @@ def authenticate_user_by_ical_secret_key return false unless @user @user.permission?(:read, Activity) + end def ical_add_birthdays?(requested_categories) diff --git a/app/controllers/v1/application_controller.rb b/app/controllers/v1/application_controller.rb index 47780715..a2bef025 100644 --- a/app/controllers/v1/application_controller.rb +++ b/app/controllers/v1/application_controller.rb @@ -1,7 +1,10 @@ +# frozen_string_literal: true + class V1::ApplicationController < ApplicationController before_action :doorkeeper_authorize! rescue_from AmberError::NotMemberOfGroupError, with: :user_is_not_member_of_group_error + # Graphiti context - passed to resources def context { user: current_user, application: current_application, action: params[:action] } end @@ -17,20 +20,10 @@ def set_model head :not_found end - def base_url - # When request is incoming via Ember proxy you want to add /api in the base_url - result = super - return result if result.include? '3000' - - "#{result}/api" - end - def permitted_serializable_attributes @permitted_serializable_attributes ||= begin - permitted_serializable_attributes = resource_klass.new(model_class.new, - context).fetchable_fields attrs = params[:attrs].presence || 'id' - permitted_serializable_attributes & attrs.split(',').map(&:to_sym) + attrs.split(',').map(&:to_sym) end end @@ -43,7 +36,7 @@ def user_is_not_member_of_group_error # rubocop:disable Metrics/MethodLength source: { pointer: '/data/relationships/group' }, - status: 422 + status: '422' }] }, status: :unprocessable_entity end diff --git a/app/controllers/v1/article_comments_controller.rb b/app/controllers/v1/article_comments_controller.rb index 1463f2f0..80dd090c 100644 --- a/app/controllers/v1/article_comments_controller.rb +++ b/app/controllers/v1/article_comments_controller.rb @@ -1,3 +1,9 @@ +# frozen_string_literal: true + class V1::ArticleCommentsController < V1::ApplicationController - before_action :doorkeeper_authorize!, except: %i[index show get_related_resources] + include GraphitiCrud + + before_action :doorkeeper_authorize!, except: %i[index show] + + graphiti_resource V1::ArticleCommentResource end diff --git a/app/controllers/v1/articles_controller.rb b/app/controllers/v1/articles_controller.rb index aa2ee18c..ea4cdda4 100644 --- a/app/controllers/v1/articles_controller.rb +++ b/app/controllers/v1/articles_controller.rb @@ -1,3 +1,9 @@ +# frozen_string_literal: true + class V1::ArticlesController < V1::ApplicationController + include GraphitiCrud + before_action :doorkeeper_authorize!, except: %i[index show] + + graphiti_resource V1::ArticleResource end diff --git a/app/controllers/v1/board_room_presences_controller.rb b/app/controllers/v1/board_room_presences_controller.rb index db95d3c2..45d1603c 100644 --- a/app/controllers/v1/board_room_presences_controller.rb +++ b/app/controllers/v1/board_room_presences_controller.rb @@ -1,2 +1,7 @@ +# frozen_string_literal: true + class V1::BoardRoomPresencesController < V1::ApplicationController + include GraphitiCrud + + graphiti_resource V1::BoardRoomPresenceResource end diff --git a/app/controllers/v1/books_controller.rb b/app/controllers/v1/books_controller.rb index f956d46c..45124f07 100644 --- a/app/controllers/v1/books_controller.rb +++ b/app/controllers/v1/books_controller.rb @@ -1,4 +1,10 @@ +# frozen_string_literal: true + class V1::BooksController < V1::ApplicationController + include GraphitiCrud + + graphiti_resource V1::BookResource + def isbn_lookup # rubocop:disable Metrics/AbcSize, Metrics/MethodLength authorize Book diff --git a/app/controllers/v1/debit/collections_controller.rb b/app/controllers/v1/debit/collections_controller.rb index fd561f8a..0dff75eb 100644 --- a/app/controllers/v1/debit/collections_controller.rb +++ b/app/controllers/v1/debit/collections_controller.rb @@ -1,7 +1,13 @@ +# frozen_string_literal: true + module V1::Debit class CollectionsController < V1::ApplicationController + include GraphitiCrud + before_action :set_model, only: %i[sepa] + graphiti_resource V1::Debit::CollectionResource + def sepa authorize Debit::Collection return head :not_found unless @model.transactions.any? @@ -34,9 +40,13 @@ def send_compressed_sepa_files(sepa_files) end def error_response - resource = V1::Debit::CollectionResource.new(@model, {}) - errors = JSONAPI::Exceptions::ValidationErrors.new(resource).errors - JSONAPI::ErrorsOperationResult.new(422, errors) + { + errors: [{ + title: 'Validation Error', + detail: 'Collection has validation errors', + status: '422' + }] + } end end end diff --git a/app/controllers/v1/debit/mandates_controller.rb b/app/controllers/v1/debit/mandates_controller.rb index ad65eb71..f909125b 100644 --- a/app/controllers/v1/debit/mandates_controller.rb +++ b/app/controllers/v1/debit/mandates_controller.rb @@ -1,4 +1,9 @@ +# frozen_string_literal: true + module V1::Debit class MandatesController < V1::ApplicationController + include GraphitiCrud + + graphiti_resource V1::Debit::MandateResource end end diff --git a/app/controllers/v1/debit/transactions_controller.rb b/app/controllers/v1/debit/transactions_controller.rb index 40c9a643..b600dd02 100644 --- a/app/controllers/v1/debit/transactions_controller.rb +++ b/app/controllers/v1/debit/transactions_controller.rb @@ -1,4 +1,9 @@ +# frozen_string_literal: true + module V1::Debit class TransactionsController < V1::ApplicationController + include GraphitiCrud + + graphiti_resource V1::Debit::TransactionResource end end diff --git a/app/controllers/v1/form/closed_question_answers_controller.rb b/app/controllers/v1/form/closed_question_answers_controller.rb index 29721bbb..f2499322 100644 --- a/app/controllers/v1/form/closed_question_answers_controller.rb +++ b/app/controllers/v1/form/closed_question_answers_controller.rb @@ -1,4 +1,9 @@ +# frozen_string_literal: true + module V1::Form class ClosedQuestionAnswersController < V1::ApplicationController + include GraphitiCrud + + graphiti_resource V1::Form::ClosedQuestionAnswerResource end end diff --git a/app/controllers/v1/form/closed_question_options_controller.rb b/app/controllers/v1/form/closed_question_options_controller.rb index ca0aa1d8..f028ca05 100644 --- a/app/controllers/v1/form/closed_question_options_controller.rb +++ b/app/controllers/v1/form/closed_question_options_controller.rb @@ -1,4 +1,9 @@ +# frozen_string_literal: true + module V1::Form class ClosedQuestionOptionsController < V1::ApplicationController + include GraphitiCrud + + graphiti_resource V1::Form::ClosedQuestionOptionResource end end diff --git a/app/controllers/v1/form/closed_questions_controller.rb b/app/controllers/v1/form/closed_questions_controller.rb index 866bc4d1..4a06abb2 100644 --- a/app/controllers/v1/form/closed_questions_controller.rb +++ b/app/controllers/v1/form/closed_questions_controller.rb @@ -1,4 +1,9 @@ +# frozen_string_literal: true + module V1::Form class ClosedQuestionsController < V1::ApplicationController + include GraphitiCrud + + graphiti_resource V1::Form::ClosedQuestionResource end end diff --git a/app/controllers/v1/form/forms_controller.rb b/app/controllers/v1/form/forms_controller.rb index 4bf74292..83c6a726 100644 --- a/app/controllers/v1/form/forms_controller.rb +++ b/app/controllers/v1/form/forms_controller.rb @@ -1,4 +1,9 @@ +# frozen_string_literal: true + module V1::Form class FormsController < V1::ApplicationController + include GraphitiCrud + + graphiti_resource V1::Form::FormResource end end diff --git a/app/controllers/v1/form/open_question_answers_controller.rb b/app/controllers/v1/form/open_question_answers_controller.rb index 277afedb..6c27457f 100644 --- a/app/controllers/v1/form/open_question_answers_controller.rb +++ b/app/controllers/v1/form/open_question_answers_controller.rb @@ -1,4 +1,9 @@ +# frozen_string_literal: true + module V1::Form class OpenQuestionAnswersController < V1::ApplicationController + include GraphitiCrud + + graphiti_resource V1::Form::OpenQuestionAnswerResource end end diff --git a/app/controllers/v1/form/open_questions_controller.rb b/app/controllers/v1/form/open_questions_controller.rb index 9327cbeb..b42786c7 100644 --- a/app/controllers/v1/form/open_questions_controller.rb +++ b/app/controllers/v1/form/open_questions_controller.rb @@ -1,4 +1,9 @@ +# frozen_string_literal: true + module V1::Form class OpenQuestionsController < V1::ApplicationController + include GraphitiCrud + + graphiti_resource V1::Form::OpenQuestionResource end end diff --git a/app/controllers/v1/form/responses_controller.rb b/app/controllers/v1/form/responses_controller.rb index efcf270e..fd8be4d9 100644 --- a/app/controllers/v1/form/responses_controller.rb +++ b/app/controllers/v1/form/responses_controller.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + module V1::Form class ResponsesController < V1::ApplicationController - # def model_fetch_keys - # { user: model_params[:user], form: model_params[:form_id] } - # end + include GraphitiCrud + + graphiti_resource V1::Form::ResponseResource end end diff --git a/app/controllers/v1/forum/categories_controller.rb b/app/controllers/v1/forum/categories_controller.rb index d889c2d8..b99918e6 100644 --- a/app/controllers/v1/forum/categories_controller.rb +++ b/app/controllers/v1/forum/categories_controller.rb @@ -1,4 +1,9 @@ +# frozen_string_literal: true + module V1::Forum class CategoriesController < V1::ApplicationController + include GraphitiCrud + + graphiti_resource V1::Forum::CategoryResource end end diff --git a/app/controllers/v1/forum/posts_controller.rb b/app/controllers/v1/forum/posts_controller.rb index affba7a7..662c11b8 100644 --- a/app/controllers/v1/forum/posts_controller.rb +++ b/app/controllers/v1/forum/posts_controller.rb @@ -1,4 +1,9 @@ +# frozen_string_literal: true + module V1::Forum class PostsController < V1::ApplicationController + include GraphitiCrud + + graphiti_resource V1::Forum::PostResource end end diff --git a/app/controllers/v1/forum/threads_controller.rb b/app/controllers/v1/forum/threads_controller.rb index 2dc3212b..73ff4e94 100644 --- a/app/controllers/v1/forum/threads_controller.rb +++ b/app/controllers/v1/forum/threads_controller.rb @@ -1,8 +1,14 @@ +# frozen_string_literal: true + module V1::Forum class ThreadsController < V1::ApplicationController + include GraphitiCrud + before_action :doorkeeper_authorize! before_action :set_model, only: %i[mark_read] + graphiti_resource V1::Forum::ThreadResource + def mark_read thread = Forum::ReadThread.where(thread: @model, user: current_user).first_or_create thread.post = @model.posts.last diff --git a/app/controllers/v1/groups_controller.rb b/app/controllers/v1/groups_controller.rb index 400f27e6..293e8c9b 100644 --- a/app/controllers/v1/groups_controller.rb +++ b/app/controllers/v1/groups_controller.rb @@ -1,7 +1,13 @@ +# frozen_string_literal: true + class V1::GroupsController < V1::ApplicationController - before_action :doorkeeper_authorize!, except: %i[get_related_resource] + include GraphitiCrud + + before_action :doorkeeper_authorize!, except: %i[index show] before_action :set_model, only: %i[export] + graphiti_resource V1::GroupResource + def export authorize @model description = params[:description] @@ -28,9 +34,8 @@ def send_export_notifications(description) def permitted_serializable_user_attributes @permitted_serializable_user_attributes ||= begin - attributes = V1::UserResource.new(User.new, context).fetchable_fields attrs = params[:user_attrs].presence || 'id' - attributes & attrs.split(',').map(&:to_sym) + attrs.split(',').map(&:to_sym) end end end diff --git a/app/controllers/v1/mail_aliases_controller.rb b/app/controllers/v1/mail_aliases_controller.rb index 977ab99c..287eb17a 100644 --- a/app/controllers/v1/mail_aliases_controller.rb +++ b/app/controllers/v1/mail_aliases_controller.rb @@ -1,2 +1,7 @@ +# frozen_string_literal: true + class V1::MailAliasesController < V1::ApplicationController + include GraphitiCrud + + graphiti_resource V1::MailAliasResource end diff --git a/app/controllers/v1/memberships_controller.rb b/app/controllers/v1/memberships_controller.rb index 5085500c..21ccdcab 100644 --- a/app/controllers/v1/memberships_controller.rb +++ b/app/controllers/v1/memberships_controller.rb @@ -1,2 +1,7 @@ +# frozen_string_literal: true + class V1::MembershipsController < V1::ApplicationController + include GraphitiCrud + + graphiti_resource V1::MembershipResource end diff --git a/app/controllers/v1/permissions_controller.rb b/app/controllers/v1/permissions_controller.rb index 500c0976..40ad6f3a 100644 --- a/app/controllers/v1/permissions_controller.rb +++ b/app/controllers/v1/permissions_controller.rb @@ -1,2 +1,7 @@ +# frozen_string_literal: true + class V1::PermissionsController < V1::ApplicationController + include GraphitiCrud + + graphiti_resource V1::PermissionResource end diff --git a/app/controllers/v1/photo_albums_controller.rb b/app/controllers/v1/photo_albums_controller.rb index 873d82c7..2191b80f 100644 --- a/app/controllers/v1/photo_albums_controller.rb +++ b/app/controllers/v1/photo_albums_controller.rb @@ -1,7 +1,13 @@ +# frozen_string_literal: true + class V1::PhotoAlbumsController < V1::ApplicationController + include GraphitiCrud + before_action :doorkeeper_authorize!, except: %i[index show] before_action :set_model, only: %i[dropzone zip] + graphiti_resource V1::PhotoAlbumResource + def dropzone authorize @model diff --git a/app/controllers/v1/photo_comments_controller.rb b/app/controllers/v1/photo_comments_controller.rb index 03d9a35c..468680d5 100644 --- a/app/controllers/v1/photo_comments_controller.rb +++ b/app/controllers/v1/photo_comments_controller.rb @@ -1,2 +1,7 @@ +# frozen_string_literal: true + class V1::PhotoCommentsController < V1::ApplicationController + include GraphitiCrud + + graphiti_resource V1::PhotoCommentResource end diff --git a/app/controllers/v1/photo_tags_controller.rb b/app/controllers/v1/photo_tags_controller.rb index 5aefb485..92c052a7 100644 --- a/app/controllers/v1/photo_tags_controller.rb +++ b/app/controllers/v1/photo_tags_controller.rb @@ -1,2 +1,7 @@ +# frozen_string_literal: true + class V1::PhotoTagsController < V1::ApplicationController + include GraphitiCrud + + graphiti_resource V1::PhotoTagResource end diff --git a/app/controllers/v1/photos_controller.rb b/app/controllers/v1/photos_controller.rb index 76df8ee2..0329c325 100644 --- a/app/controllers/v1/photos_controller.rb +++ b/app/controllers/v1/photos_controller.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true + class V1::PhotosController < V1::ApplicationController - before_action :doorkeeper_authorize!, except: %i[index show get_related_resources] - before_action do - doorkeeper_authorize! unless %w[index show].include?(action_name) || - (action_name == 'get_related_resources' && - params[:source] == 'v1/photo_albums') - end + include GraphitiCrud + + before_action :doorkeeper_authorize!, except: %i[index show] + + graphiti_resource V1::PhotoResource end diff --git a/app/controllers/v1/polls_controller.rb b/app/controllers/v1/polls_controller.rb index 888b3eaa..1a80edec 100644 --- a/app/controllers/v1/polls_controller.rb +++ b/app/controllers/v1/polls_controller.rb @@ -1,2 +1,7 @@ +# frozen_string_literal: true + class V1::PollsController < V1::ApplicationController + include GraphitiCrud + + graphiti_resource V1::PollResource end diff --git a/app/controllers/v1/room_adverts_controller.rb b/app/controllers/v1/room_adverts_controller.rb index 76b6ea57..f3a7b189 100644 --- a/app/controllers/v1/room_adverts_controller.rb +++ b/app/controllers/v1/room_adverts_controller.rb @@ -1,3 +1,9 @@ +# frozen_string_literal: true + class V1::RoomAdvertsController < V1::ApplicationController + include GraphitiCrud + before_action :doorkeeper_authorize!, except: %i[index show] + + graphiti_resource V1::RoomAdvertResource end diff --git a/app/controllers/v1/static_pages_controller.rb b/app/controllers/v1/static_pages_controller.rb index 4158a358..05e54aee 100644 --- a/app/controllers/v1/static_pages_controller.rb +++ b/app/controllers/v1/static_pages_controller.rb @@ -1,3 +1,9 @@ +# frozen_string_literal: true + class V1::StaticPagesController < V1::ApplicationController + include GraphitiCrud + before_action :doorkeeper_authorize!, except: %i[index show] + + graphiti_resource V1::StaticPageResource end diff --git a/app/controllers/v1/stored_mails_controller.rb b/app/controllers/v1/stored_mails_controller.rb index 9a9a428a..9e2391b3 100644 --- a/app/controllers/v1/stored_mails_controller.rb +++ b/app/controllers/v1/stored_mails_controller.rb @@ -1,7 +1,13 @@ +# frozen_string_literal: true + class V1::StoredMailsController < V1::ApplicationController + include GraphitiCrud + before_action :set_model, only: %i[accept reject] before_action :check_improvmx_limit, only: %i[accept] + graphiti_resource V1::StoredMailResource + def accept authorize @model @@ -33,7 +39,7 @@ def limit_error title: 'Already sent a moderated email today', detail: 'Already sent a moderated email today, try again tomorrow', code: '100', - status: 422 + status: '422' }] }, status: :unprocessable_entity end diff --git a/app/controllers/v1/study_room_presences_controller.rb b/app/controllers/v1/study_room_presences_controller.rb index ec82c76c..e87ef593 100644 --- a/app/controllers/v1/study_room_presences_controller.rb +++ b/app/controllers/v1/study_room_presences_controller.rb @@ -1,2 +1,7 @@ +# frozen_string_literal: true + class V1::StudyRoomPresencesController < V1::ApplicationController + include GraphitiCrud + + graphiti_resource V1::StudyRoomPresenceResource end diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/v1/users_controller.rb index 2f242e68..a47613a6 100644 --- a/app/controllers/v1/users_controller.rb +++ b/app/controllers/v1/users_controller.rb @@ -1,12 +1,31 @@ class V1::UsersController < V1::ApplicationController # rubocop:disable Metrics/ClassLength include SpreadsheetHelper - before_action :doorkeeper_authorize!, except: %i[activate_account reset_password - get_related_resource] + before_action :doorkeeper_authorize!, except: %i[activate_account reset_password show] before_action :set_model, only: %i[update archive activate_account resend_activation_mail generate_otp_secret activate_otp] + # Graphiti CRUD actions + def index + users = V1::UserResource.all(params, context) + render jsonapi: users + end + + def show + user = V1::UserResource.find(params, context) + render jsonapi: user + end + + def create + user = V1::UserResource.build(params, context) + if user.save + render jsonapi: user, status: :created + else + render jsonapi_errors: user + end + end + def update password = params.dig('data', 'attributes', 'password') old_password = params.dig('data', 'attributes', 'old_password') @@ -14,7 +33,12 @@ def update render json: old_password_invalid_error, status: :unprocessable_entity else remove_password_from_params_when_blank? - super + user = V1::UserResource.find(params, context) + if user.update_attributes + render jsonapi: user + else + render jsonapi_errors: user + end end end diff --git a/app/controllers/v1/vacancies_controller.rb b/app/controllers/v1/vacancies_controller.rb index c40f110e..f3ea3de8 100644 --- a/app/controllers/v1/vacancies_controller.rb +++ b/app/controllers/v1/vacancies_controller.rb @@ -1,2 +1,7 @@ +# frozen_string_literal: true + class V1::VacanciesController < V1::ApplicationController + include GraphitiCrud + + graphiti_resource V1::VacancyResource end diff --git a/app/resources/v1/activity_resource.rb b/app/resources/v1/activity_resource.rb index 45db190d..b558699e 100644 --- a/app/resources/v1/activity_resource.rb +++ b/app/resources/v1/activity_resource.rb @@ -1,52 +1,66 @@ +# frozen_string_literal: true + class V1::ActivityResource < V1::ApplicationResource - attributes :title, :description, :description_camofied, :price, :location, :start_time, - :end_time, :category, :publicly_visible, :cover_photo_url, :cover_photo + self.model = Activity - def cover_photo_url - @model.cover_photo.url + attribute :title, :string + attribute :description, :string + attribute :description_camofied, :string, writable: false do + camofy(@object['description']) end - - def description_camofied - camofy(@model['description']) + attribute :price, :float + attribute :location, :string + attribute :start_time, :datetime + attribute :end_time, :datetime + attribute :category, :string + attribute :publicly_visible, :boolean + attribute :cover_photo_url, :string, writable: false do + @object.cover_photo.url end + attribute :cover_photo, :string, readable: false # Write-only for uploads - has_one :form, always_include_linkage_data: true - has_one :author, always_include_linkage_data: true - has_one :group, always_include_linkage_data: true - - filter :upcoming, apply: ->(records, _value, _options) { records.upcoming } - filter :closing, apply: ->(records, _value, _options) { records.closing } - filter :group, apply: ->(records, value, _options) { records.where(group_id: value) } + has_one :form, resource: V1::Form::FormResource + has_one :author, resource: V1::UserResource + has_one :group - def fetchable_fields - super - [:cover_photo] + filter :upcoming, :boolean do + eq do |scope, value| + value ? scope.upcoming : scope + end end - def self.creatable_fields(_context) - %i[form title description group respond_until respond_from price - location start_time end_time category publicly_visible cover_photo] + filter :closing, :boolean do + eq do |scope, value| + value ? scope.closing : scope + end end - def self.searchable_fields - %i[title description location] + filter :group, :integer do + eq do |scope, value| + scope.where(group_id: value) + end end - def self.sortable_fields(context) - super + [:'form.respond_until'] + sort :form_respond_until, :datetime do |scope, direction| + scope.joins(:form).order("form_forms.respond_until #{direction}") end - before_create do - @model.author_id = current_user.id + searchable_fields :title, :description, :location + + before_save only: [:create] do |model| + model.author_id = current_user.id end - before_save do - user_is_member_of_group? + before_save do |model| + user_is_member_of_group?(model) end - def user_is_member_of_group? - return true unless @model.group - return true if current_user.permission?(:update, @model) - return false if current_user.current_group_member?(@model.group) + private + + def user_is_member_of_group?(model) + return true unless model.group + return true if current_user.permission?(:update, model) + return false if current_user.current_group_member?(model.group) raise AmberError::NotMemberOfGroupError end diff --git a/app/resources/v1/application_resource.rb b/app/resources/v1/application_resource.rb index 0eddfc9b..fb0edc20 100644 --- a/app/resources/v1/application_resource.rb +++ b/app/resources/v1/application_resource.rb @@ -1,91 +1,83 @@ -class V1::ApplicationResource < JSONAPI::Resource - include MarkdownHelper - include JSONAPI::Authorization::PunditScopedResource +# frozen_string_literal: true - abstract +class V1::ApplicationResource < Graphiti::Resource + include MarkdownHelper - attributes :created_at, :updated_at + # Use ActiveRecord adapter + self.adapter = Graphiti::Adapters::ActiveRecord + self.abstract_class = true - filter :search + # Default attributes for all resources + attribute :created_at, :datetime, writable: false + attribute :updated_at, :datetime, writable: false - # :nocov: - def self.creatable_fields(_context) - [] - end - # :nocov: + # Search filter - override searchable_fields in child resources + filter :search, :string do + eq do |scope, value| + searchable = self.class.config[:searchable_fields] || [] + return scope if searchable.empty? - def self.updatable_fields(context) - creatable_fields(context) + arel = scope.model.arel_table + value.to_s.split.each do |word| + conditions = searchable.map { |field| arel[field].lower.matches("%#{word.downcase}%") }.inject(:or) + scope = scope.where(conditions) + end + scope + end end - # :nocov: - def self.searchable_fields - [] + # Class method to define searchable fields + def self.searchable_fields(*fields) + if fields.any? + config[:searchable_fields] = fields + else + config[:searchable_fields] || [] + end end - # :nocov: - def self.apply_filter(records, filter, value, options) - # Monkeypatch for weird bug in filter method - # When defining a filter on application level - # it will be applied before knowing which resource it is in - # When doing it with overwriting apply_filter (as done here) it knows which resource it is - case filter - when :search - search(records, value) + # Apply Pundit scoping on index + def base_scope + if context&.dig(:action) == 'index' && current_user_or_application + Pundit.policy_scope!(current_user_or_application, self.class.model) else - super + self.class.model.all end end - def self.search(records, value) - return records if records == [] + # Authorization helpers + def current_user + context&.dig(:user) + end - arel = records.first.class.arel_table - value.each do |val| - val.split.each do |word| - records = records.where( - searchable_fields.map { |field| arel[field].lower.matches("%#{word.downcase}%") }.inject(:or) - ) - end - end - records + def current_application + context&.dig(:application) end - def self.records(options = {}) - is_index = options.fetch(:context, {}).fetch(:action, {}) == 'index' - includes = options.fetch(:includes, {}) || [] - records ||= _model_class.includes(includes) - if is_index - records = Pundit.policy_scope!(current_user_or_application(options), - _model_class).includes(includes) - end - records + def current_user_or_application + current_user || current_application end def read_permission? - current_user&.permission?(:read, @model) + current_user&.permission?(:read, @object) end def update_permission? - current_user&.permission?(:update, @model) - end - - def self.user_can_create_or_update?(context) - context[:user]&.permission?(:create, _model_class) || - context[:user]&.permission?(:update, _model_class) + current_user&.permission?(:update, @object) end - def current_user - context.fetch(:user) - end + # Class-level permission checks + class << self + def user_can_create_or_update?(context) + user = context&.dig(:user) + user&.permission?(:create, model) || user&.permission?(:update, model) + end - # :nocov: - def current_application - context.fetch(:application) - end - # :nocov: + def update_permission?(context) + context&.dig(:user)&.permission?(:update, model) + end - def self.current_user_or_application(options) - options.fetch(:context).fetch(:user) || options.fetch(:context).fetch(:application) + def read_permission?(context) + context&.dig(:user)&.permission?(:read, model) + end end end diff --git a/app/resources/v1/article_comment_resource.rb b/app/resources/v1/article_comment_resource.rb index 0a451096..8cb1fe09 100644 --- a/app/resources/v1/article_comment_resource.rb +++ b/app/resources/v1/article_comment_resource.rb @@ -1,14 +1,14 @@ +# frozen_string_literal: true + class V1::ArticleCommentResource < V1::ApplicationResource - attributes :content + self.model = ArticleComment - has_one :article - has_one :author, always_include_linkage_data: true + attribute :content, :string - def self.creatable_fields(_context) - %i[content article] - end + has_one :article + has_one :author, resource: V1::UserResource - before_create do - @model.author_id = current_user.id + before_save only: [:create] do |model| + model.author_id = current_user.id end end diff --git a/app/resources/v1/article_resource.rb b/app/resources/v1/article_resource.rb index a582a94d..15d7422e 100644 --- a/app/resources/v1/article_resource.rb +++ b/app/resources/v1/article_resource.rb @@ -1,62 +1,51 @@ +# frozen_string_literal: true + class V1::ArticleResource < V1::ApplicationResource - attributes :title, :content, :publicly_visible, :content_camofied, :cover_photo, - :amount_of_comments, :cover_photo_url, :author_name, :avatar_thumb_url, :pinned + self.model = Article - def amount_of_comments - @model.comments.size + attribute :title, :string + attribute :content, :string + attribute :publicly_visible, :boolean + attribute :content_camofied, :string, writable: false do + camofy(@object['content']) end - - def cover_photo_url - @model.cover_photo.url + attribute :cover_photo, :string, readable: false # Write-only for uploads + attribute :amount_of_comments, :integer, writable: false do + @object.comments.size end - - def author_name - @model.group ? @model.group.name : @model.author.full_name + attribute :cover_photo_url, :string, writable: false do + @object.cover_photo.url end - - def avatar_thumb_url - @model.group ? @model.group.avatar.thumb.url : @model.author.avatar.thumb.url + attribute :author_name, :string, writable: false do + @object.group ? @object.group.name : @object.author.full_name end - - def content_camofied - camofy(@model['content']) + attribute :avatar_thumb_url, :string, writable: false do + @object.group ? @object.group.avatar.thumb.url : @object.author.avatar.thumb.url end - - has_one :author, always_include_linkage_data: true - has_one :group, always_include_linkage_data: true - has_many :comments - - def fetchable_fields - super - [:cover_photo] + attribute :pinned, :boolean do + writable { self.class.update_permission?(context) } end - def self.creatable_fields(context) - attributes = %i[title content publicly_visible group cover_photo] + has_one :author, resource: V1::UserResource + has_one :group + has_many :comments, resource: V1::ArticleCommentResource - attributes += [:pinned] if update_permission?(context) - attributes - end + searchable_fields :title, :content - def self.update_permission?(context) - context[:user]&.permission?(:update, _model_class) + before_save only: [:create] do |model| + model.author_id = current_user.id end - def self.searchable_fields - %i[title content] + before_save do |model| + user_is_member_of_group?(model) end - before_create do - @model.author_id = current_user.id - end - - before_save do - user_is_member_of_group? - end + private - def user_is_member_of_group? - return true unless @model.group - return true if current_user.permission?(:update, @model) - return false if current_user.current_group_member?(@model.group) + def user_is_member_of_group?(model) + return true unless model.group + return true if current_user.permission?(:update, model) + return false if current_user.current_group_member?(model.group) raise AmberError::NotMemberOfGroupError end diff --git a/app/resources/v1/board_room_presence_resource.rb b/app/resources/v1/board_room_presence_resource.rb index 2713eb83..96c16719 100644 --- a/app/resources/v1/board_room_presence_resource.rb +++ b/app/resources/v1/board_room_presence_resource.rb @@ -1,17 +1,27 @@ +# frozen_string_literal: true + class V1::BoardRoomPresenceResource < V1::ApplicationResource - attributes :start_time, :end_time, :status + self.model = BoardRoomPresence + + attribute :start_time, :datetime + attribute :end_time, :datetime + attribute :status, :string - has_one :user, always_include_linkage_data: true + has_one :user, resource: V1::UserResource - filter :current, apply: ->(records, _value, _options) { records.current } - filter :future, apply: ->(records, _value, _options) { records.future } - filter :current_and_future, apply: ->(records, _value, _options) { records.current_and_future } + filter :current, :boolean do + eq { |scope, value| value ? scope.current : scope } + end + + filter :future, :boolean do + eq { |scope, value| value ? scope.future : scope } + end - before_create do - @model.user_id = current_user.id + filter :current_and_future, :boolean do + eq { |scope, value| value ? scope.current_and_future : scope } end - def self.creatable_fields(_context) - %i[start_time end_time status] + before_save only: [:create] do |model| + model.user_id = current_user.id end end diff --git a/app/resources/v1/book_resource.rb b/app/resources/v1/book_resource.rb index af096c24..10350764 100644 --- a/app/resources/v1/book_resource.rb +++ b/app/resources/v1/book_resource.rb @@ -1,19 +1,16 @@ -class V1::BookResource < V1::ApplicationResource - attributes :title, :author, :description, :isbn, :cover_photo, :cover_photo_url - - def cover_photo_url - @model.cover_photo.url - end +# frozen_string_literal: true - def fetchable_fields - super - [:cover_photo] - end +class V1::BookResource < V1::ApplicationResource + self.model = Book - def self.creatable_fields(_context) - %i[title author description isbn cover_photo] + attribute :title, :string + attribute :author, :string + attribute :description, :string + attribute :isbn, :string + attribute :cover_photo, :string, readable: false # Write-only + attribute :cover_photo_url, :string, writable: false do + @object.cover_photo.url end - def self.searchable_fields - %i[title author description isbn] - end + searchable_fields :title, :author, :description, :isbn end diff --git a/app/resources/v1/debit/collection_resource.rb b/app/resources/v1/debit/collection_resource.rb index 0bedc573..9b297c35 100644 --- a/app/resources/v1/debit/collection_resource.rb +++ b/app/resources/v1/debit/collection_resource.rb @@ -1,28 +1,22 @@ +# frozen_string_literal: true + class V1::Debit::CollectionResource < V1::ApplicationResource - attributes :name, :date, :import_file - model_name 'Debit::Collection' - model_hint model: User, resource: V1::UserResource + self.model = Debit::Collection - has_one :author, always_include_linkage_data: true - has_many :transactions + attribute :name, :string + attribute :date, :date + attribute :import_file, :string, readable: false # Write-only - def fetchable_fields - super - [:import_file] - end + has_one :author, resource: V1::UserResource + has_many :transactions, resource: V1::Debit::TransactionResource - def self.creatable_fields(_context) - %i[name date import_file] - end - - def self.searchable_fields - %i[name] - end + searchable_fields :name - before_create do - @model.author_id = current_user.id + before_save only: [:create] do |model| + model.author_id = current_user.id end - after_create do - CollectionImportJob.perform_later(@model.import_file, @model, context[:user]) + after_commit only: [:create] do |model| + CollectionImportJob.perform_later(model.import_file, model, context[:user]) end end diff --git a/app/resources/v1/debit/mandate_resource.rb b/app/resources/v1/debit/mandate_resource.rb index 213568d6..dc0b483b 100644 --- a/app/resources/v1/debit/mandate_resource.rb +++ b/app/resources/v1/debit/mandate_resource.rb @@ -1,14 +1,14 @@ +# frozen_string_literal: true + class V1::Debit::MandateResource < V1::ApplicationResource - attributes :start_date, :end_date, :iban, :iban_holder - model_name 'Debit::Mandate' + self.model = Debit::Mandate - has_one :user, always_include_linkage_data: true + attribute :start_date, :date + attribute :end_date, :date + attribute :iban, :string + attribute :iban_holder, :string - def self.creatable_fields(_context) - %i[user start_date end_date iban iban_holder] - end + has_one :user, resource: V1::UserResource - def self.searchable_fields - %i[iban iban_holder] - end + searchable_fields :iban, :iban_holder end diff --git a/app/resources/v1/debit/transaction_resource.rb b/app/resources/v1/debit/transaction_resource.rb index 8f0c2cfa..177997f9 100644 --- a/app/resources/v1/debit/transaction_resource.rb +++ b/app/resources/v1/debit/transaction_resource.rb @@ -1,17 +1,15 @@ +# frozen_string_literal: true + class V1::Debit::TransactionResource < V1::ApplicationResource - attributes :description, :amount - model_name 'Debit::Transaction' + self.model = Debit::Transaction - has_one :collection - has_one :user, always_include_linkage_data: true + attribute :description, :string + attribute :amount, :float - filter :collection + has_one :collection, resource: V1::Debit::CollectionResource + has_one :user, resource: V1::UserResource - def self.creatable_fields(_context) - %i[user collection description amount] - end + filter :collection, :integer - def self.searchable_fields - %i[description] - end + searchable_fields :description end diff --git a/app/resources/v1/debit/user_resource.rb b/app/resources/v1/debit/user_resource.rb index defcc2f3..2d6388ac 100644 --- a/app/resources/v1/debit/user_resource.rb +++ b/app/resources/v1/debit/user_resource.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + class V1::Debit::UserResource < V1::UserResource end diff --git a/app/resources/v1/form/closed_question_answer_resource.rb b/app/resources/v1/form/closed_question_answer_resource.rb index 2bd38e26..d5a9a0d7 100644 --- a/app/resources/v1/form/closed_question_answer_resource.rb +++ b/app/resources/v1/form/closed_question_answer_resource.rb @@ -1,10 +1,8 @@ -class V1::Form::ClosedQuestionAnswerResource < V1::ApplicationResource - model_name 'Form::ClosedQuestionAnswer' +# frozen_string_literal: true - has_one :response, always_include_linkage_data: true - has_one :option, always_include_linkage_data: true +class V1::Form::ClosedQuestionAnswerResource < V1::ApplicationResource + self.model = Form::ClosedQuestionAnswer - def self.creatable_fields(_context) - %i[option response] - end + has_one :response, resource: V1::Form::ResponseResource + has_one :option, resource: V1::Form::ClosedQuestionOptionResource end diff --git a/app/resources/v1/form/closed_question_option_resource.rb b/app/resources/v1/form/closed_question_option_resource.rb index df20564f..f1a5caa2 100644 --- a/app/resources/v1/form/closed_question_option_resource.rb +++ b/app/resources/v1/form/closed_question_option_resource.rb @@ -1,16 +1,19 @@ +# frozen_string_literal: true + class V1::Form::ClosedQuestionOptionResource < V1::ApplicationResource - model_name 'Form::ClosedQuestionOption' - attributes :option, :position + self.model = Form::ClosedQuestionOption - has_one :question, always_include_linkage_data: true - has_many :answers, always_include_linkage_data: true + attribute :option, :string + attribute :position, :integer - def self.records(options = {}) - options[:includes] = [:answers] if options[:context][:action] == 'index' - super - end + has_one :question, resource: V1::Form::ClosedQuestionResource + has_many :answers, resource: V1::Form::ClosedQuestionAnswerResource - def self.creatable_fields(_context) - %i[option position question] + def base_scope + scope = super + if context&.dig(:action) == 'index' + scope = scope.includes(:answers) + end + scope end end diff --git a/app/resources/v1/form/closed_question_resource.rb b/app/resources/v1/form/closed_question_resource.rb index f37accc8..1bac72ad 100644 --- a/app/resources/v1/form/closed_question_resource.rb +++ b/app/resources/v1/form/closed_question_resource.rb @@ -1,16 +1,21 @@ +# frozen_string_literal: true + class V1::Form::ClosedQuestionResource < V1::ApplicationResource - model_name 'Form::ClosedQuestion' - attributes :question, :field_type, :required, :position + self.model = Form::ClosedQuestion - has_one :form, always_include_linkage_data: true - has_many :options, always_include_linkage_data: true + attribute :question, :string + attribute :field_type, :string + attribute :required, :boolean + attribute :position, :integer - def self.records(options = {}) - options[:includes] = [:options] if options[:context][:action] == 'index' - super - end + has_one :form, resource: V1::Form::FormResource + has_many :options, resource: V1::Form::ClosedQuestionOptionResource - def self.creatable_fields(_context) - %i[question field_type required position form] + def base_scope + scope = super + if context&.dig(:action) == 'index' + scope = scope.includes(:options) + end + scope end end diff --git a/app/resources/v1/form/form_resource.rb b/app/resources/v1/form/form_resource.rb index 8754517c..940b07c6 100644 --- a/app/resources/v1/form/form_resource.rb +++ b/app/resources/v1/form/form_resource.rb @@ -1,38 +1,39 @@ +# frozen_string_literal: true + class V1::Form::FormResource < V1::ApplicationResource - model_name 'Form::Form' - attributes :respond_from, :respond_until, :amount_of_responses, :current_user_response_id, - :current_user_response_completed + self.model = Form::Form - def amount_of_responses - @model.responses.completed.size + attribute :respond_from, :datetime + attribute :respond_until, :datetime + attribute :amount_of_responses, :integer, writable: false do + @object.responses.completed.size end - - def current_user_response - @model.responses.find_by(user_id: current_user.id) + attribute :current_user_response_id, :integer, writable: false do + current_user_response&.id end - - def current_user_response_id - current_user_response.try(:id) + attribute :current_user_response_completed, :boolean, writable: false do + current_user_response&.completed end - def current_user_response_completed - current_user_response.try(:completed) - end + has_many :responses, resource: V1::Form::ResponseResource + has_many :open_questions, resource: V1::Form::OpenQuestionResource + has_many :closed_questions, resource: V1::Form::ClosedQuestionResource - def self.records(options = {}) - options[:includes] = %i[responses open_questions closed_questions] if options[:context][:action] == 'index' - super + before_save only: [:create] do |model| + model.author_id = current_user.id end - def self.creatable_fields(_context) - %i[group respond_from respond_until] + def base_scope + scope = super + if context&.dig(:action) == 'index' + scope = scope.includes(:responses, :open_questions, :closed_questions) + end + scope end - before_create do - @model.author_id = current_user.id - end + private - has_many :responses, always_include_linkage_data: true - has_many :open_questions, always_include_linkage_data: true - has_many :closed_questions, always_include_linkage_data: true + def current_user_response + @current_user_response ||= @object.responses.find_by(user_id: current_user&.id) + end end diff --git a/app/resources/v1/form/open_question_answer_resource.rb b/app/resources/v1/form/open_question_answer_resource.rb index b9f433f1..a65bad06 100644 --- a/app/resources/v1/form/open_question_answer_resource.rb +++ b/app/resources/v1/form/open_question_answer_resource.rb @@ -1,11 +1,10 @@ +# frozen_string_literal: true + class V1::Form::OpenQuestionAnswerResource < V1::ApplicationResource - model_name 'Form::OpenQuestionAnswer' - attributes :answer + self.model = Form::OpenQuestionAnswer - has_one :response, always_include_linkage_data: true - has_one :question, always_include_linkage_data: true + attribute :answer, :string - def self.creatable_fields(_context) - %i[answer response question] - end + has_one :response, resource: V1::Form::ResponseResource + has_one :question, resource: V1::Form::OpenQuestionResource end diff --git a/app/resources/v1/form/open_question_resource.rb b/app/resources/v1/form/open_question_resource.rb index d4e62e42..bc4fd828 100644 --- a/app/resources/v1/form/open_question_resource.rb +++ b/app/resources/v1/form/open_question_resource.rb @@ -1,16 +1,21 @@ +# frozen_string_literal: true + class V1::Form::OpenQuestionResource < V1::ApplicationResource - model_name 'Form::OpenQuestion' - attributes :question, :field_type, :required, :position + self.model = Form::OpenQuestion - has_one :form, always_include_linkage_data: true - has_many :answers, always_include_linkage_data: true + attribute :question, :string + attribute :field_type, :string + attribute :required, :boolean + attribute :position, :integer - def self.records(options = {}) - options[:includes] = [:answers] if options[:context][:action] == 'index' - super - end + has_one :form, resource: V1::Form::FormResource + has_many :answers, resource: V1::Form::OpenQuestionAnswerResource - def self.creatable_fields(_context) - %i[form question field_type required position] + def base_scope + scope = super + if context&.dig(:action) == 'index' + scope = scope.includes(:answers) + end + scope end end diff --git a/app/resources/v1/form/response_resource.rb b/app/resources/v1/form/response_resource.rb index 0b932c88..7620493e 100644 --- a/app/resources/v1/form/response_resource.rb +++ b/app/resources/v1/form/response_resource.rb @@ -1,22 +1,24 @@ +# frozen_string_literal: true + class V1::Form::ResponseResource < V1::ApplicationResource - model_name 'Form::Response' - attributes :completed + self.model = Form::Response - has_one :user, always_include_linkage_data: true - has_one :form, always_include_linkage_data: true - has_many :open_question_answers, always_include_linkage_data: true - has_many :closed_question_answers, always_include_linkage_data: true + attribute :completed, :boolean - def self.records(options = {}) - options[:includes] = %i[open_question_answers closed_question_answers] if options[:context][:action] == 'index' - super - end + has_one :user, resource: V1::UserResource + has_one :form, resource: V1::Form::FormResource + has_many :open_question_answers, resource: V1::Form::OpenQuestionAnswerResource + has_many :closed_question_answers, resource: V1::Form::ClosedQuestionAnswerResource - def self.creatable_fields(_context) - %i[form] + def base_scope + scope = super + if context&.dig(:action) == 'index' + scope = scope.includes(:open_question_answers, :closed_question_answers) + end + scope end - before_create do - @model.user_id = current_user.id + before_save only: [:create] do |model| + model.user_id = current_user.id end end diff --git a/app/resources/v1/form/user_resource.rb b/app/resources/v1/form/user_resource.rb index cc787964..fb30e343 100644 --- a/app/resources/v1/form/user_resource.rb +++ b/app/resources/v1/form/user_resource.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + class V1::Form::UserResource < V1::UserResource end diff --git a/app/resources/v1/forum/category_resource.rb b/app/resources/v1/forum/category_resource.rb index 3951407f..3482618e 100644 --- a/app/resources/v1/forum/category_resource.rb +++ b/app/resources/v1/forum/category_resource.rb @@ -1,23 +1,22 @@ +# frozen_string_literal: true + class V1::Forum::CategoryResource < V1::ApplicationResource - model_name 'Forum::Category' - attributes :name, :amount_of_threads + self.model = Forum::Category - def amount_of_threads - @model.threads.size + attribute :name, :string + attribute :amount_of_threads, :integer, writable: false do + @object.threads.size end - has_many :threads, always_include_linkage_data: true - - def self.records(options = {}) - options[:includes] = [:threads] if options[:context][:action] == 'index' - super - end + has_many :threads, resource: V1::Forum::ThreadResource - def self.creatable_fields(_context) - [:name] + def base_scope + scope = super + if context&.dig(:action) == 'index' + scope = scope.includes(:threads) + end + scope end - def self.searchable_fields - %i[name] - end + searchable_fields :name end diff --git a/app/resources/v1/forum/post_resource.rb b/app/resources/v1/forum/post_resource.rb index 67bd8f00..38f5052f 100644 --- a/app/resources/v1/forum/post_resource.rb +++ b/app/resources/v1/forum/post_resource.rb @@ -1,25 +1,21 @@ +# frozen_string_literal: true + class V1::Forum::PostResource < V1::ApplicationResource - model_name 'Forum::Post' - attributes :message, :message_camofied + self.model = Forum::Post - def message_camofied - camofy(@model['message']) + attribute :message, :string + attribute :message_camofied, :string, writable: false do + camofy(@object['message']) end - has_one :author, always_include_linkage_data: true - has_one :thread - - filter :thread + has_one :author, resource: V1::UserResource + has_one :thread, resource: V1::Forum::ThreadResource - def self.creatable_fields(_context) - %i[message user thread] - end + filter :thread, :integer - def self.searchable_fields - %i[message] - end + searchable_fields :message - before_create do - @model.author_id = current_user.id + before_save only: [:create] do |model| + model.author_id = current_user.id end end diff --git a/app/resources/v1/forum/thread_resource.rb b/app/resources/v1/forum/thread_resource.rb index 74e3c491..3ca5d129 100644 --- a/app/resources/v1/forum/thread_resource.rb +++ b/app/resources/v1/forum/thread_resource.rb @@ -1,33 +1,28 @@ +# frozen_string_literal: true + class V1::Forum::ThreadResource < V1::ApplicationResource - model_name 'Forum::Thread' - attributes :title, :closed_at, :amount_of_posts, :read + self.model = Forum::Thread - def amount_of_posts - @model.posts.size + attribute :title, :string + attribute :closed_at, :datetime do + writable { self.class.user_can_create_or_update?(context) } end - - def read - @model.read?(current_user) + attribute :amount_of_posts, :integer, writable: false do + @object.posts.size + end + attribute :read, :boolean, writable: false do + @object.read?(current_user) end - has_one :author, always_include_linkage_data: true - has_one :category, always_include_linkage_data: true - has_many :posts - - filter :category - - def self.creatable_fields(context) - attributes = %i[title author category] + has_one :author, resource: V1::UserResource + has_one :category, resource: V1::Forum::CategoryResource + has_many :posts, resource: V1::Forum::PostResource - attributes += [:closed_at] if user_can_create_or_update?(context) - attributes - end + filter :category, :integer - def self.searchable_fields - %i[title] - end + searchable_fields :title - before_create do - @model.author_id = current_user.id + before_save only: [:create] do |model| + model.author_id = current_user.id end end diff --git a/app/resources/v1/forum/user_resource.rb b/app/resources/v1/forum/user_resource.rb index bca9399d..fe314c79 100644 --- a/app/resources/v1/forum/user_resource.rb +++ b/app/resources/v1/forum/user_resource.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + class V1::Forum::UserResource < V1::UserResource end diff --git a/app/resources/v1/group_resource.rb b/app/resources/v1/group_resource.rb index 976e5fd6..411bd988 100644 --- a/app/resources/v1/group_resource.rb +++ b/app/resources/v1/group_resource.rb @@ -1,18 +1,41 @@ +# frozen_string_literal: true + class V1::GroupResource < V1::ApplicationResource - attributes :name, :avatar_url, :avatar_thumb_url, :description, :description_camofied, - :kind, :recognized_at_gma, :rejected_at_gma, :administrative, :avatar + self.model = Group - def avatar_url - @model.avatar.url + attribute :name, :string do + writable { user_can_create_or_update? } end - - def avatar_thumb_url - @model.avatar.thumb.url + attribute :avatar_url, :string, writable: false do + @object.avatar.url end - - def description_camofied - camofy(@model['description']) + attribute :avatar_thumb_url, :string, writable: false do + @object.avatar.thumb.url + end + attribute :description, :string do + readable { current_user.present? } + end + attribute :description_camofied, :string, writable: false do + readable { current_user.present? } + camofy(@object['description']) + end + attribute :kind, :string do + readable { current_user.present? } + writable { user_can_create_or_update? } + end + attribute :recognized_at_gma, :date do + readable { current_user.present? } + writable { user_can_create_or_update? } end + attribute :rejected_at_gma, :date do + readable { current_user.present? } + writable { user_can_create_or_update? } + end + attribute :administrative, :boolean do + readable { current_user.present? } + writable { user_can_create_or_update? } + end + attribute :avatar, :string, readable: false # Write-only for uploads has_many :users has_many :memberships @@ -20,27 +43,21 @@ def description_camofied has_many :permissions has_many :articles - filter :active, apply: ->(records, _value, _options) { records.active } - filter :kind - filter :administrative + filter :active, :boolean do + eq do |scope, value| + value ? scope.active : scope + end + end - def fetchable_fields - return super - [:avatar] if current_user + filter :kind, :string - super - %i[description description_camofied kind - recognized_at_gma rejected_at_gma administrative avatar] - end + filter :administrative, :boolean - def self.creatable_fields(context) - attributes = %i[avatar description] + searchable_fields :name - if user_can_create_or_update?(context) - attributes += %i[name kind recognized_at_gma rejected_at_gma administrative permissions] - end - attributes - end + private - def self.searchable_fields - %i[name] + def user_can_create_or_update? + self.class.user_can_create_or_update?(context) end end diff --git a/app/resources/v1/groups_permissions_resource.rb b/app/resources/v1/groups_permissions_resource.rb index 5e4dab02..95949d18 100644 --- a/app/resources/v1/groups_permissions_resource.rb +++ b/app/resources/v1/groups_permissions_resource.rb @@ -1,4 +1,8 @@ +# frozen_string_literal: true + class V1::GroupsPermissionsResource < V1::ApplicationResource + self.model = GroupsPermissions + has_one :permission has_one :group end diff --git a/app/resources/v1/mail_alias_resource.rb b/app/resources/v1/mail_alias_resource.rb index 3c31f077..830d45fb 100644 --- a/app/resources/v1/mail_alias_resource.rb +++ b/app/resources/v1/mail_alias_resource.rb @@ -1,21 +1,20 @@ -class V1::MailAliasResource < V1::ApplicationResource - attributes :email, :moderation_type, :description, :smtp_enabled, :last_received_at - - has_one :group, always_include_linkage_data: true - has_one :user, always_include_linkage_data: true - has_one :moderator_group, always_include_linkage_data: true +# frozen_string_literal: true - def fetchable_fields - return super - [:last_received_at] unless update_permission? +class V1::MailAliasResource < V1::ApplicationResource + self.model = MailAlias - super + attribute :email, :string + attribute :moderation_type, :string + attribute :description, :string + attribute :smtp_enabled, :boolean + attribute :last_received_at, :datetime do + readable { update_permission? } + writable false end - def self.creatable_fields(_context) - %i[email moderation_type moderator_group description smtp_enabled group user] - end + has_one :group + has_one :user, resource: V1::UserResource + has_one :moderator_group, resource: V1::GroupResource - def self.searchable_fields - %i[email description] - end + searchable_fields :email, :description end diff --git a/app/resources/v1/membership_resource.rb b/app/resources/v1/membership_resource.rb index da52369b..b95f8d35 100644 --- a/app/resources/v1/membership_resource.rb +++ b/app/resources/v1/membership_resource.rb @@ -1,14 +1,16 @@ +# frozen_string_literal: true + class V1::MembershipResource < V1::ApplicationResource - attributes :start_date, :end_date, :function + self.model = Membership - has_one :user, always_include_linkage_data: true - has_one :group, always_include_linkage_data: true + attribute :start_date, :date + attribute :end_date, :date + attribute :function, :string - before_save do - @model.start_date ||= Date.current - end + has_one :user, resource: V1::UserResource + has_one :group - def self.creatable_fields(_context) - %i[start_date end_date function group user] + before_save do |model| + model.start_date ||= Date.current end end diff --git a/app/resources/v1/permission_resource.rb b/app/resources/v1/permission_resource.rb index 177cc9f7..34980f7b 100644 --- a/app/resources/v1/permission_resource.rb +++ b/app/resources/v1/permission_resource.rb @@ -1,8 +1,11 @@ +# frozen_string_literal: true + +require 'case_transform' + class V1::PermissionResource < V1::ApplicationResource - require 'case_transform' - attributes :name + self.model = Permission - def name - CaseTransform.dash(@model.name) + attribute :name, :string, writable: false do + CaseTransform.dash(@object.name) end end diff --git a/app/resources/v1/permissions_users_resource.rb b/app/resources/v1/permissions_users_resource.rb index 8390c819..e2d17715 100644 --- a/app/resources/v1/permissions_users_resource.rb +++ b/app/resources/v1/permissions_users_resource.rb @@ -1,4 +1,8 @@ +# frozen_string_literal: true + class V1::PermissionsUsersResource < V1::ApplicationResource + self.model = PermissionsUsers + has_one :permission - has_one :user + has_one :user, resource: V1::UserResource end diff --git a/app/resources/v1/photo_album_resource.rb b/app/resources/v1/photo_album_resource.rb index a79e8910..9213a4f0 100644 --- a/app/resources/v1/photo_album_resource.rb +++ b/app/resources/v1/photo_album_resource.rb @@ -1,32 +1,36 @@ +# frozen_string_literal: true + class V1::PhotoAlbumResource < V1::ApplicationResource - attributes :title, :date, :publicly_visible + self.model = PhotoAlbum + + attribute :title, :string + attribute :date, :date + attribute :publicly_visible, :boolean - filter :without_photo_tags, apply: ->(records, _value, _options) { records.without_photo_tags } + filter :without_photo_tags, :boolean do + eq { |scope, value| value ? scope.without_photo_tags : scope } + end has_many :photos - has_one :author, always_include_linkage_data: true - has_one :group, always_include_linkage_data: true + has_one :author, resource: V1::UserResource + has_one :group - def self.creatable_fields(_context) - %i[title date publicly_visible group] - end + searchable_fields :title - def self.searchable_fields - %i[title] + before_save only: [:create] do |model| + model.author_id = current_user.id end - before_create do - @model.author_id = current_user.id + before_save do |model| + user_is_member_of_group?(model) end - before_save do - user_is_member_of_group? - end + private - def user_is_member_of_group? - return true unless @model.group - return true if current_user.permission?(:update, @model) - return false if current_user.current_group_member?(@model.group) + def user_is_member_of_group?(model) + return true unless model.group + return true if current_user.permission?(:update, model) + return false if current_user.current_group_member?(model.group) raise AmberError::NotMemberOfGroupError end diff --git a/app/resources/v1/photo_comment_resource.rb b/app/resources/v1/photo_comment_resource.rb index 2bf1c3b2..ec6d5d4b 100644 --- a/app/resources/v1/photo_comment_resource.rb +++ b/app/resources/v1/photo_comment_resource.rb @@ -1,14 +1,14 @@ +# frozen_string_literal: true + class V1::PhotoCommentResource < V1::ApplicationResource - attributes :content + self.model = PhotoComment - has_one :photo, always_include_linkage_data: true - has_one :author, always_include_linkage_data: true + attribute :content, :string - before_save do - @model.author_id = current_user.id if @model.new_record? - end + has_one :photo + has_one :author, resource: V1::UserResource - def self.creatable_fields(_context) - %i[content photo] + before_save only: [:create] do |model| + model.author_id = current_user.id end end diff --git a/app/resources/v1/photo_resource.rb b/app/resources/v1/photo_resource.rb index 484695ed..bdf06ff7 100644 --- a/app/resources/v1/photo_resource.rb +++ b/app/resources/v1/photo_resource.rb @@ -1,30 +1,41 @@ +# frozen_string_literal: true + class V1::PhotoResource < V1::ApplicationResource - attributes :image_url, :image_thumb_url, :image_medium_url, :amount_of_comments, :amount_of_tags, - :exif_make, :exif_model, :exif_date_time_original, :exif_exposure_time, - :exif_aperture_value, :exif_iso_speed_ratings, :exif_copyright, :exif_lens_model, - :exif_focal_length + self.model = Photo - def image_thumb_url - @model.image.thumb.url + attribute :image_url, :string, writable: false + attribute :image_thumb_url, :string, writable: false do + @object.image.thumb.url end - - def image_medium_url - @model.image.medium.url + attribute :image_medium_url, :string, writable: false do + @object.image.medium.url end - - def amount_of_comments - @model.comments.size + attribute :amount_of_comments, :integer, writable: false do + @object.comments.size end + attribute :amount_of_tags, :integer, writable: false do + @object.tags.size + end + attribute :exif_make, :string, writable: false + attribute :exif_model, :string, writable: false + attribute :exif_date_time_original, :datetime, writable: false + attribute :exif_exposure_time, :string, writable: false + attribute :exif_aperture_value, :string, writable: false + attribute :exif_iso_speed_ratings, :integer, writable: false + attribute :exif_copyright, :string, writable: false + attribute :exif_lens_model, :string, writable: false + attribute :exif_focal_length, :string, writable: false - def amount_of_tags - @model.tags.size + filter :with_comments, :boolean do + eq { |scope, value| value ? scope.with_comments : scope } end - filter :with_comments, apply: ->(records, _value, _options) { records.with_comments } - filter :with_tags, apply: ->(records, _value, _options) { records.with_tags } + filter :with_tags, :boolean do + eq { |scope, value| value ? scope.with_tags : scope } + end - has_one :photo_album, always_include_linkage_data: true - has_one :uploader, always_include_linkage_data: true - has_many :comments - has_many :tags + has_one :photo_album + has_one :uploader, resource: V1::UserResource + has_many :comments, resource: V1::PhotoCommentResource + has_many :tags, resource: V1::PhotoTagResource end diff --git a/app/resources/v1/photo_tag_resource.rb b/app/resources/v1/photo_tag_resource.rb index a5d1abd1..993f3da0 100644 --- a/app/resources/v1/photo_tag_resource.rb +++ b/app/resources/v1/photo_tag_resource.rb @@ -1,16 +1,16 @@ +# frozen_string_literal: true + class V1::PhotoTagResource < V1::ApplicationResource - attributes :x - attributes :y + self.model = PhotoTag - has_one :photo, always_include_linkage_data: true - has_one :author, always_include_linkage_data: true - has_one :tagged_user, always_include_linkage_data: true + attribute :x, :float + attribute :y, :float - before_create do - @model.author_id = current_user.id - end + has_one :photo + has_one :author, resource: V1::UserResource + has_one :tagged_user, resource: V1::UserResource - def self.creatable_fields(_context) - %i[x y photo tagged_user] + before_save only: [:create] do |model| + model.author_id = current_user.id end end diff --git a/app/resources/v1/poll_resource.rb b/app/resources/v1/poll_resource.rb index a9a72cb0..4aecaddd 100644 --- a/app/resources/v1/poll_resource.rb +++ b/app/resources/v1/poll_resource.rb @@ -1,19 +1,18 @@ +# frozen_string_literal: true + class V1::PollResource < V1::ApplicationResource - has_one :form, always_include_linkage_data: true - has_one :author + self.model = Poll - before_create do - @model.author_id = current_user.id - end + has_one :form, resource: V1::Form::FormResource + has_one :author, resource: V1::UserResource - def self.creatable_fields(_context) - %i[form] + filter :search, :string do + eq do |scope, value| + scope.joins(form: :closed_questions).where('form_closed_questions.question ILIKE ?', "%#{value}%") + end end - def self.search(records, value) - return records if records == [] - - records.joins(form: :closed_questions).where('form_closed_questions.question ILIKE ?', - "%#{value.first}%") + before_save only: [:create] do |model| + model.author_id = current_user.id end end diff --git a/app/resources/v1/room_advert_resource.rb b/app/resources/v1/room_advert_resource.rb index 7532eee1..5bd532ab 100644 --- a/app/resources/v1/room_advert_resource.rb +++ b/app/resources/v1/room_advert_resource.rb @@ -1,31 +1,28 @@ +# frozen_string_literal: true + class V1::RoomAdvertResource < V1::ApplicationResource - attributes :house_name, :contact, :location, :available_from, - :description, :description_camofied, :author_name, - :cover_photo_url, :cover_photo, :publicly_visible + self.model = RoomAdvert - def cover_photo_url - @model.cover_photo.url + attribute :house_name, :string + attribute :contact, :string + attribute :location, :string + attribute :available_from, :date + attribute :description, :string + attribute :description_camofied, :string, writable: false do + camofy(@object['description']) end - - def description_camofied - camofy(@model['description']) + attribute :author_name, :string, writable: false do + @object.author.full_name end - - def author_name - @model.author.full_name + attribute :cover_photo_url, :string, writable: false do + @object.cover_photo.url end + attribute :cover_photo, :string, readable: false # Write-only + attribute :publicly_visible, :boolean - has_one :author, always_include_linkage_data: true - - def fetchable_fields - super - [:cover_photo] - end - - def self.creatable_fields(_context) - %i[house_name contact location available_from description cover_photo publicly_visible] - end + has_one :author, resource: V1::UserResource - before_create do - @model.author_id = current_user.id + before_save only: [:create] do |model| + model.author_id = current_user.id end end diff --git a/app/resources/v1/static_page_resource.rb b/app/resources/v1/static_page_resource.rb index 81d3e28d..c94a3a3b 100644 --- a/app/resources/v1/static_page_resource.rb +++ b/app/resources/v1/static_page_resource.rb @@ -1,29 +1,21 @@ -class V1::StaticPageResource < V1::ApplicationResource - attributes :title, :content, :content_camofied, :slug, :publicly_visible, :category - - def content_camofied - camofy(@model['content']) - end +# frozen_string_literal: true - def self.creatable_fields(_context) - %i[title content publicly_visible category] - end +class V1::StaticPageResource < V1::ApplicationResource + self.model = StaticPage - # Allow UUID and slugs as IDs - def self.verify_key(key, _context = nil) - key && String(key) + attribute :title, :string + attribute :content, :string + attribute :content_camofied, :string, writable: false do + camofy(@object['content']) end + attribute :slug, :string, writable: false + attribute :publicly_visible, :boolean + attribute :category, :string - def self.find_by_key(key, options = {}) - begin - record = records(options).friendly.find(key) - rescue ActiveRecord::RecordNotFound - raise JSONAPI::Exceptions::RecordNotFound.new(key) # rubocop:disable Style/RaiseArgs - end - self.new(record, options[:context]) # rubocop:disable Style/RedundantSelf - end + searchable_fields :title, :content - def self.searchable_fields - %i[title content] + # Support friendly_id slugs + def base_scope + super.friendly end end diff --git a/app/resources/v1/stored_mail_resource.rb b/app/resources/v1/stored_mail_resource.rb index 8a8e677d..bf484ae2 100644 --- a/app/resources/v1/stored_mail_resource.rb +++ b/app/resources/v1/stored_mail_resource.rb @@ -1,35 +1,36 @@ -class V1::StoredMailResource < V1::ApplicationResource - extend Forwardable - - attributes :received_at, :sender, :subject, :plain_body, :attachments +# frozen_string_literal: true - has_one :mail_alias, always_include_linkage_data: true +class V1::StoredMailResource < V1::ApplicationResource + self.model = StoredMail - def plain_body + attribute :received_at, :datetime, writable: false + attribute :sender, :string, writable: false + attribute :subject, :string, writable: false + attribute :plain_body, :string, writable: false do mail.text_part.body.decoded.force_encoding('ISO-8859-1').encode('UTF-8') end - - def attachments + attribute :attachments, :array, writable: false do # :nocov: mail.attachments.map do |attachment| file = StringIO.new(attachment.to_s) - { name: attachment.filename, size: file.size } end # :nocov: end - def self.records(options = {}) - if options[:context][:action] == 'index' - options[:includes] = - { inbound_email: { raw_email_attachment: :blob } } + has_one :mail_alias + + def base_scope + scope = super + if context&.dig(:action) == 'index' + scope = scope.includes(inbound_email: { raw_email_attachment: :blob }) end - super + scope end private def mail - @mail ||= @model.inbound_email.mail + @mail ||= @object.inbound_email.mail end end diff --git a/app/resources/v1/study_room_presence_resource.rb b/app/resources/v1/study_room_presence_resource.rb index c2e7d00d..a6ea28be 100644 --- a/app/resources/v1/study_room_presence_resource.rb +++ b/app/resources/v1/study_room_presence_resource.rb @@ -1,17 +1,27 @@ +# frozen_string_literal: true + class V1::StudyRoomPresenceResource < V1::ApplicationResource - attributes :start_time, :end_time, :status + self.model = StudyRoomPresence + + attribute :start_time, :datetime + attribute :end_time, :datetime + attribute :status, :string - has_one :user, always_include_linkage_data: true + has_one :user, resource: V1::UserResource - filter :current, apply: ->(records, _value, _options) { records.current } - filter :future, apply: ->(records, _value, _options) { records.future } - filter :current_and_future, apply: ->(records, _value, _options) { records.current_and_future } + filter :current, :boolean do + eq { |scope, value| value ? scope.current : scope } + end + + filter :future, :boolean do + eq { |scope, value| value ? scope.future : scope } + end - before_create do - @model.user_id = current_user.id + filter :current_and_future, :boolean do + eq { |scope, value| value ? scope.current_and_future : scope } end - def self.creatable_fields(_context) - %i[start_time end_time status] + before_save only: [:create] do |model| + model.user_id = current_user.id end end diff --git a/app/resources/v1/user_resource.rb b/app/resources/v1/user_resource.rb index cc5dc867..4904aeac 100644 --- a/app/resources/v1/user_resource.rb +++ b/app/resources/v1/user_resource.rb @@ -1,134 +1,204 @@ +# frozen_string_literal: true + class V1::UserResource < V1::ApplicationResource # rubocop:disable Metrics/ClassLength - attributes :username, :first_name, :last_name_prefix, :last_name, :full_name, :nickname, - :login_enabled, :otp_required, :activated_at, :emergency_contact, :emergency_number, - :ifes_data_sharing_preference, :info_in_almanak, :almanak_subscription_preference, - :digtus_subscription_preference, :email, :birthday, :address, :postcode, :city, - :phone_number, :food_preferences, :vegetarian, :study, :start_study, - :picture_publication_preference, :ical_secret_key, :ical_categories, - :password, :avatar, :avatar_url, :avatar_thumb_url, - :user_details_sharing_preference, :allow_sofia_sharing, :trailer_drivers_license, - :sidekiq_access, :setup_complete + self.model = User + + # Basic attributes (always visible) + attribute :username, :string, writable: false + attribute :first_name, :string do + writable do + user_can_create_or_update? + end + end + attribute :last_name_prefix, :string do + writable do + user_can_create_or_update? + end + end + attribute :last_name, :string do + writable do + user_can_create_or_update? + end + end + attribute :full_name, :string, writable: false + attribute :nickname, :string + + # Avatar attributes (always visible) + attribute :avatar_url, :string, writable: false do + @object.avatar.url + end + attribute :avatar_thumb_url, :string, writable: false do + @object.avatar.thumb.url + end + attribute :avatar, :string, readable: false # Write-only for uploads + + # Conditional attributes - only visible if update_or_me? + attribute :login_enabled, :boolean do + readable { update_or_me? } + writable { user_can_create_or_update? && !me? } + end + attribute :otp_required, :boolean do + readable { update_or_me? } + writable { me? } + end + attribute :activated_at, :datetime do + readable { update_or_me? } + writable false + end + attribute :emergency_contact, :string do + readable { update_or_me? } + end + attribute :emergency_number, :string do + readable { update_or_me? } + end + attribute :ifes_data_sharing_preference, :string do + readable { update_or_me? } + writable { me? } + end + attribute :info_in_almanak, :boolean do + readable { update_or_me? } + writable { me? } + end + attribute :almanak_subscription_preference, :string do + readable { update_or_me? } + end + attribute :digtus_subscription_preference, :string do + readable { update_or_me? } + end + attribute :user_details_sharing_preference, :string do + readable { update_or_me? } + writable { me? } + end + attribute :allow_sofia_sharing, :boolean do + readable { update_or_me? } + writable { me? } + end + attribute :sidekiq_access, :boolean do + readable { update_or_me? } + writable { me? } + end + attribute :setup_complete, :boolean do + readable { update_or_me? } + writable { me? } + end + + # Attributes visible if read_or_me? + attribute :picture_publication_preference, :string do + readable { read_or_me? } + writable { me? } + end + + # Attributes visible if read_user_details? (and not sofia) + attribute :email, :string do + readable { read_user_details_or_sofia? } + end + attribute :birthday, :date do + readable { read_user_details_or_sofia? } + writable { user_can_create_or_update? } + end + attribute :address, :string do + readable { read_user_details? && !application_is_sofia? } + end + attribute :postcode, :string do + readable { read_user_details? && !application_is_sofia? } + end + attribute :city, :string do + readable { read_user_details? && !application_is_sofia? } + end + attribute :phone_number, :string do + readable { read_user_details? && !application_is_sofia? } + end + attribute :food_preferences, :string do + readable { read_user_details? && !application_is_sofia? } + end + attribute :vegetarian, :boolean do + readable { read_user_details? && !application_is_sofia? } + end + attribute :study, :string do + readable { read_user_details? && !application_is_sofia? } + end + attribute :start_study, :integer do + readable { read_user_details? && !application_is_sofia? } + end + attribute :trailer_drivers_license, :boolean do + readable { read_user_details? && !application_is_sofia? } + end - def avatar_url - @model.avatar.url + # iCal attributes (only me) + attribute :ical_secret_key, :string do + readable { me? } + writable false + end + attribute :ical_categories, :array do + readable { me? } + writable { me? } end - def avatar_thumb_url - @model.avatar.thumb.url + # Password (write-only) + attribute :password, :string, readable: false do + writable { me? } end + # Relationships has_many :groups - has_many :active_groups + has_many :active_groups, resource: V1::GroupResource has_many :memberships has_many :mail_aliases - has_many :mandates, always_include_linkage_data: true - has_many :group_mail_aliases + has_many :mandates, resource: V1::Debit::MandateResource + has_many :group_mail_aliases, resource: V1::MailAliasResource has_many :permissions has_many :photos - has_many :user_permissions - - filter :upcoming_birthdays, apply: lambda { |records, _value, options| - context = options[:context] - upcoming_birthdays = records.upcoming_birthdays - records.find_each do |record| - context[:model] = record - upcoming_birthdays = upcoming_birthdays.where.not(id: record.id) unless read_user_details?(context) - end - upcoming_birthdays - } - filter :me, apply: lambda { |records, _value, options| - records.where(id: current_user_or_application(options)) - } - filter :group, apply: lambda { |records, value, _options| - records.active_users_for_group(Group.find_by(name: value)) - } - - # rubocop:disable all - def fetchable_fields - # Attributes - allowed_keys = %i[username first_name last_name_prefix last_name full_name nickname - avatar_url avatar_thumb_url created_at updated_at id] - # Relationships - allowed_keys += %i[groups active_groups memberships mail_aliases mandates - group_mail_aliases permissions photos user_permissions] - # Ical fields - allowed_keys += %i[ical_secret_key ical_categories] if me? - if update_or_me? - allowed_keys += %i[login_enabled otp_required activated_at emergency_contact - emergency_number ifes_data_sharing_preference info_in_almanak - almanak_subscription_preference digtus_subscription_preference - user_details_sharing_preference allow_sofia_sharing - sidekiq_access setup_complete] + has_many :user_permissions, resource: V1::PermissionsUsersResource + + # Filters + filter :upcoming_birthdays, :boolean do + eq do |scope, value| + next scope unless value + + upcoming_birthdays = scope.upcoming_birthdays + scope.find_each do |record| + unless read_user_details_for_record?(record) + upcoming_birthdays = upcoming_birthdays.where.not(id: record.id) + end + end + upcoming_birthdays end - allowed_keys += %i[picture_publication_preference] if read_or_me? - if read_user_details? && !application_is_sofia? - allowed_keys += %i[email birthday address postcode city phone_number food_preferences vegetarian - study start_study trailer_drivers_license] - end - allowed_keys += %i[email birthday] if application_is_sofia? && @model.allow_sofia_sharing - super && allowed_keys - end - # rubocop:enable all - - def self.creatable_fields(context) # rubocop:disable Metrics/MethodLength - attributes = %i[avatar nickname email address postcode city phone_number - food_preferences vegetarian study start_study - almanak_subscription_preference digtus_subscription_preference - emergency_contact emergency_number trailer_drivers_license] - if me?(context) - attributes += %i[otp_required password - user_details_sharing_preference allow_sofia_sharing - picture_publication_preference info_in_almanak - ifes_data_sharing_preference ical_categories sidekiq_access - setup_complete] - end - - if user_can_create_or_update?(context) - attributes += %i[first_name last_name_prefix last_name birthday - user_permissions] - attributes += [:login_enabled] unless me?(context) - end - attributes - end - - def self.searchable_fields - %i[email first_name last_name last_name_prefix nickname study] end - def self.records(options = {}) - options[:includes] = %i[mandates] if options[:context][:action] == 'index' - super - end + filter :me, :boolean do + eq do |scope, value| + next scope unless value - before_save do - if @model.new_record? - @model.activation_token = User.activation_token_hash[:activation_token] - @model.activation_token_valid_till = User.activation_token_hash[:activation_token_valid_till] - @model.username = @model.generate_username + scope.where(id: current_user_or_application&.id) end end - after_create do - UserMailer.account_creation_email(@model).deliver_later if @model.login_enabled + filter :group, :string do + eq do |scope, value| + scope.active_users_for_group(Group.find_by(name: value)) + end end - def self.update_permission?(context) - context[:user]&.permission?(:update, _model_class) - end + searchable_fields :email, :first_name, :last_name, :last_name_prefix, :nickname, :study - def self.read_permission?(context) - context[:user]&.permission?(:read, _model_class) + # Callbacks + before_save only: [:create] do |model| + model.activation_token = User.activation_token_hash[:activation_token] + model.activation_token_valid_till = User.activation_token_hash[:activation_token_valid_till] + model.username = model.generate_username end - def self.me?(context) - context[:model] == context[:user] + after_commit only: [:create] do |model| + UserMailer.account_creation_email(model).deliver_later if model.login_enabled end - def self.read_user_details?(context) - context[:model].user_details_sharing_preference == 'all_users' || - ((read_permission?(context) || me?(context)) && - context[:model].user_details_sharing_preference == 'members_only') || - me?(context) || update_permission?(context) + # Scope with eager loading for index + def base_scope + scope = super + if context&.dig(:action) == 'index' + scope = scope.includes(:mandates) + end + scope end private @@ -138,15 +208,29 @@ def read_or_me? end def read_user_details? - current_user && (@model.user_details_sharing_preference == 'all_users' || - (read_or_me? && @model.user_details_sharing_preference == 'members_only') || - me? || update_or_me?) + return false unless current_user + + @object.user_details_sharing_preference == 'all_users' || + (read_or_me? && @object.user_details_sharing_preference == 'members_only') || + me? || update_or_me? + end + + def read_user_details_or_sofia? + read_user_details? || + (application_is_sofia? && @object.allow_sofia_sharing) + end + + def read_user_details_for_record?(record) + record.user_details_sharing_preference == 'all_users' || + ((self.class.read_permission?(context) || record == current_user) && + record.user_details_sharing_preference == 'members_only') || + record == current_user || self.class.update_permission?(context) end def application_is_sofia? - return false unless context.key?(:application) && context.fetch(:application) + return false unless context&.key?(:application) && context[:application] - context.fetch(:application).scopes.to_a.include?('sofia') + context[:application].scopes.to_a.include?('sofia') end def update_or_me? @@ -154,6 +238,10 @@ def update_or_me? end def me? - @model == current_user + @object == current_user + end + + def user_can_create_or_update? + self.class.user_can_create_or_update?(context) end end diff --git a/app/resources/v1/vacancy_resource.rb b/app/resources/v1/vacancy_resource.rb index 1feae4be..e808aea0 100644 --- a/app/resources/v1/vacancy_resource.rb +++ b/app/resources/v1/vacancy_resource.rb @@ -1,47 +1,45 @@ +# frozen_string_literal: true + class V1::VacancyResource < V1::ApplicationResource - attributes :title, :description, :description_camofied, :workload, :workload_peak, - :contact, :deadline, :author_name, :avatar_thumb_url, :cover_photo_url, :cover_photo + self.model = Vacancy - def cover_photo_url - @model.cover_photo.url + attribute :title, :string + attribute :description, :string + attribute :description_camofied, :string, writable: false do + camofy(@object['description']) end - - def description_camofied - camofy(@model['description']) + attribute :workload, :string + attribute :workload_peak, :string + attribute :contact, :string + attribute :deadline, :date + attribute :author_name, :string, writable: false do + @object.group ? @object.group.name : @object.author.full_name end - - def author_name - @model.group ? @model.group.name : @model.author.full_name + attribute :avatar_thumb_url, :string, writable: false do + @object.group ? @object.group.avatar.thumb.url : @object.author.avatar.thumb.url end - - def avatar_thumb_url - @model.group ? @model.group.avatar.thumb.url : @model.author.avatar.thumb.url + attribute :cover_photo_url, :string, writable: false do + @object.cover_photo.url end + attribute :cover_photo, :string, readable: false # Write-only - has_one :group, always_include_linkage_data: true - has_one :author, always_include_linkage_data: true + has_one :group + has_one :author, resource: V1::UserResource - def fetchable_fields - super - [:cover_photo] + before_save only: [:create] do |model| + model.author_id = current_user.id end - def self.creatable_fields(_context) - %i[title description group workload workload_peak contact - deadline cover_photo] + before_save do |model| + user_is_member_of_group?(model) end - before_create do - @model.author_id = current_user.id - end - - before_save do - user_is_member_of_group? - end + private - def user_is_member_of_group? - return true unless @model.group - return true if current_user.permission?(:update, @model) - return false if current_user.current_group_member?(@model.group) + def user_is_member_of_group?(model) + return true unless model.group + return true if current_user.permission?(:update, model) + return false if current_user.current_group_member?(model.group) raise AmberError::NotMemberOfGroupError end diff --git a/config/initializers/graphiti.rb b/config/initializers/graphiti.rb new file mode 100644 index 00000000..095f7271 --- /dev/null +++ b/config/initializers/graphiti.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +Graphiti.configure do |config| + config.pagination_links = true +end + +Rails.application.config.after_initialize do + # Ensure resources are loaded in development + Rails.application.eager_load! if Rails.env.development? +end diff --git a/config/initializers/jsonapi_resources.rb b/config/initializers/jsonapi_resources.rb deleted file mode 100644 index 6f5acb1a..00000000 --- a/config/initializers/jsonapi_resources.rb +++ /dev/null @@ -1,20 +0,0 @@ -JSONAPI.configure do |config| - config.default_processor_klass = JSONAPI::Authorization::AuthorizingProcessor - - # By default an exception will return a 500, when you want to handle the error in the controller - # you have to specifiy it here - # We whitelist all such that sentry can pick them up - config.whitelist_all_exceptions = true - - # Do not raise an error when unpermitted paramaters are passed. - # Instead add warnings in the metadata - config.raise_if_parameters_not_allowed = false - - config.json_key_format = :underscored_key - config.route_format = :underscored_route - - # Pagination - config.top_level_meta_include_page_count = true - config.default_paginator = :default_off - config.maximum_page_size = 50 -end diff --git a/config/routes.rb b/config/routes.rb index 3dc11a66..5e6e183c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -9,53 +9,48 @@ end namespace :v1 do - jsonapi_resources :activities do - jsonapi_relationships + resources :activities do member do post :generate_alias end end - jsonapi_resources :articles - jsonapi_resources :article_comments - jsonapi_resources :board_room_presences - jsonapi_resources :study_room_presences - jsonapi_resources :books do + resources :articles + resources :article_comments + resources :board_room_presences + resources :study_room_presences + resources :books do collection do get :isbn_lookup end end - jsonapi_resources :groups do - jsonapi_relationships + resources :groups do member do get :export end end - jsonapi_resources :mail_aliases - jsonapi_resources :memberships - jsonapi_resources :permissions, only: %i[index show] - jsonapi_resources :photo_albums do - jsonapi_relationships + resources :mail_aliases + resources :memberships + resources :permissions, only: %i[index show] + resources :photo_albums do member do post :dropzone get :zip end end - jsonapi_resources :photo_comments - jsonapi_resources :photo_tags - jsonapi_resources :photos, only: %i[index show destroy] - jsonapi_resources :polls - jsonapi_resources :room_adverts - jsonapi_resources :static_pages - jsonapi_resources :stored_mails, only: %i[index show destroy] do - jsonapi_relationships + resources :photo_comments + resources :photo_tags + resources :photos, only: %i[index show destroy] + resources :polls + resources :room_adverts + resources :static_pages + resources :stored_mails, only: %i[index show destroy] do member do post :accept post :reject end end resources :daily_verses, only: [:index] - jsonapi_resources :users, only: %i[index show create update] do - jsonapi_relationships + resources :users, only: %i[index show create update] do collection do post :reset_password post :batch_import @@ -70,34 +65,32 @@ end get 'users/me/nextcloud', to: 'users#nextcloud' - jsonapi_resources :vacancies + resources :vacancies namespace :debit do - jsonapi_resources :collections do - jsonapi_relationships + resources :collections do member do get :sepa end end - jsonapi_resources :transactions - jsonapi_resources :mandates, only: %i[index show create update] + resources :transactions + resources :mandates, only: %i[index show create update] end namespace :form do - jsonapi_resources :closed_questions - jsonapi_resources :closed_question_answers - jsonapi_resources :closed_question_options - jsonapi_resources :forms - jsonapi_resources :responses - jsonapi_resources :open_questions - jsonapi_resources :open_question_answers + resources :closed_questions + resources :closed_question_answers + resources :closed_question_options + resources :forms + resources :responses + resources :open_questions + resources :open_question_answers end namespace :forum do - jsonapi_resources :categories - jsonapi_resources :posts - jsonapi_resources :threads do - jsonapi_relationships + resources :categories + resources :posts + resources :threads do member do post :mark_read end From 16cc1f48b1ed1cc770849c5007847d547ecef6ee Mon Sep 17 00:00:00 2001 From: Lodewiges Date: Wed, 17 Dec 2025 10:27:16 +0100 Subject: [PATCH 2/6] make it so the aplication can boot --- GRAPHITI_MIGRATION.md | 21 ++++----- app/controllers/application_controller.rb | 6 --- app/controllers/concerns/graphiti_crud.rb | 12 ++--- app/controllers/v1/users_controller.rb | 12 ++--- app/paginators/default_off_paginator.rb | 22 --------- .../form/closed_question_answer_processor.rb | 47 ------------------- .../v1/form/open_question_answer_processor.rb | 38 --------------- app/processors/v1/form/response_processor.rb | 35 -------------- app/resources/v1/application_resource.rb | 41 +++++++--------- 9 files changed, 39 insertions(+), 195 deletions(-) delete mode 100644 app/paginators/default_off_paginator.rb delete mode 100644 app/processors/v1/form/closed_question_answer_processor.rb delete mode 100644 app/processors/v1/form/open_question_answer_processor.rb delete mode 100644 app/processors/v1/form/response_processor.rb diff --git a/GRAPHITI_MIGRATION.md b/GRAPHITI_MIGRATION.md index 4d35a09c..e61ec8ea 100644 --- a/GRAPHITI_MIGRATION.md +++ b/GRAPHITI_MIGRATION.md @@ -215,15 +215,12 @@ class ApplicationController < JSONAPI::ResourceController end ``` -**After:** +**After (API-only Rails apps):** ```ruby class ApplicationController < ActionController::API include Pundit::Authorization - include Graphiti::Rails - include Graphiti::Responders - - register_exception Graphiti::Errors::RecordNotFound, status: 404 - register_exception Graphiti::Errors::InvalidRequest, status: 400 + # Note: For API-only apps, do NOT include Graphiti::Rails or Graphiti::Responders + # Use .to_jsonapi method directly for rendering end ``` @@ -248,29 +245,29 @@ module GraphitiCrud def index resources = resource_class.all(params, context) - render jsonapi: resources + render json: resources.to_jsonapi end def show resource = resource_class.find(params, context) - render jsonapi: resource + render json: resource.to_jsonapi end def create resource = resource_class.build(params, context) if resource.save - render jsonapi: resource, status: :created + render json: resource.to_jsonapi, status: :created else - render jsonapi_errors: resource + render json: resource.errors.to_jsonapi, status: :unprocessable_entity end end def update resource = resource_class.find(params, context) if resource.update_attributes - render jsonapi: resource + render json: resource.to_jsonapi else - render jsonapi_errors: resource + render json: resource.errors.to_jsonapi, status: :unprocessable_entity end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 52ca7179..5cd69072 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -2,12 +2,6 @@ class ApplicationController < ActionController::API include Pundit::Authorization - include Graphiti::Rails - include Graphiti::Responders - - # Register JSON:API MIME type - register_exception Graphiti::Errors::RecordNotFound, status: 404 - register_exception Graphiti::Errors::InvalidRequest, status: 400 before_action :set_paper_trail_whodunnit before_action :set_sentry_context diff --git a/app/controllers/concerns/graphiti_crud.rb b/app/controllers/concerns/graphiti_crud.rb index ff67a77e..48f857de 100644 --- a/app/controllers/concerns/graphiti_crud.rb +++ b/app/controllers/concerns/graphiti_crud.rb @@ -17,21 +17,21 @@ def graphiti_resource(klass) def index resources = resource_class.all(params, context) - render jsonapi: resources + render json: resources.to_jsonapi end def show resource = resource_class.find(params, context) - render jsonapi: resource + render json: resource.to_jsonapi end def create resource = resource_class.build(params, context) if resource.save - render jsonapi: resource, status: :created + render json: resource.to_jsonapi, status: :created else - render jsonapi_errors: resource + render json: resource.errors.to_jsonapi, status: :unprocessable_entity end end @@ -39,9 +39,9 @@ def update resource = resource_class.find(params, context) if resource.update_attributes - render jsonapi: resource + render json: resource.to_jsonapi else - render jsonapi_errors: resource + render json: resource.errors.to_jsonapi, status: :unprocessable_entity end end diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/v1/users_controller.rb index a47613a6..638d31fc 100644 --- a/app/controllers/v1/users_controller.rb +++ b/app/controllers/v1/users_controller.rb @@ -9,20 +9,20 @@ class V1::UsersController < V1::ApplicationController # rubocop:disable Metrics/ # Graphiti CRUD actions def index users = V1::UserResource.all(params, context) - render jsonapi: users + render json: users.to_jsonapi end def show user = V1::UserResource.find(params, context) - render jsonapi: user + render json: user.to_jsonapi end def create user = V1::UserResource.build(params, context) if user.save - render jsonapi: user, status: :created + render json: user.to_jsonapi, status: :created else - render jsonapi_errors: user + render json: user.errors.to_jsonapi, status: :unprocessable_entity end end @@ -35,9 +35,9 @@ def update remove_password_from_params_when_blank? user = V1::UserResource.find(params, context) if user.update_attributes - render jsonapi: user + render json: user.to_jsonapi else - render jsonapi_errors: user + render json: user.errors.to_jsonapi, status: :unprocessable_entity end end end diff --git a/app/paginators/default_off_paginator.rb b/app/paginators/default_off_paginator.rb deleted file mode 100644 index aaa99ba5..00000000 --- a/app/paginators/default_off_paginator.rb +++ /dev/null @@ -1,22 +0,0 @@ -class DefaultOffPaginator < PagedPaginator - # This extension of the PagedPaginator makes sure that pagination - # is only applied when requested by the client - attr_reader :turned_on - - def apply(relation, _order_options) - return relation unless @turned_on - - super - end - - def parse_pagination_params(params) - @turned_on = params.present? - super - end - - def links_page_params(options = {}) - return {} unless @turned_on - - super - end -end diff --git a/app/processors/v1/form/closed_question_answer_processor.rb b/app/processors/v1/form/closed_question_answer_processor.rb deleted file mode 100644 index b5c9c554..00000000 --- a/app/processors/v1/form/closed_question_answer_processor.rb +++ /dev/null @@ -1,47 +0,0 @@ -class V1::Form::ClosedQuestionAnswerProcessor < JSONAPI::Authorization::AuthorizingProcessor - define_jsonapi_resources_callbacks :recreate_resource - set_callback :recreate_resource, :before, :authorize_create_resource - - def process - return super unless operation_type == :create_resource - - model = Form::ClosedQuestionAnswer.only_deleted.find_by(model_fetch_keys) - return super unless model - - # Model you try to create already exists but is deleted - # To prevent primary key errors we first restore the old object - # and then update that record to match the new state - model.restore - params[:resource_id] = model.id - run_callbacks :recreate_resource do - return recreate_resource - end - end - - def model_fetch_keys # rubocop:disable Metrics/AbcSize - if params[:data][:to_one][:option] - question = Form::ClosedQuestionOption.find(params[:data][:to_one][:option]).question - - if question.radio_question? - # radio question answers are unique per question, - # thus when re-answering a radio question, the belonging answer must be restored - return { response_id: params[:data][:to_one][:response], question_id: question.id } - end - end - - # checkbox question answers are not unique per question, - # thus when giving a destroyed answer, that must be restored - { response_id: params[:data][:to_one][:response], option_id: params[:data][:to_one][:option] } - end - - def recreate_resource - resource_id = params[:resource_id] - data = params[:data] - - resource = resource_klass.find_by_key(resource_id, context:) - - result = resource.replace_fields(data) - - JSONAPI::ResourceOperationResult.new(result == :completed ? :created : :accepted, resource) - end -end diff --git a/app/processors/v1/form/open_question_answer_processor.rb b/app/processors/v1/form/open_question_answer_processor.rb deleted file mode 100644 index d21355aa..00000000 --- a/app/processors/v1/form/open_question_answer_processor.rb +++ /dev/null @@ -1,38 +0,0 @@ -class V1::Form::OpenQuestionAnswerProcessor < JSONAPI::Authorization::AuthorizingProcessor - define_jsonapi_resources_callbacks :recreate_resource - set_callback :recreate_resource, :before, :authorize_create_resource - - def process - return super unless operation_type == :create_resource - - model = Form::OpenQuestionAnswer.only_deleted.find_by(model_fetch_keys) - return super unless model - - # Model you try to create already exists but is deleted - # To prevent primary key errors we first restore the old object - # and then update that record to match the new state - model.restore - params[:resource_id] = model.id - run_callbacks :recreate_resource do - return recreate_resource - end - end - - def model_fetch_keys - { response_id: params[:data][:to_one][:response], - question_id: params[:data][:to_one][:question] } - end - - def recreate_resource - # Code is duplicate of replace_fields, - # this is needed because another authorization callback is needed - resource_id = params[:resource_id] - data = params[:data] - - resource = resource_klass.find_by_key(resource_id, context:) - - result = resource.replace_fields(data) - - JSONAPI::ResourceOperationResult.new(result == :completed ? :created : :accepted, resource) - end -end diff --git a/app/processors/v1/form/response_processor.rb b/app/processors/v1/form/response_processor.rb deleted file mode 100644 index ec0b7020..00000000 --- a/app/processors/v1/form/response_processor.rb +++ /dev/null @@ -1,35 +0,0 @@ -class V1::Form::ResponseProcessor < JSONAPI::Authorization::AuthorizingProcessor - define_jsonapi_resources_callbacks :recreate_resource - set_callback :recreate_resource, :before, :authorize_create_resource - - def process - return super unless operation_type == :create_resource - - model = Form::Response.only_deleted.find_by(model_fetch_keys) - return super unless model - - # Model you try to create already exists but is deleted - # To prevent primary key errors we first restore the old object - # and then update that record to match the new state - model.restore - params[:resource_id] = model.id - run_callbacks :recreate_resource do - return recreate_resource - end - end - - def model_fetch_keys - { user: context[:user], form: params[:data][:to_one][:form] } - end - - def recreate_resource - resource_id = params[:resource_id] - data = params[:data] - - resource = resource_klass.find_by_key(resource_id, context:) - - result = resource.replace_fields(data) - - JSONAPI::ResourceOperationResult.new(result == :completed ? :created : :accepted, resource) - end -end diff --git a/app/resources/v1/application_resource.rb b/app/resources/v1/application_resource.rb index fb0edc20..f545d578 100644 --- a/app/resources/v1/application_resource.rb +++ b/app/resources/v1/application_resource.rb @@ -7,32 +7,27 @@ class V1::ApplicationResource < Graphiti::Resource self.adapter = Graphiti::Adapters::ActiveRecord self.abstract_class = true - # Default attributes for all resources - attribute :created_at, :datetime, writable: false - attribute :updated_at, :datetime, writable: false - - # Search filter - override searchable_fields in child resources - filter :search, :string do - eq do |scope, value| - searchable = self.class.config[:searchable_fields] || [] - return scope if searchable.empty? - - arel = scope.model.arel_table - value.to_s.split.each do |word| - conditions = searchable.map { |field| arel[field].lower.matches("%#{word.downcase}%") }.inject(:or) - scope = scope.where(conditions) - end - scope - end - end - - # Class method to define searchable fields + # Class method to register searchable fields and automatically add search filter def self.searchable_fields(*fields) if fields.any? - config[:searchable_fields] = fields - else - config[:searchable_fields] || [] + @searchable_fields = fields + + # Define the search filter for this resource + filter :search, :string do + eq do |scope, value| + searchable = self.class.instance_variable_get(:@searchable_fields) || [] + next scope if searchable.empty? + + arel = scope.model.arel_table + value.to_s.split.each do |word| + conditions = searchable.map { |field| arel[field].lower.matches("%#{word.downcase}%") }.inject(:or) + scope = scope.where(conditions) + end + scope + end + end end + @searchable_fields || [] end # Apply Pundit scoping on index From 38045f0372b5e1975c34adb63719919fc5389275 Mon Sep 17 00:00:00 2001 From: Lodewiges Date: Wed, 17 Dec 2025 10:37:43 +0100 Subject: [PATCH 3/6] add timestamps to all files --- app/resources/v1/activity_resource.rb | 2 ++ app/resources/v1/application_resource.rb | 6 ++++++ app/resources/v1/article_comment_resource.rb | 2 ++ app/resources/v1/article_resource.rb | 2 ++ app/resources/v1/board_room_presence_resource.rb | 2 ++ app/resources/v1/book_resource.rb | 2 ++ app/resources/v1/debit/collection_resource.rb | 2 ++ app/resources/v1/debit/mandate_resource.rb | 2 ++ app/resources/v1/debit/transaction_resource.rb | 2 ++ app/resources/v1/form/closed_question_answer_resource.rb | 2 ++ app/resources/v1/form/closed_question_option_resource.rb | 2 ++ app/resources/v1/form/closed_question_resource.rb | 2 ++ app/resources/v1/form/form_resource.rb | 2 ++ app/resources/v1/form/open_question_answer_resource.rb | 2 ++ app/resources/v1/form/open_question_resource.rb | 2 ++ app/resources/v1/form/response_resource.rb | 2 ++ app/resources/v1/forum/category_resource.rb | 2 ++ app/resources/v1/forum/post_resource.rb | 2 ++ app/resources/v1/forum/thread_resource.rb | 2 ++ app/resources/v1/group_resource.rb | 2 ++ app/resources/v1/groups_permissions_resource.rb | 2 ++ app/resources/v1/mail_alias_resource.rb | 2 ++ app/resources/v1/membership_resource.rb | 2 ++ app/resources/v1/permission_resource.rb | 2 ++ app/resources/v1/permissions_users_resource.rb | 2 ++ app/resources/v1/photo_album_resource.rb | 2 ++ app/resources/v1/photo_comment_resource.rb | 2 ++ app/resources/v1/photo_resource.rb | 2 ++ app/resources/v1/photo_tag_resource.rb | 2 ++ app/resources/v1/poll_resource.rb | 2 ++ app/resources/v1/room_advert_resource.rb | 2 ++ app/resources/v1/static_page_resource.rb | 2 ++ app/resources/v1/stored_mail_resource.rb | 2 ++ app/resources/v1/study_room_presence_resource.rb | 2 ++ app/resources/v1/user_resource.rb | 2 ++ app/resources/v1/vacancy_resource.rb | 2 ++ 36 files changed, 76 insertions(+) diff --git a/app/resources/v1/activity_resource.rb b/app/resources/v1/activity_resource.rb index b558699e..30ecd4f3 100644 --- a/app/resources/v1/activity_resource.rb +++ b/app/resources/v1/activity_resource.rb @@ -3,6 +3,8 @@ class V1::ActivityResource < V1::ApplicationResource self.model = Activity + with_timestamps + attribute :title, :string attribute :description, :string attribute :description_camofied, :string, writable: false do diff --git a/app/resources/v1/application_resource.rb b/app/resources/v1/application_resource.rb index f545d578..240821a4 100644 --- a/app/resources/v1/application_resource.rb +++ b/app/resources/v1/application_resource.rb @@ -7,6 +7,12 @@ class V1::ApplicationResource < Graphiti::Resource self.adapter = Graphiti::Adapters::ActiveRecord self.abstract_class = true + # Class method to add standard timestamps (call this in child resources) + def self.with_timestamps + attribute :created_at, :datetime, writable: false + attribute :updated_at, :datetime, writable: false + end + # Class method to register searchable fields and automatically add search filter def self.searchable_fields(*fields) if fields.any? diff --git a/app/resources/v1/article_comment_resource.rb b/app/resources/v1/article_comment_resource.rb index 8cb1fe09..b0c3536b 100644 --- a/app/resources/v1/article_comment_resource.rb +++ b/app/resources/v1/article_comment_resource.rb @@ -3,6 +3,8 @@ class V1::ArticleCommentResource < V1::ApplicationResource self.model = ArticleComment + with_timestamps + attribute :content, :string has_one :article diff --git a/app/resources/v1/article_resource.rb b/app/resources/v1/article_resource.rb index 15d7422e..6d3c22ad 100644 --- a/app/resources/v1/article_resource.rb +++ b/app/resources/v1/article_resource.rb @@ -3,6 +3,8 @@ class V1::ArticleResource < V1::ApplicationResource self.model = Article + with_timestamps + attribute :title, :string attribute :content, :string attribute :publicly_visible, :boolean diff --git a/app/resources/v1/board_room_presence_resource.rb b/app/resources/v1/board_room_presence_resource.rb index 96c16719..ef55a62f 100644 --- a/app/resources/v1/board_room_presence_resource.rb +++ b/app/resources/v1/board_room_presence_resource.rb @@ -3,6 +3,8 @@ class V1::BoardRoomPresenceResource < V1::ApplicationResource self.model = BoardRoomPresence + with_timestamps + attribute :start_time, :datetime attribute :end_time, :datetime attribute :status, :string diff --git a/app/resources/v1/book_resource.rb b/app/resources/v1/book_resource.rb index 10350764..ba792824 100644 --- a/app/resources/v1/book_resource.rb +++ b/app/resources/v1/book_resource.rb @@ -3,6 +3,8 @@ class V1::BookResource < V1::ApplicationResource self.model = Book + with_timestamps + attribute :title, :string attribute :author, :string attribute :description, :string diff --git a/app/resources/v1/debit/collection_resource.rb b/app/resources/v1/debit/collection_resource.rb index 9b297c35..485bdd14 100644 --- a/app/resources/v1/debit/collection_resource.rb +++ b/app/resources/v1/debit/collection_resource.rb @@ -3,6 +3,8 @@ class V1::Debit::CollectionResource < V1::ApplicationResource self.model = Debit::Collection + with_timestamps + attribute :name, :string attribute :date, :date attribute :import_file, :string, readable: false # Write-only diff --git a/app/resources/v1/debit/mandate_resource.rb b/app/resources/v1/debit/mandate_resource.rb index dc0b483b..ec5f22ee 100644 --- a/app/resources/v1/debit/mandate_resource.rb +++ b/app/resources/v1/debit/mandate_resource.rb @@ -3,6 +3,8 @@ class V1::Debit::MandateResource < V1::ApplicationResource self.model = Debit::Mandate + with_timestamps + attribute :start_date, :date attribute :end_date, :date attribute :iban, :string diff --git a/app/resources/v1/debit/transaction_resource.rb b/app/resources/v1/debit/transaction_resource.rb index 177997f9..2a4071eb 100644 --- a/app/resources/v1/debit/transaction_resource.rb +++ b/app/resources/v1/debit/transaction_resource.rb @@ -3,6 +3,8 @@ class V1::Debit::TransactionResource < V1::ApplicationResource self.model = Debit::Transaction + with_timestamps + attribute :description, :string attribute :amount, :float diff --git a/app/resources/v1/form/closed_question_answer_resource.rb b/app/resources/v1/form/closed_question_answer_resource.rb index d5a9a0d7..3a400ed9 100644 --- a/app/resources/v1/form/closed_question_answer_resource.rb +++ b/app/resources/v1/form/closed_question_answer_resource.rb @@ -3,6 +3,8 @@ class V1::Form::ClosedQuestionAnswerResource < V1::ApplicationResource self.model = Form::ClosedQuestionAnswer + with_timestamps + has_one :response, resource: V1::Form::ResponseResource has_one :option, resource: V1::Form::ClosedQuestionOptionResource end diff --git a/app/resources/v1/form/closed_question_option_resource.rb b/app/resources/v1/form/closed_question_option_resource.rb index f1a5caa2..ed7bcf8b 100644 --- a/app/resources/v1/form/closed_question_option_resource.rb +++ b/app/resources/v1/form/closed_question_option_resource.rb @@ -3,6 +3,8 @@ class V1::Form::ClosedQuestionOptionResource < V1::ApplicationResource self.model = Form::ClosedQuestionOption + with_timestamps + attribute :option, :string attribute :position, :integer diff --git a/app/resources/v1/form/closed_question_resource.rb b/app/resources/v1/form/closed_question_resource.rb index 1bac72ad..754b22b9 100644 --- a/app/resources/v1/form/closed_question_resource.rb +++ b/app/resources/v1/form/closed_question_resource.rb @@ -3,6 +3,8 @@ class V1::Form::ClosedQuestionResource < V1::ApplicationResource self.model = Form::ClosedQuestion + with_timestamps + attribute :question, :string attribute :field_type, :string attribute :required, :boolean diff --git a/app/resources/v1/form/form_resource.rb b/app/resources/v1/form/form_resource.rb index 940b07c6..98fea4e7 100644 --- a/app/resources/v1/form/form_resource.rb +++ b/app/resources/v1/form/form_resource.rb @@ -3,6 +3,8 @@ class V1::Form::FormResource < V1::ApplicationResource self.model = Form::Form + with_timestamps + attribute :respond_from, :datetime attribute :respond_until, :datetime attribute :amount_of_responses, :integer, writable: false do diff --git a/app/resources/v1/form/open_question_answer_resource.rb b/app/resources/v1/form/open_question_answer_resource.rb index a65bad06..76e5a40a 100644 --- a/app/resources/v1/form/open_question_answer_resource.rb +++ b/app/resources/v1/form/open_question_answer_resource.rb @@ -3,6 +3,8 @@ class V1::Form::OpenQuestionAnswerResource < V1::ApplicationResource self.model = Form::OpenQuestionAnswer + with_timestamps + attribute :answer, :string has_one :response, resource: V1::Form::ResponseResource diff --git a/app/resources/v1/form/open_question_resource.rb b/app/resources/v1/form/open_question_resource.rb index bc4fd828..080e8750 100644 --- a/app/resources/v1/form/open_question_resource.rb +++ b/app/resources/v1/form/open_question_resource.rb @@ -3,6 +3,8 @@ class V1::Form::OpenQuestionResource < V1::ApplicationResource self.model = Form::OpenQuestion + with_timestamps + attribute :question, :string attribute :field_type, :string attribute :required, :boolean diff --git a/app/resources/v1/form/response_resource.rb b/app/resources/v1/form/response_resource.rb index 7620493e..5eb87aed 100644 --- a/app/resources/v1/form/response_resource.rb +++ b/app/resources/v1/form/response_resource.rb @@ -3,6 +3,8 @@ class V1::Form::ResponseResource < V1::ApplicationResource self.model = Form::Response + with_timestamps + attribute :completed, :boolean has_one :user, resource: V1::UserResource diff --git a/app/resources/v1/forum/category_resource.rb b/app/resources/v1/forum/category_resource.rb index 3482618e..b609d5ab 100644 --- a/app/resources/v1/forum/category_resource.rb +++ b/app/resources/v1/forum/category_resource.rb @@ -3,6 +3,8 @@ class V1::Forum::CategoryResource < V1::ApplicationResource self.model = Forum::Category + with_timestamps + attribute :name, :string attribute :amount_of_threads, :integer, writable: false do @object.threads.size diff --git a/app/resources/v1/forum/post_resource.rb b/app/resources/v1/forum/post_resource.rb index 38f5052f..505e4bb6 100644 --- a/app/resources/v1/forum/post_resource.rb +++ b/app/resources/v1/forum/post_resource.rb @@ -3,6 +3,8 @@ class V1::Forum::PostResource < V1::ApplicationResource self.model = Forum::Post + with_timestamps + attribute :message, :string attribute :message_camofied, :string, writable: false do camofy(@object['message']) diff --git a/app/resources/v1/forum/thread_resource.rb b/app/resources/v1/forum/thread_resource.rb index 3ca5d129..5577e92a 100644 --- a/app/resources/v1/forum/thread_resource.rb +++ b/app/resources/v1/forum/thread_resource.rb @@ -3,6 +3,8 @@ class V1::Forum::ThreadResource < V1::ApplicationResource self.model = Forum::Thread + with_timestamps + attribute :title, :string attribute :closed_at, :datetime do writable { self.class.user_can_create_or_update?(context) } diff --git a/app/resources/v1/group_resource.rb b/app/resources/v1/group_resource.rb index 411bd988..1216914d 100644 --- a/app/resources/v1/group_resource.rb +++ b/app/resources/v1/group_resource.rb @@ -3,6 +3,8 @@ class V1::GroupResource < V1::ApplicationResource self.model = Group + with_timestamps + attribute :name, :string do writable { user_can_create_or_update? } end diff --git a/app/resources/v1/groups_permissions_resource.rb b/app/resources/v1/groups_permissions_resource.rb index 95949d18..a999738b 100644 --- a/app/resources/v1/groups_permissions_resource.rb +++ b/app/resources/v1/groups_permissions_resource.rb @@ -3,6 +3,8 @@ class V1::GroupsPermissionsResource < V1::ApplicationResource self.model = GroupsPermissions + with_timestamps + has_one :permission has_one :group end diff --git a/app/resources/v1/mail_alias_resource.rb b/app/resources/v1/mail_alias_resource.rb index 830d45fb..e61d3e06 100644 --- a/app/resources/v1/mail_alias_resource.rb +++ b/app/resources/v1/mail_alias_resource.rb @@ -3,6 +3,8 @@ class V1::MailAliasResource < V1::ApplicationResource self.model = MailAlias + with_timestamps + attribute :email, :string attribute :moderation_type, :string attribute :description, :string diff --git a/app/resources/v1/membership_resource.rb b/app/resources/v1/membership_resource.rb index b95f8d35..9c31d6cd 100644 --- a/app/resources/v1/membership_resource.rb +++ b/app/resources/v1/membership_resource.rb @@ -3,6 +3,8 @@ class V1::MembershipResource < V1::ApplicationResource self.model = Membership + with_timestamps + attribute :start_date, :date attribute :end_date, :date attribute :function, :string diff --git a/app/resources/v1/permission_resource.rb b/app/resources/v1/permission_resource.rb index 34980f7b..49b9aa93 100644 --- a/app/resources/v1/permission_resource.rb +++ b/app/resources/v1/permission_resource.rb @@ -5,6 +5,8 @@ class V1::PermissionResource < V1::ApplicationResource self.model = Permission + with_timestamps + attribute :name, :string, writable: false do CaseTransform.dash(@object.name) end diff --git a/app/resources/v1/permissions_users_resource.rb b/app/resources/v1/permissions_users_resource.rb index e2d17715..ede66a30 100644 --- a/app/resources/v1/permissions_users_resource.rb +++ b/app/resources/v1/permissions_users_resource.rb @@ -3,6 +3,8 @@ class V1::PermissionsUsersResource < V1::ApplicationResource self.model = PermissionsUsers + with_timestamps + has_one :permission has_one :user, resource: V1::UserResource end diff --git a/app/resources/v1/photo_album_resource.rb b/app/resources/v1/photo_album_resource.rb index 9213a4f0..77401199 100644 --- a/app/resources/v1/photo_album_resource.rb +++ b/app/resources/v1/photo_album_resource.rb @@ -3,6 +3,8 @@ class V1::PhotoAlbumResource < V1::ApplicationResource self.model = PhotoAlbum + with_timestamps + attribute :title, :string attribute :date, :date attribute :publicly_visible, :boolean diff --git a/app/resources/v1/photo_comment_resource.rb b/app/resources/v1/photo_comment_resource.rb index ec6d5d4b..dc722a27 100644 --- a/app/resources/v1/photo_comment_resource.rb +++ b/app/resources/v1/photo_comment_resource.rb @@ -3,6 +3,8 @@ class V1::PhotoCommentResource < V1::ApplicationResource self.model = PhotoComment + with_timestamps + attribute :content, :string has_one :photo diff --git a/app/resources/v1/photo_resource.rb b/app/resources/v1/photo_resource.rb index bdf06ff7..9926561c 100644 --- a/app/resources/v1/photo_resource.rb +++ b/app/resources/v1/photo_resource.rb @@ -3,6 +3,8 @@ class V1::PhotoResource < V1::ApplicationResource self.model = Photo + with_timestamps + attribute :image_url, :string, writable: false attribute :image_thumb_url, :string, writable: false do @object.image.thumb.url diff --git a/app/resources/v1/photo_tag_resource.rb b/app/resources/v1/photo_tag_resource.rb index 993f3da0..5ebe6517 100644 --- a/app/resources/v1/photo_tag_resource.rb +++ b/app/resources/v1/photo_tag_resource.rb @@ -3,6 +3,8 @@ class V1::PhotoTagResource < V1::ApplicationResource self.model = PhotoTag + with_timestamps + attribute :x, :float attribute :y, :float diff --git a/app/resources/v1/poll_resource.rb b/app/resources/v1/poll_resource.rb index 4aecaddd..212521ad 100644 --- a/app/resources/v1/poll_resource.rb +++ b/app/resources/v1/poll_resource.rb @@ -3,6 +3,8 @@ class V1::PollResource < V1::ApplicationResource self.model = Poll + with_timestamps + has_one :form, resource: V1::Form::FormResource has_one :author, resource: V1::UserResource diff --git a/app/resources/v1/room_advert_resource.rb b/app/resources/v1/room_advert_resource.rb index 5bd532ab..06cfb3ff 100644 --- a/app/resources/v1/room_advert_resource.rb +++ b/app/resources/v1/room_advert_resource.rb @@ -3,6 +3,8 @@ class V1::RoomAdvertResource < V1::ApplicationResource self.model = RoomAdvert + with_timestamps + attribute :house_name, :string attribute :contact, :string attribute :location, :string diff --git a/app/resources/v1/static_page_resource.rb b/app/resources/v1/static_page_resource.rb index c94a3a3b..47a95438 100644 --- a/app/resources/v1/static_page_resource.rb +++ b/app/resources/v1/static_page_resource.rb @@ -3,6 +3,8 @@ class V1::StaticPageResource < V1::ApplicationResource self.model = StaticPage + with_timestamps + attribute :title, :string attribute :content, :string attribute :content_camofied, :string, writable: false do diff --git a/app/resources/v1/stored_mail_resource.rb b/app/resources/v1/stored_mail_resource.rb index bf484ae2..9f675a6d 100644 --- a/app/resources/v1/stored_mail_resource.rb +++ b/app/resources/v1/stored_mail_resource.rb @@ -3,6 +3,8 @@ class V1::StoredMailResource < V1::ApplicationResource self.model = StoredMail + with_timestamps + attribute :received_at, :datetime, writable: false attribute :sender, :string, writable: false attribute :subject, :string, writable: false diff --git a/app/resources/v1/study_room_presence_resource.rb b/app/resources/v1/study_room_presence_resource.rb index a6ea28be..3468c92d 100644 --- a/app/resources/v1/study_room_presence_resource.rb +++ b/app/resources/v1/study_room_presence_resource.rb @@ -3,6 +3,8 @@ class V1::StudyRoomPresenceResource < V1::ApplicationResource self.model = StudyRoomPresence + with_timestamps + attribute :start_time, :datetime attribute :end_time, :datetime attribute :status, :string diff --git a/app/resources/v1/user_resource.rb b/app/resources/v1/user_resource.rb index 4904aeac..3870dcb1 100644 --- a/app/resources/v1/user_resource.rb +++ b/app/resources/v1/user_resource.rb @@ -3,6 +3,8 @@ class V1::UserResource < V1::ApplicationResource # rubocop:disable Metrics/ClassLength self.model = User + with_timestamps + # Basic attributes (always visible) attribute :username, :string, writable: false attribute :first_name, :string do diff --git a/app/resources/v1/vacancy_resource.rb b/app/resources/v1/vacancy_resource.rb index e808aea0..62b97007 100644 --- a/app/resources/v1/vacancy_resource.rb +++ b/app/resources/v1/vacancy_resource.rb @@ -3,6 +3,8 @@ class V1::VacancyResource < V1::ApplicationResource self.model = Vacancy + with_timestamps + attribute :title, :string attribute :description, :string attribute :description_camofied, :string, writable: false do From 9abe78999c64fd46ff989eec68c7ffc8779c445a Mon Sep 17 00:00:00 2001 From: Lodewiges Date: Wed, 17 Dec 2025 10:44:29 +0100 Subject: [PATCH 4/6] remove context --- app/controllers/concerns/graphiti_crud.rb | 48 +++++++++++-------- app/controllers/v1/users_controller.rb | 36 ++++++++------ app/resources/v1/application_resource.rb | 18 +++---- app/resources/v1/article_resource.rb | 2 +- app/resources/v1/debit/collection_resource.rb | 2 +- .../form/closed_question_option_resource.rb | 2 +- .../v1/form/closed_question_resource.rb | 2 +- app/resources/v1/form/form_resource.rb | 2 +- .../v1/form/open_question_resource.rb | 2 +- app/resources/v1/form/response_resource.rb | 2 +- app/resources/v1/forum/category_resource.rb | 2 +- app/resources/v1/forum/thread_resource.rb | 2 +- app/resources/v1/group_resource.rb | 2 +- app/resources/v1/stored_mail_resource.rb | 2 +- app/resources/v1/user_resource.rb | 14 +++--- 15 files changed, 78 insertions(+), 60 deletions(-) diff --git a/app/controllers/concerns/graphiti_crud.rb b/app/controllers/concerns/graphiti_crud.rb index 48f857de..34e67818 100644 --- a/app/controllers/concerns/graphiti_crud.rb +++ b/app/controllers/concerns/graphiti_crud.rb @@ -16,42 +16,52 @@ def graphiti_resource(klass) end def index - resources = resource_class.all(params, context) - render json: resources.to_jsonapi + Graphiti.with_context(context, action_name.to_sym) do + resources = resource_class.all(params) + render json: resources.to_jsonapi + end end def show - resource = resource_class.find(params, context) - render json: resource.to_jsonapi + Graphiti.with_context(context, action_name.to_sym) do + resource = resource_class.find(params) + render json: resource.to_jsonapi + end end def create - resource = resource_class.build(params, context) + Graphiti.with_context(context, action_name.to_sym) do + resource = resource_class.build(params) - if resource.save - render json: resource.to_jsonapi, status: :created - else - render json: resource.errors.to_jsonapi, status: :unprocessable_entity + if resource.save + render json: resource.to_jsonapi, status: :created + else + render json: resource.errors.to_jsonapi, status: :unprocessable_entity + end end end def update - resource = resource_class.find(params, context) + Graphiti.with_context(context, action_name.to_sym) do + resource = resource_class.find(params) - if resource.update_attributes - render json: resource.to_jsonapi - else - render json: resource.errors.to_jsonapi, status: :unprocessable_entity + if resource.update_attributes + render json: resource.to_jsonapi + else + render json: resource.errors.to_jsonapi, status: :unprocessable_entity + end end end def destroy - resource = resource_class.find(params, context) + Graphiti.with_context(context, action_name.to_sym) do + resource = resource_class.find(params) - if resource.destroy - head :no_content - else - render jsonapi_errors: resource + if resource.destroy + head :no_content + else + render json: resource.errors.to_jsonapi, status: :unprocessable_entity + end end end diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/v1/users_controller.rb index 638d31fc..907a24e3 100644 --- a/app/controllers/v1/users_controller.rb +++ b/app/controllers/v1/users_controller.rb @@ -8,21 +8,27 @@ class V1::UsersController < V1::ApplicationController # rubocop:disable Metrics/ # Graphiti CRUD actions def index - users = V1::UserResource.all(params, context) - render json: users.to_jsonapi + Graphiti.with_context(context, action_name.to_sym) do + users = V1::UserResource.all(params) + render json: users.to_jsonapi + end end def show - user = V1::UserResource.find(params, context) - render json: user.to_jsonapi + Graphiti.with_context(context, action_name.to_sym) do + user = V1::UserResource.find(params) + render json: user.to_jsonapi + end end def create - user = V1::UserResource.build(params, context) - if user.save - render json: user.to_jsonapi, status: :created - else - render json: user.errors.to_jsonapi, status: :unprocessable_entity + Graphiti.with_context(context, action_name.to_sym) do + user = V1::UserResource.build(params) + if user.save + render json: user.to_jsonapi, status: :created + else + render json: user.errors.to_jsonapi, status: :unprocessable_entity + end end end @@ -33,11 +39,13 @@ def update render json: old_password_invalid_error, status: :unprocessable_entity else remove_password_from_params_when_blank? - user = V1::UserResource.find(params, context) - if user.update_attributes - render json: user.to_jsonapi - else - render json: user.errors.to_jsonapi, status: :unprocessable_entity + Graphiti.with_context(context, action_name.to_sym) do + user = V1::UserResource.find(params) + if user.update_attributes + render json: user.to_jsonapi + else + render json: user.errors.to_jsonapi, status: :unprocessable_entity + end end end end diff --git a/app/resources/v1/application_resource.rb b/app/resources/v1/application_resource.rb index 240821a4..c6e64823 100644 --- a/app/resources/v1/application_resource.rb +++ b/app/resources/v1/application_resource.rb @@ -38,7 +38,7 @@ def self.searchable_fields(*fields) # Apply Pundit scoping on index def base_scope - if context&.dig(:action) == 'index' && current_user_or_application + if Graphiti.context[:action] == :index && current_user_or_application Pundit.policy_scope!(current_user_or_application, self.class.model) else self.class.model.all @@ -47,11 +47,11 @@ def base_scope # Authorization helpers def current_user - context&.dig(:user) + Graphiti.context[:user] end def current_application - context&.dig(:application) + Graphiti.context[:application] end def current_user_or_application @@ -68,17 +68,17 @@ def update_permission? # Class-level permission checks class << self - def user_can_create_or_update?(context) - user = context&.dig(:user) + def user_can_create_or_update? + user = Graphiti.context[:user] user&.permission?(:create, model) || user&.permission?(:update, model) end - def update_permission?(context) - context&.dig(:user)&.permission?(:update, model) + def update_permission? + Graphiti.context[:user]&.permission?(:update, model) end - def read_permission?(context) - context&.dig(:user)&.permission?(:read, model) + def read_permission? + Graphiti.context[:user]&.permission?(:read, model) end end end diff --git a/app/resources/v1/article_resource.rb b/app/resources/v1/article_resource.rb index 6d3c22ad..bea43950 100644 --- a/app/resources/v1/article_resource.rb +++ b/app/resources/v1/article_resource.rb @@ -25,7 +25,7 @@ class V1::ArticleResource < V1::ApplicationResource @object.group ? @object.group.avatar.thumb.url : @object.author.avatar.thumb.url end attribute :pinned, :boolean do - writable { self.class.update_permission?(context) } + writable { self.class.update_permission? } end has_one :author, resource: V1::UserResource diff --git a/app/resources/v1/debit/collection_resource.rb b/app/resources/v1/debit/collection_resource.rb index 485bdd14..bbe91443 100644 --- a/app/resources/v1/debit/collection_resource.rb +++ b/app/resources/v1/debit/collection_resource.rb @@ -19,6 +19,6 @@ class V1::Debit::CollectionResource < V1::ApplicationResource end after_commit only: [:create] do |model| - CollectionImportJob.perform_later(model.import_file, model, context[:user]) + CollectionImportJob.perform_later(model.import_file, model, Graphiti.context[:user]) end end diff --git a/app/resources/v1/form/closed_question_option_resource.rb b/app/resources/v1/form/closed_question_option_resource.rb index ed7bcf8b..8d0f9308 100644 --- a/app/resources/v1/form/closed_question_option_resource.rb +++ b/app/resources/v1/form/closed_question_option_resource.rb @@ -13,7 +13,7 @@ class V1::Form::ClosedQuestionOptionResource < V1::ApplicationResource def base_scope scope = super - if context&.dig(:action) == 'index' + if Graphiti.context[:action] == 'index' scope = scope.includes(:answers) end scope diff --git a/app/resources/v1/form/closed_question_resource.rb b/app/resources/v1/form/closed_question_resource.rb index 754b22b9..396702b2 100644 --- a/app/resources/v1/form/closed_question_resource.rb +++ b/app/resources/v1/form/closed_question_resource.rb @@ -15,7 +15,7 @@ class V1::Form::ClosedQuestionResource < V1::ApplicationResource def base_scope scope = super - if context&.dig(:action) == 'index' + if Graphiti.context[:action] == 'index' scope = scope.includes(:options) end scope diff --git a/app/resources/v1/form/form_resource.rb b/app/resources/v1/form/form_resource.rb index 98fea4e7..b5d4dde0 100644 --- a/app/resources/v1/form/form_resource.rb +++ b/app/resources/v1/form/form_resource.rb @@ -27,7 +27,7 @@ class V1::Form::FormResource < V1::ApplicationResource def base_scope scope = super - if context&.dig(:action) == 'index' + if Graphiti.context[:action] == 'index' scope = scope.includes(:responses, :open_questions, :closed_questions) end scope diff --git a/app/resources/v1/form/open_question_resource.rb b/app/resources/v1/form/open_question_resource.rb index 080e8750..7b84f834 100644 --- a/app/resources/v1/form/open_question_resource.rb +++ b/app/resources/v1/form/open_question_resource.rb @@ -15,7 +15,7 @@ class V1::Form::OpenQuestionResource < V1::ApplicationResource def base_scope scope = super - if context&.dig(:action) == 'index' + if Graphiti.context[:action] == 'index' scope = scope.includes(:answers) end scope diff --git a/app/resources/v1/form/response_resource.rb b/app/resources/v1/form/response_resource.rb index 5eb87aed..d65b7e06 100644 --- a/app/resources/v1/form/response_resource.rb +++ b/app/resources/v1/form/response_resource.rb @@ -14,7 +14,7 @@ class V1::Form::ResponseResource < V1::ApplicationResource def base_scope scope = super - if context&.dig(:action) == 'index' + if Graphiti.context[:action] == 'index' scope = scope.includes(:open_question_answers, :closed_question_answers) end scope diff --git a/app/resources/v1/forum/category_resource.rb b/app/resources/v1/forum/category_resource.rb index b609d5ab..527b2072 100644 --- a/app/resources/v1/forum/category_resource.rb +++ b/app/resources/v1/forum/category_resource.rb @@ -14,7 +14,7 @@ class V1::Forum::CategoryResource < V1::ApplicationResource def base_scope scope = super - if context&.dig(:action) == 'index' + if Graphiti.context[:action] == 'index' scope = scope.includes(:threads) end scope diff --git a/app/resources/v1/forum/thread_resource.rb b/app/resources/v1/forum/thread_resource.rb index 5577e92a..5338c7b7 100644 --- a/app/resources/v1/forum/thread_resource.rb +++ b/app/resources/v1/forum/thread_resource.rb @@ -7,7 +7,7 @@ class V1::Forum::ThreadResource < V1::ApplicationResource attribute :title, :string attribute :closed_at, :datetime do - writable { self.class.user_can_create_or_update?(context) } + writable { self.class.user_can_create_or_update? } end attribute :amount_of_posts, :integer, writable: false do @object.posts.size diff --git a/app/resources/v1/group_resource.rb b/app/resources/v1/group_resource.rb index 1216914d..5d7f9714 100644 --- a/app/resources/v1/group_resource.rb +++ b/app/resources/v1/group_resource.rb @@ -60,6 +60,6 @@ class V1::GroupResource < V1::ApplicationResource private def user_can_create_or_update? - self.class.user_can_create_or_update?(context) + self.class.user_can_create_or_update? end end diff --git a/app/resources/v1/stored_mail_resource.rb b/app/resources/v1/stored_mail_resource.rb index 9f675a6d..fa73e5e3 100644 --- a/app/resources/v1/stored_mail_resource.rb +++ b/app/resources/v1/stored_mail_resource.rb @@ -24,7 +24,7 @@ class V1::StoredMailResource < V1::ApplicationResource def base_scope scope = super - if context&.dig(:action) == 'index' + if Graphiti.context[:action] == 'index' scope = scope.includes(inbound_email: { raw_email_attachment: :blob }) end scope diff --git a/app/resources/v1/user_resource.rb b/app/resources/v1/user_resource.rb index 3870dcb1..545d3eb0 100644 --- a/app/resources/v1/user_resource.rb +++ b/app/resources/v1/user_resource.rb @@ -197,7 +197,7 @@ class V1::UserResource < V1::ApplicationResource # rubocop:disable Metrics/Class # Scope with eager loading for index def base_scope scope = super - if context&.dig(:action) == 'index' + if Graphiti.context[:action] == 'index' scope = scope.includes(:mandates) end scope @@ -224,19 +224,19 @@ def read_user_details_or_sofia? def read_user_details_for_record?(record) record.user_details_sharing_preference == 'all_users' || - ((self.class.read_permission?(context) || record == current_user) && + ((self.class.read_permission? || record == current_user) && record.user_details_sharing_preference == 'members_only') || - record == current_user || self.class.update_permission?(context) + record == current_user || self.class.update_permission? end def application_is_sofia? - return false unless context&.key?(:application) && context[:application] + return false unless context&.key?(:application) && Graphiti.context[:application] - context[:application].scopes.to_a.include?('sofia') + Graphiti.context[:application].scopes.to_a.include?('sofia') end def update_or_me? - self.class.update_permission?(context) || me? + self.class.update_permission? || me? end def me? @@ -244,6 +244,6 @@ def me? end def user_can_create_or_update? - self.class.user_can_create_or_update?(context) + self.class.user_can_create_or_update? end end From 5d3aa5ee2a05babbcb021797ad74bcc492fde116 Mon Sep 17 00:00:00 2001 From: Lodewiges Date: Wed, 17 Dec 2025 10:47:15 +0100 Subject: [PATCH 5/6] use writeable atributes diffrently --- app/resources/v1/article_resource.rb | 4 +--- app/resources/v1/forum/thread_resource.rb | 4 +--- app/resources/v1/user_resource.rb | 18 +++--------------- 3 files changed, 5 insertions(+), 21 deletions(-) diff --git a/app/resources/v1/article_resource.rb b/app/resources/v1/article_resource.rb index bea43950..11a68cfa 100644 --- a/app/resources/v1/article_resource.rb +++ b/app/resources/v1/article_resource.rb @@ -24,9 +24,7 @@ class V1::ArticleResource < V1::ApplicationResource attribute :avatar_thumb_url, :string, writable: false do @object.group ? @object.group.avatar.thumb.url : @object.author.avatar.thumb.url end - attribute :pinned, :boolean do - writable { self.class.update_permission? } - end + attribute :pinned, :boolean, writable: -> { self.class.update_permission? } has_one :author, resource: V1::UserResource has_one :group diff --git a/app/resources/v1/forum/thread_resource.rb b/app/resources/v1/forum/thread_resource.rb index 5338c7b7..7ff1b7f9 100644 --- a/app/resources/v1/forum/thread_resource.rb +++ b/app/resources/v1/forum/thread_resource.rb @@ -6,9 +6,7 @@ class V1::Forum::ThreadResource < V1::ApplicationResource with_timestamps attribute :title, :string - attribute :closed_at, :datetime do - writable { self.class.user_can_create_or_update? } - end + attribute :closed_at, :datetime, writable: -> { self.class.user_can_create_or_update? } attribute :amount_of_posts, :integer, writable: false do @object.posts.size end diff --git a/app/resources/v1/user_resource.rb b/app/resources/v1/user_resource.rb index 545d3eb0..0e15cf74 100644 --- a/app/resources/v1/user_resource.rb +++ b/app/resources/v1/user_resource.rb @@ -7,21 +7,9 @@ class V1::UserResource < V1::ApplicationResource # rubocop:disable Metrics/Class # Basic attributes (always visible) attribute :username, :string, writable: false - attribute :first_name, :string do - writable do - user_can_create_or_update? - end - end - attribute :last_name_prefix, :string do - writable do - user_can_create_or_update? - end - end - attribute :last_name, :string do - writable do - user_can_create_or_update? - end - end + attribute :first_name, :string, writable: -> { user_can_create_or_update? } + attribute :last_name_prefix, :string, writable: -> { user_can_create_or_update? } + attribute :last_name, :string, writable: -> { user_can_create_or_update? } attribute :full_name, :string, writable: false attribute :nickname, :string From 7756fde8c5874ec33b384de6649517e89b4a4d12 Mon Sep 17 00:00:00 2001 From: Lodewiges Date: Thu, 18 Dec 2025 15:30:01 +0100 Subject: [PATCH 6/6] filter static pages --- app/resources/v1/static_page_resource.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/resources/v1/static_page_resource.rb b/app/resources/v1/static_page_resource.rb index 47a95438..9c6e7e4e 100644 --- a/app/resources/v1/static_page_resource.rb +++ b/app/resources/v1/static_page_resource.rb @@ -5,6 +5,9 @@ class V1::StaticPageResource < V1::ApplicationResource with_timestamps + # Override id attribute to accept string slugs instead of just integers + attribute :id, :string + attribute :title, :string attribute :content, :string attribute :content_camofied, :string, writable: false do @@ -16,6 +19,17 @@ class V1::StaticPageResource < V1::ApplicationResource searchable_fields :title, :content + # Support friendly_id slugs for lookup by ID + filter :id, :string do + eq do |scope, value| + # Use FriendlyId to find by slug or numeric ID + record = scope.friendly.find(value) + scope.where(id: record.id) + rescue ActiveRecord::RecordNotFound + scope.none + end + end + # Support friendly_id slugs def base_scope super.friendly