Skip to content

Commit 13a1ff9

Browse files
committed
fetch_clubs; clubs.csv; specific club categories defined; load_clubs django command; updated club view with image and application required
1 parent 1f17e1e commit 13a1ff9

File tree

6 files changed

+1433
-9
lines changed

6 files changed

+1433
-9
lines changed

tcf_website/management/commands/club_data/csv/clubs.csv

Lines changed: 1166 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import csv
2+
import os
3+
import re
4+
import backoff
5+
import requests
6+
from tqdm import tqdm
7+
8+
BASE_URL = "https://api.presence.io/virginia/v1/organizations"
9+
session = requests.session()
10+
TIMEOUT = 300
11+
12+
# ── fine-grained → broad buckets ────────────────────────────────────────────
13+
SQUASH_MAP = {
14+
# Greek Life
15+
"Fraternity or Sorority": "Greek Life",
16+
"Inter-Fraternity Council (IFC)": "Greek Life",
17+
"Inter-Sorority Council (ISC)": "Greek Life",
18+
"Multicultural Greek Council (MGC)": "Greek Life",
19+
"National Pan-Hellenic Council (NPHC)": "Greek Life",
20+
# Identity & Culture
21+
"Cultural & Ethnic": "Identity & Culture",
22+
"Black Presidents Council": "Identity & Culture",
23+
"L2K": "Identity & Culture",
24+
"International": "Identity & Culture",
25+
"FGLI": "Identity & Culture",
26+
# Academic & Professional
27+
"Academic & Professional": "Academic & Professional",
28+
"Commerce/Business": "Academic & Professional",
29+
"Data Sciences": "Academic & Professional",
30+
"Leadership Development": "Academic & Professional",
31+
"Internships and Employment": "Academic & Professional",
32+
"Honor Society": "Academic & Professional",
33+
# Arts & Media
34+
"Visual & Performing Arts": "Arts & Media",
35+
"Media & Publications": "Arts & Media",
36+
"Acapella Groups": "Arts & Media",
37+
# Recreation & Social
38+
"Social & Hobby": "Recreation & Social",
39+
"Club Sport": "Recreation & Social",
40+
# Service & Advocacy
41+
"Public Service": "Service & Advocacy",
42+
"Political & Advocacy": "Service & Advocacy",
43+
"Peer Mentors": "Service & Advocacy",
44+
"Health and Wellness": "Service & Advocacy",
45+
"Sustainability": "Service & Advocacy",
46+
"Special Status Organization (SSO)": "Service & Advocacy",
47+
"Honors and Awards": "Service & Advocacy",
48+
# Campus Units
49+
"Administrative Unit": "Campus Units",
50+
"Department, Schools, or Centers": "Campus Units",
51+
"Department Group or Program": "Campus Units",
52+
"Residence Hall": "Campus Units",
53+
"Agency": "Campus Units",
54+
"Northern Virginia": "Campus Units",
55+
# Professional Schools
56+
"Darden School": "Professional Schools",
57+
"Law School": "Professional Schools",
58+
}
59+
60+
# ── fixed pick order ────────────────────────────────────────────────────────
61+
BUCKET_PRIORITY = [
62+
"Greek Life",
63+
"Identity & Culture",
64+
"Service & Advocacy",
65+
"Professional Schools",
66+
"Academic & Professional",
67+
"Arts & Media",
68+
"Recreation & Social",
69+
"Campus Units",
70+
]
71+
72+
73+
def strip_html_tags(text):
74+
return re.sub(r"<.*?>", "", text)
75+
76+
77+
@backoff.on_exception(
78+
backoff.expo,
79+
(requests.exceptions.Timeout, requests.exceptions.ConnectionError),
80+
max_tries=5,
81+
)
82+
def fetch_org_list():
83+
resp = session.get(BASE_URL, timeout=TIMEOUT)
84+
resp.raise_for_status()
85+
return resp.json()
86+
87+
88+
@backoff.on_exception(
89+
backoff.expo,
90+
(requests.exceptions.Timeout, requests.exceptions.ConnectionError),
91+
max_tries=5,
92+
)
93+
def fetch_org_details(uri):
94+
resp = session.get(f"{BASE_URL}/{uri}", timeout=TIMEOUT)
95+
resp.raise_for_status()
96+
return resp.json()
97+
98+
99+
def write_csv(csv_file):
100+
orgs = fetch_org_list()
101+
os.makedirs(os.path.dirname(csv_file), exist_ok=True)
102+
103+
with open(csv_file, "w", newline="", encoding="utf-8") as f:
104+
w = csv.writer(f)
105+
# Note new column "OriginalCategories"
106+
w.writerow(
107+
[
108+
"Name",
109+
"Category",
110+
"OriginalCategories",
111+
"Application",
112+
"Description",
113+
"Photo",
114+
]
115+
)
116+
117+
for org in tqdm(orgs, desc="Processing clubs"):
118+
uri = org.get("uri")
119+
if not uri:
120+
continue
121+
try:
122+
d = fetch_org_details(uri)
123+
name = d.get("name", "")
124+
raw_cats = d.get("categories", [])
125+
126+
# store the originals joined by $
127+
original_str = "$".join(raw_cats)
128+
129+
# Application flag
130+
application = "No Application or Interview Required" not in raw_cats
131+
132+
# Squash + dedupe, drop the no-app tag
133+
broad = {
134+
SQUASH_MAP.get(c, c)
135+
for c in raw_cats
136+
if c != "No Application or Interview Required"
137+
}
138+
139+
# 1) pick by fixed bucket priority
140+
main_cat = ""
141+
for bucket in BUCKET_PRIORITY:
142+
if bucket in broad:
143+
main_cat = bucket
144+
break
145+
146+
# 2) fallback
147+
if not main_cat:
148+
main_cat = "Miscellaneous"
149+
150+
desc = strip_html_tags(d.get("description", ""))
151+
photo = os.path.basename(d.get("photoUri", ""))
152+
153+
w.writerow([name, main_cat, original_str, application, desc, photo])
154+
155+
except Exception as e:
156+
print(f"Error {uri}: {e}")
157+
continue
158+
159+
print("Done.")
160+
161+
162+
if __name__ == "__main__":
163+
write_csv("club_data/csv/clubs.csv")
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import csv
2+
import os
3+
4+
from django.core.management.base import BaseCommand
5+
from django.utils.text import slugify
6+
from tqdm import tqdm
7+
8+
from tcf_website.models import Club, ClubCategory
9+
10+
11+
class Command(BaseCommand):
12+
help = "Imports club data from clubs.csv into the database"
13+
14+
def handle(self, *args, **options):
15+
self.stdout.write("Loading club data...")
16+
17+
csv_file = "tcf_website/management/commands/club_data/csv/clubs.csv"
18+
photo_base_url = "https://virginia-cdn.presence.io/organization-photos/cea28f2b-baa9-4c47-8879-da8d675e4471/"
19+
20+
# Dictionary to keep track of created categories
21+
categories = {}
22+
23+
# Clear existing clubs for a clean import
24+
Club.objects.all().delete()
25+
26+
with open(csv_file, "r", encoding="utf-8") as f:
27+
reader = csv.reader(f)
28+
# Skip header row
29+
next(reader)
30+
31+
for row in tqdm(reader, desc="Importing clubs"):
32+
if len(row) < 6:
33+
self.stdout.write(
34+
self.style.WARNING(f"Skipping row: {row} - insufficient data")
35+
)
36+
continue
37+
38+
name = row[0]
39+
category_name = row[1]
40+
# Skip original_categories (row[2])
41+
application_required = row[3].lower() == "true"
42+
description = row[4]
43+
photo = row[5]
44+
45+
# Get or create the category
46+
if category_name in categories:
47+
category = categories[category_name]
48+
else:
49+
slug = slugify(category_name).upper()[:4]
50+
category, created = ClubCategory.objects.get_or_create(
51+
name=category_name, defaults={"slug": slug}
52+
)
53+
categories[category_name] = category
54+
if created:
55+
self.stdout.write(f"Created category: {category_name}")
56+
57+
# Create photo URL
58+
photo_url = ""
59+
if photo:
60+
photo_url = f"{photo_base_url}{photo}"
61+
62+
# Create the club
63+
club = Club.objects.create(
64+
name=name,
65+
category=category,
66+
application_required=application_required,
67+
description=description,
68+
photo_url=photo_url,
69+
)
70+
71+
self.stdout.write(
72+
self.style.SUCCESS(f"Successfully imported {Club.objects.count()} clubs")
73+
)

