"""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()]