kuro223 commited on
Commit
46e055c
·
1 Parent(s): 70f6e76
app/__init__.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask
2
+ from flask_sqlalchemy import SQLAlchemy
3
+ from flask_admin import Admin
4
+ from flask_ckeditor import CKEditor
5
+ from flask_migrate import Migrate
6
+ from config import Config
7
+
8
+ db = SQLAlchemy()
9
+ migrate = Migrate() # Initialisation de Flask-Migrate
10
+ admin = Admin(name='Mon Projet', template_mode='bootstrap3')
11
+ ckeditor = CKEditor()
12
+
13
+ def create_app(config_class=Config):
14
+ app = Flask(__name__)
15
+ app.config.from_object(config_class)
16
+
17
+ db.init_app(app)
18
+ migrate.init_app(app, db)
19
+ admin.init_app(app)
20
+ ckeditor.init_app(app)
21
+
22
+ from app.admin import bp as custom_admin_bp #
23
+ app.register_blueprint(custom_admin_bp)
24
+
25
+ from app.views import bp as main_bp
26
+ app.register_blueprint(main_bp)
27
+
28
+ return app
app/admin.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint
2
+ from flask_admin.contrib.sqla import ModelView
3
+ from flask_admin import BaseView, expose
4
+ from app import db, admin
5
+ from app.models import Matiere, SousCategorie, Texte
6
+ from flask_ckeditor import CKEditorField
7
+ from wtforms import StringField, TextAreaField
8
+ from bleach import clean
9
+ from bs4 import BeautifulSoup
10
+
11
+
12
+ bp = Blueprint('custom_admin', __name__, url_prefix='/admin')
13
+
14
+ def sanitize_html(html_content):
15
+ # TRÈS PERMISSIF - UNIQUEMENT POUR LE TEST
16
+ return clean(html_content, tags=[], attributes={}, strip=False)
17
+
18
+
19
+
20
+ class MatiereView(ModelView):
21
+ column_list = ('nom', 'sous_categories') # Colonnes à afficher dans la liste
22
+ form_columns = ('nom',)
23
+
24
+ class SousCategorieView(ModelView):
25
+ column_list = ('nom', 'matiere')
26
+ form_columns = ('nom', 'matiere')
27
+ #form_overrides = dict(nom=StringField)
28
+ form_args = { # Amélioration de la sélection de la matière
29
+ 'matiere': {
30
+ 'query_factory': lambda: Matiere.query.order_by(func.lower(Matiere.nom)) #Tri insensible à la casse
31
+ }
32
+ }
33
+ def on_model_change(self, form, model, is_created):
34
+ # Vérification de l'unicité (nom, matiere_id) *avant* l'insertion/mise à jour
35
+ if is_created:
36
+ existing = SousCategorie.query.filter(
37
+ func.lower(SousCategorie.nom) == func.lower(form.nom.data),
38
+ SousCategorie.matiere_id == form.matiere.data.id
39
+ ).first()
40
+ if existing:
41
+ raise ValueError("Cette sous-catégorie existe déjà pour cette matière.")
42
+ else: #Mise à jour
43
+ existing = SousCategorie.query.filter(
44
+ func.lower(SousCategorie.nom) == func.lower(form.nom.data),
45
+ SousCategorie.matiere_id == form.matiere.data.id,
46
+ SousCategorie.id != model.id
47
+ ).first()
48
+
49
+ if existing:
50
+ raise ValueError("Cette sous-catégorie existe déjà pour cette matière.")
51
+
52
+
53
+ class TexteView(ModelView):
54
+ column_list = ('titre', 'sous_categorie')
55
+ form_columns = ('titre', 'contenu', 'sous_categorie')
56
+ form_overrides = dict(contenu=CKEditorField)
57
+ form_args = {
58
+ 'sous_categorie': {
59
+ 'query_factory': lambda: SousCategorie.query.join(Matiere).order_by(func.lower(Matiere.nom), func.lower(SousCategorie.nom))
60
+ }
61
+ }
62
+
63
+ def on_model_change(self, form, model, is_created):
64
+ model.contenu = sanitize_html(form.contenu.data)
65
+
66
+
67
+
68
+ admin.add_view(MatiereView(Matiere, db.session))
69
+ admin.add_view(SousCategorieView(SousCategorie, db.session))
70
+ admin.add_view(TexteView(Texte, db.session))
app/models.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app import db
2
+ from sqlalchemy import func # pour lower()
3
+
4
+ class Matiere(db.Model):
5
+ id = db.Column(db.Integer, primary_key=True)
6
+ nom = db.Column(db.String(64), unique=True, nullable=False)
7
+ sous_categories = db.relationship('SousCategorie', backref='matiere', lazy='dynamic', cascade="all, delete-orphan")
8
+
9
+ def __repr__(self):
10
+ return f'<Matiere {self.nom}>'
11
+
12
+
13
+ class SousCategorie(db.Model):
14
+ id = db.Column(db.Integer, primary_key=True)
15
+ nom = db.Column(db.String(64), nullable=False)
16
+ matiere_id = db.Column(db.Integer, db.ForeignKey('matiere.id'), nullable=False)
17
+ textes = db.relationship('Texte', backref='sous_categorie', lazy='dynamic', cascade="all, delete-orphan")
18
+ # Contrainte d'unicité composite
19
+ __table_args__ = (
20
+ db.UniqueConstraint('nom', 'matiere_id', name='_nom_matiere_uc'),
21
+ )
22
+
23
+
24
+ def __repr__(self):
25
+ return f'<SousCategorie {self.nom}>'
26
+
27
+ class Texte(db.Model):
28
+ id = db.Column(db.Integer, primary_key=True)
29
+ titre = db.Column(db.String(128), nullable=False)
30
+ contenu = db.Column(db.Text, nullable=False)
31
+ sous_categorie_id = db.Column(db.Integer, db.ForeignKey('sous_categorie.id'), nullable=False)
32
+ auteur = db.Column(db.String(128), nullable=True)
33
+
34
+ def __repr__(self):
35
+ return f'<Texte {self.titre}>'
app/static/css/style.css ADDED
@@ -0,0 +1,584 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Style pour la page des matières */
2
+ .page-title {
3
+ text-align: center;
4
+ margin: 2rem 0 3rem;
5
+ font-size: 2.5rem;
6
+ color: #2c3e50;
7
+ position: relative;
8
+ }
9
+
10
+ .page-title::after {
11
+ content: '';
12
+ position: absolute;
13
+ bottom: -10px;
14
+ left: 50%;
15
+ transform: translateX(-50%);
16
+ width: 60px;
17
+ height: 3px;
18
+ background: #3498db;
19
+ border-radius: 2px;
20
+ }
21
+
22
+ .subjects-grid {
23
+ display: grid;
24
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
25
+ gap: 1.5rem;
26
+ padding: 1rem;
27
+ }
28
+
29
+ .subject-card {
30
+ background: white;
31
+ border-radius: 12px;
32
+ overflow: hidden;
33
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
34
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
35
+ position: relative;
36
+ }
37
+
38
+ .subject-card:hover {
39
+ transform: translateY(-5px);
40
+ box-shadow: 0 8px 15px rgba(52, 152, 219, 0.2);
41
+ }
42
+
43
+ .subject-link {
44
+ display: flex;
45
+ justify-content: space-between;
46
+ align-items: center;
47
+ padding: 1.5rem;
48
+ text-decoration: none;
49
+ color: inherit;
50
+ position: relative;
51
+ z-index: 1;
52
+ }
53
+
54
+ .subject-link::after {
55
+ content: '';
56
+ position: absolute;
57
+ top: 0;
58
+ left: 0;
59
+ width: 100%;
60
+ height: 100%;
61
+ background: linear-gradient(135deg, #46a1dd 0%, #2980b9 100%);
62
+ opacity: 0;
63
+ transition: opacity 0.3s ease;
64
+ z-index: -1;
65
+ }
66
+
67
+ .subject-link:hover {
68
+ color: white;
69
+ }
70
+
71
+ .subject-link:hover::after {
72
+ opacity: 1;
73
+ }
74
+
75
+ .subject-name {
76
+ font-size: 1.2rem;
77
+ font-weight: 600;
78
+ transition: color 0.3s ease;
79
+ }
80
+
81
+ .subject-arrow {
82
+ font-size: 1.5rem;
83
+ opacity: 0;
84
+ transform: translateX(-10px);
85
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
86
+ }
87
+
88
+ .subject-link:hover .subject-arrow {
89
+ opacity: 1;
90
+ transform: translateX(0);
91
+ }
92
+
93
+ .no-subjects {
94
+ text-align: center;
95
+ color: #95a5a6;
96
+ font-size: 1.2rem;
97
+ padding: 2rem;
98
+ grid-column: 1 / -1;
99
+ }
100
+
101
+ /* Animation d'apparition */
102
+ @keyframes cardEntrance {
103
+ from {
104
+ opacity: 0;
105
+ transform: translateY(20px);
106
+ }
107
+ to {
108
+ opacity: 1;
109
+ transform: translateY(0);
110
+ }
111
+ }
112
+
113
+ .subject-card {
114
+ animation: cardEntrance 0.6s ease forwards;
115
+ animation-delay: calc(var(--index) * 0.1s);
116
+ }
117
+
118
+ @media (max-width: 768px) {
119
+ .subjects-grid {
120
+ grid-template-columns: 1fr;
121
+ }
122
+
123
+ .subject-link {
124
+ padding: 1rem;
125
+ }
126
+ }
127
+
128
+
129
+ /* Page matière */
130
+ .subject-container {
131
+ max-width: 1200px;
132
+ margin: 2rem auto;
133
+ padding: 0 1rem;
134
+ }
135
+
136
+ .subject-header {
137
+ text-align: center;
138
+ margin-bottom: 3rem;
139
+ position: relative;
140
+ }
141
+
142
+ .subject-title {
143
+ font-size: 2.5rem;
144
+ color: #2c3e50;
145
+ margin-bottom: 1rem;
146
+ position: relative;
147
+ display: inline-block;
148
+ }
149
+
150
+ .subject-header-decoration {
151
+ height: 4px;
152
+ width: 80px;
153
+ background: #3498db;
154
+ margin: 0 auto;
155
+ border-radius: 2px;
156
+ position: relative;
157
+ animation: headerLine 1s ease-out;
158
+ }
159
+
160
+ .subcategories-section {
161
+ background: white;
162
+ padding: 2rem;
163
+ border-radius: 12px;
164
+ box-shadow: 0 4px 20px rgba(0,0,0,0.05);
165
+ }
166
+
167
+ .section-title {
168
+ color: #4a5568;
169
+ font-size: 1.5rem;
170
+ margin-bottom: 2rem;
171
+ padding-bottom: 0.5rem;
172
+ border-bottom: 2px solid #e2e8f0;
173
+ }
174
+
175
+ .subcategories-grid {
176
+ display: grid;
177
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
178
+ gap: 1.5rem;
179
+ }
180
+
181
+ .subcategory-card {
182
+ background: white;
183
+ border-radius: 8px;
184
+ padding: 1.5rem;
185
+ text-decoration: none;
186
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
187
+ border: 1px solid #e2e8f0;
188
+ position: relative;
189
+ overflow: hidden;
190
+ }
191
+
192
+ .subcategory-card:hover {
193
+ transform: translateY(-3px);
194
+ box-shadow: 0 10px 15px rgba(52, 152, 219, 0.15);
195
+ border-color: #3498db;
196
+ }
197
+
198
+ .subcategory-content {
199
+ display: flex;
200
+ justify-content: space-between;
201
+ align-items: center;
202
+ }
203
+
204
+ .subcategory-name {
205
+ color: #2c3e50;
206
+ font-weight: 500;
207
+ font-size: 1.1rem;
208
+ transition: color 0.3s ease;
209
+ }
210
+
211
+ .arrow-icon {
212
+ width: 24px;
213
+ height: 24px;
214
+ opacity: 0;
215
+ transform: translateX(-10px);
216
+ transition: all 0.3s ease;
217
+ color: #3498db;
218
+ }
219
+
220
+ .subcategory-card:hover .arrow-icon {
221
+ opacity: 1;
222
+ transform: translateX(0);
223
+ }
224
+
225
+ .no-subcategories {
226
+ text-align: center;
227
+ padding: 3rem;
228
+ grid-column: 1 / -1;
229
+ }
230
+
231
+ .empty-icon {
232
+ width: 60px;
233
+ height: 60px;
234
+ margin-bottom: 1rem;
235
+ color: #cbd5e0;
236
+ }
237
+
238
+ .no-subcategories p {
239
+ color: #a0aec0;
240
+ font-size: 1.1rem;
241
+ }
242
+
243
+ /* Animations */
244
+ @keyframes headerLine {
245
+ from { width: 0; opacity: 0; }
246
+ to { width: 80px; opacity: 1; }
247
+ }
248
+
249
+ @media (max-width: 768px) {
250
+ .subject-container {
251
+ padding: 0;
252
+ }
253
+
254
+ .subcategories-section {
255
+ padding: 1.5rem;
256
+ }
257
+
258
+ .subject-title {
259
+ font-size: 2rem;
260
+ }
261
+ }
262
+
263
+
264
+ /*sous catégorie*/
265
+ /* Page textes */
266
+ .content-container {
267
+ max-width: 1200px;
268
+ margin: 2rem auto;
269
+ padding: 0 1.5rem;
270
+ }
271
+
272
+ .header-section {
273
+ text-align: center;
274
+ margin-bottom: 3rem;
275
+ }
276
+
277
+ .main-title {
278
+ font-size: 2.3rem;
279
+ color: #2c3e50;
280
+ margin-bottom: 0.5rem;
281
+ }
282
+
283
+ .title-underline {
284
+ width: 60px;
285
+ height: 3px;
286
+ background: linear-gradient(135deg, #46a1dd 0%, #2980b9 100%);
287
+ margin: 0 auto;
288
+ border-radius: 2px;
289
+ }
290
+
291
+ .texts-section {
292
+ background: #fff;
293
+ padding: 2rem;
294
+ border-radius: 12px;
295
+ box-shadow: 0 4px 20px rgba(0,0,0,0.05);
296
+ }
297
+
298
+ .section-subtitle {
299
+ color: #4a5568;
300
+ font-size: 1.4rem;
301
+ margin-bottom: 2rem;
302
+ padding-bottom: 0.75rem;
303
+ border-bottom: 2px solid #edf2f7;
304
+ }
305
+
306
+ .texts-grid {
307
+ display: grid;
308
+ gap: 1.2rem;
309
+ }
310
+
311
+ .text-card {
312
+ display: flex;
313
+ justify-content: space-between;
314
+ align-items: center;
315
+ padding: 1.5rem;
316
+ background: #fff;
317
+ border-radius: 8px;
318
+ border: 1px solid #e2e8f0;
319
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
320
+ text-decoration: none;
321
+ position: relative;
322
+ }
323
+
324
+ .text-card:hover {
325
+ transform: translateY(-3px);
326
+ box-shadow: 0 5px 15px rgba(52, 152, 219, 0.15);
327
+ border-color: #3498db;
328
+ }
329
+
330
+ .text-title {
331
+ color: #2c3e50;
332
+ font-size: 1.1rem;
333
+ margin-bottom: 0.5rem;
334
+ }
335
+
336
+ .text-meta {
337
+ display: flex;
338
+ gap: 1rem;
339
+ font-size: 0.9rem;
340
+ color: #718096;
341
+ }
342
+
343
+ .meta-item i {
344
+ margin-right: 0.4rem;
345
+ color: #3498db;
346
+ }
347
+
348
+ .card-arrow {
349
+ color: #3498db;
350
+ opacity: 0;
351
+ }
352
+
353
+ /* le texte, cours*/
354
+
355
+ /* Page texte */
356
+ .text-container {
357
+ max-width: 800px;
358
+ margin: 2rem auto;
359
+ padding: 0 1.5rem;
360
+ }
361
+
362
+ .text-header {
363
+ text-align: center;
364
+ margin-bottom: 3rem;
365
+ }
366
+
367
+ .text-title {
368
+ font-size: 2.4rem;
369
+ color: #2c3e50;
370
+ margin-bottom: 1rem;
371
+ line-height: 1.3;
372
+ background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
373
+ -webkit-background-clip: text;
374
+ background-clip: text;
375
+ -webkit-text-fill-color: transparent;
376
+ display: inline-block;
377
+ }
378
+
379
+ .text-meta {
380
+ display: flex;
381
+ gap: 1.5rem;
382
+ justify-content: center;
383
+ color: #718096;
384
+ font-size: 0.95rem;
385
+ }
386
+
387
+ .meta-item i {
388
+ margin-right: 0.5rem;
389
+ color: #3498db;
390
+ }
391
+
392
+ .texte-contenu {
393
+ content: '';
394
+ position: absolute;
395
+ top: 0;
396
+ left: 0;
397
+ width: 100%;
398
+ height: 100%;
399
+ background: linear-gradient(135deg, #46a1dd 0%, #2980b9 100%);
400
+ opacity: 0;
401
+ transition: opacity 0.3s ease;
402
+ z-index: -1;
403
+ box-shadow: 0 4px 20px rgba(0,0,0,0.05);
404
+ animation: fadeIn 0.6s ease-out;
405
+ }
406
+
407
+ /* Styles de contenu riche */
408
+ .texte-contenu h2,
409
+ .texte-contenu h3 {
410
+ color: #2c3e50;
411
+ margin: 2rem 0 1rem;
412
+ }
413
+
414
+ .texte-contenu h2 {
415
+ font-size: 1.6rem;
416
+ border-bottom: 2px solid #e2e8f0;
417
+ padding-bottom: 0.5rem;
418
+ }
419
+
420
+ .texte-contenu h3 {
421
+ font-size: 1.4rem;
422
+ }
423
+
424
+ .texte-contenu p {
425
+ margin-bottom: 1.5rem;
426
+ }
427
+
428
+ .texte-contenu img {
429
+ max-width: 100%;
430
+ height: auto;
431
+ border-radius: 8px;
432
+ margin: 1.5rem 0;
433
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
434
+ }
435
+
436
+ .texte-contenu ul,
437
+ .texte-contenu ol {
438
+ margin: 1.5rem 0;
439
+ padding-left: 2rem;
440
+ }
441
+
442
+ .texte-contenu li {
443
+ margin-bottom: 0.8rem;
444
+ }
445
+
446
+ .texte-contenu blockquote {
447
+ border-left: 4px solid #3498db;
448
+ margin: 1.5rem 0;
449
+ padding: 1rem 1.5rem;
450
+ background: #f8f9fa;
451
+ border-radius: 0 6px 6px 0;
452
+ }
453
+
454
+ /* additif */
455
+
456
+ /* Header */
457
+ .main-navbar {
458
+ background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
459
+ box-shadow: 0 2px 15px rgba(0, 0, 0, 0.08);
460
+ padding: 0.8rem 1rem;
461
+ position: relative;
462
+ z-index: 1000;
463
+ }
464
+
465
+ .navbar-brand {
466
+ font-weight: 700;
467
+ color: #2c3e50 !important;
468
+ font-size: 1.4rem;
469
+ display: flex;
470
+ align-items: center;
471
+ transition: transform 0.3s ease;
472
+ }
473
+
474
+ .navbar-brand:hover {
475
+ transform: translateX(5px);
476
+ }
477
+
478
+ .navbar-brand::after {
479
+ content: "";
480
+ display: inline-block;
481
+ width: 2px;
482
+ height: 24px;
483
+ background: #3498db;
484
+ margin-left: 1rem;
485
+ transform: skew(-15deg);
486
+ }
487
+
488
+ .nav-link {
489
+ color: #4a5568 !important;
490
+ font-weight: 500;
491
+ padding: 0.5rem 1.2rem !important;
492
+ border-radius: 8px;
493
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
494
+ position: relative;
495
+ }
496
+
497
+ .nav-link:hover {
498
+ color: #3498db !important;
499
+ background: rgba(52, 152, 219, 0.08);
500
+ }
501
+
502
+ .nav-link.admin-link {
503
+ background: rgba(52, 152, 219, 0.1);
504
+ margin-left: 1rem;
505
+ }
506
+
507
+ .nav-link.admin-link:hover {
508
+ background: rgba(52, 152, 219, 0.2);
509
+ }
510
+
511
+ .nav-link.admin-link::before {
512
+ content: "\f023";
513
+ font-family: "Font Awesome 5 Free";
514
+ font-weight: 900;
515
+ margin-right: 0.5rem;
516
+ }
517
+
518
+ /* Footer */
519
+ .main-footer {
520
+ background: #2c3e50;
521
+ color: #ecf0f1;
522
+ padding: 2rem 0;
523
+ margin-top: auto;
524
+ border-top: 3px solid #3498db;
525
+ }
526
+
527
+ .main-footer .social-links {
528
+ display: flex;
529
+ justify-content: center;
530
+ gap: 1.5rem;
531
+ margin: 1.5rem 0;
532
+ }
533
+
534
+ .main-footer .social-links a {
535
+ color: #bdc3c7;
536
+ font-size: 1.4rem;
537
+ transition: all 0.3s ease;
538
+ }
539
+
540
+ .main-footer .social-links a:hover {
541
+ color: #3498db;
542
+ transform: translateY(-3px);
543
+ }
544
+
545
+ .main-footer .copyright {
546
+ font-size: 0.9rem;
547
+ opacity: 0.8;
548
+ margin-top: 1rem;
549
+ }
550
+
551
+ /* Responsive */
552
+ @media (max-width: 768px) {
553
+ .navbar-brand {
554
+ font-size: 1.2rem;
555
+ }
556
+
557
+ .nav-link {
558
+ padding: 0.5rem !important;
559
+ }
560
+
561
+ .main-footer {
562
+ padding: 1.5rem 0;
563
+ }
564
+ }
565
+ .copyright {
566
+ font-size: 0.9rem;
567
+ opacity: 0.8;
568
+ margin-top: 1rem;
569
+ text-align: center;
570
+ }
571
+
572
+ .text-meta {
573
+ display: flex;
574
+ gap: 1.5rem;
575
+ margin-top: 0.8rem;
576
+ }
577
+
578
+ .meta-item {
579
+ display: flex;
580
+ align-items: center;
581
+ gap: 0.5rem;
582
+ font-size: 0.9rem;
583
+ color: #6c757d;
584
+ }
app/templates/base.html ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fr">
3
+ <head>
4
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
5
+ <meta charset="UTF-8">
6
+ <title>{% block title %}Mon Projet{% endblock %}</title>
7
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
8
+ <!-- Bootstrap CSS -->
9
+ <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
10
+ {% block head %}{% endblock %}
11
+ </head>
12
+ <body class="d-flex flex-column min-vh-100">
13
+
14
+ <nav class="navbar navbar-expand-lg navbar-light bg-light">
15
+ <a class="navbar-brand" href="{{ url_for('main.index') }}">Mariam Ai</a>
16
+ <ul class="navbar-nav ml-auto"> <!-- ml-auto pour aligner à droite -->
17
+
18
+ </ul>
19
+ </nav>
20
+
21
+ <div class="container mt-4 flex-grow-1">
22
+ {% block content %}{% endblock %}
23
+ </div> <!-- End of main container -->
24
+
25
+ <footer class="main-footer">
26
+ <div class="container">
27
+ <div class="social-links">
28
+ <a href="https://whatsapp.com/channel/0029VazlRNk9WtBuQuAzUU2d"><i class="fab fa-whatsapp"></i></a>
29
+ <a href="https://www.tiktok.com/@mariamai241"><i class="fab fa-tiktok"></i></a>
30
+ </div>
31
+ <div class="copyright">
32
+ ©2025 Mariam Ai. Tous droits réservés.
33
+ </div>
34
+ </div>
35
+ </footer>
36
+
37
+ <!-- Bootstrap JS, Popper.js, and jQuery (Optional, but often needed) -->
38
+ <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
39
+ <script src="https://cdn.jsdelivr.net/npm/@popperjs/[email protected]/dist/umd/popper.min.js"></script>
40
+ <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
41
+ {% block scripts %}{% endblock %}
42
+ </body>
43
+ </html>
44
+
app/templates/index.html ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block content %}
3
+ <div class="container">
4
+ <h1 class="page-title">Matières</h1>
5
+
6
+ <div class="subjects-grid">
7
+ {% for matiere in matieres %}
8
+ <div class="subject-card">
9
+ <a class="subject-link" href="{{ url_for('main.matiere', matiere_id=matiere.id) }}">
10
+ <span class="subject-name">{{ matiere.nom }}</span>
11
+ <span class="subject-arrow">→</span>
12
+ </a>
13
+ </div>
14
+ {% else %}
15
+ <p class="no-subjects">Aucune matière disponible</p>
16
+ {% endfor %}
17
+ </div>
18
+ </div>
19
+ {% endblock %}
app/templates/matiere.html ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block content %}
4
+ <div class="subject-container">
5
+ <div class="subject-header">
6
+ <h1 class="subject-title">{{ matiere.nom }}</h1>
7
+ <div class="subject-header-decoration"></div>
8
+ </div>
9
+
10
+ <div class="subcategories-section">
11
+ <h2 class="section-title">Sous-catégories</h2>
12
+
13
+ <div class="subcategories-grid">
14
+ {% for sous_categorie in sous_categories %}
15
+ <a href="{{ url_for('main.sous_categorie', sous_categorie_id=sous_categorie.id) }}" class="subcategory-card">
16
+ <div class="subcategory-content">
17
+ <span class="subcategory-name">{{ sous_categorie.nom }}</span>
18
+ <svg class="arrow-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
19
+ <path stroke-linecap="round" stroke-linejoin="round" d="M14 5l7 7m0 0l-7 7m7-7H3"/>
20
+ </svg>
21
+ </div>
22
+ </a>
23
+ {% else %}
24
+ <div class="no-subcategories">
25
+ <svg class="empty-icon" viewBox="0 0 24 24">
26
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2M12 3a4 4 0 1 0 0 8 4 4 0 0 0 0-8z"/>
27
+ </svg>
28
+ <p>Aucune sous-catégorie disponible</p>
29
+ </div>
30
+ {% endfor %}
31
+ </div>
32
+ </div>
33
+ </div>
34
+ {% endblock %}
app/templates/sous_categorie.html ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block content %}
4
+ <div class="content-container">
5
+ <div class="header-section">
6
+ <h1 class="main-title">{{ sous_categorie.nom }}</h1>
7
+ <div class="title-underline"></div>
8
+ </div>
9
+
10
+ <div class="texts-section">
11
+ <h2 class="section-subtitle">Textes</h2>
12
+
13
+ <div class="texts-grid">
14
+ {% for texte in textes %}
15
+ <a href="{{ url_for('main.texte', texte_id=texte.id) }}" class="text-card">
16
+ <div class="card-content">
17
+ <h3 class="text-title">{{ texte.titre }}</h3>
18
+ <div class="text-meta">
19
+ <!-- Ajoutez des métadonnées si disponibles -->
20
+ <span class="meta-item"><i class="fas fa-calendar-alt"></i> Date</span>
21
+ <span class="meta-item"><i class="fas fa-user-edit"></i> Auteur</span>
22
+ </div>
23
+ </div>
24
+ <i class="fas fa-arrow-right card-arrow"></i>
25
+ </a>
26
+ {% else %}
27
+ <div class="empty-state">
28
+ <i class="fas fa-book-open empty-icon"></i>
29
+ <p>Aucun texte disponible dans cette sous-catégorie</p>
30
+ </div>
31
+ {% endfor %}
32
+ </div>
33
+ </div>
34
+ </div>
35
+ {% endblock %}
app/templates/texte.html ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block content %}
4
+ <div class="container">
5
+ <h1>{{ texte.titre }}</h1>
6
+ <p class="author">
7
+ {% if texte.auteur %}
8
+ Auteur : {{ texte.auteur }}
9
+ {% else %}
10
+ Auteur non spécifié
11
+ {% endif %}
12
+ </p>
13
+ <div>
14
+ {{ texte.contenu|safe }}
15
+ </div>
16
+ </div>
17
+ {% endblock %}
app/views.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, render_template, abort
2
+ from app.models import Matiere, SousCategorie, Texte
3
+ from sqlalchemy import func
4
+
5
+ bp = Blueprint('main', __name__)
6
+
7
+
8
+ @bp.route('/')
9
+ def index():
10
+ matieres = Matiere.query.order_by(func.lower(Matiere.nom)).all()
11
+ return render_template('index.html', matieres=matieres)
12
+
13
+ @bp.route('/matiere/<int:matiere_id>')
14
+ def matiere(matiere_id):
15
+ matiere = Matiere.query.get_or_404(matiere_id)
16
+ sous_categories = matiere.sous_categories.order_by(func.lower(SousCategorie.nom)).all() # Tri insensible
17
+ return render_template('matiere.html', matiere=matiere, sous_categories=sous_categories)
18
+
19
+ @bp.route('/sous_categorie/<int:sous_categorie_id>')
20
+ def sous_categorie(sous_categorie_id):
21
+ sous_categorie = SousCategorie.query.get_or_404(sous_categorie_id)
22
+ textes = sous_categorie.textes.order_by(Texte.titre).all() # Tri par titre
23
+ return render_template('sous_categorie.html', sous_categorie=sous_categorie, textes=textes)
24
+
25
+ @bp.route('/texte/<int:texte_id>')
26
+ def texte(texte_id):
27
+ texte = Texte.query.get_or_404(texte_id)
28
+ return render_template('texte.html', texte=texte)
config.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ class Config:
4
+ SECRET_KEY = "&#&&&yecwwiuu7"
5
+
6
+ SQLALCHEMY_DATABASE_URI = 'postgresql://avnadmin:AVNS_eOMpjBgCDOvgWH4pR0M@pg-3850092-assanimoufida264-3f2d.k.aivencloud.com:17135/defaultdb?sslmode=require'
7
+
8
+
9
+ SQLALCHEMY_TRACK_MODIFICATIONS = False
10
+
11
+ FLASK_ADMIN_SWATCH = 'cerulean'
12
+
migrations/README ADDED
@@ -0,0 +1 @@
 
 
1
+ Single-database configuration for Flask.
migrations/alembic.ini ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # A generic, single database configuration.
2
+
3
+ [alembic]
4
+ # template used to generate migration files
5
+ # file_template = %%(rev)s_%%(slug)s
6
+
7
+ # set to 'true' to run the environment during
8
+ # the 'revision' command, regardless of autogenerate
9
+ # revision_environment = false
10
+
11
+
12
+ # Logging configuration
13
+ [loggers]
14
+ keys = root,sqlalchemy,alembic,flask_migrate
15
+
16
+ [handlers]
17
+ keys = console
18
+
19
+ [formatters]
20
+ keys = generic
21
+
22
+ [logger_root]
23
+ level = WARN
24
+ handlers = console
25
+ qualname =
26
+
27
+ [logger_sqlalchemy]
28
+ level = WARN
29
+ handlers =
30
+ qualname = sqlalchemy.engine
31
+
32
+ [logger_alembic]
33
+ level = INFO
34
+ handlers =
35
+ qualname = alembic
36
+
37
+ [logger_flask_migrate]
38
+ level = INFO
39
+ handlers =
40
+ qualname = flask_migrate
41
+
42
+ [handler_console]
43
+ class = StreamHandler
44
+ args = (sys.stderr,)
45
+ level = NOTSET
46
+ formatter = generic
47
+
48
+ [formatter_generic]
49
+ format = %(levelname)-5.5s [%(name)s] %(message)s
50
+ datefmt = %H:%M:%S
migrations/env.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from logging.config import fileConfig
3
+
4
+ from flask import current_app
5
+
6
+ from alembic import context
7
+
8
+ # this is the Alembic Config object, which provides
9
+ # access to the values within the .ini file in use.
10
+ config = context.config
11
+
12
+ # Interpret the config file for Python logging.
13
+ # This line sets up loggers basically.
14
+ fileConfig(config.config_file_name)
15
+ logger = logging.getLogger('alembic.env')
16
+
17
+
18
+ def get_engine():
19
+ try:
20
+ # this works with Flask-SQLAlchemy<3 and Alchemical
21
+ return current_app.extensions['migrate'].db.get_engine()
22
+ except (TypeError, AttributeError):
23
+ # this works with Flask-SQLAlchemy>=3
24
+ return current_app.extensions['migrate'].db.engine
25
+
26
+
27
+ def get_engine_url():
28
+ try:
29
+ return get_engine().url.render_as_string(hide_password=False).replace(
30
+ '%', '%%')
31
+ except AttributeError:
32
+ return str(get_engine().url).replace('%', '%%')
33
+
34
+
35
+ # add your model's MetaData object here
36
+ # for 'autogenerate' support
37
+ # from myapp import mymodel
38
+ # target_metadata = mymodel.Base.metadata
39
+ config.set_main_option('sqlalchemy.url', get_engine_url())
40
+ target_db = current_app.extensions['migrate'].db
41
+
42
+ # other values from the config, defined by the needs of env.py,
43
+ # can be acquired:
44
+ # my_important_option = config.get_main_option("my_important_option")
45
+ # ... etc.
46
+
47
+
48
+ def get_metadata():
49
+ if hasattr(target_db, 'metadatas'):
50
+ return target_db.metadatas[None]
51
+ return target_db.metadata
52
+
53
+
54
+ def run_migrations_offline():
55
+ """Run migrations in 'offline' mode.
56
+
57
+ This configures the context with just a URL
58
+ and not an Engine, though an Engine is acceptable
59
+ here as well. By skipping the Engine creation
60
+ we don't even need a DBAPI to be available.
61
+
62
+ Calls to context.execute() here emit the given string to the
63
+ script output.
64
+
65
+ """
66
+ url = config.get_main_option("sqlalchemy.url")
67
+ context.configure(
68
+ url=url, target_metadata=get_metadata(), literal_binds=True
69
+ )
70
+
71
+ with context.begin_transaction():
72
+ context.run_migrations()
73
+
74
+
75
+ def run_migrations_online():
76
+ """Run migrations in 'online' mode.
77
+
78
+ In this scenario we need to create an Engine
79
+ and associate a connection with the context.
80
+
81
+ """
82
+
83
+ # this callback is used to prevent an auto-migration from being generated
84
+ # when there are no changes to the schema
85
+ # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
86
+ def process_revision_directives(context, revision, directives):
87
+ if getattr(config.cmd_opts, 'autogenerate', False):
88
+ script = directives[0]
89
+ if script.upgrade_ops.is_empty():
90
+ directives[:] = []
91
+ logger.info('No changes in schema detected.')
92
+
93
+ conf_args = current_app.extensions['migrate'].configure_args
94
+ if conf_args.get("process_revision_directives") is None:
95
+ conf_args["process_revision_directives"] = process_revision_directives
96
+
97
+ connectable = get_engine()
98
+
99
+ with connectable.connect() as connection:
100
+ context.configure(
101
+ connection=connection,
102
+ target_metadata=get_metadata(),
103
+ **conf_args
104
+ )
105
+
106
+ with context.begin_transaction():
107
+ context.run_migrations()
108
+
109
+
110
+ if context.is_offline_mode():
111
+ run_migrations_offline()
112
+ else:
113
+ run_migrations_online()
migrations/script.py.mako ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+ ${imports if imports else ""}
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = ${repr(up_revision)}
14
+ down_revision = ${repr(down_revision)}
15
+ branch_labels = ${repr(branch_labels)}
16
+ depends_on = ${repr(depends_on)}
17
+
18
+
19
+ def upgrade():
20
+ ${upgrades if upgrades else "pass"}
21
+
22
+
23
+ def downgrade():
24
+ ${downgrades if downgrades else "pass"}
migrations/versions/80f91a17b1db_initial_migration.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Initial migration
2
+
3
+ Revision ID: 80f91a17b1db
4
+ Revises:
5
+ Create Date: 2025-02-17 12:20:44.860732
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+ from sqlalchemy.dialects import postgresql
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = '80f91a17b1db'
14
+ down_revision = None
15
+ branch_labels = None
16
+ depends_on = None
17
+
18
+
19
+ def upgrade():
20
+ # ### commands auto generated by Alembic - please adjust! ###
21
+ op.create_table('sous_categorie',
22
+ sa.Column('id', sa.Integer(), nullable=False),
23
+ sa.Column('nom', sa.String(length=64), nullable=False),
24
+ sa.Column('matiere_id', sa.Integer(), nullable=False),
25
+ sa.ForeignKeyConstraint(['matiere_id'], ['matiere.id'], ),
26
+ sa.PrimaryKeyConstraint('id'),
27
+ sa.UniqueConstraint('nom', 'matiere_id', name='_nom_matiere_uc')
28
+ )
29
+ op.create_table('texte',
30
+ sa.Column('id', sa.Integer(), nullable=False),
31
+ sa.Column('titre', sa.String(length=128), nullable=False),
32
+ sa.Column('contenu', sa.Text(), nullable=False),
33
+ sa.Column('sous_categorie_id', sa.Integer(), nullable=False),
34
+ sa.ForeignKeyConstraint(['sous_categorie_id'], ['sous_categorie.id'], ),
35
+ sa.PrimaryKeyConstraint('id')
36
+ )
37
+ op.drop_table('cours')
38
+ op.drop_table('subjects')
39
+ op.drop_table('texts')
40
+ op.drop_table('commentaire')
41
+ op.drop_table('categories')
42
+ op.drop_table('categorie')
43
+ with op.batch_alter_table('matiere', schema=None) as batch_op:
44
+ batch_op.alter_column('nom',
45
+ existing_type=sa.VARCHAR(length=255),
46
+ type_=sa.String(length=64),
47
+ existing_nullable=False)
48
+ batch_op.create_unique_constraint(None, ['nom'])
49
+ batch_op.drop_column('description')
50
+
51
+ # ### end Alembic commands ###
52
+
53
+
54
+ def downgrade():
55
+ # ### commands auto generated by Alembic - please adjust! ###
56
+ with op.batch_alter_table('matiere', schema=None) as batch_op:
57
+ batch_op.add_column(sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True))
58
+ batch_op.drop_constraint(None, type_='unique')
59
+ batch_op.alter_column('nom',
60
+ existing_type=sa.String(length=64),
61
+ type_=sa.VARCHAR(length=255),
62
+ existing_nullable=False)
63
+
64
+ op.create_table('categorie',
65
+ sa.Column('id', sa.INTEGER(), server_default=sa.text("nextval('categorie_id_seq'::regclass)"), autoincrement=True, nullable=False),
66
+ sa.Column('nom', sa.VARCHAR(length=255), autoincrement=False, nullable=False),
67
+ sa.Column('matiere_id', sa.INTEGER(), autoincrement=False, nullable=False),
68
+ sa.ForeignKeyConstraint(['matiere_id'], ['matiere.id'], name='fk_matiere', ondelete='CASCADE'),
69
+ sa.PrimaryKeyConstraint('id', name='categorie_pkey'),
70
+ postgresql_ignore_search_path=False
71
+ )
72
+ op.create_table('categories',
73
+ sa.Column('id', sa.INTEGER(), server_default=sa.text("nextval('categories_id_seq'::regclass)"), autoincrement=True, nullable=False),
74
+ sa.Column('name', sa.VARCHAR(length=100), autoincrement=False, nullable=False),
75
+ sa.Column('subject_id', sa.INTEGER(), autoincrement=False, nullable=False),
76
+ sa.ForeignKeyConstraint(['subject_id'], ['subjects.id'], name='categories_subject_id_fkey'),
77
+ sa.PrimaryKeyConstraint('id', name='categories_pkey'),
78
+ postgresql_ignore_search_path=False
79
+ )
80
+ op.create_table('commentaire',
81
+ sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
82
+ sa.Column('contenu', sa.TEXT(), autoincrement=False, nullable=False),
83
+ sa.Column('auteur', sa.VARCHAR(length=255), autoincrement=False, nullable=False),
84
+ sa.Column('date', postgresql.TIMESTAMP(), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True),
85
+ sa.Column('cours_id', sa.INTEGER(), autoincrement=False, nullable=False),
86
+ sa.ForeignKeyConstraint(['cours_id'], ['cours.id'], name='fk_cours', ondelete='CASCADE'),
87
+ sa.PrimaryKeyConstraint('id', name='commentaire_pkey')
88
+ )
89
+ op.create_table('texts',
90
+ sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
91
+ sa.Column('title', sa.VARCHAR(length=150), autoincrement=False, nullable=True),
92
+ sa.Column('content', sa.TEXT(), autoincrement=False, nullable=False),
93
+ sa.Column('category_id', sa.INTEGER(), autoincrement=False, nullable=False),
94
+ sa.ForeignKeyConstraint(['category_id'], ['categories.id'], name='texts_category_id_fkey'),
95
+ sa.PrimaryKeyConstraint('id', name='texts_pkey')
96
+ )
97
+ op.create_table('subjects',
98
+ sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
99
+ sa.Column('name', sa.VARCHAR(length=100), autoincrement=False, nullable=False),
100
+ sa.PrimaryKeyConstraint('id', name='subjects_pkey')
101
+ )
102
+ op.create_table('cours',
103
+ sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
104
+ sa.Column('titre', sa.VARCHAR(length=255), autoincrement=False, nullable=False),
105
+ sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True),
106
+ sa.Column('categorie_id', sa.INTEGER(), autoincrement=False, nullable=False),
107
+ sa.ForeignKeyConstraint(['categorie_id'], ['categorie.id'], name='fk_categorie', ondelete='CASCADE'),
108
+ sa.PrimaryKeyConstraint('id', name='cours_pkey')
109
+ )
110
+ op.drop_table('texte')
111
+ op.drop_table('sous_categorie')
112
+ # ### end Alembic commands ###
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ Flask==3.0.3
2
+ Flask-Admin
3
+ Flask-SQLAlchemy
4
+ psycopg2-binary
5
+ Flask-WTF
6
+ bleach
7
+ beautifulsoup4
8
+ Flask-CKEditor
9
+ Flask-migrate
run.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from app import create_app
2
+ import os
3
+ app = create_app()
4
+
5
+ if __name__ == '__main__':
6
+ app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 5000)))