File size: 9,539 Bytes
d3295cf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4f5fdec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d3295cf
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
"""Field definitions shared by the public pages, the submission form, the admin
editor and the Excel importer.

Every content field of a listing is described once here so labels, help text,
limits and vocabularies cannot drift apart between the form and the database.
"""
from __future__ import annotations

from dataclasses import dataclass
from typing import Optional

# Closed vocabularies. The first spelling is canonical; the rest are accepted
# variants (case-insensitive) that are normalised to the canonical spelling.
OPPORTUNITY_TYPES = {
    "Credit": ["credit", "academic credit"],
    "Volunteer": ["volunteer"],
    "Paid": ["paid"],
    "Federal Work Study": ["federal work study", "federal workstudy", "federal work-study"],
}
TERMS = {
    "Fall": ["fall"],
    "Spring": ["spring"],
    "Summer": ["summer"],
}
STANDINGS = {
    "Freshman": ["freshman", "freshmen"],
    "Sophomore": ["sophomore", "sophmore"],
    "Junior": ["junior"],
    "Senior": ["senior"],
}
# Areas of study that appear in the September 2026 CURAS workbook, with the
# misspellings found there mapped to the canonical spelling.
AREAS_OF_STUDY = {
    "All majors": ["all majors", "all majors welcome"],
    "American Studies": ["american studies"],
    "Art History": ["art history"],
    "Biochemistry": ["biochemistry", "biochemisty"],
    "Biology": ["biology"],
    "Biomedical Physics": ["biomedical physics"],
    "Business": ["business"],
    "Chemistry": ["chemistry"],
    "Classical and Near Eastern Civilizations": ["classical and near eastern civilizations"],
    "Classical Languages": ["classical languages"],
    "Communication Studies": ["communication studies"],
    "Computer Science": ["computer science"],
    "Cultural Anthropology": ["cultural anthropology"],
    "Data Science": ["data science"],
    "Economics": ["economics"],
    "Education": ["education"],
    "Environmental Science": ["environmental science", "enviromental science", "envirometnal science"],
    "Exercise Science": ["exercise science", "exerscise science"],
    "Fine and Performing Arts": ["fine and performing arts"],
    "Foreign Languages": ["foreign languages"],
    "French": ["french"],
    "Gender Studies": ["gender studies"],
    "German": ["german"],
    "Health Administration and Policy": [
        "health administration and policy",
        "health administration policy",
        "health administrationand policy",
    ],
    "History": ["history"],
    "International Business": ["international business"],
    "International Relations": ["international relations"],
    "Mathematics": ["mathematics"],
    "Medical Anthropology": ["medical anthropology", "medical anthrolpology"],
    "Neuroscience": ["neuroscience", "neursocience"],
    "Nursing": ["nursing"],
    "Philosophy": ["philosophy"],
    "Physics": ["physics"],
    "Political Science": ["political science"],
    "Pre-Health Professions": ["pre-health professions", "pre health professions"],
    "Psychology": ["psychology"],
    "Social Work": ["social work"],
    "Sociology": ["sociology"],
    "Spanish and Hispanic Studies": ["spanish and hispanic studies"],
    "Sustainability": ["sustainability"],
    "Theology": ["theology"],
}

# Lengths are generous upper bounds to stop abuse, not editorial limits.
SHORT = 300
MEDIUM = 2000
LONG = 8000


@dataclass(frozen=True)
class Field:
    key: str
    label: str
    kind: str                     # "text" | "textarea" | "url" | "choices"
    required: bool = False
    help: str = ""
    maxlen: int = MEDIUM
    choices: Optional[dict] = None   # for kind == "choices": canonical -> variants
    allow_other: bool = True         # for kind == "choices": free-text addition
    placeholder: str = ""


