From 82b4fb1ea3bd3e171af3876f355478a6eb4cb17c Mon Sep 17 00:00:00 2001 From: cir9no <44470218+cir9no@users.noreply.github.com> Date: Sun, 26 Jan 2025 16:58:09 +0800 Subject: [PATCH] feat: add make key face photo sup --- frontend/src/metadata/api.js | 8 +++ frontend/src/metadata/context.js | 13 ++++ frontend/src/metadata/store/index.js | 8 +++ .../metadata/store/operations/constants.js | 2 + .../src/metadata/store/server-operator.js | 9 +++ .../metadata/views/face-recognition/index.js | 5 ++ .../face-recognition/person-photos/index.js | 8 ++- .../views/gallery/context-menu/index.js | 16 ++++- frontend/src/metadata/views/gallery/main.js | 10 ++- seahub/repo_metadata/apis.py | 67 ++++++++++++++++++- seahub/repo_metadata/urls.py | 3 +- seahub/repo_metadata/utils.py | 7 ++ 12 files changed, 149 insertions(+), 7 deletions(-) diff --git a/frontend/src/metadata/api.js b/frontend/src/metadata/api.js index 6c708b4eef..b065703355 100644 --- a/frontend/src/metadata/api.js +++ b/frontend/src/metadata/api.js @@ -369,6 +369,14 @@ class MetadataManagerAPI { return this.req.post(url, params); }; + setPeoplePhoto = (repoID, peopleId, recordId) => { + const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/people-cover-photo/' + peopleId + '/'; + const params = { + record_id: recordId + }; + return this.req.put(url, params); + }; + // ocr openOCR = (repoID) => { const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/ocr/'; diff --git a/frontend/src/metadata/context.js b/frontend/src/metadata/context.js index dfb7575a66..b201eeea1a 100644 --- a/frontend/src/metadata/context.js +++ b/frontend/src/metadata/context.js @@ -179,6 +179,14 @@ class Context { return viewId === FACE_RECOGNITION_VIEW_ID; }; + canSetPeoplePhoto = () => { + const viewId = this.getSetting('viewID'); + if (this.permission === 'r' || viewId !== FACE_RECOGNITION_VIEW_ID) { + return false; + } + return true; + }; + restoreRows = () => { // todo }; @@ -282,6 +290,11 @@ class Context { return this.metadataAPI.removePeoplePhotos(repoID, recordId, photoIds); }; + setPeoplePhoto = (recordId, photoId) => { + const repoID = this.settings['repoID']; + return this.metadataAPI.setPeoplePhoto(repoID, recordId, photoId); + }; + // file tag updateFileTags = (data) => { const repoID = this.settings['repoID']; diff --git a/frontend/src/metadata/store/index.js b/frontend/src/metadata/store/index.js index abbacc95aa..dd74cc0332 100644 --- a/frontend/src/metadata/store/index.js +++ b/frontend/src/metadata/store/index.js @@ -622,6 +622,14 @@ class Store { this.applyOperation(operation); }; + setPeoplePhoto = (peopleId, selectedPhoto) => { + const type = OPERATION_TYPE.SET_PEOPLE_COVER_PHOTO; + const operation = this.createOperation({ + type, repo_id: this.repoId, people_id: peopleId, selected_photo: selectedPhoto + }); + this.applyOperation(operation); + }; + // tag updateFileTags = (data) => { const type = OPERATION_TYPE.UPDATE_FILE_TAGS; diff --git a/frontend/src/metadata/store/operations/constants.js b/frontend/src/metadata/store/operations/constants.js index da0f1f0d23..f1199a6b76 100644 --- a/frontend/src/metadata/store/operations/constants.js +++ b/frontend/src/metadata/store/operations/constants.js @@ -32,6 +32,7 @@ export const OPERATION_TYPE = { DELETE_PEOPLE_PHOTOS: 'delete_people_photos', REMOVE_PEOPLE_PHOTOS: 'remove_people_photos', ADD_PEOPLE_PHOTOS: 'add_people_photos', + SET_PEOPLE_COVER_PHOTO: 'set_people_cover_photo', // tag UPDATE_FILE_TAGS: 'update_file_tags', @@ -72,6 +73,7 @@ export const OPERATION_ATTRIBUTES = { [OPERATION_TYPE.DELETE_PEOPLE_PHOTOS]: ['repo_id', 'people_id', 'deleted_photos'], [OPERATION_TYPE.REMOVE_PEOPLE_PHOTOS]: ['repo_id', 'people_id', 'removed_photos', 'success_callback'], [OPERATION_TYPE.ADD_PEOPLE_PHOTOS]: ['repo_id', 'people_id', 'old_people_id', 'added_photos', 'success_callback'], + [OPERATION_TYPE.SET_PEOPLE_COVER_PHOTO]: ['repo_id', 'people_id', 'selected_photo'], [OPERATION_TYPE.MODIFY_SETTINGS]: ['repo_id', 'view_id', 'settings'], diff --git a/frontend/src/metadata/store/server-operator.js b/frontend/src/metadata/store/server-operator.js index e9bebcbec0..3004aa0775 100644 --- a/frontend/src/metadata/store/server-operator.js +++ b/frontend/src/metadata/store/server-operator.js @@ -234,6 +234,15 @@ class ServerOperator { }); break; } + case OPERATION_TYPE.SET_PEOPLE_COVER_PHOTO: { + const { people_id, selected_photo } = operation; + window.sfMetadataContext.setPeoplePhoto(people_id, selected_photo).then(res => { + callback({ operation }); + }).catch(error => { + callback({ error: gettext('Failed to set people cover photo') }); + }); + break; + } // tags case OPERATION_TYPE.UPDATE_FILE_TAGS: { diff --git a/frontend/src/metadata/views/face-recognition/index.js b/frontend/src/metadata/views/face-recognition/index.js index a941d860ab..e00c740246 100644 --- a/frontend/src/metadata/views/face-recognition/index.js +++ b/frontend/src/metadata/views/face-recognition/index.js @@ -31,6 +31,10 @@ const FaceRecognition = () => { store.removePeoplePhotos(peopleId, peoplePhotos, { success_callback }); }, [store]); + const onSetPeoplePhoto = useCallback((peopleId, peoplePhoto) => { + store.setPeoplePhoto(peopleId, peoplePhoto); + }, [store]); + const openPeople = useCallback((people) => { peopleRef.current = people; const name = people._is_someone ? (people._name || gettext('Person image')) : gettext('Unknown people'); @@ -64,6 +68,7 @@ const FaceRecognition = () => { onAddPeoplePhotos={onAddPeoplePhotos} onRemovePeoplePhotos={onRemovePeoplePhotos} onDeletePeoplePhotos={onDeletePeoplePhotos} + onSetPeoplePhoto={onSetPeoplePhoto} /> ) : ( diff --git a/frontend/src/metadata/views/face-recognition/person-photos/index.js b/frontend/src/metadata/views/face-recognition/person-photos/index.js index 1ff2b677e6..94c539bb7a 100644 --- a/frontend/src/metadata/views/face-recognition/person-photos/index.js +++ b/frontend/src/metadata/views/face-recognition/person-photos/index.js @@ -22,7 +22,7 @@ import '../../gallery/index.css'; dayjs.extend(utc); -const PeoplePhotos = ({ view, people, onClose, onDeletePeoplePhotos, onAddPeoplePhotos, onRemovePeoplePhotos }) => { +const PeoplePhotos = ({ view, people, onClose, onDeletePeoplePhotos, onAddPeoplePhotos, onSetPeoplePhoto, onRemovePeoplePhotos }) => { const [isLoading, setLoading] = useState(true); const [isLoadingMore, setLoadingMore] = useState(false); const [metadata, setMetadata] = useState({ rows: [] }); @@ -148,6 +148,11 @@ const PeoplePhotos = ({ view, people, onClose, onDeletePeoplePhotos, onAddPeople }); }, [people, onAddPeoplePhotos, deletedByIds]); + const handleSetPeoplePhoto = useCallback((selectedImage) => { + const { id } = selectedImage; + onSetPeoplePhoto(people._id, id); + }, [people, onSetPeoplePhoto]); + const loadData = useCallback((view) => { setLoading(true); metadataAPI.getPeoplePhotos(repoID, people._id, 0, PER_LOAD_NUMBER).then(res => { @@ -245,6 +250,7 @@ const PeoplePhotos = ({ view, people, onClose, onDeletePeoplePhotos, onAddPeople onDelete={handelDelete} onRemoveImage={people._is_someone ? handelRemove : null} onAddImage={!people._is_someone ? handelAdd : null} + onSetPeoplePhoto={handleSetPeoplePhoto} /> ); diff --git a/frontend/src/metadata/views/gallery/context-menu/index.js b/frontend/src/metadata/views/gallery/context-menu/index.js index 65ff101b42..0509809363 100644 --- a/frontend/src/metadata/views/gallery/context-menu/index.js +++ b/frontend/src/metadata/views/gallery/context-menu/index.js @@ -18,10 +18,11 @@ const CONTEXT_MENU_KEY = { DELETE: 'delete', DUPLICATE: 'duplicate', REMOVE: 'remove', + SET_PEOPLE_PHOTO: 'set_people_photo', ADD_PHOTO_TO_GROUP: 'add_photo_to_group', }; -const GalleryContextMenu = ({ metadata, selectedImages, onDelete, onDuplicate, addFolder, onRemoveImage, onAddImage }) => { +const GalleryContextMenu = ({ metadata, selectedImages, onDelete, onDuplicate, addFolder, onRemoveImage, onAddImage, onSetPeoplePhoto }) => { const [isZipDialogOpen, setIsZipDialogOpen] = useState(false); const [isCopyDialogOpen, setIsCopyDialogOpen] = useState(false); const [isPeoplesDialogShow, setPeoplesDialogShow] = useState(false); @@ -31,6 +32,7 @@ const GalleryContextMenu = ({ metadata, selectedImages, onDelete, onDuplicate, a const canDuplicateRow = window.sfMetadataContext.canDuplicateRow(); const canRemovePhotoFromPeople = window.sfMetadataContext.canRemovePhotoFromPeople(); const canAddPhotoToPeople = window.sfMetadataContext.canAddPhotoToPeople(); + const canSetPeoplePhoto = window.sfMetadataContext.canSetPeoplePhoto(); const options = useMemo(() => { let validOptions = [{ value: CONTEXT_MENU_KEY.DOWNLOAD, label: gettext('Download') }]; @@ -46,8 +48,11 @@ const GalleryContextMenu = ({ metadata, selectedImages, onDelete, onDuplicate, a if (onAddImage && canAddPhotoToPeople) { validOptions.push({ value: CONTEXT_MENU_KEY.ADD_PHOTO_TO_GROUP, label: gettext('Add to group') }); } + if (onSetPeoplePhoto && canSetPeoplePhoto) { + validOptions.push({ value: CONTEXT_MENU_KEY.SET_PEOPLE_PHOTO, label: gettext('Set as cover photo') }); + } return validOptions; - }, [checkCanDeleteRow, canDuplicateRow, canRemovePhotoFromPeople, canAddPhotoToPeople, selectedImages, onDuplicate, onDelete, onRemoveImage, onAddImage]); + }, [checkCanDeleteRow, canDuplicateRow, canRemovePhotoFromPeople, canAddPhotoToPeople, selectedImages, onDuplicate, onDelete, onRemoveImage, onAddImage, canSetPeoplePhoto, onSetPeoplePhoto]); const closeZipDialog = () => { setIsZipDialogOpen(false); @@ -104,8 +109,13 @@ const GalleryContextMenu = ({ metadata, selectedImages, onDelete, onDuplicate, a case CONTEXT_MENU_KEY.ADD_PHOTO_TO_GROUP: setPeoplesDialogShow(true); break; + case CONTEXT_MENU_KEY.SET_PEOPLE_PHOTO: + onSetPeoplePhoto(selectedImages[0]); + break; + default: + break; } - }, [handleDownload, onDelete, selectedImages, toggleCopyDialog, onRemoveImage]); + }, [handleDownload, onDelete, selectedImages, toggleCopyDialog, onRemoveImage, onSetPeoplePhoto]); const closePeoplesDialog = useCallback(() => { setPeoplesDialogShow(false); diff --git a/frontend/src/metadata/views/gallery/main.js b/frontend/src/metadata/views/gallery/main.js index 221ea1a622..6f3f8f9ea4 100644 --- a/frontend/src/metadata/views/gallery/main.js +++ b/frontend/src/metadata/views/gallery/main.js @@ -19,7 +19,7 @@ import './index.css'; const OVER_SCAN_ROWS = 20; -const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord, onAddFolder, onRemoveImage, onAddImage }) => { +const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord, onAddFolder, onRemoveImage, onAddImage, onSetPeoplePhoto }) => { const [isFirstLoading, setFirstLoading] = useState(true); const [zoomGear, setZoomGear] = useState(0); const [containerWidth, setContainerWidth] = useState(0); @@ -237,6 +237,13 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord, }); }, [onRemoveImage, updateCurrentDirent]); + const handleMakeSelectedAsCoverPhoto = useCallback((selectedImage) => { + onSetPeoplePhoto(selectedImage, () => { + updateCurrentDirent(); + setSelectedImages([]); + }); + }, [onSetPeoplePhoto, updateCurrentDirent]); + const handleClickOutside = useCallback((event) => { const className = getEventClassName(event); const isClickInsideImage = className.includes('metadata-gallery-image-item') || className.includes('metadata-gallery-grid-image'); @@ -401,6 +408,7 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord, addFolder={onAddFolder} onRemoveImage={onRemoveImage ? handelRemoveSelectedImages : null} onAddImage={onAddImage} + onSetPeoplePhoto={handleMakeSelectedAsCoverPhoto} /> {isImagePopupOpen && ( diff --git a/seahub/repo_metadata/apis.py b/seahub/repo_metadata/apis.py index b03eb905e2..a7d29c349f 100644 --- a/seahub/repo_metadata/apis.py +++ b/seahub/repo_metadata/apis.py @@ -17,7 +17,7 @@ from seahub.repo_metadata.utils import add_init_metadata_task, gen_unique_id, in get_unmodifiable_columns, can_read_metadata, init_faces, \ extract_file_details, get_table_by_name, remove_faces_table, FACES_SAVE_PATH, \ init_tags, init_tag_self_link_columns, remove_tags_table, add_init_face_recognition_task, init_ocr, \ - remove_ocr_column, get_update_record + remove_ocr_column, get_update_record, update_people_cover_photo from seahub.repo_metadata.metadata_server_api import MetadataServerAPI, list_metadata_view_records from seahub.utils.repo import is_repo_admin from seaserv import seafile_api @@ -2633,3 +2633,68 @@ class MetadataMergeTags(APIView): return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error') return Response({'success': True}) + + +class PeopleCoverPhoto(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated,) + throttle_classes = (UserRateThrottle,) + + def put(self, request, repo_id, people_id): + record_id = request.data.get('record_id') + if not record_id: + error_msg = 'record_id invalid' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + metadata = RepoMetadata.objects.filter(repo_id=repo_id).first() + if ( + not metadata + or not metadata.enabled + or not metadata.face_recognition_enabled + ): + error_msg = f'The face recognition is disabled for repo {repo_id}.' + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # resource check + repo = seafile_api.get_repo(repo_id) + if not repo: + error_msg = 'Library %s not found.' % repo_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + if not is_repo_admin(request.user.username, repo_id): + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + metadata_server_api = MetadataServerAPI(repo_id, request.user.username) + + from seafevents.repo_metadata.constants import METADATA_TABLE + + sql = f'SELECT {METADATA_TABLE.columns.obj_id.name} FROM `{METADATA_TABLE.name}` WHERE `{METADATA_TABLE.columns.id.name}` = "{record_id}"' + + try: + query_result = metadata_server_api.query_rows(sql) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + obj_id = query_result.get('results', [dict()])[0].get( + METADATA_TABLE.columns.obj_id.name, '' + ) + if not obj_id: + return api_error(status.HTTP_404_NOT_FOUND, 'obj_id not found') + + params = { + 'repo_id': repo_id, + 'obj_id': obj_id, + 'people_id': people_id, + } + + try: + update_people_cover_photo(params) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + return Response({'success': True}) diff --git a/seahub/repo_metadata/urls.py b/seahub/repo_metadata/urls.py index b36d938b77..d3c9bec996 100644 --- a/seahub/repo_metadata/urls.py +++ b/seahub/repo_metadata/urls.py @@ -3,7 +3,7 @@ from .apis import MetadataRecords, MetadataManage, MetadataColumns, MetadataReco MetadataFolders, MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView, MetadataViewsDuplicateView, FacesRecords, \ FaceRecognitionManage, FacesRecord, MetadataExtractFileDetails, PeoplePhotos, MetadataTagsStatusManage, MetadataTags, \ MetadataTagsLinks, MetadataFileTags, MetadataTagFiles, MetadataMergeTags, MetadataTagsFiles, MetadataDetailsSettingsView, \ - MetadataOCRManageView + MetadataOCRManageView, PeopleCoverPhoto urlpatterns = [ re_path(r'^$', MetadataManage.as_view(), name='api-v2.1-metadata'), @@ -23,6 +23,7 @@ urlpatterns = [ re_path(r'^face-records/$', FacesRecords.as_view(), name='api-v2.1-metadata-face-records'), re_path(r'^people-photos/(?P.+)/$', PeoplePhotos.as_view(), name='api-v2.1-metadata-people-photos'), re_path(r'^face-recognition/$', FaceRecognitionManage.as_view(), name='api-v2.1-metadata-face-recognition'), + re_path(r'^people-cover-photo/(?P.+)/$', PeopleCoverPhoto.as_view(), name='api-v2.1-metadata-people-cover-photo'), re_path(r'^extract-file-details/$', MetadataExtractFileDetails.as_view(), name='api-v2.1-metadata-extract-file-details'), diff --git a/seahub/repo_metadata/utils.py b/seahub/repo_metadata/utils.py index 37afafcbdf..18583f510c 100644 --- a/seahub/repo_metadata/utils.py +++ b/seahub/repo_metadata/utils.py @@ -41,6 +41,13 @@ def extract_file_details(params): resp = requests.post(url, json=params, headers=headers, timeout=30) return json.loads(resp.content)['details'] +def update_people_cover_photo(params): + payload = {'exp': int(time.time()) + 300, } + token = jwt.encode(payload, SECRET_KEY, algorithm='HS256') + headers = {"Authorization": "Token %s" % token} + url = urljoin(SEAFEVENTS_SERVER_URL, '/update-people-cover-photo') + resp = requests.post(url, json=params, headers=headers, timeout=30) + return json.loads(resp.content) def generator_base64_code(length=4): possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz0123456789'