Improved modapp
This commit is contained in:
139
app.py
139
app.py
@@ -1,11 +1,15 @@
|
|||||||
from flask import Flask, render_template, request, redirect, url_for, flash, abort, make_response, session
|
from flask import Flask, render_template, request, redirect, url_for, flash, abort, make_response, session, jsonify
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from flask_limiter import Limiter
|
||||||
|
from flask_limiter.util import get_remote_address
|
||||||
|
from flask_cors import CORS
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
from argon2 import PasswordHasher
|
from argon2 import PasswordHasher
|
||||||
from argon2.exceptions import VerifyMismatchError
|
from argon2.exceptions import VerifyMismatchError
|
||||||
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///quotes.db'
|
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///quotes.db'
|
||||||
app.config['SECRET_KEY'] = 'your_secret_key' # Use environment variable in production
|
app.config['SECRET_KEY'] = 'your_secret_key' # Use environment variable in production
|
||||||
@@ -21,13 +25,18 @@ ADMIN_CREDENTIALS = {
|
|||||||
'password': '$argon2i$v=19$m=65536,t=4,p=1$cWZDc1pQaUJLTUJoaVI4cw$kn8XKz6AEZi8ebXfyyZuzommSypliVFrsGqzOyUEIHA' # Example hash
|
'password': '$argon2i$v=19$m=65536,t=4,p=1$cWZDc1pQaUJLTUJoaVI4cw$kn8XKz6AEZi8ebXfyyZuzommSypliVFrsGqzOyUEIHA' # Example hash
|
||||||
}
|
}
|
||||||
|
|
||||||
# Define the Quote model
|
# Define the Quote modelclass Quote(db.Model):
|
||||||
class Quote(db.Model):
|
class Quote(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
text = db.Column(db.Text, nullable=False)
|
text = db.Column(db.Text, nullable=False)
|
||||||
votes = db.Column(db.Integer, default=0)
|
votes = db.Column(db.Integer, default=0)
|
||||||
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||||
status = db.Column(db.Integer, default=0) # 0 = pending, 1 = approved, 2 = rejected
|
status = db.Column(db.Integer, default=0) # 0 = pending, 1 = approved, 2 = rejected
|
||||||
|
ip_address = db.Column(db.String(45)) # Store IPv4 and IPv6 addresses
|
||||||
|
user_agent = db.Column(db.String(255)) # Store user-agent strings
|
||||||
|
submitted_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Home route to display quotes
|
# Home route to display quotes
|
||||||
# Home route to display quotes
|
# Home route to display quotes
|
||||||
@@ -51,7 +60,11 @@ def submit():
|
|||||||
flash("Quote cannot be empty.", 'error')
|
flash("Quote cannot be empty.", 'error')
|
||||||
return redirect(url_for('submit'))
|
return redirect(url_for('submit'))
|
||||||
|
|
||||||
new_quote = Quote(text=quote_text)
|
ip_address = request.remote_addr # Get the user's IP address
|
||||||
|
user_agent = request.headers.get('User-Agent') # Get the user's browser info
|
||||||
|
|
||||||
|
new_quote = Quote(text=quote_text, ip_address=ip_address, user_agent=user_agent)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
db.session.add(new_quote)
|
db.session.add(new_quote)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@@ -68,7 +81,6 @@ def submit():
|
|||||||
|
|
||||||
return render_template('submit.html', approved_count=approved_count, pending_count=pending_count)
|
return render_template('submit.html', approved_count=approved_count, pending_count=pending_count)
|
||||||
|
|
||||||
|
|
||||||
# Route to handle voting (upvote/downvote)
|
# Route to handle voting (upvote/downvote)
|
||||||
@app.route('/vote/<int:id>/<action>')
|
@app.route('/vote/<int:id>/<action>')
|
||||||
def vote(id, action):
|
def vote(id, action):
|
||||||
@@ -151,6 +163,10 @@ def quote():
|
|||||||
quote = Quote.query.get_or_404(quote_id)
|
quote = Quote.query.get_or_404(quote_id)
|
||||||
return render_template('quote.html', quote=quote)
|
return render_template('quote.html', quote=quote)
|
||||||
|
|
||||||
|
@app.route('/faq')
|
||||||
|
def faq():
|
||||||
|
return render_template('faq.html')
|
||||||
|
|
||||||
# Admin login route
|
# Admin login route
|
||||||
@app.route('/login', methods=['GET', 'POST'])
|
@app.route('/login', methods=['GET', 'POST'])
|
||||||
def login():
|
def login():
|
||||||
@@ -178,11 +194,11 @@ def modapp():
|
|||||||
if not session.get('admin'):
|
if not session.get('admin'):
|
||||||
flash('You need to log in first.', 'danger')
|
flash('You need to log in first.', 'danger')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
# Fetch all quotes (pending, approved, and rejected)
|
|
||||||
all_quotes = Quote.query.order_by(Quote.date.desc()).all()
|
|
||||||
|
|
||||||
return render_template('modapp.html', all_quotes=all_quotes)
|
all_quotes = Quote.query.order_by(Quote.date.desc()).all()
|
||||||
|
total_quotes = Quote.query.filter_by(status=1).count() # Count only approved quotes
|
||||||
|
|
||||||
|
return render_template('modapp.html', all_quotes=all_quotes, total_quotes=total_quotes)
|
||||||
|
|
||||||
@app.route('/modapp/bulk_action', methods=['POST'])
|
@app.route('/modapp/bulk_action', methods=['POST'])
|
||||||
def bulk_action():
|
def bulk_action():
|
||||||
@@ -298,6 +314,113 @@ def logout():
|
|||||||
with app.app_context():
|
with app.app_context():
|
||||||
db.create_all()
|
db.create_all()
|
||||||
|
|
||||||
|
|
||||||
|
# Initialize rate limiter and CORS for cross-origin API access
|
||||||
|
limiter = Limiter(app, key_func=get_remote_address)
|
||||||
|
CORS(app)
|
||||||
|
|
||||||
|
# API to get all approved quotes
|
||||||
|
@app.route('/api/quotes', methods=['GET'])
|
||||||
|
def get_all_quotes():
|
||||||
|
quotes = Quote.query.filter_by(status=1).all() # Only approved quotes
|
||||||
|
quote_list = [{
|
||||||
|
'id': quote.id,
|
||||||
|
'text': quote.text,
|
||||||
|
'votes': quote.votes,
|
||||||
|
'date': quote.date.strftime('%Y-%m-%d')
|
||||||
|
} for quote in quotes]
|
||||||
|
|
||||||
|
return jsonify(quote_list), 200
|
||||||
|
|
||||||
|
# API to get a specific quote by ID
|
||||||
|
@app.route('/api/quotes/<int:id>', methods=['GET'])
|
||||||
|
def get_quote(id):
|
||||||
|
quote = Quote.query.filter_by(id=id, status=1).first_or_404() # Only approved quotes
|
||||||
|
quote_data = {
|
||||||
|
'id': quote.id,
|
||||||
|
'text': quote.text,
|
||||||
|
'votes': quote.votes,
|
||||||
|
'date': quote.date.strftime('%Y-%m-%d')
|
||||||
|
}
|
||||||
|
return jsonify(quote_data), 200
|
||||||
|
|
||||||
|
# API to get a random approved quote
|
||||||
|
@app.route('/api/random', methods=['GET'])
|
||||||
|
def get_random_quote():
|
||||||
|
count = Quote.query.filter_by(status=1).count()
|
||||||
|
if count == 0:
|
||||||
|
return jsonify({"error": "No approved quotes available"}), 404
|
||||||
|
|
||||||
|
random_offset = random.randint(0, count - 1)
|
||||||
|
random_quote = Quote.query.filter_by(status=1).offset(random_offset).first()
|
||||||
|
|
||||||
|
quote_data = {
|
||||||
|
'id': random_quote.id,
|
||||||
|
'text': random_quote.text,
|
||||||
|
'votes': random_quote.votes,
|
||||||
|
'date': random_quote.date.strftime('%Y-%m-%d')
|
||||||
|
}
|
||||||
|
return jsonify(quote_data), 200
|
||||||
|
|
||||||
|
# API to get the top quotes by vote count
|
||||||
|
@app.route('/api/top', methods=['GET'])
|
||||||
|
def get_top_quotes():
|
||||||
|
top_quotes = Quote.query.filter_by(status=1).order_by(Quote.votes.desc()).limit(10).all() # Limit to top 10
|
||||||
|
quote_list = [{
|
||||||
|
'id': quote.id,
|
||||||
|
'text': quote.text,
|
||||||
|
'votes': quote.votes,
|
||||||
|
'date': quote.date.strftime('%Y-%m-%d')
|
||||||
|
} for quote in top_quotes]
|
||||||
|
|
||||||
|
return jsonify(quote_list), 200
|
||||||
|
|
||||||
|
# API to search for quotes
|
||||||
|
@app.route('/api/search', methods=['GET'])
|
||||||
|
def search_quotes():
|
||||||
|
query = request.args.get('q', '').strip()
|
||||||
|
if not query:
|
||||||
|
return jsonify({"error": "No search term provided"}), 400
|
||||||
|
|
||||||
|
quotes = Quote.query.filter(Quote.text.ilike(f'%{query}%'), Quote.status == 1).all() # Search in approved quotes
|
||||||
|
if not quotes:
|
||||||
|
return jsonify({"error": "No quotes found for search term"}), 404
|
||||||
|
|
||||||
|
quote_list = [{
|
||||||
|
'id': quote.id,
|
||||||
|
'text': quote.text,
|
||||||
|
'votes': quote.votes,
|
||||||
|
'date': quote.date.strftime('%Y-%m-%d')
|
||||||
|
} for quote in quotes]
|
||||||
|
|
||||||
|
return jsonify(quote_list), 200
|
||||||
|
|
||||||
|
# API to submit a new quote
|
||||||
|
@app.route('/api/submit', methods=['POST'])
|
||||||
|
@limiter.limit("5 per minute") # Rate limiting to prevent abuse
|
||||||
|
def submit_quote():
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
# Validate the input
|
||||||
|
if not data or not data.get('text'):
|
||||||
|
return jsonify({"error": "Quote text is required"}), 400
|
||||||
|
|
||||||
|
quote_text = data.get('text').strip()
|
||||||
|
|
||||||
|
# Basic validation to prevent spam
|
||||||
|
if len(quote_text) < 5 or len(quote_text) > 1000:
|
||||||
|
return jsonify({"error": "Quote must be between 5 and 1000 characters"}), 400
|
||||||
|
|
||||||
|
new_quote = Quote(text=quote_text)
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.session.add(new_quote)
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({"success": "Quote submitted successfully!", "id": new_quote.id}), 201
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
return jsonify({"error": f"Error submitting quote: {str(e)}"}), 500
|
||||||
|
|
||||||
# Run the Flask app
|
# Run the Flask app
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(host='127.0.0.1', port=5050, debug=True)
|
app.run(host='127.0.0.1', port=5050, debug=True)
|
||||||
|
|||||||
@@ -1,2 +1,5 @@
|
|||||||
flask_sqlalchemy
|
Flask==2.3.2
|
||||||
argon2-cffi
|
Flask-SQLAlchemy==3.0.5
|
||||||
|
Flask-Limiter==2.4
|
||||||
|
Flask-CORS==3.0.10
|
||||||
|
argon2-cffi==21.3.0
|
||||||
|
|||||||
@@ -5,12 +5,45 @@ body {
|
|||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
transition: background-color 0.3s, color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark Mode */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
background-color: #121212;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #ffa500;
|
||||||
|
}
|
||||||
|
|
||||||
|
table, td, input.text, textarea, select {
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: #ffa500;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.button {
|
||||||
|
background-color: #ffa500;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.button:hover {
|
||||||
|
background-color: #ff8c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footertext, .toplinks, h2 {
|
||||||
|
color: #ffa500;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Links */
|
/* Links */
|
||||||
a {
|
a {
|
||||||
color: #c08000;
|
color: #c08000;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
transition: color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:visited, a:link, a:hover {
|
a:visited, a:link, a:hover {
|
||||||
@@ -78,6 +111,7 @@ input.text, textarea, select {
|
|||||||
padding: 5px;
|
padding: 5px;
|
||||||
border: 1px solid #c08000;
|
border: 1px solid #c08000;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.3s, color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
input.button {
|
input.button {
|
||||||
@@ -86,6 +120,7 @@ input.button {
|
|||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
input.button:hover {
|
input.button:hover {
|
||||||
@@ -141,6 +176,7 @@ footer {
|
|||||||
border: 1px solid #c08000;
|
border: 1px solid #c08000;
|
||||||
margin: 0 5px;
|
margin: 0 5px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.3s, color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
#pagination a:hover {
|
#pagination a:hover {
|
||||||
@@ -167,3 +203,29 @@ footer {
|
|||||||
.quote-id:hover {
|
.quote-id:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dark Mode Specific */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
td.quote-box {
|
||||||
|
border-color: #ffa500;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
background-color: #ffa500;
|
||||||
|
color: #121212;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pagination a {
|
||||||
|
border-color: #ffa500;
|
||||||
|
color: #ffa500;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pagination a:hover {
|
||||||
|
background-color: #333333;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote-id {
|
||||||
|
color: #ffa500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,7 +32,9 @@
|
|||||||
<a href="/random">Random</a> /
|
<a href="/random">Random</a> /
|
||||||
<a href="/submit">Submit</a> /
|
<a href="/submit">Submit</a> /
|
||||||
<a href="/browse">Browse</a> /
|
<a href="/browse">Browse</a> /
|
||||||
<a href="/modapp">ModApp</a>
|
<a href="/modapp">ModApp</a> /
|
||||||
|
<a href="/search">Search</a>
|
||||||
|
<a href="/faq">FAQ</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -79,6 +81,9 @@
|
|||||||
<a href="/random">Random</a> /
|
<a href="/random">Random</a> /
|
||||||
<a href="/submit">Submit</a> /
|
<a href="/submit">Submit</a> /
|
||||||
<a href="/browse">Browse</a> /
|
<a href="/browse">Browse</a> /
|
||||||
|
<a href="/modapp">ModApp</a> /
|
||||||
|
<a href="/search">Search</a>
|
||||||
|
<a href="/faq">FAQ</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
123
templates/faq.html
Normal file
123
templates/faq.html
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>ircquotes: FAQ</title>
|
||||||
|
<link rel="stylesheet" href="/static/css/styles.css">
|
||||||
|
</head>
|
||||||
|
<body bgcolor="#ffffff" text="#000000" link="#c08000" vlink="#c08000" alink="#c08000">
|
||||||
|
|
||||||
|
<!-- Navigation -->
|
||||||
|
<center>
|
||||||
|
<table cellpadding="2" cellspacing="0" width="80%" border="0">
|
||||||
|
<tr>
|
||||||
|
<td bgcolor="#c08000" align="left">
|
||||||
|
<font size="+1"><b><i>ircquotes</i></b></font>
|
||||||
|
</td>
|
||||||
|
<td bgcolor="#c08000" align="right">
|
||||||
|
<font face="arial" size="+1"><b>FAQ</b></font>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<table cellpadding="2" cellspacing="0" width="80%" border="0">
|
||||||
|
<tr>
|
||||||
|
<td class="footertext" align="left" bgcolor="#f0f0f0"></td>
|
||||||
|
<td align="right" bgcolor="#f0f0f0" class="toplinks" colspan="2">
|
||||||
|
<a href="/">Home</a> /
|
||||||
|
<a href="/random">Random</a> /
|
||||||
|
<a href="/submit">Submit</a> /
|
||||||
|
<a href="/browse">Browse</a> /
|
||||||
|
<a href="/modapp">ModApp</a> /
|
||||||
|
<a href="/search">Search</a>
|
||||||
|
<a href="/faq">FAQ</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</center>
|
||||||
|
|
||||||
|
<!-- FAQ Content -->
|
||||||
|
<center>
|
||||||
|
<table cellpadding="2" cellspacing="0" width="80%">
|
||||||
|
<tr>
|
||||||
|
<td class="bodytext">
|
||||||
|
<h2>Frequently Asked Questions (FAQ)</h2>
|
||||||
|
|
||||||
|
<h3>What is ircquotes?</h3>
|
||||||
|
<p>ircquotes is a community-driven website where users can submit and browse memorable quotes from IRC (Internet Relay Chat). You can browse quotes, submit your own, and vote on others.</p>
|
||||||
|
|
||||||
|
<h3>How does the API work?</h3>
|
||||||
|
<p>The ircquotes API allows users to retrieve quotes programmatically. It is designed for developers who want to integrate IRC quotes into their own applications.</p>
|
||||||
|
|
||||||
|
<h4>Available API Endpoints</h4>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Get All Approved Quotes</strong>: <code>GET /api/quotes</code></li>
|
||||||
|
<li><strong>Get a Specific Quote by ID</strong>: <code>GET /api/quotes/<id></code></li>
|
||||||
|
<li><strong>Get a Random Quote</strong>: <code>GET /api/random</code></li>
|
||||||
|
<li><strong>Get Top Quotes</strong>: <code>GET /api/top</code></li>
|
||||||
|
<li><strong>Search Quotes</strong>: <code>GET /api/search?q=<search_term></code></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4>Submitting Quotes via the API</h4>
|
||||||
|
<p>The API also allows you to submit quotes, but this feature is rate-limited to prevent abuse. Each user is allowed 5 submissions per minute.</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Submit a Quote</strong>: <code>POST /api/submit</code></li>
|
||||||
|
<li><strong>Request Body</strong>: The request body should be in JSON format and contain the quote text like this:</li>
|
||||||
|
<pre><code>{
|
||||||
|
"text": "This is a memorable quote!"
|
||||||
|
}</code></pre>
|
||||||
|
<li><strong>Validation Rules</strong>: Quotes must be between 5 and 1000 characters.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Rules for Submitting Quotes</h3>
|
||||||
|
<p>To ensure that ircquotes remains a fun and enjoyable platform for everyone, we ask that you follow a few simple rules when submitting quotes:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>No Offensive Content</strong>: Do not submit quotes that contain offensive language, hate speech, or other harmful content.</li>
|
||||||
|
<li><strong>No Spam</strong>: Please avoid submitting irrelevant or repetitive content. The site is for memorable IRC quotes, not spam.</li>
|
||||||
|
<li><strong>Stay On-Topic</strong>: Ensure that your submissions are actual quotes from IRC, not made-up content.</li>
|
||||||
|
<li><strong>Rate Limiting</strong>: The submission rate is limited to 5 quotes per minute to prevent spam.</li>
|
||||||
|
<li><strong>Moderation</strong>: All quotes are subject to approval by site moderators. Rejected quotes will not be publicly visible.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>How are quotes moderated?</h3>
|
||||||
|
<p>Quotes go through a moderation process where they are either approved, rejected, or deleted. Approved quotes become visible to all users, while rejected ones remain in the system but aren't shown publicly.</p>
|
||||||
|
|
||||||
|
<h3>How can I get the top 100 quotes?</h3>
|
||||||
|
<p>You can view the top 100 quotes by visiting the <a href="/top">Top 100 page</a>. The quotes are ranked by votes, with the highest-voted quotes appearing at the top.</p>
|
||||||
|
|
||||||
|
<h3>How do I search for quotes?</h3>
|
||||||
|
<p>Use the search bar on the site or the API endpoint <code>/api/search?q=<search_term></code> to find specific quotes.</p>
|
||||||
|
|
||||||
|
<h3>Can I delete my submitted quotes?</h3>
|
||||||
|
<p>No, quotes are submitted anonymously, so there's no direct way to delete quotes after submission. However, site moderators can remove inappropriate content.</p>
|
||||||
|
|
||||||
|
<h3>Who can I contact for help?</h3>
|
||||||
|
<p>If you encounter any issues or have questions, feel free to reach out to the admin via the contact page or submit a help request through the site's interface.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</center>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<center>
|
||||||
|
<table border="0" cellpadding="2" cellspacing="0" width="80%" bgcolor="#c08000">
|
||||||
|
<tr>
|
||||||
|
<td bgcolor="#f0f0f0" class="toplinks" colspan="2">
|
||||||
|
<a href="/">Home</a> /
|
||||||
|
<a href="/random">Random</a> /
|
||||||
|
<a href="/submit">Submit</a> /
|
||||||
|
<a href="/browse">Browse</a> /
|
||||||
|
<a href="/modapp">ModApp</a> /
|
||||||
|
<a href="/search">Search</a>
|
||||||
|
<a href="/faq">FAQ</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="footertext" align="left"> </td>
|
||||||
|
<td class="footertext" align="right">{{ total_quotes }} quotes approved</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</center>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -33,8 +33,8 @@
|
|||||||
<a href="/submit">Submit</a> /
|
<a href="/submit">Submit</a> /
|
||||||
<a href="/browse">Browse</a> /
|
<a href="/browse">Browse</a> /
|
||||||
<a href="/modapp">ModApp</a> /
|
<a href="/modapp">ModApp</a> /
|
||||||
<!-- This link will now load search.html -->
|
|
||||||
<a href="/search">Search</a>
|
<a href="/search">Search</a>
|
||||||
|
<a href="/faq">FAQ</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
<a href="/browse">Browse</a> /
|
<a href="/browse">Browse</a> /
|
||||||
<a href="/modapp">ModApp</a> /
|
<a href="/modapp">ModApp</a> /
|
||||||
<a href="/search">Search</a>
|
<a href="/search">Search</a>
|
||||||
|
<a href="/faq">FAQ</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -68,6 +69,9 @@
|
|||||||
<a href="/random">Random</a> /
|
<a href="/random">Random</a> /
|
||||||
<a href="/submit">Submit</a> /
|
<a href="/submit">Submit</a> /
|
||||||
<a href="/browse">Browse</a> /
|
<a href="/browse">Browse</a> /
|
||||||
|
<a href="/modapp">ModApp</a> /
|
||||||
|
<a href="/search">Search</a>
|
||||||
|
<a href="/faq">FAQ</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -40,22 +40,26 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="bodytext">
|
<td class="bodytext">
|
||||||
<h2>All Quotes for Moderation</h2>
|
<h2>All Quotes for Moderation</h2>
|
||||||
|
|
||||||
<!-- Check if there are any quotes -->
|
<!-- Check if there are any quotes -->
|
||||||
{% if all_quotes %}
|
{% if all_quotes %}
|
||||||
<form method="POST" action="/modapp/bulk_action">
|
<form method="POST" action="/modapp/bulk_action">
|
||||||
<table>
|
<table border="1" cellpadding="5" cellspacing="0" width="100%">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Select</th>
|
<th>Select</th>
|
||||||
<th>Quote ID</th>
|
<th>Quote ID</th>
|
||||||
<th>Quote</th>
|
<th>Quote</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
|
<th>Submitted At</th>
|
||||||
|
<th>IP Address</th>
|
||||||
|
<th>User Agent</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- Loop through each quote -->
|
<!-- Loop through each quote -->
|
||||||
{% for quote in all_quotes %}
|
{% for quote in all_quotes %}
|
||||||
<tr style="background-color: '{% if quote.status == 1 %}#d4edda{% elif quote.status == 2 %}#f8d7da{% else %}#fff{% endif %}'">
|
<tr style="background-color:
|
||||||
<td><input type="checkbox" name="quote_ids" value="{{ quote.id }}"></td>
|
{% if quote.status == 1 %} #d4edda {% elif quote.status == 2 %} #f8d7da {% else %} #fff {% endif %}">
|
||||||
|
<td><input type="checkbox" name="quote_ids[]" value="{{ quote.id }}"></td>
|
||||||
<td>#{{ quote.id }}</td>
|
<td>#{{ quote.id }}</td>
|
||||||
<td>{{ quote.text }}</td>
|
<td>{{ quote.text }}</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -67,6 +71,15 @@
|
|||||||
Rejected
|
Rejected
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if quote.submitted_at %}
|
||||||
|
{{ quote.submitted_at.strftime('%Y-%m-%d %H:%M:%S') }}
|
||||||
|
{% else %}
|
||||||
|
N/A
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ quote.ip_address }}</td>
|
||||||
|
<td>{{ quote.user_agent }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="/approve/{{ quote.id }}">Approve</a> |
|
<a href="/approve/{{ quote.id }}">Approve</a> |
|
||||||
<a href="/reject/{{ quote.id }}">Reject</a> |
|
<a href="/reject/{{ quote.id }}">Reject</a> |
|
||||||
@@ -109,5 +122,11 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</center>
|
</center>
|
||||||
|
|
||||||
|
<!-- Dark Mode Toggle -->
|
||||||
|
<center>
|
||||||
|
<button id="theme-toggle">Toggle Dark Mode</button>
|
||||||
|
</center>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -32,7 +32,9 @@
|
|||||||
<a href="/random">Random</a> /
|
<a href="/random">Random</a> /
|
||||||
<a href="/submit">Submit</a> /
|
<a href="/submit">Submit</a> /
|
||||||
<a href="/browse">Browse</a> /
|
<a href="/browse">Browse</a> /
|
||||||
<a href="/top">Top 100</a>
|
<a href="/modapp">ModApp</a> /
|
||||||
|
<a href="/search">Search</a>
|
||||||
|
<a href="/faq">FAQ</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -26,10 +26,12 @@
|
|||||||
<td class="footertext" align="left" bgcolor="#f0f0f0"></td>
|
<td class="footertext" align="left" bgcolor="#f0f0f0"></td>
|
||||||
<td align="right" bgcolor="#f0f0f0" class="toplinks" colspan="2">
|
<td align="right" bgcolor="#f0f0f0" class="toplinks" colspan="2">
|
||||||
<a href="/">Home</a> /
|
<a href="/">Home</a> /
|
||||||
<a href="/?latest">Latest</a> /
|
<a href="/random">Random</a> /
|
||||||
<a href="/?browse">Browse</a> /
|
<a href="/submit">Submit</a> /
|
||||||
<a href="/random" rel="nofollow">Random</a> /
|
<a href="/browse">Browse</a> /
|
||||||
<a href="/?top">Top 100</a>
|
<a href="/modapp">ModApp</a> /
|
||||||
|
<a href="/search">Search</a>
|
||||||
|
<a href="/faq">FAQ</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -57,10 +59,12 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td bgcolor="#f0f0f0" class="toplinks" colspan="2">
|
<td bgcolor="#f0f0f0" class="toplinks" colspan="2">
|
||||||
<a href="/">Home</a> /
|
<a href="/">Home</a> /
|
||||||
<a href="/?latest">Latest</a> /
|
<a href="/random">Random</a> /
|
||||||
<a href="/?browse">Browse</a> /
|
<a href="/submit">Submit</a> /
|
||||||
<a href="/random" rel="nofollow">Random</a> /
|
<a href="/browse">Browse</a> /
|
||||||
<a href="/?top">Top 100</a>
|
<a href="/modapp">ModApp</a> /
|
||||||
|
<a href="/search">Search</a>
|
||||||
|
<a href="/faq">FAQ</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
<a href="/browse">Browse</a> /
|
<a href="/browse">Browse</a> /
|
||||||
<a href="/modapp">ModApp</a> /
|
<a href="/modapp">ModApp</a> /
|
||||||
<a href="/search">Search</a>
|
<a href="/search">Search</a>
|
||||||
|
<a href="/faq">FAQ</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -83,6 +84,9 @@
|
|||||||
<a href="/random">Random</a> /
|
<a href="/random">Random</a> /
|
||||||
<a href="/submit">Submit</a> /
|
<a href="/submit">Submit</a> /
|
||||||
<a href="/browse">Browse</a> /
|
<a href="/browse">Browse</a> /
|
||||||
|
<a href="/modapp">ModApp</a> /
|
||||||
|
<a href="/search">Search</a>
|
||||||
|
<a href="/faq">FAQ</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -29,9 +29,11 @@
|
|||||||
<td align="right" bgcolor="#f0f0f0" class="toplinks" colspan="2">
|
<td align="right" bgcolor="#f0f0f0" class="toplinks" colspan="2">
|
||||||
<a href="/">Home</a> /
|
<a href="/">Home</a> /
|
||||||
<a href="/random">Random</a> /
|
<a href="/random">Random</a> /
|
||||||
<a href="/submit"><b>Add Quote</b></a> /
|
<a href="/submit">Submit</a> /
|
||||||
|
<a href="/browse">Browse</a> /
|
||||||
<a href="/modapp">ModApp</a> /
|
<a href="/modapp">ModApp</a> /
|
||||||
<a href="/search">Search</a>
|
<a href="/search">Search</a>
|
||||||
|
<a href="/faq">FAQ</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
Reference in New Issue
Block a user