-
Notifications
You must be signed in to change notification settings - Fork 7
Mandate Constitution Upload #832
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| # Generated by Django 5.2.4 on 2025-08-06 06:27 | ||
|
|
||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ("clubs", "0124_rankingweights"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name="asset", | ||
| name="is_constitution", | ||
| field=models.BooleanField(default=False), | ||
| ), | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -61,6 +61,18 @@ import { | |
| } from '../FormComponents' | ||
| import { doFormikInitialValueFixes } from '../ModelForm' | ||
|
|
||
| function isConstitutionRequired( | ||
| club: Club | Partial<Club>, | ||
| isEdit: boolean = false, | ||
| ): boolean { | ||
| // if on edit page and club already has constitution, it's not required | ||
| if (isEdit && club.has_constitution) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this if branch implements separate behavior on |
||
| return false | ||
| } | ||
|
|
||
| return !club.has_constitution | ||
| } | ||
|
|
||
| export const CLUB_APPLICATIONS = [ | ||
| { | ||
| value: ClubApplicationRequired.Open, | ||
|
|
@@ -289,6 +301,11 @@ export default function ClubEditCard({ | |
| delete data.image | ||
| } | ||
|
|
||
| const constitution = data.constitution | ||
| if (constitution !== null) { | ||
| delete data.constitution | ||
| } | ||
|
|
||
| const entries = Object.entries(data) | ||
| let body = {} | ||
|
|
||
|
|
@@ -433,6 +450,25 @@ export default function ClubEditCard({ | |
| msg += ` However, failed to upload ${OBJECT_NAME_SINGULAR} image file!` | ||
| } | ||
| } | ||
|
|
||
| if (constitution && constitution instanceof File) { | ||
| const formData = new FormData() | ||
| formData.append('file', constitution) | ||
| formData.append('is_constitution', 'true') | ||
| const resp = await doApiRequest( | ||
| `/clubs/${clubCode}/upload_file/?format=json`, | ||
| { | ||
| method: 'POST', | ||
| body: formData, | ||
| }, | ||
| ) | ||
| if (resp.ok) { | ||
| msg += ` ${OBJECT_NAME_TITLE_SINGULAR} constitution also saved.` | ||
| } else { | ||
| msg += ` However, failed to upload ${OBJECT_NAME_SINGULAR} constitution file!` | ||
| } | ||
| } | ||
|
|
||
| await onSubmit({ isEdit: true, club: info, message: msg }) | ||
| setStatus({}) | ||
| setSubmitting(false) | ||
|
|
@@ -586,6 +622,14 @@ export default function ClubEditCard({ | |
| label: `${OBJECT_NAME_TITLE_SINGULAR} Logo`, | ||
| disabled: !REAPPROVAL_QUEUE_ENABLED, | ||
| }, | ||
| { | ||
| name: 'constitution', | ||
| help: `Upload your ${OBJECT_NAME_SINGULAR} constitution. This is required for all clubs. Please upload a PDF, DOC, or DOCX file.`, | ||
| accept: '.pdf,.doc,.docx', | ||
| type: 'file', | ||
| label: `${OBJECT_NAME_TITLE_SINGULAR} Constitution`, | ||
| required: isConstitutionRequired(club, isEdit), | ||
| }, | ||
| { | ||
| name: 'size', | ||
| type: 'select', | ||
|
|
@@ -971,8 +1015,8 @@ export default function ClubEditCard({ | |
| submit({ ...values, emailOverride: false }, actions) | ||
| } | ||
| enableReinitialize | ||
| validate={(values) => { | ||
| const errors: { email?: string } = {} | ||
| validate={(values: any) => { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you sure there's no expected schema for |
||
| const errors: { email?: string; constitution?: string } = {} | ||
| if (values.email.includes('upenn.edu') && !emailModal) { | ||
| showEmailModal(true) | ||
| errors.email = 'Please confirm your email' | ||
|
|
@@ -1058,6 +1102,7 @@ export default function ClubEditCard({ | |
| multiselect: SelectField, | ||
| select: SelectField, | ||
| image: FileField, | ||
| file: FileField, | ||
| address: FormikAddressField, | ||
| checkboxText: CheckboxTextField, | ||
| creatableMultiSelect: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| import { Field, Form, Formik } from 'formik' | ||
| import { ReactElement, useState } from 'react' | ||
| import { ReactElement, useEffect, useState } from 'react' | ||
| import TimeAgo from 'react-timeago' | ||
|
|
||
| import { Club, File } from '../../types' | ||
|
|
@@ -15,23 +15,49 @@ import BaseCard from './BaseCard' | |
|
|
||
| type FilesCardProps = { | ||
| club: Club | ||
| refreshTrigger?: number | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this prop ever being incremented on file upload? We may also want to explore more idiomatic ways of forcing refresh for this card vs. a meaningless number. |
||
| } | ||
|
|
||
| /** | ||
| * A card that allows club officers to view, download, delete, and add files to the club. | ||
| */ | ||
| export default function FilesCard({ club }: FilesCardProps): ReactElement<any> { | ||
| export default function FilesCard({ | ||
| club, | ||
| refreshTrigger, | ||
| }: FilesCardProps): ReactElement<any> { | ||
| const [files, setFiles] = useState<File[]>(club.files) | ||
| const [clubHasConstitution, setClubHasConstitution] = useState<boolean>( | ||
| club.has_constitution, | ||
| ) | ||
|
|
||
| const reloadFiles = async (): Promise<void> => { | ||
| await doApiRequest(`/clubs/${club.code}/assets/?format=json`) | ||
| .then((resp) => resp.json()) | ||
| .then(setFiles) | ||
|
|
||
| // use clublist serializer (minimal data) | ||
| await doApiRequest(`/clubs/${club.code}/?format=json`) | ||
| .then((resp) => resp.json()) | ||
| .then((data) => { | ||
| setClubHasConstitution(data.has_constitution) | ||
| }) | ||
| } | ||
|
|
||
| // trigger reload from ClubEditCard submissions | ||
| useEffect(() => { | ||
| if (refreshTrigger && refreshTrigger > 0) { | ||
| reloadFiles() | ||
| } | ||
| }, [refreshTrigger]) | ||
|
|
||
| const submitForm = (data, { setSubmitting, resetForm, setStatus }) => { | ||
| const formData = new FormData() | ||
| formData.append('file', data.file) | ||
|
|
||
| // if filename contains "constitution" then file must be constitution | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just confirming that this is fallback logic for file uploads not done within our new upload constitution UI vs a check for files that are already being uploaded through the new UI meant for uploading constitutions. |
||
| const isConstitution = data.file.name.toLowerCase().includes('constitution') | ||
| formData.append('is_constitution', isConstitution.toString()) | ||
|
|
||
| doApiRequest(`/clubs/${club.code}/upload_file/?format=json`, { | ||
| method: 'POST', | ||
| body: formData, | ||
|
|
@@ -56,6 +82,28 @@ export default function FilesCard({ club }: FilesCardProps): ReactElement<any> { | |
| {OBJECT_NAME_SINGULAR} members and {SITE_NAME} administrators.{' '} | ||
| {OBJECT_TAB_FILES_DESCRIPTION} | ||
| </Text> | ||
|
|
||
| {/* constitution requirement message */} | ||
| {!clubHasConstitution && ( | ||
| <div className="notification is-warning is-light mb-4"> | ||
| <div className="content"> | ||
| <p className="has-text-weight-bold"> | ||
| <Icon name="alert-triangle" /> Constitution Upload Required | ||
| </p> | ||
| <p> | ||
| Your {OBJECT_NAME_SINGULAR} is required to upload a constitution. | ||
| You can upload your constitution using the form below or through | ||
| the club edit form. | ||
| </p> | ||
| <p className="has-text-weight-semibold"> | ||
| Important: If you choose to upload your constitution from this | ||
| tab, please ensure the file name contains the word "constitution" | ||
| to automatically mark it as a constitution file. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Refer to the above comment but I think this process is too confusing / ambiguous and if we can't avoid this, we should just have a single unambiguous place where uploading any valid word or pdf doc would idempotently update a club's constitution |
||
| </p> | ||
| </div> | ||
| </div> | ||
| )} | ||
|
|
||
| <table className="table is-fullwidth"> | ||
| <thead> | ||
| <tr> | ||
|
|
@@ -68,7 +116,14 @@ export default function FilesCard({ club }: FilesCardProps): ReactElement<any> { | |
| {files && files.length ? ( | ||
| files.map((a) => ( | ||
| <tr key={`${a.id}-${a.name}`}> | ||
| <td>{a.name}</td> | ||
| <td> | ||
| {a.name} | ||
| {a.is_constitution && ( | ||
| <span className="tag is-info is-small ml-2"> | ||
| Constitution | ||
| </span> | ||
| )} | ||
| </td> | ||
| <td> | ||
| <TimeAgo date={a.created_at} /> | ||
| </td> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we invalidate this cache on file upload or is this implicit in the Django decorator?