chiron 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -0
- data/CLAUDE.md +30 -1
- data/docs/development_journal.md +84 -1
- data/lib/chiron/templates/python/commands/quality/python-testing.md +578 -0
- data/lib/chiron/templates/python/commands/workflows/debug-python.md +222 -0
- data/lib/chiron/templates/python/commands/workflows/flask-development.md +667 -0
- data/lib/chiron/templates/python/commands/workflows/python-refactor.md +336 -0
- data/lib/chiron/templates/shared/commands/context/branch-context.md +176 -0
- data/lib/chiron/templates/shared/commands/context/catchup.md +6 -1
- data/lib/chiron/templates/shared/commands/context/quickstart.md +8 -3
- data/lib/chiron/templates/shared/commands/workflows/branch-management.md +256 -0
- data/lib/chiron/templates/shared/development_journal.md.erb +9 -3
- data/lib/chiron/version.rb +1 -1
- metadata +7 -1
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
# Flask Development Workflow
|
|
2
|
+
|
|
3
|
+
Comprehensive guide for developing Flask applications with best practices and modern patterns.
|
|
4
|
+
|
|
5
|
+
## Project Setup
|
|
6
|
+
|
|
7
|
+
### Basic Flask Application Structure
|
|
8
|
+
```
|
|
9
|
+
myapp/
|
|
10
|
+
├── app/
|
|
11
|
+
│ ├── __init__.py # Application factory
|
|
12
|
+
│ ├── models/ # Database models
|
|
13
|
+
│ ├── views/ # Route handlers
|
|
14
|
+
│ │ ├── __init__.py
|
|
15
|
+
│ │ ├── auth.py
|
|
16
|
+
│ │ └── main.py
|
|
17
|
+
│ ├── templates/ # Jinja2 templates
|
|
18
|
+
│ ├── static/ # CSS, JS, images
|
|
19
|
+
│ └── utils/ # Helper functions
|
|
20
|
+
├── migrations/ # Database migrations
|
|
21
|
+
├── tests/ # Test files
|
|
22
|
+
├── config.py # Configuration
|
|
23
|
+
├── requirements.txt # Dependencies
|
|
24
|
+
└── run.py # Application entry point
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Application Factory Pattern
|
|
28
|
+
```python
|
|
29
|
+
# app/__init__.py
|
|
30
|
+
from flask import Flask
|
|
31
|
+
from flask_sqlalchemy import SQLAlchemy
|
|
32
|
+
from flask_migrate import Migrate
|
|
33
|
+
from flask_login import LoginManager
|
|
34
|
+
import os
|
|
35
|
+
|
|
36
|
+
db = SQLAlchemy()
|
|
37
|
+
migrate = Migrate()
|
|
38
|
+
login_manager = LoginManager()
|
|
39
|
+
|
|
40
|
+
def create_app(config_class=None):
|
|
41
|
+
app = Flask(__name__)
|
|
42
|
+
|
|
43
|
+
# Configuration
|
|
44
|
+
if config_class:
|
|
45
|
+
app.config.from_object(config_class)
|
|
46
|
+
else:
|
|
47
|
+
app.config.from_object('config.Config')
|
|
48
|
+
|
|
49
|
+
# Initialize extensions
|
|
50
|
+
db.init_app(app)
|
|
51
|
+
migrate.init_app(app, db)
|
|
52
|
+
login_manager.init_app(app)
|
|
53
|
+
login_manager.login_view = 'auth.login'
|
|
54
|
+
|
|
55
|
+
# Register blueprints
|
|
56
|
+
from app.views.main import bp as main_bp
|
|
57
|
+
app.register_blueprint(main_bp)
|
|
58
|
+
|
|
59
|
+
from app.views.auth import bp as auth_bp
|
|
60
|
+
app.register_blueprint(auth_bp, url_prefix='/auth')
|
|
61
|
+
|
|
62
|
+
return app
|
|
63
|
+
|
|
64
|
+
# Import models for migrations
|
|
65
|
+
from app import models
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Configuration Management
|
|
69
|
+
```python
|
|
70
|
+
# config.py
|
|
71
|
+
import os
|
|
72
|
+
from dotenv import load_dotenv
|
|
73
|
+
|
|
74
|
+
basedir = os.path.abspath(os.path.dirname(__file__))
|
|
75
|
+
load_dotenv(os.path.join(basedir, '.env'))
|
|
76
|
+
|
|
77
|
+
class Config:
|
|
78
|
+
SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard-to-guess-string'
|
|
79
|
+
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
|
|
80
|
+
'sqlite:///' + os.path.join(basedir, 'app.db')
|
|
81
|
+
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
|
82
|
+
MAIL_SERVER = os.environ.get('MAIL_SERVER')
|
|
83
|
+
MAIL_PORT = int(os.environ.get('MAIL_PORT') or 587)
|
|
84
|
+
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'true').lower() in ['true', 'on', '1']
|
|
85
|
+
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
|
|
86
|
+
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
|
|
87
|
+
|
|
88
|
+
class DevelopmentConfig(Config):
|
|
89
|
+
DEBUG = True
|
|
90
|
+
|
|
91
|
+
class TestingConfig(Config):
|
|
92
|
+
TESTING = True
|
|
93
|
+
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
|
|
94
|
+
|
|
95
|
+
class ProductionConfig(Config):
|
|
96
|
+
DEBUG = False
|
|
97
|
+
|
|
98
|
+
config = {
|
|
99
|
+
'development': DevelopmentConfig,
|
|
100
|
+
'testing': TestingConfig,
|
|
101
|
+
'production': ProductionConfig,
|
|
102
|
+
'default': DevelopmentConfig
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Models and Database
|
|
107
|
+
|
|
108
|
+
### SQLAlchemy Models
|
|
109
|
+
```python
|
|
110
|
+
# app/models.py
|
|
111
|
+
from app import db, login_manager
|
|
112
|
+
from flask_login import UserMixin
|
|
113
|
+
from werkzeug.security import generate_password_hash, check_password_hash
|
|
114
|
+
from datetime import datetime
|
|
115
|
+
|
|
116
|
+
class User(UserMixin, db.Model):
|
|
117
|
+
id = db.Column(db.Integer, primary_key=True)
|
|
118
|
+
username = db.Column(db.String(64), index=True, unique=True)
|
|
119
|
+
email = db.Column(db.String(120), index=True, unique=True)
|
|
120
|
+
password_hash = db.Column(db.String(128))
|
|
121
|
+
posts = db.relationship('Post', backref='author', lazy='dynamic')
|
|
122
|
+
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
|
123
|
+
|
|
124
|
+
def set_password(self, password):
|
|
125
|
+
self.password_hash = generate_password_hash(password)
|
|
126
|
+
|
|
127
|
+
def check_password(self, password):
|
|
128
|
+
return check_password_hash(self.password_hash, password)
|
|
129
|
+
|
|
130
|
+
def __repr__(self):
|
|
131
|
+
return f'<User {self.username}>'
|
|
132
|
+
|
|
133
|
+
class Post(db.Model):
|
|
134
|
+
id = db.Column(db.Integer, primary_key=True)
|
|
135
|
+
title = db.Column(db.String(100), nullable=False)
|
|
136
|
+
body = db.Column(db.Text)
|
|
137
|
+
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
|
|
138
|
+
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
|
139
|
+
|
|
140
|
+
def __repr__(self):
|
|
141
|
+
return f'<Post {self.title}>'
|
|
142
|
+
|
|
143
|
+
@login_manager.user_loader
|
|
144
|
+
def load_user(id):
|
|
145
|
+
return User.query.get(int(id))
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Database Migrations
|
|
149
|
+
```bash
|
|
150
|
+
# Initialize migrations
|
|
151
|
+
flask db init
|
|
152
|
+
|
|
153
|
+
# Create migration
|
|
154
|
+
flask db migrate -m "Add user and post tables"
|
|
155
|
+
|
|
156
|
+
# Apply migration
|
|
157
|
+
flask db upgrade
|
|
158
|
+
|
|
159
|
+
# Downgrade migration
|
|
160
|
+
flask db downgrade
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Views and Routing
|
|
164
|
+
|
|
165
|
+
### Blueprint Structure
|
|
166
|
+
```python
|
|
167
|
+
# app/views/main.py
|
|
168
|
+
from flask import Blueprint, render_template, request, flash, redirect, url_for
|
|
169
|
+
from flask_login import login_required, current_user
|
|
170
|
+
from app import db
|
|
171
|
+
from app.models import Post
|
|
172
|
+
|
|
173
|
+
bp = Blueprint('main', __name__)
|
|
174
|
+
|
|
175
|
+
@bp.route('/')
|
|
176
|
+
@bp.route('/index')
|
|
177
|
+
def index():
|
|
178
|
+
posts = Post.query.order_by(Post.timestamp.desc()).all()
|
|
179
|
+
return render_template('index.html', posts=posts)
|
|
180
|
+
|
|
181
|
+
@bp.route('/create', methods=['GET', 'POST'])
|
|
182
|
+
@login_required
|
|
183
|
+
def create_post():
|
|
184
|
+
if request.method == 'POST':
|
|
185
|
+
title = request.form['title']
|
|
186
|
+
body = request.form['body']
|
|
187
|
+
|
|
188
|
+
if not title:
|
|
189
|
+
flash('Title is required!')
|
|
190
|
+
else:
|
|
191
|
+
post = Post(title=title, body=body, author=current_user)
|
|
192
|
+
db.session.add(post)
|
|
193
|
+
db.session.commit()
|
|
194
|
+
flash('Your post has been created!')
|
|
195
|
+
return redirect(url_for('main.index'))
|
|
196
|
+
|
|
197
|
+
return render_template('create_post.html')
|
|
198
|
+
|
|
199
|
+
@bp.route('/post/<int:id>')
|
|
200
|
+
def show_post(id):
|
|
201
|
+
post = Post.query.get_or_404(id)
|
|
202
|
+
return render_template('post.html', post=post)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Authentication Blueprint
|
|
206
|
+
```python
|
|
207
|
+
# app/views/auth.py
|
|
208
|
+
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
|
209
|
+
from flask_login import login_user, logout_user, login_required, current_user
|
|
210
|
+
from werkzeug.urls import url_parse
|
|
211
|
+
from app import db
|
|
212
|
+
from app.models import User
|
|
213
|
+
|
|
214
|
+
bp = Blueprint('auth', __name__)
|
|
215
|
+
|
|
216
|
+
@bp.route('/login', methods=['GET', 'POST'])
|
|
217
|
+
def login():
|
|
218
|
+
if current_user.is_authenticated:
|
|
219
|
+
return redirect(url_for('main.index'))
|
|
220
|
+
|
|
221
|
+
if request.method == 'POST':
|
|
222
|
+
username = request.form['username']
|
|
223
|
+
password = request.form['password']
|
|
224
|
+
remember_me = bool(request.form.get('remember_me'))
|
|
225
|
+
|
|
226
|
+
user = User.query.filter_by(username=username).first()
|
|
227
|
+
|
|
228
|
+
if user is None or not user.check_password(password):
|
|
229
|
+
flash('Invalid username or password')
|
|
230
|
+
return redirect(url_for('auth.login'))
|
|
231
|
+
|
|
232
|
+
login_user(user, remember=remember_me)
|
|
233
|
+
|
|
234
|
+
next_page = request.args.get('next')
|
|
235
|
+
if not next_page or url_parse(next_page).netloc != '':
|
|
236
|
+
next_page = url_for('main.index')
|
|
237
|
+
|
|
238
|
+
return redirect(next_page)
|
|
239
|
+
|
|
240
|
+
return render_template('auth/login.html')
|
|
241
|
+
|
|
242
|
+
@bp.route('/logout')
|
|
243
|
+
@login_required
|
|
244
|
+
def logout():
|
|
245
|
+
logout_user()
|
|
246
|
+
return redirect(url_for('main.index'))
|
|
247
|
+
|
|
248
|
+
@bp.route('/register', methods=['GET', 'POST'])
|
|
249
|
+
def register():
|
|
250
|
+
if current_user.is_authenticated:
|
|
251
|
+
return redirect(url_for('main.index'))
|
|
252
|
+
|
|
253
|
+
if request.method == 'POST':
|
|
254
|
+
username = request.form['username']
|
|
255
|
+
email = request.form['email']
|
|
256
|
+
password = request.form['password']
|
|
257
|
+
|
|
258
|
+
# Validation
|
|
259
|
+
if User.query.filter_by(username=username).first():
|
|
260
|
+
flash('Username already exists')
|
|
261
|
+
return redirect(url_for('auth.register'))
|
|
262
|
+
|
|
263
|
+
if User.query.filter_by(email=email).first():
|
|
264
|
+
flash('Email already registered')
|
|
265
|
+
return redirect(url_for('auth.register'))
|
|
266
|
+
|
|
267
|
+
user = User(username=username, email=email)
|
|
268
|
+
user.set_password(password)
|
|
269
|
+
db.session.add(user)
|
|
270
|
+
db.session.commit()
|
|
271
|
+
|
|
272
|
+
flash('Registration successful')
|
|
273
|
+
return redirect(url_for('auth.login'))
|
|
274
|
+
|
|
275
|
+
return render_template('auth/register.html')
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Forms and Validation
|
|
279
|
+
|
|
280
|
+
### Flask-WTF Forms
|
|
281
|
+
```python
|
|
282
|
+
# app/forms.py
|
|
283
|
+
from flask_wtf import FlaskForm
|
|
284
|
+
from wtforms import StringField, TextAreaField, PasswordField, BooleanField, SubmitField
|
|
285
|
+
from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError
|
|
286
|
+
from app.models import User
|
|
287
|
+
|
|
288
|
+
class LoginForm(FlaskForm):
|
|
289
|
+
username = StringField('Username', validators=[DataRequired()])
|
|
290
|
+
password = PasswordField('Password', validators=[DataRequired()])
|
|
291
|
+
remember_me = BooleanField('Remember Me')
|
|
292
|
+
submit = SubmitField('Sign In')
|
|
293
|
+
|
|
294
|
+
class RegistrationForm(FlaskForm):
|
|
295
|
+
username = StringField('Username', validators=[
|
|
296
|
+
DataRequired(),
|
|
297
|
+
Length(min=4, max=64)
|
|
298
|
+
])
|
|
299
|
+
email = StringField('Email', validators=[DataRequired(), Email()])
|
|
300
|
+
password = PasswordField('Password', validators=[
|
|
301
|
+
DataRequired(),
|
|
302
|
+
Length(min=8)
|
|
303
|
+
])
|
|
304
|
+
password2 = PasswordField('Repeat Password', validators=[
|
|
305
|
+
DataRequired(),
|
|
306
|
+
EqualTo('password')
|
|
307
|
+
])
|
|
308
|
+
submit = SubmitField('Register')
|
|
309
|
+
|
|
310
|
+
def validate_username(self, username):
|
|
311
|
+
user = User.query.filter_by(username=username.data).first()
|
|
312
|
+
if user is not None:
|
|
313
|
+
raise ValidationError('Please use a different username.')
|
|
314
|
+
|
|
315
|
+
def validate_email(self, email):
|
|
316
|
+
user = User.query.filter_by(email=email.data).first()
|
|
317
|
+
if user is not None:
|
|
318
|
+
raise ValidationError('Please use a different email.')
|
|
319
|
+
|
|
320
|
+
class PostForm(FlaskForm):
|
|
321
|
+
title = StringField('Title', validators=[
|
|
322
|
+
DataRequired(),
|
|
323
|
+
Length(min=1, max=100)
|
|
324
|
+
])
|
|
325
|
+
body = TextAreaField('Content', validators=[
|
|
326
|
+
DataRequired(),
|
|
327
|
+
Length(min=1, max=1000)
|
|
328
|
+
])
|
|
329
|
+
submit = SubmitField('Create Post')
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Using Forms in Views
|
|
333
|
+
```python
|
|
334
|
+
# Updated view using forms
|
|
335
|
+
@bp.route('/create', methods=['GET', 'POST'])
|
|
336
|
+
@login_required
|
|
337
|
+
def create_post():
|
|
338
|
+
form = PostForm()
|
|
339
|
+
if form.validate_on_submit():
|
|
340
|
+
post = Post(
|
|
341
|
+
title=form.title.data,
|
|
342
|
+
body=form.body.data,
|
|
343
|
+
author=current_user
|
|
344
|
+
)
|
|
345
|
+
db.session.add(post)
|
|
346
|
+
db.session.commit()
|
|
347
|
+
flash('Your post has been created!')
|
|
348
|
+
return redirect(url_for('main.index'))
|
|
349
|
+
|
|
350
|
+
return render_template('create_post.html', form=form)
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Templates and Frontend
|
|
354
|
+
|
|
355
|
+
### Base Template
|
|
356
|
+
```html
|
|
357
|
+
<!-- app/templates/base.html -->
|
|
358
|
+
<!DOCTYPE html>
|
|
359
|
+
<html lang="en">
|
|
360
|
+
<head>
|
|
361
|
+
<meta charset="UTF-8">
|
|
362
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
363
|
+
<title>{% if title %}{{ title }} - MyApp{% else %}MyApp{% endif %}</title>
|
|
364
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
365
|
+
</head>
|
|
366
|
+
<body>
|
|
367
|
+
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
|
368
|
+
<div class="container">
|
|
369
|
+
<a class="navbar-brand" href="{{ url_for('main.index') }}">MyApp</a>
|
|
370
|
+
<div class="navbar-nav ms-auto">
|
|
371
|
+
{% if current_user.is_authenticated %}
|
|
372
|
+
<a class="nav-link" href="{{ url_for('main.create_post') }}">New Post</a>
|
|
373
|
+
<a class="nav-link" href="{{ url_for('auth.logout') }}">Logout</a>
|
|
374
|
+
{% else %}
|
|
375
|
+
<a class="nav-link" href="{{ url_for('auth.login') }}">Login</a>
|
|
376
|
+
<a class="nav-link" href="{{ url_for('auth.register') }}">Register</a>
|
|
377
|
+
{% endif %}
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
</nav>
|
|
381
|
+
|
|
382
|
+
<main class="container mt-4">
|
|
383
|
+
{% with messages = get_flashed_messages() %}
|
|
384
|
+
{% if messages %}
|
|
385
|
+
{% for message in messages %}
|
|
386
|
+
<div class="alert alert-info alert-dismissible fade show" role="alert">
|
|
387
|
+
{{ message }}
|
|
388
|
+
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
389
|
+
</div>
|
|
390
|
+
{% endfor %}
|
|
391
|
+
{% endif %}
|
|
392
|
+
{% endwith %}
|
|
393
|
+
|
|
394
|
+
{% block content %}{% endblock %}
|
|
395
|
+
</main>
|
|
396
|
+
|
|
397
|
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
398
|
+
</body>
|
|
399
|
+
</html>
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Form Template with Error Handling
|
|
403
|
+
```html
|
|
404
|
+
<!-- app/templates/create_post.html -->
|
|
405
|
+
{% extends "base.html" %}
|
|
406
|
+
|
|
407
|
+
{% block content %}
|
|
408
|
+
<h2>Create New Post</h2>
|
|
409
|
+
|
|
410
|
+
<form method="POST">
|
|
411
|
+
{{ form.hidden_tag() }}
|
|
412
|
+
|
|
413
|
+
<div class="mb-3">
|
|
414
|
+
{{ form.title.label(class="form-label") }}
|
|
415
|
+
{{ form.title(class="form-control" + (" is-invalid" if form.title.errors else "")) }}
|
|
416
|
+
{% if form.title.errors %}
|
|
417
|
+
{% for error in form.title.errors %}
|
|
418
|
+
<div class="invalid-feedback">{{ error }}</div>
|
|
419
|
+
{% endfor %}
|
|
420
|
+
{% endif %}
|
|
421
|
+
</div>
|
|
422
|
+
|
|
423
|
+
<div class="mb-3">
|
|
424
|
+
{{ form.body.label(class="form-label") }}
|
|
425
|
+
{{ form.body(class="form-control" + (" is-invalid" if form.body.errors else ""), rows="5") }}
|
|
426
|
+
{% if form.body.errors %}
|
|
427
|
+
{% for error in form.body.errors %}
|
|
428
|
+
<div class="invalid-feedback">{{ error }}</div>
|
|
429
|
+
{% endfor %}
|
|
430
|
+
{% endif %}
|
|
431
|
+
</div>
|
|
432
|
+
|
|
433
|
+
<div class="mb-3">
|
|
434
|
+
{{ form.submit(class="btn btn-primary") }}
|
|
435
|
+
<a href="{{ url_for('main.index') }}" class="btn btn-secondary">Cancel</a>
|
|
436
|
+
</div>
|
|
437
|
+
</form>
|
|
438
|
+
{% endblock %}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
## API Development
|
|
442
|
+
|
|
443
|
+
### RESTful API with Flask-RESTful
|
|
444
|
+
```python
|
|
445
|
+
# app/api/__init__.py
|
|
446
|
+
from flask import Blueprint
|
|
447
|
+
from flask_restful import Api
|
|
448
|
+
|
|
449
|
+
bp = Blueprint('api', __name__)
|
|
450
|
+
api = Api(bp)
|
|
451
|
+
|
|
452
|
+
from app.api import users, posts
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### API Resources
|
|
456
|
+
```python
|
|
457
|
+
# app/api/posts.py
|
|
458
|
+
from flask_restful import Resource, request, marshal_with, fields
|
|
459
|
+
from flask_login import login_required, current_user
|
|
460
|
+
from app.models import Post
|
|
461
|
+
from app import db
|
|
462
|
+
|
|
463
|
+
post_fields = {
|
|
464
|
+
'id': fields.Integer,
|
|
465
|
+
'title': fields.String,
|
|
466
|
+
'body': fields.String,
|
|
467
|
+
'timestamp': fields.DateTime(dt_format='iso8601'),
|
|
468
|
+
'author': fields.String(attribute='author.username')
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
class PostResource(Resource):
|
|
472
|
+
@marshal_with(post_fields)
|
|
473
|
+
def get(self, id):
|
|
474
|
+
post = Post.query.get_or_404(id)
|
|
475
|
+
return post
|
|
476
|
+
|
|
477
|
+
@login_required
|
|
478
|
+
def delete(self, id):
|
|
479
|
+
post = Post.query.get_or_404(id)
|
|
480
|
+
if post.author != current_user:
|
|
481
|
+
return {'message': 'Permission denied'}, 403
|
|
482
|
+
|
|
483
|
+
db.session.delete(post)
|
|
484
|
+
db.session.commit()
|
|
485
|
+
return {'message': 'Post deleted'}, 200
|
|
486
|
+
|
|
487
|
+
class PostListResource(Resource):
|
|
488
|
+
@marshal_with(post_fields)
|
|
489
|
+
def get(self):
|
|
490
|
+
posts = Post.query.order_by(Post.timestamp.desc()).all()
|
|
491
|
+
return posts
|
|
492
|
+
|
|
493
|
+
@login_required
|
|
494
|
+
@marshal_with(post_fields)
|
|
495
|
+
def post(self):
|
|
496
|
+
data = request.get_json()
|
|
497
|
+
|
|
498
|
+
post = Post(
|
|
499
|
+
title=data['title'],
|
|
500
|
+
body=data.get('body', ''),
|
|
501
|
+
author=current_user
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
db.session.add(post)
|
|
505
|
+
db.session.commit()
|
|
506
|
+
|
|
507
|
+
return post, 201
|
|
508
|
+
|
|
509
|
+
# Register resources
|
|
510
|
+
from app.api import api
|
|
511
|
+
api.add_resource(PostResource, '/posts/<int:id>')
|
|
512
|
+
api.add_resource(PostListResource, '/posts')
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
## Testing Flask Applications
|
|
516
|
+
|
|
517
|
+
### Test Configuration
|
|
518
|
+
```python
|
|
519
|
+
# tests/conftest.py
|
|
520
|
+
import pytest
|
|
521
|
+
from app import create_app, db
|
|
522
|
+
from app.models import User, Post
|
|
523
|
+
from config import TestingConfig
|
|
524
|
+
|
|
525
|
+
@pytest.fixture
|
|
526
|
+
def app():
|
|
527
|
+
app = create_app(TestingConfig)
|
|
528
|
+
|
|
529
|
+
with app.app_context():
|
|
530
|
+
db.create_all()
|
|
531
|
+
yield app
|
|
532
|
+
db.drop_all()
|
|
533
|
+
|
|
534
|
+
@pytest.fixture
|
|
535
|
+
def client(app):
|
|
536
|
+
return app.test_client()
|
|
537
|
+
|
|
538
|
+
@pytest.fixture
|
|
539
|
+
def runner(app):
|
|
540
|
+
return app.test_cli_runner()
|
|
541
|
+
|
|
542
|
+
@pytest.fixture
|
|
543
|
+
def user(app):
|
|
544
|
+
user = User(username='testuser', email='test@example.com')
|
|
545
|
+
user.set_password('testpass')
|
|
546
|
+
db.session.add(user)
|
|
547
|
+
db.session.commit()
|
|
548
|
+
return user
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
### Testing Views
|
|
552
|
+
```python
|
|
553
|
+
# tests/test_views.py
|
|
554
|
+
def test_index_page(client):
|
|
555
|
+
response = client.get('/')
|
|
556
|
+
assert response.status_code == 200
|
|
557
|
+
assert b'Welcome' in response.data
|
|
558
|
+
|
|
559
|
+
def test_login_required(client):
|
|
560
|
+
response = client.get('/create')
|
|
561
|
+
assert response.status_code == 302 # Redirect to login
|
|
562
|
+
|
|
563
|
+
def test_user_login(client, user):
|
|
564
|
+
response = client.post('/auth/login', data={
|
|
565
|
+
'username': 'testuser',
|
|
566
|
+
'password': 'testpass'
|
|
567
|
+
}, follow_redirects=True)
|
|
568
|
+
|
|
569
|
+
assert response.status_code == 200
|
|
570
|
+
assert b'Welcome' in response.data
|
|
571
|
+
|
|
572
|
+
def test_create_post(client, user):
|
|
573
|
+
# Login first
|
|
574
|
+
client.post('/auth/login', data={
|
|
575
|
+
'username': 'testuser',
|
|
576
|
+
'password': 'testpass'
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
# Create post
|
|
580
|
+
response = client.post('/create', data={
|
|
581
|
+
'title': 'Test Post',
|
|
582
|
+
'body': 'This is a test post'
|
|
583
|
+
}, follow_redirects=True)
|
|
584
|
+
|
|
585
|
+
assert response.status_code == 200
|
|
586
|
+
assert b'Test Post' in response.data
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
## Deployment
|
|
590
|
+
|
|
591
|
+
### Production Configuration
|
|
592
|
+
```python
|
|
593
|
+
# app/__init__.py additions for production
|
|
594
|
+
def create_app(config_class=None):
|
|
595
|
+
app = Flask(__name__)
|
|
596
|
+
|
|
597
|
+
# ... existing configuration ...
|
|
598
|
+
|
|
599
|
+
if not app.debug and not app.testing:
|
|
600
|
+
# Logging setup
|
|
601
|
+
import logging
|
|
602
|
+
from logging.handlers import RotatingFileHandler
|
|
603
|
+
|
|
604
|
+
if not os.path.exists('logs'):
|
|
605
|
+
os.mkdir('logs')
|
|
606
|
+
|
|
607
|
+
file_handler = RotatingFileHandler(
|
|
608
|
+
'logs/myapp.log', maxBytes=10240, backupCount=10
|
|
609
|
+
)
|
|
610
|
+
file_handler.setFormatter(logging.Formatter(
|
|
611
|
+
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
|
|
612
|
+
))
|
|
613
|
+
file_handler.setLevel(logging.INFO)
|
|
614
|
+
app.logger.addHandler(file_handler)
|
|
615
|
+
|
|
616
|
+
app.logger.setLevel(logging.INFO)
|
|
617
|
+
app.logger.info('MyApp startup')
|
|
618
|
+
|
|
619
|
+
return app
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
### Docker Setup
|
|
623
|
+
```dockerfile
|
|
624
|
+
# Dockerfile
|
|
625
|
+
FROM python:3.11-slim
|
|
626
|
+
|
|
627
|
+
WORKDIR /app
|
|
628
|
+
|
|
629
|
+
COPY requirements.txt .
|
|
630
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
631
|
+
|
|
632
|
+
COPY . .
|
|
633
|
+
|
|
634
|
+
EXPOSE 5000
|
|
635
|
+
|
|
636
|
+
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "run:app"]
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
### Environment Variables
|
|
640
|
+
```bash
|
|
641
|
+
# .env
|
|
642
|
+
SECRET_KEY=your-secret-key-here
|
|
643
|
+
DATABASE_URL=sqlite:///app.db
|
|
644
|
+
MAIL_SERVER=smtp.gmail.com
|
|
645
|
+
MAIL_PORT=587
|
|
646
|
+
MAIL_USE_TLS=1
|
|
647
|
+
MAIL_USERNAME=your-email@gmail.com
|
|
648
|
+
MAIL_PASSWORD=your-password
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
## Best Practices Checklist
|
|
652
|
+
|
|
653
|
+
- [ ] Use application factory pattern
|
|
654
|
+
- [ ] Organize code with blueprints
|
|
655
|
+
- [ ] Implement proper error handling
|
|
656
|
+
- [ ] Use Flask-WTF for forms and CSRF protection
|
|
657
|
+
- [ ] Implement user authentication and authorization
|
|
658
|
+
- [ ] Write comprehensive tests
|
|
659
|
+
- [ ] Use environment variables for configuration
|
|
660
|
+
- [ ] Implement logging for production
|
|
661
|
+
- [ ] Use database migrations
|
|
662
|
+
- [ ] Follow REST conventions for APIs
|
|
663
|
+
- [ ] Implement input validation
|
|
664
|
+
- [ ] Use HTTPS in production
|
|
665
|
+
- [ ] Set up proper session management
|
|
666
|
+
- [ ] Implement rate limiting for APIs
|
|
667
|
+
- [ ] Document your API endpoints
|