diff --git a/README.md b/README.md index ba0ee89..26206f2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,109 @@ -"# wixon_blog" "# wixon_blog" -"# wixon_blog" +# WIXON Blog + +Flask 기반 블로그 애플리케이션입니다. + +## 프로젝트 구조 + +``` +wixon_blog/ +├── app.py # Flask 메인 애플리케이션 +├── templates/ # Jinja2 템플릿 +│ ├── base.html # 기본 레이아웃 +│ ├── index.html # 메인 페이지 +│ ├── login.html # 로그인 페이지 +│ ├── post.html # 포스트 상세 +│ ├── write.html # 포스트 작성 +│ ├── edit_post.html # 포스트 수정 +│ └── admin/ # 관리자 페이지 +│ ├── base_admin.html # 관리자 베이스 템플릿 +│ ├── dashboard.html # 대시보드 +│ ├── posts.html # 포스트 관리 +│ ├── post_detail.html # 포스트 수정 +│ ├── members.html # 회원 목록 +│ └── member_form.html # 회원 추가/수정 +├── static/ +│ ├── css/ +│ │ ├── style.css # 메인 스타일 +│ │ └── admin.css # 관리자 스타일 +│ ├── images/ # 이미지 파일 +│ ├── font/ # 웹 폰트 +│ └── upload/img/ # 업로드 이미지 +└── README.md +``` + +## 기술 스택 + +- **Backend**: Flask (Python) +- **Database**: MySQL +- **Frontend**: UIKit 3.6.16, jQuery, Summernote Editor +- **인증**: bcrypt 암호화, Session 기반 + +## 주요 기능 + +### 사용자 기능 +- 블로그 포스트 조회 +- 로그인/로그아웃 +- 포스트 작성 (로그인 필요) +- 포스트 수정/삭제 (작성자 또는 관리자) + +### 관리자 기능 (`/admin/`) +- **대시보드**: 통계 (총 포스트, 공개/비공개, 회원 수) +- **포스트 관리**: 목록, 검색, 필터링, 수정, 삭제, 복구 +- **회원 관리**: 목록, 추가, 수정, 삭제, 비밀번호 재설정 + +## URL 구조 + +| URL | 설명 | +|-----|------| +| `/` | 메인 페이지 | +| `/login` | 로그인 | +| `/logout` | 로그아웃 | +| `/post/` | 포스트 상세 | +| `/write` | 포스트 작성 | +| `/edit_post/` | 포스트 수정 | +| `/admin/` | 관리자 대시보드 | +| `/admin/posts` | 포스트 관리 | +| `/admin/members` | 회원 관리 | + +## 실행 방법 + +```bash +# 의존성 설치 +pip install flask pymysql bcrypt beautifulsoup4 markupsafe + +# 앱 실행 +python3 app.py +``` + +서버가 `http://0.0.0.0:8899`에서 실행됩니다. + +## 데이터베이스 테이블 + +### blog +| 컬럼 | 설명 | +|------|------| +| id | PK | +| user_id | 작성자 ID (FK) | +| title | 제목 | +| category | 카테고리 (IT, NEWS, ETC) | +| contents | 내용 (HTML) | +| thumbnail_img | 썸네일 URL | +| public_yn | 공개 여부 (Y/N) | +| use_yn | 사용 여부 (Y/N, 소프트 삭제) | +| add_date | 작성일 | + +### member +| 컬럼 | 설명 | +|------|------| +| mb_idx | PK | +| mb_id | 로그인 ID | +| mb_passwd | 비밀번호 (bcrypt) | +| mb_name | 이름 | + +## 관리자 계정 + +- 관리자 권한: `admin`, `wixon`, `javamon` 아이디 보유자 + +## 라이선스 + +WIXON Associates Inc. diff --git a/app.py b/app.py index 9b10cba..93c2466 100644 --- a/app.py +++ b/app.py @@ -9,6 +9,8 @@ import uuid import re from markupsafe import Markup from jinja2 import filters +from functools import wraps +import math UPLOAD_FOLDER = 'static/upload/img' # 경로를 Flask 앱 루트 기준으로 수정 ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'} @@ -25,6 +27,8 @@ def remove_html_tags(text): @app.before_request def load_user(): + g.is_login = False + g.user_info = None if 'user_info' in session: g.is_login = True g.user_info = session['user_info'] @@ -110,7 +114,8 @@ def index(): r, posts = sql_execute(query, (), is_data=True) r, random_post = sql_execute(r_query, (), is_data=True) - posts.append(random_post[0]) + if random_post: + posts.append(random_post[0]) # 태그 제거 후 150자로 제한 for post in posts: @@ -261,5 +266,249 @@ def delete_post(id): return jsonify(success=False, message='Could not delete the post'), 500 +# ============================================ +# 관리자 페이지 +# ============================================ + +def admin_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if 'username' not in session or session['username'] not in ['admin', 'wixon', 'javamon']: + flash('관리자 권한이 필요합니다.') + return redirect(url_for('login')) + return f(*args, **kwargs) + return decorated_function + + +@app.route('/admin/') +@admin_required +def admin_dashboard(): + stats = { + 'total_posts': 0, + 'public_posts': 0, + 'private_posts': 0, + 'deleted_posts': 0, + 'total_members': 0 + } + + r, data = sql_execute("SELECT COUNT(*) as cnt FROM blog WHERE use_yn='Y'", (), is_data=True) + if r and data: + stats['total_posts'] = data[0]['cnt'] + + r, data = sql_execute("SELECT COUNT(*) as cnt FROM blog WHERE use_yn='Y' AND public_yn='Y'", (), is_data=True) + if r and data: + stats['public_posts'] = data[0]['cnt'] + + r, data = sql_execute("SELECT COUNT(*) as cnt FROM blog WHERE use_yn='Y' AND public_yn='N'", (), is_data=True) + if r and data: + stats['private_posts'] = data[0]['cnt'] + + r, data = sql_execute("SELECT COUNT(*) as cnt FROM blog WHERE use_yn='N'", (), is_data=True) + if r and data: + stats['deleted_posts'] = data[0]['cnt'] + + r, data = sql_execute("SELECT COUNT(*) as cnt FROM member", (), is_data=True) + if r and data: + stats['total_members'] = data[0]['cnt'] + + return render_template('admin/dashboard.html', stats=stats) + + +@app.route('/admin/posts') +@admin_required +def admin_posts(): + page = request.args.get('page', 1, type=int) + per_page = 20 + offset = (page - 1) * per_page + + category = request.args.get('category', '') + public_yn = request.args.get('public_yn', '') + use_yn = request.args.get('use_yn', 'Y') + search = request.args.get('search', '') + + where_clauses = [] + params = [] + + if category: + where_clauses.append("blog.category = %s") + params.append(category) + if public_yn: + where_clauses.append("blog.public_yn = %s") + params.append(public_yn) + if use_yn: + where_clauses.append("blog.use_yn = %s") + params.append(use_yn) + if search: + where_clauses.append("blog.title LIKE %s") + params.append(f"%{search}%") + + where_sql = " AND ".join(where_clauses) if where_clauses else "1=1" + + count_query = f"SELECT COUNT(*) as cnt FROM blog INNER JOIN member ON blog.user_id = member.mb_idx WHERE {where_sql}" + r, count_data = sql_execute(count_query, tuple(params), is_data=True) + total = count_data[0]['cnt'] if r and count_data else 0 + total_pages = math.ceil(total / per_page) if total > 0 else 1 + + query = f""" + SELECT blog.*, member.mb_name, member.mb_id + FROM blog + INNER JOIN member ON blog.user_id = member.mb_idx + WHERE {where_sql} + ORDER BY blog.add_date DESC + LIMIT %s OFFSET %s + """ + params.extend([per_page, offset]) + r, posts = sql_execute(query, tuple(params), is_data=True) + + pagination = { + 'page': page, + 'per_page': per_page, + 'total': total, + 'total_pages': total_pages, + 'has_prev': page > 1, + 'has_next': page < total_pages, + 'prev_num': page - 1, + 'next_num': page + 1 + } + + return render_template('admin/posts.html', posts=posts if posts else [], pagination=pagination) + + +@app.route('/admin/posts/', methods=['GET', 'POST']) +@admin_required +def admin_post_detail(post_id): + if request.method == 'POST': + title = request.form.get('title') + category = request.form.get('category') + public_yn = 'Y' if request.form.get('public') == 'on' else 'N' + contents = request.form.get('contents') + + soup = BeautifulSoup(contents, 'html.parser') + first_image = soup.find('img') + thumbnail_img = first_image['src'] if first_image else None + + query = "UPDATE blog SET title=%s, category=%s, public_yn=%s, contents=%s, thumbnail_img=%s WHERE id=%s" + res = sql_execute(query, (title, category, public_yn, contents, thumbnail_img, post_id)) + + if res: + flash('포스트가 수정되었습니다.') + return redirect(url_for('admin_posts')) + else: + flash('포스트 수정에 실패했습니다.') + + query = "SELECT blog.*, member.mb_name, member.mb_id FROM blog INNER JOIN member ON blog.user_id = member.mb_idx WHERE blog.id = %s" + r, data = sql_execute(query, (post_id,), is_data=True) + + if r and data: + return render_template('admin/post_detail.html', post=data[0]) + else: + flash('포스트를 찾을 수 없습니다.') + return redirect(url_for('admin_posts')) + + +@app.route('/admin/posts//delete', methods=['DELETE']) +@admin_required +def admin_post_delete(post_id): + query = "UPDATE blog SET use_yn='N' WHERE id=%s" + res = sql_execute(query, (post_id,)) + if res: + return jsonify(success=True, message='포스트가 삭제되었습니다.') + return jsonify(success=False, message='삭제 실패'), 500 + + +@app.route('/admin/posts//restore', methods=['POST']) +@admin_required +def admin_post_restore(post_id): + query = "UPDATE blog SET use_yn='Y' WHERE id=%s" + res = sql_execute(query, (post_id,)) + if res: + return jsonify(success=True, message='포스트가 복구되었습니다.') + return jsonify(success=False, message='복구 실패'), 500 + + +@app.route('/admin/members') +@admin_required +def admin_members(): + query = "SELECT mb_idx, mb_id, mb_name FROM member ORDER BY mb_idx DESC" + r, members = sql_execute(query, (), is_data=True) + return render_template('admin/members.html', members=members if members else []) + + +@app.route('/admin/members/add', methods=['GET', 'POST']) +@admin_required +def admin_member_add(): + if request.method == 'POST': + mb_id = request.form.get('mb_id') + mb_name = request.form.get('mb_name') + password = request.form.get('password').encode('utf-8') + hashed_pw = bcrypt.hashpw(password, bcrypt.gensalt()).decode('utf-8') + + r, existing = sql_execute("SELECT mb_id FROM member WHERE mb_id=%s", (mb_id,), is_data=True) + if r and existing: + flash('이미 존재하는 아이디입니다.') + return render_template('admin/member_form.html', member=None) + + query = "INSERT INTO member (mb_id, mb_passwd, mb_name) VALUES (%s, %s, %s)" + res = sql_execute(query, (mb_id, hashed_pw, mb_name)) + if res: + flash('회원이 추가되었습니다.') + return redirect(url_for('admin_members')) + flash('회원 추가에 실패했습니다.') + return render_template('admin/member_form.html', member=None) + + +@app.route('/admin/members/', methods=['GET', 'POST']) +@admin_required +def admin_member_detail(mb_idx): + if request.method == 'POST': + mb_name = request.form.get('mb_name') + query = "UPDATE member SET mb_name=%s WHERE mb_idx=%s" + res = sql_execute(query, (mb_name, mb_idx)) + if res: + flash('회원 정보가 수정되었습니다.') + return redirect(url_for('admin_members')) + flash('회원 정보 수정에 실패했습니다.') + + query = "SELECT mb_idx, mb_id, mb_name FROM member WHERE mb_idx=%s" + r, member = sql_execute(query, (mb_idx,), is_data=True) + + if r and member: + return render_template('admin/member_form.html', member=member[0]) + else: + flash('회원을 찾을 수 없습니다.') + return redirect(url_for('admin_members')) + + +@app.route('/admin/members//delete', methods=['DELETE']) +@admin_required +def admin_member_delete(mb_idx): + if session.get('user_info', {}).get('mb_idx') == mb_idx: + return jsonify(success=False, message='자기 자신은 삭제할 수 없습니다.'), 400 + + query = "DELETE FROM member WHERE mb_idx=%s" + res = sql_execute(query, (mb_idx,)) + if res: + return jsonify(success=True, message='회원이 삭제되었습니다.') + return jsonify(success=False, message='삭제 실패'), 500 + + +@app.route('/admin/members//reset-password', methods=['POST']) +@admin_required +def admin_member_reset_password(mb_idx): + data = request.get_json() + new_password = data.get('password', '').encode('utf-8') + + if len(new_password) < 4: + return jsonify(success=False, message='비밀번호는 4자 이상이어야 합니다.'), 400 + + hashed_pw = bcrypt.hashpw(new_password, bcrypt.gensalt()).decode('utf-8') + + query = "UPDATE member SET mb_passwd=%s WHERE mb_idx=%s" + res = sql_execute(query, (hashed_pw, mb_idx)) + if res: + return jsonify(success=True, message='비밀번호가 재설정되었습니다.') + return jsonify(success=False, message='비밀번호 재설정 실패'), 500 + + if __name__ == '__main__': app.run(host='0.0.0.0', port=8899) diff --git a/static/css/admin.css b/static/css/admin.css new file mode 100644 index 0000000..a764b2c --- /dev/null +++ b/static/css/admin.css @@ -0,0 +1,312 @@ +/* ============================================ + WIXON Blog Admin Styles + ============================================ */ + +/* Admin Layout */ +.admin__wrap { + display: flex; + min-height: 100vh; +} + +/* Sidebar */ +.admin__sidebar { + width: 250px; + background: #1e1e2d; + color: #fff; + padding: 20px; + position: fixed; + height: 100vh; + display: flex; + flex-direction: column; +} + +.admin__logo { + text-align: center; + margin-bottom: 30px; + padding-bottom: 20px; + border-bottom: 1px solid #3a3a4d; +} + +.admin__logo img { + max-width: 120px; +} + +.admin__logo span { + display: block; + color: #a2a3b7; + font-size: 12px; + margin-top: 5px; +} + +.admin__menu { + list-style: none; + padding: 0; + margin: 0; + flex: 1; +} + +.admin__menu li a { + display: flex; + align-items: center; + padding: 12px 15px; + color: #a2a3b7; + text-decoration: none; + border-radius: 5px; + margin-bottom: 5px; + transition: all 0.3s ease; +} + +.admin__menu li a:hover, +.admin__menu li a.active { + background: #1b1b28; + color: #fff; +} + +.admin__menu li a span.uk-icon { + margin-right: 10px; +} + +/* User Section */ +.admin__user { + padding-top: 20px; + border-top: 1px solid #3a3a4d; +} + +.admin__user .user__name { + color: #fff; + font-weight: 500; + margin-bottom: 10px; + display: block; +} + +.admin__user a { + color: #a2a3b7; + text-decoration: none; + font-size: 13px; + margin-right: 15px; +} + +.admin__user a:hover { + color: #fff; +} + +/* Main Content */ +.admin__content { + flex: 1; + margin-left: 250px; + padding: 30px; + background: #f5f5f5; + min-height: 100vh; +} + +.admin__content h1 { + font-size: 24px; + margin-bottom: 25px; + color: #333; +} + +/* Stats Cards */ +.stat__card { + background: #fff; + border-radius: 8px; + padding: 25px; + box-shadow: 0 2px 10px rgba(0,0,0,0.05); +} + +.stat__card h3 { + font-size: 14px; + color: #666; + margin: 0 0 10px 0; + font-weight: 500; +} + +.stat__number { + font-size: 2.5em; + font-weight: bold; + color: #901438; + margin: 0; +} + +.stat__number.green { + color: #28a745; +} + +.stat__number.orange { + color: #fd7e14; +} + +.stat__number.red { + color: #dc3545; +} + +.stat__number.blue { + color: #007bff; +} + +/* Tables */ +.admin__table { + background: #fff; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 10px rgba(0,0,0,0.05); +} + +.admin__table table { + margin: 0; +} + +.admin__table th { + background: #f8f9fa; + font-weight: 600; + font-size: 13px; + color: #666; + text-transform: uppercase; +} + +.admin__table td { + vertical-align: middle; +} + +.admin__table .uk-icon-link { + margin-right: 8px; + color: #666; +} + +.admin__table .uk-icon-link:hover { + color: #901438; +} + +/* Labels */ +.uk-label { + font-size: 11px; + padding: 3px 8px; + border-radius: 3px; +} + +.uk-label-success { + background: #28a745; +} + +.uk-label-danger { + background: #dc3545; +} + +.uk-label-warning { + background: #fd7e14; +} + +/* Filter Form */ +.filter__form { + background: #fff; + padding: 20px; + border-radius: 8px; + margin-bottom: 20px; + box-shadow: 0 2px 10px rgba(0,0,0,0.05); +} + +/* Pagination */ +.admin__pagination { + margin-top: 20px; + padding: 20px; + background: #fff; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0,0,0,0.05); +} + +/* Forms */ +.admin__form { + background: #fff; + padding: 30px; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0,0,0,0.05); + max-width: 800px; +} + +.admin__form .uk-form-label { + font-weight: 600; + color: #333; +} + +/* Alerts */ +.admin__content .uk-alert { + border-radius: 5px; +} + +/* Action Buttons */ +.action__buttons { + display: flex; + gap: 10px; + margin-bottom: 20px; +} + +/* Header with actions */ +.page__header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 25px; +} + +.page__header h1 { + margin: 0; +} + +/* Post Title Link */ +.post__title__link { + color: #333; + text-decoration: none; +} + +.post__title__link:hover { + color: #901438; +} + +/* Responsive */ +@media (max-width: 960px) { + .admin__sidebar { + width: 70px; + padding: 15px 10px; + } + + .admin__sidebar .admin__logo img { + max-width: 40px; + } + + .admin__sidebar .admin__logo span, + .admin__menu li a span:not(.uk-icon), + .admin__user .user__name { + display: none; + } + + .admin__menu li a { + justify-content: center; + padding: 12px; + } + + .admin__menu li a span.uk-icon { + margin-right: 0; + } + + .admin__user a { + display: block; + text-align: center; + margin: 5px 0; + } + + .admin__content { + margin-left: 70px; + padding: 20px; + } +} + +@media (max-width: 640px) { + .page__header { + flex-direction: column; + align-items: flex-start; + gap: 15px; + } + + .filter__form .uk-grid > div { + margin-bottom: 10px; + } +} diff --git a/templates/admin/base_admin.html b/templates/admin/base_admin.html new file mode 100644 index 0000000..e1fd69a --- /dev/null +++ b/templates/admin/base_admin.html @@ -0,0 +1,74 @@ + + + + WIXON Blog - Admin + + + + + + + + + + + + {% block staticfiles %}{% endblock %} + + +
+ + + + +
+ {% with messages = get_flashed_messages() %} + {% if messages %} +
+ + {{ messages[0] }} +
+ {% endif %} + {% endwith %} + + {% block content %}{% endblock %} +
+
+ + {% block scripts %}{% endblock %} + + diff --git a/templates/admin/dashboard.html b/templates/admin/dashboard.html new file mode 100644 index 0000000..fcea632 --- /dev/null +++ b/templates/admin/dashboard.html @@ -0,0 +1,70 @@ +{% extends 'admin/base_admin.html' %} + +{% block content %} +

