ParthSadaria commited on
Commit
470cd60
·
verified ·
1 Parent(s): 75ad061

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +278 -0
app.py ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException, Header, Depends, Request
2
+ from fastapi.responses import JSONResponse, HTMLResponse
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ import json
5
+ import uvicorn
6
+ import os
7
+ from typing import List, Dict, Optional
8
+
9
+ # -----------------------------
10
+ # CONFIGURATION & CONSTANTS
11
+ # -----------------------------
12
+ DATA_FILE = "ddcet_dataset_raw.json"
13
+ BLOG_FILE = "blogs.json"
14
+ ADMIN_KEY_ENV = os.getenv("ADMIN_KEY", "secret123") # Set this in HF Spaces Secrets
15
+
16
+ # -----------------------------
17
+ # LOAD DATA
18
+ # -----------------------------
19
+
20
+ # Helper to load raw dataset safely
21
+ def load_raw_data():
22
+ if not os.path.exists(DATA_FILE):
23
+ # Fallback for demo if file missing
24
+ return {"subjects": []}
25
+ with open(DATA_FILE, "r", encoding="utf-8") as f:
26
+ return json.load(f)
27
+
28
+ raw_data = load_raw_data()
29
+
30
+ def load_blogs():
31
+ if not os.path.exists(BLOG_FILE):
32
+ return []
33
+ try:
34
+ with open(BLOG_FILE, "r", encoding="utf-8") as f:
35
+ return json.load(f)
36
+ except:
37
+ return []
38
+
39
+ def save_blogs(data):
40
+ with open(BLOG_FILE, "w", encoding="utf-8") as f:
41
+ json.dump(data, f, indent=4)
42
+
43
+ blogs_data = load_blogs()
44
+
45
+ # Create quick lookup maps for performance
46
+ subjects_map: Dict[int, Dict] = {}
47
+ units_map: Dict[int, Dict] = {}
48
+ questions_map: Dict[int, List[Dict]] = {}
49
+
50
+ if "subjects" in raw_data:
51
+ for subj in raw_data["subjects"]:
52
+ sid = subj["SubjectID"]
53
+ subjects_map[sid] = {"SubjectID": sid, "SubjectName": subj["SubjectName"]}
54
+ for unit in subj["units"]:
55
+ uid = unit["UnitID"]
56
+ units_map[uid] = {"UnitID": uid, "UnitName": unit.get("UnitName",""), "SubjectID": sid}
57
+ questions_map[uid] = unit.get("questions", [])
58
+
59
+ # -----------------------------
60
+ # AUTHENTICATION
61
+ # -----------------------------
62
+ async def verify_admin(x_admin_key: str = Header(None)):
63
+ """
64
+ Checks for x-admin-key in the header.
65
+ """
66
+ if x_admin_key != ADMIN_KEY_ENV:
67
+ raise HTTPException(status_code=401, detail="Invalid Admin Key")
68
+ return x_admin_key
69
+
70
+ # -----------------------------
71
+ # FASTAPI INSTANCE
72
+ # -----------------------------
73
+ app = FastAPI(title="DDCET MCQ API Fast", version="1.0")
74
+
75
+ app.add_middleware(
76
+ CORSMiddleware,
77
+ allow_origins=["*"],
78
+ allow_credentials=True,
79
+ allow_methods=["*"],
80
+ allow_headers=["*"],
81
+ )
82
+
83
+ # -----------------------------
84
+ # ENDPOINTS
85
+ # -----------------------------
86
+
87
+ @app.get("/subjects", response_class=JSONResponse)
88
+ def get_subjects():
89
+ return list(subjects_map.values())
90
+
91
+ @app.get("/units/{subject_id}", response_class=JSONResponse)
92
+ def get_units(subject_id: int):
93
+ units = [u for u in units_map.values() if u["SubjectID"] == subject_id]
94
+ if not units:
95
+ raise HTTPException(status_code=404, detail="No units found for this subject")
96
+ return units
97
+
98
+ @app.get("/questions/{unit_id}", response_class=JSONResponse)
99
+ def get_questions(unit_id: int):
100
+ qs = questions_map.get(unit_id)
101
+ if qs is None:
102
+ raise HTTPException(status_code=404, detail="No questions found for this unit")
103
+ return qs
104
+
105
+ @app.get("/question/{mcqid}", response_class=JSONResponse)
106
+ def get_question(mcqid: int):
107
+ for qlist in questions_map.values():
108
+ for q in qlist:
109
+ if q["QuestionObject"]["MCQID"] == mcqid:
110
+ return q
111
+ raise HTTPException(status_code=404, detail="Question not found")
112
+
113
+ @app.get("/blogs")
114
+ def get_blogs():
115
+ return blogs_data
116
+
117
+ # PROTECTED ENDPOINT
118
+ @app.post("/blogs", dependencies=[Depends(verify_admin)])
119
+ def add_blog(blog: dict):
120
+ blog["id"] = blog.get("id", len(blogs_data) + 1)
121
+ # Ensure fields exist for the frontend
122
+ blog["date"] = blog.get("date", "")
123
+
124
+ blogs_data.insert(0, blog) # Add to top
125
+ save_blogs(blogs_data)
126
+ return {"message": "Blog added", "blog": blog}
127
+
128
+ # -----------------------------
129
+ # ADMIN PANEL HTML UI
130
+ # -----------------------------
131
+ @app.get("/admin", response_class=HTMLResponse)
132
+ def admin_panel():
133
+ html_content = """
134
+ <!DOCTYPE html>
135
+ <html lang="en" class="dark">
136
+ <head>
137
+ <meta charset="UTF-8">
138
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
139
+ <title>Admin Panel</title>
140
+ <script src="https://cdn.tailwindcss.com"></script>
141
+ <style>
142
+ body { background-color: #09090b; color: #e4e4e7; font-family: monospace; }
143
+ .input-field { background: #18181b; border: 1px solid #27272a; color: white; padding: 0.5rem; width: 100%; outline: none; margin-bottom: 1rem; }
144
+ .input-field:focus { border-color: #52525b; }
145
+ .btn { background: #fafafa; color: black; padding: 0.5rem 1rem; font-weight: bold; cursor: pointer; transition: 0.2s; }
146
+ .btn:hover { background: #d4d4d8; }
147
+ .card { border: 1px solid #27272a; padding: 1rem; margin-bottom: 1rem; background: #0c0c0e; }
148
+ </style>
149
+ </head>
150
+ <body class="max-w-3xl mx-auto p-6">
151
+ <div class="flex justify-between items-center mb-8 border-b border-zinc-800 pb-4">
152
+ <h1 class="text-xl font-bold tracking-widest uppercase">Admin /// Console</h1>
153
+ <div class="flex gap-2">
154
+ <input type="password" id="apiKey" placeholder="Enter Admin Key" class="bg-zinc-900 border border-zinc-800 px-3 py-1 text-xs w-48 text-white">
155
+ <button onclick="saveKey()" class="text-xs border border-zinc-700 px-2 hover:bg-zinc-800">AUTH</button>
156
+ </div>
157
+ </div>
158
+
159
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-8">
160
+ <!-- ADD BLOG FORM -->
161
+ <div>
162
+ <h2 class="text-sm text-zinc-500 mb-4 uppercase">/// New Entry</h2>
163
+ <form id="blogForm" onsubmit="submitBlog(event)">
164
+ <label class="text-xs text-zinc-500">Title</label>
165
+ <input type="text" name="title" required class="input-field">
166
+
167
+ <label class="text-xs text-zinc-500">Image URL</label>
168
+ <input type="url" name="image" required class="input-field">
169
+
170
+ <label class="text-xs text-zinc-500">Link URL</label>
171
+ <input type="url" name="url" required class="input-field">
172
+
173
+ <label class="text-xs text-zinc-500">Summary</label>
174
+ <textarea name="summary" rows="3" required class="input-field"></textarea>
175
+
176
+ <div class="flex gap-4">
177
+ <div class="w-1/2">
178
+ <label class="text-xs text-zinc-500">Author</label>
179
+ <input type="text" name="author" value="Admin" class="input-field">
180
+ </div>
181
+ <div class="w-1/2">
182
+ <label class="text-xs text-zinc-500">Date (YYYY-MM-DD)</label>
183
+ <input type="date" name="date" class="input-field text-zinc-400">
184
+ </div>
185
+ </div>
186
+
187
+ <button type="submit" class="btn w-full mt-2">PUBLISH DATA</button>
188
+ </form>
189
+ <div id="msg" class="mt-4 text-xs"></div>
190
+ </div>
191
+
192
+ <!-- PREVIEW LIST -->
193
+ <div>
194
+ <h2 class="text-xs text-zinc-500 mb-4 uppercase">/// Database Entries</h2>
195
+ <div id="blogList" class="space-y-2 h-[500px] overflow-y-auto pr-2">
196
+ <div class="text-zinc-600 text-xs animate-pulse">Loading stream...</div>
197
+ </div>
198
+ </div>
199
+ </div>
200
+
201
+ <script>
202
+ // Auth Logic
203
+ function getAuth() { return localStorage.getItem('admin_key') || ''; }
204
+ function saveKey() {
205
+ const key = document.getElementById('apiKey').value;
206
+ localStorage.setItem('admin_key', key);
207
+ alert('Key stored locally.');
208
+ loadBlogs();
209
+ }
210
+ document.getElementById('apiKey').value = getAuth();
211
+
212
+ // Fetch Blogs
213
+ async function loadBlogs() {
214
+ const res = await fetch('/blogs');
215
+ const data = await res.json();
216
+ const list = document.getElementById('blogList');
217
+ list.innerHTML = '';
218
+ data.forEach(b => {
219
+ list.innerHTML += `
220
+ <div class="card group hover:border-zinc-600 transition-colors">
221
+ <div class="font-bold text-sm text-white">${b.title}</div>
222
+ <div class="text-xs text-zinc-500 truncate mt-1">${b.summary}</div>
223
+ <div class="mt-2 text-[10px] text-zinc-600 font-mono uppercase">${b.author} // ${b.date}</div>
224
+ </div>
225
+ `;
226
+ });
227
+ }
228
+
229
+ // Submit Blog
230
+ async function submitBlog(e) {
231
+ e.preventDefault();
232
+ const key = getAuth();
233
+ if(!key) { alert("Please enter Admin Key in top right corner"); return; }
234
+
235
+ const formData = new FormData(e.target);
236
+ const data = Object.fromEntries(formData.entries());
237
+
238
+ // Set default date if empty
239
+ if(!data.date) data.date = new Date().toISOString().split('T')[0];
240
+
241
+ try {
242
+ const res = await fetch('/blogs', {
243
+ method: 'POST',
244
+ headers: {
245
+ 'Content-Type': 'application/json',
246
+ 'x-admin-key': key
247
+ },
248
+ body: JSON.stringify(data)
249
+ });
250
+
251
+ if(res.ok) {
252
+ document.getElementById('msg').innerHTML = '<span class="text-green-500">Success: Entry added.</span>';
253
+ e.target.reset();
254
+ loadBlogs();
255
+ } else {
256
+ const err = await res.json();
257
+ document.getElementById('msg').innerHTML = `<span class="text-red-500">Error: ${err.detail || 'Failed'}</span>`;
258
+ }
259
+ } catch(err) {
260
+ console.error(err);
261
+ }
262
+ }
263
+
264
+ // Init
265
+ loadBlogs();
266
+ </script>
267
+ </body>
268
+ </html>
269
+ """
270
+ return html_content
271
+
272
+ # -----------------------------
273
+ # RUN SERVER
274
+ # -----------------------------
275
+ if __name__ == "__main__":
276
+ # In HF Spaces, PORT is usually 7860
277
+ port = int(os.environ.get("PORT", 7860))
278
+ uvicorn.run(app, host="0.0.0.0", port=port)