Compare commits

...

11 Commits

19 changed files with 315 additions and 104 deletions

View File

@ -0,0 +1,21 @@
from flask import Blueprint, render_template, request
from app.models.collection import Collection
from app.extensions import db
collections = Blueprint("collections", __name__, url_prefix="/collections", template_folder="templates")
@collections.route("/<int:id>", methods=["DELETE"])
def delete_collection(id: int):
collection: Collection = Collection.query.get_or_404(id)
length = len(Collection.query.all()) - 1
db.session.delete(collection)
db.session.commit()
if length <= 0:
return "<small class=\"form-text\">Здесь ничего нет</small>", 200
return "", 204

View File

@ -0,0 +1,18 @@
<div data-collection-id="{{ collection.id }}" class="card">
<div class="card-header"><h5>{{ collection.name }}</h5></div>
<ul class="list-group list-group-flush">
<li class="list-group-item">Кол-во пользрвателей: {{ collection.users|length }}</li>
</ul>
<form action="/api/collections/{{ collection.id }}" method="DELETE" class="card-footer px-2 pt-2">
<button
type="submit"
hx-delete="/api/collections/{{ collection.id }}"
hx-swap="outerHTML"
hx-target='[data-collection-id="{{ collection.id }}"]'
hx-confirm="Вы уверены, что хотите удалить коллекцию?"
class="btn btn-outline-danger"
>
Удалить
</button>
</form>
</div>

View File

@ -0,0 +1,9 @@
<div id="cards-grid" class="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-4">
{% for collection in collections %}
<div class="col mb-3">
{% include "collections/card.j2" %}
</div>
{% else %}
<small class="form-text">Здесь ничего нет.</small>
{% endfor %}
</div>

View File

@ -3,9 +3,11 @@ from flask import Blueprint
from .tasks.routes import tasks
from .sessions.routes import sessions
from .users.routes import users
from .collections.routes import collections
api = Blueprint("api", __name__, url_prefix="/api")
api.register_blueprint(users)
api.register_blueprint(tasks)
api.register_blueprint(sessions)
api.register_blueprint(sessions)
api.register_blueprint(collections)

View File

@ -41,7 +41,7 @@ async def parse_users(session_id: int):
@users.route("/<int:session_id>", methods=["GET"])
def get_users(session_id: int):
users = Session.query.get_or_404(session_id).users
users = [] #Session.query.get_or_404(session_id)
return render_template("user_cards.j2", users=users)

View File

@ -2,6 +2,7 @@ from flask import Blueprint, render_template
from app.blueprints.api.sessions.routes import get_sessions
from app.blueprints.api.users.routes import get_users
from app.blueprints.api.tasks.routes import get_tasks
from app.models.collection import Collection
frontend = Blueprint("frontend", __name__, url_prefix="/", template_folder="templates", static_folder="static", static_url_path="/static/frontend")
@ -20,4 +21,9 @@ def tasks():
@frontend.route("/parse/<int:id>")
def parse(id: int):
return render_template("parse.j2", session_id=id, users_template=get_users(id))
return render_template("parse.j2", session_id=id, users_template=get_users(id))
@frontend.route("/collections")
def collections():
collections = Collection.query.all()
return render_template("collections.j2", collections=collections)

View File

@ -0,0 +1,36 @@
const tabs_lists = document.querySelectorAll('.nav-tabs[role="tablist"]');
let tabs = [];
tabs_lists.forEach((tabs_list) => {
tabs = [...tabs, ...tabs_list.querySelectorAll('[role="tab"]')];
})
function setInputsState(section, disabled = false) {
section.querySelectorAll('input, textarea, button, select').forEach((el) => {
// console.log(el);
el.disabled = disabled;
});
}
function tabSwitchHandler(event) {
const id_content_hide = event.target.getAttribute('data-bs-target');
const id_content_show = event.relatedTarget.getAttribute('data-bs-target');
// console.log('==== Hide ====');
const content_hide = document.querySelector(id_content_hide);
setInputsState(content_hide, true);
// console.log('==== Show ====');
const content_show = document.querySelector(id_content_show);
setInputsState(content_show, false);
}
tabs.forEach((tab) => {
tab.addEventListener('hide.bs.tab', tabSwitchHandler);
if (tab.getAttribute('aria-selected') == "false") {
let id_content_hide = tab.getAttribute('data-bs-target');
let content_hide = document.querySelector(id_content_hide);
setInputsState(content_hide, true);
}
})

