Skip to content

Commit 94e666d

Browse files
committed
Merge remote-tracking branch 'origin/main' into 1342-server-tests-for-gender-decoder
2 parents bde22d7 + 7fbabcf commit 94e666d

File tree

62 files changed

+4595
-2403
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+4595
-2403
lines changed

openapi/openapi.yaml

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ paths:
77
put:
88
tags: [ai-resource]
99
operationId: generateJobApplicationDraft
10+
parameters:
11+
- name: lang
12+
in: query
13+
required: true
14+
schema: {type: string}
1015
requestBody:
1116
content:
1217
application/json:
@@ -18,10 +23,19 @@ paths:
1823
content:
1924
application/json:
2025
schema: {$ref: '#/components/schemas/AIJobDescriptionDTO'}
21-
/api/ai/translateJobDescription:
26+
/api/ai/translateJobDescriptionForJob:
2227
put:
2328
tags: [ai-resource]
24-
operationId: translateText
29+
operationId: translateJobDescriptionForJob
30+
parameters:
31+
- name: jobId
32+
in: query
33+
required: true
34+
schema: {type: string}
35+
- name: toLang
36+
in: query
37+
required: true
38+
schema: {type: string}
2539
requestBody:
2640
content:
2741
application/json:
@@ -118,6 +132,30 @@ paths:
118132
content:
119133
application/json:
120134
schema: {$ref: '#/components/schemas/PageApplicationOverviewDTO'}
135+
/api/applications/profile:
136+
get:
137+
tags: [application-resource]
138+
operationId: getApplicantProfile
139+
responses:
140+
'200':
141+
description: OK
142+
content:
143+
application/json:
144+
schema: {$ref: '#/components/schemas/ApplicantDTO'}
145+
put:
146+
tags: [application-resource]
147+
operationId: updateApplicantProfile
148+
requestBody:
149+
content:
150+
application/json:
151+
schema: {$ref: '#/components/schemas/ApplicantDTO'}
152+
required: true
153+
responses:
154+
'200':
155+
description: OK
156+
content:
157+
application/json:
158+
schema: {$ref: '#/components/schemas/ApplicantDTO'}
121159
/api/applications/rename-document/{documentDictionaryId}:
122160
put:
123161
tags: [application-resource]
@@ -2515,7 +2553,8 @@ components:
25152553
enum: [FULLY_FUNDED, PARTIALLY_FUNDED, SCHOLARSHIP, SELF_FUNDED, INDUSTRY_SPONSORED,
25162554
GOVERNMENT_FUNDED, RESEARCH_GRANT]
25172555
image: {$ref: '#/components/schemas/Image'}
2518-
jobDescription: {type: string}
2556+
jobDescriptionDE: {type: string}
2557+
jobDescriptionEN: {type: string}
25192558
jobId: {type: string, format: uuid}
25202559
lastModifiedAt: {type: string, format: date-time}
25212560
location:
@@ -2563,7 +2602,8 @@ components:
25632602
GOVERNMENT_FUNDED, RESEARCH_GRANT]
25642603
imageId: {type: string, format: uuid}
25652604
imageUrl: {type: string}
2566-
jobDescription: {type: string}
2605+
jobDescriptionDE: {type: string}
2606+
jobDescriptionEN: {type: string}
25672607
jobId: {type: string, format: uuid}
25682608
location:
25692609
type: string
@@ -2591,7 +2631,8 @@ components:
25912631
endDate: {type: string, format: date}
25922632
fieldOfStudies: {type: string}
25932633
fundingType: {type: string}
2594-
jobDescription: {type: string}
2634+
jobDescriptionDE: {type: string}
2635+
jobDescriptionEN: {type: string}
25952636
jobId: {type: string, format: uuid}
25962637
lastModifiedAt: {type: string, format: date-time}
25972638
location: {type: string}
@@ -2626,7 +2667,8 @@ components:
26262667
enum: [FULLY_FUNDED, PARTIALLY_FUNDED, SCHOLARSHIP, SELF_FUNDED, INDUSTRY_SPONSORED,
26272668
GOVERNMENT_FUNDED, RESEARCH_GRANT]
26282669
imageId: {type: string, format: uuid}
2629-
jobDescription: {type: string}
2670+
jobDescriptionDE: {type: string}
2671+
jobDescriptionEN: {type: string}
26302672
jobId: {type: string, format: uuid}
26312673
location:
26322674
type: string

src/main/java/de/tum/cit/aet/ai/exception/AiPromptException.java

Lines changed: 0 additions & 8 deletions
This file was deleted.

