diff --git a/client/src/components/TopicsTable/TopicsTable.tsx b/client/src/components/TopicsTable/TopicsTable.tsx
index 5d17fccac..22c7ec670 100644
--- a/client/src/components/TopicsTable/TopicsTable.tsx
+++ b/client/src/components/TopicsTable/TopicsTable.tsx
@@ -44,6 +44,8 @@ const TopicsTable = (props: ITopicOverviewsTableProps) => {
return 'red'
case TopicState.DRAFT:
return 'yellow'
+ case TopicState.EXPIRED:
+ return 'orange'
default:
return 'gray'
}
diff --git a/client/src/pages/LandingPage/LandingPage.tsx b/client/src/pages/LandingPage/LandingPage.tsx
index 7fe23dbd7..ef7d54f56 100644
--- a/client/src/pages/LandingPage/LandingPage.tsx
+++ b/client/src/pages/LandingPage/LandingPage.tsx
@@ -182,7 +182,7 @@ const LandingPage = () => {
justify='center'
onClick={(e) => e.stopPropagation()}
>
- {topic.state !== TopicState.CLOSED && (
+ {topic.state === TopicState.OPEN && (
- ) : 'applicationDeadline' in topic &&
- topic.applicationDeadline &&
- new Date(topic.applicationDeadline) < new Date() ? (
-
- ) : (
+ ) : 'state' in topic && topic.state === TopicState.OPEN ? (
+ ) : (
+
)}
)
diff --git a/client/src/pages/ReplaceApplicationPage/components/SelectTopicStep/components/CollapsibleTopicElement.tsx b/client/src/pages/ReplaceApplicationPage/components/SelectTopicStep/components/CollapsibleTopicElement.tsx
index 7412d1f56..c6e1527f8 100644
--- a/client/src/pages/ReplaceApplicationPage/components/SelectTopicStep/components/CollapsibleTopicElement.tsx
+++ b/client/src/pages/ReplaceApplicationPage/components/SelectTopicStep/components/CollapsibleTopicElement.tsx
@@ -12,6 +12,7 @@ import {
Loader,
} from '@mantine/core'
import type { ITopicOverview, ITopic } from '../../../../../requests/responses/topic'
+import { TopicState } from '../../../../../requests/responses/topic'
import ThesisTypeBadge from '../../../../LandingPage/components/ThesisTypBadge/ThesisTypBadge'
import type { IPublishedThesis } from '../../../../../requests/responses/thesis'
import { useHover } from '@mantine/hooks'
@@ -34,10 +35,7 @@ const CollapsibleTopicElement = ({ topic, onApply }: ICollapsibleTopicElementPro
const isTopicOverview = 'topicId' in topic
const fullTopic = useTopic(isTopicOverview && expanded ? topic.topicId : undefined)
- const deadlinePassed =
- !!fullTopic &&
- !!fullTopic.applicationDeadline &&
- new Date(fullTopic.applicationDeadline) < new Date()
+ const canApply = !!fullTopic && fullTopic.state === TopicState.OPEN
return (
onApply(fullTopic || undefined)}
fullWidth
- disabled={!fullTopic || deadlinePassed}
+ disabled={!canApply}
>
Apply
- {deadlinePassed && (
+ {!canApply && !!fullTopic && (
- Application deadline has passed.
+ Applications are not open for this topic.
)}
diff --git a/client/src/pages/TopicPage/TopicPage.tsx b/client/src/pages/TopicPage/TopicPage.tsx
index 324e5f670..f49d8cd2a 100644
--- a/client/src/pages/TopicPage/TopicPage.tsx
+++ b/client/src/pages/TopicPage/TopicPage.tsx
@@ -11,6 +11,7 @@ import ApplicationsTable from '../../components/ApplicationsTable/ApplicationsTa
import { NotePencil } from '@phosphor-icons/react'
import TopicAdittionalInformationCard from './components/TopicAdittionalInformationCard'
import TopicInformationCard from './components/TopicInformationCard'
+import { TopicState } from '../../requests/responses/topic'
const TopicPage = () => {
const { topicId } = useParams<{ topicId: string }>()
@@ -42,8 +43,7 @@ const TopicPage = () => {
return isExaminer || isSupervisor
}
- const deadlinePassed =
- !!topic.applicationDeadline && new Date(topic.applicationDeadline) < new Date()
+ const canApply = topic.state === TopicState.OPEN
return (
@@ -51,11 +51,7 @@ const TopicPage = () => {
{topic.title}
{!managementAccess && !checkIfUserIsExaminerOrSupervisor() && (
- {deadlinePassed ? (
- } size='md' disabled>
- Apply Now
-
- ) : (
+ {canApply ? (
+ ) : (
+ } size='md' disabled>
+ Apply Now
+
)}
- {deadlinePassed && (
+ {!canApply && (
- Application deadline has passed.
+ Applications are not open for this topic.
)}
diff --git a/client/src/requests/responses/topic.ts b/client/src/requests/responses/topic.ts
index 6dac20708..eb2d01536 100644
--- a/client/src/requests/responses/topic.ts
+++ b/client/src/requests/responses/topic.ts
@@ -5,6 +5,7 @@ export enum TopicState {
OPEN = 'OPEN',
DRAFT = 'DRAFT',
CLOSED = 'CLOSED',
+ EXPIRED = 'EXPIRED',
}
export interface ITopicOverview {
diff --git a/client/src/utils/format.ts b/client/src/utils/format.ts
index 00c4cc91a..e44818d33 100644
--- a/client/src/utils/format.ts
+++ b/client/src/utils/format.ts
@@ -130,6 +130,7 @@ export function formatTopicState(state: TopicState) {
[TopicState.OPEN]: 'Open',
[TopicState.DRAFT]: 'Draft',
[TopicState.CLOSED]: 'Closed',
+ [TopicState.EXPIRED]: 'Expired',
}
return stateMap[state]
diff --git a/server/src/main/java/de/tum/cit/aet/thesis/constants/TopicState.java b/server/src/main/java/de/tum/cit/aet/thesis/constants/TopicState.java
index 450ed09c7..3c892abcd 100644
--- a/server/src/main/java/de/tum/cit/aet/thesis/constants/TopicState.java
+++ b/server/src/main/java/de/tum/cit/aet/thesis/constants/TopicState.java
@@ -8,7 +8,8 @@
public enum TopicState {
OPEN("OPEN"),
CLOSED("WRITING"),
- DRAFT("DRAFT");
+ DRAFT("DRAFT"),
+ EXPIRED("EXPIRED");
private final String value;
}
diff --git a/server/src/main/java/de/tum/cit/aet/thesis/cron/AutomaticRejects.java b/server/src/main/java/de/tum/cit/aet/thesis/cron/AutomaticRejects.java
index 9da8d8f79..48b051246 100644
--- a/server/src/main/java/de/tum/cit/aet/thesis/cron/AutomaticRejects.java
+++ b/server/src/main/java/de/tum/cit/aet/thesis/cron/AutomaticRejects.java
@@ -62,8 +62,8 @@ public void rejectOldApplications() {
for (ResearchGroupSettings settings : enabledResearchGroups) {
Map> reminderApplicationsByUser = new HashMap<>();
- List openTopics = topicService.getOpenFromResearchGroup(settings.getResearchGroupId());
- for (Topic topic : openTopics) {
+ List publishedTopics = topicService.getPublishedFromResearchGroup(settings.getResearchGroupId());
+ for (Topic topic : publishedTopics) {
Instant referenceDate;
if (topic.getApplicationDeadline() != null) {
diff --git a/server/src/main/java/de/tum/cit/aet/thesis/entity/Topic.java b/server/src/main/java/de/tum/cit/aet/thesis/entity/Topic.java
index d01e80a02..bb78964b4 100644
--- a/server/src/main/java/de/tum/cit/aet/thesis/entity/Topic.java
+++ b/server/src/main/java/de/tum/cit/aet/thesis/entity/Topic.java
@@ -129,6 +129,8 @@ public TopicState getTopicState() {
return TopicState.CLOSED;
} else if (this.publishedAt == null) {
return TopicState.DRAFT;
+ } else if (this.applicationDeadline != null && this.applicationDeadline.isBefore(Instant.now())) {
+ return TopicState.EXPIRED;
} else {
return TopicState.OPEN;
}
diff --git a/server/src/main/java/de/tum/cit/aet/thesis/repository/TopicRepository.java b/server/src/main/java/de/tum/cit/aet/thesis/repository/TopicRepository.java
index 86f6357a9..e368df12e 100644
--- a/server/src/main/java/de/tum/cit/aet/thesis/repository/TopicRepository.java
+++ b/server/src/main/java/de/tum/cit/aet/thesis/repository/TopicRepository.java
@@ -8,6 +8,7 @@
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
+import java.util.List;
import java.util.Set;
import java.util.UUID;
@@ -20,9 +21,10 @@ public interface TopicRepository extends JpaRepository {
(
CAST(:states AS TEXT[]) IS NULL
OR (
- ('CLOSED' = ANY(CAST(:states AS TEXT[])) AND t.closed_at IS NOT NULL)
- OR ('DRAFT' = ANY(CAST(:states AS TEXT[])) AND t.closed_at IS NULL AND t.published_at IS NULL)
- OR ('OPEN' = ANY(CAST(:states AS TEXT[])) AND t.closed_at IS NULL AND t.published_at IS NOT NULL)
+ ('CLOSED' = ANY(CAST(:states AS TEXT[])) AND t.closed_at IS NOT NULL)
+ OR ('DRAFT' = ANY(CAST(:states AS TEXT[])) AND t.closed_at IS NULL AND t.published_at IS NULL)
+ OR ('OPEN' = ANY(CAST(:states AS TEXT[])) AND t.closed_at IS NULL AND t.published_at IS NOT NULL AND (t.application_deadline IS NULL OR t.application_deadline >= NOW()))
+ OR ('EXPIRED' = ANY(CAST(:states AS TEXT[])) AND t.closed_at IS NULL AND t.published_at IS NOT NULL AND t.application_deadline IS NOT NULL AND t.application_deadline < NOW())
)
)
""", nativeQuery = true)
@@ -37,6 +39,8 @@ Page searchTopics(
@Query("SELECT DISTINCT t FROM Topic t " +
"WHERE (:searchQuery IS NULL OR LOWER(t.title) LIKE :searchQuery) " +
"AND t.closedAt IS NULL " +
+ "AND t.publishedAt IS NOT NULL " +
+ "AND (t.applicationDeadline IS NULL OR t.applicationDeadline >= CURRENT_TIMESTAMP) " +
"AND ( :userId IS NULL " +
" OR ( :excludeSupervised = true " +
" AND EXISTS (SELECT 1 FROM TopicRole r " +
@@ -57,7 +61,18 @@ Page findOpenTopicsForUserByRoles(
SELECT COUNT(*)
FROM Topic t
WHERE t.closedAt IS NULL
+ AND t.publishedAt IS NOT NULL
+ AND (t.applicationDeadline IS NULL OR t.applicationDeadline >= CURRENT_TIMESTAMP)
AND (:researchGroupId IS NULL OR t.researchGroup.id = :researchGroupId)
""")
long countOpenTopics(@Param("researchGroupId") UUID researchGroupId);
+
+ @Query("""
+ SELECT t FROM Topic t
+ WHERE t.closedAt IS NULL AND t.publishedAt IS NOT NULL
+ AND (:researchGroupId IS NULL OR t.researchGroup.id = :researchGroupId)
+ """)
+ List findPublishedTopics(
+ @Param("researchGroupId") UUID researchGroupId
+ );
}
diff --git a/server/src/main/java/de/tum/cit/aet/thesis/service/ApplicationService.java b/server/src/main/java/de/tum/cit/aet/thesis/service/ApplicationService.java
index 9fd403259..8fc4440fb 100644
--- a/server/src/main/java/de/tum/cit/aet/thesis/service/ApplicationService.java
+++ b/server/src/main/java/de/tum/cit/aet/thesis/service/ApplicationService.java
@@ -203,6 +203,10 @@ public Application createApplication(User user, UUID researchGroupId, UUID topic
Topic topic = topicId == null ? null : topicService.findById(topicId);
Instant now = Instant.now(clock);
+ if (topic != null && topic.getPublishedAt() == null) {
+ throw new ResourceInvalidParametersException("This topic is not open for applications.");
+ }
+
if (topic != null && topic.getClosedAt() != null) {
throw new ResourceInvalidParametersException("This topic is already closed. You cannot submit new applications for it.");
}
diff --git a/server/src/main/java/de/tum/cit/aet/thesis/service/TopicService.java b/server/src/main/java/de/tum/cit/aet/thesis/service/TopicService.java
index 3384fe444..8a06f51a3 100644
--- a/server/src/main/java/de/tum/cit/aet/thesis/service/TopicService.java
+++ b/server/src/main/java/de/tum/cit/aet/thesis/service/TopicService.java
@@ -111,8 +111,12 @@ public Page getAll(
String[] typesFilter = types == null || types.length == 0 ? null : types;
- if (states != null && Arrays.stream(states).anyMatch(s -> s.equals(TopicState.CLOSED.name()) || s.equals(TopicState.DRAFT.name()))) {
- currentUserProvider().assertCanAccessResearchGroup(researchGroup);
+ if (states != null && Arrays.stream(states).anyMatch(s -> s.equals(TopicState.CLOSED.name()) || s.equals(TopicState.DRAFT.name()) || s.equals(TopicState.EXPIRED.name()))) {
+ CurrentUserProvider cup = currentUserProvider();
+ if (!cup.isAdmin() && !cup.getUser().hasAnyGroup("supervisor", "advisor")) {
+ throw new org.springframework.security.access.AccessDeniedException("Only privileged users can query non-OPEN topic states.");
+ }
+ cup.assertCanAccessResearchGroup(researchGroup);
}
String[] statesFilter = (states != null && states.length > 0) ? states : new String[] { TopicState.OPEN.name() };
@@ -178,6 +182,10 @@ public List getOpenFromResearchGroup(UUID researchGroupId) {
).toList();
}
+ public List getPublishedFromResearchGroup(UUID researchGroupId) {
+ return topicRepository.findPublishedTopics(researchGroupId);
+ }
+
// TODO: we should avoid using @Transactional because it can lead to performance issue and concurrency problems
@Transactional
public Topic createTopic(
diff --git a/server/src/test/java/de/tum/cit/aet/thesis/mock/EntityMockFactory.java b/server/src/test/java/de/tum/cit/aet/thesis/mock/EntityMockFactory.java
index 0041f2020..52092e488 100644
--- a/server/src/test/java/de/tum/cit/aet/thesis/mock/EntityMockFactory.java
+++ b/server/src/test/java/de/tum/cit/aet/thesis/mock/EntityMockFactory.java
@@ -13,6 +13,7 @@
import de.tum.cit.aet.thesis.entity.key.ThesisRoleId;
import de.tum.cit.aet.thesis.entity.key.UserGroupId;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -92,6 +93,9 @@ public static Topic createTopic(String title, ResearchGroup researchGroup) {
topic.setTitle(title);
topic.setRoles(new ArrayList<>());
topic.setResearchGroup(researchGroup);
+ topic.setCreatedAt(Instant.now());
+ topic.setUpdatedAt(Instant.now());
+ topic.setPublishedAt(Instant.now());
return topic;
}