Docfile commited on
Commit
d1dc645
·
verified ·
1 Parent(s): 22dd8c4

Upload index.py

Browse files
Files changed (1) hide show
  1. index.py +180 -0
index.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, render_template, redirect, url_for, flash, jsonify, abort
2
+ from flask_sqlalchemy import SQLAlchemy
3
+ from datetime import datetime, timedelta, timezone
4
+ from apscheduler.schedulers.background import BackgroundScheduler
5
+ import markdown
6
+
7
+ app = Flask(__name__)
8
+ app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///forum.db'
9
+ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
10
+ app.config['SECRET_KEY'] = 'change_this_secret_key' # À modifier pour la production
11
+ db = SQLAlchemy(app)
12
+
13
+ # -------------------------------
14
+ # Ajout d'un filtre escapejs pour Jinja2
15
+ # -------------------------------
16
+ def escapejs_filter(s):
17
+ if s is None:
18
+ return ""
19
+ return (s.replace('\\', '\\\\')
20
+ .replace("'", "\\'")
21
+ .replace('"', '\\"')
22
+ .replace('\n', '\\n')
23
+ .replace('\r', '\\r'))
24
+ app.jinja_env.filters['escapejs'] = escapejs_filter
25
+
26
+ # -------------------------------
27
+ # Modèles de données
28
+ # -------------------------------
29
+ class Thread(db.Model):
30
+ id = db.Column(db.Integer, primary_key=True)
31
+ title = db.Column(db.String(200), nullable=False)
32
+ timestamp = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
33
+ messages = db.relationship('Message', backref='thread', lazy=True, cascade="all, delete-orphan")
34
+
35
+ class Message(db.Model):
36
+ id = db.Column(db.Integer, primary_key=True)
37
+ thread_id = db.Column(db.Integer, db.ForeignKey('thread.id'), nullable=False)
38
+ content = db.Column(db.Text, nullable=False)
39
+ timestamp = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
40
+ vote_count = db.Column(db.Integer, default=0)
41
+ reports = db.Column(db.Integer, default=0)
42
+ removed = db.Column(db.Boolean, default=False)
43
+
44
+ # -------------------------------
45
+ # Routes de base et fonctionnalités
46
+ # -------------------------------
47
+
48
+ # Accueil : liste des fils de discussion
49
+ @app.route('/')
50
+ def index():
51
+ threads = Thread.query.order_by(Thread.timestamp.desc()).all()
52
+ return render_template('index.html', threads=threads)
53
+
54
+ # Création d'un nouveau fil de discussion
55
+ @app.route('/new_thread', methods=['GET', 'POST'])
56
+ def new_thread():
57
+ if request.method == 'POST':
58
+ title = request.form.get('title', '').strip()
59
+ content = request.form.get('content', '').strip()
60
+ if not title or not content:
61
+ flash("Le titre et le contenu initial sont obligatoires.", "error")
62
+ return redirect(url_for('new_thread'))
63
+ # Création du thread
64
+ thread = Thread(title=title)
65
+ db.session.add(thread)
66
+ db.session.commit()
67
+ # Message initial
68
+ message = Message(thread_id=thread.id, content=content)
69
+ db.session.add(message)
70
+ db.session.commit()
71
+ flash("Fil de discussion créé.", "success")
72
+ return redirect(url_for('thread', thread_id=thread.id))
73
+ return render_template('new_thread.html')
74
+
75
+ # Visualisation d'un fil et réponse
76
+ @app.route('/thread/<int:thread_id>', methods=['GET', 'POST'])
77
+ def thread(thread_id):
78
+ thread_obj = Thread.query.get_or_404(thread_id)
79
+ if request.method == 'POST':
80
+ content = request.form.get('content', '').strip()
81
+ if not content:
82
+ flash("Le contenu du message ne peut pas être vide.", "error")
83
+ return redirect(url_for('thread', thread_id=thread_id))
84
+ message = Message(thread_id=thread_id, content=content)
85
+ db.session.add(message)
86
+ db.session.commit()
87
+ flash("Réponse postée.", "success")
88
+ return redirect(url_for('thread', thread_id=thread_id))
89
+ # Afficher uniquement les messages de moins de 72h et non supprimés
90
+ expiration_threshold = datetime.now(timezone.utc) - timedelta(hours=72)
91
+ messages = Message.query.filter(
92
+ Message.thread_id == thread_id,
93
+ Message.timestamp >= expiration_threshold,
94
+ Message.removed == False
95
+ ).order_by(Message.timestamp.asc()).all()
96
+ return render_template('thread.html', thread=thread_obj, messages=messages)
97
+
98
+ # Système de vote (upvote/downvote)
99
+ @app.route('/vote/<int:message_id>/<action>', methods=['POST'])
100
+ def vote(message_id, action):
101
+ message = Message.query.get_or_404(message_id)
102
+ if action == 'up':
103
+ message.vote_count += 1
104
+ elif action == 'down':
105
+ message.vote_count -= 1
106
+ else:
107
+ abort(400)
108
+ db.session.commit()
109
+ flash("Vote enregistré.", "success")
110
+ return redirect(request.referrer or url_for('index'))
111
+
112
+ # Signalement d'un message
113
+ @app.route('/report/<int:message_id>', methods=['POST'])
114
+ def report(message_id):
115
+ message = Message.query.get_or_404(message_id)
116
+ message.reports += 1
117
+ db.session.commit()
118
+ flash("Message signalé.", "success")
119
+ return redirect(request.referrer or url_for('index'))
120
+
121
+ # Prévisualisation d'un message en Markdown
122
+ @app.route('/preview', methods=['POST'])
123
+ def preview():
124
+ content = request.form.get('content', '')
125
+ rendered = markdown.markdown(content)
126
+ return jsonify({'preview': rendered})
127
+
128
+ # Recherche dans les threads et messages
129
+ @app.route('/search')
130
+ def search():
131
+ query = request.args.get('q', '').strip()
132
+ if not query:
133
+ flash("Veuillez entrer un terme de recherche.", "error")
134
+ return redirect(url_for('index'))
135
+ threads = Thread.query.filter(Thread.title.ilike(f'%{query}%')).all()
136
+ messages = Message.query.filter(Message.content.ilike(f'%{query}%')).all()
137
+ return render_template('search.html', query=query, threads=threads, messages=messages)
138
+
139
+ # Page de modération : affichage des messages signalés
140
+ @app.route('/moderate')
141
+ def moderate():
142
+ reported_messages = Message.query.filter(
143
+ Message.reports >= 3, Message.removed == False
144
+ ).order_by(Message.reports.desc()).all()
145
+ return render_template('moderate.html', messages=reported_messages)
146
+
147
+ # Action de modération : retirer un message
148
+ @app.route('/remove/<int:message_id>', methods=['POST'])
149
+ def remove_message(message_id):
150
+ message = Message.query.get_or_404(message_id)
151
+ message.removed = True
152
+ db.session.commit()
153
+ flash("Message retiré.", "success")
154
+ return redirect(url_for('moderate'))
155
+
156
+ # -------------------------------
157
+ # Suppression automatique des messages de plus de 72 heures
158
+ # -------------------------------
159
+ def delete_old_messages():
160
+ with app.app_context():
161
+ expiration_threshold = datetime.now(timezone.utc) - timedelta(hours=72)
162
+ old_messages = Message.query.filter(Message.timestamp < expiration_threshold).all()
163
+ count = len(old_messages)
164
+ for msg in old_messages:
165
+ db.session.delete(msg)
166
+ db.session.commit()
167
+ if count:
168
+ print(f"{count} messages supprimés définitivement.")
169
+
170
+ scheduler = BackgroundScheduler(daemon=True)
171
+ scheduler.add_job(func=delete_old_messages, trigger="interval", hours=1)
172
+ scheduler.start()
173
+
174
+ # -------------------------------
175
+ # Lancement de l'application
176
+ # -------------------------------
177
+ if __name__ == '__main__':
178
+ with app.app_context():
179
+ db.create_all()
180
+ app.run(debug=True)