src/main/java/de/tum/cit/aet/ai/exception/AiResponseException.java

Lines changed: 0 additions & 8 deletions
This file was deleted.

src/main/java/de/tum/cit/aet/ai/service/AiService.java

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import de.tum.cit.aet.ai.dto.AIJobDescriptionDTO;
44
import de.tum.cit.aet.ai.dto.AIJobDescriptionTranslationDTO;
55
import de.tum.cit.aet.job.dto.JobFormDTO;
6+
import de.tum.cit.aet.job.service.JobService;
67
import lombok.extern.slf4j.Slf4j;
78
import org.springframework.ai.chat.client.ChatClient;
89
import org.springframework.beans.factory.annotation.Value;
@@ -21,22 +22,36 @@ public class AiService {
2122

2223
private final ChatClient chatClient;
2324

24-
public AiService(ChatClient.Builder chatClientBuilder) {
25+
private final JobService jobService;
26+
27+
public AiService(ChatClient.Builder chatClientBuilder, JobService jobService) {
2528
this.chatClient = chatClientBuilder.build();
29+
this.jobService = jobService;
2630
}
2731

2832
/**
2933
* Generates a polished job application draft from the provided job form data.
3034
* The draft is generated using the configured ChatClient with AGG\-compliant,
3135
* gender\-inclusive language.
3236
*
33-
* @param jobFormDTO the job form data containing description, requirements, and tasks
37+
* @param jobFormDTO the job form data containing description, requirements, and tasks
38+
* @param descriptionLanguage the language for the generated job description
3439
* @return The generated job posting content
3540
*/
36-
public AIJobDescriptionDTO generateJobApplicationDraft(JobFormDTO jobFormDTO) {
41+
public AIJobDescriptionDTO generateJobApplicationDraft(JobFormDTO jobFormDTO, String descriptionLanguage) {
42+
String input = "de".equals(descriptionLanguage) ? jobFormDTO.jobDescriptionDE() : jobFormDTO.jobDescriptionEN();
43+
3744
return chatClient
3845
.prompt()
39-
.user(u -> u.text(jobGenerationResource).param("jobDescription", jobFormDTO.jobDescription()))
46+
.user(u ->
47+
u
48+
.text(jobGenerationResource)
49+
.param("jobDescription", input)
50+
.param("title", jobFormDTO.title() != null ? jobFormDTO.title() : "")
51+
.param("researchArea", jobFormDTO.researchArea() != null ? jobFormDTO.researchArea() : "")
52+
.param("fieldOfStudies", jobFormDTO.fieldOfStudies() != null ? jobFormDTO.fieldOfStudies() : "")
53+
.param("location", jobFormDTO.location() != null ? jobFormDTO.location().toString() : "")
54+
)
4055
.call()
4156
.entity(AIJobDescriptionDTO.class);
4257
}
@@ -49,11 +64,29 @@ public AIJobDescriptionDTO generateJobApplicationDraft(JobFormDTO jobFormDTO) {
4964
* @param text the text to translate (German or English)
5065
* @return The translated text response with detected and target language info
5166
*/
52-
public AIJobDescriptionTranslationDTO translateText(String text) {
67+
private AIJobDescriptionTranslationDTO translateText(String text, String toLang) {
5368
return chatClient
5469
.prompt()
55-
.user(u -> u.text(translationResource).param("text", text))
70+
.user(u -> u.text(translationResource).param("text", text).param("targetLanguage", toLang))
5671
.call()
5772
.entity(AIJobDescriptionTranslationDTO.class);
5873
}
74+
75+
/**
76+
* Translates the provided job description text and persists the translated version
77+
* in the job entity for the specified language.
78+
*
79+
* @param jobId the ID of the job to update
80+
* @param toLang the target language for translation ("de" or "en")
81+
* @param text the job description text to translate
82+
* @return The translated text response with detected and target language info
83+
*/
84+
public AIJobDescriptionTranslationDTO translateAndPersistJobDescription(String jobId, String toLang, String text) {
85+
AIJobDescriptionTranslationDTO translated = translateText(text, toLang);
86+
String translatedText = translated.translatedText();
87+
if (translatedText != null && !translatedText.isBlank()) {
88+
jobService.updateJobDescriptionLanguage(jobId, toLang, translatedText);
89+
}
90+
return translated;
91+
}
5992
}

src/main/java/de/tum/cit/aet/ai/web/AiResource.java

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,28 +30,38 @@ public AiResource(AiService aiService) {
3030
/**
3131
* Generate a job application draft from the provided structured job form.
3232
*
33-
* @param jobForm the job form data used to build the AI prompt
33+
* @param descriptionLanguage the language for the generated job description
34+
* @param jobForm the job form data used to build the AI prompt
3435
* @return a ResponseEntity containing the generated draft as JSON string
3536
*/
3637
@ProfessorOrEmployeeOrAdmin
3738
@PutMapping(value = "generateJobDescription", produces = MediaType.APPLICATION_JSON_VALUE)
38-
public ResponseEntity<AIJobDescriptionDTO> generateJobApplicationDraft(@RequestBody JobFormDTO jobForm) {
39-
log.info("PUT /api/ai/generateJobDescription - Request received");
40-
return ResponseEntity.ok(aiService.generateJobApplicationDraft(jobForm));
39+
public ResponseEntity<AIJobDescriptionDTO> generateJobApplicationDraft(
40+
@RequestBody JobFormDTO jobForm,
41+
@RequestParam("lang") String descriptionLanguage
42+
) {
43+
log.info("PUT /api/ai/generateJobDescription - Request received (lang={})", descriptionLanguage);
44+
return ResponseEntity.ok(aiService.generateJobApplicationDraft(jobForm, descriptionLanguage));
4145
}
4246

4347
/**
4448
* Translate text between German and English.
4549
* Automatically detects the source language and translates to the other language.
4650
* Preserves the original text structure and formatting.
4751
*
52+
* @param jobId the ID of the job for which the description is being translated
53+
* @param toLang the target language for translation ("de" or "en")
4854
* @param text the text to translate (German or English)
4955
* @return a ResponseEntity containing the translated text with language info
5056
*/
5157
@ProfessorOrEmployeeOrAdmin
52-
@PutMapping(value = "translateJobDescription", produces = MediaType.APPLICATION_JSON_VALUE)
53-
public ResponseEntity<AIJobDescriptionTranslationDTO> translateText(@RequestBody String text) {
54-
log.info("PUT /api/ai/translateJobDescription - Request received");
55-
return ResponseEntity.ok(aiService.translateText(text));
58+
@PutMapping(value = "translateJobDescriptionForJob", produces = MediaType.APPLICATION_JSON_VALUE)
59+
public ResponseEntity<AIJobDescriptionTranslationDTO> translateJobDescriptionForJob(
60+
@RequestParam("jobId") String jobId,
61+
@RequestParam("toLang") String toLang,
62+
@RequestBody String text
63+
) {
64+
log.info("PUT /api/ai/translateJobDescriptionForJob - Request received (jobId={}, toLang={})", jobId, toLang);
65+
return ResponseEntity.ok(aiService.translateAndPersistJobDescription(jobId, toLang, text));
5666
}
5767
}

src/main/java/de/tum/cit/aet/application/service/ApplicationService.java

Lines changed: 84 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -208,33 +208,7 @@ private void syncSnapshotDataToApplicant(Application application) {
208208
Applicant applicant = application.getApplicant();
209209
User user = applicant.getUser();
210210

211-
// Update user data
212-
user.setFirstName(application.getApplicantFirstName());
213-
user.setLastName(application.getApplicantLastName());
214-
user.setGender(application.getApplicantGender());
215-
user.setNationality(application.getApplicantNationality());
216-
user.setBirthday(application.getApplicantBirthday());
217-
user.setPhoneNumber(application.getApplicantPhoneNumber());
218-
user.setWebsite(application.getApplicantWebsite());
219-
user.setLinkedinUrl(application.getApplicantLinkedinUrl());
220-
userRepository.save(user);
221-
222-
// Update applicant data
223-
applicant.setStreet(application.getApplicantStreet());
224-
applicant.setPostalCode(application.getApplicantPostalCode());
225-
applicant.setCity(application.getApplicantCity());
226-
applicant.setCountry(application.getApplicantCountry());
227-
applicant.setBachelorDegreeName(application.getApplicantBachelorDegreeName());
228-
applicant.setBachelorGradeUpperLimit(application.getApplicantBachelorGradeUpperLimit());
229-
applicant.setBachelorGradeLowerLimit(application.getApplicantBachelorGradeLowerLimit());
230-
applicant.setBachelorGrade(application.getApplicantBachelorGrade());
231-
applicant.setBachelorUniversity(application.getApplicantBachelorUniversity());
232-
applicant.setMasterDegreeName(application.getApplicantMasterDegreeName());
233-
applicant.setMasterGradeUpperLimit(application.getApplicantMasterGradeUpperLimit());
234-
applicant.setMasterGradeLowerLimit(application.getApplicantMasterGradeLowerLimit());
235-
applicant.setMasterGrade(application.getApplicantMasterGrade());
236-
applicant.setMasterUniversity(application.getApplicantMasterUniversity());
237-
applicantRepository.save(applicant);
211+
applyApplicantData(user, applicant, ApplicantDTO.getFromApplicationSnapshot(application));
238212
}
239213

240214
private void confirmApplicationToApplicant(Application application) {
@@ -473,6 +447,53 @@ public void renameDocument(UUID documentId, String newName) {
473447
documentDictionaryService.renameDocument(documentId, newName);
474448
}
475449

450+
/**
451+
* Retrieves the current user's applicant profile with all personal information.
452+
* Creates an empty applicant profile if none exists yet.
453+
*
454+
* @return the ApplicantDTO with current user and applicant data
455+
*/
456+
public ApplicantDTO getApplicantProfile() {
457+
UUID userId = currentUserService.getUserId();
458+
if (userId == null) {
459+
throw new InvalidParameterException("UserId must not be null.");
460+
}
461+
462+
Optional<Applicant> applicantOptional = applicantRepository.findById(userId);
463+
Applicant applicant;
464+
if (applicantOptional.isEmpty()) {
465+
applicant = createApplicant(userId);
466+
} else {
467+
applicant = applicantOptional.get();
468+
}
469+
470+
return ApplicantDTO.getFromEntity(applicant);
471+
}
472+
473+
/**
474+
* Updates the current user's applicant profile with personal information.
475+
* Writes directly to `User` and `Applicant` entities.
476+
*
477+
* @param dto the updated applicant data
478+
* @return the updated ApplicantDTO
479+
*/
480+
@Transactional
481+
public ApplicantDTO updateApplicantProfile(ApplicantDTO dto) {
482+
UUID userId = currentUserService.getUserId();
483+
if (userId == null) {
484+
throw new InvalidParameterException("UserId must not be null.");
485+
}
486+
487+
User user = userRepository.findById(userId).orElseThrow(() -> EntityNotFoundException.forId("User", userId));
488+
Applicant applicant = applicantRepository.findById(userId).orElseGet(() -> createApplicant(userId));
489+
490+
applyApplicantData(user, applicant, dto);
491+
userRepository.save(user);
492+
applicantRepository.save(applicant);
493+
494+
return ApplicantDTO.getFromEntity(applicant);
495+
}
496+
476497
/**
477498
* Creates an Applicant for the given userId
478499
*
@@ -486,6 +507,42 @@ private Applicant createApplicant(UUID userId) {
486507
return applicantRepository.save(applicant);
487508
}
488509

510+
/**
511+
* Applies applicant and user data from a DTO and persists both entities.
512+
*/
513+
private void applyApplicantData(User user, Applicant applicant, ApplicantDTO dto) {
514+
if (dto.user() != null) {
515+
if (dto.user().firstName() != null) user.setFirstName(dto.user().firstName());
516+
if (dto.user().lastName() != null) user.setLastName(dto.user().lastName());
517+
if (dto.user().email() != null) user.setEmail(dto.user().email());
518+
user.setGender(dto.user().gender());
519+
user.setNationality(dto.user().nationality());
520+
user.setBirthday(dto.user().birthday());
521+
user.setPhoneNumber(dto.user().phoneNumber());
522+
user.setWebsite(dto.user().website());
523+
user.setLinkedinUrl(dto.user().linkedinUrl());
524+
}
525+
526+
applicant.setStreet(dto.street());
527+
applicant.setPostalCode(dto.postalCode());
528+
applicant.setCity(dto.city());
529+
applicant.setCountry(dto.country());
530+
531+
applicant.setBachelorDegreeName(dto.bachelorDegreeName());
532+
applicant.setBachelorGradeUpperLimit(dto.bachelorGradeUpperLimit());
533+
applicant.setBachelorGradeLowerLimit(dto.bachelorGradeLowerLimit());
534+
applicant.setBachelorGrade(dto.bachelorGrade());
535+
applicant.setBachelorUniversity(dto.bachelorUniversity());
536+
537+
applicant.setMasterDegreeName(dto.masterDegreeName());
538+
applicant.setMasterGradeUpperLimit(dto.masterGradeUpperLimit());
539+
applicant.setMasterGradeLowerLimit(dto.masterGradeLowerLimit());
540+
applicant.setMasterGrade(dto.masterGrade());
541+
applicant.setMasterUniversity(dto.masterUniversity());
542+
543+
// Save operations moved to updateApplicantProfile
544+
}
545+
489546
/**
490547
* Asserts that the current user can manage the application with the given ID.
491548
*

0 commit comments

Comments
 (0)