View File

@ -1,7 +1,7 @@
{% extends "base.j2" %}
{% block title %}
PaperParser: Вход в аккаунт
Вход в аккаунт
{% endblock title %}
{% block main %}

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<title>{% block title %}{% endblock %}</title>
<title>PaperParser: {% block title %}{% endblock %}</title>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
@ -14,20 +14,22 @@
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
</head>
<body class="d-flex flex-column min-vh-100">
<body class="d-flex flex-column vh-100">
<header>
<!-- place navbar here -->
<nav class="navbar navbar-expand-sm navbar-light bg-light mb-3">
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-3">
<div class="container justify-content-beetwen">
<a class="navbar-brand" href="/">PaperParser</a>
<ul class="nav">
<li><a class="nav-link" href="/tasks">Задачи</a></li>
<li><a class="btn btn-outline-success" href="/add" role="button">Войти</a></li>
<ul class="navbar-nav">
<li class="nav-item"><a class="nav-link" href="/collections">Пользователи</a></li>
<li class="nav-item"><a class="nav-link" href="/tasks">Задачи</a></li>
<li class="nav-item"><a class="btn btn-outline-success ms-2" href="/add" role="button">Войти</a></li>
</ul>
</div>
</nav>
</header>
<main>
<main class="mb-3">
{% block main %}
{% endblock %}

View File

@ -0,0 +1,14 @@
{% extends "base.j2" %}
{% block title %}
Базы пользователей
{% endblock title %}
{% block main %}
<div class="container">
<div class="separator">
<h2>Базы пользователей</h2>
<hr class="divider">
</div>
{% include "collections/grid.j2" %}
{% endblock main %}

View File

@ -1,7 +1,7 @@
{% extends "base.j2" %}
{% block title %}
PaperParser: Главная
Главная
{% endblock title %}
{% block main %}

View File