tcf_website/migrations/0021_clubcategory_alter_review_course_and_more.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Generated by Django 4.2.20 on 2025-04-27 16:14
1+
# Generated by Django 4.2.18 on 2025-05-02 18:24
22

33
import django.contrib.postgres.indexes
44
from django.db import migrations, models
@@ -67,6 +67,8 @@ class Migration(migrations.Migration):
6767
"combined_name",
6868
models.CharField(blank=True, editable=False, max_length=255),
6969
),
70+
("application_required", models.BooleanField(default=False)),
71+
("photo_url", models.CharField(blank=True, max_length=255)),
7072
(
7173
"category",
7274
models.ForeignKey(

tcf_website/models/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,8 @@ class Club(models.Model):
227227
description = models.TextField(blank=True)
228228
category = models.ForeignKey(ClubCategory, on_delete=models.CASCADE)
229229
combined_name = models.CharField(max_length=255, blank=True, editable=False)
230+
application_required = models.BooleanField(default=False)
231+
photo_url = models.CharField(max_length=255, blank=True)
230232

231233
def save(self, *args, **kwargs):
232234
# maintain combined_name for trigram search

tcf_website/templates/club/club.html

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,36 @@ <h4>{{ club.category.name }}</h4>
2424
</div>
2525

2626
<!-- Club Description -->
27-
{% if club.description %}
2827
<div class="card">
2928
<div class="card-body">
30-
<h4 class="card-title">
31-
Club Description
32-
</h4>
33-
<p class="card-text">
34-
{{ club.description }}
35-
</p>
29+
<div class="d-flex flex-column flex-md-row">
30+
<div class="flex-grow-1 mr-md-4">
31+
<h4 class="card-title">
32+
Club Description
33+
</h4>
34+
<p class="card-text">
35+
{{ club.description }}
36+
</p>
37+
38+
<div class="mt-3">
39+
<span class="badge {% if club.application_required %}badge-warning{% else %}badge-success{% endif %} p-2">
40+
{% if club.application_required %}
41+
Application Required
42+
{% else %}
43+
No Application Required
44+
{% endif %}
45+
</span>
46+
</div>
47+
</div>
48+
49+
{% if club.photo_url %}
50+
<div class="text-center mt-3 mt-md-0" style="min-width: 200px;">
51+
<img src="{{ club.photo_url }}" alt="{{ club.name }}" class="img-fluid" style="max-height: 200px;">
52+
</div>
53+
{% endif %}
54+
</div>
3655
</div>
3756
</div>
38-
{% endif %}
3957

4058
<br/>
4159
</div>

0 commit comments

Comments
 (0)