From c731e15365150e7f6db75edc5fffc91787cac602 Mon Sep 17 00:00:00 2001 From: Leo Kacenjar Date: Mon, 28 Jul 2025 07:56:15 -0600 Subject: [PATCH 1/4] Asap 204 change all file handling to requests (#239) * Use requests library for both file downloads. Add errors to logs as well as API response. * Locally mount code for inference container. * Add some tests for file related issues. * Install local module during github setup. --- .github/workflows/python_components.yml | 1 + docker-compose.yml | 2 + python_components/ci/requirements.txt | 18 ++-- python_components/ci/scripts/entrypoint.sh | 1 + .../document_inference/helpers.py | 16 +++- .../document_inference/lambda_function.py | 1 + .../tests/test_file_downloads.py | 89 +++++++++++++++++++ .../evaluation/evaluation/utility/document.py | 17 +++- .../evaluation/lambda_function.py | 1 + 9 files changed, 134 insertions(+), 12 deletions(-) create mode 100644 python_components/document_inference/tests/test_file_downloads.py diff --git a/.github/workflows/python_components.yml b/.github/workflows/python_components.yml index b97ba750..da4becc1 100644 --- a/.github/workflows/python_components.yml +++ b/.github/workflows/python_components.yml @@ -26,6 +26,7 @@ jobs: python -m pip install --upgrade pip if [ -f ./python_components/ci/requirements.txt ]; then pip install -r ./python_components/ci/requirements.txt; fi pip install python_components/evaluation + pip install python_components/document_inference python -m spacy download en_core_web_sm - name: Run Linting run: | diff --git a/docker-compose.yml b/docker-compose.yml index b6af5838..438d54fa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,6 +38,8 @@ services: - "9002:8080" environment: - ASAP_LOCAL_MODE=True + volumes: + - ./python_components/document_inference:/var/task lambda_evaluation: container_name: lambda_evaluation build: diff --git a/python_components/ci/requirements.txt b/python_components/ci/requirements.txt index 49bc2a8e..198e37d8 100644 --- a/python_components/ci/requirements.txt +++ b/python_components/ci/requirements.txt @@ -2,23 +2,25 @@ backports.tarfile==1.2.0 beautifulsoup4==4.13.3 black==25.1.0 boto3==1.38.30 -deepeval @ git+https://github.com/codeforamerica/deepeval.git@fix/support-read-only-file-systems +deepeval==3.1.4 flake8==7.1.2 google-api-python-client==2.171.0 google-auth-oauthlib==1.2.2 -importlib-metadata==8.0.0 +inflect==7.3.1 isort==6.0.1 +jaraco-functools==4.2.1 jaraco.collections==5.1.0 -requests-aws4auth==1.3.1 +llm==0.26 pandas==2.2.3 pip-chill==1.0.3 -PyMuPDF==1.25.5 -pytest==8.3.5 -pydantic==2.11.5 +pymupdf==1.25.5 +pypdf==5.8.0 +pysocks==1.7.1 +pytest-httpserver==1.1.3 +requests-aws4auth==1.3.1 scikit-learn==1.6.1 selenium==4.34.2 spacy==3.8.7 tldextract==5.1.3 tomli==2.0.1 -tqdm==4.67.1 -xgboost==2.1.4 +xgboost==2.1.4 \ No newline at end of file diff --git a/python_components/ci/scripts/entrypoint.sh b/python_components/ci/scripts/entrypoint.sh index 73624f00..f4ff6a3e 100755 --- a/python_components/ci/scripts/entrypoint.sh +++ b/python_components/ci/scripts/entrypoint.sh @@ -3,6 +3,7 @@ set -e # Install any local packages. pip install python_components/evaluation +pip install python_components/document_inference # Execute the main command exec "$@" \ No newline at end of file diff --git a/python_components/document_inference/document_inference/helpers.py b/python_components/document_inference/document_inference/helpers.py index 548e28b1..5fc15f02 100644 --- a/python_components/document_inference/document_inference/helpers.py +++ b/python_components/document_inference/document_inference/helpers.py @@ -1,7 +1,7 @@ import json import logging import os -import urllib +import shutil import boto3 import fitz @@ -49,7 +49,19 @@ def get_secret(secret_name: str, local_mode: bool) -> str: def get_file(url: str, output_path: str) -> str: file_name = os.path.basename(url) local_path = f"{output_path}/{file_name}" - urllib.request.urlretrieve(url, local_path) + headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", + "Accept": "application/pdf,application/octet-stream,*/*", + "Accept-Language": "en-US,en;q=0.9", + "Accept-Encoding": "gzip, deflate", + "DNT": "1", + "Connection": "keep-alive", + "Upgrade-Insecure-Requests": "1", + } + with requests.get(url, headers=headers, stream=True) as response: + response.raise_for_status() + with open(f"{output_path}/{file_name}", "wb") as file: + shutil.copyfileobj(response.raw, file) return local_path diff --git a/python_components/document_inference/lambda_function.py b/python_components/document_inference/lambda_function.py index 2affcfb0..ef097be3 100644 --- a/python_components/document_inference/lambda_function.py +++ b/python_components/document_inference/lambda_function.py @@ -76,4 +76,5 @@ def handler(event, context): else: return {"statusCode": 200, "body": helpers.json_dump_collection()} except Exception as e: + helpers.logger.error(f"Error during execution: {e}") return {"statusCode": 500, "body": str(e)} diff --git a/python_components/document_inference/tests/test_file_downloads.py b/python_components/document_inference/tests/test_file_downloads.py new file mode 100644 index 00000000..252830de --- /dev/null +++ b/python_components/document_inference/tests/test_file_downloads.py @@ -0,0 +1,89 @@ +import os + +from document_inference.helpers import get_file +from pytest_httpserver import HTTPServer +from werkzeug import Request, Response + +""" +Tests to assert that our file getting method works with some known curveballs. +""" + + +def test_default_behavior(httpserver: HTTPServer): + def handler(request: Request): + return Response("Plain pdf content!", 200) + + httpserver.expect_request("/test.pdf").respond_with_handler(handler) + _remove_file_if_exists("/tmp/test.pdf") + get_file(httpserver.url_for("/test.pdf"), "/tmp") + _assert_file_contents("/tmp/test.pdf", "Plain pdf content!") + + +def test_content_disposition(httpserver: HTTPServer): + def handler(request: Request): + return Response( + "Great pdf content!", + 200, + headers={"Content-Disposition": "attachment", "filename": "mypdf.pdf"}, + ) + + httpserver.expect_request("/test.pdf").respond_with_handler(handler) + _remove_file_if_exists("/tmp/test.pdf") + get_file(httpserver.url_for("/test.pdf"), "/tmp") + _assert_file_contents("/tmp/test.pdf", "Great pdf content!") + + +def test_header_assertion(httpserver: HTTPServer): + def handler(request: Request): + try: + headers_as_text = str(request.headers) + assert ( + "python" not in headers_as_text + ), f"Headers contain 'python': {headers_as_text}" + assert ( + "urllib" not in headers_as_text + ), f"Headers contain 'urllib': {headers_as_text}" + assert ( + "Mozilla" in headers_as_text + ), f"Headers missing 'Mozilla': {headers_as_text}" + return Response("Great pdf validated by headers content!", 200) + except AssertionError as e: + print(f"Assertion failed: {e}") + return Response(f"Assertion failed: {e}", 500) + + httpserver.expect_request("/test.pdf").respond_with_handler(handler) + _remove_file_if_exists("/tmp/test.pdf") + get_file(httpserver.url_for("/test.pdf"), "/tmp") + _assert_file_contents("/tmp/test.pdf", "Great pdf validated by headers content!") + + +def test_308_redirect(httpserver: HTTPServer): + def redirect_handler(request: Request): + return Response( + "", + status=308, + headers={ + "Location": httpserver.url_for("/redirected.pdf"), + "Cache-Control": "max-age=3600", + }, + ) + + def final_handler(request: Request): + return Response("Great redirected content!", 200) + + httpserver.expect_request("/original.pdf").respond_with_handler(redirect_handler) + httpserver.expect_request("/redirected.pdf").respond_with_handler(final_handler) + _remove_file_if_exists("/tmp/original.pdf") + get_file(httpserver.url_for("/original.pdf"), "/tmp") + _assert_file_contents("/tmp/original.pdf", "Great redirected content!") + + +def _remove_file_if_exists(path: str): + if os.path.exists(path): + os.remove(path) + assert not os.path.exists(path) + + +def _assert_file_contents(path: str, contents: str): + with open(path, "r") as f: + assert f.read() == contents diff --git a/python_components/evaluation/evaluation/utility/document.py b/python_components/evaluation/evaluation/utility/document.py index cd4abb49..116795ae 100644 --- a/python_components/evaluation/evaluation/utility/document.py +++ b/python_components/evaluation/evaluation/utility/document.py @@ -1,6 +1,6 @@ import datetime import os -import urllib +import shutil from abc import ABC, abstractmethod from pathlib import Path from typing import Any, List @@ -8,6 +8,7 @@ import boto3 import fitz import pandas as pd +import requests from deepeval.models import DeepEvalBaseMLLM from deepeval.test_case import MLLMImage from evaluation.utility.helpers import logger @@ -81,7 +82,19 @@ def add_images_to_document( def get_file(url: str, output_path: str) -> str: file_name = os.path.basename(url) local_path = f"{output_path}/{file_name}" - urllib.request.urlretrieve(url, local_path) + headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", + "Accept": "application/pdf,application/octet-stream,*/*", + "Accept-Language": "en-US,en;q=0.9", + "Accept-Encoding": "gzip, deflate", + "DNT": "1", + "Connection": "keep-alive", + "Upgrade-Insecure-Requests": "1", + } + with requests.get(url, headers=headers, stream=True) as response: + response.raise_for_status() + with open(f"{output_path}/{file_name}", "wb") as file: + shutil.copyfileobj(response.raw, file) return local_path diff --git a/python_components/evaluation/lambda_function.py b/python_components/evaluation/lambda_function.py index 7e0964cd..dcc791a2 100644 --- a/python_components/evaluation/lambda_function.py +++ b/python_components/evaluation/lambda_function.py @@ -107,4 +107,5 @@ def handler(event, context): output = str(e) if local_mode: output = traceback.format_exc() + utility.helpers.logger.error(f"Error during execution: {output}") return {"statusCode": 500, "body": output} From 0caf1681a0dd36dd7c46be6a5365569475766bd8 Mon Sep 17 00:00:00 2001 From: Leo Kacenjar Date: Mon, 28 Jul 2025 12:08:36 -0600 Subject: [PATCH 2/4] Asap 173 admin user UI (#222) * Devise working for login. * Migrate away from custom session table. * Use database for session management. * Bring back the tests and fix API authentication. * Get inline validation going. * Return success flash, update tests and lint away. * Remove obsolete test. * Fix accessibility scan. * Theme user profile form. Rename is_admin to is_site_admin. Add is_user_admin. * Start working on admin list. * Remove development creds and add to gitignore. * Fix linting. * Admin views for list, add and edit with validation. * Add admin fields and site picker. * Clean up admin table. * Block admin pages for non-admins. * Add a test. * Fix linting. * Fix broken tests. * Improve mobile experience. * Try braking up tests into two batches. * Make password labels and validation more clear. * Improve current password language. --- .github/workflows/ci.yml | 10 +- app/controllers/admin/users_controller.rb | 68 ++++++++++++++ app/controllers/concerns/access.rb | 14 ++- app/controllers/configurations_controller.rb | 2 +- app/controllers/sites_controller.rb | 2 +- .../users/registrations_controller.rb | 1 - .../controllers/modal_controller.js | 4 +- app/views/admin/users/edit.html.erb | 82 ++++++++++++++++ app/views/admin/users/index.html.erb | 41 ++++++++ app/views/admin/users/new.html.erb | 67 +++++++++++++ app/views/layouts/application.html.erb | 10 +- app/views/sites/index.html.erb | 4 +- app/views/users/registrations/edit.html.erb | 94 ++++++++++++------- app/views/users/sessions/new.html.erb | 4 +- config/locales/en.yml | 37 ++------ config/routes.rb | 4 +- .../20250711141619_change_admin_roles.rb | 8 ++ lib/tasks/users.rake | 3 +- spec/factories/users.rb | 4 +- spec/features/admin_spec.rb | 64 ++++++++++++- spec/features/site_spec.rb | 2 +- spec/requests/api/sites_spec.rb | 2 +- 22 files changed, 436 insertions(+), 91 deletions(-) create mode 100644 app/controllers/admin/users_controller.rb create mode 100644 app/views/admin/users/edit.html.erb create mode 100644 app/views/admin/users/index.html.erb create mode 100644 app/views/admin/users/new.html.erb create mode 100644 db/migrate/20250711141619_change_admin_roles.rb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 95347033..c0872fcf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,11 +52,17 @@ jobs: bundle exec rails db:create bundle exec rails db:migrate - - name: Run tests + - name: Run Non-JS tests env: RAILS_ENV: test DATABASE_URL: postgres://postgres:postgres@localhost:5432/access_pdf_test - run: bundle exec rspec + run: bundle exec rspec spec/models spec/requests + + - name: Run JS tests + env: + RAILS_ENV: test + DATABASE_URL: postgres://postgres:postgres@localhost:5432/access_pdf_test + run: bundle exec rspec spec/features - name: Keep screenshots from failed system tests uses: actions/upload-artifact@v4 diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb new file mode 100644 index 00000000..33d10023 --- /dev/null +++ b/app/controllers/admin/users_controller.rb @@ -0,0 +1,68 @@ +class Admin::UsersController < ApplicationController + include Access + + before_action :ensure_user_user_admin + + before_action :set_user, only: [:new, :edit, :update] + before_action :site_list, only: [:new, :create, :edit, :update] + before_action :set_minimum_password_length, only: [:new, :edit, :update] + + def index + @users = User.all + end + + def new + render "/admin/users/new" + end + + def create + @user = User.new(user_params) + if @user.save + redirect_to admin_users_path, notice: "User added successfully" + else + render :new, status: 422 + end + end + + def edit + render "/admin/users/edit" + end + + def update + if params[:user][:password].blank? + params[:user].delete(:password) + params[:user].delete(:password_confirmation) + success = @user.update_without_password(user_params) + elsif @user.id == current_user.id + bypass_sign_in @user, scope: "user" + success = @user.update_with_password(user_params) + else + success = @user.update(user_params) + end + if success + redirect_to admin_users_path, notice: "User updated successfully" + else + render :edit, status: 422 + end + end + + private + + def site_list + @sites = Site.all.order(:location, :name).group_by(&:location).map do |location, sites| + [location, sites.map { |site| [site.name, site.id] }] + end + end + + def set_user + @user = params[:id].present? ? User.find(params[:id]) : User.new + end + + def user_params + params.require(:user).permit(:email, :password, :password_confirmation, :current_password, :is_site_admin, :is_user_admin, :site_id) + end + + def set_minimum_password_length + @minimum_password_length = User.password_length.min + end +end diff --git a/app/controllers/concerns/access.rb b/app/controllers/concerns/access.rb index 07554a99..cef179ea 100644 --- a/app/controllers/concerns/access.rb +++ b/app/controllers/concerns/access.rb @@ -1,18 +1,24 @@ module Access - def ensure_user_admin - unless current_user.is_admin? + def ensure_user_site_admin + unless current_user.is_site_admin? + redirect_to sites_path, alert: "You don't have permission to access that page." + end + end + + def ensure_user_user_admin + unless current_user.is_user_admin? redirect_to sites_path, alert: "You don't have permission to access that page." end end def ensure_user_site_access - if !current_user.is_admin? && (current_user.site.nil? || current_user.site.id != @site.id) + if !current_user.is_site_admin? && (current_user.site.nil? || current_user.site.id != @site.id) redirect_to sites_path, alert: "You don't have permission to access that site." end end def ensure_user_document_access - if !current_user.is_admin? && (current_user.site.nil? || current_user.site.documents.find(@document.id).nil?) + if !current_user.is_site_admin? && (current_user.site.nil? || current_user.site.documents.find(@document.id).nil?) redirect_to sites_path, alert: "You don't have permission to perform that action on document." end end diff --git a/app/controllers/configurations_controller.rb b/app/controllers/configurations_controller.rb index a516be67..bf13c4c5 100644 --- a/app/controllers/configurations_controller.rb +++ b/app/controllers/configurations_controller.rb @@ -1,7 +1,7 @@ class ConfigurationsController < AuthenticatedController include Access - before_action :ensure_user_admin + before_action :ensure_user_site_admin ASAP_API_USER = "/asap-pdf/production/RAILS_API_USER-20250613220933079900000001" ASAP_API_PASSWORD = "/asap-pdf/production/RAILS_API_PASSWORD-20250613220933080000000003" diff --git a/app/controllers/sites_controller.rb b/app/controllers/sites_controller.rb index 8aa19d16..fde11e78 100644 --- a/app/controllers/sites_controller.rb +++ b/app/controllers/sites_controller.rb @@ -6,7 +6,7 @@ class SitesController < AuthenticatedController before_action :ensure_user_site_access, only: [:insights, :show, :edit, :update, :destroy] def index - @sites = if current_user.is_admin? + @sites = if current_user.is_site_admin? Site.all else current_user.site.nil? ? [] : [current_user.site] diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index c266662e..b9e664fe 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class Users::RegistrationsController < Devise::RegistrationsController - layout "centered" # before_action :configure_sign_up_params, only: [:create] # before_action :configure_account_update_params, only: [:update] diff --git a/app/javascript/controllers/modal_controller.js b/app/javascript/controllers/modal_controller.js index 8304a8d3..37554b8f 100644 --- a/app/javascript/controllers/modal_controller.js +++ b/app/javascript/controllers/modal_controller.js @@ -5,7 +5,9 @@ export default class extends Controller { connect() { super.connect(); - this.wrapperTarget.addEventListener('close', this.onModalClose.bind(this)) + if (this.hasWrapperTarget) { + this.wrapperTarget.addEventListener('close', this.onModalClose.bind(this)) + } } submitAndClose(event) { diff --git a/app/views/admin/users/edit.html.erb b/app/views/admin/users/edit.html.erb new file mode 100644 index 00000000..607a53bb --- /dev/null +++ b/app/views/admin/users/edit.html.erb @@ -0,0 +1,82 @@ +
+
+

Edit <%= @user.email %>

+ + <%= form_with(model: [:admin, @user]) do |f| %> + +
+ <%= f.label :email, class: "label-text text-black mb-2 font-semibold" %> + <%= f.email_field :email, autofocus: true, autocomplete: "email", class: "input input-bordered w-full" + (@user.errors[:email].any? ? " input-error " : " border-black ") %> + <% if @user.errors[:email].any? %> +
+ <%= @user.errors.full_message(:email, @user.errors[:email].first) %> +
+ <% end %> +
+ + <% if @user.id == current_user.id %> +
+ <%= f.label :current_password, class: "label-text text-black mb-2 font-semibold" %> + <%= f.password_field :current_password, autocomplete: "current-password", class: "input input-bordered w-full" + (@user.errors[:current_password].any? ? " input-error " : " border-black ") %> + We need your current password to confirm any password changes. + <% if @user.errors[:current_password].any? %> +
+ <%= @user.errors.full_message(:current_password, @user.errors[:current_password].first) %> +
+ <% end %> +
+ <% end %> + +
+ <%= f.label :password, "New password", class: "label-text text-black mb-2 font-semibold" %> + <%= f.password_field :password, autocomplete: "new-password", class: "input input-bordered w-full" + (@user.errors[:password].any? ? " input-error " : " border-black ") %> + <% if @minimum_password_length %> + + <% if @minimum_password_length %><%= @minimum_password_length %> characters minimum.<% end %> Leave blank if you don't want to change it. + <% end %> + <% if @user.errors[:password].any? %> +
+ <%= @user.errors.full_message(:password, @user.errors[:password].first) %> +
+ <% end %> +
+ +
+ <%= f.label :password_confirmation, "New password confirmation", class: "label-text text-black mb-2 font-semibold" %> + <%= f.password_field :password_confirmation, autocomplete: "new-password", class: "input input-bordered w-full" + (@user.errors[:password_confirmation].any? ? " input-error " : " border-black ") %> + <% if @user.errors[:password_confirmation].any? %> +
+ <%= @user.errors.full_message(:password_confirmation, @user.errors[:password_confirmation].first) %> +
+ <% end %> +
+ +
+ <%= f.check_box :is_site_admin %> + <%= f.label :is_site_admin, class: "label-text text-black mb-2 font-semibold" %> +
User should be able to see and edit all sites and documents.
+
+ +
+ <%= f.check_box :is_user_admin %> + <%= f.label :is_user_admin, class: "label-text text-black mb-2 font-semibold" %> +
User should be able to edit other users and create admins.
+
+ +
+ <%= f.label :site_id, "Site", class: "label-text text-black mb-2 font-semibold" %> + <%= f.select :site_id, grouped_options_for_select(@sites, @user.site_id), + { include_blank: "None" }, + { class: "input input-bordered w-full select" } %> +
+ +
+ <%= button_tag type: "submit", class: "btn btn-primary text-white", id: "submit-user-form" do %> + + Update + <% end %> + <%= link_to "Back", :back %> +
+ <% end %> +
+
diff --git a/app/views/admin/users/index.html.erb b/app/views/admin/users/index.html.erb new file mode 100644 index 00000000..7cadcf62 --- /dev/null +++ b/app/views/admin/users/index.html.erb @@ -0,0 +1,41 @@ +
+
+
+
+
+

Manage Users

+ + + Add User + +
+
+
+ + + + + + + + + + + + <% @users.each do |user| %> + + + + + + + + <% end %> + +
EmailSiteSite AdminUser AdminActions
<%= user.email %><%= user.site.present? ? user.site.name : "None" %><%= user.is_site_admin? ? "Yes" : "No" %><%= user.is_user_admin? ? "Yes" : "No" %> + <%= link_to "Edit", edit_admin_user_path(user), class: "text-primary" %> +
+
+
+
+
\ No newline at end of file diff --git a/app/views/admin/users/new.html.erb b/app/views/admin/users/new.html.erb new file mode 100644 index 00000000..5bdec638 --- /dev/null +++ b/app/views/admin/users/new.html.erb @@ -0,0 +1,67 @@ +
+
+

Add New User

+ + <%= form_for(@user, url: admin_users_path) do |f| %> +
+ <%= f.label :email, class: "label-text text-black mb-2 font-semibold" %> + <%= f.email_field :email, autofocus: true, autocomplete: "email", class: "input input-bordered w-full" + (@user.errors[:email].any? ? " input-error " : " border-black ") %> + <% if @user.errors[:email].any? %> +
+ <%= @user.errors.full_message(:email, @user.errors[:email].first)%> +
+ <% end %> +
+ +
+ <%= f.label :password, class: "label-text text-black mb-2 font-semibold" %> + <%= f.password_field :password, autocomplete: "new-password", class: "input input-bordered w-full" + (@user.errors[:password].any? ? " input-error " : " border-black ") %> + <% if @minimum_password_length %> + <%= @minimum_password_length %> characters minimum. + <% end %> + <% if @user.errors[:password].any? %> +
+ <%= @user.errors.full_message(:password, @user.errors[:password].first)%> +
+ <% end %> +
+ +
+ <%= f.label :password_confirmation, class: "label-text text-black mb-2 font-semibold" %> + <%= f.password_field :password_confirmation, autocomplete: "new-password", class: "input input-bordered w-full" + (@user.errors[:password_confirmation].any? ? " input-error " : " border-black ") %> + <% if @user.errors[:password_confirmation].any? %> +
+ <%= @user.errors.full_message(:password_confirmation, @user.errors[:password_confirmation].first)%> +
+ <% end %> +
+ +
+ <%= f.check_box :is_site_admin %> + <%= f.label :is_site_admin, class: "label-text text-black mb-2 font-semibold" %> +
User should be able to see and edit all sites and documents.
+
+ +
+ <%= f.check_box :is_user_admin %> + <%= f.label :is_user_admin, class: "label-text text-black mb-2 font-semibold" %> +
User should be able to edit other users and create admins.
+
+ +
+ <%= f.label :site_id, "Site", class: "label-text text-black mb-2 font-semibold" %> + <%= f.select :site_id, grouped_options_for_select(@sites, @user.site_id), + { include_blank: "None" }, + { class: "input input-bordered w-full select" } %> +
+ +
+ <%= button_tag type: "submit", class: "btn btn-primary text-white", id: "submit-user-form" do %> + + Save + <% end %> + <%= link_to "Back", :back %> +
+ <% end %> +
+
\ No newline at end of file diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 1d01d144..26d65bdb 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -36,14 +36,22 @@ -<% if current_user.is_admin? %> +<% if current_user.is_site_admin? %>