diff --git a/app/controllers/admin/university/apps_controller.rb b/app/controllers/admin/university/apps_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..5832899bcb9d5308535d997ba0a2e2f9a8de72c8 --- /dev/null +++ b/app/controllers/admin/university/apps_controller.rb @@ -0,0 +1,70 @@ +class Admin::University::AppsController < Admin::University::ApplicationController + load_and_authorize_resource class: University::App, + through: :current_university, + through_association: :apps + + def index + @apps = apply_scopes(@apps).ordered.page(params[:page]) + breadcrumb + end + + def show + @should_display_token = @app.display_token! + breadcrumb + end + + def new + breadcrumb + end + + def edit + breadcrumb + add_breadcrumb t('edit') + end + + def create + if @app.save + redirect_to admin_university_app_path(@app), + notice: t('admin.successfully_created_html', model: @app.to_s) + else + breadcrumb + render :new, status: :unprocessable_entity + end + end + + def update + if @app.update(app_params) + redirect_to admin_university_app_path(@app), + notice: t('admin.successfully_updated_html', model: @app.to_s) + else + breadcrumb + add_breadcrumb t('edit') + end + end + + def destroy + @app.destroy + redirect_to admin_university_apps_url, + notice: t('admin.successfully_destroyed_html', model: @app.to_s) + end + + def regenerate_token + @app.regenerate_token! + redirect_to admin_university_app_path(@app), notice: t('university.apps.token_successfully_regenerated') + end + + protected + + def breadcrumb + super + add_breadcrumb University::App.model_name.human(count: 2), + admin_university_apps_path + breadcrumb_for @app + end + + def app_params + params.require(:university_app) + .permit(:name) + .merge(university_id: current_university.id) + end +end \ No newline at end of file diff --git a/app/controllers/api/osuny/application_controller.rb b/app/controllers/api/osuny/application_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..74e5ba47e8acb595529a58bf52c2eb90195d2ecc --- /dev/null +++ b/app/controllers/api/osuny/application_controller.rb @@ -0,0 +1,8 @@ +class Api::Osuny::ApplicationController < Api::ApplicationController + protected + + def verify_app_token + @app = current_university.apps.find_by(token: request.headers['X-Osuny-Token']) + raise_403_unless @app + end +end \ No newline at end of file diff --git a/app/controllers/api/osuny/communication/websites/posts_controller.rb b/app/controllers/api/osuny/communication/websites/posts_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..9047d91a0bc231c4684a150d40968a0f0c5361e6 --- /dev/null +++ b/app/controllers/api/osuny/communication/websites/posts_controller.rb @@ -0,0 +1,66 @@ +class Api::Osuny::Communication::Websites::PostsController < Api::Osuny::ApplicationController + skip_before_action :verify_authenticity_token, only: :import + before_action :verify_app_token, only: :import + + def import + create_post + import_blocks + render json: :ok + end + + protected + + def create_post + post.language = website.default_language + post.update post_params + post.save + end + + def post + @post ||= website.posts + .where( + university: current_university, + website: website, + migration_identifier: migration_identifier + ) + .first_or_initialize + end + + def import_blocks + blocks.each do |b| + migration_identifier = b[:migration_identifier] + template_kind = b[:template_kind] + block = post.blocks + .where( + template_kind: template_kind, + migration_identifier: migration_identifier + ) + .first_or_initialize + block.university = current_university + data = b[:data].to_unsafe_hash + block.data = block.template.data.merge data + block.save + end + end + + def blocks + return [] unless params[:post].has_key?(:blocks) + @blocks ||= params[:post][:blocks] + end + + def website + @website ||= current_university.websites.find params[:website_id] + end + + def migration_identifier + @migration_identifier ||= params[:migration_identifier] + end + + def post_params + params.require(:post) + .permit( + :title, :language, :meta_description, :summary, + ) + end + +end diff --git a/app/controllers/api/osuny/communication/websites_controller.rb b/app/controllers/api/osuny/communication/websites_controller.rb index a6d4a480946862ded51a537e7d21147f1056a1ad..7b8c17d328d6b93dab05feb2277d0bd6feeb93e4 100644 --- a/app/controllers/api/osuny/communication/websites_controller.rb +++ b/app/controllers/api/osuny/communication/websites_controller.rb @@ -1,4 +1,4 @@ -class Api::Osuny::Communication::WebsitesController < Api::ApplicationController +class Api::Osuny::Communication::WebsitesController < Api::Osuny::ApplicationController def index @websites = current_university.communication_websites.in_production diff --git a/app/controllers/api/osuny/communication_controller.rb b/app/controllers/api/osuny/communication_controller.rb index 597ec060fd44c983375bd75e6d909159830f2557..483a8c844ea33324e38cff99544a8a7781a7a41d 100644 --- a/app/controllers/api/osuny/communication_controller.rb +++ b/app/controllers/api/osuny/communication_controller.rb @@ -1,4 +1,4 @@ -class Api::Osuny::CommunicationController < Api::ApplicationController +class Api::Osuny::CommunicationController < Api::Osuny::ApplicationController def index end end diff --git a/app/controllers/api/osuny/server/websites_controller.rb b/app/controllers/api/osuny/server/websites_controller.rb index c34306c0b7924980007af42be44b44266cb780a1..797aaf0b304232d2955c115a52e3ccdb8b5f94df 100644 --- a/app/controllers/api/osuny/server/websites_controller.rb +++ b/app/controllers/api/osuny/server/websites_controller.rb @@ -1,4 +1,4 @@ -class Api::Osuny::Server::WebsitesController < Api::ApplicationController +class Api::Osuny::Server::WebsitesController < Api::Osuny::ApplicationController skip_before_action :verify_authenticity_token, only: [:theme_released] def index diff --git a/app/controllers/api/osuny/server_controller.rb b/app/controllers/api/osuny/server_controller.rb index 3b59a6e157a1e90880c63543d7d01ad1c2dcae21..2732f8f5e9e55504bf27d4a36f4f81211f54b503 100644 --- a/app/controllers/api/osuny/server_controller.rb +++ b/app/controllers/api/osuny/server_controller.rb @@ -1,4 +1,4 @@ -class Api::Osuny::ServerController < Api::ApplicationController +class Api::Osuny::ServerController < Api::Osuny::ApplicationController def index end end diff --git a/app/models/university.rb b/app/models/university.rb index 783967bd869c6ae216961b3ec9f10e7626f21adc..2789a655b08f108b736ea9a3e72e1e65db3358dd 100644 --- a/app/models/university.rb +++ b/app/models/university.rb @@ -56,6 +56,7 @@ class University < ApplicationRecord # We use after_destroy to let the attachment go first has_many :active_storage_blobs, class_name: 'ActiveStorage::Blob' has_many :imports, dependent: :destroy + has_many :apps, dependent: :destroy belongs_to :default_language, class_name: "Language" validates_presence_of :name diff --git a/app/models/university/app.rb b/app/models/university/app.rb new file mode 100644 index 0000000000000000000000000000000000000000..70a5952ba2f3f2fec3b4a7f9ff1458033fdf11ab --- /dev/null +++ b/app/models/university/app.rb @@ -0,0 +1,52 @@ +# == Schema Information +# +# Table name: university_apps +# +# id :uuid not null, primary key +# name :string +# token :string indexed +# token_was_displayed :boolean default(FALSE) +# created_at :datetime not null +# updated_at :datetime not null +# university_id :uuid not null, indexed +# +# Indexes +# +# index_university_apps_on_token (token) UNIQUE +# index_university_apps_on_university_id (university_id) +# +# Foreign Keys +# +# fk_rails_2d07655e23 (university_id => universities.id) +# +class University::App < ApplicationRecord + TOKEN_LENGTH = 30 + + include WithUniversity + + validates :token, uniqueness: true + + before_validation :generate_token + + scope :ordered, -> { order(:name) } + + def display_token! + return false if token_was_displayed? + toggle!(:token_was_displayed) + true + end + + def regenerate_token! + update(token: nil, token_was_displayed: false) + end + + def to_s + "#{name}" + end + + protected + + def generate_token + self.token = SecureRandom.base64(TOKEN_LENGTH) if self.token.blank? + end +end diff --git a/app/views/admin/university/apps/_form.html.erb b/app/views/admin/university/apps/_form.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..2d99fdf5ef184f84ddec7e1b7361e0d70e26b857 --- /dev/null +++ b/app/views/admin/university/apps/_form.html.erb @@ -0,0 +1,10 @@ +<%= simple_form_for [:admin, app] do |f| %> + <%= f.error_notification %> + <%= f.error_notification message: f.object.errors[:base].to_sentence if f.object.errors[:base].present? %> + + <%= f.input :name %> + + <% content_for :action_bar_right do %> + <%= submit f %> + <% end %> +<% end %> diff --git a/app/views/admin/university/apps/_list.html.erb b/app/views/admin/university/apps/_list.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..dac7abf24e89b47e09dcf5049c7b0eed199239aa --- /dev/null +++ b/app/views/admin/university/apps/_list.html.erb @@ -0,0 +1,23 @@ +<div class="table-responsive"> + <table class="<%= table_classes %>"> + <thead> + <tr> + <th><%= University::App.human_attribute_name('name') %></th> + <th></th> + </tr> + </thead> + <tbody> + <% apps.each do |app| %> + <tr> + <td><%= link_to app, admin_university_app_path(app) %></td> + <td class="text-end"> + <div class="btn-group" role="group"> + <%= edit_link app %> + <%= destroy_link app %> + </div> + </td> + </tr> + <% end %> + </tbody> + </table> +</div> diff --git a/app/views/admin/university/apps/edit.html.erb b/app/views/admin/university/apps/edit.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..75e77d2eac6e9ff041e6bb2d13ae5a809060209c --- /dev/null +++ b/app/views/admin/university/apps/edit.html.erb @@ -0,0 +1,3 @@ +<% content_for :title, @app %> + +<%= render 'form', app: @app %> diff --git a/app/views/admin/university/apps/index.html.erb b/app/views/admin/university/apps/index.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..47be60d351f12117bb747579dd7621b05fd57477 --- /dev/null +++ b/app/views/admin/university/apps/index.html.erb @@ -0,0 +1,14 @@ +<% content_for :title, University::App.model_name.human(count: 2) %> + +<% +action = '' +action += link_to t('create'), + new_admin_university_app_path, + class: button_classes if can?(:create, University::App) +subtitle = t('admin.elements', count: @apps.total_count) +%> +<%= osuny_panel University::App.model_name.human(count: 2), subtitle: subtitle, action: action do %> + <%= render 'filters', current_path: admin_university_apps_path, filters: @filters if @filters.any? %> + <%= render 'admin/university/apps/list', apps: @apps %> + <%= paginate @apps, theme: 'bootstrap-5' %> +<% end %> diff --git a/app/views/admin/university/apps/new.html.erb b/app/views/admin/university/apps/new.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..f2d7dae9625c3b921b69aee42a58881b46637c7c --- /dev/null +++ b/app/views/admin/university/apps/new.html.erb @@ -0,0 +1,3 @@ +<% content_for :title, University::App.model_name.human %> + +<%= render 'form', app: @app %> diff --git a/app/views/admin/university/apps/show.html.erb b/app/views/admin/university/apps/show.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..bd95b1d36717d4576217304d1a9e9db54824ab55 --- /dev/null +++ b/app/views/admin/university/apps/show.html.erb @@ -0,0 +1,24 @@ +<% content_for :title, @app %> + +<% if @should_display_token %> + <p class="text-danger"><%= t('university.apps.token_display_notice') %></p> +<% end %> + +<div class="row"> + <div class="col-lg-6"> + <%= osuny_label University::App.human_attribute_name('token') %> + <input type="string" value="<%= @should_display_token ? @app.token : masked_string(@app.token) %>" class="form-control" disabled> + </div> +</div> + +<% content_for :action_bar_left do %> + <%= link_to t('university.apps.regenerate_token'), + [:regenerate_token, :admin, @app], + method: :post, + data: { confirm: t('please_confirm') }, + class: 'btn btn-warning btn-xs' %> +<% end %> + +<% content_for :action_bar_right do %> + <%= edit_link @app %> +<% end %> diff --git a/config/locales/university/en.yml b/config/locales/university/en.yml index 2c134393a3a608622f7b72ef6f1db8ba3e6579dc..6672718206516c68aebb258457fcd05759bd8e33 100644 --- a/config/locales/university/en.yml +++ b/config/locales/university/en.yml @@ -24,6 +24,9 @@ en: sso_target_url: Target URL url: URL zipcode: Zipcode + university/app: + name: Name + token: Secret token university/organization: address: Address address_name: Address name @@ -128,6 +131,9 @@ en: university: one: University other: Universities + university/app: + one: App + other: Apps university/person: one: Person other: People @@ -202,7 +208,11 @@ en: import_btn: Import cohorts import_hint_html: "Possible values for <i>gender</i> are: m (male), f (female) and n (non binary).<br><i>Phone_professional</i>, <i>phone_personal</i>, <i>mobile</i> and <i>zipcode</i> fields must have a text format, not numbers.<br><i>Country</i> field must contain the ISO 3166 code of the country, so 2 upcase characters (<a href=\"https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes\" target=\_blank\">list</a>).<br><i>Social_twitter</i> field should have no @.<br><i>School</i> field should contain the internal school id.<br><i>Program</i> field should contain the internal program id." title: Cohorts imports - contributing: + apps: + regenerate_token: Regenerate token + token_display_notice: Make sure to store your token somewhere safe. You won’t be able to see it again! + token_successfully_regenerated: The token was successfully regenerated. + contributing: one: contributing university other: contributing universities contributions_total: Contributions (total) diff --git a/config/locales/university/fr.yml b/config/locales/university/fr.yml index c88446772c0bb3e42008c3d87b8f0cc0466f54f8..7e71a8b68a68e594ba5a9d7b26ba602bc9440f94 100644 --- a/config/locales/university/fr.yml +++ b/config/locales/university/fr.yml @@ -24,6 +24,9 @@ fr: sso_target_url: URL cible url: 'URL' zipcode: Code postal + university/app: + name: Nom + token: Jeton secret university/organization: address: Adresse address_name: Nom de l'adresse @@ -128,6 +131,9 @@ fr: university: one: Université other: Universités + university/app: + one: App + other: Apps university/person: one: Personne other: Personnes @@ -202,7 +208,11 @@ fr: import_btn: Importer des promotions import_hint_html: "Les valeurs pour <i>gender</i> peuvent être m (masculin), f (féminin) et n (non binaire).<br>Les champs <i>phone_professional</i>, <i>phone_personal</i>, <i>mobile</i> et <i>zipcode</i> doivent être au format texte, pas nombre.<br>Le champ <i>country</i> doit contenir le code ISO 3166 du pays, sur 2 caratères en majuscule (<a href=\"https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes\" target=\_blank\">liste</a>)<br>Le champ <i>social_twitter</i> ne doit pas contenir d'@.<br>Le champ <i>school</i> doit contenir l'id interne de l'école.<br>Le champ <i>program</i> doit contenir l'id interne de la formation." title: Imports de promotions - contributing: + apps: + regenerate_token: Regénérer le jeton + token_display_notice: Assurez-vous de stocker votre jeton secret de manière sécurisée. Vous ne pourrez pas le revoir ! + token_successfully_regenerated: Le jeton a bien été regénéré. + contributing: one: université contributrice other: universités contributrices contributions_total: Contributions (total) diff --git a/config/routes/admin/university.rb b/config/routes/admin/university.rb index 10d8c533ac62025ac1d2e7a9e773f3a78ca7b09a..440e4fb71b06c4deeebda6c29c22a886e6150046 100644 --- a/config/routes/admin/university.rb +++ b/config/routes/admin/university.rb @@ -8,6 +8,9 @@ namespace :university do resources :imports, only: [:index, :show, :new, :create] end end + resources :apps do + post :regenerate_token, on: :member + end resources :alumni, only: [:index, :show] do member do get 'cohorts' => 'alumni/cohorts#edit' @@ -32,7 +35,7 @@ namespace :university do end end resources :organizations do - collection do + collection do get :search, defaults: { format: 'json' } resources :categories, controller: 'organizations/categories', as: 'organization_categories' end diff --git a/config/routes/api.rb b/config/routes/api.rb index 45b8a12697656e0efcd632765092a1b46ebc8b97..ead34b0004c1ecce6cf71f63d11ec43fad9e05e3 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -5,6 +5,9 @@ namespace :api do get 'communication' => 'communication#index' namespace :communication do get 'websites' => 'websites#index' + namespace :websites do + post ':website_id/posts/import' => 'posts#import' + end end get 'server' => 'server#index' namespace :server do diff --git a/db/migrate/20230917145029_create_university_applications.rb b/db/migrate/20230917145029_create_university_applications.rb new file mode 100644 index 0000000000000000000000000000000000000000..d53e7416f32c0f2fe3848c83e5d3d75dea89341f --- /dev/null +++ b/db/migrate/20230917145029_create_university_applications.rb @@ -0,0 +1,11 @@ +class CreateUniversityApplications < ActiveRecord::Migration[7.0] + def change + create_table :university_apps, id: :uuid do |t| + t.string :name + t.references :university, null: false, foreign_key: true, type: :uuid + t.string :token + + t.timestamps + end + end +end diff --git a/db/migrate/20230917153555_add_migration_id_to_communication_websites_post.rb b/db/migrate/20230917153555_add_migration_id_to_communication_websites_post.rb new file mode 100644 index 0000000000000000000000000000000000000000..299d20e65c41ba9f3de2c25cd4d0fba8d784b724 --- /dev/null +++ b/db/migrate/20230917153555_add_migration_id_to_communication_websites_post.rb @@ -0,0 +1,5 @@ +class AddMigrationIdToCommunicationWebsitesPost < ActiveRecord::Migration[7.0] + def change + add_column :communication_website_posts, :migration_identifier, :string + end +end diff --git a/db/migrate/20230917160437_add_migration_id_to_communication_blocks.rb b/db/migrate/20230917160437_add_migration_id_to_communication_blocks.rb new file mode 100644 index 0000000000000000000000000000000000000000..acfbdbd05aafc668755095bac6a0e745ffc8f42f --- /dev/null +++ b/db/migrate/20230917160437_add_migration_id_to_communication_blocks.rb @@ -0,0 +1,5 @@ +class AddMigrationIdToCommunicationBlocks < ActiveRecord::Migration[7.0] + def change + add_column :communication_blocks, :migration_identifier, :string + end +end diff --git a/db/migrate/20230918105825_add_keys_to_university_apps.rb b/db/migrate/20230918105825_add_keys_to_university_apps.rb new file mode 100644 index 0000000000000000000000000000000000000000..c0e6933621254ac5496eb90eab433a89738a9ac3 --- /dev/null +++ b/db/migrate/20230918105825_add_keys_to_university_apps.rb @@ -0,0 +1,6 @@ +class AddKeysToUniversityApps < ActiveRecord::Migration[7.0] + def change + add_column :university_apps, :access_key, :string + rename_column :university_apps, :token, :secret_key + end +end diff --git a/db/migrate/20230925125538_set_single_token_for_university_apps.rb b/db/migrate/20230925125538_set_single_token_for_university_apps.rb new file mode 100644 index 0000000000000000000000000000000000000000..95495862681e4901a24ffd70c94fbe8f81f33153 --- /dev/null +++ b/db/migrate/20230925125538_set_single_token_for_university_apps.rb @@ -0,0 +1,8 @@ +class SetSingleTokenForUniversityApps < ActiveRecord::Migration[7.0] + def change + remove_column :university_apps, :access_key + rename_column :university_apps, :secret_key, :token + add_index :university_apps, :token, unique: true + add_column :university_apps, :token_was_displayed, :boolean, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 24a02c177b7127439773f0b44873d8a04db73ef1..4d356e988ad865c048c69905e8f16d5ef3a55e61 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -976,10 +976,11 @@ ActiveRecord::Schema[7.0].define(version: 2023_09_25_144224) do create_table "university_apps", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.string "name" t.uuid "university_id", null: false - t.string "secret_key" + t.string "token" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.string "access_key" + t.boolean "token_was_displayed", default: false + t.index ["token"], name: "index_university_apps_on_token", unique: true t.index ["university_id"], name: "index_university_apps_on_university_id" end diff --git a/test/fixtures/university/apps.yml b/test/fixtures/university/apps.yml new file mode 100644 index 0000000000000000000000000000000000000000..09f154e0b504b29af65474e4102fff684c7e4b81 --- /dev/null +++ b/test/fixtures/university/apps.yml @@ -0,0 +1,31 @@ +# == Schema Information +# +# Table name: university_apps +# +# id :uuid not null, primary key +# name :string +# token :string indexed +# token_was_displayed :boolean default(FALSE) +# created_at :datetime not null +# updated_at :datetime not null +# university_id :uuid not null, indexed +# +# Indexes +# +# index_university_apps_on_token (token) UNIQUE +# index_university_apps_on_university_id (university_id) +# +# Foreign Keys +# +# fk_rails_2d07655e23 (university_id => universities.id) +# + +one: + name: MyString + university: one + token: MyString + +two: + name: MyString + university: two + token: MyOtherString diff --git a/test/models/university/app_test.rb b/test/models/university/app_test.rb new file mode 100644 index 0000000000000000000000000000000000000000..9562b2c9eb23d57e7436b906e0231fec2a39d92c --- /dev/null +++ b/test/models/university/app_test.rb @@ -0,0 +1,28 @@ +# == Schema Information +# +# Table name: university_apps +# +# id :uuid not null, primary key +# name :string +# token :string indexed +# token_was_displayed :boolean default(FALSE) +# created_at :datetime not null +# updated_at :datetime not null +# university_id :uuid not null, indexed +# +# Indexes +# +# index_university_apps_on_token (token) UNIQUE +# index_university_apps_on_university_id (university_id) +# +# Foreign Keys +# +# fk_rails_2d07655e23 (university_id => universities.id) +# +require "test_helper" + +class University::ApplicationTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end