Skip to content

Commit

Permalink
[To Main] Tenant CRUD implementation (#2524)
Browse files Browse the repository at this point in the history
* [To Feature Branch] - DESENG-605 - Tenant backend api (#2508)

* DESENG-605: Tenant API and Unit test

* DESENG-605 - Add Tenant - Frontend UI (#2510)

* DESENG-605 - Tenant detail page (#2511)

* DESENG-605: Tenant API and Unit test

* DESENG-605: Tenant detail page

* DESENG-606: Add tenant edit page (#2513)

* DESENG-605: Delete tenant flow

* DESENG-605 - Delete tenant functionality (#2515)

* DESENG-605 - Adding Unit test (#2518)

* DESENG-605: Tenant unit test for listing and detail

* Multi tenancy - UX Pass 1 (#2519)

* DESENG-605-606-unit-tests (#2522)

* Frontend unit tests

* Update changelog

---------

Co-authored-by: Ratheesh kumar R <[email protected]>
  • Loading branch information
NatSquared and ratheesh-aot authored May 24, 2024
1 parent 4993462 commit 0959d82
Show file tree
Hide file tree
Showing 51 changed files with 3,228 additions and 228 deletions.
20 changes: 17 additions & 3 deletions CHANGELOG.MD
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
## May 23, 2024

- **Feature** Finish tenant management UX [🎟️ DESENG-605](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-605), [🎟️ DESENG-606](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-606)
- Added a new tenant detail page
- Added a form for filling out tenant details
- Added a new tenant creation page
- Added a new tenant edit page

- **Feature** Add design system components to common components
- Added Table components, DetailsContainer, and Detail for layout
- Reworked button components to match the design system and support new colors
- Reworked modal components to match the design system
- Reworked image upload component to match the design system
- Added new form components that align with the design system

- **Bugfix** Security issue with email verification [🎟️ DESENG-618](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-618)
- Removed verification token from the response object
- Updated the test to reflect the change
Expand Down Expand Up @@ -29,13 +42,14 @@
## May 13, 2024

- **Bugfix** Date shown on rejection email preview is incorrect [🎟️ DESENG-597](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-597)

- Fixed the date formatting issue and using the end_date in the email preview
- Updated unit tests

- **Feature** Create role for metadata management [🎟️ DESENG-603](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-603)
- Implemented a new role named "manage_metadata" within the Admin group to restrci access for metadata management
- Updated the frontend to restrict access to the "metadata management" link in the menu for users without the newly added role
- Backend changes to incorporate the new role for access control purposes, ensuring only authorized users can perform metadata management actions
- Implemented a new role named "manage_metadata" within the Admin group to restrci access for metadata management
- Updated the frontend to restrict access to the "metadata management" link in the menu for users without the newly added role
- Backend changes to incorporate the new role for access control purposes, ensuring only authorized users can perform metadata management actions

## May 10, 2024

Expand Down
44 changes: 44 additions & 0 deletions met-api/migrations/versions/2c2ce3421cd6_tenant_table_update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""Tenant table update
Revision ID: 2c2ce3421cd6
Revises: 1407e0ad88f6
Create Date: 2024-05-14 11:45:22.800840
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '2c2ce3421cd6'
down_revision = '5388f257abfb'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('tenant', sa.Column('contact_name', sa.String(length=50), nullable=True, comment='Name of the primary contact'))
op.add_column('tenant', sa.Column('contact_email', sa.String(length=255), nullable=True, comment='Email of the primary contact'))
op.add_column('tenant', sa.Column('logo_credit', sa.String(length=60), nullable=True, comment='Hero banner image credit'))
op.add_column('tenant', sa.Column('logo_description', sa.String(length=80), nullable=True, comment='Hero banner image description'))
# Set the default values for existing rows
op.execute('UPDATE tenant SET contact_name = \'Default Contact Name\' WHERE contact_name IS NULL')
op.execute('UPDATE tenant SET contact_email = \'[email protected]\' WHERE contact_email IS NULL')
# Remove server_default after setting the default values
# future inserts into the table do not use these default values
op.alter_column('tenant', 'contact_name', server_default=None)
op.alter_column('tenant', 'contact_email', server_default=None)
# Alter the columns to set them as NOT NULL
op.alter_column('tenant', 'contact_name', nullable=False)
op.alter_column('tenant', 'contact_email', nullable=False)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('tenant', 'logo_description')
op.drop_column('tenant', 'logo_credit')
op.drop_column('tenant', 'contact_email')
op.drop_column('tenant', 'contact_name')
# ### end Alembic commands ###
28 changes: 28 additions & 0 deletions met-api/migrations/versions/ae232e299180_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""add unique constraint to tenant short_name
Revision ID: ae232e299180
Revises: 2c2ce3421cd6
Create Date: 2024-05-17 15:41:52.346746
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'ae232e299180'
down_revision = '2c2ce3421cd6'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_unique_constraint(None, 'tenant', ['short_name'])
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'tenant', type_='unique')
# ### end Alembic commands ###
43 changes: 41 additions & 2 deletions met-api/src/met_api/models/tenant.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Manages the tenants
"""
from __future__ import annotations

from typing import List, Optional
from .base_model import BaseModel
from .db import db

Expand All @@ -13,13 +13,52 @@ class Tenant(BaseModel):

__tablename__ = 'tenant'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
short_name = db.Column(db.String(10), comment='A small code for the tenant ie GDX , EAO.', nullable=False)
short_name = db.Column(db.String(10), nullable=False, unique=True,
comment='A small code for the tenant ie GDX , EAO.')
name = db.Column(db.String(50), comment='Full name of the ministry.ie Env Assessment Office')
contact_name = db.Column(
db.String(50), comment='Name of the primary contact', nullable=False, default='Default Contact Name'
)
contact_email = db.Column(
db.String(255), comment='Email of the primary contact', nullable=False, default='Default Contact Email'
)
description = db.Column(db.String(300))
title = db.Column(db.String(30), nullable=False)
logo_url = db.Column(db.String(300), nullable=True)
logo_credit = db.Column(db.String(60), comment='Hero banner image credit', nullable=True)
logo_description = db.Column(db.String(80), comment='Hero banner image description', nullable=True)

@staticmethod
def find_by_short_name(short_name: str) -> Tenant:
"""Find tenant using short name."""
return db.session.query(Tenant).filter(Tenant.short_name.ilike(short_name)).one_or_none()

@staticmethod
def find_by_id(identifier: int) -> Optional[Tenant]:
"""Find tenant by id."""
return db.session.query(Tenant).filter(Tenant.id == identifier).one_or_none()

@staticmethod
def find_all() -> List[Tenant]:
"""Return all tenants."""
return db.session.query(Tenant).all()

def save(self):
"""Save tenant to the database."""
db.session.add(self)
db.session.commit()

def delete(self):
"""Delete tenant from the database."""
if self.id is None or not Tenant.find_by_id(self.id):
raise ValueError('Tenant not found.')
db.session.delete(self)
db.session.commit()

def update(self, data: dict):
"""Update tenant with data."""
if self.id is None or not Tenant.find_by_id(self.id):
raise ValueError('Tenant not found.')
for key, value in data.items():
setattr(self, key, value)
db.session.commit()
65 changes: 60 additions & 5 deletions met-api/src/met_api/resources/tenant.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@

from http import HTTPStatus

from flask import request
from flask_cors import cross_origin
from flask_restx import Namespace, Resource
from marshmallow import ValidationError

from met_api.auth import auth
from met_api.auth import jwt as _jwt
from met_api.schemas import utils as schema_utils
from met_api.schemas.tenant import TenantSchema
from met_api.services.tenant_service import TenantService
from met_api.utils.roles import Role
from met_api.utils.tenant_validator import require_role
Expand Down Expand Up @@ -47,20 +52,70 @@ def get():
except ValueError as err:
return str(err), HTTPStatus.INTERNAL_SERVER_ERROR

@staticmethod
@cross_origin(origins=allowedorigins())
@_jwt.requires_auth
def post():
"""Create a new tenant."""
try:
request_json = request.get_json()
valid_format, errors = schema_utils.validate(request_json, 'tenant')
if not valid_format:
print(errors)
return {'message': schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST

@cors_preflight('GET OPTIONS')
@API.route('/<tenant_id>')
tenant = TenantSchema().load(request_json)
created_tenant = TenantService().create(tenant)
return created_tenant, HTTPStatus.CREATED
except (KeyError, ValueError) as err:
return str(err), HTTPStatus.INTERNAL_SERVER_ERROR
except ValidationError as err:
return str(err.messages), HTTPStatus.INTERNAL_SERVER_ERROR


@cors_preflight('GET PATCH DELETE OPTIONS')
@API.route('/<tenant_short_name>')
class Tenant(Resource):
"""Resource for managing a single tenant."""

@staticmethod
@cross_origin(origins=allowedorigins())
@auth.optional
def get(tenant_id):
def get(tenant_short_name: str):
"""Fetch a tenant."""
try:
tenant = TenantService().get(tenant_id)

tenant = TenantService().get(tenant_short_name)
return tenant, HTTPStatus.OK

except ValueError as err:
return str(err), HTTPStatus.INTERNAL_SERVER_ERROR

@staticmethod
@cross_origin(origins=allowedorigins())
@_jwt.requires_auth
def delete(tenant_short_name: str):
"""Delete a tenant."""
try:
TenantService().delete(tenant_short_name)
return {'status': 'success', 'message': 'Tenant deleted successfully'}, HTTPStatus.OK
except KeyError as err:
return str(err), HTTPStatus.INTERNAL_SERVER_ERROR
except ValueError as err:
return str(err), HTTPStatus.INTERNAL_SERVER_ERROR

@staticmethod
@cross_origin(origins=allowedorigins())
@_jwt.requires_auth
def patch(tenant_short_name: str):
"""Update a tenant."""
try:
request_json = request.get_json()
valid_format, errors = schema_utils.validate(request_json, 'tenant_update')
if not valid_format:
return {'message': schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST
updated_tenant = TenantService().update(tenant_short_name, request_json)
return updated_tenant, HTTPStatus.OK
except (KeyError, ValueError) as err:
return str(err), HTTPStatus.INTERNAL_SERVER_ERROR
except ValidationError as err:
return str(err.messages), HTTPStatus.INTERNAL_SERVER_ERROR
95 changes: 95 additions & 0 deletions met-api/src/met_api/schemas/schemas/tenant.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://met.gov.bc.ca/.well_known/schemas/tenant",
"type": "object",
"title": "Tenant Schema",
"description": "The schema for a tenant.",
"default": {},
"examples": [
{
"short_name": "GDX",
"name": "Government Digital Experience",
"contact_name": "John Doe",
"contact_email": "[email protected]",
"title": "Modern Engagement",
"description": "Responsible for digital experience projects.",
"logo_url": "https://example.com/logo.png",
"logo_credit": "Logo by Example",
"logo_description": "Official logo of the Government Digital Experience."
}
],
"required": ["short_name", "contact_name", "contact_email", "title"],
"properties": {
"id": {
"$id": "#/properties/id",
"type": "integer",
"title": "Tenant ID",
"description": "The unique identifier for the tenant.",
"examples": [1]
},
"short_name": {
"$id": "#/properties/short_name",
"type": "string",
"title": "Short Name",
"description": "A small code for the tenant, e.g., GDX, EAO.",
"examples": ["GDX"]
},
"name": {
"$id": "#/properties/name",
"type": "string",
"title": "Full Name",
"description": "Full name of the tenant or ministry, e.g., Government Digital Experience.",
"examples": ["Government Digital Experience"]
},
"contact_name": {
"$id": "#/properties/contact_name",
"type": "string",
"title": "Contact Name",
"description": "Name of the primary contact.",
"examples": ["John Doe"]
},
"contact_email": {
"$id": "#/properties/contact_email",
"type": "string",
"title": "Contact Email",
"description": "Email of the primary contact.",
"examples": ["[email protected]"]
},
"title": {
"$id": "#/properties/title",
"type": "string",
"title": "Title",
"description": "Title of the tenant's website.",
"examples": ["Modern Engagement"]
},
"description": {
"$id": "#/properties/description",
"type": "string",
"title": "Description",
"description": "A brief description of the tenant.",
"examples": ["Responsible for digital experience projects."]
},
"logo_url": {
"$id": "#/properties/logo_url",
"type": "string",
"title": "Logo URL",
"description": "URL to the tenant's logo.",
"examples": ["https://example.com/logo.png"]
},
"logo_credit": {
"$id": "#/properties/logo_credit",
"type": "string",
"title": "Logo Credit",
"description": "Credit for the logo image.",
"examples": ["Logo by Example"]
},
"logo_description": {
"$id": "#/properties/logo_description",
"type": "string",
"title": "Logo Description",
"description": "Description of the logo image.",
"examples": ["Official logo of the Government Digital Experience."]
}
}
}

Loading

0 comments on commit 0959d82

Please sign in to comment.