# Order here is the order on the submission form, the admin editor and the
# public "full details" view.
CONTENT_FIELDS: list[Field] = [
    Field("faculty_name", "Faculty name", "text", required=True, maxlen=SHORT,
          help="As it should appear publicly, for example Dr. Jane Smith."),
    Field("faculty_profile_url", "Faculty profile link", "url", maxlen=SHORT,
          help="Creighton campus directory page or another faculty profile page (https://...)."),
    Field("researchworks_url", "ResearchWorks profile link", "url", maxlen=SHORT,
          help="Link to your ResearchWorks profile and publications, or a lab website."),
    Field("keywords", "Keywords", "text", maxlen=SHORT,
          help="Separate keywords with commas."),
    Field("description", "Description", "textarea", required=True, maxlen=LONG,
          help="What the research is about and what students would do."),
    Field("methods", "Methods and techniques", "textarea", maxlen=LONG),
    Field("opportunity_type", "Type of opportunity", "choices", required=True,
          choices=OPPORTUNITY_TYPES, maxlen=SHORT),
    Field("term_availability", "Term availability", "choices", required=True,
          choices=TERMS, maxlen=SHORT),
    Field("commitment", "Commitment", "textarea", maxlen=MEDIUM,
          help="Expected hours per week and duration, for example 5-10 hours per week for two semesters."),
    Field("standing", "Standing (year)", "choices", required=True,
          choices=STANDINGS, maxlen=SHORT),
    Field("prerequisites", "Prerequisites", "textarea", maxlen=MEDIUM,
          help="Courses, skills or experience students should have. Write None if there are none."),
    Field("areas_of_study", "Areas of study", "choices", required=True,
          choices=AREAS_OF_STUDY, maxlen=MEDIUM,
          help="Majors or fields the opportunity suits."),
    Field("mentorship", "Mentorship", "textarea", maxlen=MEDIUM,
          help="How you work with students, for example meeting frequency and supervision style."),
    Field("previous_student_work", "Previous student work", "textarea", maxlen=LONG,
          help="Examples of past student projects, presentations or publications."),
    Field("faculty_note", "Faculty note", "textarea", maxlen=MEDIUM,
          help="Anything else students should know."),
    Field("application_deadline", "Application deadline", "text", required=True, maxlen=SHORT,
          help="Rolling, a date, or a note about current availability.", placeholder="Rolling"),
    Field("application_requirements", "Application requirements", "textarea", required=True,
          maxlen=MEDIUM, help="What students should send to express interest, for example an introductory email."),
]

FIELD_KEYS = [f.key for f in CONTENT_FIELDS]
FIELDS_BY_KEY = {f.key: f for f in CONTENT_FIELDS}

# Fields shown in the collapsed summary on the public page; the rest are in
# the expandable "Full details" section.
SUMMARY_KEYS = ["opportunity_type", "term_availability", "standing", "areas_of_study", "application_deadline"]

# Submitter fields (not part of the listing itself).
SUBMITTER_FIELDS: list[Field] = [
    Field("submitter_name", "Your name", "text", required=True, maxlen=SHORT),
    Field("submitter_netid", "Your NetID", "text", required=True, maxlen=64,
          help="Your Creighton NetID. Letters and digits only."),
    Field("submitter_email", "Your email", "text", maxlen=SHORT,
          help="Optional. Used only if CURAS has a question about your submission."),
]

LIST_SEPARATOR = ", "

# Shown on every listing: who may apply.
ELIGIBILITY_NOTE = "Creighton students only"

# Colour code for areas of study, shown as tags on the public page. Each key is
# a CSS class suffix defined in static/style.css (pastel background, dark text).
# Areas that often appear together on one listing are given different colours
# where possible; related fields share a hue.
AREA_COLORS = {
    "All majors": "all",
    "American Studies": "brown",
    "Art History": "pink",
    "Biochemistry": "red",
    "Biology": "green",
    "Biomedical Physics": "sky",
    "Business": "gold",
    "Chemistry": "teal",
    "Classical and Near Eastern Civilizations": "brown",
    "Classical Languages": "sky",
    "Communication Studies": "orange",
    "Computer Science": "indigo",
    "Cultural Anthropology": "teal",
    "Data Science": "lime",
    "Economics": "gold",
    "Education": "mint",
    "Environmental Science": "lime",
    "Exercise Science": "orange",
    "Fine and Performing Arts": "magenta",
    "Foreign Languages": "purple",
    "French": "purple",
    "Gender Studies": "magenta",
    "German": "purple",
    "Health Administration and Policy": "yellow",
    "History": "brown",
    "International Business": "sky",
    "International Relations": "teal",
    "Mathematics": "purple",
    "Medical Anthropology": "brown",
    "Neuroscience": "yellow",
    "Nursing": "magenta",
    "Philosophy": "magenta",
    "Physics": "blue",
    "Political Science": "red",
    "Pre-Health Professions": "mint",
    "Psychology": "pink",
    "Social Work": "gold",
    "Sociology": "brown",
    "Spanish and Hispanic Studies": "purple",
    "Sustainability": "mint",
    "Theology": "magenta",
}
OTHER_AREA_COLOR = "gray"


def area_color(item: Optional[str]) -> str:
    """CSS colour key for one area-of-study item (free text gets the neutral key)."""
    if not item:
        return OTHER_AREA_COLOR
    wanted = item.strip().lower()
    for name, key in AREA_COLORS.items():
        if name.lower() == wanted:
            return key
    return OTHER_AREA_COLOR


def split_list(value: Optional[str]) -> list[str]:
    """Split a stored comma-separated list field into items."""
    if not value:
        return []
    return [p.strip() for p in value.split(",") if p.strip()]