Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions client/src/components/TopicsTable/TopicsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ const TopicsTable = (props: ITopicOverviewsTableProps) => {
return 'red'
case TopicState.DRAFT:
return 'yellow'
case TopicState.EXPIRED:
return 'orange'
default:
return 'gray'
}
Expand Down
2 changes: 1 addition & 1 deletion client/src/pages/LandingPage/LandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ const LandingPage = () => {
justify='center'
onClick={(e) => e.stopPropagation()}
>
{topic.state !== TopicState.CLOSED && (
{topic.state === TopicState.OPEN && (
<Button
component={Link}
to={`/submit-application/${topic.topicId}`}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Card, Flex, Text, Group, Stack, Button, Tooltip, Anchor, Box } from '@mantine/core'
import type { ITopicOverview } from '../../../../../requests/responses/topic'
import { TopicState } from '../../../../../requests/responses/topic'
import { useHover, useMediaQuery } from '@mantine/hooks'
import { DownloadSimple, Users } from '@phosphor-icons/react'
import AvatarUserList from '../../../../../components/AvatarUserList/AvatarUserList'
Expand Down Expand Up @@ -129,16 +130,14 @@ const TopicCard = ({ topic, setOpenTopic }: ITopicCardProps) => {
<DownloadSimple />
</Button>
</Group>
) : 'applicationDeadline' in topic &&
topic.applicationDeadline &&
new Date(topic.applicationDeadline) < new Date() ? (
<Button fullWidth mt='md' component={Link} to={`/topics/${topicId}`}>
More Information
</Button>
) : (
) : 'state' in topic && topic.state === TopicState.OPEN ? (
<Button fullWidth mt='md' component={Link} to={`/submit-application/${topicId}`}>
Apply
</Button>
) : (
<Button fullWidth mt='md' component={Link} to={`/topics/${topicId}`}>
More Information
</Button>
)}
</Card>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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 (
<Card
Expand Down Expand Up @@ -157,13 +155,13 @@ const CollapsibleTopicElement = ({ topic, onApply }: ICollapsibleTopicElementPro
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- useTopic may return `false` (error sentinel) which must also map to undefined
onClick={() => onApply(fullTopic || undefined)}
fullWidth
disabled={!fullTopic || deadlinePassed}
disabled={!canApply}
>
Apply
</Button>
{deadlinePassed && (
{!canApply && !!fullTopic && (
<Text size='xs' c='dimmed' ta='center'>
Application deadline has passed.
Applications are not open for this topic.
</Text>
)}
</Stack>
Expand Down
18 changes: 9 additions & 9 deletions client/src/pages/TopicPage/TopicPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }>()
Expand Down Expand Up @@ -42,20 +43,15 @@ const TopicPage = () => {
return isExaminer || isSupervisor
}

const deadlinePassed =
!!topic.applicationDeadline && new Date(topic.applicationDeadline) < new Date()
const canApply = topic.state === TopicState.OPEN

return (
<Stack gap={'2rem'}>
<Stack gap={'1rem'}>
<Title>{topic.title}</Title>
{!managementAccess && !checkIfUserIsExaminerOrSupervisor() && (
<Stack gap='xs' mr='auto'>
{deadlinePassed ? (
<Button leftSection={<NotePencil size={24} />} size='md' disabled>
Apply Now
</Button>
) : (
{canApply ? (
<Button
component={Link}
to={`/submit-application/${topic.topicId}`}
Expand All @@ -64,10 +60,14 @@ const TopicPage = () => {
>
Apply Now
</Button>
) : (
<Button leftSection={<NotePencil size={24} />} size='md' disabled>
Apply Now
</Button>
)}
{deadlinePassed && (
{!canApply && (
<Text size='sm' c='dimmed'>
Application deadline has passed.
Applications are not open for this topic.
</Text>
)}
</Stack>
Expand Down
1 change: 1 addition & 0 deletions client/src/requests/responses/topic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export enum TopicState {
OPEN = 'OPEN',
DRAFT = 'DRAFT',
CLOSED = 'CLOSED',
EXPIRED = 'EXPIRED',
}

export interface ITopicOverview {
Expand Down
1 change: 1 addition & 0 deletions client/src/utils/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export function formatTopicState(state: TopicState) {
[TopicState.OPEN]: 'Open',
[TopicState.DRAFT]: 'Draft',
[TopicState.CLOSED]: 'Closed',
[TopicState.EXPIRED]: 'Expired',
}

return stateMap[state]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
public enum TopicState {
OPEN("OPEN"),
CLOSED("WRITING"),
DRAFT("DRAFT");
DRAFT("DRAFT"),
EXPIRED("EXPIRED");

private final String value;
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ public void rejectOldApplications() {
for (ResearchGroupSettings settings : enabledResearchGroups) {
Map<User, List<ApplicationRejectObject>> reminderApplicationsByUser = new HashMap<>();

List<Topic> openTopics = topicService.getOpenFromResearchGroup(settings.getResearchGroupId());
for (Topic topic : openTopics) {
List<Topic> publishedTopics = topicService.getPublishedFromResearchGroup(settings.getResearchGroupId());
for (Topic topic : publishedTopics) {
Instant referenceDate;

if (topic.getApplicationDeadline() != null) {
Expand Down
2 changes: 2 additions & 0 deletions server/src/main/java/de/tum/cit/aet/thesis/entity/Topic.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment thread
bensofficial marked this conversation as resolved.
} else {
return TopicState.OPEN;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -20,9 +21,10 @@ public interface TopicRepository extends JpaRepository<Topic, UUID> {
(
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())
)
Comment thread
bensofficial marked this conversation as resolved.
)
""", nativeQuery = true)
Expand All @@ -37,6 +39,8 @@ Page<Topic> 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) " +
Comment thread
bensofficial marked this conversation as resolved.
"AND ( :userId IS NULL " +
" OR ( :excludeSupervised = true " +
" AND EXISTS (SELECT 1 FROM TopicRole r " +
Expand All @@ -57,7 +61,18 @@ Page<Topic> 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)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
bensofficial marked this conversation as resolved.
""")
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<Topic> findPublishedTopics(
@Param("researchGroupId") UUID researchGroupId
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,12 @@ public Page<Topic> 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);
}
Comment thread
bensofficial marked this conversation as resolved.
String[] statesFilter = (states != null && states.length > 0) ? states : new String[] { TopicState.OPEN.name() };

Expand Down Expand Up @@ -178,6 +182,10 @@ public List<Topic> getOpenFromResearchGroup(UUID researchGroupId) {
).toList();
}

public List<Topic> getPublishedFromResearchGroup(UUID researchGroupId) {
return topicRepository.findPublishedTopics(researchGroupId);
}
Comment thread
bensofficial marked this conversation as resolved.

// TODO: we should avoid using @Transactional because it can lead to performance issue and concurrency problems
@Transactional
public Topic createTopic(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
Loading