diff --git a/backend/composer/api/filtersets.py b/backend/composer/api/filtersets.py index 04ebe8e1..acb8292b 100644 --- a/backend/composer/api/filtersets.py +++ b/backend/composer/api/filtersets.py @@ -3,7 +3,7 @@ import django_filters from django_filters import BaseInFilter, NumberFilter -from composer.enums import SentenceState, CSState +from composer.enums import SentenceState, CSState, NoteType from composer.models import ( Sentence, ConnectivityStatement, @@ -13,6 +13,7 @@ Via, Specie, Destination, ) +from django.contrib.auth.models import User from django_filters import rest_framework from django_filters import CharFilter, BaseInFilter @@ -203,11 +204,26 @@ class NoteFilter(django_filters.FilterSet): field_name="connectivity_statement_id", queryset=ConnectivityStatement.objects.all(), ) + include_system_notes = django_filters.BooleanFilter( + field_name="include_system_notes", method="get_notes", label="Include System Notes" + ) class Meta: model = Note fields = [] + @staticmethod + def get_notes(queryset, name, value): + if value: + return queryset + system_user = User.objects.get(username="system") + combined_queryset = queryset.filter( + Q(user=system_user, note__icontains="invalid") | + Q(user=system_user, note__icontains="exported") | + ~Q(user=system_user) + ) + return combined_queryset.distinct() + class ViaFilter(django_filters.FilterSet): connectivity_statement_id = django_filters.ModelChoiceFilter( diff --git a/backend/composer/migrations/0062_auto_20241122_1333.py b/backend/composer/migrations/0062_auto_20241122_1333.py new file mode 100644 index 00000000..55bc40d9 --- /dev/null +++ b/backend/composer/migrations/0062_auto_20241122_1333.py @@ -0,0 +1,36 @@ +# Generated by Django 4.1.4 on 2024-11-22 12:33 + +""" +This migration script migrates the notes of type TRANSITION to be associated with the system user. +And convert their note to the new format - "User {user.first_name} {user.last_name} transitioned this record from {from_state} to {to_state}" +""" + +from django.db import migrations +from composer.enums import NoteType + + +def migrate_notes_to_system_user(apps, schema_editor): + User = apps.get_model('auth', 'User') + Note = apps.get_model('composer', 'Note') + + system_user = User.objects.get(username='system') + notes = Note.objects.filter(type=NoteType.TRANSITION) + for note in notes: + note_parts = note.note.split(' ') + from_state = note_parts[2] + to_state = note_parts[4] + new_note = f"User {note.user.first_name} {note.user.last_name} transitioned this record from {from_state} to {to_state}" + note.note = new_note + note.user = system_user + note.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ("composer", "0061_graphrenderingstate_and_more"), + ] + + operations = [ + migrations.RunPython(migrate_notes_to_system_user), + ] diff --git a/backend/composer/signals.py b/backend/composer/signals.py index ec780213..4b5a15f5 100644 --- a/backend/composer/signals.py +++ b/backend/composer/signals.py @@ -1,6 +1,7 @@ from django.core.exceptions import ValidationError from django.dispatch import receiver from django.db.models.signals import post_save, m2m_changed, pre_save, post_delete +from django.contrib.auth import get_user_model from django_fsm.signals import post_transition @@ -29,8 +30,10 @@ def export_batch_post_save(sender, instance=None, created=False, **kwargs): @receiver(post_transition) def post_transition_callback(sender, instance, name, source, target, **kwargs): + User = get_user_model() method_kwargs = kwargs.get("method_kwargs", {}) user = method_kwargs.get("by") + system_user = User.objects.get(username='system') if issubclass(sender, ConnectivityStatement): connectivity_statement = instance else: @@ -40,11 +43,11 @@ def post_transition_callback(sender, instance, name, source, target, **kwargs): else: sentence = None Note.objects.create( - user=user, + user=system_user, type=NoteType.TRANSITION, connectivity_statement=connectivity_statement, sentence=sentence, - note=f"Transitioned from {source} to {target}", + note=f"User {user.first_name} {user.last_name} transitioned this record from {source} to {target}", ) diff --git a/frontend/src/apiclient/backend/api.ts b/frontend/src/apiclient/backend/api.ts index 27c70fce..6204af8f 100644 --- a/frontend/src/apiclient/backend/api.ts +++ b/frontend/src/apiclient/backend/api.ts @@ -492,7 +492,7 @@ export interface ConnectivityStatementUpdate { * @type {string} * @memberof ConnectivityStatementUpdate */ - 'state'?: string; + 'state': string; /** * * @type {Array} @@ -3825,13 +3825,14 @@ export const ComposerApiAxiosParamCreator = function (configuration?: Configurat /** * Note * @param {number | null} [connectivityStatementId] + * @param {boolean} [includeSystemNotes] Include System Notes * @param {number} [limit] Number of results to return per page. * @param {number} [offset] The initial index from which to return the results. * @param {number | null} [sentenceId] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - composerNoteList: async (connectivityStatementId?: number | null, limit?: number, offset?: number, sentenceId?: number | null, options: RawAxiosRequestConfig = {}): Promise => { + composerNoteList: async (connectivityStatementId?: number | null, includeSystemNotes?: boolean, limit?: number, offset?: number, sentenceId?: number | null, options: RawAxiosRequestConfig = {}): Promise => { const localVarPath = `/api/composer/note/`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -3857,6 +3858,10 @@ export const ComposerApiAxiosParamCreator = function (configuration?: Configurat localVarQueryParameter['connectivity_statement_id'] = connectivityStatementId; } + if (includeSystemNotes !== undefined) { + localVarQueryParameter['include_system_notes'] = includeSystemNotes; + } + if (limit !== undefined) { localVarQueryParameter['limit'] = limit; } @@ -5675,14 +5680,15 @@ export const ComposerApiFp = function(configuration?: Configuration) { /** * Note * @param {number | null} [connectivityStatementId] + * @param {boolean} [includeSystemNotes] Include System Notes * @param {number} [limit] Number of results to return per page. * @param {number} [offset] The initial index from which to return the results. * @param {number | null} [sentenceId] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async composerNoteList(connectivityStatementId?: number | null, limit?: number, offset?: number, sentenceId?: number | null, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.composerNoteList(connectivityStatementId, limit, offset, sentenceId, options); + async composerNoteList(connectivityStatementId?: number | null, includeSystemNotes?: boolean, limit?: number, offset?: number, sentenceId?: number | null, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.composerNoteList(connectivityStatementId, includeSystemNotes, limit, offset, sentenceId, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['ComposerApi.composerNoteList']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); @@ -6360,14 +6366,15 @@ export const ComposerApiFactory = function (configuration?: Configuration, baseP /** * Note * @param {number | null} [connectivityStatementId] + * @param {boolean} [includeSystemNotes] Include System Notes * @param {number} [limit] Number of results to return per page. * @param {number} [offset] The initial index from which to return the results. * @param {number | null} [sentenceId] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - composerNoteList(connectivityStatementId?: number | null, limit?: number, offset?: number, sentenceId?: number | null, options?: RawAxiosRequestConfig): AxiosPromise { - return localVarFp.composerNoteList(connectivityStatementId, limit, offset, sentenceId, options).then((request) => request(axios, basePath)); + composerNoteList(connectivityStatementId?: number | null, includeSystemNotes?: boolean, limit?: number, offset?: number, sentenceId?: number | null, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.composerNoteList(connectivityStatementId, includeSystemNotes, limit, offset, sentenceId, options).then((request) => request(axios, basePath)); }, /** * Note @@ -7001,6 +7008,7 @@ export class ComposerApi extends BaseAPI { /** * Note * @param {number | null} [connectivityStatementId] + * @param {boolean} [includeSystemNotes] Include System Notes * @param {number} [limit] Number of results to return per page. * @param {number} [offset] The initial index from which to return the results. * @param {number | null} [sentenceId] @@ -7008,8 +7016,8 @@ export class ComposerApi extends BaseAPI { * @throws {RequiredError} * @memberof ComposerApi */ - public composerNoteList(connectivityStatementId?: number | null, limit?: number, offset?: number, sentenceId?: number | null, options?: RawAxiosRequestConfig) { - return ComposerApiFp(this.configuration).composerNoteList(connectivityStatementId, limit, offset, sentenceId, options).then((request) => request(this.axios, this.basePath)); + public composerNoteList(connectivityStatementId?: number | null, includeSystemNotes?: boolean, limit?: number, offset?: number, sentenceId?: number | null, options?: RawAxiosRequestConfig) { + return ComposerApiFp(this.configuration).composerNoteList(connectivityStatementId, includeSystemNotes, limit, offset, sentenceId, options).then((request) => request(this.axios, this.basePath)); } /** diff --git a/frontend/src/components/Widgets/NotesFomList.tsx b/frontend/src/components/Widgets/NotesFomList.tsx index e0ec3384..938f8db7 100644 --- a/frontend/src/components/Widgets/NotesFomList.tsx +++ b/frontend/src/components/Widgets/NotesFomList.tsx @@ -1,5 +1,5 @@ import React, {useEffect, useState} from 'react' -import { Box } from '@mui/material' +import { Box, Checkbox } from '@mui/material' import noteService from '../../services/NoteService' import Timeline from '@mui/lab/Timeline'; import TimelineItem from '@mui/lab/TimelineItem'; @@ -31,6 +31,7 @@ const TimeLineIcon = () => { const NoteDetails = (props: any) => { const { extraData } = props const [noteList, setNoteList] = useState([]) + const [showSystemNotes, setShowSystemNotes] = useState(false) const [refresh, setRefresh] = useState(false) const theme = useTheme() @@ -39,12 +40,12 @@ const NoteDetails = (props: any) => { useEffect(() => { const { connectivity_statement_id, sentence_id } = extraData if (connectivity_statement_id || sentence_id) { - noteService.getNotesList(connectivity_statement_id, undefined,undefined, sentence_id).then(result => { + noteService.getNotesList(connectivity_statement_id, showSystemNotes, undefined, undefined, sentence_id).then(result => { setNoteList(result?.results) setRefresh(false) }) } - }, [extraData?.connectivity_statement_id, extraData?.sentence_id, refresh, extraData]) + }, [extraData?.connectivity_statement_id, extraData?.sentence_id, refresh, extraData, showSystemNotes]) return ( @@ -63,6 +64,21 @@ const NoteDetails = (props: any) => { }}> + + Note's History + + setShowSystemNotes(!showSystemNotes)} + /> + Show System Notes + + res.data) + async getNotesList(connectivityStatementId?: number, includeSystemNotes?: boolean, limit?: number, offset?: number, sentenceId?: number) { + return composerApi.composerNoteList(connectivityStatementId, includeSystemNotes, limit, offset, sentenceId).then((res: any) => res.data) } async getObject(id: string): Promise { return {} as Note diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 38f83970..fcde265d 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -863,6 +863,11 @@ paths: schema: type: integer nullable: true + - in: query + name: include_system_notes + schema: + type: boolean + description: Include System Notes - name: limit required: false in: query @@ -2244,7 +2249,7 @@ components: nullable: true state: type: string - maxLength: 50 + readOnly: true available_transitions: type: array items: @@ -2350,6 +2355,7 @@ components: - projection_phenotype - sentence - sex + - state - statement_preview - tags Destination: @@ -2953,7 +2959,7 @@ components: nullable: true state: type: string - maxLength: 50 + readOnly: true available_transitions: type: array items: