USER
i get this error when trying to follow someone:19:192 Uncaught TypeError: Cannot read properties of null (reading 'classList')
at follow (19:192:39)
at HTMLButtonElement.onclick (19:183:132)
this is video.html:
{% extends "base.html" %}
{% block content %}
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<div class="container mt-4">
<div class="row">
<!-- Related Videos Section -->
<div class="col-md-3 order-md-2"> <!-- Left Side for Related Videos -->
<h5>Related Videos</h5>
{% for vid in last_videos %}
<div class="related-video mb-3">
<a href="/video/{{ vid.id }}" class="text-decoration-none text-dark">
<img src="/static/pre/{{ vid.id }}.jpg" class="img-fluid mb-1">
<p class="mt-3">{{ vid.title }}</p>
</a>
</div>
{% endfor %}
</div>
<!-- Main Video Section -->
<div class="col-md-9 order-md-1"> <!-- Right Side for Main Video -->
<div class="video-container paused" data-volume-level="high">
<img class="thumbnail-img">
<div class="video-controls-container">
<!-- Control elements -->
</div>
<video width="100%" autoplay src="{{ video_src }}" controls autoplay>
<track kind="captions" srclang="en" src="assets/subtitles.vtt">
</video>
</div>
<!-- Video Title and Details -->
<h3 class="mt-3">{{ current_video.title }}</h3>
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header">
<img src="..." class="rounded me-2" alt="...">
<strong class="me-auto">Писька</strong>
<small>10 метров от вас</small>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
Я иду к тебе мой пупсик
</div>
</div>
<div class="d-flex align-items-center mb-3">
<img src="{{ avatar_url }}" alt="Avatar" class="img-fluid rounded-circle me-2" style="width:90px; height:90px;">
<div>
<a href="/user/{{ author.id }}" class="text-decoration-none text-dark">{{ author.name }} {{ author.surname }} </a>
<div>
<button onclick="follow()" class="btn btn-outline-danger btn-sm {% if subscription %} active {% endif %}" type="button" name="follow" id="follow" {% if not authenticated or current_user.id == current_video.author %} disabled {% endif %}>Подписаться
</button>
<script>
function follow(userId) {
const followBtn = document.getElementById('follow-btn');
const isFollowing = followBtn.classList.contains('active');
// Determine the action based on current state
const action = isFollowing ? 'unfollow' : 'follow';
fetch(`/api/${action}/${userId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
if (data.success) {
followBtn.classList.toggle('active');
followBtn.textContent = action === 'follow' ? 'Unfollow' : 'Follow';
} else {
console.error(data.message);
}
})
.catch(error => console.error('Error:', error));
}
// Add this function call in the button
document.getElementById('follow-btn').addEventListener('click', function() {
const userId = {{ author.id }}; // Define the appropriate user ID from your template context
follow(userId);
});
</script>
</div>
</div>
</div>
<p style="color: #808080">{{ current_video.description }}</p>
{% if authenticated and (current_user.id |int == current_video.author |int or current_user.id == 1) %}
<a href="{{ url_for('edit_video', video_id=current_video.id) }}" class="btn btn-warning">Edit Video</a>
<form action="{{ url_for('delete_video', video_id=current_video.id) }}" style="display: inline;">
<button type="submit" class="btn btn-danger">Delete Video</button>
</form>
{% endif %}
<button id="shareBtn" class="btn btn-primary me-2">Поделиться</button>
<script>
// Share Functionality
document.getElementById('shareBtn').addEventListener('click', () => {
const currentURL = window.location.href;
// Create a temporary input element to hold the URL
const tempInput = document.createElement('input');
tempInput.value = currentURL;
document.body.appendChild(tempInput);
// Select and copy the URL from the temporary input element
tempInput.select();
document.execCommand('copy'); // Legacy method for compatibility
// Remove the temporary input element from the DOM
document.body.removeChild(tempInput);
// Show a confirmation alert
alert('Ссылка скопирована!');
});
// You can add other JavaScript functions here, like liking comments or editing them.
</script>
<script>
function like(videoId) {
fetch(`/api/like/${videoId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
const likesSpan = document.getElementById('likes');
likesSpan.innerText = data.new_like_count;
} else {
console.error('Failed to like the video:', data.message);
}
})
.catch(error => console.error('Error:', error));
}
</script>
<script>
function likeComment(commentId) {
fetch(`/api/like_comment/${commentId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({}), // Optionally send payload if needed
})
.then(response => response.json())
.then(data => {
if (data.success) {
const likeBtn = document.getElementById(`like-btn-${commentId}`);
const likesCount = document.getElementById(`comment-likes-${commentId}`);
likesCount.innerText = data.new_like_count;
likeBtn.classList.toggle('active');
} else {
console.error(data.message);
}
})
.catch(error => console.error('Error:', error));
}
</script>
<!-- Comments Section -->
<button onclick="like({{ current_video.id }})" class="btn btn-outline-danger btn-sm" id="like-btn">
<svg width="20px" height="20px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#000000" stroke-width="1" stroke-linecap="round" stroke-linejoin="miter"><polygon points="7 9 11 2 14 2 13 9 22 9 20 22 7 22 7 9"></polygon><rect x="2" y="9" width="5" height="13"></rect></svg>
<span id="likes">{{ current_video.likes }} лайков</span>
</button>
<div class="comments-section mt-5">
<h4>Comments</h4>
{% if authenticated and not current_user.is_blocked %}
<form action="" method="post" id="comment-form">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.content(class="form-control", placeholder="Ваш комментарий...") }}
{{ form.parent_id(value=0) }}
{% for error in form.content.errors %}
<div class="alert alert-danger" role="alert">
{{ error }}
</div>
{% endfor %}
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
{% elif authenticated and current_user.is_blocked %}
<p>Your account is blocked, you cannot comment.</p>
{% else %}
<p>Вы должны <a href="{{ url_for('login') }}">Войти</a> или <a href="{{ url_for('register') }}">Зарегистрироваться</a> чтобы писать комментарии.</p>
{% endif %}
<!-- Comments List -->
{% for comment in comments %}
<div class="media mb-3">
<img src="{{ url_for('static', filename='avatars/' ~ comment.user.avatar) }}" alt="Avatar" class="mr-3 rounded-circle" style="width:50px; height:50px;">
<div class="media-body">
<h5 class="mt-0">
<a href="{{ url_for('user', user_id=comment.user.id) }}" class="text-decoration-none text-dark">{{ comment.user.name }} {{ comment.user.surname }}</a>
</h5>
<p>{{ comment.content }}</p>
<small class="text-muted">{{ comment.datetime.strftime('%Y-%m-%d %H:%M') }}</small>
<!-- Like Button -->
<div class="d-flex mt-2">
<button onclick="likeComment({{ comment.id }})" class="btn btn-sm btn-outline-primary me-2" id="like-btn-{{ comment.id }}">
Like <span id="comment-likes-{{ comment.id }}">{{ comment.likes }}</span>
</button>
<!-- Edit and Delete Buttons for Author -->
{% if current_user.is_authenticated and current_user.id == comment.user_id %}
<a href="{{ url_for('edit_comment', comment_id=comment.id) }}" class="btn btn-sm btn-outline-warning me-2">Edit</a>
<form action="{{ url_for('delete_comment', comment_id=comment.id) }}" method="post" style="display: inline;">
<button type="submit" class="btn btn-sm btn-outline-danger">Delete</button>
</form>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
<script>
// JavaScript for actions such as copy, like, follow
document.getElementById('copyBtn').addEventListener('click', () => {
const currentURL = window.location.href;
const tempInput = document.createElement('input');
tempInput.value = currentURL;
document.body.appendChild(tempInput);
tempInput.select();
document.execCommand('copy');
document.body.removeChild(tempInput);
alert('Ссылка скопирована!');
});
// Follow and like function implementations
</script>
<style>
.video-container {
width: 100%; /* Ensure full width within column */
}
.related-video img {
width: 100%;
border-radius: 4px;
}
</style>
{% endblock %}
this is app.py:
import os
import json
from passlib.context import CryptContext
from PIL import Image
from flask import Flask, render_template, url_for, redirect, request, send_from_directory, flash
from flask_login import LoginManager, login_user, current_user, logout_user
from flask_restful import Api
from data import db_session
from data.videos import Video
from data.users import User
from sqlalchemy.orm import joinedload
from data.login_form import LoginForm
from data.register_form import RegisterForm
from data.video_form import VideoForm
from data.users_resource import UsersResource, UserListResource
from data.videos_resource import VideosResource, VideoListResource
from data.likes_resource import LikeResource, NotLikeResource
from data.subscriptions_resource import FollowResource, NotFollowResource
from werkzeug.utils import secure_filename
from data.avatar_form import AvatarForm
from data.comments import Comment
from data.comment_form import CommentForm
app = Flask(__name__)
app.config['SECRET_KEY'] = 'yandexlyceum_secret_key'
api = Api(app)
login_manager = LoginManager()
login_manager.init_app(app)
pwd_context = CryptContext(
schemes=["pbkdf2_sha256"],
default="pbkdf2_sha256",
pbkdf2_sha256__default_rounds=30000
)
# Set the folder to store avatars
AVATAR_UPLOAD_FOLDER = os.path.join(app.root_path, 'static', 'avatars')
# Allowed extensions
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
app.config['AVATAR_UPLOAD_FOLDER'] = AVATAR_UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 2MB max upload sizes
@app.route('/favicon.ico')
def favicon():
"""Возвращает кастомную иконку страницы в браузере"""
return send_from_directory(os.path.join(app.root_path, 'static/img'),
'vhs.ico', mimetype='image/vnd.microsoft.icon')
def load_user(user_id):
"""Loads the user by ID."""
return db_sess.query(User).get(user_id)
@app.route('/upload_avatar', methods=['GET', 'POST'])
@app.route('/upload_avatar', methods=['POST'])
def upload_avatar():
"""Allows authenticated users to upload and customize their avatar."""
form = AvatarForm()
if request.method == 'POST':
# Handling form submission and image saving
if 'avatar' in request.files:
avatar = request.files['avatar']
filename = secure_filename(avatar.filename)
file_ext = os.path.splitext(filename)[1].lower()
avatar_filename = f"user_{current_user.id}{file_ext}"
avatar_path = os.path.join(app.config['AVATAR_UPLOAD_FOLDER'], avatar_filename)
# Save and process the avatar
try:
with Image.open(avatar) as img:
img = img.convert('RGB')
img.thumbnail((300, 300))
img.save(avatar_path, optimize=True, quality=85)
# Update user's avatar in the database
current_user.avatar = avatar_filename
db_sess.commit()
flash("Аватар успешно обновлен!", "success")
return redirect(url_for('user', user_id=current_user.id))
except Exception as e:
flash('Загрузка не удалась. Пожалуйста, попробуйте снова.', 'danger')
return redirect(url_for('upload_avatar'))
return render_template('upload_avatar.html', form=form)
@app.errorhandler(403)
def forbidden(e):
"""Возвращает кастомную страницу ошибки 403"""
params = {
'title': 'Доступ запрещен',
'message': 'Похоже, у вас нет доступа к этой странице ¯\\_(ツ)_/¯',
'authenticated': current_user.is_authenticated,
'current_user': current_user
}
return render_template('error1.html', **params), 403
def to_int(value):
try:
return int(value)
except ValueError:
return 0 # or handle as needed
app.jinja_env.filters['to_int'] = to_int
@app.errorhandler(404)
def page_not_found(e):
"""Возвращает кастомную страницу ошибки 404"""
params = {
'title': 'Страница не найдена',
'message': 'Похоже, мы не можем найти нужную вам страницу ¯\\_(ツ)_/¯',
'authenticated': current_user.is_authenticated,
'current_user': current_user
}
return render_template('error2.html', **params), 404
@app.errorhandler(405)
def method_not_allowed(e):
"""Возвращает кастомную страницу ошибки 405"""
params = {
'title': 'Метод не разрешен',
'message': 'Похоже, этот метод не разрешен ¯\\_(ツ)_/¯',
'authenticated': current_user.is_authenticated,
'current_user': current_user
}
return render_template('error3.html', **params), 405
@app.errorhandler(500)
def internal_server_error(e):
"""Возвращает кастомную страницу ошибки 500"""
params = {
'title': 'Ошибка сервера',
'message': 'Похоже, на сервере произошла непредвиденная ошибка ¯\\_(ツ)_/¯',
'authenticated': current_user.is_authenticated,
'current_user': current_user
}
return render_template('error4.html', **params), 500
@app.route('/')
@app.route('/index')
def index():
"""Возвращает домашнюю страницу"""
all_videos = db_sess.query(Video).all()
if len(all_videos) > 10:
last_videos = all_videos[len(all_videos) - 10:][::-1]
else:
last_videos = all_videos[::-1]
best_videos = sorted(all_videos, key=lambda x: -x.likes)[:3]
underrated_videos = list(filter(lambda x: x.likes == 0, all_videos))
if len(underrated_videos) > 9:
underrated_videos = underrated_videos[len(underrated_videos) - 9:][::-1]
else:
underrated_videos = underrated_videos[::-1]
params = {
'title': 'Video Hosting Service',
'last_videos': last_videos,
'best_videos': best_videos,
'underrated_videos': underrated_videos,
'user': user,
'db_sess': db_sess,
'User': User,
'Video': Video,
'authenticated': current_user.is_authenticated,
'all_videos': all_videos,
'current_user': current_user
}
return render_template('index.html', **params)
@login_manager.user_loader
def load_user(user_id):
"""Возвращает пользователя"""
return db_sess.query(User).get(user_id)
@app.route('/login', methods=['GET', 'POST'])
def login():
"""Возвращает страницу авторизации и авторизует пользователя"""
if current_user.is_authenticated:
return forbidden('')
form = LoginForm()
params = {
'title': 'Авторизация',
'authenticated': current_user.is_authenticated,
'current_user': current_user
}
if form.validate_on_submit():
user = db_sess.query(User).filter(User.login == form.login.data).first()
if user and pwd_context.verify(form.password.data, user.password):
login_user(user, remember=form.remember_me.data)
return redirect('/')
return render_template('login.html',
message='Неправильный логин или пароль',
form=form, **params)
return render_template('login.html', form=form, **params)
from flask import render_template, request, redirect, url_for, flash
from flask_login import login_required, current_user
@app.route('/edit_comment/<int:comment_id>', methods=['GET', 'POST'])
@login_required
def edit_comment(comment_id):
# Fetch the comment from the database using comment_id
comment = db_sess.query(Comment).filter(Comment.id == comment_id).first()
# Ensure the comment exists and the current_user is the author
if comment is None or comment.user_id != current_user.id:
flash('Comment not found or you do not have permission to edit it.', 'danger')
return redirect(url_for('video', video_id=comment.video_id)) # Redirect to the video's page
if request.method == 'POST':
new_content = request.form.get('content')
if new_content:
comment.content = new_content
db_sess.commit()
flash('Comment updated successfully!', 'success')
else:
flash('No content provided for update.', 'danger')
return redirect(url_for('video', video_id=comment.video_id))
return render_template('edit_comment.html', comment=comment)
@app.route('/register', methods=['GET', 'POST'])
def register():
"""Возвращает страницу регистрации и регистрирует пользователя"""
if current_user.is_authenticated:
return forbidden('')
form = RegisterForm()
params = {
'title': 'Регистрация',
'authenticated': current_user.is_authenticated,
'current_user': current_user
}
if form.validate_on_submit():
user = db_sess.query(User).filter(User.login == form.login.data).all()
if user:
return render_template('register.html',
message='Пользователь с таким логином уже существует',
form=form, **params)
if form.password.data != form.repeat_password.data:
return render_template('register.html',
message='Пароли не совпадают',
form=form, **params)
user = User()
user.login = form.login.data
user.password = pwd_context.hash(form.password.data)
user.name = form.name.data
user.surname = form.surname.data
db_sess.add(user)
db_sess.commit()
login_user(user, remember=form.remember_me.data)
return redirect('/')
return render_template('register.html', form=form, **params)
from flask import jsonify, request, current_app
@app.route('/api/like_comment/<int:comment_id>', methods=['POST']) # Ensure the user is logged in
def like_comment(comment_id):
# Logic to handle liking a comment
comment = db_sess.query(Comment).filter_by(id=comment_id).first()
if not comment:
return jsonify({'success': False, 'message': 'Comment not found.'}), 404
# Example logic: increment the like count
comment.likes += 1
db_sess.commit()
return jsonify({'success': True, 'new_like_count': comment.likes})
@app.route('/logout')
def logout():
"""
Производит выход пользователя из системы, возвращает домашнюю страницу.
Если пользователь не авторизован, возвращает страницу ошибки 403
"""
if current_user.is_authenticated:
logout_user()
return redirect('/')
else:
return forbidden('')
@app.route('/api/pin_comment/<int:comment_id>', methods=['POST'])
def pin_comment(comment_id):
comment = db_sess.query(Comment).get(comment_id)
if not comment:
return {'error': 'Comment not found'}, 404
video = db_sess.query(Video).get(comment.video_id)
# Сбрасываем все закрепленные комментарии для данного видео
db_sess.query(Comment).filter(Comment.video_id == video.id).update({'pinned': False})
# Закрепляем выбранный комментарий
comment.pinned = True
db_sess.commit()
return {'success': True}
@app.route('/delete_comment/<int:comment_id>', methods=['POST'])
def delete_comment(comment_id):
comment = db_sess.query(Comment).get(comment_id)
if not comment:
flash('Комментарий не найден.', 'danger')
return redirect(request.referrer)
if not current_user.is_authenticated or current_user.id != comment.user_id:
flash('Вы не можете удалить этот комментарий.', 'danger')
return redirect(request.referrer)
db_sess.delete(comment)
db_sess.commit()
flash('Комментарий удален.', 'success')
return redirect(request.referrer)
from flask import abort
from data.video_views import VideoView
@app.route('/video/<int:video_id>', methods=['GET', 'POST'])
def video(video_id):
current_video = db_sess.query(Video).get(video_id)
if current_video is None:
return page_not_found('')
# Проверяем, разрешены ли комментарии для этого видео
comments_enabled = current_video.comments_enabled
# Получаем автора видео
author = db_sess.query(User).get(current_video.author)
all_videos = db_sess.query(Video).all()
# Проверяем, поставил ли текущий пользователь лайк и подписан ли
like = False
subscription = False
# Check if the user is logged in
if current_user.is_authenticated:
# Check if the user has already viewed the video
view_exists = db_sess.query(VideoView).filter_by(
user_id=current_user.id, video_id=video_id).first()
# If not, increment the view count and add a record to video_views
if not view_exists:
current_video.views += 1
new_view = VideoView(user_id=current_user.id, video_id=video_id)
db_sess.add(new_view)
db_sess.commit()
if current_user.is_authenticated:
likes = json.loads(current_user.likes)
subscriptions = json.loads(current_user.subscriptions)
if video_id in likes:
like = True
if current_video.author in subscriptions:
subscription = True
# Обрабатываем форму комментария
# Обрабатываем форму комментария только если комментарии включены
form = CommentForm()
if comments_enabled and form.validate_on_submit():
if current_user.is_authenticated and not current_user.is_blocked:
# Добавление комментария
comment = Comment(
content=form.content.data,
user_id=current_user.id,
video_id=video_id,
parent_id=form.parent_id.data or None
)
db_sess.add(comment)
db_sess.commit()
flash('Комментарий добавлен!', 'success')
return redirect(url_for('video', video_id=video_id))
else:
flash('Вы не можете оставить комментарий.', 'danger')
return redirect
# Получаем комментарии к видео
comments = db_sess.query(Comment).filter(Comment.video_id == video_id).options(
joinedload(Comment.user),
joinedload(Comment.parent).joinedload(Comment.user),
joinedload(Comment.replies).joinedload(Comment.user)
).order_by(Comment.datetime.desc()).all() # Изменено на desc() для сортировки в обратном порядке
# Создаем словарь комментариев по id для удобства доступа
comments_by_id = {comment.id: comment for comment in comments}
# Строим дерево комментариев
root_comments = []
for comment in comments:
if comment.parent_id is None:
root_comments.append(comment)
else:
parent = comments_by_id.get(comment.parent_id)
if parent:
if not hasattr(parent, 'replies_sorted'):
parent.replies_sorted = []
parent.replies_sorted.append(comment)
last_videos = all_videos[len(all_videos) - 10:][::-1]
params = {
'title': f"{current_video.title} - {author.name} {author.surname}",
'current_video': current_video,
'video_src': url_for("static", filename=f'vid/{video_id}.mp4'),
'author': author,
'like': like,
'subscription': subscription,
'db_sess': db_sess,
'comments': root_comments,
'User': User,
'authenticated': current_user.is_authenticated,
'current_user': current_user,
'avatar_url': url_for('static', filename=f'avatars/{author.avatar}'),
'comments': comments,
'last_videos': last_videos,
'form': form
}
return render_template('video.html', **params)
@app.route('/add_video', methods=['GET', 'POST'])
def add_video():
if not current_user.is_authenticated:
return forbidden('')
form = VideoForm()
params = {
'title': 'Добавление видео',
'authenticated': current_user.is_authenticated,
'current_user': current_user
}
# Добавляем проверку на блокировку
if current_user.is_blocked:
flash('Ваш аккаунт заблокирован и вы не можете загружать видео.', 'danger')
return render_template('add_video.html', form=form, **params)
if form.validate_on_submit():
video_file = request.files['video']
if not video_file.filename.lower().endswith('.mp4'):
return render_template('add_video.html',
message='Данный формат видео не поддерживается. '
'Пожалуйста, загрузите видео в формате mp4.',
form=form, **params)
preview_file = request.files['preview']
if not preview_file.filename.lower().endswith('.jpg'):
return render_template('add_video.html',
message='Данный формат обложки не поддерживается. '
'Пожалуйста, загрузите изображение в формате jpg.',
form=form, **params)
video = Video()
video.title = form.title.data
video.description = '<br>'.join(form.description.data.split('\n'))
video.author = current_user.id
video.comments_enabled = form.comments_enabled.data # Сохраняем значение флажка
db_sess.add(video)
db_sess.commit()
video_file.save(os.path.join('static/vid', f'{video.id}.mp4'))
preview_file.save(os.path.join('static/pre', f'{video.id}.jpg'))
with Image.open(f'static/pre/{video.id}.jpg') as preview_file:
width, height = preview_file.size
if width >= height:
new_width = height * 16 // 9
delta = (width - new_width) // 2
preview_file = preview_file.crop((delta, 0, new_width + delta, height))
else:
new_height = width * 9 // 16
delta = (height - new_height) // 2
preview_file = preview_file.crop((0, delta, width, new_height + delta))
preview_file.save(f'static/pre/{video.id}.jpg')
return redirect('/')
return render_template('add_video.html', form=form, **params)
@app.route('/edit_video/<int:video_id>', methods=['GET', 'POST'])
def edit_video(video_id):
"""
Возвращает страницу редактирования видео
если пользователь авторизован и является автором,
иначе доступ запрщен
"""
form = VideoForm()
video = db_sess.query(Video).get(video_id)
if not current_user.is_authenticated:
return forbidden('')
if not current_user.id != video.author:
return forbidden('')
params = {
'title': 'Редактирование видео',
'video': video,
'description': '\n'.join(video.description.split('<br>')),
'authenticated': current_user.is_authenticated,
'current_user': current_user
}
if form.validate_on_submit():
video_file = request.files['video']
if not video_file.filename.lower().endswith('.mp4'):
return render_template('edit_video.html',
message='Данный формат видео не поддерживается',
form=form, **params)
preview_file = request.files['preview']
if not preview_file.filename.lower().endswith('.jpg'):
return render_template('edit_video.html',
message='Данный формат обложки не поддерживается',
form=form, **params)
video.title = form.title.data
video.description = '<br>'.join(form.description.data.split('\n'))
db_sess.commit()
video_file.save(os.path.join('static/vid', f'{video.id}.mp4'))
preview_file.save(os.path.join('static/pre', f'{video.id}.jpg'))
with Image.open(f'static/pre/{video.id}.jpg') as preview_file:
width, height = preview_file.size
if width >= height:
new_width = height * 16 // 9
delta = (width - new_width) // 2
preview_file = preview_file.crop((delta, 0, new_width + delta, height))
else:
new_height = width * 9 // 16
delta = (height - new_height) // 2
preview_file = preview_file.crop((0, delta, width, new_height + delta))
preview_file.save(f'static/pre/{video.id}.jpg')
return redirect('/')
return render_template('edit_video.html', form=form, **params)
@app.route('/delete_video/<int:video_id>')
def delete_video(video_id):
video = db_sess.query(Video).get(video_id)
if not video:
return page_not_found('Video not found.')
# No need for explicit comment deletion; should cascade
db_sess.delete(video)
db_sess.commit()
# Remove video file and preview
try:
os.remove(os.path.join('static/vid', f'{video.id}.mp4'))
os.remove(os.path.join('static/pre', f'{video.id}.jpg'))
except FileNotFoundError:
pass # Handle if the files do not exist
return redirect(f'/user/{current_user.id}')
@app.route('/user/<int:user_id>')
def user(user_id):
"""Возвращает страницу выбранного пользователя с его видео"""
user = db_sess.query(User).get(user_id)
if user is None:
return page_not_found('')
# Получаем все видео пользователя
videos = db_sess.query(Video).filter(Video.author == user_id).all()[::-1]
# Вычисляем количество видео
video_count = len(videos)
# Альтернативно, для оптимизации можно использовать:
# video_count = db_sess.query(Video).filter(Video.author == user_id).count()
subscription = False
if current_user.is_authenticated:
subscriptions = json.loads(current_user.subscriptions)
if user_id in subscriptions:
subscription = True
# Пагинация видео (разбивка на группы по 3 видео)
paginated_videos = [videos[i * 3:i * 3 + 3] for i in range(len(videos) // 3)] + [videos[(len(videos) // 3) * 3:]]
# Добавляем все необходимые параметры, включая video_count
params = {
'title': f'{user.name} {user.surname}',
'user': user,
'videos': paginated_videos,
'subscription': subscription,
'empty': True if len(videos) == 0 else False,
'authenticated': current_user.is_authenticated,
'current_user': current_user,
'video_count': video_count # Передаём количество видео
}
return render_template('user.html', **params)
def decline_subscription(number):
"""
Возвращает правильную форму слова "подписчик" в зависимости от числа.
Например:
1 -> подписчик
2 -> подписчика
5 -> подписчиков
"""
number = abs(number) # Убираем знак, если число отрицательное
last_two = number % 100
last = number % 10
if 11 <= last_two <= 14:
return "подписчиков"
if last == 1:
return "подписчик"
if 2 <= last <= 4:
return "подписчика"
return "подписчиков"
def decline_views(number):
"""
Возвращает правильную форму слова "подписчик" в зависимости от числа.
Например:
1 -> подписчик
2 -> подписчика
5 -> подписчиков
"""
number = abs(number) # Убираем знак, если число отрицательное
last_two = number % 100
last = number % 10
if 11 <= last_two <= 14:
return "просмотров"
if last == 1:
return "просмотр"
if 2 <= last <= 4:
return "просмотра"
return "посмотров"
# Регистрация фильтра в Jinja2
app.jinja_env.filters['decline_subscription'] = decline_subscription
app.jinja_env.filters['decline_views'] = decline_views
from data.users import User
from sqlalchemy import update
@app.route('/admin/users', methods=['GET', 'POST'])
def admin_users():
"""Административная страница для управления флажками, блокировками и статусом админа пользователей"""
if not current_user.is_authenticated or not current_user.is_admin:
return forbidden('') # Только администратор имеет доступ
users = db_sess.query(User).all()
if request.method == 'POST':
# Обработка изменений флажков, блокировок и статуса админа
for user in users:
if user.id == 1:
continue # Не изменять статус администратора
# Обработка флажков is_checked
is_checked = request.form.get(f'checked_{user.id}', 'off') == 'on'
if user.is_checked != is_checked:
user.is_checked = is_checked
db_sess.add(user)
# Обработка блокировок is_blocked
is_blocked = request.form.get(f'blocked_{user.id}', 'off') == 'on'
if user.is_blocked != is_blocked:
user.is_blocked = is_blocked
db_sess.add(user)
# Обработка статуса администратора is_admin
is_admin = request.form.get(f'is_admin_{user.id}', 'off') == 'on'
if user.is_admin != is_admin:
user.is_admin = is_admin
db_sess.add(user)
db_sess.commit()
flash('Изменения успешно сохранены.', 'success')
return redirect(url_for('admin_users'))
params = {
'title': 'Администрирование пользователей',
'users': users,
'authenticated': current_user.is_authenticated,
'current_user': current_user
}
return render_template('admin_users.html', **params)
@app.route('/feed')
def feed():
"""
Возвращает страницу с лентой видео от людей,
на которых подписан пользователь.
Если пользователь не авторизован, доступ запрещен
"""
if not current_user.is_authenticated:
return forbidden('')
subscriptions = json.loads(current_user.subscriptions)
videos = []
for i in subscriptions:
videos += db_sess.query(Video).filter(Video.author == i).all()[::-1][:12]
new_videos = sorted(videos, key=lambda x: x.datetime)[::-1]
new_videos = [
{
'video': i,
'author': db_sess.query(User).get(i.author)
}
for i in new_videos
]
params = {
'title': f'Лента',
'new_videos': [new_videos[i * 3:i * 3 + 3] for i in range(len(new_videos) // 3)] +
[new_videos[(len(new_videos) // 3) * 3:]],
'empty': True if len(new_videos) == 0 else False,
'subscripted': True if len(subscriptions) != 0 else False,
'authenticated': current_user.is_authenticated,
'current_user': current_user
}
return render_template('feed.html', **params)
@app.route('/favorite')
def favorite():
"""
Возвращает страницу с любимыми видео пользователя.
Если пользователь не авторизован, доступ запрещен
"""
if not current_user.is_authenticated:
return forbidden('')
likes = json.loads(current_user.likes)[::-1]
liked_videos = [
{
'video': video,
'author': db_sess.query(User).get(video.author) if video.author else None
}
for i in likes
if (video := db_sess.query(Video).get(i)) is not None
]
params = {
'title': f'Любимое',
'liked_videos': [liked_videos[i * 3:i * 3 + 3] for i in range(len(liked_videos) // 3)] +
[liked_videos[(len(liked_videos) // 3) * 3:]],
'empty': True if len(liked_videos) == 0 else False,
'authenticated': current_user.is_authenticated,
'current_user': current_user
}
return render_template('favorite.html', **params)
@app.route('/people')
def people():
"""
Возвращает страницу с пользователями, на которых подписан пользователь.
Если пользователь не авторизован, доступ запрещен
"""
if not current_user.is_authenticated:
return forbidden('')
subscriptions = json.loads(current_user.subscriptions)
subscripted_users = [
{
'user': db_sess.query(User).get(i)
}
for i in subscriptions
]
subscripted_users.sort(key=lambda x: x['user'].name + x['user'].surname + x['user'].login)
params = {
'title': f'Люди',
'subscripted_users': subscripted_users,
'empty': True if len(subscripted_users) == 0 else False,
'authenticated': current_user.is_authenticated,
'current_user': current_user
}
return render_template('people.html', **params)
from data.edit_profile_form import EditProfileForm # Импортируем форму
@app.route('/edit_profile', methods=['GET', 'POST'])
def edit_profile():
if not current_user.is_authenticated:
return forbidden('')
form = EditProfileForm(obj=current_user)
params = {
'title': 'Редактирование профиля',
'authenticated': current_user.is_authenticated,
'current_user': current_user
}
if form.validate_on_submit():
if form.login.data and form.login.data != current_user.login:
existing_user = db_sess.query(User).filter(User.login == form.login.data).first()
if existing_user:
flash('Пользователь с таким логином уже существует.', 'danger')
return render_template('edit_profile.html', form=form, **params)
if form.name.data:
current_user.name = form.name.data
if form.surname.data:
current_user.surname = form.surname.data
if form.password.data:
current_user.password = pwd_context.hash(form.password.data)
current_user.channel_description = form.channel_description.data
db_sess.commit()
flash('Профиль обновлён успешно!', 'success')
return redirect(url_for('user', user_id=current_user.id))
return render_template('edit_profile.html', form=form, **params)
@app.route('/api/comment_like/<int:comment_id>', methods=['POST'])
def comment_like(comment_id):
if not current_user.is_authenticated:
return {'error': 'Unauthorized'}, 401
comment = db_sess.query(Comment).get(comment_id)
if not comment:
return {'error': 'Comment not found'}, 404
likes = json.loads(current_user.comment_likes)
if comment_id in likes:
return {'error': 'Already liked'}, 400
likes.append(comment_id)
current_user.comment_likes = json.dumps(likes)
comment.likes += 1
db_sess.commit()
return {'success': True, 'likes': comment.likes}
@app.route('/api/comment_unlike/<int:comment_id>', methods=['POST'])
def comment_unlike(comment_id):
if not current_user.is_authenticated:
return {'error': 'Unauthorized'}, 401
comment = db_sess.query(Comment).get(comment_id)
if not comment:
return {'error': 'Comment not found'}, 404
likes = json.loads(current_user.comment_likes)
if comment_id not in likes:
return {'error': 'Not liked'}, 400
likes.remove(comment_id)
current_user.comment_likes = json.dumps(likes)
comment.likes -= 1
db_sess.commit()
return {'success': True, 'likes': comment.likes}
@app.route('/about')
def about():
params = {
'title': f'Про сайт'
}
return render_template('about.html', **params)
from flask import request, render_template
from sqlalchemy import or_
from flask import request, render_template
from sqlalchemy import or_
@app.route('/search')
def search():
"""Возвращает страницу с результатами поиска видео"""
query = request.args.get('search', '').strip()
if query:
# Use ilike for case-insensitive search. Ensure database supports case-insensitive collation.
videos = db_sess.query(Video).filter(Video.title.ilike(f'%{query}%')).all()
else:
videos = []
params = {
'title': f'Поиск: {query}',
'query': query,
'videos': videos,
'empty': len(videos) == 0,
'db_sess': db_sess,
'User': User,
'authenticated': current_user.is_authenticated,
'current_user': current_user
}
return render_template('search.html', **params)
from data.db_session import SqlAlchemyBase
db_session.global_init("db/hosting.sql")
from data import __all_models
db_sess = db_session.create_session()
db_sess.rollback
SqlAlchemyBase.metadata.create_all(bind=db_sess.bind)
api.add_resource(UsersResource, '/api/users/<int:user_id>')
api.add_resource(UserListResource, '/api/users')
api.add_resource(VideosResource, '/api/videos/<int:video_id>')
api.add_resource(VideoListResource, '/api/videos')
api.add_resource(LikeResource, '/api/like/<int:video_id>')
api.add_resource(NotLikeResource, '/api/not_like/<int:video_id>')
api.add_resource(FollowResource, '/api/follow/<int:user_id>')
api.add_resource(NotFollowResource, '/api/not_follow/<int:user_id>')
if __name__ == '__main__':
from waitress import serve
app.run(debug=True, port=8080, host='0.0.0.0')