\ No newline at end of file
diff --git a/training-front-end/src/components/AdminEditUserDetails.vue b/training-front-end/src/components/AdminEditUserDetails.vue
new file mode 100644
index 00000000..62cb3aa5
--- /dev/null
+++ b/training-front-end/src/components/AdminEditUserDetails.vue
@@ -0,0 +1,229 @@
+
+
+
+
diff --git a/training-front-end/src/components/ValidatedInput.vue b/training-front-end/src/components/ValidatedInput.vue
index 53cbf089..3b635044 100644
--- a/training-front-end/src/components/ValidatedInput.vue
+++ b/training-front-end/src/components/ValidatedInput.vue
@@ -51,7 +51,7 @@
{
it('displays the basic user information', async () => {
const props = {user: users[0]}
const wrapper = mount(AdminEditReporting, {props})
- const name = wrapper.find('input[id="input-full-name"]')
- const email = wrapper.find('input[id="input-email"]')
- const agency = wrapper.find('input[id="input-agency"]')
- const bureau = wrapper.find('input[id="input-bureau"]')
-
- expect(name.element.value).toBe('Hugh Steeply')
- expect(email.element.value).toBe('helen.steeply@ous.gov')
- expect(agency.element.value).toBe('Office of Unspecified Services')
- expect(bureau.element.value).toBe('Secret Service')
+ const name = wrapper.find('dd[id="user-name-value"]')
+ const email = wrapper.find('dd[id="user-email-value"]')
+ const agency = wrapper.find('dd[id="user-agency-organization-value"]')
+ const bureau = wrapper.find('dd[id="user-bureau-value"]')
+
+ expect(name.text()).toContain('Hugh Steeply')
+ expect(email.text()).toContain('helen.steeply@ous.gov')
+ expect(agency.text()).toContain('Office of Unspecified Services')
+ expect(bureau.text()).toContain('Secret Service')
})
it('displays table with header and user reporting agencies', async () => {
diff --git a/training-front-end/src/content/training_purchase/lesson05.mdx b/training-front-end/src/content/training_purchase/lesson05.mdx
index 12503c61..576fad89 100644
--- a/training-front-end/src/content/training_purchase/lesson05.mdx
+++ b/training-front-end/src/content/training_purchase/lesson05.mdx
@@ -119,12 +119,15 @@ When a convenience check is used to purchase services, the Internal Revenue Serv
The IRS states that agencies may rely on the merchant category code (MCC) in determining whether a transaction is subject to Form 1099 reporting. Failure to file a correct information return [Form 1099](https://www.irs.gov/forms-pubs/about-form-1099-misc) by the due date may result in a penalty imposed by the IRS.
## What are Government-to-Government transactions?
-Government-to-government transactions are payments between different agencies (inter-governmental) or payments within the same agency (intra-governmental). In most instances, these transactions are classified under Merchant Category Code 9399, Miscellaneous Government Services.
+Government-to-government -- also referred to as "intra-governmental" -- transactions are payments between government entities, including within the same agency. These transactions may be classified under Merchant Category Code 9399, Miscellaneous Government Services.
-Effective October 1, 2022, the general limit on charge card payments within the same agency (intra-governmental) is set at $9,999.99. The general limit on charge card payments between different federal agencies (inter-governmental) remains $24,999.99. The government’s card acceptance policies can be found in [Treasury Financial Manual (TFM) Vol. I, Part 5, Chapter 7000](https://tfm.fiscal.treasury.gov/v1/p5/c700.html). It also addresses limitations on credit card transactions.
+Effective October 1, 2022, the following apply:
-A few notes to keep in mind:
+ - The inter-governmental transaction card limit is $9,999.99, meaning no individual card transaction can exceed this limit.
+ - The maximum daily limit from a single payor is $24,999.99.
+ - Total monthly transactions, based on a 30 day rolling period, from a single payor can be no more than $100,000.00.
-- Customers cannot divide an inter-governmental transaction into smaller pieces to evade this limit. For example, a buyer cannot make two purchases of $15,000 to avoid the $24,999.99 limit.
-- Customers cannot divide an intra-governmental transaction into smaller pieces to evade this limit. For example, a buyer cannot make two purchases of $6,000 to avoid the $9,999.99 limit.
-- Treasury retains the option to change limits. One goal is to reduce the fees the government pays when it accepts the purchase card/account for large transactions.
+The government’s card acceptance policies can be found in [Treasury Financial Manual (TFM) Vol. I, Part 5, Chapter 7000](https://tfm.fiscal.treasury.gov/v1/p5/c700). It also addresses limitations on credit card transactions. Additionally, consider the following:
+
+ - Customers cannot divide transactions into smaller pieces to evade these limits.
+ - Treasury retains the option to change limits. One goal is to reduce the fees the government pays when it accepts the purchase card/account for large transactions.
\ No newline at end of file
diff --git a/training-front-end/src/content/training_purchase_pc/lesson03.mdx b/training-front-end/src/content/training_purchase_pc/lesson03.mdx
index 525576dd..dcba97a9 100644
--- a/training-front-end/src/content/training_purchase_pc/lesson03.mdx
+++ b/training-front-end/src/content/training_purchase_pc/lesson03.mdx
@@ -111,11 +111,16 @@ When a convenience check is used to purchase services, the Internal Revenue Serv
The IRS states that agencies may rely on the merchant category code (MCC) in determining whether a transaction is subject to Form 1099 reporting. Failure to file a correct information return ([Form 1099](https://www.irs.gov/forms-pubs/about-form-1099-misc)) by the due date may result in a penalty imposed by the IRS.
## What are Government-to-Government transactions?
-Government-to-government transactions are payments between different agencies (inter-governmental) or payments within the same agency (intra-governmental). In most instances, these transactions are classified under Merchant Category Code 9399, Miscellaneous Government Services.
+Government-to-government -- also referred to as "intra-governmental" -- transactions are payments between government entities, including within the same agency. These transactions may be classified under Merchant Category Code 9399, Miscellaneous Government Services.
-Effective October 1, 2022, the general limit on charge card payments within the same agency (intra-governmental) is set at $9,999.99. The general limit on charge card payments between different federal agencies (inter-governmental) remains $24,999.99. The government’s card acceptance policies can be found in [Treasury Financial Manual (TFM) Vol. I, Part 5, Chapter 7000](https://tfm.fiscal.treasury.gov/v1/p5/c700.html). It also addresses limitations on credit card transactions.
+Effective October 1, 2022, the following apply:
+
+ - The inter-governmental transaction card limit is $9,999.99, meaning no individual card transaction can exceed this limit.
+ - The maximum daily limit from a single payor is $24,999.99.
+ - Total monthly transactions, based on a 30 day rolling period, from a single payor can be no more than $100,000.00.
+
+The government’s card acceptance policies can be found in [Treasury Financial Manual (TFM) Vol. I, Part 5, Chapter 7000](https://tfm.fiscal.treasury.gov/v1/p5/c700). It also addresses limitations on credit card transactions. Additionally, consider the following:
+
+ - Customers cannot divide transactions into smaller pieces to evade these limits.
+ - Treasury retains the option to change limits. One goal is to reduce the fees the government pays when it accepts the purchase card/account for large transactions.
-A few notes to keep in mind:
-- Customers cannot divide an inter-governmental transaction into smaller pieces to evade this limit. For example, a buyer cannot make two purchases of $15,000 to avoid the $24,999.99 limit.
-- Customers cannot divide an intra-governmental transaction into smaller pieces to evade this limit. For example, a buyer cannot make two purchases of $6,000 to avoid the $9,999.99 limit.
-- Treasury retains the option to change limits. One goal is to reduce the fees the government pays when it accepts the purchase card/account for large transactions.
diff --git a/training-front-end/src/pages/admin/index.astro b/training-front-end/src/pages/admin/index.astro
index 908c9392..879758b8 100644
--- a/training-front-end/src/pages/admin/index.astro
+++ b/training-front-end/src/pages/admin/index.astro
@@ -27,7 +27,7 @@ const pageTitle = "Admin Panel";
diff --git a/training/api/api_v1/users.py b/training/api/api_v1/users.py
index 630cbca6..359ed8be 100644
--- a/training/api/api_v1/users.py
+++ b/training/api/api_v1/users.py
@@ -3,7 +3,7 @@
import logging
from training.api.auth import RequireRole
from fastapi import APIRouter, status, HTTPException, Response, Depends, Query
-from training.schemas import User, UserCreate, UserSearchResult
+from training.schemas import User, UserCreate, UserSearchResult, UserUpdate
from training.repositories import UserRepository
from training.api.deps import user_repository
from training.api.auth import user_from_form
@@ -14,9 +14,9 @@
@router.post("/users", response_model=User, status_code=status.HTTP_201_CREATED)
def create_user(
- new_user: UserCreate,
- repo: UserRepository = Depends(user_repository),
- user=Depends(RequireRole(["Admin"]))
+ new_user: UserCreate,
+ repo: UserRepository = Depends(user_repository),
+ user=Depends(RequireRole(["Admin"]))
):
db_user = repo.find_by_email(new_user.email)
if db_user:
@@ -31,10 +31,10 @@ def create_user(
@router.patch("/users/edit-user-for-reporting", response_model=User)
def edit_user_for_reporting(
- user_id: int,
- agency_id_list: list[int],
- repo: UserRepository = Depends(user_repository),
- user=Depends(RequireRole(["Admin"]))
+ user_id: int,
+ agency_id_list: list[int],
+ repo: UserRepository = Depends(user_repository),
+ user=Depends(RequireRole(["Admin"]))
):
try:
updated_user = repo.edit_user_for_reporting(user_id, agency_id_list)
@@ -72,10 +72,10 @@ def download_report_csv(user=Depends(user_from_form), repo: UserRepository = Dep
@router.get("/users", response_model=UserSearchResult)
def get_users(
- searchText: Annotated[str, Query(min_length=1)],
- page_number: int = 1,
- repo: UserRepository = Depends(user_repository),
- user=Depends(RequireRole(["Admin"]))
+ searchText: Annotated[str, Query(min_length=1)],
+ page_number: int = 1,
+ repo: UserRepository = Depends(user_repository),
+ user=Depends(RequireRole(["Admin"]))
):
'''
Get/users is used to search users for admin portal
@@ -85,3 +85,33 @@ def get_users(
It returns UserSearchResult object with a list of users and total_count used for UI pagination
'''
return repo.get_users(searchText, page_number)
+
+
+@router.patch("/users/{user_id}", response_model=User)
+def update_user_by_id(
+ user_id: int,
+ updated_user: UserUpdate,
+ repo: UserRepository = Depends(user_repository),
+ user=Depends(RequireRole(["Admin"]))
+):
+ """
+ Updates user details by User ID
+ :param updated_user: Updated user model
+ :param user_id: User ID
+ :param repo: UserRepository repository
+ :param user: Required role to complete operation
+ :return: Returns the updated user object
+ """
+ if user["id"] == user_id:
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Can not update your own profile"
+ )
+ try:
+ logging.info(f"{user['email']} updated user {updated_user.email} user profile")
+ return repo.update_user(user_id, updated_user)
+ except ValueError:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="invalid user id"
+ )
diff --git a/training/repositories/user.py b/training/repositories/user.py
index 9f946a03..9f62e903 100644
--- a/training/repositories/user.py
+++ b/training/repositories/user.py
@@ -76,3 +76,18 @@ def get_users(self, searchText: str, page_number: int) -> UserSearchResult:
or_(models.User.name.ilike(f"%{searchText}%"), models.User.email.ilike(f"%{searchText}%"))).limit(page_size).offset(offset).all()
user_search_result = UserSearchResult(users=search_results, total_count=count)
return user_search_result
+
+ def update_user(self, user_id: int, user: schemas.UserUpdate) -> models.User:
+ """
+ Updates user name and agency values
+ :param user_id: User's ID to update
+ :param user: User object with updated values
+ :return: Updated User object
+ """
+ db_user = self.find_by_id(user_id)
+ if db_user is None:
+ raise ValueError("invalid user id")
+ db_user.name = user.name
+ db_user.agency_id = user.agency_id
+ self._session.commit()
+ return db_user
diff --git a/training/schemas/__init__.py b/training/schemas/__init__.py
index c862d76a..ab426a90 100644
--- a/training/schemas/__init__.py
+++ b/training/schemas/__init__.py
@@ -1,6 +1,6 @@
from .agency import Agency, AgencyCreate, AgencyWithBureaus
from .temp_user import TempUser, IncompleteTempUser, WebDestination
-from .user import User, UserCreate, UserQuizCompletionReportData, UserSearchResult, UserJWT
+from .user import User, UserCreate, UserQuizCompletionReportData, UserSearchResult, UserJWT, UserUpdate
from .quiz_choice import QuizChoice, QuizChoiceCreate, QuizChoicePublic
from .quiz_question import QuizQuestion, QuizQuestionCreate, QuizQuestionPublic, QuizQuestionType
from .quiz_content import QuizContent, QuizContentCreate, QuizContentPublic
diff --git a/training/schemas/user.py b/training/schemas/user.py
index d4bd44f7..99296dc4 100644
--- a/training/schemas/user.py
+++ b/training/schemas/user.py
@@ -14,6 +14,10 @@ class UserCreate(UserBase):
pass
+class UserUpdate(UserBase):
+ agency_id: int
+
+
class User(UserBase):
id: int
agency_id: int
@@ -24,6 +28,7 @@ class User(UserBase):
def is_admin(self) -> bool:
role_names = [role.name.upper() for role in self.roles]
return "Admin".upper() in role_names
+
model_config = ConfigDict(from_attributes=True)
diff --git a/training/tests/test_api_users.py b/training/tests/test_api_users.py
index b079efe8..4e07b2c6 100644
--- a/training/tests/test_api_users.py
+++ b/training/tests/test_api_users.py
@@ -13,6 +13,7 @@
@pytest.fixture
def admin_user():
return {
+ 'id': 1,
'name': 'Albus Dumbledore',
'email': 'dumbledore@hogwarts.edu',
'roles': ['Admin']
@@ -86,3 +87,33 @@ def test_edit_user_for_reporting(mock_user_repo: UserRepository, goodJWT: str):
assert response.status_code == status.HTTP_200_OK
assert role.model_dump() in response.json()["roles"]
assert agency.model_dump() in response.json()["report_agencies"]
+
+
+def test_edit_user_details(mock_user_repo: UserRepository, goodJWT: str):
+ updated_user = UserSchemaFactory.build()
+ updated_user.name = "some name"
+ updated_user.agency_id = 1
+ mock_user_repo.update_user.return_value = updated_user
+ user_id = updated_user.id
+ URL = f"/api/v1/users/{user_id}"
+ response = client.patch(
+ URL,
+ json=updated_user.model_dump(),
+ headers={"Authorization": f"Bearer {goodJWT}"}
+ )
+ assert response.status_code == status.HTTP_200_OK
+ assert response.json()["name"] == updated_user.name
+ assert response.json()["agency_id"] == updated_user.agency_id
+
+
+def test_edit_user_details_same_user(mock_user_repo: UserRepository, goodJWT: str):
+ updated_user = UserSchemaFactory.build()
+ mock_user_repo.update_user.return_value = updated_user
+ user_id = 1
+ URL = f"/api/v1/users/{user_id}"
+ response = client.patch(
+ URL,
+ json=updated_user.model_dump(),
+ headers={"Authorization": f"Bearer {goodJWT}"}
+ )
+ assert response.status_code == status.HTTP_403_FORBIDDEN
diff --git a/training/tests/test_user_repository.py b/training/tests/test_user_repository.py
index a6905998..f357190f 100644
--- a/training/tests/test_user_repository.py
+++ b/training/tests/test_user_repository.py
@@ -117,3 +117,31 @@ def test_get_users_by_email(user_repo_with_data: UserRepository, valid_user_ids:
assert result is not None
for item in result.users:
assert search_criteria in item.email
+
+
+def test_update_user_passing(user_repo_with_data: UserRepository, agency_repo_with_data: AgencyRepository):
+ agency_id = agency_repo_with_data.find_all()[0].id
+ user_id = user_repo_with_data.find_all()[0]
+ updated_user = models.User(
+ email="updateduser@example.com",
+ name="Updated User",
+ agency_id=agency_id,
+ )
+
+ result = user_repo_with_data.update_user(user_id.id, updated_user)
+ assert result is not None
+ assert result.name == "Updated User"
+ assert result.agency_id == agency_id
+ assert result.email != "updateduser@example.com"
+
+
+def test_update_user_invalid_user(user_repo_with_data: UserRepository):
+ invalid_user_id = 0
+ updated_user = models.User(
+ email="updateduser@example.com",
+ name="Updated User",
+ agency_id=1,
+ )
+
+ with pytest.raises(Exception):
+ user_repo_with_data.update_user(invalid_user_id, updated_user)