diff --git a/src/api_service/websocket_functions.py b/src/api_service/websocket_functions.py
index fec39e9f..237e34b9 100644
--- a/src/api_service/websocket_functions.py
+++ b/src/api_service/websocket_functions.py
@@ -106,3 +106,25 @@ def send_update_cluster_label_set_command(user_id: int):
'command': 'update_cluster_labels_sets'
}
send_message(user_group_name, message)
+
+def send_update_institutions_command(user_id: int):
+ """
+ Sends a message indicating that a Institution state update has occurred
+ @param user_id: Institution's user's id to send the WS message
+ """
+ user_group_name = f'notifications_{user_id}'
+ message = {
+ 'command': 'update_institutions'
+ }
+ send_message(user_group_name, message)
+
+def send_update_user_for_institution_command(user_id: int):
+ """
+ Sends a message indicating that a Institution_user state update has occurred
+ @param user_id: Institution's user's id to send the WS message
+ """
+ user_group_name = f'notifications_{user_id}'
+ message = {
+ 'command': 'update_user_for_institution'
+ }
+ send_message(user_group_name, message)
\ No newline at end of file
diff --git a/src/frontend/static/frontend/src/components/MainNavbar.tsx b/src/frontend/static/frontend/src/components/MainNavbar.tsx
index 1d97cc26..f4e4e5ad 100644
--- a/src/frontend/static/frontend/src/components/MainNavbar.tsx
+++ b/src/frontend/static/frontend/src/components/MainNavbar.tsx
@@ -99,6 +99,9 @@ const MainNavbar = (props: MainNavbarProps) => {
+
+
+
}
{/* Analysis menu */}
@@ -173,21 +176,21 @@ const MainNavbar = (props: MainNavbarProps) => {
/>
}
-
- {/* Institutions panel (only for user who are admin of at least one institution) */}
- {currentUser.is_institution_admin &&
-
- }
}
+ {/* Institutions */}
+ {currentUser &&
+
+
+ Institutions
+
+
+
+ }
+
{/* About us */}
diff --git a/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx b/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx
index a3b58c97..8b0cd8cf 100644
--- a/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx
+++ b/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx
@@ -4,8 +4,8 @@ import { Header, Button, Modal, Table, DropdownItemProps, Icon, Confirm, Form }
import { DjangoCGDSStudy, DjangoSurvivalColumnsTupleSimple, DjangoTag, DjangoUserFile, TagType } from '../../utils/django_interfaces'
import ky, { Options } from 'ky'
import { getDjangoHeader, alertGeneralError, formatDateLocale, cleanRef, getFilenameFromSource, makeSourceAndAppend, getDefaultSource } from '../../utils/util_functions'
-import { NameOfCGDSDataset, Nullable, CustomAlert, CustomAlertTypes, SourceType, OkResponse } from '../../utils/interfaces'
-import { Biomarker, BiomarkerType, BiomarkerOrigin, ConfirmModal, FormBiomarkerData, MoleculesSectionData, MoleculesTypeOfSelection, SaveBiomarkerStructure, SaveMoleculeStructure, FeatureSelectionPanelData, SourceStateBiomarker, FeatureSelectionAlgorithm, FitnessFunction, FitnessFunctionParameters, BiomarkerState, AdvancedAlgorithm as AdvancedAlgorithmParameters, BBHAVersion, BiomarkerSimple, CrossValidationParameters } from './types'
+import { NameOfCGDSDataset, Nullable, CustomAlert, CustomAlertTypes, SourceType, OkResponse, ConfirmModal } from '../../utils/interfaces'
+import { Biomarker, BiomarkerType, BiomarkerOrigin, FormBiomarkerData, MoleculesSectionData, MoleculesTypeOfSelection, SaveBiomarkerStructure, SaveMoleculeStructure, FeatureSelectionPanelData, SourceStateBiomarker, FeatureSelectionAlgorithm, FitnessFunction, FitnessFunctionParameters, BiomarkerState, AdvancedAlgorithm as AdvancedAlgorithmParameters, BBHAVersion, BiomarkerSimple, CrossValidationParameters } from './types'
import { ManualForm } from './modalContentBiomarker/manualForm/ManualForm'
import { PaginatedTable, PaginationCustomFilter } from '../common/PaginatedTable'
import { TableCellWithTitle } from '../common/TableCellWithTitle'
@@ -1704,7 +1704,7 @@ export class BiomarkersPanel extends React.Component<{}, BiomarkersPanelState> {
{/* Stop button */}
{isInProcess &&
this.setState({ biomarkerToStop: biomarker })}
/>
}
@@ -1712,7 +1712,7 @@ export class BiomarkersPanel extends React.Component<{}, BiomarkersPanelState> {
{/* Delete button */}
{!isInProcess &&
this.confirmBiomarkerDeletion(biomarker)}
/>
diff --git a/src/frontend/static/frontend/src/components/biomarkers/types.ts b/src/frontend/static/frontend/src/components/biomarkers/types.ts
index 9411a955..20e63fbc 100644
--- a/src/frontend/static/frontend/src/components/biomarkers/types.ts
+++ b/src/frontend/static/frontend/src/components/biomarkers/types.ts
@@ -99,13 +99,6 @@ interface Biomarker extends BiomarkerSimple {
mrnas: SaveMoleculeStructure[]
}
-interface ConfirmModal {
- confirmModal: boolean,
- headerText: string,
- contentText: string,
- onConfirm: Function,
-}
-
/** Represents a molecule info to show in molecules Dropdown. */
type MoleculeSymbol = {
key: string,
@@ -630,7 +623,6 @@ export {
MoleculesMultipleSelection,
MoleculesSectionData,
MoleculeSectionItem,
- ConfirmModal,
MoleculeSymbol,
MoleculesSymbolFinder,
ClusteringScoringMethod,
diff --git a/src/frontend/static/frontend/src/components/institutions/InstitutionForm.tsx b/src/frontend/static/frontend/src/components/institutions/InstitutionForm.tsx
new file mode 100644
index 00000000..ef82c92c
--- /dev/null
+++ b/src/frontend/static/frontend/src/components/institutions/InstitutionForm.tsx
@@ -0,0 +1,176 @@
+import ky from 'ky'
+import React, { useEffect, useState } from 'react'
+import { Form, Button } from 'semantic-ui-react'
+import { getDjangoHeader } from '../../utils/util_functions'
+import { CustomAlertTypes, Nullable } from '../../utils/interfaces'
+import { DjangoInstitution } from '../../utils/django_interfaces'
+
+const defaultForm: {
+ id: undefined | number;
+ name: string;
+ location: string;
+ email: string;
+ telephone_number: string;
+ isLoading: boolean;
+} = {
+ id: undefined,
+ name: '',
+ location: '',
+ email: '',
+ telephone_number: '',
+ isLoading: false
+}
+
+declare const urlCreateInstitution: string
+declare const urlEditInstitution: string
+
+interface Props {
+ institutionToEdit: Nullable,
+ handleResetInstitutionToEdit: (callbackToCancel: () => void) => void,
+ handleUpdateAlert(isOpen: boolean, type: CustomAlertTypes, message: string, callback: Nullable, isEdit?: boolean): void,
+}
+
+const InstitutionForm = (props: Props) => {
+ const [formData, setFormData] = useState(defaultForm)
+
+ /**
+ * Handle form state data
+ * @param e event
+ * @param param.name key name to edit field in form
+ * @param param.value key value to edit field in form
+ */
+
+ const handleChange = (e, { name, value }) => {
+ setFormData({ ...formData, [name]: value })
+ }
+
+ /**
+ * Handle user form to edit or create
+ */
+ const handleSubmit = () => {
+ const myHeaders = getDjangoHeader()
+
+ if (props.institutionToEdit?.id) {
+ const jsonParams = {
+ id: formData.id,
+ name: formData.name,
+ location: formData.location,
+ email: formData.email,
+ telephone_number: formData.telephone_number
+ }
+ const editUrl = `${urlEditInstitution}/${formData.id}/`
+
+ ky.patch(editUrl, { headers: myHeaders, json: jsonParams }).then((response) => {
+ setFormData(prevState => ({ ...prevState, isLoading: true }))
+ response.json().then((jsonResponse: DjangoInstitution) => {
+ props.handleUpdateAlert(true, CustomAlertTypes.SUCCESS, `Institution ${jsonResponse.name} Updated!`, () => setFormData(defaultForm), true)
+ }).catch((err) => {
+ setFormData(prevState => ({ ...prevState, isLoading: false }))
+ props.handleUpdateAlert(true, CustomAlertTypes.ERROR, 'Error creating an institution!', () => setFormData(prevState => ({ ...prevState, isLoading: false })))
+ console.error('Error parsing JSON ->', err)
+ })
+ }).catch((err) => {
+ props.handleUpdateAlert(true, CustomAlertTypes.ERROR, 'Error creating an institution!', () => setFormData(prevState => ({ ...prevState, isLoading: false })))
+ console.error('Error adding new Institution ->', err)
+ })
+ } else {
+ const jsonParams = {
+ name: formData.name,
+ location: formData.location,
+ email: formData.email,
+ telephone_number: formData.telephone_number
+ }
+ ky.post(urlCreateInstitution, { headers: myHeaders, json: jsonParams }).then((response) => {
+ setFormData(prevState => ({ ...prevState, isLoading: true }))
+ response.json().then((jsonResponse: DjangoInstitution) => {
+ props.handleUpdateAlert(true, CustomAlertTypes.SUCCESS, `Institution ${jsonResponse.name} created!`, () => setFormData(defaultForm))
+ }).catch((err) => {
+ props.handleUpdateAlert(true, CustomAlertTypes.ERROR, 'Error creating an institution!', () => setFormData(prevState => ({ ...prevState, isLoading: false })))
+ setFormData(prevState => ({ ...prevState, isLoading: false }))
+ console.error('Error parsing JSON ->', err)
+ })
+ }).catch((err) => {
+ props.handleUpdateAlert(true, CustomAlertTypes.ERROR, 'Error creating an institution!', () => setFormData(prevState => ({ ...prevState, isLoading: false })))
+ console.error('Error adding new Institution ->', err)
+ })
+ }
+ }
+
+ /**
+ * Handle if user Reset or cancel edit
+ */
+ const handleCancelForm = () => {
+ if (props.institutionToEdit) {
+ props.handleResetInstitutionToEdit(() => setFormData(defaultForm))
+ } else {
+ setFormData(defaultForm)
+ }
+ }
+
+ /**
+ * use effect to handle if a institution for edit is sent
+ */
+ useEffect(() => {
+ if (props.institutionToEdit) {
+ setFormData({
+ ...props.institutionToEdit,
+ id: props.institutionToEdit?.id,
+ isLoading: false
+ })
+ }
+ }, [props.institutionToEdit])
+ return (
+ <>
+
+
+
+
+
+
+
+ >
+ )
+}
+
+export default InstitutionForm
diff --git a/src/frontend/static/frontend/src/components/institutions/InstitutionModal.tsx b/src/frontend/static/frontend/src/components/institutions/InstitutionModal.tsx
new file mode 100644
index 00000000..8ae7c689
--- /dev/null
+++ b/src/frontend/static/frontend/src/components/institutions/InstitutionModal.tsx
@@ -0,0 +1,256 @@
+import React, { useContext, useEffect, useRef, useState } from 'react'
+import {
+ ModalHeader,
+ ModalContent,
+ Modal,
+ Icon,
+ Segment,
+ Table,
+ Button,
+ Select
+} from 'semantic-ui-react'
+import { DjangoInstitutionUser } from '../../utils/django_interfaces'
+import { CustomAlertTypes, Nullable, SemanticListItem } from '../../utils/interfaces'
+import { PaginatedTable } from '../common/PaginatedTable'
+import { TableCellWithTitle } from '../common/TableCellWithTitle'
+import { InstitutionTableData } from './InstitutionsPanel'
+import ky from 'ky'
+import { getDjangoHeader } from '../../utils/util_functions'
+import { CurrentUserContext } from '../Base'
+
+declare const urlGetUsersCandidates: string
+declare const urlEditInstitutionAdmin: string
+declare const urlNonUserListInstitution: string
+declare const urlAddRemoveUserToInstitution: string
+
+export interface InstitutionModalState {
+ isOpen: boolean,
+ institution: Nullable
+}
+
+interface Props extends InstitutionModalState {
+ /* Close modal function */
+ handleCloseModal: () => void,
+ handleChangeConfirmModalState: (setOption: boolean, headerText: string, contentText: string, onConfirm: Function) => void,
+ handleUpdateAlert(isOpen: boolean, type: CustomAlertTypes, message: string, callback: Nullable): void,
+}
+
+/**
+ * Institution modal
+ * @param {Props} props Props for component
+ * @returns Component
+ */
+export const InstitutionModal = (props: Props) => {
+ const [userIdToAdd, setUserIdToAdd] = useState(0)
+ const [userList, setUserList] = useState([])
+ const abortController = useRef(new AbortController())
+ const currentUser = useContext(CurrentUserContext)
+
+ /**
+ * Function to add user to institution.
+ */
+ const handleAddUser = () => {
+ if (userIdToAdd) {
+ const myHeaders = getDjangoHeader()
+ const body = {
+ userId: userIdToAdd,
+ isAdding: true,
+ institutionId: props.institution?.id
+ }
+ ky.post(urlAddRemoveUserToInstitution, { headers: myHeaders, signal: abortController.current.signal, json: body }).then((response) => {
+ response.json().then(() => {
+ usersListNonInInstitution()
+ }).catch((err) => {
+ console.error('Error parsing JSON ->', err)
+ })
+ }).catch((err) => {
+ console.error('Error getting users ->', err)
+ })
+ }
+ }
+
+ /**
+ * Function to remove user from institution.
+ * @param idUserCandidate user id to remove from institution.
+ */
+ const handleRemoveUser = (idUserCandidate: number) => {
+ const myHeaders = getDjangoHeader()
+ const body = {
+ userId: idUserCandidate,
+ isAdding: false,
+ institutionId: props.institution?.id
+ }
+ ky.post(urlAddRemoveUserToInstitution, { headers: myHeaders, signal: abortController.current.signal, json: body }).then((response) => {
+ response.json().then(() => {
+ usersListNonInInstitution()
+ }).catch((err) => {
+ console.error('Error parsing JSON ->', err)
+ })
+ }).catch((err) => {
+ console.error('Error getting users ->', err)
+ })
+ }
+
+ /**
+ * Function to manage admin.
+ * @param adminSwitched switch if true is going to be admin, false if going to be normal user
+ * @param idInstitution institution id.
+ */
+ const handleSwitchUserAdmin = (adminSwitched: boolean, idInstitution: number) => {
+ const myHeaders = getDjangoHeader()
+ const editUrl = `${urlEditInstitutionAdmin}/${idInstitution}/`
+
+ ky.patch(editUrl, { headers: myHeaders }).then((response) => {
+ response.json().then(() => {
+ props.handleUpdateAlert(true, CustomAlertTypes.SUCCESS, adminSwitched ? 'User is admin!' : 'User is not admin!', null)
+ }).catch((err) => {
+ props.handleUpdateAlert(true, CustomAlertTypes.ERROR, 'Error for switch role!', null)
+ console.error('Error parsing JSON ->', err)
+ })
+ }).catch((err) => {
+ props.handleUpdateAlert(true, CustomAlertTypes.ERROR, 'Error for switch role!', null)
+ console.error('Error adding new Institution ->', err)
+ })
+ }
+
+ /**
+ * Function to search users that are not in institution.
+ */
+ const usersListNonInInstitution = () => {
+ const myHeaders = getDjangoHeader()
+
+ const editUrl = `${urlNonUserListInstitution}/${props.institution?.id}/`
+
+ ky.get(editUrl, { headers: myHeaders, signal: abortController.current.signal }).then((response) => {
+ response.json().then((jsonResponse: { id: number, username: string }[]) => {
+ setUserList(jsonResponse.map(user => ({ key: user.id.toString(), value: user.id.toString(), text: user.username })))
+ }).catch((err) => {
+ console.error('Error parsing JSON ->', err)
+ })
+ }).catch((err) => {
+ console.error('Error getting users ->', err)
+ })
+ }
+
+ useEffect(() => {
+ if (props.institution?.is_user_admin && props.institution?.id) {
+ usersListNonInInstitution()
+ }
+
+ return () => {
+ abortController.current.abort()
+ }
+ }, [props.institution?.id])
+
+ return (
+ }
+ >
+ {props.institution?.name}
+
+
+
+
+
+ )
+}
diff --git a/src/frontend/static/frontend/src/components/institutions/InstitutionUsersInfo.tsx b/src/frontend/static/frontend/src/components/institutions/InstitutionUsersInfo.tsx
deleted file mode 100644
index 6c9b9a3c..00000000
--- a/src/frontend/static/frontend/src/components/institutions/InstitutionUsersInfo.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import React, { useContext } from 'react'
-import { List, Icon, Header, Segment, ListContent } from 'semantic-ui-react'
-import { DjangoInstitution, DjangoUser } from '../../utils/django_interfaces'
-import { Nullable } from '../../utils/interfaces'
-import { CurrentUserContext } from '../Base'
-import Avatar from 'react-avatar'
-
-/**
- * Component's props
- */
-interface InstitutionUsersInfoProps {
- /** Selected Institution to show its users */
- selectedInstitution: Nullable,
- /** Callback to show a modal to remove an User from a the selected Institution */
- confirmFileDeletion: (institutionUser: DjangoUser) => void
-}
-
-/**
- * Renders an Institution's Users info and some extra functionality like
- * add a User to the Institution
- * @param props Component's props
- * @returns Component
- */
-export const InstitutionUsersInfo = (props: InstitutionUsersInfoProps) => {
- const currentUser = useContext(CurrentUserContext)
- const selectedInstitution = props.selectedInstitution
-
- if (!selectedInstitution) {
- return null
- }
-
- return (
-
-
-
- Users in {selectedInstitution.name}
-
-
-
-
- {selectedInstitution.users.map((institutionUser) => (
-
-
- {/* Remove from Institution button */}
- {institutionUser.id !== currentUser?.id &&
- { props.confirmFileDeletion(institutionUser) }}
- />
- }
-
-
-
- {institutionUser.username}
-
-
- ))}
-
-
-
- )
-}
diff --git a/src/frontend/static/frontend/src/components/institutions/InstitutionsList.tsx b/src/frontend/static/frontend/src/components/institutions/InstitutionsList.tsx
deleted file mode 100644
index 23a5727b..00000000
--- a/src/frontend/static/frontend/src/components/institutions/InstitutionsList.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import React from 'react'
-import { List, Icon } from 'semantic-ui-react'
-import { DjangoInstitution } from '../../utils/django_interfaces'
-import { Nullable } from '../../utils/interfaces'
-import '../../css/institutions.css'
-
-/**
- * Component's props
- */
-interface InstitutionsListProps {
- /** List of institutions */
- institutions: DjangoInstitution[],
- /** Callback of 'Show users' button */
- showUsers: (institution: DjangoInstitution) => void
- /** Selected institution to highlight */
- selectedInstitution: Nullable
-}
-
-/**
- * Renders a list of Institutions the current User belongs to
- * @param props Component's props
- * @returns Component
- */
-export const InstitutionsList = (props: InstitutionsListProps) => {
- return (
-
- {props.institutions.map((institution) => (
-
-
- {/* Edit button */}
- props.showUsers(institution)}
- />
-
-
- {institution.name}
-
- {institution.location}
-
- {institution.email} {institution.telephone_number}
-
-
-
- ))}
-
- )
-}
diff --git a/src/frontend/static/frontend/src/components/institutions/InstitutionsPanel.tsx b/src/frontend/static/frontend/src/components/institutions/InstitutionsPanel.tsx
index af67dc4c..5ea37f14 100644
--- a/src/frontend/static/frontend/src/components/institutions/InstitutionsPanel.tsx
+++ b/src/frontend/static/frontend/src/components/institutions/InstitutionsPanel.tsx
@@ -1,19 +1,20 @@
import React from 'react'
import { Base } from '../Base'
-import { Grid, Icon, Segment, Header, Form, DropdownItemProps } from 'semantic-ui-react'
-import { DjangoInstitution, DjangoUserCandidates, DjangoCommonResponse, DjangoAddRemoveUserToInstitutionInternalCode, DjangoResponseCode, DjangoUser } from '../../utils/django_interfaces'
+import { Grid, Icon, Segment, Header, Table, Confirm } from 'semantic-ui-react'
+import { DjangoInstitution, DjangoUserCandidates, DjangoUser } from '../../utils/django_interfaces'
import ky from 'ky'
-import { alertGeneralError, getDjangoHeader } from '../../utils/util_functions'
-import { InstitutionsList } from './InstitutionsList'
-import { InstitutionUsersInfo } from './InstitutionUsersInfo'
-import { RemoveUserFromInstitutionModal } from './RemoveUserFromInstitutionModal'
-import { Nullable } from '../../utils/interfaces'
+import { getDjangoHeader } from '../../utils/util_functions'
+import { ConfirmModal, CustomAlert, CustomAlertTypes, Nullable } from '../../utils/interfaces'
import { InfoPopup } from '../pipeline/experiment-result/gene-gem-details/InfoPopup'
+import { PaginatedTable } from '../common/PaginatedTable'
+import { TableCellWithTitle } from '../common/TableCellWithTitle'
+import InstitutionForm from './InstitutionForm'
+import { InstitutionModal, InstitutionModalState } from './InstitutionModal'
+import { Alert } from '../common/Alert'
// URLs defined in files.html
-declare const urlUserInstitutionsAsAdmin: string
-declare const urlGetUsersCandidates: string
-declare const urlAddRemoveUserToInstitution: string
+declare const urlGetUserInstitutions: string
+declare const urlDeleteInstitution: string
/**
* Component's state
@@ -29,6 +30,15 @@ interface InstitutionsPanelState {
removingUserFromInstitution: boolean,
selectedUserToRemove: Nullable,
showRemoveUserFromInstitutionModal: boolean,
+ modalState: InstitutionModalState,
+ institutionToEdit: Nullable,
+ alert: CustomAlert,
+ isDeletingInstitution: boolean,
+ confirmModal: ConfirmModal,
+}
+
+export interface InstitutionTableData extends DjangoInstitution {
+ is_user_admin: boolean
}
/**
@@ -52,22 +62,15 @@ export class InstitutionsPanel extends React.Component<{}, InstitutionsPanelStat
addingRemovingUserToInstitution: false,
removingUserFromInstitution: false,
selectedUserToRemove: null,
- showRemoveUserFromInstitutionModal: false
+ showRemoveUserFromInstitutionModal: false,
+ modalState: this.defaultModalState(),
+ institutionToEdit: null,
+ alert: this.getDefaultAlertProps(),
+ isDeletingInstitution: false,
+ confirmModal: this.getDefaultConfirmModal()
}
}
- selectedInstitutionChanged (institution: DjangoInstitution) {
- this.setState({
- selectedInstitution: institution
- })
- }
-
- /**
- * When the component has been mounted, It requests for
- * tags and files
- */
- componentDidMount () { this.getUserInstitutions() }
-
/**
* Abort controller if component unmount
*/
@@ -77,194 +80,165 @@ export class InstitutionsPanel extends React.Component<{}, InstitutionsPanelStat
}
/**
- * Fetches the Institutions which the current user belongs to
+ * Generates a default alert structure
+ * @returns Default alert.
*/
- getUserInstitutions () {
- ky.get(urlUserInstitutionsAsAdmin, { signal: this.abortController.signal }).then((response) => {
- response.json().then((institutions: DjangoInstitution[]) => {
- // If it's showing an institution, refresh it's state
- // For example, in the case of adding or removing a user to/from an Institution
- let newSelectedInstitution: Nullable = null
-
- if (this.state.selectedInstitution !== null) {
- newSelectedInstitution = institutions.find((institution) => {
- return institution.id === this.state.selectedInstitution?.id
- }) ?? null
- }
+ getDefaultAlertProps (): CustomAlert {
+ return {
+ message: '', // This have to change during cycle of component
+ isOpen: false,
+ type: CustomAlertTypes.SUCCESS,
+ duration: 500
+ }
+ }
- this.setState({ institutions, selectedInstitution: newSelectedInstitution })
- }).catch((err) => {
- console.log('Error parsing JSON ->', err)
- })
- }).catch((err) => {
- console.log("Error getting user's datasets ->", err)
- })
+ getDefaultConfirmModal = (): ConfirmModal => {
+ return {
+ confirmModal: false,
+ headerText: '',
+ contentText: '',
+ onConfirm: () => console.log('DefaultConfirmModalFunction, this should change during cycle of component')
+ }
}
/**
- * Fetches the User's uploaded files
+ * Reset the confirm modal, to be used again
*/
- searchUsers = () => {
- this.setState({ isFetchingUsersCandidates: true }, () => {
- const searchParams = {
- querySearch: this.state.searchUserText
- }
- ky.get(urlGetUsersCandidates, { searchParams, signal: this.abortController.signal }).then((response) => {
- this.setState({ isFetchingUsersCandidates: false })
- response.json().then((userCandidates: DjangoUserCandidates[]) => {
- this.setState({ userCandidates })
- }).catch((err) => {
- console.log('Error parsing JSON ->', err)
- })
- }).catch((err) => {
- if (!this.abortController.signal.aborted) {
- this.setState({ isFetchingUsersCandidates: false })
- }
-
- console.log("Error getting user's datasets ->", err)
- })
- })
+ handleCancelConfirmModalState () {
+ this.setState({ confirmModal: this.getDefaultConfirmModal() })
}
/**
- * Handles search user input changes
- * @param value Value to assign to the specified field
+ * Changes confirm modal state
+ * @param setOption New state of option
+ * @param headerText Optional text of header in confirm modal, by default will be empty
+ * @param contentText optional text of content in confirm modal, by default will be empty
+ * @param onConfirm Modal onConfirm callback
*/
- handleInputChange = (value: string) => {
- this.setState({ searchUserText: value }, () => {
- clearTimeout(this.filterTimeout)
- this.filterTimeout = window.setTimeout(this.searchUsers, 300)
- })
+ handleChangeConfirmModalState = (setOption: boolean, headerText: string, contentText: string, onConfirm: Function) => {
+ const confirmModal = this.state.confirmModal
+ confirmModal.confirmModal = setOption
+ confirmModal.headerText = headerText
+ confirmModal.contentText = contentText
+ confirmModal.onConfirm = onConfirm
+ this.setState({ confirmModal })
}
/**
- * Set a selected Institution in state to show it users
- * @param selectedInstitution Selected Institution to show users
+ * Update Alert
+ * @param isOpen flag to open or close alert.
+ * @param type type of alert.
+ * @param message message of alert.
+ * @param callback Callback function if is needed.
+ * @param isEdit option if is in edit mode.
*/
- showUsers = (selectedInstitution: DjangoInstitution) => { this.setState({ selectedInstitution }) }
+ handleUpdateAlert (isOpen: boolean, type: CustomAlertTypes, message: string, callback: Nullable, isEdit?: boolean) {
+ const alert = this.state.alert
+ alert.isOpen = isOpen
+ alert.type = type
+ alert.message = message
+ let institutionToEdit = this.state.institutionToEdit
+
+ if (isEdit) {
+ institutionToEdit = null
+ }
- /**
- * Cleans the inputs for adding a user to an Institution
- */
- cleanSearchAndCandidates () {
- this.setState({
- userCandidates: [],
- selectedUserIdToAdd: null,
- searchUserText: ''
- })
+ if (callback) {
+ callback()
+ this.setState({ alert, institutionToEdit })
+ } else {
+ this.setState({ alert, institutionToEdit })
+ }
}
/**
- * Sets as selected a new user to add him to an Institution,
- * @param selectedUserId Id of the selected user in the Dropdown
+ * Reset the confirm modal, to be used again
*/
- selectUser (selectedUserId) { this.setState({ selectedUserIdToAdd: selectedUserId }) }
+ handleCloseAlert = () => {
+ const alert = this.state.alert
+ alert.isOpen = false
+ this.setState({ alert })
+ }
/**
- * Makes a request to the server to add/remove a User to/from an Institution
- * @param userId Id of the user to add or remove
- * @param isAdding True if want to add, false if want to remove
+ * Default modal attributes
+ * @returns {InstitutionModalState} Default modal
*/
- addOrRemoveUserToInstitution = (userId: number, isAdding: boolean) => {
- // Sets the Request's Headers
- const myHeaders = getDjangoHeader()
-
- const params = {
- userId,
- institutionId: this.state.selectedInstitution?.id,
- isAdding
+ defaultModalState (): InstitutionModalState {
+ return {
+ isOpen: false,
+ institution: null
}
-
- const loadingFlag = isAdding ? 'addingRemovingUserToInstitution' : 'removingUserFromInstitution'
-
- this.setState({ [loadingFlag]: true }, () => {
- ky.post(urlAddRemoveUserToInstitution, { headers: myHeaders, json: params }).then((response) => {
- this.setState({ [loadingFlag]: false })
- response.json().then((jsonResponse: DjangoCommonResponse) => {
- this.cleanSearchAndCandidates()
-
- if (jsonResponse.status.code === DjangoResponseCode.SUCCESS) {
- this.getUserInstitutions()
- this.handleClose()
- } else {
- if (jsonResponse.status.internal_code === DjangoAddRemoveUserToInstitutionInternalCode.CANNOT_REMOVE_YOURSELF) {
- alert('You cannot remove yourself from the Institution!')
- } else {
- alertGeneralError()
- }
- }
- }).catch((err) => {
- console.log('Error parsing JSON ->', err)
- })
- }).catch((err) => {
- this.setState({ [loadingFlag]: false })
- console.log("Error getting user's datasets ->", err)
- })
- })
}
/**
- * Makes a request to the server to add a User from an Institution
+ * Close modal
*/
- addUserToInstitution = () => {
- if (this.state.selectedUserIdToAdd !== null) {
- this.addOrRemoveUserToInstitution(this.state.selectedUserIdToAdd, true)
- }
+ handleCloseModal () {
+ this.setState({ modalState: this.defaultModalState() })
}
/**
- * Makes a request to the server to remove a User from an Institution
+ * Open modal
+ * @param {InstitutionTableData} institution institution for modal.
*/
- removeUserFromInstitution = () => {
- if (this.state.selectedUserToRemove?.id) {
- this.addOrRemoveUserToInstitution(this.state.selectedUserToRemove.id, false)
+ handleOpenModal (institution: InstitutionTableData) {
+ const modalState = {
+ isOpen: true,
+ institution
}
+ this.setState({ modalState })
}
/**
- * Show a modal to confirm a removal of an User from an Institution
- * @param institutionUser Selected User to remove
+ * Reset institution.
+ * @param callbackToCancel callbackfunction.
*/
- confirmFileDeletion = (institutionUser: DjangoUser) => {
- this.setState({
- selectedUserToRemove: institutionUser,
- showRemoveUserFromInstitutionModal: true
- })
+ handleResetInstitutionToEdit (callbackToCancel: () => void) {
+ this.setState({ institutionToEdit: null })
+ callbackToCancel()
}
/**
- * Closes the removal confirm modals
+ * Set form to edit
+ * @param {InstitutionTableData} institution institution for modal.
*/
- handleClose = () => { this.setState({ showRemoveUserFromInstitutionModal: false }) }
+ handleSetInstitutionToEdit (institution: InstitutionTableData) {
+ this.setState({ institutionToEdit: institution })
+ }
/**
- * Checks if search input should be enabled or not
- * @returns True if user can choose a user to add to an Institution, false otherwise
+ * Delete institution.
+ * @param institutionId id from institution to delete.
*/
- formIsDisabled = (): boolean => !this.state.selectedInstitution || this.state.addingRemovingUserToInstitution
-
- render () {
- const userOptions: DropdownItemProps[] = this.state.userCandidates.map((userCandidate) => {
- return {
- key: userCandidate.id,
- text: `${userCandidate.username} (${userCandidate.email})`,
- value: userCandidate.id
- }
+ handleDeleteInstitution (institutionId: number) {
+ const url = `${urlDeleteInstitution}/${institutionId}/`
+ const myHeaders = getDjangoHeader()
+ ky.delete(url, { headers: myHeaders }).then((response) => {
+ response.json().then(() => {
+ this.handleUpdateAlert(true, CustomAlertTypes.SUCCESS, 'Institution deleted!', null)
+ }).catch((err) => {
+ this.handleUpdateAlert(true, CustomAlertTypes.ERROR, 'Error deleting an Institution!', null)
+ console.error('Error parsing JSON ->', err)
+ })
+ .finally(() => {
+ const confirmModal = this.state.confirmModal
+ confirmModal.confirmModal = false
+ this.setState({ confirmModal })
+ })
+ }).catch((err) => {
+ this.handleUpdateAlert(true, CustomAlertTypes.ERROR, 'Error deleting an Institution!', null)
+ console.error('Error adding new Institution ->', err)
+ }).finally(() => {
+ const confirmModal = this.state.confirmModal
+ confirmModal.confirmModal = false
+ this.setState({ confirmModal })
})
+ }
- const formIsDisabled = this.formIsDisabled()
-
+ render () {
return (
- {/* Modal to confirm User removal from an Institution */}
-
{/* List of institutions */}
@@ -277,65 +251,112 @@ export class InstitutionsPanel extends React.Component<{}, InstitutionsPanelStat
-
- this.handleResetInstitutionToEdit(callback)}
+ handleUpdateAlert={(isOpen: boolean, type: CustomAlertTypes, message: string, callback: Nullable, isEdit?: boolean) => this.handleUpdateAlert(isOpen, type, message, callback, isEdit)}
+ />
+ {/* Todo: remove component
+
+ /> */}
{/* Files overview panel */}
-
-
-
-
-
- {/* User Search */}
- this.selectUser(value)}
- onSearchChange={(_e, { searchQuery }) => this.handleInputChange(searchQuery)}
- disabled={formIsDisabled}
- loading={this.state.isFetchingUsersCandidates || this.state.addingRemovingUserToInstitution}
- />
-
-
- Add user
-
-
-
-
-
-
- {/* Institution's Users info */}
-
-
-
+
+ headerTitle='Institutions'
+ headers={[
+ { name: 'Name', serverCodeToSort: 'name', width: 3 },
+ { name: 'Location', serverCodeToSort: 'location', width: 1 },
+ { name: 'Email', serverCodeToSort: 'email', width: 1 },
+ { name: 'Phone number', serverCodeToSort: 'telephone_number', width: 1 },
+ { name: 'Actions', width: 1 }
+ ]}
+ defaultSortProp={{ sortField: 'upload_date', sortOrderAscendant: false }}
+ showSearchInput
+ searchLabel='Name'
+ searchPlaceholder='Search by name'
+ urlToRetrieveData={urlGetUserInstitutions}
+ updateWSKey='update_institutions'
+ mapFunction={(institution: InstitutionTableData) => {
+ return (
+
+
+
+
+
+
+ {/* Details button */}
+ this.handleOpenModal(institution)}
+ />
+
+ {/* Edit button */}
+
+ {
+ institution.is_user_admin &&
+ this.handleSetInstitutionToEdit(institution)}
+ />
+ }
+ {/* Delete button */}
+ {institution.is_user_admin &&
+ this.handleChangeConfirmModalState(true, 'Delete institution', `Are you sure about deleting institution ${institution.name}`, () => this.handleDeleteInstitution(institution.id as number))}
+ />
+ }
+
+
+ )
+ }}
+ />
+ ) => this.handleUpdateAlert(isOpen, type, message, callback)}
+ handleChangeConfirmModalState={this.handleChangeConfirmModalState}
+ handleCloseModal={() => this.handleCloseModal()}
+ isOpen={this.state.modalState.isOpen} institution={this.state.modalState.institution}
+ />
+
+ this.handleCancelConfirmModalState()}
+ onConfirm={() => {
+ this.state.confirmModal.onConfirm()
+ const confirmModal = this.state.confirmModal
+ confirmModal.confirmModal = false
+ this.setState({ confirmModal })
+ }}
+ />
)
}
diff --git a/src/frontend/static/frontend/src/components/institutions/RemoveUserFromInstitutionModal.tsx b/src/frontend/static/frontend/src/components/institutions/RemoveUserFromInstitutionModal.tsx
deleted file mode 100644
index 124ac020..00000000
--- a/src/frontend/static/frontend/src/components/institutions/RemoveUserFromInstitutionModal.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import React from 'react'
-import { Modal, Header, Button } from 'semantic-ui-react'
-import { DjangoUser, DjangoInstitution } from '../../utils/django_interfaces'
-import { Nullable } from '../../utils/interfaces'
-
-/**
- * Component's props
- */
-interface RemoveUserFromInstitutionModalProps {
- /** Selected User to remove from Institution */
- selectedUserToRemove: Nullable,
- /** Selected Institution */
- selectedInstitution: Nullable,
- /** Flag to show the modal */
- showRemoveUserModal: boolean,
- /** Flag to show a loading during request */
- removingUserFromInstitution: boolean,
- /** Callback to hide the modal */
- handleClose: () => void,
- /** Callback to remove the User from the Institution */
- removeUser: () => void,
-}
-
-/**
- * Renders a modal to confirm the removal of an User from an Institution
- * @param props Component's props
- * @returns Component
- */
-export const RemoveUserFromInstitutionModal = (props: RemoveUserFromInstitutionModalProps) => {
- if (!props.selectedUserToRemove || !props.selectedInstitution) {
- return null
- }
-
- return (
-
-
-
- Are you sure you want to remove {props.selectedUserToRemove.username} from the Institution {props.selectedInstitution.name}?
-
-
-
-
-
-
- )
-}
diff --git a/src/frontend/static/frontend/src/utils/django_interfaces.ts b/src/frontend/static/frontend/src/utils/django_interfaces.ts
index e731cd33..c5751235 100644
--- a/src/frontend/static/frontend/src/utils/django_interfaces.ts
+++ b/src/frontend/static/frontend/src/utils/django_interfaces.ts
@@ -446,7 +446,19 @@ interface DjangoInstitution {
location: string,
email: string,
telephone_number: string,
- users: DjangoUser[]
+ users?: DjangoUser[]
+}
+
+/**
+ * Django Institution user
+ */
+interface DjangoInstitutionUser {
+ user: {
+ id: number,
+ username: string,
+ },
+ id: number,
+ is_institution_admin: boolean,
}
/**
@@ -530,7 +542,7 @@ interface DjangoNumberSamplesInCommonResult extends DjangoCommonResponse ({ ...command, functionToExecute: debounce(command.functionToExecute, 300) }))
+
+ this.websocket.onmessage = function (event) {
try {
const dataParsed: WebsocketMessage = JSON.parse(event.data)
const commandToAttend = config.commandsToAttend.find((commandToAttend) => commandToAttend.key === dataParsed.command)
// If matches with any function defined by the user, executes it
+
if (commandToAttend !== undefined) {
commandToAttend.functionToExecute()
}
@@ -38,7 +42,7 @@ class WebsocketClientCustom {
console.log('Data:', event.data)
console.log('Exception:', ex)
}
- }, 300)
+ }
this.websocket.onerror = function (event) {
console.log('Error establishing websocket connection', event)
diff --git a/src/frontend/templates/frontend/institutions.html b/src/frontend/templates/frontend/institutions.html
index bb76824b..f3139720 100644
--- a/src/frontend/templates/frontend/institutions.html
+++ b/src/frontend/templates/frontend/institutions.html
@@ -14,8 +14,13 @@
{% block js %}
{% render_bundle 'institutions' 'js' %}
diff --git a/src/institutions/__pycache__/models.cpython-312.pyc.2847318479840 b/src/institutions/__pycache__/models.cpython-312.pyc.2847318479840
new file mode 100644
index 00000000..2c0294ff
Binary files /dev/null and b/src/institutions/__pycache__/models.cpython-312.pyc.2847318479840 differ
diff --git a/src/institutions/models.py b/src/institutions/models.py
index 31171db3..93aff1da 100644
--- a/src/institutions/models.py
+++ b/src/institutions/models.py
@@ -1,5 +1,6 @@
from django.contrib.auth import get_user_model
from django.db import models
+from api_service.websocket_functions import send_update_institutions_command, send_update_user_for_institution_command
class Institution(models.Model):
@@ -10,6 +11,24 @@ class Institution(models.Model):
telephone_number = models.CharField(max_length=30, blank=True, null=True)
users = models.ManyToManyField(get_user_model(), through='InstitutionAdministration')
+ class Meta:
+ ordering = ['-id']
+
+ def delete(self, *args, **kwargs) -> None:
+ """Deletes the instance and sends a websockets message to update state in the frontend """
+ related_users = list(self.users.all())
+ super().delete(*args, **kwargs)
+ # Sends a websockets message to all users for update the institutions state in the frontend
+ for user in related_users:
+ send_update_institutions_command(user.id)
+
+ def save(self, *args, **kwargs) -> None:
+ """Everytime the institution status changes, uses websocket to update state in the frontend"""
+ super().save(*args, **kwargs)
+ # Sends a websockets message to all users for update the institutions state in the frontend
+ for user in self.users.all():
+ send_update_institutions_command(user.id)
+
def __str__(self):
return self.name
@@ -20,6 +39,27 @@ class InstitutionAdministration(models.Model):
institution = models.ForeignKey(Institution, on_delete=models.CASCADE)
is_institution_admin = models.BooleanField(default=False)
+ class Meta:
+ ordering = ['-id']
+
+ def delete(self, *args, **kwargs) -> None:
+ """Deletes the instance and sends a websockets message to update state in the frontend """
+ related_users = list(self.institution.users.all())
+ super().delete(*args, **kwargs)
+ # Sends a websockets message to all users for update the institutions state in the frontend
+ for user in related_users:
+ send_update_institutions_command(user.id)
+ send_update_user_for_institution_command(user.id)
+
+ def save(self, *args, **kwargs) -> None:
+ """Everytime the institution status changes, uses websocket to update state in the frontend"""
+ super().save(*args, **kwargs)
+ related_users = list(self.institution.users.all())
+ # Sends a websockets message to all users for update the institutions state in the frontend
+ for user in related_users:
+ send_update_institutions_command(user.id)
+ send_update_user_for_institution_command(user.id)
+
def __str__(self):
as_admin_label = '(as admin)' if self.is_institution_admin else ''
return f'{self.user} -> {self.institution} {as_admin_label}'
diff --git a/src/institutions/serializers.py b/src/institutions/serializers.py
index 629cdc4a..bf1f1755 100644
--- a/src/institutions/serializers.py
+++ b/src/institutions/serializers.py
@@ -1,7 +1,7 @@
-from django.contrib.auth import get_user_model
from rest_framework import serializers
-from .models import Institution
+from .models import Institution, InstitutionAdministration
from users.serializers import UserSerializer
+from django.contrib.auth import get_user_model
class InstitutionSerializer(serializers.ModelSerializer):
@@ -19,9 +19,33 @@ class Meta:
model = Institution
fields = ['id', 'name']
+class InstitutionListSerializer(serializers.ModelSerializer):
+ """A simple serializer for get only the Institution fields"""
+ is_user_admin = serializers.SerializerMethodField(method_name='get_is_user_admin')
+ class Meta:
+ model = Institution
+ fields = ['id', 'name', 'location', 'email', 'telephone_number', 'is_user_admin']
+ def get_is_user_admin(self, obj: Institution) -> bool:
+ user = self.context['request'].user
+ return obj.institutionadministration_set.filter(user=user, is_institution_admin=True).exists()
+
+class LimitedUserSerializer(serializers.ModelSerializer):
+ """A lightweight serializer for User model"""
+ class Meta:
+ model = get_user_model()
+ fields = ['id', 'username']
class UserCandidateSerializer(serializers.ModelSerializer):
"""Serializer useful to add a User to an Institution"""
+ user = LimitedUserSerializer()
class Meta:
- model = get_user_model()
- fields = ['id', 'username', 'email']
+ model = InstitutionAdministration
+ fields = ['id', 'user', 'is_institution_admin']
+
+class InstitutionAdminUpdateSerializer(serializers.ModelSerializer):
+ """
+ Serializer for updating 'is_institution_admin' in InstitutionAdministration
+ """
+ class Meta:
+ model = InstitutionAdministration
+ fields = ['id', 'is_institution_admin']
diff --git a/src/institutions/urls.py b/src/institutions/urls.py
index 24fe9831..7bb42cac 100644
--- a/src/institutions/urls.py
+++ b/src/institutions/urls.py
@@ -4,9 +4,19 @@
urlpatterns = [
path('', views.institutions_action, name='institutions'),
- path('my-institutions-as-admin', views.InstitutionAsAdminList.as_view(), name='user_institutions_as_admin'),
- path('my-institutions', views.InstitutionList.as_view(), name='user_institutions'),
+ path('my-institutions', views.UserInstitution.as_view(), name='user_institutions'),
+ path('my-institutions-list', views.UserInstitutionList.as_view(), name='user_institutions_list'),
path('user-candidates', views.UserCandidatesList.as_view(), name='user_candidates'),
+ path('user-candidates//', views.UserCandidatesList.as_view()),
+ path('create', views.CreateInstitutionView.as_view(), name='create_institution'),
+ path('edit', views.UpdateInstitutionView.as_view(), name='edit_institution'),
+ path('edit//', views.UpdateInstitutionView.as_view()),
+ path('delete', views.DeleteInstitutionView.as_view(), name='delete-institution'),
+ path('delete//', views.DeleteInstitutionView.as_view()),
+ path('admin-update', views.UpdateInstitutionAdminView.as_view(), name='update-institution-admin'),
+ path('admin-update//', views.UpdateInstitutionAdminView.as_view()),
+ path('non-users', views.InstitutionNonUsersListView.as_view(), name='institution-non-users-list'),
+ path('non-users//', views.InstitutionNonUsersListView.as_view()),
path(
'add-remove-user-to-institution',
views.add_remove_user_to_institution_action,
diff --git a/src/institutions/views.py b/src/institutions/views.py
index 70789e0c..bfbc7ecd 100644
--- a/src/institutions/views.py
+++ b/src/institutions/views.py
@@ -1,56 +1,62 @@
from django.contrib.auth.decorators import login_required
from django.shortcuts import render
-from rest_framework import generics, permissions
+from rest_framework import generics, permissions, filters
import json
from common.enums import ResponseCode
from common.functions import encode_json_response_status
from common.response import ResponseStatus
from .enums import AddRemoveUserToInstitutionStatusErrorCode
-from .models import Institution
-from .serializers import InstitutionSerializer, UserCandidateSerializer
+from .models import Institution, InstitutionAdministration
+from .serializers import InstitutionSerializer, UserCandidateSerializer, InstitutionListSerializer, InstitutionAdminUpdateSerializer, LimitedUserSerializer
from django.contrib.auth.models import User
+from common.pagination import StandardResultsSetPagination
+from django_filters.rest_framework import DjangoFilterBackend
+from rest_framework.response import Response
+from rest_framework.views import APIView
+from django.shortcuts import get_object_or_404
+from rest_framework.exceptions import ValidationError
+from django.contrib.auth import get_user_model
+from api_service.websocket_functions import send_update_institutions_command, send_update_user_for_institution_command
-class InstitutionAsAdminList(generics.ListAPIView):
- """REST endpoint: list for Institution model of which the current user is admin"""
+
+class UserInstitutionList(generics.ListAPIView):
+ """REST endpoint: list for Institution model of which the current user is part"""
def get_queryset(self):
return Institution.objects.filter(
- institutionadministration__user=self.request.user,
- institutionadministration__is_institution_admin=True
- )
+ institutionadministration__user=self.request.user
+ ).distinct()
- serializer_class = InstitutionSerializer
+ serializer_class = InstitutionListSerializer
permission_classes = [permissions.IsAuthenticated]
-
-
-class InstitutionList(generics.ListAPIView):
+ pagination_class = StandardResultsSetPagination
+ filter_backends = [filters.OrderingFilter, filters.SearchFilter]
+ search_fields = ['name', 'location']
+ ordering_fields = ['name', 'location', 'email', 'telephone_number']
+class UserInstitution(generics.ListAPIView):
"""REST endpoint: list for Institution model of which the current user is part"""
def get_queryset(self):
return Institution.objects.filter(institutionadministration__user=self.request.user)
-
serializer_class = InstitutionSerializer
permission_classes = [permissions.IsAuthenticated]
-
class UserCandidatesList(generics.ListAPIView):
"""REST endpoint: list for User model. Used to add to an Institution"""
def get_queryset(self):
- # Parses the request search param
- query_search = self.request.GET.get('querySearch', '')
- query_search = query_search.strip()
-
- if not query_search:
- return User.objects.none()
-
- # Returns only 3 results
- return User.objects.filter(username__icontains=query_search)[:3]
-
+ institution_id = self.kwargs.get('institution_id')
+ return InstitutionAdministration.objects.filter(
+ institution_id=institution_id
+ )
serializer_class = UserCandidateSerializer
permission_classes = [permissions.IsAuthenticated]
-
+
+ pagination_class = StandardResultsSetPagination
+ filter_backends = [filters.OrderingFilter, filters.SearchFilter]
+ search_fields = ['user__username']
+ ordering_fields = ['user__username']
@login_required
def add_remove_user_to_institution_action(request):
@@ -81,13 +87,17 @@ def add_remove_user_to_institution_action(request):
institutionadministration__is_institution_admin=True
)
+
# Adds/Remove user
if is_adding:
institution.users.add(user)
else:
institution.users.remove(user)
- institution.save()
+ for user in institution.users.all():
+ send_update_institutions_command(user.id)
+ send_update_user_for_institution_command(user.id)
+
response = {
'status': ResponseStatus(ResponseCode.SUCCESS)
@@ -119,6 +129,147 @@ def add_remove_user_to_institution_action(request):
return encode_json_response_status(response)
+class CreateInstitutionView(generics.CreateAPIView):
+ """
+ REST endpoint: Create an Institution and assign the creator as admin
+ """
+ serializer_class = InstitutionSerializer
+ permission_classes = [permissions.IsAuthenticated]
+
+ def perform_create(self, serializer):
+ """
+ Overrides the create method to add the creator as admin
+ """
+ # Save the institution instance
+ institution = serializer.save()
+ # Create a relationship between the creator and the institution as admin
+ InstitutionAdministration.objects.create(
+ user=self.request.user,
+ institution=institution,
+ is_institution_admin=True
+ )
+
+ def create(self, request, *args, **kwargs):
+ """
+ Handles the POST request to create an institution
+ """
+ serializer = self.get_serializer(data=request.data)
+ serializer.is_valid(raise_exception=True)
+ self.perform_create(serializer)
+ return Response(
+ serializer.data
+ )
+
+class UpdateInstitutionView(generics.UpdateAPIView):
+ """
+ REST endpoint: Update an Institution
+ Allows editing 'name', 'location', 'email', and 'telephone_number'.
+ """
+ queryset = Institution.objects.all()
+ serializer_class = InstitutionListSerializer
+ permission_classes = [permissions.IsAuthenticated]
+ lookup_field = 'id'
+
+ def get_queryset(self):
+ """
+ Allow only admins of the institution to edit
+ """
+ return Institution.objects.filter(
+ institutionadministration__user=self.request.user,
+ institutionadministration__is_institution_admin=True
+ )
+
+ def update(self, request, *args, **kwargs):
+ """
+ Handle partial updates
+ """
+ partial = kwargs.pop('partial', True) # Permite actualizaciones parciales
+ instance = self.get_object()
+ serializer = self.get_serializer(instance, data=request.data, partial=partial)
+ serializer.is_valid(raise_exception=True)
+ self.perform_update(serializer)
+
+ return Response(serializer.data)
+
+class DeleteInstitutionView(generics.DestroyAPIView):
+ """
+ REST endpoint: Delete an Institution
+ Allows deletion only if the user is an admin of the institution.
+ """
+ queryset = Institution.objects.all()
+ permission_classes = [permissions.IsAuthenticated]
+ lookup_field = 'id'
+
+ def get_queryset(self):
+ """
+ Allow only admins of the institution to delete it.
+ """
+ return Institution.objects.filter(
+ institutionadministration__user=self.request.user,
+ institutionadministration__is_institution_admin=True
+ )
+
+ def delete(self, request, *args, **kwargs):
+ """
+ Handle the delete operation with additional checks.
+ """
+ try:
+ instance = self.get_object()
+ self.perform_destroy(instance)
+ response = {
+ 'status': ResponseStatus(
+ ResponseCode.ERROR,
+ message="Institution deleted successfully."
+ )
+ }
+ return encode_json_response_status(response)
+
+ except Institution.DoesNotExist:
+ response = {
+ 'status': ResponseStatus(
+ ResponseCode.ERROR,
+ message="Institution not found or you do not have permission to delete it."
+ )
+ }
+ return encode_json_response_status(response)
+
+
+class UpdateInstitutionAdminView(APIView):
+ """
+ REST endpoint: Update the 'is_institution_admin' field in InstitutionAdministration
+ """
+ queryset = InstitutionAdministration.objects.all()
+ serializer_class = InstitutionAdminUpdateSerializer
+ permission_classes = [permissions.IsAuthenticated]
+ def patch(self, request, id):
+ """
+ Ensure that only an institution admin can update this relationship.
+ """
+ relation = get_object_or_404(InstitutionAdministration, institution__institutionadministration__user=self.request.user, id=id)
+ if relation.user.id == self.request.user.id:
+ raise ValidationError('Can not change admin for current user.')
+ relation.is_institution_admin = not relation.is_institution_admin
+ relation.save(update_fields=['is_institution_admin'])
+ return Response({'ok': True})
+
+class InstitutionNonUsersListView(generics.ListAPIView):
+ """
+ REST endpoint: Get all users NOT associated with a specific institution.
+ """
+ serializer_class = LimitedUserSerializer
+ permission_classes = [permissions.IsAuthenticated]
+
+ def get_queryset(self):
+ """
+ Return all users not associated with a specific institution.
+ """
+ institution_id = self.kwargs.get('institution_id')
+
+ associated_user_ids = InstitutionAdministration.objects.filter(
+ institution_id=institution_id
+ ).values_list('user_id', flat=True)
+
+ return get_user_model().objects.exclude(id__in=associated_user_ids)
@login_required
def institutions_action(request):
diff --git a/src/statistical_properties/views.py b/src/statistical_properties/views.py
index ed9a7940..39c732f7 100644
--- a/src/statistical_properties/views.py
+++ b/src/statistical_properties/views.py
@@ -163,6 +163,8 @@ def get_queryset(self):
ordering_fields = ['name', 'description', 'state', 'created']
+
+# Todo: Verificar
class StatisticalValidationDestroy(generics.DestroyAPIView):
"""REST endpoint: delete for StatisticalValidation model."""