diff --git a/CHANGELOG.md b/CHANGELOG.md index a4eb5da4c..0bdaab046 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Tooltips to main side navigation. - Location hash for tabs in borehole detail view. - Language dropdown in the header. +- Added health check endpoint for the .NET API. ### Changed @@ -23,6 +24,7 @@ - Removed unused `IsViewer` flag from user. - Removed unused `UserEvent` from user. - Migrated `User` API endpoints to .NET API. +- Migrated `Workgroup` API endpoints to .NET API. - Use `filled` style for form components. ### Fixed diff --git a/src/api-legacy/Dockerfile b/src/api-legacy/Dockerfile index b283fe890..9246e6889 100644 --- a/src/api-legacy/Dockerfile +++ b/src/api-legacy/Dockerfile @@ -8,12 +8,12 @@ COPY . ./bms ARG VERSION ARG REVISION -ENV APP_VERSION ${VERSION} -ENV APP_REVISION ${REVISION} +ENV APP_VERSION=${VERSION} +ENV APP_REVISION=${REVISION} -CMD python -u bms/main.py \ +CMD ["/bin/sh", "-c", "python -u bms/main.py \ --pg-host=${DB_HOST} \ --pg-port=${DB_PORT} \ --pg-database=${DB_DATABASE} \ --pg-user=${DB_USERNAME} \ - --pg-password=${DB_PASSWORD} + --pg-password=${DB_PASSWORD}"] diff --git a/src/api-legacy/__init__.py b/src/api-legacy/__init__.py index cbde09dff..78f5d9f9e 100644 --- a/src/api-legacy/__init__.py +++ b/src/api-legacy/__init__.py @@ -62,8 +62,3 @@ # User actions from bms.v1.user.handler import UserHandler - -# Workgroup actions -from bms.v1.user.workgrpup.admin import WorkgroupAdminHandler -from bms.v1.user.workgrpup import ListWorkgroups -from bms.v1.user.workgrpup import CreateWorkgroup diff --git a/src/api-legacy/main.py b/src/api-legacy/main.py index 6a1a4dcc5..aaf5f536e 100644 --- a/src/api-legacy/main.py +++ b/src/api-legacy/main.py @@ -88,7 +88,6 @@ async def close(application): # user handlers SettingHandler, UserHandler, - WorkgroupAdminHandler, # Borehole handlers BoreholeViewerHandler, @@ -132,8 +131,6 @@ async def close(application): # User handlers (r'/api/v1/user', UserHandler), - (r'/api/v1/user/workgroup/edit', WorkgroupAdminHandler), - # Borehole handlers (r'/api/v1/borehole', BoreholeViewerHandler), (r'/api/v1/borehole/edit', BoreholeProducerHandler), diff --git a/src/api-legacy/v1/user/workgrpup/__init__.py b/src/api-legacy/v1/user/workgrpup/__init__.py deleted file mode 100644 index 4d253e408..000000000 --- a/src/api-legacy/v1/user/workgrpup/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# -*- coding: utf-8 -*- - -from bms.v1.user.workgrpup.create import CreateWorkgroup -from bms.v1.user.workgrpup.delete import DeleteWorkgroup -from bms.v1.user.workgrpup.disable import DisableWorkgroup -from bms.v1.user.workgrpup.enable import EnableWorkgroup -from bms.v1.user.workgrpup.list import ListWorkgroups -from bms.v1.user.workgrpup.role import SetRole -from bms.v1.user.workgrpup.update import UpdateWorkgroup diff --git a/src/api-legacy/v1/user/workgrpup/admin.py b/src/api-legacy/v1/user/workgrpup/admin.py deleted file mode 100644 index 3a0a70958..000000000 --- a/src/api-legacy/v1/user/workgrpup/admin.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- coding: utf-8 -*- -from bms import ( - AuthorizationException -) -from bms.v1.handlers.admin import Admin -from bms.v1.user.workgrpup import ( - CreateWorkgroup, - DeleteWorkgroup, - DisableWorkgroup, - EnableWorkgroup, - ListWorkgroups, - CreateWorkgroup, - SetRole, - UpdateWorkgroup -) - - -class WorkgroupAdminHandler(Admin): - async def execute(self, request): - - action = request.pop('action', None) - - if action in [ - 'CREATE', - 'DISABLE', - 'DELETE', - 'ENABLE', - 'LIST', - 'SET', - 'UPDATE' - ]: - - async with self.pool.acquire() as conn: - - exe = None - - if action in [ - 'CREATE', - 'DELETE', - 'DISABLE', - 'ENABLE', - 'LIST', - 'SET', - 'UPDATE' - ]: - if self.user['admin'] is False: - raise AuthorizationException() - - if action == 'LIST': - exe = ListWorkgroups(conn) - - elif action == 'CREATE': - exe = CreateWorkgroup(conn) - - elif action == 'SET': - exe = SetRole(conn) - - elif action == 'DISABLE': - exe = DisableWorkgroup(conn) - - elif action == 'ENABLE': - exe = EnableWorkgroup(conn) - - elif action == 'DELETE': - exe = DeleteWorkgroup(conn) - - elif action == 'UPDATE': - exe = UpdateWorkgroup(conn) - - if exe is not None: - return ( - await exe.execute(**request) - ) - - raise Exception("Action '%s' unknown" % action) diff --git a/src/api-legacy/v1/user/workgrpup/create.py b/src/api-legacy/v1/user/workgrpup/create.py deleted file mode 100644 index 828e5ad14..000000000 --- a/src/api-legacy/v1/user/workgrpup/create.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- -from bms import ( - DuplicateException -) -from bms.v1.action import Action - - -class CreateWorkgroup(Action): - - async def execute(self, name, is_supplier = False): - exists = await self.conn.fetchval( - """ - SELECT EXISTS( - SELECT 1 - FROM - bdms.workgroups - WHERE - name_wgp = $1 - ); - """, name) - if exists: - raise DuplicateException() - - return { - "id": ( - await self.conn.fetchval(""" - INSERT INTO bdms.workgroups( - name_wgp, - settings_wgp, - supplier_wgp - ) - VALUES ( - $1, - '{}', - $2 - ) - RETURNING id_wgp - """, - name, is_supplier - ) - ) - } diff --git a/src/api-legacy/v1/user/workgrpup/delete.py b/src/api-legacy/v1/user/workgrpup/delete.py deleted file mode 100644 index 6c77e4232..000000000 --- a/src/api-legacy/v1/user/workgrpup/delete.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -from bms.v1.action import Action - - -class DeleteWorkgroup(Action): - - async def execute(self, id): - - # Check if user has done contributions - boreholes = await self.conn.fetchval(""" - SELECT - count(id_bho) as boreholes - - FROM bdms.workgroups - - LEFT JOIN bdms.borehole - ON id_wgp = id_wgp_fk - - WHERE - id_wgp = $1 - - GROUP BY - id_wgp - """, id) - - if boreholes > 0: - raise Exception( - f"Workgroup cannot be deleted because of {boreholes} boreholes" - ) - - await self.conn.execute(""" - DELETE FROM bdms.workgroups - WHERE id_wgp = $1 - """, id) - - return None diff --git a/src/api-legacy/v1/user/workgrpup/disable.py b/src/api-legacy/v1/user/workgrpup/disable.py deleted file mode 100644 index 174b01f55..000000000 --- a/src/api-legacy/v1/user/workgrpup/disable.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: utf-8 -*- -from bms.v1.action import Action - - -class DisableWorkgroup(Action): - - async def execute(self, id): - return { - "id": ( - await self.conn.fetchval(""" - UPDATE - bdms.workgroups - - SET - disabled_wgp = now() - - WHERE - id_wgp = $1 - """, - id - ) - ) - } diff --git a/src/api-legacy/v1/user/workgrpup/enable.py b/src/api-legacy/v1/user/workgrpup/enable.py deleted file mode 100644 index cb770cf0c..000000000 --- a/src/api-legacy/v1/user/workgrpup/enable.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: utf-8 -*- -from bms.v1.action import Action - - -class EnableWorkgroup(Action): - - async def execute(self, id): - return { - "id": ( - await self.conn.fetchval(""" - UPDATE - bdms.workgroups - - SET - disabled_wgp = NULL - - WHERE - id_wgp = $1 - """, - id - ) - ) - } diff --git a/src/api-legacy/v1/user/workgrpup/list.py b/src/api-legacy/v1/user/workgrpup/list.py deleted file mode 100644 index 47b1301ed..000000000 --- a/src/api-legacy/v1/user/workgrpup/list.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- -from bms.v1.action import Action -import math - - -class ListWorkgroups(Action): - - async def execute(self): - - val = await self.conn.fetchval( - """ - SELECT - array_to_json( - array_agg( - row_to_json(t) - ) - ) - FROM ( - SELECT - id_wgp as id, - name_wgp as name, - supplier_wgp as supplier, - to_char( - disabled_wgp, - 'YYYY-MM-DD"T"HH24:MI:SSOF' - ) as disabled, - to_char( - created_wgp, - 'YYYY-MM-DD"T"HH24:MI:SSOF' - ) as created, - count(id_bho) as boreholes - - FROM bdms.workgroups - - LEFT JOIN bdms.borehole - ON id_wgp = id_wgp_fk - - /*WHERE - supplier_wgp IS FALSE*/ - - GROUP BY - id_wgp, name_wgp - - ORDER BY - name_wgp - ) as t - """ - ) - - return { - "data": self.decode(val) if val is not None else [] - } diff --git a/src/api-legacy/v1/user/workgrpup/role.py b/src/api-legacy/v1/user/workgrpup/role.py deleted file mode 100644 index 6badff3ec..000000000 --- a/src/api-legacy/v1/user/workgrpup/role.py +++ /dev/null @@ -1,58 +0,0 @@ -# -*- coding: utf-8 -*- -from bms.v1.action import Action - - -class SetRole(Action): - - async def execute( - self, user_id, workgroup_id, role_name, active = True - ): - id_rol = await self.conn.fetchval(""" - SELECT - id_rol - FROM - bdms.roles - WHERE - name_rol = $1 - """, role_name) - - if id_rol is None: - raise Exception(f"Not found {role_name}") - - if active is False: - await self.conn.execute(""" - DELETE FROM - bdms.users_roles - WHERE - id_usr_fk = $1 - AND - id_rol_fk = $2 - AND - id_wgp_fk = $3 - """, user_id, id_rol, workgroup_id) - - else: - # Check if ROLE (role_name) already assigned - val = await self.conn.fetchrow(""" - SELECT - id_usr_fk, - id_rol_fk, - id_wgp_fk - FROM - bdms.users_roles - WHERE - id_usr_fk = $1 - AND - id_rol_fk = $2 - AND - id_wgp_fk = $3 - """, user_id, id_rol, workgroup_id) - - if val is None: - await self.conn.execute(""" - INSERT INTO bdms.users_roles( - id_usr_fk, id_rol_fk, id_wgp_fk) - VALUES ($1, $2, $3); - """, user_id, id_rol, workgroup_id) - - return None diff --git a/src/api-legacy/v1/user/workgrpup/update.py b/src/api-legacy/v1/user/workgrpup/update.py deleted file mode 100644 index ba9bc07c4..000000000 --- a/src/api-legacy/v1/user/workgrpup/update.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: utf-8 -*- -from bms.v1.action import Action - - -class UpdateWorkgroup(Action): - - async def execute(self, id, name): - return { - "id": ( - await self.conn.fetchval(""" - UPDATE - bdms.workgroups - - SET - name_wgp = $1 - - WHERE - id_wgp = $2 - """, - name, id - ) - ) - } diff --git a/src/api/BDMS.csproj b/src/api/BDMS.csproj index 4f9574205..3f6f80b9c 100644 --- a/src/api/BDMS.csproj +++ b/src/api/BDMS.csproj @@ -25,6 +25,7 @@ + diff --git a/src/api/BdmsContext.cs b/src/api/BdmsContext.cs index e700a9062..476fdb965 100644 --- a/src/api/BdmsContext.cs +++ b/src/api/BdmsContext.cs @@ -26,6 +26,10 @@ public class BdmsContext : DbContext public DbSet UserWorkgroupRoles { get; set; } public DbSet Workflows { get; set; } public DbSet Workgroups { get; set; } + + public IQueryable WorkgroupsWithIncludes => Workgroups + .Include(w => w.Boreholes); + public DbSet BoreholeFiles { get; set; } public DbSet LithologicalDescriptions { get; set; } public DbSet FaciesDescriptions { get; set; } diff --git a/src/api/BdmsContextExtensions.cs b/src/api/BdmsContextExtensions.cs index 783fce71b..dc6bd7864 100644 --- a/src/api/BdmsContextExtensions.cs +++ b/src/api/BdmsContextExtensions.cs @@ -45,8 +45,8 @@ public static void SeedData(this BdmsContext context) .StrictMode(true) .RuleFor(o => o.Id, f => workgroup_ids++) .RuleFor(o => o.Name, f => f.Music.Genre()) - .RuleFor(o => o.Created, f => f.Date.Past().ToUniversalTime().OrNull(f, .1f)) - .RuleFor(o => o.Disabled, f => f.Date.Past().ToUniversalTime().OrNull(f, .1f)) + .RuleFor(o => o.CreatedAt, f => f.Date.Past().ToUniversalTime().OrNull(f, .1f)) + .RuleFor(o => o.DisabledAt, f => f.Date.Past().ToUniversalTime().OrNull(f, .1f)) .RuleFor(o => o.IsSupplier, f => f.Random.Bool().OrNull(f, .1f)) .RuleFor(o => o.Settings, f => null) .RuleFor(o => o.Boreholes, _ => default!); diff --git a/src/api/Controllers/BackfillController.cs b/src/api/Controllers/BackfillController.cs index ba34baa30..f14e580f0 100644 --- a/src/api/Controllers/BackfillController.cs +++ b/src/api/Controllers/BackfillController.cs @@ -8,7 +8,7 @@ namespace BDMS.Controllers; [ApiController] [Route("api/v{version:apiVersion}/[controller]")] -public class BackfillController : BdmsControllerBase +public class BackfillController : BoreholeControllerBase { public BackfillController(BdmsContext context, ILogger logger, IBoreholeLockService boreholeLockService) : base(context, logger, boreholeLockService) diff --git a/src/api/Controllers/BdmsControllerBase.cs b/src/api/Controllers/BoreholeControllerBase.cs similarity index 92% rename from src/api/Controllers/BdmsControllerBase.cs rename to src/api/Controllers/BoreholeControllerBase.cs index 605adc8cb..9a44b4e3b 100644 --- a/src/api/Controllers/BdmsControllerBase.cs +++ b/src/api/Controllers/BoreholeControllerBase.cs @@ -3,9 +3,13 @@ namespace BDMS.Controllers; +/// +/// Base controller for all borehole editing actions. +/// +/// The controller to edit a borehole. [ApiController] [Route("api/v{version:apiVersion}/[controller]")] -public abstract class BdmsControllerBase : ControllerBase +public abstract class BoreholeControllerBase : ControllerBase where TEntity : IIdentifyable, IChangeTracking, new() { private readonly BdmsContext context; @@ -27,7 +31,7 @@ public abstract class BdmsControllerBase : ControllerBase /// protected IBoreholeLockService BoreholeLockService => boreholeLockService; - protected BdmsControllerBase(BdmsContext context, ILogger logger, IBoreholeLockService boreholeLockService) + protected BoreholeControllerBase(BdmsContext context, ILogger logger, IBoreholeLockService boreholeLockService) { this.context = context; this.logger = logger; diff --git a/src/api/Controllers/CasingController.cs b/src/api/Controllers/CasingController.cs index 86d3d7e34..b28f08cb7 100644 --- a/src/api/Controllers/CasingController.cs +++ b/src/api/Controllers/CasingController.cs @@ -9,7 +9,7 @@ namespace BDMS.Controllers; [ApiController] [Route("api/v{version:apiVersion}/[controller]")] -public class CasingController : BdmsControllerBase +public class CasingController : BoreholeControllerBase { public CasingController(BdmsContext context, ILogger logger, IBoreholeLockService boreholeLockService) : base(context, logger, boreholeLockService) diff --git a/src/api/Controllers/ChronostratigraphyController.cs b/src/api/Controllers/ChronostratigraphyController.cs index fed239ff7..60f8892f7 100644 --- a/src/api/Controllers/ChronostratigraphyController.cs +++ b/src/api/Controllers/ChronostratigraphyController.cs @@ -8,7 +8,7 @@ namespace BDMS.Controllers; [ApiController] [Route("api/v{version:apiVersion}/[controller]")] -public class ChronostratigraphyController : BdmsControllerBase +public class ChronostratigraphyController : BoreholeControllerBase { public ChronostratigraphyController(BdmsContext context, ILogger logger, IBoreholeLockService boreholeLockService) : base(context, logger, boreholeLockService) diff --git a/src/api/Controllers/CompletionController.cs b/src/api/Controllers/CompletionController.cs index b6f99e99d..80cfd2cd0 100644 --- a/src/api/Controllers/CompletionController.cs +++ b/src/api/Controllers/CompletionController.cs @@ -10,7 +10,7 @@ namespace BDMS.Controllers; [ApiController] [Route("api/v{version:apiVersion}/[controller]")] -public class CompletionController : BdmsControllerBase +public class CompletionController : BoreholeControllerBase { public CompletionController(BdmsContext context, ILogger logger, IBoreholeLockService boreholeLockService) : base(context, logger, boreholeLockService) diff --git a/src/api/Controllers/FaciesDescriptionController.cs b/src/api/Controllers/FaciesDescriptionController.cs index 491c22453..c61a3442f 100644 --- a/src/api/Controllers/FaciesDescriptionController.cs +++ b/src/api/Controllers/FaciesDescriptionController.cs @@ -8,7 +8,7 @@ namespace BDMS.Controllers; [ApiController] [Route("api/v{version:apiVersion}/[controller]")] -public class FaciesDescriptionController : BdmsControllerBase +public class FaciesDescriptionController : BoreholeControllerBase { public FaciesDescriptionController(BdmsContext context, ILogger logger, IBoreholeLockService boreholeLockService) : base(context, logger, boreholeLockService) diff --git a/src/api/Controllers/FieldMeasurementController.cs b/src/api/Controllers/FieldMeasurementController.cs index 5c8ec9134..260cdeaed 100644 --- a/src/api/Controllers/FieldMeasurementController.cs +++ b/src/api/Controllers/FieldMeasurementController.cs @@ -8,7 +8,7 @@ namespace BDMS.Controllers; [ApiController] [Route("api/v{version:apiVersion}/[controller]")] -public class FieldMeasurementController : BdmsControllerBase +public class FieldMeasurementController : BoreholeControllerBase { public FieldMeasurementController(BdmsContext context, ILogger logger, IBoreholeLockService boreholeLockService) : base(context, logger, boreholeLockService) diff --git a/src/api/Controllers/GroundwaterLevelMeasurementController.cs b/src/api/Controllers/GroundwaterLevelMeasurementController.cs index 89a9f1050..61def43c2 100644 --- a/src/api/Controllers/GroundwaterLevelMeasurementController.cs +++ b/src/api/Controllers/GroundwaterLevelMeasurementController.cs @@ -8,7 +8,7 @@ namespace BDMS.Controllers; [ApiController] [Route("api/v{version:apiVersion}/[controller]")] -public class GroundwaterLevelMeasurementController : BdmsControllerBase +public class GroundwaterLevelMeasurementController : BoreholeControllerBase { public GroundwaterLevelMeasurementController(BdmsContext context, ILogger logger, IBoreholeLockService boreholeLockService) : base(context, logger, boreholeLockService) diff --git a/src/api/Controllers/HydrotestController.cs b/src/api/Controllers/HydrotestController.cs index 659b2d31e..e5834b3b6 100644 --- a/src/api/Controllers/HydrotestController.cs +++ b/src/api/Controllers/HydrotestController.cs @@ -8,7 +8,7 @@ namespace BDMS.Controllers; [ApiController] [Route("api/v{version:apiVersion}/[controller]")] -public class HydrotestController : BdmsControllerBase +public class HydrotestController : BoreholeControllerBase { public HydrotestController(BdmsContext context, ILogger logger, IBoreholeLockService boreholeLockService) : base(context, logger, boreholeLockService) diff --git a/src/api/Controllers/InstrumentationController.cs b/src/api/Controllers/InstrumentationController.cs index 14792ecc3..645ba8499 100644 --- a/src/api/Controllers/InstrumentationController.cs +++ b/src/api/Controllers/InstrumentationController.cs @@ -8,7 +8,7 @@ namespace BDMS.Controllers; [ApiController] [Route("api/v{version:apiVersion}/[controller]")] -public class InstrumentationController : BdmsControllerBase +public class InstrumentationController : BoreholeControllerBase { public InstrumentationController(BdmsContext context, ILogger logger, IBoreholeLockService boreholeLockService) : base(context, logger, boreholeLockService) diff --git a/src/api/Controllers/LayerController.cs b/src/api/Controllers/LayerController.cs index 02e4a15fe..f4da0cc68 100644 --- a/src/api/Controllers/LayerController.cs +++ b/src/api/Controllers/LayerController.cs @@ -8,7 +8,7 @@ namespace BDMS.Controllers; [ApiController] [Route("api/v{version:apiVersion}/[controller]")] -public class LayerController : BdmsControllerBase +public class LayerController : BoreholeControllerBase { public LayerController(BdmsContext context, ILogger logger, IBoreholeLockService boreholeLockService) : base(context, logger, boreholeLockService) diff --git a/src/api/Controllers/LithologicalDescriptionController.cs b/src/api/Controllers/LithologicalDescriptionController.cs index bf0905a3d..281513a88 100644 --- a/src/api/Controllers/LithologicalDescriptionController.cs +++ b/src/api/Controllers/LithologicalDescriptionController.cs @@ -8,7 +8,7 @@ namespace BDMS.Controllers; [ApiController] [Route("api/v{version:apiVersion}/[controller]")] -public class LithologicalDescriptionController : BdmsControllerBase +public class LithologicalDescriptionController : BoreholeControllerBase { public LithologicalDescriptionController(BdmsContext context, ILogger logger, IBoreholeLockService boreholeLockService) : base(context, logger, boreholeLockService) diff --git a/src/api/Controllers/LithostratigraphyController.cs b/src/api/Controllers/LithostratigraphyController.cs index ae9c66698..01546714e 100644 --- a/src/api/Controllers/LithostratigraphyController.cs +++ b/src/api/Controllers/LithostratigraphyController.cs @@ -8,7 +8,7 @@ namespace BDMS.Controllers; [ApiController] [Route("api/v{version:apiVersion}/[controller]")] -public class LithostratigraphyController : BdmsControllerBase +public class LithostratigraphyController : BoreholeControllerBase { public LithostratigraphyController(BdmsContext context, ILogger logger, IBoreholeLockService boreholeLockService) : base(context, logger, boreholeLockService) diff --git a/src/api/Controllers/SectionController.cs b/src/api/Controllers/SectionController.cs index d0454a25a..abcc40751 100644 --- a/src/api/Controllers/SectionController.cs +++ b/src/api/Controllers/SectionController.cs @@ -8,7 +8,7 @@ namespace BDMS.Controllers; [ApiController] [Route("api/v{version:apiVersion}/[controller]")] -public class SectionController : BdmsControllerBase
+public class SectionController : BoreholeControllerBase
{ public SectionController(BdmsContext context, ILogger
logger, IBoreholeLockService boreholeLockService) : base(context, logger, boreholeLockService) diff --git a/src/api/Controllers/StratigraphyController.cs b/src/api/Controllers/StratigraphyController.cs index 53db506f9..a0948f995 100644 --- a/src/api/Controllers/StratigraphyController.cs +++ b/src/api/Controllers/StratigraphyController.cs @@ -9,7 +9,7 @@ namespace BDMS.Controllers; [ApiController] [Route("api/v{version:apiVersion}/[controller]")] -public class StratigraphyController : BdmsControllerBase +public class StratigraphyController : BoreholeControllerBase { public StratigraphyController(BdmsContext context, ILogger logger, IBoreholeLockService boreholeLockService) : base(context, logger, boreholeLockService) diff --git a/src/api/Controllers/WaterIngressController.cs b/src/api/Controllers/WaterIngressController.cs index c64e4a71d..beec526a1 100644 --- a/src/api/Controllers/WaterIngressController.cs +++ b/src/api/Controllers/WaterIngressController.cs @@ -9,7 +9,7 @@ namespace BDMS.Controllers; [ApiController] [Route("api/v{version:apiVersion}/[controller]")] -public class WaterIngressController : BdmsControllerBase +public class WaterIngressController : BoreholeControllerBase { public WaterIngressController(BdmsContext context, ILogger logger, IBoreholeLockService boreholeLockService) : base(context, logger, boreholeLockService) diff --git a/src/api/Controllers/WorkgroupController.cs b/src/api/Controllers/WorkgroupController.cs new file mode 100644 index 000000000..6349e34c3 --- /dev/null +++ b/src/api/Controllers/WorkgroupController.cs @@ -0,0 +1,198 @@ +using BDMS.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace BDMS.Controllers; + +[ApiController] +[Route("api/v{version:apiVersion}/[controller]")] +public class WorkgroupController : ControllerBase +{ + private readonly BdmsContext context; + private ILogger logger; + + /// + /// Initializes a new instance of the class. + /// + /// The EF database context containing data for the BDMS application. + /// The logger used by the controller. + public WorkgroupController(BdmsContext context, ILogger logger) + { + this.context = context; + this.logger = logger; + } + + /// + /// Gets a list of workgroups. + /// + [HttpGet] + [SwaggerResponse(StatusCodes.Status200OK, "Returns a list of workgroups.")] + public async Task> GetAll() + { + var workgroups = await context + .WorkgroupsWithIncludes + .AsNoTracking() + .ToListAsync() + .ConfigureAwait(false); + + return workgroups; + } + + /// + /// Create a new workgroup./>. + /// + [HttpPost] + [SwaggerResponse(StatusCodes.Status200OK, "The workgroup was created successfully.")] + [SwaggerResponse(StatusCodes.Status400BadRequest, "The workgroup could not be created due to invalid input.")] + [SwaggerResponse(StatusCodes.Status401Unauthorized, "The current user is not authorized to create workgroups.")] + [SwaggerResponse(StatusCodes.Status500InternalServerError, "The server encountered an unexpected condition that prevented it from fulfilling the request. ")] + public async Task Create(Workgroup workgroup) + { + try + { + if (workgroup == null) + { + return BadRequest(); + } + + var isDuplicate = await context.Workgroups.AnyAsync(w => w.Name == workgroup.Name).ConfigureAwait(false); + if (isDuplicate) + { + return Problem("A workgroup with the same name already exists."); + } + + workgroup.Settings = "{}"; + workgroup.CreatedAt = DateTime.UtcNow; + if (workgroup.IsSupplier == null) + { + workgroup.IsSupplier = false; + } + + var enityEntry = await context.Workgroups.AddAsync(workgroup).ConfigureAwait(false); + await context.SaveChangesAsync().ConfigureAwait(false); + + return Ok(enityEntry.Entity); + } + catch (Exception e) + { + var message = "Error while creating workgroup."; + logger.LogError(e, message); + return Problem(message); + } + } + + /// + /// Updates the . + /// + [HttpPut] + [SwaggerResponse(StatusCodes.Status200OK, "The workgroup was updated successfully.")] + [SwaggerResponse(StatusCodes.Status400BadRequest, "The workgroup could not be updated due to invalid input.")] + [SwaggerResponse(StatusCodes.Status404NotFound, "The workgroup could not be found.")] + [SwaggerResponse(StatusCodes.Status401Unauthorized, "The current user is not authorized to update workgroups.")] + [SwaggerResponse(StatusCodes.Status500InternalServerError, "The server encountered an unexpected condition that prevented it from fulfilling the request. ")] + public async Task Edit(Workgroup workgroup) + { + try + { + if (workgroup == null) + { + return BadRequest(); + } + + var workgroupToEdit = await context.WorkgroupsWithIncludes.SingleOrDefaultAsync(w => w.Id == workgroup.Id).ConfigureAwait(false); + if (workgroupToEdit == null) + { + return NotFound(); + } + + workgroupToEdit.Name = workgroup.Name; + workgroupToEdit.DisabledAt = workgroup.DisabledAt; + + await context.SaveChangesAsync().ConfigureAwait(false); + var updatedWorkgroup = await context.Workgroups.SingleOrDefaultAsync(w => w.Id == workgroup.Id).ConfigureAwait(false); + return Ok(updatedWorkgroup); + } + catch (Exception e) + { + var message = "Error while updating workgroup."; + logger.LogError(e, message); + return Problem(message); + } + } + + /// + /// Deletes the workgroup with the specified . + /// + [HttpDelete("{id}")] + [SwaggerResponse(StatusCodes.Status200OK, "The workgroup was deleted successfully.")] + [SwaggerResponse(StatusCodes.Status400BadRequest, "The workgroup could not be updated due to invalid input.")] + [SwaggerResponse(StatusCodes.Status404NotFound, "The workgroup could not be found.")] + [SwaggerResponse(StatusCodes.Status401Unauthorized, "The current user is not authorized to delete workgroups.")] + [SwaggerResponse(StatusCodes.Status500InternalServerError, "The server encountered an unexpected condition that prevented it from fulfilling the request. ")] + public async Task Delete(int id) + { + try + { + var workgroup = await context.WorkgroupsWithIncludes.SingleOrDefaultAsync(w => w.Id == id).ConfigureAwait(false); + if (workgroup == null) + { + return NotFound(); + } + + if (workgroup.Boreholes?.Count > 0) + { + return Problem("The workgroup is associated with boreholes and cannot be deleted."); + } + + context.Workgroups.Remove(workgroup); + await context.SaveChangesAsync().ConfigureAwait(false); + return Ok(); + } + catch (Exception e) + { + var message = "Error while deleting workgroup."; + logger.LogError(e, message); + return Problem(message); + } + } + + [HttpPost("setRole")] + [SwaggerResponse(StatusCodes.Status200OK, "The role was set successfully.")] + [SwaggerResponse(StatusCodes.Status400BadRequest, "The role could not be set due to invalid input.")] + [SwaggerResponse(StatusCodes.Status404NotFound, "The user or workgroup could not be found.")] + [SwaggerResponse(StatusCodes.Status401Unauthorized, "The current user is not authorized to set roles.")] + [SwaggerResponse(StatusCodes.Status500InternalServerError, "The server encountered an unexpected condition that prevented it from fulfilling the request. ")] + public async Task SetRole(UserWorkgroupRole userWorkgroupRole) + { + try + { + if (userWorkgroupRole == null) + { + return BadRequest(); + } + + var existingRole = await context.UserWorkgroupRoles + .SingleOrDefaultAsync(r => r.UserId == userWorkgroupRole.UserId && r.WorkgroupId == userWorkgroupRole.WorkgroupId && r.Role == userWorkgroupRole.Role) + .ConfigureAwait(false); + + if (userWorkgroupRole.IsActive == true && existingRole == default) + { + await context.AddAsync(userWorkgroupRole).ConfigureAwait(false); + } + else if (userWorkgroupRole.IsActive == false && existingRole != default) + { + context.UserWorkgroupRoles.Remove(existingRole); + } + + await context.SaveChangesAsync().ConfigureAwait(false); + return Ok(); + } + catch (Exception e) + { + var message = "Error while setting role."; + logger.LogError(e, message); + return Problem(message); + } + } +} diff --git a/src/api/Dockerfile b/src/api/Dockerfile index 0269582c1..73fda2052 100644 --- a/src/api/Dockerfile +++ b/src/api/Dockerfile @@ -6,13 +6,13 @@ WORKDIR /src RUN apt-get -y update RUN apt-get -y install git vim curl htop RUN dotnet tool install --global dotnet-ef --version 8.0.0 -ENV PATH $PATH:/root/.dotnet/tools +ENV PATH=$PATH:/root/.dotnet/tools # Restore dependencies and tools COPY BDMS.csproj . RUN dotnet restore -ENTRYPOINT dotnet watch run --no-launch-profile +ENTRYPOINT ["dotnet", "watch", "run", "--no-launch-profile"] FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build ARG VERSION @@ -51,8 +51,8 @@ ENV LC_ALL=C.UTF-8 COPY --from=build /app/publish . -HEALTHCHECK CMD curl --fail http://localhost:8080/ || exit 1 +HEALTHCHECK CMD curl --fail http://localhost:8080/health || exit 1 # Switch to the non-root user 'app' defined in the base image USER $APP_UID -ENTRYPOINT dotnet "BDMS.dll" +ENTRYPOINT ["dotnet", "BDMS.dll"] diff --git a/src/api/Models/UserWorkgroupRole.cs b/src/api/Models/UserWorkgroupRole.cs index 7d652846d..25b0e14dc 100644 --- a/src/api/Models/UserWorkgroupRole.cs +++ b/src/api/Models/UserWorkgroupRole.cs @@ -36,6 +36,12 @@ public class UserWorkgroupRole [Column("id_rol_fk", TypeName = "int")] public Role Role { get; set; } + /// + /// Gets or sets whether the role is active or not. + /// + [NotMapped] + public bool? IsActive { get; set; } + /// public override string ToString() => $"WorkgroupId: {WorkgroupId}, {Role}"; } diff --git a/src/api/Models/Workgroup.cs b/src/api/Models/Workgroup.cs index 720378188..edbdde12f 100644 --- a/src/api/Models/Workgroup.cs +++ b/src/api/Models/Workgroup.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; namespace BDMS.Models; @@ -24,13 +25,18 @@ public class Workgroup : IIdentifyable /// Gets or sets the created date. /// [Column("created_wgp")] - public DateTime? Created { get; set; } + public DateTime? CreatedAt { get; set; } /// /// Gets or sets the disabled date. /// [Column("disabled_wgp")] - public DateTime? Disabled { get; set; } + public DateTime? DisabledAt { get; set; } + + /// + /// Gets the value whether the is disabled or not. + /// + public bool IsDisabled => DisabledAt.HasValue; /// /// Gets or sets the Settings for the . @@ -47,7 +53,13 @@ public class Workgroup : IIdentifyable /// /// Gets the boreholes for the workgroup. /// - public ICollection Boreholes { get; set; } + [JsonIgnore] + public ICollection? Boreholes { get; set; } + + /// + /// Number of boreholes in the workgroup. + /// + public int BoreholeCount => Boreholes?.Count ?? 0; /// public override string ToString() => Name; diff --git a/src/api/Program.cs b/src/api/Program.cs index 808d4c946..d1071c059 100644 --- a/src/api/Program.cs +++ b/src/api/Program.cs @@ -54,7 +54,7 @@ builder.Services.AddHttpContextAccessor(); builder.Services.AddHttpClient(); -var connectionString = builder.Configuration.GetConnectionString("BdmsContext"); +var connectionString = builder.Configuration.GetConnectionString(nameof(BdmsContext)); builder.Services.AddNpgsql(connectionString, options => { options.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); @@ -73,7 +73,7 @@ options.SwaggerDoc("v2", new OpenApiInfo { Version = "v2", - Title = "BDMS REST API v2", + Title = "Boreholes REST API v2", }); options.AddSecurityDefinition("OpenIdConnect", new OpenApiSecurityScheme { @@ -139,6 +139,11 @@ builder.Services.AddScoped(); builder.Services.AddSingleton(TimeProvider.System); +builder.Services + .AddHealthChecks() + .AddDbContextCheck("Database") + .AddCheck("S3"); + var app = builder.Build(); // Migrate db changes on startup @@ -166,5 +171,6 @@ app.MapControllers(); app.MapReverseProxy(); +app.MapHealthChecks("/health").AllowAnonymous(); app.Run(); diff --git a/src/api/S3HealthCheck.cs b/src/api/S3HealthCheck.cs new file mode 100644 index 000000000..2c2b06030 --- /dev/null +++ b/src/api/S3HealthCheck.cs @@ -0,0 +1,38 @@ +using Amazon.S3; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using System.Net; + +namespace BDMS; + +public class S3HealthCheck : IHealthCheck +{ + private readonly IAmazonS3 s3Client; + + /// + /// Creates a new instance of . + /// + /// The client. + /// The object. + public S3HealthCheck(IAmazonS3 s3Client, IConfiguration configuration) => this.s3Client = s3Client; + + /// + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + var healthCheckResult = HealthCheckResult.Healthy(); + + try + { + var response = await s3Client.ListBucketsAsync(cancellationToken).ConfigureAwait(false); + if (response.HttpStatusCode != HttpStatusCode.OK) + { + healthCheckResult = HealthCheckResult.Unhealthy(); + } + } + catch (Exception) + { + healthCheckResult = HealthCheckResult.Unhealthy(); + } + + return await Task.FromResult(healthCheckResult).ConfigureAwait(false); + } +} diff --git a/src/client/Dockerfile b/src/client/Dockerfile index 7a0835f7d..807d22bd0 100644 --- a/src/client/Dockerfile +++ b/src/client/Dockerfile @@ -1,7 +1,7 @@ FROM node:20-buster-slim AS development ARG VERSION ARG REVISION -ENV VITE_APP_VERSION ${VERSION}+${REVISION} +ENV VITE_APP_VERSION=${VERSION}+${REVISION} RUN apt-get -y update RUN apt-get -y install git vim curl htop python3 python3-pip RUN python3 -m pip install mkdocs @@ -17,12 +17,12 @@ COPY ./docs ./docs COPY ./mkdocs.yml ./ RUN mkdocs build -d ./public/help -ENTRYPOINT npm run start -- --host +ENTRYPOINT ["npm", "run", "start", "--", "--host"] FROM node:20-buster-slim AS deploy ARG VERSION ARG REVISION -ENV VITE_APP_VERSION ${VERSION}+${REVISION} +ENV VITE_APP_VERSION=${VERSION}+${REVISION} RUN apt-get -y update RUN apt-get -y install git diff --git a/src/client/src/api-lib/actions/workgroups.js b/src/client/src/api-lib/actions/workgroups.js deleted file mode 100644 index e43c0afe3..000000000 --- a/src/client/src/api-lib/actions/workgroups.js +++ /dev/null @@ -1,53 +0,0 @@ -import { fetch } from "./index"; - -export function createWorkgroup(name) { - return fetch("/user/workgroup/edit", { - action: "CREATE", - name: name, - }); -} - -export function enableWorkgroup(id) { - return fetch("/user/workgroup/edit", { - action: "ENABLE", - id: id, - }); -} - -export function disableWorkgroup(id) { - return fetch("/user/workgroup/edit", { - action: "DISABLE", - id: id, - }); -} - -export function deleteWorkgroup(id) { - return fetch("/user/workgroup/edit", { - action: "DELETE", - id: id, - }); -} - -export function updateWorkgroup(id, name) { - return fetch("/user/workgroup/edit", { - action: "UPDATE", - id: id, - name: name, - }); -} - -export function listWorkgroups() { - return fetch("/user/workgroup/edit", { - type: "LIST", - }); -} - -export function setRole(user_id, workgroup_id, role_name, activateRole = true) { - return fetch("/user/workgroup/edit", { - action: "SET", - user_id: user_id, - workgroup_id: workgroup_id, - role_name: role_name, - active: activateRole, - }); -} diff --git a/src/client/src/api-lib/index.js b/src/client/src/api-lib/index.js index 612a288de..11a2d5b1a 100644 --- a/src/client/src/api-lib/index.js +++ b/src/client/src/api-lib/index.js @@ -6,16 +6,6 @@ import { acceptTerms, draftTerms, getTerms, getTermsDraft, publishTerms } from " import { loadUser, setAuthentication, unsetAuthentication } from "./actions/user"; -import { - createWorkgroup, - deleteWorkgroup, - disableWorkgroup, - enableWorkgroup, - listWorkgroups, - setRole, - updateWorkgroup, -} from "./actions/workgroups"; - import { createBorehole, deleteBorehole, @@ -65,13 +55,6 @@ export { setAuthentication, unsetAuthentication, loadUser, - createWorkgroup, - enableWorkgroup, - disableWorkgroup, - deleteWorkgroup, - listWorkgroups, - setRole, - updateWorkgroup, loadBorehole, updateBorehole, loadBoreholes, diff --git a/src/client/src/api/apiInterfaces.ts b/src/client/src/api/apiInterfaces.ts index 5552b554e..f0796550b 100644 --- a/src/client/src/api/apiInterfaces.ts +++ b/src/client/src/api/apiInterfaces.ts @@ -1,27 +1,29 @@ export enum Role { - View, - Editor, - Controller, - Validator, - Publisher, + View = "View", + Editor = "Editor", + Controller = "Controller", + Validator = "Validator", + Publisher = "Publisher", } export interface Workgroup { - // TODO: Add boreholes id: number; name: string; - created?: Date; - disabled?: Date; + isDisabled?: boolean; + disabledAt?: Date | string; + createdAt?: Date | string; settings?: string; isSupplier?: boolean; + boreholeCount: number; } export interface WorkgroupRole { userId: number; - user: User; + user?: User; workgroupId: number; - workgroup: Workgroup; + workgroup?: Workgroup; role: Role; + isActive?: boolean; } export interface Term { diff --git a/src/client/src/api/workgroup.ts b/src/client/src/api/workgroup.ts new file mode 100644 index 000000000..dd68074f3 --- /dev/null +++ b/src/client/src/api/workgroup.ts @@ -0,0 +1,27 @@ +import { fetchApiV2 } from "./fetchApiV2"; +import { Role, Workgroup, WorkgroupRole } from "./apiInterfaces.ts"; + +export const fetchWorkgroups = async () => await fetchApiV2("workgroup", "GET"); + +export const createWorkgroup = async (workgroup: Workgroup) => await fetchApiV2("workgroup", "POST", workgroup); + +export const updateWorkgroup = async (workgroup: Workgroup) => { + if (workgroup.disabledAt) { + workgroup.disabledAt = new Date(workgroup.disabledAt).toISOString(); + } + workgroup.isDisabled = undefined; + + return await fetchApiV2("workgroup", "PUT", workgroup); +}; + +export const deleteWorkgroup = async (id: number) => await fetchApiV2(`workgroup/${id}`, "DELETE"); + +export const setRole = async (userId: number, workgroupId: number, role: Role, isActive: boolean) => { + const workgroupRole: WorkgroupRole = { + workgroupId: workgroupId, + userId: userId, + role: role, + isActive: isActive, + }; + return await fetchApiV2("workgroup/setRole", "POST", workgroupRole); +}; diff --git a/src/client/src/pages/settings/admin/adminSettings.jsx b/src/client/src/pages/settings/admin/adminSettings.jsx index b1b4e6ca9..c619ab834 100644 --- a/src/client/src/pages/settings/admin/adminSettings.jsx +++ b/src/client/src/pages/settings/admin/adminSettings.jsx @@ -7,19 +7,10 @@ import { deleteUser, fetchUser, fetchUsers, updateUser } from "../../../api/user import { Button, Checkbox, Form, Icon, Input, Label, Loader, Modal, Table } from "semantic-ui-react"; -import { - createWorkgroup, - deleteWorkgroup, - disableWorkgroup, - enableWorkgroup, - listWorkgroups, - setRole, - updateWorkgroup, -} from "../../../api-lib/index"; - import DateText from "../../../components/legacyComponents/dateText.js"; import TranslationText from "../../../components/legacyComponents/translationText.jsx"; import { WorkgroupRoleSettings } from "./workgroupRoleSettings"; +import { createWorkgroup, deleteWorkgroup, fetchWorkgroups, setRole, updateWorkgroup } from "../../../api/workgroup"; class AdminSettings extends React.Component { static contextType = AlertContext; @@ -34,8 +25,8 @@ class AdminSettings extends React.Component { usersSearch: "", workgroupFilter: "enabled", // 'all', 'enabled' or 'disabled', workgroupsSearch: "", - users: false, - workgroups: false, + users: null, + workgroups: null, roleUpdate: false, @@ -54,11 +45,12 @@ class AdminSettings extends React.Component { }; this.reset = this.reset.bind(this); this.listUsers = this.listUsers.bind(this); + this.listWorkgroups = this.listWorkgroups.bind(this); } componentDidMount() { this.listUsers(); - this.props.listWorkgroups(); + this.listWorkgroups(); } reset(state, andThen) { @@ -98,6 +90,22 @@ class AdminSettings extends React.Component { ); } + async listWorkgroups(reloadUser) { + const workgroups = await fetchWorkgroups(); + this.setState({ + workgroups: workgroups, + }); + + // immediately update currently selected workgroup. + if (reloadUser) { + await fetchUser(this.state.user.id).then(user => { + this.setState({ + user: user, + }); + }); + } + } + async listUsers() { const users = await fetchUsers(); this.setState({ @@ -118,9 +126,8 @@ class AdminSettings extends React.Component { roleUpdate: true, }, () => { - let isRoleActive = uwg !== undefined && uwg.some(x => x.role.toLowerCase().startsWith(role.toLowerCase())); - - setRole(this.state.user.id, workgroup.id, role === "Publisher" ? "PUBLIC" : role, !isRoleActive).then(() => { + let isRoleActive = uwg !== undefined && uwg.some(x => x.role === role); + setRole(this.state.user.id, workgroup.id, role, !isRoleActive).then(() => { this.listUsers(); }); }, @@ -342,7 +349,7 @@ class AdminSettings extends React.Component {

- ) : this.state.deleteUser !== null && this.state.deleteUser.deletable ? ( + ) : this.state.deleteUser.deletable ? (

@@ -521,7 +528,7 @@ class AdminSettings extends React.Component { {this.state.users && - this.state.users.map(currentUser => + this.state.users?.map(currentUser => (this.state.usersSearch !== "" && (currentUser.name.toUpperCase().includes(this.state.usersSearch.toUpperCase()) || currentUser.firstName.toUpperCase().includes(this.state.usersSearch.toUpperCase()) || @@ -656,12 +663,14 @@ class AdminSettings extends React.Component { label=" " onClick={() => { if (this.state.wId === null) { - createWorkgroup(this.state.wName).then(() => { - this.props.listWorkgroups(true); + createWorkgroup({ name: this.state.wName }).then(() => { + this.listWorkgroups(true); }); } else { - updateWorkgroup(this.state.wId, this.state.wName).then(() => { - this.props.listWorkgroups(true); + const workgroup = this.state.workgroup; + workgroup.name = this.state.wName; + updateWorkgroup(workgroup).then(() => { + this.listWorkgroups(true); }); } }}> @@ -752,14 +761,14 @@ class AdminSettings extends React.Component { open={this.state.deleteWorkgroup !== null} size="tiny"> - {this.state.deleteWorkgroup !== null && this.state.deleteWorkgroup.disabled !== null ? ( + {this.state.deleteWorkgroup !== null && this.state.deleteWorkgroup.disabledAt !== null ? ( ) : ( )} - {this.state.deleteWorkgroup === null ? null : this.state.deleteWorkgroup.disabled !== null ? ( + {this.state.deleteWorkgroup === null ? null : this.state.deleteWorkgroup.disabledAt !== null ? (

- ) : this.state.deleteWorkgroup !== null && this.state.deleteWorkgroup.boreholes === 0 ? ( + ) : this.state.deleteWorkgroup.boreholeCount === 0 ? (

@@ -844,16 +853,18 @@ class AdminSettings extends React.Component {

- {this.state.deleteWorkgroup === null ? null : this.state.deleteWorkgroup.disabled !== null ? ( + {this.state.deleteWorkgroup === null ? null : this.state.deleteWorkgroup.disabledAt !== null ? (
@@ -1039,7 +1056,6 @@ class AdminSettings extends React.Component { } AdminSettings.propTypes = { - listWorkgroups: PropTypes.func, t: PropTypes.func, user: PropTypes.object, users: PropTypes.object, @@ -1052,17 +1068,5 @@ const mapStateToProps = state => { }; }; -const mapDispatchToProps = dispatch => { - return { - dispatch: dispatch, - listWorkgroups: (ru = false) => { - if (ru === true) { - fetchUser(); - } - return dispatch(listWorkgroups()); - }, - }; -}; - -const ConnectedAdminSettings = connect(mapStateToProps, mapDispatchToProps)(withTranslation(["common"])(AdminSettings)); +const ConnectedAdminSettings = connect(mapStateToProps)(withTranslation(["common"])(AdminSettings)); export default ConnectedAdminSettings; diff --git a/src/client/src/pages/settings/admin/workgroupRoleSettings.jsx b/src/client/src/pages/settings/admin/workgroupRoleSettings.jsx deleted file mode 100644 index 8c43688b0..000000000 --- a/src/client/src/pages/settings/admin/workgroupRoleSettings.jsx +++ /dev/null @@ -1,70 +0,0 @@ -import { Checkbox, Form } from "semantic-ui-react"; - -export const WorkgroupRoleSettings = props => { - const { workgroup, user, setRole } = props; - const uwg = user.workgroupRoles.filter(w => w.workgroupId === workgroup?.id); - return ( -
- - - x.role === "View")} - label="VIEW" - onChange={e => { - e.stopPropagation(); - setRole(uwg, workgroup, "VIEW"); - }} - /> - - {workgroup.supplier === false ? ( - - x.role === "Editor")} - label="EDITOR" - onChange={e => { - e.stopPropagation(); - setRole(uwg, workgroup, "EDIT"); - }} - /> - - ) : null} - {workgroup.supplier === false ? ( - - x.role === "Controller")} - label="CONTROLLER" - onChange={e => { - e.stopPropagation(); - setRole(uwg, workgroup, "CONTROL"); - }} - /> - - ) : null} - - - {workgroup.supplier === false ? ( - - x.role === "Validator")} - label="VALIDATOR" - onChange={e => { - e.stopPropagation(); - setRole(uwg, workgroup, "VALID"); - }} - /> - - ) : null} - - x.role === "Publisher")} - label="PUBLISHER" - onChange={e => { - e.stopPropagation(); - setRole(uwg, workgroup, "PUBLIC"); - }} - /> - - -
- ); -}; diff --git a/src/client/src/pages/settings/admin/workgroupRoleSettings.tsx b/src/client/src/pages/settings/admin/workgroupRoleSettings.tsx new file mode 100644 index 000000000..eb91f6676 --- /dev/null +++ b/src/client/src/pages/settings/admin/workgroupRoleSettings.tsx @@ -0,0 +1,80 @@ +import { Checkbox, Form } from "semantic-ui-react"; +import { Role, User, Workgroup, WorkgroupRole } from "../../../api/apiInterfaces.js"; +import { FC } from "react"; + +export interface WorkgroupRoleSettingsProps { + user: User; + workgroup: Workgroup; + setRole: (uwg: WorkgroupRole[], workgroup: Workgroup, role: Role) => void; +} + +export const WorkgroupRoleSettings: FC = props => { + const { workgroup, user, setRole } = props; + const workgroupRoles = user.workgroupRoles?.filter(w => w.workgroupId === workgroup?.id); + return ( + workgroupRoles !== undefined && ( +
+ + + x.role === Role.View)} + label="VIEW" + onChange={e => { + e.stopPropagation(); + setRole(workgroupRoles, workgroup, Role.View); + }} + /> + + {workgroup.isSupplier === false ? ( + + x.role === Role.Editor)} + label="EDITOR" + onChange={e => { + e.stopPropagation(); + setRole(workgroupRoles, workgroup, Role.Editor); + }} + /> + + ) : null} + {workgroup.isSupplier === false ? ( + + x.role === Role.Controller)} + label="CONTROLLER" + onChange={e => { + e.stopPropagation(); + setRole(workgroupRoles, workgroup, Role.Controller); + }} + /> + + ) : null} + + + {workgroup.isSupplier === false ? ( + + x.role === Role.Validator)} + label="VALIDATOR" + onChange={e => { + e.stopPropagation(); + setRole(workgroupRoles, workgroup, Role.Validator); + }} + /> + + ) : null} + + x.role === Role.Publisher)} + label="PUBLISHER" + onChange={e => { + e.stopPropagation(); + setRole(workgroupRoles, workgroup, Role.Publisher); + }} + /> + + +
+ ) + ); +}; diff --git a/tests/Controllers/WorkgroupControllerTest.cs b/tests/Controllers/WorkgroupControllerTest.cs new file mode 100644 index 000000000..8820a0ace --- /dev/null +++ b/tests/Controllers/WorkgroupControllerTest.cs @@ -0,0 +1,200 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using static BDMS.Helpers; + +namespace BDMS.Controllers; + +[TestClass] +public class WorkgroupControllerTest +{ + private BdmsContext context; + private WorkgroupController workgroupController; + + [TestInitialize] + public void TestInitialize() + { + context = ContextFactory.GetTestContext(); + workgroupController = new WorkgroupController(context, new Mock>().Object) { ControllerContext = GetControllerContextAdmin() }; + } + + [TestCleanup] + public async Task TestCleanup() => await context.DisposeAsync(); + + [TestMethod] + public async Task GetAll() + { + var workgroups = await workgroupController.GetAll(); + Assert.AreEqual(6, workgroups.Count()); + } + + [TestMethod] + public async Task CreateWorkgroup() + { + var workgroup = new Models.Workgroup { Name = "New Workgroup" }; + var result = await workgroupController.Create(workgroup); + ActionResultAssert.IsOk(result); + + var createdWorkgroup = (result as OkObjectResult).Value as Models.Workgroup; + Assert.AreEqual(workgroup.Name, createdWorkgroup.Name); + Assert.IsNotNull(createdWorkgroup.CreatedAt); + Assert.IsFalse(createdWorkgroup.IsDisabled); + Assert.IsFalse(createdWorkgroup.IsSupplier); + Assert.AreEqual("{}", createdWorkgroup.Settings); + } + + [TestMethod] + public async Task EditWorkgroup() + { + var workgroup = new Models.Workgroup { Name = "New Workgroup" }; + var createResult = await workgroupController.Create(workgroup); + ActionResultAssert.IsOk(createResult); + var createdWorkgroup = (createResult as OkObjectResult).Value as Models.Workgroup; + + await context.Boreholes.AddAsync(new Models.Borehole { WorkgroupId = createdWorkgroup.Id }); + await context.SaveChangesAsync(); + + var workgroupToEdit = await context.WorkgroupsWithIncludes.AsNoTracking().SingleOrDefaultAsync(u => u.Id == createdWorkgroup.Id); + Assert.AreEqual("New Workgroup", workgroupToEdit.Name); + Assert.IsFalse(workgroupToEdit.IsDisabled); + Assert.AreEqual(1, workgroupToEdit.BoreholeCount); + + workgroupToEdit.Name = "Updated Workgroup"; + workgroupToEdit.DisabledAt = DateTime.UtcNow; + workgroupToEdit.IsSupplier = true; + workgroupToEdit.Boreholes.Clear(); + + var result = await workgroupController.Edit(workgroupToEdit); + ActionResultAssert.IsOk(result); + var updatedWorkgroup = (result as OkObjectResult).Value as Models.Workgroup; + + Assert.AreEqual(createdWorkgroup.Id, updatedWorkgroup.Id); + Assert.AreEqual("Updated Workgroup", updatedWorkgroup.Name); + Assert.IsTrue(updatedWorkgroup.IsDisabled); + Assert.IsFalse(updatedWorkgroup.IsSupplier); + Assert.AreEqual(1, updatedWorkgroup.BoreholeCount); + } + + [TestMethod] + public async Task EditWorkgroupNotFound() + { + var result = await workgroupController.Edit(new Models.Workgroup { Id = 0 }); + ActionResultAssert.IsNotFound(result); + } + + [TestMethod] + public async Task DeleteWorkgroup() + { + var workgroup = new Models.Workgroup { Name = "New Workgroup" }; + var createResult = await workgroupController.Create(workgroup); + ActionResultAssert.IsOk(createResult); + var createdWorkgroup = (createResult as OkObjectResult).Value as Models.Workgroup; + + var deleteResult = await workgroupController.Delete(createdWorkgroup.Id); + ActionResultAssert.IsOk(deleteResult); + + var workgroupExists = await context.Workgroups.AsNoTracking().AnyAsync(u => u.Id == createdWorkgroup.Id); + Assert.IsFalse(workgroupExists); + } + + [TestMethod] + public async Task DeleteWorkgroupNotFound() + { + var result = await workgroupController.Delete(0); + ActionResultAssert.IsNotFound(result); + } + + [TestMethod] + public async Task DeleteWorkgroupNotDeletable() + { + var workgroup = await context.WorkgroupsWithIncludes.AsNoTracking().SingleOrDefaultAsync(u => u.Id == 1); + Assert.IsNotNull(workgroup); + + var result = await workgroupController.Delete(workgroup.Id); + ActionResultAssert.IsInternalServerError(result, "The workgroup is associated with boreholes and cannot be deleted."); + } + + [TestMethod] + public async Task SetRole() + { + var user = await context.Users.AsNoTracking().SingleOrDefaultAsync(u => u.FirstName == "editor"); + var workgroup = new Models.Workgroup { Name = "WINDDESPERADO" }; + var createResult = await workgroupController.Create(workgroup); + ActionResultAssert.IsOk(createResult); + var createdWorkgroup = (createResult as OkObjectResult).Value as Models.Workgroup; + + // Set role for user + var setRoleResult = await workgroupController.SetRole( + new Models.UserWorkgroupRole() + { + UserId = user.Id, + WorkgroupId = createdWorkgroup.Id, + Role = Models.Role.Editor, + IsActive = true, + }); + ActionResultAssert.IsOk(setRoleResult); + var userWorkgroupRoles = await context.UserWorkgroupRoles.AsNoTracking().Where(r => r.WorkgroupId == createdWorkgroup.Id && r.UserId == user.Id).ToListAsync(); + Assert.AreEqual(1, userWorkgroupRoles.Count); + Assert.AreEqual(Models.Role.Editor, userWorkgroupRoles[0].Role); + + // Cannot set the same role twice + setRoleResult = await workgroupController.SetRole( + new Models.UserWorkgroupRole() + { + UserId = user.Id, + WorkgroupId = createdWorkgroup.Id, + Role = Models.Role.Editor, + IsActive = true, + }); + ActionResultAssert.IsOk(setRoleResult); + userWorkgroupRoles = await context.UserWorkgroupRoles.AsNoTracking().Where(r => r.WorkgroupId == createdWorkgroup.Id && r.UserId == user.Id).ToListAsync(); + Assert.AreEqual(1, userWorkgroupRoles.Count); + Assert.AreEqual(Models.Role.Editor, userWorkgroupRoles[0].Role); + + // Set another role for user + setRoleResult = await workgroupController.SetRole( + new Models.UserWorkgroupRole() + { + UserId = user.Id, + WorkgroupId = createdWorkgroup.Id, + Role = Models.Role.View, + IsActive = true, + }); + ActionResultAssert.IsOk(setRoleResult); + userWorkgroupRoles = await context.UserWorkgroupRoles.AsNoTracking().Where(r => r.WorkgroupId == createdWorkgroup.Id && r.UserId == user.Id).ToListAsync(); + Assert.AreEqual(2, userWorkgroupRoles.Count); + Assert.IsTrue(userWorkgroupRoles.Any(r => r.Role == Models.Role.Editor)); + Assert.IsTrue(userWorkgroupRoles.Any(r => r.Role == Models.Role.View)); + + // Cannot remove a role that does not exist + setRoleResult = await workgroupController.SetRole( + new Models.UserWorkgroupRole() + { + UserId = user.Id, + WorkgroupId = createdWorkgroup.Id, + Role = Models.Role.Publisher, + IsActive = false, + }); + ActionResultAssert.IsOk(setRoleResult); + userWorkgroupRoles = await context.UserWorkgroupRoles.AsNoTracking().Where(r => r.WorkgroupId == createdWorkgroup.Id && r.UserId == user.Id).ToListAsync(); + Assert.AreEqual(2, userWorkgroupRoles.Count); + Assert.IsTrue(userWorkgroupRoles.Any(r => r.Role == Models.Role.Editor)); + Assert.IsTrue(userWorkgroupRoles.Any(r => r.Role == Models.Role.View)); + + // Remove a role + setRoleResult = await workgroupController.SetRole( + new Models.UserWorkgroupRole() + { + UserId = user.Id, + WorkgroupId = createdWorkgroup.Id, + Role = Models.Role.Editor, + IsActive = false, + }); + ActionResultAssert.IsOk(setRoleResult); + userWorkgroupRoles = await context.UserWorkgroupRoles.AsNoTracking().Where(r => r.WorkgroupId == createdWorkgroup.Id && r.UserId == user.Id).ToListAsync(); + Assert.AreEqual(1, userWorkgroupRoles.Count); + Assert.AreEqual(Models.Role.View, userWorkgroupRoles[0].Role); + } +}