Skip to content

Commit

Permalink
Finish UI for club approval history + testcase
Browse files Browse the repository at this point in the history
  • Loading branch information
julianweng committed Nov 11, 2024
1 parent 539bd01 commit 835355d
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 9 deletions.
28 changes: 28 additions & 0 deletions backend/clubs/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2975,6 +2975,34 @@ class Meta:
)


class ApprovalHistorySerializer(serializers.ModelSerializer):
approved = serializers.BooleanField()
approved_on = serializers.DateTimeField()
approved_by = serializers.SerializerMethodField("get_approved_by")
approved_comment = serializers.CharField()
history_date = serializers.DateTimeField()

def get_approved_by(self, obj):
user = self.context["request"].user
if not user.is_authenticated:
return None
if not user.has_perm("clubs.see_pending_clubs"):
return None
if obj.approved_by is None:
return "Unknown"
return obj.approved_by.get_full_name()

class Meta:
model = Club
fields = (
"approved",
"approved_on",
"approved_by",
"approved_comment",
"history_date",
)


class AdminNoteSerializer(ClubRouteMixin, serializers.ModelSerializer):
creator = serializers.SerializerMethodField("get_creator")
title = serializers.CharField(max_length=255, default="Note")
Expand Down
11 changes: 8 additions & 3 deletions backend/clubs/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@
ApplicationSubmissionCSVSerializer,
ApplicationSubmissionSerializer,
ApplicationSubmissionUserSerializer,
ApprovalHistorySerializer,
AssetSerializer,
AuthenticatedClubSerializer,
AuthenticatedMembershipSerializer,
Expand Down Expand Up @@ -1283,9 +1284,11 @@ def history(self, request, *args, **kwargs):
"""
club = self.get_object()
return Response(
club.history.order_by("approved_on").values(
"approved", "approved_on", "approved_by", "history_date"
)
ApprovalHistorySerializer(
club.history.order_by("history_date"),
many=True,
context={"request": request},
).data
)

@action(detail=True, methods=["get"])
Expand Down Expand Up @@ -2171,6 +2174,8 @@ def get_serializer_class(self):
return ClubConstitutionSerializer
if self.action == "notes_about":
return NoteSerializer
if self.action == "history":
return ApprovalHistorySerializer
if self.action in {"list", "fields"}:
if self.request is not None and (
self.request.accepted_renderer.format == "xlsx"
Expand Down
13 changes: 13 additions & 0 deletions backend/tests/clubs/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2182,6 +2182,12 @@ def test_club_sensitive_field_renew(self):
club.refresh_from_db()
self.assertTrue(club.approved)

# store result of approval history query
resp = self.client.get(reverse("clubs-history", args=(club.code,)))
self.assertIn(resp.status_code, [200], resp.content)
previous_history = json.loads(resp.content.decode("utf-8"))
self.assertTrue(previous_history[-1]["approved"])

with patch("django.conf.settings.REAPPROVAL_QUEUE_OPEN", True):
for field in {"name"}:
# edit sensitive field
Expand All @@ -2191,6 +2197,13 @@ def test_club_sensitive_field_renew(self):
content_type="application/json",
)
self.assertIn(resp.status_code, [200, 201], resp.content)
resp = self.client.get(reverse("clubs-history", args=(club.code,)))
# find the approval history
resp = self.client.get(reverse("clubs-history", args=(club.code,)))
self.assertIn(resp.status_code, [200], resp.content)
history = json.loads(resp.content.decode("utf-8"))
self.assertEqual(len(history), len(previous_history) + 1)
self.assertFalse(history[-1]["approved"])

# ensure club is marked as not approved
club.refresh_from_db()
Expand Down
112 changes: 110 additions & 2 deletions frontend/components/ClubPage/ClubApprovalDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { EmotionJSX } from '@emotion/react/types/jsx-namespace'
import moment from 'moment-timezone'
import { useRouter } from 'next/router'
import { ReactElement, useEffect, useState } from 'react'
import Select from 'react-select'
Expand All @@ -18,6 +20,7 @@ import {
SITE_NAME,
} from '../../utils/branding'
import { Contact, Icon, Modal, Text, TextQuote } from '../common'
import { Chevron } from '../DropdownFilter'
import { ModalContent } from './Actions'

type Props = {
Expand All @@ -34,9 +37,100 @@ type HistoricItem = {
approved: boolean | null
approved_on: string | null
approved_by: string | null
approved_comment: string | null
history_date: string
}

const ClubHistoryDropdown = ({ history }: { history: HistoricItem[] }) => {
const [active, setActive] = useState<boolean>(false)
const [reason, setReason] = useState<string | null>(null)
const getReason = (item: HistoricItem): EmotionJSX.Element | string => {
return item.approved_comment ? (
item.approved_comment.length > 100 ? (
<span
style={{
cursor: 'pointer',
textDecoration: 'underline',
}}
onClick={() => setReason(item.approved_comment)}
>
View Reason
</span>
) : (
item.approved_comment
)
) : (
'No reason provided'
)
}
return (
<>
<div
style={{
cursor: 'pointer',
}}
className="mt-2"
onClick={() => setActive(!active)}
>
{active ? 'Hide' : 'Show'} History
<Chevron
name="chevron-down"
alt="toggle dropdown"
open={active}
color="inherit"
className="ml-1"
/>
</div>
<Modal
show={reason !== null}
closeModal={() => setReason(null)}
marginBottom={false}
width="80%"
>
<ModalContent>{reason}</ModalContent>
</Modal>
{active && (
<div style={{ maxHeight: '300px', overflowY: 'auto' }}>
{history.map((item, i) => (
<div
key={i}
className="mt-2"
style={{
fontSize: '70%',
}}
>
{item.approved === true ? (
<TextQuote className="py-0">
<b>Approved</b> by <b>{item.approved_by}</b> on{' '}
{moment(item.history_date)
.tz('America/New_York')
.format('LLL')}{' '}
- {getReason(item)}
</TextQuote>
) : item.approved === false ? (
<TextQuote className="py-0">
<b>Rejected</b> by <b>{item.approved_by}</b> on{' '}
{moment(item.history_date)
.tz('America/New_York')
.format('LLL')}{' '}
- {getReason(item)}
</TextQuote>
) : (
<TextQuote className="py-0">
<b>Submitted for re-approval</b> on{' '}
{moment(item.history_date)
.tz('America/New_York')
.format('LLL')}
</TextQuote>
)}
</div>
))}
</div>
)}
</>
)
}

const ClubApprovalDialog = ({ club }: Props): ReactElement | null => {
const router = useRouter()
const year = getCurrentSchoolYear()
Expand Down Expand Up @@ -73,9 +167,21 @@ const ClubApprovalDialog = ({ club }: Props): ReactElement | null => {

doApiRequest(`/clubs/${club.code}/history/?format=json`)
.then((resp) => resp.json())
.then(setHistory)
}
.then((data) => {
// Get last version of club for each change in approved status
const lastVersions: HistoricItem[] = []

for (let i = data.length - 1; i >= 0; i--) {
const item = data[i]
const lastItem = lastVersions[lastVersions.length - 1]

if (item.approved !== lastItem?.approved || !lastItem) {
lastVersions.push(item)
}
}
setHistory(lastVersions)
})
}
setComment(
selectedTemplates.map((template) => template.content).join('\n\n'),
)
Expand Down Expand Up @@ -142,6 +248,7 @@ const ClubApprovalDialog = ({ club }: Props): ReactElement | null => {
>
<Icon name="x" /> Revoke Approval
</button>
<ClubHistoryDropdown history={history} />
</div>
)}
{(club.active || canDeleteClub) && club.approved !== true ? (
Expand Down Expand Up @@ -378,6 +485,7 @@ const ClubApprovalDialog = ({ club }: Props): ReactElement | null => {
</button>
</>
)}
<ClubHistoryDropdown history={history} />
</div>
) : null}
{(seeFairStatus || isOfficer) && fairs.length > 0 && (
Expand Down
8 changes: 4 additions & 4 deletions frontend/components/DropdownFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,17 @@ const TableContainer = styled.div`
}
`

const Chevron = styled(Icon)<{ open?: boolean }>`
export const Chevron = styled(Icon)<{ open?: boolean; color?: string }>`
cursor: pointer;
color: ${CLUBS_GREY};
color: ${({ color }) => color ?? CLUBS_GREY};
transform: rotate(0deg) translateY(0);
transition: transform ${ANIMATION_DURATION}ms ease;
${({ open }) => open && 'transform: rotate(180deg) translateY(-4px);'}
${({ open }) => open && 'transform: rotate(180deg);'}
${mediaMaxWidth(MD)} {
margin-top: 0.1em !important;
margin-left: 0.1em !important;
color: ${LIGHT_GRAY};
color: ${({ color }) => color ?? LIGHT_GRAY};
${({ open }) => open && 'transform: rotate(180deg)'}
}
`
Expand Down

0 comments on commit 835355d

Please sign in to comment.