From ff7136d7926cf935cfaa5d63d110bf3b9866acd4 Mon Sep 17 00:00:00 2001 From: Emily <44536222+emi-hi@users.noreply.github.com> Date: Mon, 4 Mar 2024 12:28:02 -0800 Subject: [PATCH] Feat Cthub 168 PARTIAL admin page (#193) * feat: adds backend work for retrieving current user and user list * feat: adds (UNSTYLED) frontend work for listing users and permissions * chore: removes idir serializermethodField * feat: adds styling to user admin --- django/api/serializers/permission.py | 16 ++++ django/api/serializers/user.py | 36 +++++++++ django/api/urls.py | 4 + django/api/viewsets/user.py | 56 +++++++++++++ react/src/app/styles/App.scss | 41 ++++++++++ react/src/app/styles/Users.scss | 25 ++++++ react/src/app/styles/index.scss | 26 +----- react/src/uploads/UploadContainer.js | 11 +++ react/src/uploads/components/UploadPage.js | 4 +- react/src/users/UsersContainer.js | 38 +++++++++ react/src/users/components/UsersPage.js | 93 ++++++++++++++++++++++ react/src/users/routes.js | 9 +++ 12 files changed, 333 insertions(+), 26 deletions(-) create mode 100644 django/api/serializers/permission.py create mode 100644 django/api/serializers/user.py create mode 100644 django/api/viewsets/user.py create mode 100644 react/src/app/styles/Users.scss create mode 100644 react/src/users/UsersContainer.js create mode 100644 react/src/users/components/UsersPage.js create mode 100644 react/src/users/routes.js diff --git a/django/api/serializers/permission.py b/django/api/serializers/permission.py new file mode 100644 index 00000000..73efc23e --- /dev/null +++ b/django/api/serializers/permission.py @@ -0,0 +1,16 @@ +from rest_framework.serializers import ModelSerializer, SerializerMethodField + +from api.models.permission import Permission +from api.models.user_permission import UserPermission + +class PermissionSerializer(ModelSerializer): + description = SerializerMethodField() + def get_description(self, obj): + permission = Permission.objects.filter(id=obj.permission_id).first() + if permission: + return permission.description + class Meta: + model = Permission + fields = ( + 'id', 'description', + ) diff --git a/django/api/serializers/user.py b/django/api/serializers/user.py new file mode 100644 index 00000000..fc2b490f --- /dev/null +++ b/django/api/serializers/user.py @@ -0,0 +1,36 @@ +""" +Further reading: +https://www.django-rest-framework.org/api-guide/serializers/ +""" +from rest_framework.serializers import ModelSerializer, SerializerMethodField + +from api.models.user import User +from api.models.user_permission import UserPermission +from api.serializers.permission import PermissionSerializer + +class UserSerializer(ModelSerializer): + """ + Default Serializer for User + """ + user_permissions = SerializerMethodField() + + def get_user_permissions(self, obj): + user_permission = UserPermission.objects.filter(user_id=obj.id) + permissions = PermissionSerializer(user_permission, read_only=True, many=True) + return permissions.data + + class Meta: + model = User + fields = ('idir', 'user_permissions') + + +class UserSaveSerializer(ModelSerializer): + def update(self, instance, validated_data): + request = self.context.get('request') + permissions = validated_data.pop('permissions') + print(request) + print(permissions) + #check if user exists, if not add them + + #update user_permissions + diff --git a/django/api/urls.py b/django/api/urls.py index 2ee60438..1d2c48db 100644 --- a/django/api/urls.py +++ b/django/api/urls.py @@ -20,6 +20,7 @@ from api.viewsets.icbc_data import IcbcViewset from api.viewsets.minio import MinioViewSet from api.viewsets.upload import UploadViewset +from api.viewsets.user import UserViewSet ROUTER = routers.SimpleRouter(trailing_slash=False) @@ -34,6 +35,9 @@ ROUTER.register( r'minio', MinioViewSet, basename='minio' ) +ROUTER.register( + r'users', UserViewSet +) urlpatterns = [ path('admin/', admin.site.urls), diff --git a/django/api/viewsets/user.py b/django/api/viewsets/user.py new file mode 100644 index 00000000..13d0629c --- /dev/null +++ b/django/api/viewsets/user.py @@ -0,0 +1,56 @@ +from rest_framework.decorators import action +from rest_framework.permissions import AllowAny +from rest_framework.response import Response +from rest_framework.viewsets import GenericViewSet +from api.models.user import User +from api.serializers.user import UserSerializer, UserSaveSerializer + +class UserViewSet(GenericViewSet): + """ + This viewset automatically provides `list`, `create`, `retrieve`, + and `update` actions. + """ + permission_classes = (AllowAny,) + http_method_names = ['get', 'post', 'put', 'patch'] + queryset = User.objects.all() + + serializer_classes = { + 'default': UserSerializer, + 'update': UserSaveSerializer, + 'create': UserSaveSerializer, + } + + + def get_serializer_class(self): + if self.action in list(self.serializer_classes.keys()): + return self.serializer_classes[self.action] + + return self.serializer_classes['default'] + + + @action(detail=False) + def current(self, request): + """ + Get the current user + """ + user = User.objects.filter(idir=request.user).first() + serializer = self.get_serializer(user) + return Response(serializer.data) + + def list(self, request): + request = self.request + ##check if user is admin before producing list of all users + users = User.objects.all() + current_user = users.filter(idir=request.user).first() + if current_user: + current_user_serializer = UserSerializer(current_user) + current_user_permissions = current_user_serializer.data['user_permissions'] + is_admin = False + if current_user_permissions: + for i in current_user_permissions: + for v in i.values(): + if v == 'admin': + is_admin = True + if is_admin == True: + serializer = UserSerializer(users, many=True) + return Response(serializer.data) \ No newline at end of file diff --git a/react/src/app/styles/App.scss b/react/src/app/styles/App.scss index 66b0427c..b05e911f 100644 --- a/react/src/app/styles/App.scss +++ b/react/src/app/styles/App.scss @@ -2,6 +2,17 @@ Base styling for "App" ie General Container */ +$bg-alt-black: #1E1F21; +$bg-primary: #244074; +$bg-black: #000; +$bg-white: #FFF; +$default-text-color: #000; +$default-table-border-color: rgba(249, 249, 249, 0.2); +$default-table-color: #F9F9F9; +$highlight: #0078FD; +$default-link-blue: #568DBA; +$default-background-grey: #F2F2F2; +$md: 991px; .App { .App-header { @@ -38,3 +49,33 @@ ie General Container padding: 0 1rem; } } + + +body { + background-color: $bg-white; + color: $default-text-color; + font-family: 'Roboto', 'Open Sans', sans-serif; + font-weight: 400; + height: 100%; + margin: 0; +} + +h2, h3, h4 { + font-family: 'Roboto', 'Open Sans', sans-serif; + color: #003366; + font-weight: 500; +} + +h2 { + font-size: 20px; +} + +h3 { + display: inline; + font-size: 17px; +} + +h4 { + font-size: 14px; +} + diff --git a/react/src/app/styles/Users.scss b/react/src/app/styles/Users.scss new file mode 100644 index 00000000..564814c6 --- /dev/null +++ b/react/src/app/styles/Users.scss @@ -0,0 +1,25 @@ +.add-user-box { + height: 4rem; + background-color: $default-background-grey; + margin-bottom: 1rem; + width: 40%; +} + +.permissions { + background-color: $default-background-grey; + align-content: space-around; + width: 10%; + .checkbox { + width: 40%; + } +} + +.user-input { + margin-left: 10px; + margin-right: 10px; + background-color: white; +} + +.button-dark-blue { + background-color: #003366 !important; +} \ No newline at end of file diff --git a/react/src/app/styles/index.scss b/react/src/app/styles/index.scss index e9655906..1a7bf42a 100644 --- a/react/src/app/styles/index.scss +++ b/react/src/app/styles/index.scss @@ -1,30 +1,6 @@ -/* -Base Style -Everything else will generally be controlled via bootstrap -*/ -$bg-alt-black: #1E1F21; -$bg-primary: #244074; -$bg-black: #000; -$bg-white: #FFF; -$default-text-color: #000; -$default-table-border-color: rgba(249, 249, 249, 0.2); -$default-table-color: #F9F9F9; -$highlight: #0078FD; -$default-link-blue: #568DBA; -$default-background-grey: #F2F2F2; -$md: 991px; - @import 'App.scss'; @import 'Login.scss'; @import 'ReactTable.scss'; @import 'FileUpload.scss'; @import 'Roboto.scss'; - -body { - background-color: $bg-white; - color: $default-text-color; - font-family: 'Roboto', 'Open Sans', sans-serif; - font-weight: 700; - height: 100%; - margin: 0; -} \ No newline at end of file +@import 'Users.scss'; diff --git a/react/src/uploads/UploadContainer.js b/react/src/uploads/UploadContainer.js index c312d410..8bcbf52d 100644 --- a/react/src/uploads/UploadContainer.js +++ b/react/src/uploads/UploadContainer.js @@ -4,8 +4,10 @@ import CircularProgress from '@mui/material/CircularProgress'; import Alert from '@mui/material/Alert'; import React, { useState, useEffect } from 'react'; import ROUTES_UPLOAD from './routes'; +import ROUTES_USERS from '../users/routes'; import UploadPage from './components/UploadPage'; import AlertDialog from '../app/components/AlertDialog'; +import UsersContainer from '../users/UsersContainer'; const UploadContainer = () => { const [uploadFiles, setUploadFiles] = useState([]); // array of objects for files to be uploaded @@ -18,6 +20,7 @@ const UploadContainer = () => { const [alertSeverity, setAlertSeverity] = useState(''); // existing data with what is being uploaded const [open, setOpen] = useState(false); + const [adminUser, setAdminUser] = useState(false); const dialogue = 'Selecting replace will delete all previously uploaded records for this dataset'; const leftButtonText = 'Cancel'; const rightButtonText = 'Replace existing data'; @@ -34,6 +37,12 @@ const UploadContainer = () => { axios.get(ROUTES_UPLOAD.LIST).then((response) => { setDatasetList(response.data); setLoading(false); + axios.get(ROUTES_USERS.CURRENT).then((currentUserResp) => { + const permissions = currentUserResp.data.user_permissions.map((each) => each.description); + if (permissions.includes('admin')) { + setAdminUser(true); + } + }); }); }; @@ -137,6 +146,8 @@ const UploadContainer = () => { handleRadioChange={handleRadioChange} downloadSpreadsheet={downloadSpreadsheet} /> + {adminUser + && } ); diff --git a/react/src/uploads/components/UploadPage.js b/react/src/uploads/components/UploadPage.js index 8ea4c15d..81a51c86 100644 --- a/react/src/uploads/components/UploadPage.js +++ b/react/src/uploads/components/UploadPage.js @@ -33,7 +33,9 @@ const UploadPage = (props) => {
- Dataset to Upload     +

+ Select Program     +

{ handleRadioChange(event); }} /> + { handleRadioChange(event); }} /> + + + {user.idir} + + + + + + ); + }; + return ( + + +
+

Admin

+
+ + + + +

+ IDIR Username +

+
+ + + + + + +
+
+
+ + + +

Upload

+
+ +

Admin

+
+
+ {users.map((user) => ( + userRow(user) + ))} + + + +
+
+
+ ); +}; +UsersPage.propTypes = { + users: PropTypes.arrayOf(PropTypes.shape()).isRequired, +}; +export default UsersPage; diff --git a/react/src/users/routes.js b/react/src/users/routes.js new file mode 100644 index 00000000..945af831 --- /dev/null +++ b/react/src/users/routes.js @@ -0,0 +1,9 @@ +const API_BASE_PATH = '/api/users'; + +const USERS = { + LIST: API_BASE_PATH, + CURRENT: `${API_BASE_PATH}/current`, + +}; + +export default USERS;