shahad altamimi
إضافة تصنيف الكلمة (إجباري قبل البحث) وحفظه في قاعدة البيانات
f8aae10
Raw
History Blame Contribute Delete
7.99 kB
# ============================================================
# 📄 الملف: app/database.py
# 🎯 الغرض: طبقة قاعدة البيانات الوحيدة — MongoDB.
#
# يُقرأ رابط الاتصال MONGODB_URI من البيئة/الأسرار (لا يُكتب في الكود):
# • محليًا : من ملف .env (مثال: mongodb://localhost:27017)
# • على HF : من أسرار الـ Space (رابط Atlas: mongodb+srv://...)
#
# مجموعتان (collections):
# evaluators : المقيّمون (email فريد).
# ratings : وثيقة لكل كلمة قُيّمت (صح/خطأ) + اسم وإيميل المقيّم.
#
# أمان: عمليات الحذف في الواجهة مقصورة على إيميل المقيّم الحالي،
# فلا يقدر مقيّم على حذف تقييمات غيره.
# ============================================================
import os
from datetime import datetime
import certifi
import pandas as pd
import streamlit as st
# اسم قاعدة البيانات الافتراضي إن لم يُحدَّد داخل الرابط
DEFAULT_DB_NAME = "reverse_dictionary"
# خرائط الأعمدة → أسماء عربية للعرض في الواجهة
_AR_COLUMNS = {
"created_at": "التاريخ والوقت",
"query": "الاستعلام",
"word_type": "التصنيف",
"model": "النموذج",
"word": "الكلمة",
"similarity": "نسبة التشابه",
"verdict": "التقييم",
"evaluator_name": "المقيّم",
"evaluator_email": "الإيميل",
}
# ------------------------------------------------------------
# 🔌 قراءة الإعدادات والاتصال
# ------------------------------------------------------------
def _setting(key: str) -> str | None:
"""يقرأ إعدادًا من متغيّرات البيئة أولًا (HF Spaces / .env)، ثم من
st.secrets فقط إن وُجد ملف secrets.toml — لتفادي تحذير «No secrets found»."""
val = os.getenv(key)
if val:
return val
secret_paths = (
"/root/.streamlit/secrets.toml",
"/app/.streamlit/secrets.toml",
os.path.expanduser("~/.streamlit/secrets.toml"),
".streamlit/secrets.toml",
)
if any(os.path.exists(p) for p in secret_paths):
try:
return st.secrets[key]
except Exception:
return None
return None
def is_configured() -> bool:
"""هل MONGODB_URI مضبوط؟ (التطبيق يتطلّبه للتخزين)."""
return _setting("MONGODB_URI") is not None
@st.cache_resource(show_spinner=False)
def _get_db():
"""يفتح الاتصال مرة واحدة، وينشئ فهرس الإيميل الفريد.
tlsCAFile يُستخدم تلقائيًا لروابط Atlas (mongodb+srv) ويُتجاهل محليًا."""
from pymongo import MongoClient
uri = _setting("MONGODB_URI") or ""
# شهادات TLS لازمة لروابط Atlas (mongodb+srv/ssl)، ومضرّة محليًا (localhost)
low = uri.lower()
use_tls = ("mongodb+srv" in low) or ("tls=true" in low) or ("ssl=true" in low)
kwargs = {"tlsCAFile": certifi.where()} if use_tls else {}
client = MongoClient(uri, **kwargs)
try:
db = client.get_default_database() # اسم القاعدة من الرابط إن وُجد
except Exception:
db = None
if db is None: # الرابط بلا اسم قاعدة → الافتراضي
db = client[DEFAULT_DB_NAME]
db["evaluators"].create_index("email", unique=True)
return db
def backend_label() -> str:
"""وصف مكان التخزين (يُعرض في الشريط الجانبي)."""
return "MongoDB"
# ------------------------------------------------------------
# 👤 المقيّمون
# ------------------------------------------------------------
def upsert_evaluator(name: str, email: str) -> None:
"""يُدرج المقيّم أو يحدّث اسمه (المفتاح: الإيميل)."""
_get_db()["evaluators"].update_one(
{"email": email},
{"$set": {"name": name},
"$setOnInsert": {"email": email, "created_at": datetime.now()}},
upsert=True,
)
# ------------------------------------------------------------
# 💾 حفظ تقييم استعلام كامل (كل النماذج وكلماتها دفعة واحدة)
# ------------------------------------------------------------
def save_evaluation(query: str, per_model: list[dict],
evaluator_name: str, evaluator_email: str,
word_type: str = "") -> int:
"""per_model = [{"model": str, "items": [{"lemma","score","chosen"}, ...]}, ...].
word_type = تصنيف الكلمة المختار في الواجهة (فعل / معنى معجمي متقدم / معنى عام).
يرجّع عدد الوثائق المحفوظة."""
ts = datetime.now() # وقت واحد لكل وثائق هذا الحفظ (لتماسك التجميع)
docs = []
for block in per_model:
for item in block["items"]:
docs.append({
"created_at": ts,
"query": query,
"word_type": word_type,
"model": block["model"],
"word": item["lemma"],
"similarity": round(float(item["score"]), 3),
"verdict": "صح" if item["chosen"] else "خطأ",
"evaluator_name": evaluator_name,
"evaluator_email": evaluator_email,
})
if not docs:
return 0
_get_db()["ratings"].insert_many(docs)
return len(docs)
# ------------------------------------------------------------
# 📖 قراءة التقييمات (يمكن قصرها على مقيّم) — ترجع DataFrame بأعمدة عربية
# ------------------------------------------------------------
def load_ratings(evaluator_email: str | None = None) -> pd.DataFrame:
flt = {"evaluator_email": evaluator_email} if evaluator_email else {}
rows = list(_get_db()["ratings"].find(flt).sort("created_at", 1))
if not rows:
return pd.DataFrame()
df = pd.DataFrame(rows).drop(columns=["_id"], errors="ignore")
return df.rename(columns=_AR_COLUMNS)
# ------------------------------------------------------------
# 📊 ملخّص أداء النماذج (نسبة الصح) — يمكن قصره على مقيّم
# ------------------------------------------------------------
def get_summary(evaluator_email: str | None = None) -> pd.DataFrame:
df = load_ratings(evaluator_email)
if df.empty:
return pd.DataFrame()
summary = df.groupby("النموذج")["التقييم"].value_counts().unstack(fill_value=0)
for col in ["صح", "خطأ"]:
if col not in summary.columns:
summary[col] = 0
summary["الإجمالي"] = summary["صح"] + summary["خطأ"]
summary["نسبة الصح"] = (summary["صح"] / summary["الإجمالي"] * 100).round(1).astype(str) + "%"
return summary[["صح", "خطأ", "الإجمالي", "نسبة الصح"]]
# ------------------------------------------------------------
# 🗑️ حذف (مقصور على إيميل المقيّم الحالي للأمان)
# ------------------------------------------------------------
def delete_query(created_at, query: str, evaluator_email: str) -> int:
"""يحذف تقييم استعلام واحد لهذا المقيّم."""
res = _get_db()["ratings"].delete_many({
"created_at": created_at,
"query": query,
"evaluator_email": evaluator_email,
})
return res.deleted_count
def delete_all_for(evaluator_email: str) -> int:
"""يحذف كل تقييمات المقيّم الحالي فقط."""
res = _get_db()["ratings"].delete_many({"evaluator_email": evaluator_email})
return res.deleted_count