"""
token = _get_token()
repo_id = _get_repo_id()
if not token:
return "❌ Commit skipped: HF_TOKEN missing (add it in Settings → Repository secrets)."
if not repo_id:
return "❌ Commit skipped: No SPACE_ID. Add REPO_ID secret manually."
from huggingface_hub import HfApi
api = HfApi(token=token)
try:
api.upload_folder(
folder_path=local_folder_path,
repo_id=repo_id,
repo_type="space",
path_in_repo=f"submissions/{folder_name}",
commit_message=f"Add submission: {folder_name}"
)
return "✅ Committed submission to the Space repository."
except Exception as e:
return f"❌ Commit failed: {e}"
# ============================
# NAME SANITIZER + BUILDER
# ============================
def sanitize(text: str) -> str:
"""Make safe for folder names while preserving readability."""
if not text:
return ""
text = text.strip()
# Collapse whitespace to a single underscore
text = re.sub(r"\s+", "_", text)
# Keep letters, numbers, underscore, dash; remove the rest
text = re.sub(r"[^A-Za-z0-9_-]", "", text)
# Avoid accidental leading/trailing underscores
return text.strip("_")
def build_folder_name(unit, weed, crop, year, sender, org):
"""
Folder pattern (unchanged in intent):
UNIT_WEED_[CROP]_YEAR_SENDER_ORG_timestamp_uuid8
"""
parts = [sanitize(unit), sanitize(weed)]
crop_s = sanitize(crop)
if crop_s:
parts.append(crop_s)
parts.extend([sanitize(year), sanitize(sender), sanitize(org)])
base = "_".join([p for p in parts if p])
timestamp = datetime.utcnow().strftime("%Y%m%d-%H%M%S")
short_uuid = str(uuid.uuid4())[:8]
return f"{base}_{timestamp}_{short_uuid}"
# ============================
# SUBMISSION FUNCTION
# ============================
def analyze_outliers(file_path):
"""
Light non-blocking outlier detection for curator guidance.
Tries to read CSV/XLSX files and checks:
- Latitude outside [-90, 90]
- Longitude outside [-180, 180]
- Dates not in YYYY-MM-DD
- Negative numeric values where suspicious
Returns a list of warning strings.
"""
warnings = []
# Try to read the file with pandas
try:
import pandas as pd
if file_path.lower().endswith(".csv"):
df = pd.read_csv(file_path)
elif file_path.lower().endswith(".xlsx") or file_path.lower().endswith(".xls"):
df = pd.read_excel(file_path)
elif file_path.lower().endswith(".tsv"):
df = pd.read_csv(file_path, sep="\t")
else:
warnings.append("⚠️ Could not parse file for outlier detection (unsupported format).")
return warnings
# ---- Basic checks ----
# Latitude
if "Latitude" in df.columns:
bad_lat = df[(df["Latitude"] < -90) | (df["Latitude"] > 90)]
for idx, val in bad_lat["Latitude"].items():
warnings.append(f"Row {idx}: Latitude={val} is outside [-90, 90].")
# Longitude
if "Longitude" in df.columns:
bad_lon = df[(df["Longitude"] < -180) | (df["Longitude"] > 180)]
for idx, val in bad_lon["Longitude"].items():
warnings.append(f"Row {idx}: Longitude={val} is outside [-180, 180].")
# Date
if "Date" in df.columns:
for idx, val in df["Date"].items():
try:
datetime.strptime(str(val), "%Y-%m-%d")
except:
warnings.append(f"Row {idx}: Date='{val}' is not YYYY-MM-DD.")
# Value numeric sanity (optional)
if "Value" in df.columns:
numeric = pd.to_numeric(df["Value"], errors="coerce")
for idx, val in numeric.items():
if pd.isna(val):
continue
if val < 0:
warnings.append(f"Row {idx}: Value={val} is negative (check if expected).")
if val > 1e6:
warnings.append(f"Row {idx}: Value={val} is extremely high (possible error).")
except Exception as e:
warnings.append(f"⚠️ Outlier detection crashed: {e}")
return warnings
def submit_data(
sender,
org,
unit,
weed,
crop,
crop_establishment,
tillage_system,
trial_type,
publication_status,
year,
doi,
extra,
uploaded_file,
):
# 1. Ensure repo has root folder "submissions"
print(ensure_submissions_folder_in_repo())
# 2. Validate
errors = []
if not sender:
errors.append("• Sender Name is required.")
if not unit:
errors.append("• Unit is required.")
if not weed:
errors.append("• WEED is required.")
if not year or not re.fullmatch(r"\d{4}", year):
errors.append("• Year must be YYYY.")
if not uploaded_file:
errors.append("• You must upload a file.")
if errors:
return "❌ Submission not saved:\n" + "\n".join(errors)
# 3. Build local path
local_root = "submissions"
os.makedirs(local_root, exist_ok=True)
folder_name = build_folder_name(unit, weed, crop, year, sender, org)
submission_dir = os.path.join(local_root, folder_name)
os.makedirs(submission_dir, exist_ok=True)
# 4. Save uploaded file
ext = os.path.splitext(uploaded_file.name)[1]
file_dest = os.path.join(submission_dir, f"uploaded_file{ext}")
shutil.copy(uploaded_file.name, file_dest)
# 5. Save metadata
metadata_path = os.path.join(submission_dir, "metadata.txt")
with open(metadata_path, "w", encoding="utf-8") as f:
f.write(f"Sender: {sender}\n")
f.write(f"Organization: {org}\n")
f.write(f"Unit: {unit}\n")
f.write(f"Weed: {weed}\n")
f.write(f"Crop: {crop}\n")
f.write(f"Crop establishment method: {crop_establishment}\n")
f.write(f"Tillage system: {tillage_system}\n")
f.write(f"Trial type: {trial_type}\n")
f.write(f"Publication status: {publication_status}\n")
f.write(f"Year: {year}\n")
f.write(f"DOI: {doi}\n")
f.write(f"Extra: {extra}\n")
# 5b. New: Run outlier detection + save warnings in folder
warnings = analyze_outliers(file_dest)
warnings_path = os.path.join(submission_dir, "warnings.txt")
with open(warnings_path, "w", encoding="utf-8") as wf:
if warnings:
wf.write("⚠️ Potential Issues Found:\n\n")
for w in warnings:
wf.write("• " + w + "\n")
else:
wf.write("No warnings. All basic checks passed.\n")
# 6. Commit to repo
commit_msg = commit_submission_folder(submission_dir, folder_name)
# 7. Return result
return (
"### ✅ Submission successful!\n"
f"**Saved locally:** `{submission_dir}`\n\n"
f"**Repository status:** {commit_msg}\n\n"
"You can view the uploaded file in **Files & Versions → submissions/**.\n"
"A `warnings.txt` file has been generated to help curators identify potential issues.\n"
)
# ============================
# GRADIO INTERFACE
# ============================
theme = gr.themes.Soft(
primary_hue="green",
secondary_hue="green",
neutral_hue="slate",
).set(
body_text_color="#162016",
body_text_color_dark="#162016",
body_text_color_subdued="rgba(22, 32, 22, 0.72)",
body_text_color_subdued_dark="rgba(22, 32, 22, 0.72)",
body_background_fill="transparent",
block_background_fill="rgba(255,255,255,0.8)",
block_border_color="rgba(47, 93, 59, 0.08)",
block_radius="24px",
button_large_radius="999px",
input_background_fill="rgba(255,255,255,0.9)",
input_background_fill_dark="rgba(255,255,255,0.9)",
input_border_color="rgba(47, 93, 59, 0.1)",
input_border_color_dark="rgba(47, 93, 59, 0.1)",
input_placeholder_color="rgba(22, 32, 22, 0.72)",
input_placeholder_color_dark="rgba(22, 32, 22, 0.72)",
)
with gr.Blocks(title=APP_TITLE) as demo:
gr.HTML(
"""
Contribute to OPheno
Submission Portal
A collaborative space to contribute weed emergence and phenology
observations to the OPheno project.
At the heart of OPheno is collaboration.
This portal helps researchers, practitioners, and collaborators submit datasets
in a structured way so maintainers can review, curate, and integrate them into
the broader OPheno knowledge base.
Provide Clear Attribution
Include your name and organization so contributions can be credited appropriately.
Use Structured Data
Upload CSV, Excel, TSV, JSON, or ZIP files that contain the relevant observations.
Help Curators Review
Add notes about methods, units, and context so your dataset can be interpreted correctly.
"""
)
with gr.Column(elem_id="submission-form", elem_classes=["form-panel"]):
gr.HTML(
"""
Submission Form
Share observations with the OPheno project.
Complete the form below to submit your file and supporting metadata. The underlying
validation, upload, and repository commit workflow remain unchanged.
"""
)
gr.HTML(
"""
Authors (if there are several authors, separate names with commas starting with the contributor name)
Organization (optional)
Unit / Type of value (e.g. Counted_weeds, weeds per square meter)
WEED (single or multiple; split with ; when more than one, e.g. ECHCG;AMARE)
Crop (optional; e.g. ZEAMA)
Crop establishment method (if a crop is present; e.g. direct seeding, transplanting)
Tillage system (e.g. conventional tillage, conservation tillage, no-till)
Trial type (e.g. field trial, pot trial, semi-field trial)
Publication status (e.g. published, under review, not published)
Year (YYYY)
DOI (if the data is coming from a paper that has an available DOI, please add it)
Extra Information (management practices, important details, specific characteristics, etc)
"""
)
sender = gr.Textbox(show_label=False, container=False, elem_id="metadata-sender", elem_classes="metadata-sync-field")
org = gr.Textbox(show_label=False, container=False, elem_id="metadata-org", elem_classes="metadata-sync-field")
unit = gr.Textbox(show_label=False, container=False, elem_id="metadata-unit", elem_classes="metadata-sync-field")
weed = gr.Textbox(show_label=False, container=False, elem_id="metadata-weed", elem_classes="metadata-sync-field")
crop = gr.Textbox(show_label=False, container=False, elem_id="metadata-crop", elem_classes="metadata-sync-field")
crop_establishment = gr.Textbox(show_label=False, container=False, elem_id="metadata-crop-establishment", elem_classes="metadata-sync-field")
tillage_system = gr.Textbox(show_label=False, container=False, elem_id="metadata-tillage-system", elem_classes="metadata-sync-field")
trial_type = gr.Textbox(show_label=False, container=False, elem_id="metadata-trial-type", elem_classes="metadata-sync-field")
publication_status = gr.Textbox(show_label=False, container=False, elem_id="metadata-publication-status", elem_classes="metadata-sync-field")
year = gr.Textbox(show_label=False, container=False, elem_id="metadata-year", elem_classes="metadata-sync-field")
doi = gr.Textbox(show_label=False, container=False, elem_id="metadata-doi", elem_classes="metadata-sync-field")
extra = gr.Textbox(show_label=False, container=False, lines=4, elem_id="metadata-extra", elem_classes="metadata-sync-field")
gr.HTML(
"""
Upload Data File
Drop a file into the upload area below, or click the box to browse. Accepted formats: CSV, Excel, TSV, JSON, or ZIP.
"""
)
uploaded_file = gr.File(
label="Upload Data File",
show_label=False,
interactive=True,
elem_id="upload-data-file",
elem_classes="upload-file-field",
file_count="single",
file_types=[".csv", ".xlsx", ".xls", ".tsv", ".json", ".zip"]
)
submit_btn = gr.Button("Submit Data", variant="primary")
output = gr.Markdown(elem_id="submission-output")
submit_btn.click(
fn=submit_data,
inputs=[
sender,
org,
unit,
weed,
crop,
crop_establishment,
tillage_system,
trial_type,
publication_status,
year,
doi,
extra,
uploaded_file,
],
outputs=output
)
demo.launch(
server_name="0.0.0.0",
server_port=7860,
css=CUSTOM_CSS,
js=FORCE_INPUT_TEXT_JS,
head=CUSTOM_HEAD,
theme=theme,
ssr_mode=False,
)