From a049dcc053021069a22f50e8a9e8ec09ee9b336c Mon Sep 17 00:00:00 2001 From: lorenzo farnararo Date: Sat, 20 Jul 2024 00:05:00 +0200 Subject: [PATCH] adding some view and some something ;) --- .rubocop.yml | 1 + app/controllers/members_controller.rb | 33 ++ app/models/member.rb | 9 +- app/views/application/_header.erb | 109 ++++++ app/views/home/_member.html.erb | 15 + app/views/home/index.html.erb | 34 +- app/views/layouts/application.html.erb | 120 +----- config/application.rb | 2 + config/initializers/simple_form_bootstrap.rb | 375 +++++++++++++++++++ config/locales/simple_form.it.yml | 39 ++ config/routes.rb | 2 + db/migrate/20240718060539_create_member.rb | 7 +- db/schema.rb | 7 +- db/seeds.rb | 12 +- lib/templates/erb/scaffold/_form.html.erb | 15 + test/models/member_test.rb | 4 +- 16 files changed, 643 insertions(+), 141 deletions(-) create mode 100644 app/controllers/members_controller.rb create mode 100644 app/views/application/_header.erb create mode 100644 app/views/home/_member.html.erb create mode 100644 config/initializers/simple_form_bootstrap.rb create mode 100644 config/locales/simple_form.it.yml create mode 100644 lib/templates/erb/scaffold/_form.html.erb diff --git a/.rubocop.yml b/.rubocop.yml index 5a92dcf..a9f9385 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -17,6 +17,7 @@ Style/Documentation: Metrics/BlockLength: Exclude: - config/environments/* + - config/initializers/simple_form_bootstrap.rb Metrics/MethodLength: Max: 35 diff --git a/app/controllers/members_controller.rb b/app/controllers/members_controller.rb new file mode 100644 index 0000000..1065083 --- /dev/null +++ b/app/controllers/members_controller.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class MembersController < ApplicationController + def update + @member = Member.find(params[:id]) + + if @member.update(member_params) + respond_to do |format| + format.turbo_stream + end + else + render :new, status: :unprocessable_entity + end + end + + def member_params + params.require(:member) + .permit( + :first_name, + :last_name, + :born_at, + :born_in, + :tax_code, + :citizenship, + :address, + :postal_code, + :municipality, + :province, + :telephone, + :email + ) + end +end diff --git a/app/models/member.rb b/app/models/member.rb index 402fdbb..7476ce9 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -4,7 +4,14 @@ class Member < ApplicationRecord has_many :payments, dependent: :destroy has_many :memberships, dependent: :destroy - validates :name, :surname, :born_at, :born_in, :citizenship, :address, :postal_code, presence: true + validates :first_name, + :last_name, + :born_at, + :born_in, + :citizenship, + :address, + :postal_code, + :municipality, presence: true def status %w[ok warning error].sample diff --git a/app/views/application/_header.erb b/app/views/application/_header.erb new file mode 100644 index 0000000..8486f67 --- /dev/null +++ b/app/views/application/_header.erb @@ -0,0 +1,109 @@ + diff --git a/app/views/home/_member.html.erb b/app/views/home/_member.html.erb new file mode 100644 index 0000000..8ea9ac8 --- /dev/null +++ b/app/views/home/_member.html.erb @@ -0,0 +1,15 @@ +<%= simple_form_for member, html: { class: 'row' } do |f| %> + <%= f.input :first_name, wrapper_html: { class: 'col-6' } %> + <%= f.input :last_name, wrapper_html: { class: 'col-6' } %> + <%= f.input :born_at %> + <%= f.input :born_in %> + <%= f.input :tax_code %> + <%= f.input :citizenship, wrapper_html: { class: 'col-6' } %> + <%= f.input :address, wrapper_html: { class: 'col-6' } %> + <%= f.input :postal_code, wrapper_html: { class: 'col-2' } %> + <%= f.input :municipality, wrapper_html: { class: 'col-8' } %> + <%= f.input :province, wrapper_html: { class: 'col-2' } %> + <%= f.input :telephone, wrapper_html: { class: 'col-6' } %> + <%= f.input :email, wrapper_html: { class: 'col-6' } %> + <%= f.submit 'Save', class: 'd-none' %> +<% end %> diff --git a/app/views/home/index.html.erb b/app/views/home/index.html.erb index b3c742e..c210a85 100644 --- a/app/views/home/index.html.erb +++ b/app/views/home/index.html.erb @@ -1,17 +1,19 @@ - -
- <% @members.each do |member| %> -
- <%= member.name %> -
- <% end %> +
+ +
+ <% @members.each do |member| %> +
+ <%= render partial: 'member', locals: { member: } %> +
+ <% end %> +
\ No newline at end of file diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 053e0dd..d479d33 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -20,121 +20,17 @@
- + <%= render partial: 'header' %>
-
- <%= yield %> -
+
+
+
+ <%= yield %> +
+
+
diff --git a/config/application.rb b/config/application.rb index 12c2cef..36d65aa 100644 --- a/config/application.rb +++ b/config/application.rb @@ -25,5 +25,7 @@ class Application < Rails::Application # # config.time_zone = "Central Time (US & Canada)" # config.eager_load_paths << Rails.root.join("extras") + + config.i18n.default_locale = :it end end diff --git a/config/initializers/simple_form_bootstrap.rb b/config/initializers/simple_form_bootstrap.rb new file mode 100644 index 0000000..09ccabd --- /dev/null +++ b/config/initializers/simple_form_bootstrap.rb @@ -0,0 +1,375 @@ +# frozen_string_literal: true + +# These defaults are defined and maintained by the community at +# https://github.com/heartcombo/simple_form-bootstrap +# Please submit feedback, changes and tests only there. + +# Uncomment this and change the path if necessary to include your own +# components. +# See https://github.com/heartcombo/simple_form#custom-components +# to know more about custom components. +# Dir[Rails.root.join('lib/components/**/*.rb')].each { |f| require f } + +# Use this setup block to configure all options available in SimpleForm. +SimpleForm.setup do |config| + # Default class for buttons + config.button_class = 'btn' + + # Define the default class of the input wrapper of the boolean input. + config.boolean_label_class = 'form-check-label' + + # How the label text should be generated altogether with the required text. + config.label_text = ->(label, required, _explicit_label) { "#{label} #{required}" } + + # Define the way to render check boxes / radio buttons with labels. + config.boolean_style = :inline + + # You can wrap each item in a collection of radio/check boxes with a tag + config.item_wrapper_tag = :div + + # Defines if the default input wrapper class should be included in radio + # collection wrappers. + config.include_default_input_wrapper_class = false + + # CSS class to add for error notification helper. + config.error_notification_class = 'alert alert-danger' + + # Method used to tidy up errors. Specify any Rails Array method. + # :first lists the first message for each field. + # :to_sentence to list all errors for each field. + config.error_method = :to_sentence + + # add validation classes to `input_field` + config.input_field_error_class = 'is-invalid' + config.input_field_valid_class = 'is-valid' + + # vertical forms + # + # vertical default_wrapper + config.wrappers :vertical_form, class: 'mb-3' do |b| + b.use :html5 + b.use :placeholder + b.optional :maxlength + b.optional :minlength + b.optional :pattern + b.optional :min_max + b.optional :readonly + b.use :label, class: 'form-label' + b.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' + b.use :full_error, wrap_with: { class: 'invalid-feedback' } + b.use :hint, wrap_with: { class: 'form-text' } + end + + # vertical input for boolean + config.wrappers :vertical_boolean, tag: 'fieldset', class: 'mb-3' do |b| + b.use :html5 + b.optional :readonly + b.wrapper :form_check_wrapper, class: 'form-check' do |bb| + bb.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' + bb.use :label, class: 'form-check-label' + bb.use :full_error, wrap_with: { class: 'invalid-feedback' } + bb.use :hint, wrap_with: { class: 'form-text' } + end + end + + # vertical input for radio buttons and check boxes + config.wrappers :vertical_collection, + item_wrapper_class: 'form-check', + item_label_class: 'form-check-label', + tag: 'fieldset', + class: 'mb-3' do |b| + b.use :html5 + b.optional :readonly + b.wrapper :legend_tag, tag: 'legend', class: 'col-form-label pt-0' do |ba| + ba.use :label_text + end + b.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' + b.use :full_error, wrap_with: { class: 'invalid-feedback d-block' } + b.use :hint, wrap_with: { class: 'form-text' } + end + + # vertical input for inline radio buttons and check boxes + config.wrappers :vertical_collection_inline, + item_wrapper_class: 'form-check form-check-inline', + item_label_class: 'form-check-label', + tag: 'fieldset', + class: 'mb-3' do |b| + b.use :html5 + b.optional :readonly + b.wrapper :legend_tag, tag: 'legend', class: 'col-form-label pt-0' do |ba| + ba.use :label_text + end + b.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' + b.use :full_error, wrap_with: { class: 'invalid-feedback d-block' } + b.use :hint, wrap_with: { class: 'form-text' } + end + + # vertical file input + config.wrappers :vertical_file, class: 'mb-3' do |b| + b.use :html5 + b.use :placeholder + b.optional :maxlength + b.optional :minlength + b.optional :readonly + b.use :label, class: 'form-label' + b.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' + b.use :full_error, wrap_with: { class: 'invalid-feedback' } + b.use :hint, wrap_with: { class: 'form-text' } + end + + # vertical select input + config.wrappers :vertical_select, class: 'mb-3' do |b| + b.use :html5 + b.optional :readonly + b.use :label, class: 'form-label' + b.use :input, class: 'form-select', error_class: 'is-invalid', valid_class: 'is-valid' + b.use :full_error, wrap_with: { class: 'invalid-feedback' } + b.use :hint, wrap_with: { class: 'form-text' } + end + + # vertical multi select + config.wrappers :vertical_multi_select, class: 'mb-3' do |b| + b.use :html5 + b.optional :readonly + b.use :label, class: 'form-label' + b.wrapper class: 'd-flex flex-row justify-content-between align-items-center' do |ba| + ba.use :input, class: 'form-select mx-1', error_class: 'is-invalid', valid_class: 'is-valid' + end + b.use :full_error, wrap_with: { class: 'invalid-feedback d-block' } + b.use :hint, wrap_with: { class: 'form-text' } + end + + # vertical range input + config.wrappers :vertical_range, class: 'mb-3' do |b| + b.use :html5 + b.use :placeholder + b.optional :readonly + b.optional :step + b.use :label, class: 'form-label' + b.use :input, class: 'form-range', error_class: 'is-invalid', valid_class: 'is-valid' + b.use :full_error, wrap_with: { class: 'invalid-feedback' } + b.use :hint, wrap_with: { class: 'form-text' } + end + + # horizontal forms + # + # horizontal default_wrapper + config.wrappers :horizontal_form, class: 'row mb-3' do |b| + b.use :html5 + b.use :placeholder + b.optional :maxlength + b.optional :minlength + b.optional :pattern + b.optional :min_max + b.optional :readonly + b.use :label, class: 'col-sm-3 col-form-label' + b.wrapper :grid_wrapper, class: 'col-sm-9' do |ba| + ba.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' + ba.use :full_error, wrap_with: { class: 'invalid-feedback' } + ba.use :hint, wrap_with: { class: 'form-text' } + end + end + + # horizontal input for boolean + config.wrappers :horizontal_boolean, class: 'row mb-3' do |b| + b.use :html5 + b.optional :readonly + b.wrapper :grid_wrapper, class: 'col-sm-9 offset-sm-3' do |wr| + wr.wrapper :form_check_wrapper, class: 'form-check' do |bb| + bb.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' + bb.use :label, class: 'form-check-label' + bb.use :full_error, wrap_with: { class: 'invalid-feedback' } + bb.use :hint, wrap_with: { class: 'form-text' } + end + end + end + + # horizontal input for radio buttons and check boxes + config.wrappers :horizontal_collection, item_wrapper_class: 'form-check', item_label_class: 'form-check-label', + class: 'row mb-3' do |b| + b.use :html5 + b.optional :readonly + b.use :label, class: 'col-sm-3 col-form-label pt-0' + b.wrapper :grid_wrapper, class: 'col-sm-9' do |ba| + ba.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' + ba.use :full_error, wrap_with: { class: 'invalid-feedback d-block' } + ba.use :hint, wrap_with: { class: 'form-text' } + end + end + + # horizontal input for inline radio buttons and check boxes + config.wrappers :horizontal_collection_inline, item_wrapper_class: 'form-check form-check-inline', + item_label_class: 'form-check-label', class: 'row mb-3' do |b| + b.use :html5 + b.optional :readonly + b.use :label, class: 'col-sm-3 col-form-label pt-0' + b.wrapper :grid_wrapper, class: 'col-sm-9' do |ba| + ba.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' + ba.use :full_error, wrap_with: { class: 'invalid-feedback d-block' } + ba.use :hint, wrap_with: { class: 'form-text' } + end + end + + # horizontal file input + config.wrappers :horizontal_file, class: 'row mb-3' do |b| + b.use :html5 + b.use :placeholder + b.optional :maxlength + b.optional :minlength + b.optional :readonly + b.use :label, class: 'col-sm-3 col-form-label' + b.wrapper :grid_wrapper, class: 'col-sm-9' do |ba| + ba.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' + ba.use :full_error, wrap_with: { class: 'invalid-feedback' } + ba.use :hint, wrap_with: { class: 'form-text' } + end + end + + # horizontal select input + config.wrappers :horizontal_select, class: 'row mb-3' do |b| + b.use :html5 + b.optional :readonly + b.use :label, class: 'col-sm-3 col-form-label' + b.wrapper :grid_wrapper, class: 'col-sm-9' do |ba| + ba.use :input, class: 'form-select', error_class: 'is-invalid', valid_class: 'is-valid' + ba.use :full_error, wrap_with: { class: 'invalid-feedback' } + ba.use :hint, wrap_with: { class: 'form-text' } + end + end + + # horizontal multi select + config.wrappers :horizontal_multi_select, class: 'row mb-3' do |b| + b.use :html5 + b.optional :readonly + b.use :label, class: 'col-sm-3 col-form-label' + b.wrapper :grid_wrapper, class: 'col-sm-9' do |ba| + ba.wrapper class: 'd-flex flex-row justify-content-between align-items-center' do |bb| + bb.use :input, class: 'form-select mx-1', error_class: 'is-invalid', valid_class: 'is-valid' + end + ba.use :full_error, wrap_with: { class: 'invalid-feedback d-block' } + ba.use :hint, wrap_with: { class: 'form-text' } + end + end + + # horizontal range input + config.wrappers :horizontal_range, class: 'row mb-3' do |b| + b.use :html5 + b.use :placeholder + b.optional :readonly + b.optional :step + b.use :label, class: 'col-sm-3 col-form-label pt-0' + b.wrapper :grid_wrapper, class: 'col-sm-9' do |ba| + ba.use :input, class: 'form-range', error_class: 'is-invalid', valid_class: 'is-valid' + ba.use :full_error, wrap_with: { class: 'invalid-feedback' } + ba.use :hint, wrap_with: { class: 'form-text' } + end + end + + # inline forms + # + # inline default_wrapper + config.wrappers :inline_form, class: 'col-12' do |b| + b.use :html5 + b.use :placeholder + b.optional :maxlength + b.optional :minlength + b.optional :pattern + b.optional :min_max + b.optional :readonly + b.use :label, class: 'visually-hidden' + + b.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' + b.use :error, wrap_with: { class: 'invalid-feedback' } + b.optional :hint, wrap_with: { class: 'form-text' } + end + + # inline input for boolean + config.wrappers :inline_boolean, class: 'col-12' do |b| + b.use :html5 + b.optional :readonly + b.wrapper :form_check_wrapper, class: 'form-check' do |bb| + bb.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' + bb.use :label, class: 'form-check-label' + bb.use :error, wrap_with: { class: 'invalid-feedback' } + bb.optional :hint, wrap_with: { class: 'form-text' } + end + end + + # bootstrap custom forms + # + # custom input switch for boolean + config.wrappers :custom_boolean_switch, class: 'mb-3' do |b| + b.use :html5 + b.optional :readonly + b.wrapper :form_check_wrapper, tag: 'div', class: 'form-check form-switch' do |bb| + bb.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' + bb.use :label, class: 'form-check-label' + bb.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback' } + bb.use :hint, wrap_with: { class: 'form-text' } + end + end + + # Input Group - custom component + # see example app and config at https://github.com/heartcombo/simple_form-bootstrap + config.wrappers :input_group, class: 'mb-3' do |b| + b.use :html5 + b.use :placeholder + b.optional :maxlength + b.optional :minlength + b.optional :pattern + b.optional :min_max + b.optional :readonly + b.use :label, class: 'form-label' + b.wrapper :input_group_tag, class: 'input-group' do |ba| + ba.optional :prepend + ba.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' + ba.optional :append + ba.use :full_error, wrap_with: { class: 'invalid-feedback' } + end + b.use :hint, wrap_with: { class: 'form-text' } + end + + # Floating Labels form + # + # floating labels default_wrapper + config.wrappers :floating_labels_form, class: 'form-floating mb-3' do |b| + b.use :html5 + b.use :placeholder + b.optional :maxlength + b.optional :minlength + b.optional :pattern + b.optional :min_max + b.optional :readonly + b.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' + b.use :label + b.use :full_error, wrap_with: { class: 'invalid-feedback' } + b.use :hint, wrap_with: { class: 'form-text' } + end + + # custom multi select + config.wrappers :floating_labels_select, class: 'form-floating mb-3' do |b| + b.use :html5 + b.optional :readonly + b.use :input, class: 'form-select', error_class: 'is-invalid', valid_class: 'is-valid' + b.use :label + b.use :full_error, wrap_with: { class: 'invalid-feedback' } + b.use :hint, wrap_with: { class: 'form-text' } + end + + # The default wrapper to be used by the FormBuilder. + config.default_wrapper = :vertical_form + + # Custom wrappers for input types. This should be a hash containing an input + # type as key and the wrapper that will be used for all inputs with specified type. + config.wrapper_mappings = { + boolean: :vertical_boolean, + check_boxes: :vertical_collection, + date: :vertical_multi_select, + datetime: :vertical_multi_select, + file: :vertical_file, + radio_buttons: :vertical_collection, + range: :vertical_range, + time: :vertical_multi_select, + select: :vertical_select + } +end diff --git a/config/locales/simple_form.it.yml b/config/locales/simple_form.it.yml new file mode 100644 index 0000000..84ca5a2 --- /dev/null +++ b/config/locales/simple_form.it.yml @@ -0,0 +1,39 @@ +it: + simple_form: + "yes": 'Si' + "no": 'No' + required: + text: 'obbligatorio' + mark: '*' + # You can uncomment the line below if you need to overwrite the whole required html. + # When using html, text and mark won't be used. + # html: '*' + error_notification: + default_message: "Please review the problems below:" + labels: + member: + first_name: Nome + last_name: Cognome + born_at: Nato il + born_in: Nato a + tax_code: Codice fiscale + postal_code: CAP + municipality: Comune + province: Provincia + telephone: Telefono + email: Email + # user: + # new: + # email: 'E-mail to sign in.' + # edit: + # email: 'E-mail.' + # hints: + # defaults: + # username: 'User name to sign in.' + # password: 'No special characters, please.' + # include_blanks: + # defaults: + # age: 'Rather not say' + # prompts: + # defaults: + # age: 'Select your age' diff --git a/config/routes.rb b/config/routes.rb index 96d91b7..54bac07 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,5 +3,7 @@ Rails.application.routes.draw do root to: 'home#index' + resources :members + get 'up' => 'rails/health#show', as: :rails_health_check end diff --git a/db/migrate/20240718060539_create_member.rb b/db/migrate/20240718060539_create_member.rb index 6e96e49..44d4182 100644 --- a/db/migrate/20240718060539_create_member.rb +++ b/db/migrate/20240718060539_create_member.rb @@ -3,15 +3,16 @@ class CreateMember < ActiveRecord::Migration[7.2] def change create_table :members do |t| - t.string :name, null: false - t.string :surname, null: false + t.string :first_name, null: false + t.string :last_name, null: false t.date :born_at, null: false t.string :born_in, null: false t.string :tax_code t.string :citizenship, null: false t.string :address, null: false t.string :postal_code, null: false - t.string :province + t.string :municipality, null: false + t.string :province, null: false t.string :telephone t.string :email diff --git a/db/schema.rb b/db/schema.rb index eefba89..e342cf8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -19,15 +19,16 @@ end create_table "members", force: :cascade do |t| - t.string "name", null: false - t.string "surname", null: false + t.string "first_name", null: false + t.string "last_name", null: false t.date "born_at", null: false t.string "born_in", null: false t.string "tax_code" t.string "citizenship", null: false t.string "address", null: false t.string "postal_code", null: false - t.string "province" + t.string "municipality", null: false + t.string "province", null: false t.string "telephone" t.string "email" t.text "notes" diff --git a/db/seeds.rb b/db/seeds.rb index a72d4ef..9912a6a 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -15,13 +15,17 @@ Faker::Config.locale = 'it' 10.times do - Member.create( - name: Faker::Name.first_name, - surname: Faker::Name.last_name, + Member.create!( + first_name: Faker::Name.first_name, + last_name: Faker::Name.last_name, born_at: Faker::Date.between(from: '2015-12-31', to: '2000-01-01'), born_in: Faker::Address.city, citizenship: Faker::Address.country, address: Faker::Address.full_address, - postal_code: Faker::Address.postcode + postal_code: Faker::Address.postcode, + municipality: Faker::Address.community, + province: Faker::Address.community.first(2), + telephone: Faker::PhoneNumber.cell_phone, + email: Faker::Internet.email ) end diff --git a/lib/templates/erb/scaffold/_form.html.erb b/lib/templates/erb/scaffold/_form.html.erb new file mode 100644 index 0000000..106b71e --- /dev/null +++ b/lib/templates/erb/scaffold/_form.html.erb @@ -0,0 +1,15 @@ +<%# frozen_string_literal: true %> +<%%= simple_form_for(@<%= singular_table_name %>) do |f| %> + <%%= f.error_notification %> + <%%= f.error_notification message: f.object.errors[:base].to_sentence if f.object.errors[:base].present? %> + +
+ <%- attributes.each do |attribute| -%> + <%%= f.<%= attribute.reference? ? :association : :input %> :<%= attribute.name %> %> + <%- end -%> +
+ +
+ <%%= f.button :submit %> +
+<%% end %> diff --git a/test/models/member_test.rb b/test/models/member_test.rb index 0089690..159c596 100644 --- a/test/models/member_test.rb +++ b/test/models/member_test.rb @@ -6,8 +6,8 @@ class MemberTest < ActiveSupport::TestCase test 'valudates presence' do member = Member.create - assert(member.errors.key?('name')) - assert(member.errors.key?('surname')) + assert(member.errors.key?('first_name')) + assert(member.errors.key?('last_name')) assert(member.errors.key?('born_at')) assert(member.errors.key?('born_in')) assert(member.errors.key?('citizenship'))