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 .tasks.routes import tasks
from .sessions.routes import sessions from .sessions.routes import sessions
from .users.routes import users from .users.routes import users
from .collections.routes import collections
api = Blueprint("api", __name__, url_prefix="/api") api = Blueprint("api", __name__, url_prefix="/api")
api.register_blueprint(users) api.register_blueprint(users)
api.register_blueprint(tasks) 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"]) @users.route("/<int:session_id>", methods=["GET"])
def get_users(session_id: int): 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) 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.sessions.routes import get_sessions
from app.blueprints.api.users.routes import get_users from app.blueprints.api.users.routes import get_users
from app.blueprints.api.tasks.routes import get_tasks 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") frontend = Blueprint("frontend", __name__, url_prefix="/", template_folder="templates", static_folder="static", static_url_path="/static/frontend")
@ -21,3 +22,8 @@ def tasks():
@frontend.route("/parse/<int:id>") @frontend.route("/parse/<int:id>")
def parse(id: int): 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" %} {% extends "base.j2" %}
{% block title %} {% block title %}
PaperParser: Вход в аккаунт Вход в аккаунт
{% endblock title %} {% endblock title %}
{% block main %} {% block main %}

View File

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

View File

@ -1,101 +1,104 @@
{% extends "base.j2" %} {% extends "base.j2" %}
{% block title %} {% block title %}
PaperParser: Парсинг Парсинг
{% endblock title %} {% endblock title %}
{% block main %} {% block main %}
<div class="container"> <form method="post" action="/test" class="container">
<!-- Actions --> <div class="row">
<section id="actions-section" class="mb-3"> <!-- Actions -->
<div class="separator"> <section class="col-lg mb-3">
<h2>Действия</h2> <div class="separator">
<hr class="divider"> <h2>Действия</h2>
</div> <hr class="divider">
<section id="actions-parse-add-section"> </div>
<label for="" class="form-label">Парсинг пользователей и добавление в группу</label> <nav class="mb-3">
<div class="row row-cols-1 row-cols-sm-2"> <div class="nav nav-tabs" id="nav-tab" role="tablist">
<div class="col mb-3"> <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>
<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"> <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"> <div class="form-floating">
<input type="text" class="form-control" name="group" id="group-from-input" <input type="text" class="form-control" name="group" id="group-from-input" placeholder="Группа для парсинга">
placeholder="Группа для парсинга">
<label for="group-from-input">Группа для парсинга</label> <label for="group-from-input">Группа для парсинга</label>
</div> </div>
<button type="submit" class="btn btn-outline-primary">Спарсить</button> </section>
</form>
</div> <section class="tab-pane fade" id="actions-add-section">
<div class="col mb-3"> <div class="form-floating">
<form hx-post="/api/tasks/session/{{session_id}}" hx-swap="none" hx-indicator="#loading-spinner" class="input-group" id="group-to-form"> <input type="text" name="url" class="form-control" id="group-to-input"
<div class="form-floating"> placeholder="Группа для добавлнеия">
<input type="text" name="url" class="form-control" id="group-to-input" <label for="group-to-input">Группа для добавления</label>
placeholder="Группа, в которую нужно добавить"> </div>
<label for="group-to-input">Группа для добавления</label> </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> </div>
<button type="submit" name="task" value="add" class="btn btn-outline-secondary">Добавить</button> </section>
</form>
</div>
</div> </div>
</section> </section>
<section id="actions-message-section"> <section class="col-lg mb-3">
<form hx-post="/api/tasks/session/{{session_id}}" hx-swap="none" hx-indicator="#loading-spinner" id="message-form"> <div class="separator">
<label for="" class="form-label">Сообщение для пользователей</label> <h2>Настройки</h2>
<div class="row row-cols-1 row-cols-sm-2"> <hr class="divider">
<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>
</div> </div>
<hr class="divider">
</div> <div class="mb-3 form-floating">
<div id="cards-grid" class="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-4"> <input
{{ users_template }} type="text"
</div> class="form-control"
</section> name="collection"
</div> 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 %} {% 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" %} {% extends "base.j2" %}
{% block title %} {% block title %}
PaperParser: Заадчи Заадчи
{% endblock title %} {% endblock title %}
{% block main %} {% 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 sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.task import Task from app.models.task import Task
from app.models.user import User
from app.extensions import db from app.extensions import db
class Session(db.Model): class Session(db.Model):
@ -10,7 +9,6 @@ class Session(db.Model):
name: Mapped[str] = mapped_column(unique=True) name: Mapped[str] = mapped_column(unique=True)
authorized: Mapped[bool] 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") 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.schema import Column, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.extensions import db from app.extensions import db
from app.models.collection import Collection
class Task(db.Model): class Task(db.Model):
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
@ -13,6 +14,10 @@ class Task(db.Model):
session_id = Column(Integer, ForeignKey("session.id")) session_id = Column(Integer, ForeignKey("session.id"))
session: Mapped['Session'] = relationship("Session", back_populates="tasks") 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()) datetime: Mapped[DateTime] = Column(DateTime, default=datetime.datetime.utcnow())
status: Mapped[str] status: Mapped[str]
status_message: 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) phone: Mapped[str] = mapped_column(nullable=True, unique=True)
username: Mapped[str] = mapped_column(nullable=True, unique=True) username: Mapped[str] = mapped_column(nullable=True, unique=True)
session_id = Column(Integer, ForeignKey("session.id")) collection_id = Column(Integer, ForeignKey("collection.id"))
session: Mapped["Session"] = relationship("Session", back_populates="users") 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 ###