adding statistics page for admin
This commit is contained in:
29
web/app.py
29
web/app.py
@@ -21,6 +21,7 @@ from web.db import (
|
|||||||
set_user_keywords,
|
set_user_keywords,
|
||||||
get_all_regions,
|
get_all_regions,
|
||||||
get_all_keywords,
|
get_all_keywords,
|
||||||
|
stats_overview,
|
||||||
upsert_region,
|
upsert_region,
|
||||||
upsert_keyword,
|
upsert_keyword,
|
||||||
list_regions_full,
|
list_regions_full,
|
||||||
@@ -487,6 +488,34 @@ def admin_taxonomy():
|
|||||||
return render_template('admin/taxonomy.html', title='Taxonomy', regions=regions, keywords=keywords)
|
return render_template('admin/taxonomy.html', title='Taxonomy', regions=regions, keywords=keywords)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/admin/stats', methods=['GET'])
|
||||||
|
def admin_stats():
|
||||||
|
if not require_admin():
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
# Optional filters via query params
|
||||||
|
keyword = request.args.get('keyword')
|
||||||
|
region = request.args.get('region')
|
||||||
|
try:
|
||||||
|
stats = stats_overview()
|
||||||
|
# For detailed jobs table, reuse get_all_jobs() and filter
|
||||||
|
jobs = get_all_jobs()
|
||||||
|
if keyword:
|
||||||
|
jobs = [j for j in jobs if (j.get('keyword') or '') == keyword]
|
||||||
|
if region:
|
||||||
|
jobs = [j for j in jobs if (j.get('region') or '') == region]
|
||||||
|
except Exception as e:
|
||||||
|
flash(f'Error computing stats: {e}')
|
||||||
|
stats = {
|
||||||
|
'total_jobs': 0,
|
||||||
|
'total_keywords': 0,
|
||||||
|
'total_regions': 0,
|
||||||
|
'jobs_per_keyword': [],
|
||||||
|
'jobs_per_region': []
|
||||||
|
}
|
||||||
|
jobs = []
|
||||||
|
return render_template('admin/stats.html', title='Statistics', stats=stats, jobs=jobs, regions=get_all_regions(), keywords=get_all_keywords())
|
||||||
|
|
||||||
|
|
||||||
def init():
|
def init():
|
||||||
"""Main function to run the Flask app."""
|
"""Main function to run the Flask app."""
|
||||||
# Ensure DB is initialized
|
# Ensure DB is initialized
|
||||||
|
|||||||
41
web/db.py
41
web/db.py
@@ -783,3 +783,44 @@ def change_keyword_color(keyword_id: int, new_color: str) -> bool:
|
|||||||
except Exception:
|
except Exception:
|
||||||
session.rollback()
|
session.rollback()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def stats_overview() -> Dict[str, Any]:
|
||||||
|
"""Return an overview of job DB statistics.
|
||||||
|
|
||||||
|
Returns a dict with keys:
|
||||||
|
- total_jobs: int
|
||||||
|
- total_keywords: int (distinct keywords in listings)
|
||||||
|
- total_regions: int (distinct regions in listings)
|
||||||
|
- jobs_per_keyword: List[{"keyword": str, "count": int}]
|
||||||
|
- jobs_per_region: List[{"region": str, "count": int}]
|
||||||
|
"""
|
||||||
|
with _ensure_session() as session:
|
||||||
|
total_jobs = session.execute(text(
|
||||||
|
"SELECT COUNT(*) FROM job_listings l INNER JOIN job_descriptions d ON l.job_id = d.job_id AND l.url = d.url"
|
||||||
|
)).scalar_one()
|
||||||
|
total_keywords = session.execute(text(
|
||||||
|
"SELECT COUNT(DISTINCT keyword) FROM job_listings WHERE keyword IS NOT NULL AND keyword != ''"
|
||||||
|
)).scalar_one()
|
||||||
|
total_regions = session.execute(text(
|
||||||
|
"SELECT COUNT(DISTINCT region) FROM job_listings WHERE region IS NOT NULL AND region != ''"
|
||||||
|
)).scalar_one()
|
||||||
|
|
||||||
|
rows = session.execute(text(
|
||||||
|
"SELECT COALESCE(keyword, '') AS keyword, COUNT(*) as cnt FROM job_listings l INNER JOIN job_descriptions d ON l.job_id = d.job_id AND l.url = d.url GROUP BY keyword ORDER BY cnt DESC"
|
||||||
|
)).fetchall()
|
||||||
|
jobs_per_keyword = [
|
||||||
|
{"keyword": r[0], "count": int(r[1])} for r in rows]
|
||||||
|
|
||||||
|
rows = session.execute(text(
|
||||||
|
"SELECT COALESCE(region, '') AS region, COUNT(*) as cnt FROM job_listings l INNER JOIN job_descriptions d ON l.job_id = d.job_id AND l.url = d.url GROUP BY region ORDER BY cnt DESC"
|
||||||
|
)).fetchall()
|
||||||
|
jobs_per_region = [{"region": r[0], "count": int(r[1])} for r in rows]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total_jobs": int(total_jobs or 0),
|
||||||
|
"total_keywords": int(total_keywords or 0),
|
||||||
|
"total_regions": int(total_regions or 0),
|
||||||
|
"jobs_per_keyword": jobs_per_keyword,
|
||||||
|
"jobs_per_region": jobs_per_region,
|
||||||
|
}
|
||||||
|
|||||||
46
web/templates/admin/stats.html
Normal file
46
web/templates/admin/stats.html
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{% extends 'base.html' %} {% block content %}
|
||||||
|
<div id="admin-stats">
|
||||||
|
<h2>Database Statistics</h2>
|
||||||
|
<div class="stats-summary">
|
||||||
|
<p><strong>Total jobs:</strong> {{ stats.total_jobs }}</p>
|
||||||
|
<p><strong>Total keywords:</strong> {{ stats.total_keywords }}</p>
|
||||||
|
<p><strong>Total regions:</strong> {{ stats.total_regions }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Jobs per keyword</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Keyword</th>
|
||||||
|
<th>Count</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for row in stats.jobs_per_keyword %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ row.keyword or '(empty)' }}</td>
|
||||||
|
<td>{{ row.count }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>Jobs per region</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Region</th>
|
||||||
|
<th>Count</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for row in stats.jobs_per_region %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ row.region or '(empty)' }}</td>
|
||||||
|
<td>{{ row.count }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
{% if current_user and current_user.is_admin %} |
|
{% if current_user and current_user.is_admin %} |
|
||||||
<a href="{{ url_for('scrape_page') }}">Scrape Jobs</a> |
|
<a href="{{ url_for('scrape_page') }}">Scrape Jobs</a> |
|
||||||
<a href="{{ url_for('admin_taxonomy') }}">Taxonomy</a> |
|
<a href="{{ url_for('admin_taxonomy') }}">Taxonomy</a> |
|
||||||
|
<a href="{{ url_for('admin_stats') }}">Statistics</a> |
|
||||||
<a href="{{ url_for('admin_users') }}">Users</a> {% endif %} {% if
|
<a href="{{ url_for('admin_users') }}">Users</a> {% endif %} {% if
|
||||||
session.get('username') %} |
|
session.get('username') %} |
|
||||||
<a href="{{ url_for('logout') }}">Logout</a> {% else %} |
|
<a href="{{ url_for('logout') }}">Logout</a> {% else %} |
|
||||||
|
|||||||
Reference in New Issue
Block a user