대시보드

+ + +
+
+
+

총 포스트

+

{{ stats.total_posts }}

+
+
+
+
+

공개 포스트

+

{{ stats.public_posts }}

+
+
+
+
+

비공개 포스트

+

{{ stats.private_posts }}

+
+
+
+
+

총 회원 수

+

{{ stats.total_members }}

+
+
+
+ + +{% if stats.deleted_posts > 0 %} +
+ +

+ + 삭제된 포스트 {{ stats.deleted_posts }}개가 있습니다. + 삭제된 포스트 보기 +

+
+{% endif %} + + + +{% endblock %} diff --git a/templates/admin/member_form.html b/templates/admin/member_form.html new file mode 100644 index 0000000..ba57af3 --- /dev/null +++ b/templates/admin/member_form.html @@ -0,0 +1,108 @@ +{% extends 'admin/base_admin.html' %} + +{% block content %} + + +
+
+ +
+ +
+ {% if member %} + +

아이디는 변경할 수 없습니다.

+ {% else %} + + {% endif %} +
+
+ +
+ +
+ +
+
+ + {% if not member %} +
+ +
+ +
+
+ {% else %} +
+ +
+ +
+
+ {% endif %} + +
+ +
+ + 취소 +
+
+
+{% endblock %} + +{% block scripts %} +{% if member %} + +{% endif %} +{% endblock %} diff --git a/templates/admin/members.html b/templates/admin/members.html new file mode 100644 index 0000000..19ec1eb --- /dev/null +++ b/templates/admin/members.html @@ -0,0 +1,139 @@ +{% extends 'admin/base_admin.html' %} + +{% block content %} + + + +
+ + + + + + + + + + + {% if members %} + {% for member in members %} + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
번호아이디이름관리
{{ member.mb_idx }} + {{ member.mb_id }} + {% if member.mb_id in ['admin', 'wixon'] %} + 관리자 + {% endif %} + {{ member.mb_name }} + + 수정 + + + {% if member.mb_id not in ['admin', 'wixon'] %} + + {% endif %} +
+ 등록된 회원이 없습니다. +
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/admin/post_detail.html b/templates/admin/post_detail.html new file mode 100644 index 0000000..d54e0b8 --- /dev/null +++ b/templates/admin/post_detail.html @@ -0,0 +1,122 @@ +{% extends 'admin/base_admin.html' %} + +{% block content %} + + +
+
+
+ +
+ +
+
+ +
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+ {{ post.mb_name }} ({{ post.mb_id }}) +   |   + {{ post.add_date.strftime('%Y-%m-%d %H:%M') }} +
+
+
+ +
+ +
+ +
+
+ +
+ + 취소 +
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/admin/posts.html b/templates/admin/posts.html new file mode 100644 index 0000000..0483ede --- /dev/null +++ b/templates/admin/posts.html @@ -0,0 +1,222 @@ +{% extends 'admin/base_admin.html' %} + +{% block content %} + + + +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + 초기화 +
+
+
+
+ + +
+ + + + + + + + + + + + + + + {% if posts %} + {% for post in posts %} + + + + + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
ID제목카테고리작성자공개상태작성일관리
{{ post.id }} + + {{ post.title[:50] }}{% if post.title|length > 50 %}...{% endif %} + + {{ post.category }}{{ post.mb_name }} + {% if post.public_yn == 'Y' %} + 공개 + {% else %} + 비공개 + {% endif %} + + {% if post.use_yn == 'Y' %} + 사용중 + {% else %} + 삭제됨 + {% endif %} + {{ post.add_date.strftime('%Y-%m-%d') }} + + + {% if post.use_yn == 'Y' %} + + {% else %} + + {% endif %} +
+ 포스트가 없습니다. +
+
+ + +{% if pagination.total > 0 %} +
+
+
+ 총 {{ pagination.total }}개 중 {{ ((pagination.page - 1) * pagination.per_page) + 1 }} - {{ [pagination.page * pagination.per_page, pagination.total]|min }}개 표시 +
+
    + {% if pagination.has_prev %} +
  • + + + +
  • + {% endif %} + + {% for page_num in range(1, pagination.total_pages + 1) %} + {% if page_num == pagination.page %} +
  • {{ page_num }}
  • + {% elif page_num == 1 or page_num == pagination.total_pages or (page_num >= pagination.page - 2 and page_num <= pagination.page + 2) %} +
  • + + {{ page_num }} + +
  • + {% elif page_num == pagination.page - 3 or page_num == pagination.page + 3 %} +
  • ...
  • + {% endif %} + {% endfor %} + + {% if pagination.has_next %} +
  • + + + +
  • + {% endif %} +
+
+
+{% endif %} +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/base.html b/templates/base.html index 75a3b07..4893600 100644 --- a/templates/base.html +++ b/templates/base.html @@ -42,7 +42,7 @@