Skip to content

Commit

Permalink
Merge branch 'master' into feat/ownership-requests
Browse files Browse the repository at this point in the history
  • Loading branch information
gabeweng authored Nov 8, 2024
2 parents d5e4b6c + d121715 commit d94a5f4
Show file tree
Hide file tree
Showing 15 changed files with 327 additions and 397 deletions.
36 changes: 36 additions & 0 deletions backend/clubs/management/commands/osa_perms_updates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from django.core.management.base import BaseCommand

from clubs.models import Club


class Command(BaseCommand):
help = "Give superuser to hard-coded user accounts affiliated with OSA."
web_execute = True

def handle(self, *args, **kwargs):
User = get_user_model()
content_type = ContentType.objects.get_for_model(Club)
approve_perm = Permission.objects.get(
codename="approve_club", content_type=content_type
)
pending_perm = Permission.objects.get(
codename="see_pending_clubs", content_type=content_type
)
if not settings.OSA_KEYS:
raise ValueError("OSA_KEYS not set in settings")
if not (approvers := Group.objects.filter(name="Approvers").first()):
raise ValueError("Approvers group not found")
for key in settings.OSA_KEYS:
if not key or not (user := User.objects.get(username=key)):
continue
user.is_superuser = True
user.is_staff = True
user.user_permissions.add(approve_perm)
user.user_permissions.add(pending_perm)
approvers.user_set.add(user)
user.save()
approvers.save()
51 changes: 50 additions & 1 deletion backend/clubs/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6758,6 +6758,52 @@ def remove_clubs_from_exception(self, *args, **kwargs):
)
return Response([])

@action(detail=True, methods=["GET"])
def club_applications(self, *args, **kwargs):
"""
Retrieve club applications for given cycle
---
requestBody:
content: {}
responses:
"200":
content:
application/json:
schema:
type: array
items:
type: object
properties:
name:
type: string
id:
type: integer
application_end_time:
type: string
format: date-time
application_end_time_exception:
type: string
club__name:
type: string
club__code:
type: string
---
"""
cycle = self.get_object()

return Response(
ClubApplication.objects.filter(application_cycle=cycle)
.select_related("club")
.values(
"name",
"id",
"application_end_time",
"application_end_time_exception",
"club__name",
"club__code",
)
)

@action(detail=True, methods=["GET"])
def applications(self, *args, **kwargs):
"""
Expand All @@ -6766,7 +6812,10 @@ def applications(self, *args, **kwargs):
requestBody: {}
responses:
"200":
content: {}
content:
text/csv:
schema:
type: string
---
"""
cycle = self.get_object()
Expand Down
2 changes: 2 additions & 0 deletions backend/pennclubs/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,3 +265,5 @@

# Cybersource settings
CYBERSOURCE_CLIENT_VERSION = "0.15"

OSA_KEYS = None
2 changes: 2 additions & 0 deletions backend/pennclubs/settings/development.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,5 @@
"run_environment": "apitest.cybersource.com",
}
CYBERSOURCE_TARGET_ORIGIN = "https://localhost:3001"

OSA_KEYS = ["gwashington"]
2 changes: 2 additions & 0 deletions backend/pennclubs/settings/production.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,5 @@
"run_environment": "api.cybersource.com",
}
CYBERSOURCE_TARGET_ORIGIN = "https://pennclubs.com"

OSA_KEYS = os.getenv("OSA_KEYS", "").split(",")
28 changes: 28 additions & 0 deletions backend/tests/clubs/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -777,3 +777,31 @@ def test_graduate_users_output(self):
"Updated the membership status of 1 student club relationships!",
out.getvalue(),
)


class OsaPermsUpdatesTestCase(TestCase):
def setUp(self):
self.user1 = get_user_model().objects.create_user("gwashington")

def test_osa_perms_updates(self):
# Test error when OSA_KEYS is not set
with mock.patch("django.conf.settings.OSA_KEYS", None):
with self.assertRaises(ValueError):
call_command("osa_perms_updates")
self.assertFalse(self.user1.is_superuser)