@ -1,101 +1,104 @@
{% extends "base.j2" %}
{% block title %}
PaperParser: Парсинг
Парсинг
{% endblock title %}
{% block main %}
<div class="container">
<!-- Actions -->
<section id="actions-section" class="mb-3">
<div class="separator">
<h2>Действия</h2>
<hr class="divider">
</div>
<section id="actions-parse-add-section">
<label for="" class="form-label">Парсинг пользователей и добавление в группу</label>
<div class="row row-cols-1 row-cols-sm-2">
<div class="col mb-3">
<form hx-post="/api/users/parse/{{session_id}}" hx-swap="innerHTML" hx-target="#cards-grid" hx-indicator="#loading-spinner" class="input-group" id="group-from-form">
<form method="post" action="/test" class="container">
<div class="row">
<!-- Actions -->
<section class="col-lg mb-3">
<div class="separator">
<h2>Действия</h2>
<hr class="divider">
</div>
<nav class="mb-3">
<div class="nav nav-tabs" id="nav-tab" role="tablist">
<button class="nav-link active" id="nav-home-tab" data-bs-toggle="tab" data-bs-target="#actions-parse-section" type="button" role="tab" aria-controls="actions-parse-section" aria-selected="true">Сбор</button>
<button class="nav-link" id="nav-home-tab" data-bs-toggle="tab" data-bs-target="#actions-add-section" type="button" role="tab" aria-controls="actions-add-section" aria-selected="false">Добавление</button>
<button class="nav-link" id="nav-profile-tab" data-bs-toggle="tab" data-bs-target="#actions-message-section" type="button" role="tab" aria-controls="actions-message-section" aria-selected="false">Рассылка</button>
</div>
</nav>
<div class="tab-content" id="nav-tabContent">
<section class="tab-pane fade show active" id="actions-parse-section">
<div class="form-floating">
<input type="text" class="form-control" name="group" id="group-from-input"
placeholder="Группа для парсинга">
<input type="text" class="form-control" name="group" id="group-from-input" placeholder="Группа для парсинга">
<label for="group-from-input">Группа для парсинга</label>
</div>
<button type="submit" class="btn btn-outline-primary">Спарсить</button>
</form>
</div>
<div class="col mb-3">
<form hx-post="/api/tasks/session/{{session_id}}" hx-swap="none" hx-indicator="#loading-spinner" class="input-group" id="group-to-form">
<div class="form-floating">
<input type="text" name="url" class="form-control" id="group-to-input"
placeholder="Группа, в которую нужно добавить">
<label for="group-to-input">Группа для добавления</label>
</section>
<section class="tab-pane fade" id="actions-add-section">
<div class="form-floating">
<input type="text" name="url" class="form-control" id="group-to-input"
placeholder="Группа для добавлнеия">
<label for="group-to-input">Группа для добавления</label>
</div>
</section>
<section class="tab-pane fade" id="actions-message-section">
<div class="d-flex flex-column gap-3">
<div>
<textarea class="form-control" name="message" rows="8" id="message-textarea"
placeholder="Напишите здесь своё сообщение"></textarea>
</div>
<div class="mb-3">
<label for="formFile" class="form-label">Выберите изображение</label>
<input class="form-control" name="file" type="file" id="picture-file-input">
</div>
</div>
<button type="submit" name="task" value="add" class="btn btn-outline-secondary">Добавить</button>
</form>
</div>
</section>
</div>
</section>
<section id="actions-message-section">
<form hx-post="/api/tasks/session/{{session_id}}" hx-swap="none" hx-indicator="#loading-spinner" id="message-form">
<label for="" class="form-label">Сообщение для пользователей</label>
<div class="row row-cols-1 row-cols-sm-2">
<div class="col">
<div class="mb-3">
<textarea class="form-control" name="message" rows="8" id="message-textarea"
placeholder="Напишите здесь своё сообщение"></textarea>
</div>
</div>
<div class="col">
<div class="mb-3">
<label for="formFile" class="form-label">Выберите изображение</label>
<input class="form-control" name="file" type="file" id="picture-file-input">
</div>
<div class="mb-3">
<button type="submit" name="task" value="message" class="btn btn-outline-primary" style="width: 100%;">
Отправить сообщение
</button>
</div>
</div>
</div>
</form>
</section>
</section>
<!-- Users -->
<section id="users-section" class="mb-3">
<div id="separator">
<div class="d-flex justify-content-between">
<div class="d-flex gap-2 align-items-center">
<h2 style="margin-bottom: 0;">Пользователи</h2>
<div class="spinner-container htmx-indicator" id="loading-spinner">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
<div>
<button class="btn btn-outline-primary disabled" onClick="exportToCSV()">Экспорт</button>
<button
class="btn btn-outline-danger"
hx-delete="/api/users/{{session_id}}"
hx-swap="innerHTML"
hx-target="#cards-grid"
hx-confirm="Вы уверены, что хотите удалить пользователей для этой сессии?"
hx-indicator="#loading-spinner"
>
Удалить
</button>
</div>
<section class="col-lg mb-3">
<div class="separator">
<h2>Настройки</h2>
<hr class="divider">
</div>
<hr class="divider">
</div>
<div id="cards-grid" class="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-4">
{{ users_template }}
</div>
</section>
</div>
{% endblock main %}
<div class="mb-3 form-floating">
<input
type="text"
class="form-control"
name="collection"
id="task-name-input"
placeholder="Название задачи"
>
<label for="task-name-input">Название задачи</label>
</div>
<section class="d-flex flex-column gap-3 mb-3">
<div class="input-group">
<div class="form-floating">
<input
type="text"
class="form-control"
name="collection"
id="collection-name-input"
placeholder="Группа для парсинга"
>
<label for="collection-name-input">Название базы пользователей</label>
</div>
<button type="submit" class="btn btn-outline-primary">Добавить</button>
</div>
<select class="form-select flex-fill" size="3">
{% for collection in collections %}
<option value="{{collection.id}}">{{ collection.name }}</option>
{% else %}
<option disabled>Добавьте новую базу пользователей</option>
{% endfor %}
</select>
</section>
<button type="submit" class="w-100 btn btn-primary">Создать задачу</button>
</section>
</div>
</form>
{% endblock main %}
{% block scripts %}
<script src="{{ url_for('static', filename='frontend/tabs.js') }}"></script>
{% endblock scripts %}

