Compare commits

..

No commits in common. "dd1518b69cd2d8ef5abb6a242da617dd756fd6ef" and "0cb92510b8a1d182a8fee93c847d9a5db5729bb6" have entirely different histories.

19 changed files with 102 additions and 313 deletions

View File

@ -1,21 +0,0 @@
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

@ -1,18 +0,0 @@
<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

@ -1,9 +0,0 @@
<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,11 +3,9 @@ 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 = Session.query.get_or_404(session_id).users
return render_template("user_cards.j2", users=users) return render_template("user_cards.j2", users=users)

View File

@ -2,7 +2,6 @@ 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")
@ -22,8 +21,3 @@ 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

@ -1,36 +0,0 @@
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>PaperParser: {% block title %}{% endblock %}</title> <title>{% 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,22 +14,20 @@
<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 vh-100"> <body class="d-flex flex-column min-vh-100">
<header> <header>
<!-- place navbar here --> <!-- place navbar here -->
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-3"> <nav class="navbar navbar-expand-sm 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="navbar-nav"> <ul class="nav">
<li class="nav-item"><a class="nav-link" href="/collections">Пользователи</a></li> <li><a class="nav-link" href="/tasks">Задачи</a></li>
<li class="nav-item"><a class="nav-link" href="/tasks">Задачи</a></li> <li><a class="btn btn-outline-success" href="/add" role="button">Войти</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

@ -1,14 +0,0 @@
{% 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,104 +1,101 @@
{% extends "base.j2" %} {% extends "base.j2" %}
{% block title %} {% block title %}
Парсинг PaperParser: Парсинг
{% endblock title %} {% endblock title %}
{% block main %} {% block main %}
<form method="post" action="/test" class="container"> <div class="container">
<div class="row">
<!-- Actions --> <!-- Actions -->
<section class="col-lg mb-3"> <section id="actions-section" class="mb-3">
<div class="separator"> <div class="separator">
<h2>Действия</h2> <h2>Действия</h2>
<hr class="divider"> <hr class="divider">
</div> </div>
<nav class="mb-3"> <section id="actions-parse-add-section">
<div class="nav nav-tabs" id="nav-tab" role="tablist"> <label for="" class="form-label">Парсинг пользователей и добавление в группу</label>
<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> <div class="row row-cols-1 row-cols-sm-2">
<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> <div class="col mb-3">
<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> <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">
</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" placeholder="Группа для парсинга"> <input type="text" class="form-control" name="group" id="group-from-input"
placeholder="Группа для парсинга">
<label for="group-from-input">Группа для парсинга</label> <label for="group-from-input">Группа для парсинга</label>
</div> </div>
</section> <button type="submit" class="btn btn-outline-primary">Спарсить</button>
</form>
<section class="tab-pane fade" id="actions-add-section"> </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"> <div class="form-floating">
<input type="text" name="url" class="form-control" id="group-to-input" <input type="text" name="url" class="form-control" id="group-to-input"
placeholder="Группа для добавлнеия"> placeholder="Группа, в которую нужно добавить">
<label for="group-to-input">Группа для добавления</label> <label for="group-to-input">Группа для добавления</label>
</div> </div>
<button type="submit" name="task" value="add" class="btn btn-outline-secondary">Добавить</button>
</form>
</div>
</div>
</section> </section>
<section id="actions-message-section">
<section class="tab-pane fade" id="actions-message-section"> <form hx-post="/api/tasks/session/{{session_id}}" hx-swap="none" hx-indicator="#loading-spinner" id="message-form">
<div class="d-flex flex-column gap-3"> <label for="" class="form-label">Сообщение для пользователей</label>
<div> <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" <textarea class="form-control" name="message" rows="8" id="message-textarea"
placeholder="Напишите здесь своё сообщение"></textarea> placeholder="Напишите здесь своё сообщение"></textarea>
</div> </div>
</div>
<div class="col">
<div class="mb-3"> <div class="mb-3">
<label for="formFile" class="form-label">Выберите изображение</label> <label for="formFile" class="form-label">Выберите изображение</label>
<input class="form-control" name="file" type="file" id="picture-file-input"> <input class="form-control" name="file" type="file" id="picture-file-input">
</div> </div>
<div class="mb-3">
<button type="submit" name="task" value="message" class="btn btn-outline-primary" style="width: 100%;">
Отправить сообщение
</button>
</div> </div>
</div>
</div>
</form>
</section> </section>
</div>
</section> </section>
<section class="col-lg mb-3"> <!-- Users -->
<div class="separator"> <section id="users-section" class="mb-3">
<h2>Настройки</h2> <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>
<hr class="divider"> <hr class="divider">
</div> </div>
<div id="cards-grid" class="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-4">
<div class="mb-3 form-floating"> {{ users_template }}
<input
type="text"
class="form-control"
name="collection"
id="task-name-input"
placeholder="Название задачи"
>
<label for="task-name-input">Название задачи</label>
</div> </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> </section>
<button type="submit" class="w-100 btn btn-primary">Создать задачу</button> </div>
</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 %}

View File

@ -1,12 +0,0 @@
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,6 +2,7 @@ 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):
@ -9,6 +10,7 @@ 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,7 +4,6 @@ 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)
@ -14,10 +13,6 @@ 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)
collection_id = Column(Integer, ForeignKey("collection.id")) session_id = Column(Integer, ForeignKey("session.id"))
collection: Mapped["Collection"] = relationship("Collection", back_populates="users") session: Mapped["Session"] = relationship("Session", back_populates="users")

View File

@ -1,51 +0,0 @@
"""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

@ -1,34 +0,0 @@
"""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 ###