with mock.patch("django.conf.settings.OSA_KEYS", ["gwashington"]):
# Test error when Approvers group is not found
with self.assertRaises(ValueError):
call_command("osa_perms_updates")
self.assertFalse(self.user1.is_superuser)

# Create Approvers group
Group.objects.create(name="Approvers")
call_command("osa_perms_updates")
self.user1.refresh_from_db()
self.assertTrue(self.user1.groups.filter(name="Approvers").exists())
self.assertTrue(self.user1.is_staff)
self.assertTrue(self.user1.is_superuser)
self.assertTrue(self.user1.has_perm("approve_club"))
self.assertTrue(self.user1.has_perm("see_pending_clubs"))
4 changes: 3 additions & 1 deletion frontend/components/ClubCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ const ClubCard = ({ club, fullWidth }: ClubCardProps): ReactElement => {
const { name, active, approved, subtitle, tags, enables_subscription, code } =
club
const img = club.image_url
const textDescription = shorten(subtitle || 'This club has no description.')
const textDescription = shorten(
subtitle || 'This club has not provided a mission statement.',
)

return (
<CardWrapper className={fullWidth ? '' : 'column is-half-desktop'}>
Expand Down
135 changes: 131 additions & 4 deletions frontend/components/ClubEditPage/ClubEditCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ import {
SITE_ID,
SITE_NAME,
} from '../../utils/branding'
import { ModalContent } from '../ClubPage/Actions'
import { LiveBanner, LiveSub, LiveTitle } from '../ClubPage/LiveEventsDialog'
import { Checkbox, CheckboxLabel, Contact, Text } from '../common'
import { Checkbox, CheckboxLabel, Contact, Modal, Text } from '../common'
import {
CheckboxField,
CheckboxTextField,
Expand Down Expand Up @@ -150,6 +151,48 @@ const Card = ({
</div>
)
}
interface EmailModalProps {
closeModal: () => void
email: string
setEmail: (inp: string) => void
confirmSubmission: () => void
}

const EmailModal = ({
closeModal,
email,
setEmail,
confirmSubmission,
}: EmailModalProps): ReactElement => {
return (
<Modal
width={'450px'}
show={true}
closeModal={closeModal}
marginBottom={false}
>
<div className="card-content" style={{ marginBottom: '24px' }}>
<Text>
Warning: This email will be shown to the public. We highly recommend
you don't use a personal email, and instead use a club email. Feel
free to ignore this warning if the email is not a personal email.
</Text>
<Field
name="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="input mb-4"
style={{ maxWidth: '350px' }}
></Field>
<div>
<button onClick={confirmSubmission} className="button is-primary">
Confirm
</button>
</div>
</div>
</Modal>
)
}

/**
* Remove fields in an object that are not part of a whitelist.
Expand Down Expand Up @@ -189,6 +232,7 @@ export default function ClubEditCard({
isEdit,
onSubmit = () => Promise.resolve(undefined),
}: ClubEditCardProps): ReactElement {
const [showRankModal, setShowRankModal] = useState<boolean>(false)
const [showTargetFields, setShowTargetFields] = useState<boolean>(
!!(
club.target_majors?.length ||
Expand Down Expand Up @@ -227,6 +271,8 @@ export default function ClubEditCard({
),
)

const [emailModal, showEmailModal] = useState<boolean>(false)

const submit = (data, { setSubmitting, setStatus }): Promise<void> => {
const photo = data.image
if (photo !== null) {
Expand Down Expand Up @@ -397,6 +443,58 @@ export default function ClubEditCard({
{
name: 'General',
type: 'group',
description: (
<div className="mb-4">
<a onClick={() => setShowRankModal(true)}>
How does filling out this information affect your club?
</a>
<Modal
show={showRankModal}
closeModal={() => setShowRankModal(false)}
marginBottom={false}
width="80%"
>
<ModalContent className="content mb-4">
<h2>How we calculate club rankings</h2>
<hr />
<h5>
The following positively affects your club's ranking in homepage
search results:
</h5>
<ul>
<li>
Upcoming events with filled out name, description, and image
</li>
<li>Upcoming, open applications for membership</li>
<li>
Having at least 3 active officers, plus a bonus for any
additional non-officer member on the platform
</li>
<li>
Having between 3 and 7 useful tags (please email <Contact />{' '}
if none apply)
</li>
<li>
Posting a public (non-personal) contact email and 2 or more
social links
</li>
<li>
Having a club logo image uploaded and subtitle filled out
</li>
<li>
Filling out a club mission with images and detail (rewarded up
to 1000 words)
</li>
<li>Displaying 3 or more student testimonials (experiences)</li>
<li>Filling out the {FIELD_PARTICIPATION_LABEL} section</li>
<li>
Updating the club listing recently (within the last 8 months)
</li>
</ul>
</ModalContent>
</Modal>
</div>
),
fields: [
{
name: 'name',
Expand Down Expand Up @@ -446,8 +544,9 @@ export default function ClubEditCard({
},
{
name: 'description',
label: 'Club Mission',
required: true,
placeholder: `Type your ${OBJECT_NAME_SINGULAR} description here!`,
placeholder: `Type your ${OBJECT_NAME_SINGULAR} mission here!`,
type: 'html',
hidden: !REAPPROVAL_QUEUE_ENABLED,
},
Expand Down Expand Up @@ -795,6 +894,7 @@ export default function ClubEditCard({

const creationDefaults = {
subtitle: '',
email: '',
email_public: true,
accepting_members: false,
size: CLUB_SIZES[0].value,
Expand All @@ -816,9 +916,36 @@ export default function ClubEditCard({
: creationDefaults

return (
<Formik initialValues={initialValues} onSubmit={submit} enableReinitialize>
{({ dirty, isSubmitting }) => (
<Formik
initialValues={initialValues}
onSubmit={(values, actions) =>
submit({ ...values, emailOverride: false }, actions)
}
enableReinitialize
validate={(values) => {
const errors: { email?: string } = {}
if (values.email.includes('upenn.edu') && !emailModal) {
showEmailModal(true)
errors.email = 'Please confirm your email'
}
return errors
}}
validateOnChange={false}
validateOnBlur={false}
>
{({ dirty, isSubmitting, setFieldValue, submitForm, values }) => (
<Form>
{emailModal && (
<EmailModal
closeModal={() => showEmailModal(false)}
email={values.email}
setEmail={(newEmail) => setFieldValue('email', newEmail)}
confirmSubmission={() => {
showEmailModal(false)
submitForm()
}}
/>
)}
{!REAPPROVAL_QUEUE_ENABLED && (
<LiveBanner>
<LiveTitle>Queue Closed for Summer Break</LiveTitle>
Expand Down
2 changes: 1 addition & 1 deletion frontend/components/ClubPage/Description.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type Props = {
const Description = ({ club }: Props): ReactElement => (
<Wrapper>
<div style={{ width: '100%' }}>
<StrongText>Description</StrongText>
<StrongText>Club Mission</StrongText>
<div
className="content"
dangerouslySetInnerHTML={{
Expand Down
5 changes: 2 additions & 3 deletions frontend/components/EmbedOption.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,8 @@ const EmbedOption = (props: Props): ReactElement => {
<h1>Embed Content</h1>
<p>
You can use this tool to embed multimedia content into your club
description. If you run into any issues using the tool, please
contact <Contact />. Here are examples of some of the things you can
embed.
mission. If you run into any issues using the tool, please contact{' '}
<Contact />. Here are examples of some of the things you can embed.
</p>
<div className="content mb-3">
<ul>
Expand Down
Loading

0 comments on commit d94a5f4

Please sign in to comment.