View File

@ -1,7 +1,7 @@
{% extends "base.j2" %}
{% block title %}
PaperParser: Заадчи
Заадчи
{% endblock title %}
{% block main %}

12
app/models/collection.py Normal file
View File

@ -0,0 +1,12 @@
from typing import List
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.extensions import db
class Collection(db.Model):
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(unique=True, nullable=False)
tasks: Mapped[List['Task']] = relationship("Task", back_populates="collection")
users: Mapped[List['User']] = relationship("User", cascade="all, delete-orphan", back_populates="collection")

View File

@ -2,7 +2,6 @@ from typing import List
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.task import Task
from app.models.user import User
from app.extensions import db
class Session(db.Model):
@ -10,7 +9,6 @@ class Session(db.Model):
name: Mapped[str] = mapped_column(unique=True)
authorized: Mapped[bool]
users: Mapped[List[User]] = relationship("User", cascade="all, delete-orphan", back_populates="session")
tasks: Mapped[List[Task]] = relationship("Task", cascade="all, delete-orphan", back_populates="session")

View File

@ -4,6 +4,7 @@ from sqlalchemy import Integer, DateTime
from sqlalchemy.schema import Column, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.extensions import db
from app.models.collection import Collection
class Task(db.Model):
id: Mapped[int] = mapped_column(primary_key=True)
@ -13,6 +14,10 @@ class Task(db.Model):
session_id = Column(Integer, ForeignKey("session.id"))
session: Mapped['Session'] = relationship("Session", back_populates="tasks")
collection_id = Column(Integer, ForeignKey("collection.id"))
collection: Mapped['Collection'] = relationship('Collection', back_populates='tasks')
datetime: Mapped[DateTime] = Column(DateTime, default=datetime.datetime.utcnow())
status: Mapped[str]
status_message: Mapped[str]

View File

@ -15,5 +15,5 @@ class User(db.Model):
phone: Mapped[str] = mapped_column(nullable=True, unique=True)
username: Mapped[str] = mapped_column(nullable=True, unique=True)
session_id = Column(Integer, ForeignKey("session.id"))
session: Mapped["Session"] = relationship("Session", back_populates="users")
collection_id = Column(Integer, ForeignKey("collection.id"))
collection: Mapped["Collection"] = relationship("Collection", back_populates="users")

View File

@ -0,0 +1,51 @@
"""user collections
Revision ID: 21750f5bbab1
Revises: cdd471f48b0d
Create Date: 2024-02-24 22:47:05.197801
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '21750f5bbab1'
down_revision = 'cdd471f48b0d'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('collection',
sa.Column('id', sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('task', schema=None) as batch_op:
batch_op.add_column(sa.Column('collection_id', sa.Integer(), nullable=True))
batch_op.create_foreign_key(None, 'collection', ['collection_id'], ['id'])
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.add_column(sa.Column('collection_id', sa.Integer(), nullable=True))
batch_op.drop_constraint('user_session_id_fkey', type_='foreignkey')
batch_op.create_foreign_key(None, 'collection', ['collection_id'], ['id'])
batch_op.drop_column('session_id')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.add_column(sa.Column('session_id', sa.INTEGER(), autoincrement=False, nullable=True))
batch_op.drop_constraint(None, type_='foreignkey')
batch_op.create_foreign_key('user_session_id_fkey', 'session', ['session_id'], ['id'])
batch_op.drop_column('collection_id')
with op.batch_alter_table('task', schema=None) as batch_op:
batch_op.drop_constraint(None, type_='foreignkey')
batch_op.drop_column('collection_id')
op.drop_table('collection')
# ### end Alembic commands ###

View File

@ -0,0 +1,34 @@
"""Added name field to collections
Revision ID: d573f6529ad5
Revises: 21750f5bbab1
Create Date: 2024-03-06 01:45:54.772732
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'd573f6529ad5'
down_revision = '21750f5bbab1'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('collection', schema=None) as batch_op:
batch_op.add_column(sa.Column('name', sa.String(), nullable=False))
batch_op.create_unique_constraint(None, ['name'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('collection', schema=None) as batch_op:
batch_op.drop_constraint(None, type_='unique')
batch_op.drop_column('name')
# ### end Alembic commands ###