Compare commits
No commits in common. "main" and "server-rx" have entirely different histories.
104
README.md
104
README.md
@ -1,104 +0,0 @@
|
|||||||
# ICPC-WEBPRINT
|
|
||||||
|
|
||||||
This platform helps contest participants to print their code using no drivers, only web application in any browser. Supports authorization, printing PDF for administrators and teachers.
|
|
||||||
|
|
||||||
Platform was already used on some events, like "The FPMI Cup : February, 2025" and "MIPT Open Programming Championship : April, 2025".
|
|
||||||
|
|
||||||
## Instalation
|
|
||||||
### Environment and Service
|
|
||||||
Both RX and TX servers have the same way to install dependencies. First, be sure to have venv globally installed on your system:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
apt install python3-venv
|
|
||||||
# or
|
|
||||||
python3 -m pip install venv
|
|
||||||
```
|
|
||||||
|
|
||||||
After that create virtual environment:
|
|
||||||
|
|
||||||
```
|
|
||||||
cd print-tx # and then print-rx
|
|
||||||
python3 -m venv .venv
|
|
||||||
```
|
|
||||||
|
|
||||||
Enter environment and install dependencies from `requirements.txt`:
|
|
||||||
|
|
||||||
```
|
|
||||||
source .venv/bin/activate
|
|
||||||
python3 -m pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
Great! Now you have working system. You can start your project either running
|
|
||||||
```
|
|
||||||
python3 -m gunicorn wsgi_back:app -c gunicorn.py
|
|
||||||
```
|
|
||||||
in venv, or creating services on your Linux. Template could be found in `example.serivce` file. After that you can start your web app with running:
|
|
||||||
```
|
|
||||||
systemctl start example.service
|
|
||||||
```
|
|
||||||
|
|
||||||
### Setting Secrets
|
|
||||||
|
|
||||||
Change secret code, that is used to access RX server from TX in `server-tx.json` and `server-rx.json`.
|
|
||||||
Change salt and session secret key in `server-tx/app/config.py`:
|
|
||||||
```
|
|
||||||
_SALT = '$2b$12$DE09JKsqy6Ii/zAxEAA5n.' #edit
|
|
||||||
_SECRET_SESSION_KEY = "GAZAKBAYEV-AHMET-PROJECT" #edit
|
|
||||||
```
|
|
||||||
You can also to set super admin user (it can add and remove others):
|
|
||||||
```
|
|
||||||
_SUPER_ADMINS = ( #edit
|
|
||||||
"god"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Accounts with login:password like "god:god" will be checked and created if not exist, when application start.
|
|
||||||
|
|
||||||
### Setting ports
|
|
||||||
|
|
||||||
You can set port of TX part in file `gunicorn.py`, and RX part in config file `server-rx.json`. Using TX interface, in admin panel, you can configure RX server and get `server-rx.json` to load it.
|
|
||||||
|
|
||||||
### Setting default printer
|
|
||||||
|
|
||||||
Learn more about `lp` service in Linux ([Wiki](https://ru.wikipedia.org/wiki/Lp)).
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- Four groups: Super Admin, Admin (create and remove users, set configuration), Tutor (print PDF and code), Team Member (default, print only code)
|
|
||||||
- Print code & PDF
|
|
||||||
- Create & Delete users
|
|
||||||
- Statistic of printed pages & last printing time of member
|
|
||||||
- Setting RX server using UI
|
|
||||||
- Setting TX configuration: PDF Watermark and limits.
|
|
||||||
|
|
||||||
## Setting user group
|
|
||||||
|
|
||||||
Groups are just team names! Use `$root` for Admins, `$tutor$` for Teachers and other names for members.
|
|
||||||
|
|
||||||
## Interface
|
|
||||||
|
|
||||||
### Team Member
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### Tutor
|
|
||||||

|
|
||||||
|
|
||||||
### Админ Панель
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### Создание и Редактирование пользователя
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Просмотреть данную галерею можно на [NextCloud](https://cloud.gazakbayev.net/index.php/s/gqzmo9S8KFGcQLi?dir=/) автора.
|
|
@ -1,20 +0,0 @@
|
|||||||
#
|
|
||||||
# NOTICE! Replace PATH_TO_SCRIPT with required directory!
|
|
||||||
# Paste content of this file into /etc/systemd/system/YOUR_NAME.service
|
|
||||||
#
|
|
||||||
|
|
||||||
# Suggested names for service: print-rx.service & print-tx.server
|
|
||||||
# REMEMBER to create virtual environment before using script (read README.md)
|
|
||||||
|
|
||||||
[Unit]
|
|
||||||
Description=Print Server TX/RX server script.
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
User=www-data
|
|
||||||
Group=www-data
|
|
||||||
WorkingDirectory=PATH_TO_SCRIPT
|
|
||||||
ExecStart=PATH_TO_SCRIPT/.venv/bin/gunicorn wsgi_back:app -c gunicorn.py
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
@ -1,36 +0,0 @@
|
|||||||
from flask import Flask
|
|
||||||
from flask import globals
|
|
||||||
from flask import render_template
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from app.admin import m_admin
|
|
||||||
from app.auth import m_auth
|
|
||||||
from app.print import m_print
|
|
||||||
from app.config import _SECRET_SESSION_KEY as SESSION_KEY
|
|
||||||
|
|
||||||
def get_app():
|
|
||||||
app = Flask(__name__)
|
|
||||||
app.secret_key = SESSION_KEY
|
|
||||||
|
|
||||||
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=30)
|
|
||||||
app.config['MAX_CONTENT_LENGTH'] = 10 * 1024 * 1024 # 10mb
|
|
||||||
|
|
||||||
@app.errorhandler(Exception)
|
|
||||||
def e_handler(e):
|
|
||||||
code = getattr(e, 'code', 500)
|
|
||||||
if code.__eq__(404):
|
|
||||||
return app.redirect('/')
|
|
||||||
return render_template('error.html', error=e), code
|
|
||||||
|
|
||||||
app.register_blueprint(m_print)
|
|
||||||
app.register_blueprint(m_auth, url_prefix='/auth')
|
|
||||||
app.register_blueprint(m_admin, url_prefix='/admin')
|
|
||||||
|
|
||||||
from app._tools.database import DataManager
|
|
||||||
from app._tools.database import SessionManager
|
|
||||||
DataManager.init_table()
|
|
||||||
DataManager.check_admins()
|
|
||||||
SessionManager.init_table()
|
|
||||||
|
|
||||||
|
|
||||||
return app
|
|
@ -1,168 +0,0 @@
|
|||||||
import time
|
|
||||||
import sqlite3 as sql
|
|
||||||
import json
|
|
||||||
|
|
||||||
from app._tools.models.user import User
|
|
||||||
from app._tools.models.session import Session
|
|
||||||
from app._tools import passhash as Hash
|
|
||||||
|
|
||||||
from app.config import _USER_DATABASE as U_DB_PATH
|
|
||||||
from app.config import _TOKEN_DATABASE as T_DB_PATH
|
|
||||||
from app.config import _SERVER_CONFIG as S_CFG_PATH
|
|
||||||
from app.config import _SCHEMA_USERS as SCHEMA_USERS
|
|
||||||
from app.config import _SCHEMA_TOKENS as SCHEMA_TOKENS
|
|
||||||
from app.config import _SUPER_ADMINS as SUPER_ADMINS
|
|
||||||
from app.config import _SALT as SALT
|
|
||||||
|
|
||||||
class ConnectManager:
|
|
||||||
def __init__(self, db_file : str):
|
|
||||||
self._conn = sql.connect(
|
|
||||||
db_file,
|
|
||||||
timeout = 10,
|
|
||||||
check_same_thread = False
|
|
||||||
)
|
|
||||||
self._curs = self._conn.cursor()
|
|
||||||
self._is_destroyed = False
|
|
||||||
def _save_and_close(self, mode : int):
|
|
||||||
if mode == 0:
|
|
||||||
self._conn.commit()
|
|
||||||
self._conn.close()
|
|
||||||
def execute(self, request, mode : int = 0, close_connection : bool = True):
|
|
||||||
"""
|
|
||||||
mode:
|
|
||||||
- 0 if just execute (with commit);
|
|
||||||
- 1 if fetch one (no commit);
|
|
||||||
- 2 if fetch many (no commit);
|
|
||||||
"""
|
|
||||||
if self._is_destroyed:
|
|
||||||
return "Connection was destroyed."
|
|
||||||
if isinstance(request, tuple):
|
|
||||||
response = self._curs.execute(*request)
|
|
||||||
else:
|
|
||||||
response = self._curs.execute(request)
|
|
||||||
if mode == 1:
|
|
||||||
response = self._curs.fetchone()
|
|
||||||
if mode == 2:
|
|
||||||
response = self._curs.fetchall()
|
|
||||||
if close_connection:
|
|
||||||
self._is_destroyed = True
|
|
||||||
self._save_and_close(mode)
|
|
||||||
return response
|
|
||||||
|
|
||||||
class SessionManager:
|
|
||||||
"""
|
|
||||||
Static methods only;
|
|
||||||
No need to create class sample.
|
|
||||||
"""
|
|
||||||
def __init__(self):
|
|
||||||
pass
|
|
||||||
def init_table():
|
|
||||||
c_man = ConnectManager(T_DB_PATH)
|
|
||||||
c_man.execute(SCHEMA_TOKENS, close_connection = False)
|
|
||||||
c_man.execute("VACUUM;")
|
|
||||||
def add_session(token : str, user_id : int, expires : int = None):
|
|
||||||
"""
|
|
||||||
Expires feature is not realized yet :(
|
|
||||||
"""
|
|
||||||
c_man = ConnectManager(T_DB_PATH)
|
|
||||||
c_man.execute(
|
|
||||||
("INSERT INTO tokens VALUES (?, ?, ?)", (token, user_id, 0))
|
|
||||||
)
|
|
||||||
def get_session(token : str):
|
|
||||||
c_man = ConnectManager(T_DB_PATH)
|
|
||||||
data = c_man.execute(
|
|
||||||
("SELECT * FROM tokens WHERE token=?", (token,)),
|
|
||||||
1
|
|
||||||
)
|
|
||||||
if data == None:
|
|
||||||
return None
|
|
||||||
return Session(*data)
|
|
||||||
def remove_session(token : str):
|
|
||||||
c_man = ConnectManager(T_DB_PATH)
|
|
||||||
c_man.execute(
|
|
||||||
("DELETE FROM tokens WHERE token=?", (token,))
|
|
||||||
)
|
|
||||||
def remove_session_by_user(id : int):
|
|
||||||
c_man = ConnectManager(T_DB_PATH)
|
|
||||||
c_man.execute(
|
|
||||||
("DELETE FROM tokens WHERE user_id=?", (id,))
|
|
||||||
)
|
|
||||||
|
|
||||||
class DataManager:
|
|
||||||
"""
|
|
||||||
Static methods only;
|
|
||||||
No need to create class sample.
|
|
||||||
"""
|
|
||||||
def __init__(self):
|
|
||||||
pass
|
|
||||||
def init_table():
|
|
||||||
c_man = ConnectManager(U_DB_PATH)
|
|
||||||
c_man.execute(SCHEMA_USERS, close_connection = False)
|
|
||||||
c_man.execute("VACUUM;")
|
|
||||||
def check_admins():
|
|
||||||
for k in SUPER_ADMINS:
|
|
||||||
if DataManager.get_user_by_name(k) == None:
|
|
||||||
DataManager.add_user(
|
|
||||||
k,
|
|
||||||
"$root$",
|
|
||||||
"Super Admin",
|
|
||||||
Hash.hashPassword(k, SALT)
|
|
||||||
)
|
|
||||||
print(f"Create super admin {k} with password {k}")
|
|
||||||
def get_user_by_name(username : str) -> User:
|
|
||||||
c_man = ConnectManager(U_DB_PATH)
|
|
||||||
data = c_man.execute(
|
|
||||||
("SELECT * FROM users WHERE username=?", (username,)),
|
|
||||||
1
|
|
||||||
)
|
|
||||||
if data == None:
|
|
||||||
return None
|
|
||||||
return User(*data)
|
|
||||||
def get_user(id : int) -> User:
|
|
||||||
c_man = ConnectManager(U_DB_PATH)
|
|
||||||
data = c_man.execute(
|
|
||||||
("SELECT * FROM users WHERE id=?", (id,)),
|
|
||||||
1
|
|
||||||
)
|
|
||||||
if data == None:
|
|
||||||
return None
|
|
||||||
return User(*data)
|
|
||||||
def get_all_users() -> list:
|
|
||||||
c_man = ConnectManager(U_DB_PATH)
|
|
||||||
data = c_man.execute(
|
|
||||||
"SELECT * FROM users",
|
|
||||||
2
|
|
||||||
)
|
|
||||||
userlist = list()
|
|
||||||
for user in data:
|
|
||||||
userlist.append(User(*user))
|
|
||||||
return userlist
|
|
||||||
def add_user(username : str, team : str, loc : str, passkey : str):
|
|
||||||
c_man = ConnectManager(U_DB_PATH)
|
|
||||||
c_man.execute(
|
|
||||||
("INSERT INTO users (username, team, loc, passkey, reg_ts) VALUES (?, ?, ?, ?, ?)", (username, team, loc, passkey, int(time.time())))
|
|
||||||
)
|
|
||||||
def edit_user(id : int, field : str, value):
|
|
||||||
c_man = ConnectManager(U_DB_PATH)
|
|
||||||
c_man.execute(
|
|
||||||
(f"UPDATE users SET {field} = ? WHERE id = ?", (value, id))
|
|
||||||
)
|
|
||||||
def del_user(id : int):
|
|
||||||
c_man = ConnectManager(U_DB_PATH)
|
|
||||||
c_man.execute(
|
|
||||||
("DELETE FROM users WHERE id = ?", (id,))
|
|
||||||
)
|
|
||||||
|
|
||||||
class ConfigManager:
|
|
||||||
"""
|
|
||||||
Static methods only;
|
|
||||||
No need to create class sample.
|
|
||||||
"""
|
|
||||||
def __init__():
|
|
||||||
pass
|
|
||||||
def get_config():
|
|
||||||
with open(S_CFG_PATH, 'r', encoding = "utf-8") as file:
|
|
||||||
return json.load(file)
|
|
||||||
def set_config(config):
|
|
||||||
with open(S_CFG_PATH, "w", encoding = "utf-8") as file:
|
|
||||||
json.dump(config, file, indent = 4, ensure_ascii = False)
|
|
@ -1,31 +0,0 @@
|
|||||||
#
|
|
||||||
# @auth_required decorator
|
|
||||||
# @admin_required decorator
|
|
||||||
#
|
|
||||||
|
|
||||||
from flask import session, redirect
|
|
||||||
from app._tools.database import SessionManager
|
|
||||||
from app._tools.database import DataManager
|
|
||||||
|
|
||||||
def auth_required(func):
|
|
||||||
def auth_check(*args, **kwargs):
|
|
||||||
if ("token" not in session):
|
|
||||||
return redirect("/auth")
|
|
||||||
token_i = SessionManager.get_session(session["token"])
|
|
||||||
if (token_i == None):
|
|
||||||
return redirect("/auth")
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
auth_check.__name__ = func.__name__
|
|
||||||
return auth_check
|
|
||||||
|
|
||||||
def admin_required(func):
|
|
||||||
def admin_check(*args, **kwargs):
|
|
||||||
token_i = SessionManager.get_session(session["token"])
|
|
||||||
user = DataManager.get_user(token_i.user_id)
|
|
||||||
if (user == None):
|
|
||||||
return redirect("/")
|
|
||||||
if (user.team != "$root$"):
|
|
||||||
return redirect("/")
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
admin_check.__name__ = func.__name__
|
|
||||||
return admin_check
|
|
@ -1,10 +0,0 @@
|
|||||||
class Session:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
token : str,
|
|
||||||
user_id : int,
|
|
||||||
expires : int
|
|
||||||
):
|
|
||||||
self.token = token
|
|
||||||
self.user_id = user_id
|
|
||||||
self.expires = expires
|
|
@ -1,20 +0,0 @@
|
|||||||
class User:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
id : int,
|
|
||||||
username : str,
|
|
||||||
team : str,
|
|
||||||
loc : str,
|
|
||||||
passkey : str,
|
|
||||||
reg_ts : int,
|
|
||||||
lastprint_ts : int,
|
|
||||||
pages : int
|
|
||||||
):
|
|
||||||
self.id = id
|
|
||||||
self.username = username
|
|
||||||
self.team = team
|
|
||||||
self.loc = loc
|
|
||||||
self.passkey = passkey
|
|
||||||
self.reg_ts = reg_ts
|
|
||||||
self.lastprint_ts = lastprint_ts
|
|
||||||
self.pages = pages
|
|
@ -1,27 +0,0 @@
|
|||||||
import bcrypt
|
|
||||||
from secrets import token_hex
|
|
||||||
|
|
||||||
_ENCODING = "utf-8"
|
|
||||||
|
|
||||||
def hashPassword(
|
|
||||||
pswd : str,
|
|
||||||
salt : str
|
|
||||||
) -> str:
|
|
||||||
return bcrypt.hashpw(
|
|
||||||
pswd.encode(encoding=_ENCODING),
|
|
||||||
salt.encode(encoding=_ENCODING)
|
|
||||||
).decode(encoding=_ENCODING)
|
|
||||||
|
|
||||||
def getSalt() -> str:
|
|
||||||
return bcrypt.gensalt().decode(encoding=_ENCODING)
|
|
||||||
|
|
||||||
def isValid(
|
|
||||||
pswd : str,
|
|
||||||
salt : str,
|
|
||||||
hash : str
|
|
||||||
) -> bool:
|
|
||||||
hashed = hashPassword(pswd, salt)
|
|
||||||
return (hash.__eq__(hashed))
|
|
||||||
|
|
||||||
def genToken():
|
|
||||||
return token_hex(32)
|
|
@ -1,9 +0,0 @@
|
|||||||
from flask import Blueprint
|
|
||||||
|
|
||||||
m_admin = Blueprint(
|
|
||||||
'admin',
|
|
||||||
__name__,
|
|
||||||
template_folder='templates'
|
|
||||||
)
|
|
||||||
|
|
||||||
from . import views
|
|
@ -1,101 +0,0 @@
|
|||||||
{% extends "general.html" %}
|
|
||||||
|
|
||||||
{% block title %}Панель администратора{% endblock %}
|
|
||||||
{% block header %}Админ: {{ user.username }} | Панель | <a href="/">На главную</a>{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="card">
|
|
||||||
<h2>Список пользователей</h2>
|
|
||||||
<p><a href = "/admin/create">Тык</a>, чтобы создать пользователя</p>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="username">Логин</th>
|
|
||||||
<th class="team">Команда</th>
|
|
||||||
<th class="actions">Действия</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for cuser in users %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ cuser.username }}</td>
|
|
||||||
<td>{{ cuser.team }}</td>
|
|
||||||
<td class="action-buttons">
|
|
||||||
{% if cuser.team not in ("$root$") %}
|
|
||||||
<a href="edit/{{ cuser.id }}" class="icon-button" title="Редактировать">✏️</button>
|
|
||||||
<a href="remove/{{ cuser.id }}" class="icon-button" title="Удалить!">🗑️</button>
|
|
||||||
{% else %}
|
|
||||||
Только по запросу | ID: {{ cuser.id }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h2>Активность пользователей</h2>
|
|
||||||
<p>Активность администраторов и преподавательского состава не отображается.</p>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Логин</th>
|
|
||||||
<th>Последняя печать</th>
|
|
||||||
<th>Количество страниц</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for cuser in users %}
|
|
||||||
{% if cuser.team not in ("$root$", "$tutor$") %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ cuser.username }}</td>
|
|
||||||
<td>{{ cuser.lastprint_ts|format_timestamp }}</td>
|
|
||||||
<td>{{ cuser.pages }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<h2>Настройки печати</h2>
|
|
||||||
<br>
|
|
||||||
<form action="edit/settings" method="POST">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="watermark">Watermark на листах (доступные переменные: $time$, $team$, $page и $loc$):</label>
|
|
||||||
<input type="text" id="watermark" name="watermark" value="{{ settings['print']['watermark'] }}" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="pages-per-request">Лимит сгенерированных страниц на один запрос</label>
|
|
||||||
<input type="number" id="pages-per-request" name="pages-per-request" value="{{ settings['print']['limits']['pages-per-request'] }}" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="seconds-per-request">Лимит запросов на пользователя (задержка между запросами в секундах)</label>
|
|
||||||
<input type="number" id="seconds-per-request" name="seconds-per-request" value="{{ settings['print']['limits']['seconds-per-request'] }}" required>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="button">Сохранить текущие настройки</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h2>Настройки принимающего сервера</h2>
|
|
||||||
<p><span style = "color: red; font-weight: bold;">Перед любыми изменениям убедитесь, что вы знаете, что делаете!</span> По любым вопросам обращайтесь к Администратору системы.</p>
|
|
||||||
<p></p>
|
|
||||||
<br>
|
|
||||||
<form action="edit/server-rx" method="POST">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="server-ip">Принимающий сервер : IP</label>
|
|
||||||
<input type="text" id="server-ip" name="server-ip" value="{{ settings['server']['ip'] }}" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="server-port">Принимающий сервер : Port</label>
|
|
||||||
<input type="number" id="server-port" name="server-port" value="{{ settings['server']['port'] }}" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="server-secret">Принимающий сервер : Секретная фраза</label>
|
|
||||||
<input type="texts" id="server-secret" name="server-secret" value="{{ settings['server']['secret'] }}" required>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="button">Сохранить и скачать server-rx.json</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@ -1,29 +0,0 @@
|
|||||||
{% extends "general.html" %}
|
|
||||||
|
|
||||||
{% block title %}Создание пользователя{% endblock %}
|
|
||||||
{% block header %}Создание пользователя{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="card">
|
|
||||||
<h2>Создание новой команды</h2>
|
|
||||||
<form action="account_created" method="POST">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="username">Логин</label>
|
|
||||||
<input type="text" id="username" name="username" placeholder="Логин, например, ahmet" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="team_name">Команда</label>
|
|
||||||
<input type="text" id="team_name" name="team_name" placeholder="Короткое название команды, например, Физтех.Бегемотики" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="location">Позиция (стол, секция, пр.)</label>
|
|
||||||
<input type="text" id="location" name="location" placeholder="Метка, например, '1р1с' (т.е., 1 ряд 1 стол)" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="password">Пароль</label>
|
|
||||||
<input type="text" id="password" name="password" required>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="button">Создать пользователя</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@ -1,29 +0,0 @@
|
|||||||
{% extends "general.html" %}
|
|
||||||
|
|
||||||
{% block title %}Создание пользователя{% endblock %}
|
|
||||||
{% block header %}Создание пользователя{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="card">
|
|
||||||
<h2>Создание новой команды</h2>
|
|
||||||
<form action="account_created_many" method="POST">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="username">Логин</label>
|
|
||||||
<input type="text" id="username" name="username" placeholder="Логин, например, ahmet" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="team_name">Команда</label>
|
|
||||||
<input type="text" id="team_name" name="team_name" placeholder="Короткое название команды, например, Физтех.Бегемотики" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="location">Позиция (стол, секция, пр.)</label>
|
|
||||||
<input type="text" id="location" name="location" placeholder="Метка, например, '1р1с' (т.е., 1 ряд 1 стол)" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="password">Пароль</label>
|
|
||||||
<input type="text" id="password" name="password" required>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="button">Создать пользователя</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@ -1,33 +0,0 @@
|
|||||||
{% extends "general.html" %}
|
|
||||||
|
|
||||||
{% block title %}Пользователь создан{% endblock %}
|
|
||||||
{% block header %}Пользователь создан{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="card">
|
|
||||||
<h2>Успешно создан пользователь {{ cuser.username }}!</h2>
|
|
||||||
<form action="">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="username">Логин</label>
|
|
||||||
<input type="text" id="username" name="username" value="{{ cuser.username }}" readonly>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="team_name">Команда</label>
|
|
||||||
<input type="text" id="team_name" name="team_name" value="{{ cuser.team }}" readonly>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="location">Позиция (стол, секция, пр.)</label>
|
|
||||||
<input type="text" id="location" name="locat ion" value="{{ cuser.loc }}" readonly>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="password">Пароль (отображается один раз, на данной странице)</label>
|
|
||||||
<input type="text" id="password" name="password" value="{{ password_onetime }}" readonly>
|
|
||||||
</div>
|
|
||||||
Данную страницу можно отправить новому пользователю платформы!
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<a href="../admin">Вернуться обратно</a>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@ -1,29 +0,0 @@
|
|||||||
{% extends "general.html" %}
|
|
||||||
|
|
||||||
{% block title %}Редактирование пользователя{% endblock %}
|
|
||||||
{% block header %}Редактирование {{ edituser.username }}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="card">
|
|
||||||
<h2>Редактирование команды</h2>
|
|
||||||
<form action="proc/{{ edituser.id }}" method="POST">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="username">Логин (не изменяем)</label>
|
|
||||||
<input type="text" id="username" name="username" value="{{ edituser.username }}" readonly>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="team_name">Команда</label>
|
|
||||||
<input type="text" id="team_name" name="team_name" value="{{ edituser.team }}" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="location">Позиция (стол, секция, пр.)</label>
|
|
||||||
<input type="text" id="location" name="location" value="{{ edituser.loc }}" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" id="new_password">
|
|
||||||
<label for="new_password">Новый пароль (должен превышать 3 символа, если хотите изменить, иначе - пустое)</label>
|
|
||||||
<input type="text" id="new_password" name="new_password">
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="button">Сохранить изменения</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@ -1,187 +0,0 @@
|
|||||||
from . import m_admin
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from flask import render_template
|
|
||||||
from flask import Response
|
|
||||||
from flask import redirect
|
|
||||||
from flask import request
|
|
||||||
from flask import session
|
|
||||||
from app._tools.decorators import auth_required
|
|
||||||
from app._tools.decorators import admin_required
|
|
||||||
from app._tools.database import DataManager
|
|
||||||
from app._tools.database import SessionManager
|
|
||||||
from app._tools.database import ConfigManager
|
|
||||||
from app.config import _SUPER_ADMINS as SUPER
|
|
||||||
from app.config import _SALT as SALT
|
|
||||||
from app._tools import passhash
|
|
||||||
|
|
||||||
@m_admin.app_template_filter("format_timestamp")
|
|
||||||
def format_timestamp(value):
|
|
||||||
try:
|
|
||||||
dt = datetime.fromtimestamp(value)
|
|
||||||
return dt.strftime("%d.%m.%Y %H:%M")
|
|
||||||
except:
|
|
||||||
return "Invalid timestamp"
|
|
||||||
|
|
||||||
@m_admin.route('/')
|
|
||||||
@auth_required
|
|
||||||
@admin_required
|
|
||||||
def panel():
|
|
||||||
user = DataManager.get_user(SessionManager.get_session(session["token"]).user_id)
|
|
||||||
users = DataManager.get_all_users()
|
|
||||||
return render_template('admin/admin_main.html', user = user, users = users, settings = ConfigManager.get_config())
|
|
||||||
|
|
||||||
@m_admin.route('/create')
|
|
||||||
@auth_required
|
|
||||||
@admin_required
|
|
||||||
def create():
|
|
||||||
return render_template("admin/user_create.html")
|
|
||||||
|
|
||||||
@m_admin.route('/create_many')
|
|
||||||
@auth_required
|
|
||||||
@admin_required
|
|
||||||
def create_manu():
|
|
||||||
return render_template("admin/user_create_many.html")
|
|
||||||
|
|
||||||
@m_admin.route('/account_created_many', methods=['POST'])
|
|
||||||
@auth_required
|
|
||||||
@admin_required
|
|
||||||
def create_many_proc():
|
|
||||||
data = dict(request.form)
|
|
||||||
usernames = str(data['username']).split(';')
|
|
||||||
passwords = str(data['password']).split(';')
|
|
||||||
teams = str(data['team_name']).split(';')
|
|
||||||
locations = str(data['location']).split(';')
|
|
||||||
|
|
||||||
for i in range(len(usernames)):
|
|
||||||
if (DataManager.get_user_by_name(usernames[i]) != None):
|
|
||||||
return render_template("error.html", error = "Пользователь с таким логином существует!")
|
|
||||||
if len(passwords[i]) < 4:
|
|
||||||
return render_template("error.html", error = "Длина пароля должна превышать 3 символа!")
|
|
||||||
user = DataManager.get_user(SessionManager.get_session(session["token"]).user_id)
|
|
||||||
if teams[i] == "$root$" and user.username not in SUPER:
|
|
||||||
return render_template("error.html", error = "У вас нет прав на работу с группой $root$!")
|
|
||||||
DataManager.add_user(
|
|
||||||
usernames[i],
|
|
||||||
teams[i],
|
|
||||||
locations[i],
|
|
||||||
passhash.hashPassword(passwords[i], SALT)
|
|
||||||
)
|
|
||||||
cuser = DataManager.get_user_by_name(data['username'])
|
|
||||||
return "Done"
|
|
||||||
|
|
||||||
@m_admin.route('/account_created', methods=['POST'])
|
|
||||||
@auth_required
|
|
||||||
@admin_required
|
|
||||||
def create_proc():
|
|
||||||
data = dict(request.form)
|
|
||||||
if (DataManager.get_user_by_name(data['username']) != None):
|
|
||||||
return render_template("error.html", error = "Пользователь с таким логином существует!")
|
|
||||||
if len(data['password']) < 4:
|
|
||||||
return render_template("error.html", error = "Длина пароля должна превышать 3 символа!")
|
|
||||||
user = DataManager.get_user(SessionManager.get_session(session["token"]).user_id)
|
|
||||||
if data['team_name'] == "$root$" and user.username not in SUPER:
|
|
||||||
return render_template("error.html", error = "У вас нет прав на работу с группой $root$!")
|
|
||||||
DataManager.add_user(
|
|
||||||
data['username'],
|
|
||||||
data['team_name'],
|
|
||||||
data['location'],
|
|
||||||
passhash.hashPassword(data['password'], SALT)
|
|
||||||
)
|
|
||||||
cuser = DataManager.get_user_by_name(data['username'])
|
|
||||||
return render_template("admin/user_created.html", cuser = cuser, password_onetime = data['password'])
|
|
||||||
|
|
||||||
@m_admin.route('/edit/<id>')
|
|
||||||
@auth_required
|
|
||||||
@admin_required
|
|
||||||
def edit_team(id : int):
|
|
||||||
euser = DataManager.get_user(id)
|
|
||||||
return render_template('admin/user_edit.html', edituser = euser)
|
|
||||||
|
|
||||||
@m_admin.route('/edit/settings', methods=['POST'])
|
|
||||||
@auth_required
|
|
||||||
@admin_required
|
|
||||||
def edit_settings():
|
|
||||||
data = dict(request.form)
|
|
||||||
cur_config = ConfigManager.get_config()
|
|
||||||
cur_config['print']['watermark'] = data['watermark']
|
|
||||||
data['pages-per-request'] = int(data['pages-per-request'])
|
|
||||||
data['seconds-per-request'] = int(data['seconds-per-request'])
|
|
||||||
if data['pages-per-request'] in range(1, 51):
|
|
||||||
cur_config['print']['limits']['pages-per-request'] = data['pages-per-request']
|
|
||||||
else:
|
|
||||||
return render_template("error.html", error = "Лимит страниц должен быть в рамках от 1 до 50 страниц!")
|
|
||||||
if data['seconds-per-request'] in range(5, 61):
|
|
||||||
cur_config['print']['limits']['seconds-per-request'] = data['seconds-per-request']
|
|
||||||
else:
|
|
||||||
return render_template("error.html", error = "Задержка между запросами должна быть от 5 до 60 секунд!")
|
|
||||||
ConfigManager.set_config(cur_config)
|
|
||||||
return redirect("/admin/")
|
|
||||||
|
|
||||||
@m_admin.route('/edit/server-rx', methods=['POST'])
|
|
||||||
@auth_required
|
|
||||||
@admin_required
|
|
||||||
def edit_server_rx():
|
|
||||||
data = dict(request.form)
|
|
||||||
cur_config = ConfigManager.get_config()
|
|
||||||
if int(data['server-port']) not in range(1, 65536):
|
|
||||||
return render_template("error.html", error = "Порт в диапазоне от 1 до 65535!")
|
|
||||||
cur_config['server']['port'] = int(data['server-port'])
|
|
||||||
cur_config['server']['ip'] = str(data['server-ip'])
|
|
||||||
cur_config['server']['secret'] = str(data['server-secret'])
|
|
||||||
ConfigManager.set_config(cur_config)
|
|
||||||
gen_json = dict()
|
|
||||||
gen_json = cur_config['server']
|
|
||||||
gen_json = json.dumps(
|
|
||||||
gen_json,
|
|
||||||
indent = 4
|
|
||||||
)
|
|
||||||
response = Response(gen_json, mimetype='application/json')
|
|
||||||
response.headers['Content-Disposition'] = 'attachment; filename=server-rx.json'
|
|
||||||
response.headers['Content-Type'] = 'application/json'
|
|
||||||
response.headers['Refresh'] = '1; url=' + "/admin"
|
|
||||||
return response
|
|
||||||
|
|
||||||
@m_admin.route('/edit/proc/<id>', methods=['POST'])
|
|
||||||
@auth_required
|
|
||||||
@admin_required
|
|
||||||
def edit_proc(id : int):
|
|
||||||
data = dict(request.form)
|
|
||||||
e_user = DataManager.get_user(id)
|
|
||||||
user = DataManager.get_user(SessionManager.get_session(session["token"]).user_id)
|
|
||||||
if data['team_name'] == "$root$" and user.username not in SUPER:
|
|
||||||
return render_template("error.html", error = "У вас нет прав на работу с группой $root$!")
|
|
||||||
if e_user.team == "$root$" and user.username not in SUPER:
|
|
||||||
return render_template("error.html", error = "Вы не можете редактировать других Администраторов!")
|
|
||||||
if (len(data['new_password']) > 3):
|
|
||||||
DataManager.edit_user(
|
|
||||||
id,
|
|
||||||
'passkey',
|
|
||||||
passhash.hashPassword(data['new_password'], SALT)
|
|
||||||
)
|
|
||||||
if e_user.team != data['team_name']:
|
|
||||||
DataManager.edit_user(
|
|
||||||
id,
|
|
||||||
'team',
|
|
||||||
data['team_name']
|
|
||||||
)
|
|
||||||
if e_user.loc != data['location']:
|
|
||||||
DataManager.edit_user(
|
|
||||||
id,
|
|
||||||
'loc',
|
|
||||||
data['location']
|
|
||||||
)
|
|
||||||
return redirect("/admin")
|
|
||||||
|
|
||||||
@m_admin.route('/remove/<id>')
|
|
||||||
@auth_required
|
|
||||||
@admin_required
|
|
||||||
def remove_team(id : int):
|
|
||||||
user = DataManager.get_user(SessionManager.get_session(session["token"]).user_id)
|
|
||||||
if user.username not in SUPER:
|
|
||||||
return render_template("error.html", error = "Обратитесь к Супер-администратору. У вас нет прав на удаление пользователей!")
|
|
||||||
SessionManager.remove_session_by_user(id)
|
|
||||||
DataManager.del_user(id)
|
|
||||||
return redirect("/admin")
|
|
@ -1,9 +0,0 @@
|
|||||||
from flask import Blueprint
|
|
||||||
|
|
||||||
m_auth = Blueprint(
|
|
||||||
'auth',
|
|
||||||
__name__,
|
|
||||||
template_folder='templates'
|
|
||||||
)
|
|
||||||
|
|
||||||
from . import views
|
|
@ -1,21 +0,0 @@
|
|||||||
{% extends "general.html" %}
|
|
||||||
|
|
||||||
{% block title %}Авторизация{% endblock %}
|
|
||||||
{% block header %}Авторизация на платформе{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="card">
|
|
||||||
<h2>Авторизация</h2>
|
|
||||||
<form action="session/login" method="POST">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="username">Логин</label>
|
|
||||||
<input type="text" id="username" name="username" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="password">Пароль</label>
|
|
||||||
<input type="password" id="password" name="password" required>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="button">Авторизация</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@ -1,41 +0,0 @@
|
|||||||
from . import m_auth
|
|
||||||
|
|
||||||
from flask import render_template
|
|
||||||
from flask import redirect
|
|
||||||
from flask import request
|
|
||||||
from flask import session
|
|
||||||
from app._tools.database import SessionManager
|
|
||||||
from app._tools.database import DataManager
|
|
||||||
from app._tools.decorators import auth_required
|
|
||||||
from app._tools import passhash
|
|
||||||
from app.config import _SALT as SALT
|
|
||||||
|
|
||||||
@m_auth.route('/')
|
|
||||||
def login_page():
|
|
||||||
return render_template('auth/login_form.html')
|
|
||||||
|
|
||||||
@m_auth.route('/session/login', methods=['POST'])
|
|
||||||
def login():
|
|
||||||
data = dict(request.form)
|
|
||||||
if (len(data['username']) == 0 or len(data['password']) == 0):
|
|
||||||
return redirect("/auth")
|
|
||||||
user_i = DataManager.get_user_by_name(data['username'])
|
|
||||||
if user_i == None:
|
|
||||||
return render_template("error.html", error = "Пользователь не найден!")
|
|
||||||
if not passhash.isValid(data['password'], SALT, user_i.passkey):
|
|
||||||
return render_template("error.html", error = "Неверный пароль!")
|
|
||||||
|
|
||||||
new_token = passhash.genToken()
|
|
||||||
SessionManager.add_session(new_token, user_i.id)
|
|
||||||
|
|
||||||
session.permanent = True
|
|
||||||
session["token"] = new_token
|
|
||||||
return redirect("/")
|
|
||||||
|
|
||||||
@m_auth.route('/logout')
|
|
||||||
@auth_required
|
|
||||||
def logout():
|
|
||||||
current_token = session["token"]
|
|
||||||
session.clear()
|
|
||||||
SessionManager.remove_session(current_token)
|
|
||||||
return redirect("/")
|
|
@ -1,42 +0,0 @@
|
|||||||
|
|
||||||
#
|
|
||||||
# Web-site platform settings
|
|
||||||
# It's better to edit only #edit fields!
|
|
||||||
#
|
|
||||||
|
|
||||||
_USER_DATABASE = "databases/users.db" #default
|
|
||||||
_TOKEN_DATABASE = "databases/tokens.db" #default
|
|
||||||
_SERVER_CONFIG = "server-tx.json" #default
|
|
||||||
_SALT = '$2b$12$DE09JKsqy6Ii/zAxEAA5n.' #edit
|
|
||||||
_SECRET_SESSION_KEY = "GAZAKBAYEV-AHMET-PROJECT" #edit
|
|
||||||
_SUPER_ADMINS = ( #edit
|
|
||||||
"god"
|
|
||||||
)
|
|
||||||
|
|
||||||
#
|
|
||||||
# !!! WARNING !!!
|
|
||||||
# Please, don't edit this
|
|
||||||
# w/o understanding!
|
|
||||||
#
|
|
||||||
|
|
||||||
_SCHEMA_TOKENS = """
|
|
||||||
CREATE TABLE IF NOT EXISTS tokens (
|
|
||||||
token TEXT,
|
|
||||||
user_id INTEGER,
|
|
||||||
expires INTEGER
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
_SCHEMA_USERS = """
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id INTEGER PRIMARY KEY
|
|
||||||
UNIQUE
|
|
||||||
NOT NULL,
|
|
||||||
username TEXT,
|
|
||||||
team TEXT,
|
|
||||||
loc TEXT,
|
|
||||||
passkey TEXT,
|
|
||||||
reg_ts INTEGER,
|
|
||||||
lastprint_ts INTEGER DEFAULT (0),
|
|
||||||
pages INTEGER DEFAULT (0)
|
|
||||||
);
|
|
||||||
"""
|
|
@ -1,9 +0,0 @@
|
|||||||
from flask import Blueprint
|
|
||||||
|
|
||||||
m_print = Blueprint(
|
|
||||||
'print',
|
|
||||||
__name__,
|
|
||||||
template_folder='templates'
|
|
||||||
)
|
|
||||||
|
|
||||||
from . import views
|
|
Binary file not shown.
@ -1,20 +0,0 @@
|
|||||||
{% extends "general.html" %}
|
|
||||||
|
|
||||||
{% block title %}Загрузка кода на печать{% endblock %}
|
|
||||||
{% block header %}Команда: {{ user.team }} | Локация: {{ user.loc }} | <a href="/auth/logout">Выйти</a>{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% block extra %}
|
|
||||||
{% endblock %}
|
|
||||||
<div class="card">
|
|
||||||
<h2>Отправка кода на печать</h2>
|
|
||||||
<p>Скопируйте код с IDE и вставьте его в поле "Код".</p>
|
|
||||||
<form action="/printcode" method="POST">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="code">Код</label>
|
|
||||||
<textarea id="code" name="code" rows="25"></textarea>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="button">Узреть свой код на бересте!</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@ -1,4 +0,0 @@
|
|||||||
{% extends "print/print_code_t.html" %}
|
|
||||||
|
|
||||||
{% block title %}Загрузка кода на печать{% endblock %}
|
|
||||||
{% block header %}Админ: {{ user.username }} | <a href="/admin">Панель администратора</a> | <a href="/auth/logout">Выйти</a>{% endblock %}
|
|
@ -1,18 +0,0 @@
|
|||||||
{% extends "print/print_code.html" %}
|
|
||||||
|
|
||||||
{% block title %}Загрузка кода на печать{% endblock %}
|
|
||||||
{% block header %}Преподаватель: {{ user.username }} | <a href="/auth/logout">Выйти</a>{% endblock %}
|
|
||||||
|
|
||||||
{% block extra %}
|
|
||||||
<div class="card">
|
|
||||||
<h2>Отправка файла на печать</h2>
|
|
||||||
<p>У вас есть права на печать документов напрямую, загружая .pdf!</p>
|
|
||||||
<form class="file-upload" action="/printpdf" method="POST" enctype="multipart/form-data">
|
|
||||||
<label for="file">Загрузка файла .PDF</label>
|
|
||||||
<input type="file" id="file" name="file" accept=".pdf">
|
|
||||||
<label style="margin-top: 10px;" for="samples">Количество копий (допустимый максимум - 15, можно повторять запросы)</label>
|
|
||||||
<input type="number" id="samples" name="samples" value="1">
|
|
||||||
<button style="margin-top: 10px;" type="submit" class="button">Распечатать</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@ -1,175 +0,0 @@
|
|||||||
from . import m_print
|
|
||||||
|
|
||||||
import requests
|
|
||||||
import time
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from flask import render_template
|
|
||||||
from flask import redirect
|
|
||||||
from flask import request
|
|
||||||
from flask import session
|
|
||||||
from reportlab.pdfgen import canvas
|
|
||||||
from reportlab.lib.pagesizes import A4
|
|
||||||
from reportlab.pdfbase import pdfmetrics
|
|
||||||
from reportlab.pdfbase.ttfonts import TTFont
|
|
||||||
from io import BytesIO
|
|
||||||
from app._tools.decorators import auth_required
|
|
||||||
from app._tools.database import DataManager
|
|
||||||
from app._tools.database import SessionManager
|
|
||||||
from app._tools.database import ConfigManager
|
|
||||||
|
|
||||||
|
|
||||||
RENDER_TEMPLATE = {
|
|
||||||
"$root$" : "print/print_code_a.html",
|
|
||||||
"$tutor$" : "print/print_code_t.html"
|
|
||||||
}
|
|
||||||
pdfmetrics.registerFont(TTFont('webprint', ConfigManager.get_config()['font-path']))
|
|
||||||
|
|
||||||
def watermark_gen(p : canvas.Canvas, config, user, page) -> None:
|
|
||||||
REPLACEMENTS = {
|
|
||||||
"$time$": datetime.now().strftime('%d.%m.%Y %H:%M'),
|
|
||||||
"$team$": getattr(user, 'team', 'Unknown Team'),
|
|
||||||
"$loc$": getattr(user, 'loc', 'Unknown Location'),
|
|
||||||
"$page$": str(page)
|
|
||||||
}
|
|
||||||
wm_format = str(config['print']['watermark'])
|
|
||||||
for key, value in REPLACEMENTS.items():
|
|
||||||
wm_format = wm_format.replace(key, value)
|
|
||||||
p.setFont("webprint", 13)
|
|
||||||
p.setFillColorRGB(0.5, 0.5, 0.5)
|
|
||||||
p.rotate(45)
|
|
||||||
p.drawString(200, 200, wm_format * 10)
|
|
||||||
p.rotate(-45)
|
|
||||||
|
|
||||||
@m_print.route('/')
|
|
||||||
@auth_required
|
|
||||||
def print_code():
|
|
||||||
user = DataManager.get_user(SessionManager.get_session(session["token"]).user_id)
|
|
||||||
render = 'print/print_code.html'
|
|
||||||
if user.team in RENDER_TEMPLATE:
|
|
||||||
render = RENDER_TEMPLATE[user.team]
|
|
||||||
return render_template(
|
|
||||||
f'{render}', user = user
|
|
||||||
)
|
|
||||||
|
|
||||||
@m_print.route('/printcode', methods=['POST'])
|
|
||||||
@auth_required
|
|
||||||
def print_proc():
|
|
||||||
user_text = request.form.get('code')
|
|
||||||
lines = user_text.split('\n')
|
|
||||||
if len(lines) < 4:
|
|
||||||
return render_template("error.html", error = "Код, в котором так мало строк, можно запомнить и без печати :)")
|
|
||||||
user = DataManager.get_user(SessionManager.get_session(session["token"]).user_id)
|
|
||||||
config = ConfigManager.get_config()
|
|
||||||
timedelta = int(time.time()) - user.lastprint_ts
|
|
||||||
timedelta = config['print']['limits']['seconds-per-request'] - timedelta
|
|
||||||
if timedelta > 0:
|
|
||||||
return render_template("error.html", error = f"Пожалуйста, подождите ещё {timedelta} с. перед повторным запросом!")
|
|
||||||
DataManager.edit_user(user.id, "lastprint_ts", int(time.time()))
|
|
||||||
buffer = BytesIO()
|
|
||||||
proc_pdf = canvas.Canvas(buffer, pagesize = A4)
|
|
||||||
width, height = A4
|
|
||||||
|
|
||||||
left_margin = 15
|
|
||||||
top_margin = height - 30
|
|
||||||
line_height = 15
|
|
||||||
y = top_margin
|
|
||||||
|
|
||||||
pages = 1
|
|
||||||
watermark_gen(proc_pdf, config, user, pages)
|
|
||||||
proc_pdf.setFillColorRGB(0, 0, 0, 1)
|
|
||||||
proc_pdf.setFont("webprint", 9)
|
|
||||||
for line in lines:
|
|
||||||
line = line.replace('\r', '')
|
|
||||||
if y < 30:
|
|
||||||
pages += 1
|
|
||||||
if (pages > config['print']['limits']['pages-per-request']):
|
|
||||||
return render_template(
|
|
||||||
"error.html",
|
|
||||||
error = f"Достигнут лимит на генерацию в {config['print']['limits']['pages-per-request']} страниц за запрос!"
|
|
||||||
)
|
|
||||||
proc_pdf.showPage()
|
|
||||||
watermark_gen(proc_pdf, config, user, pages)
|
|
||||||
proc_pdf.setFillColorRGB(0, 0, 0, 1)
|
|
||||||
proc_pdf.setFont("webprint", 9)
|
|
||||||
y = top_margin
|
|
||||||
proc_pdf.drawString(left_margin, y, line)
|
|
||||||
y -= line_height
|
|
||||||
|
|
||||||
proc_pdf.showPage()
|
|
||||||
proc_pdf.save()
|
|
||||||
buffer.seek(0)
|
|
||||||
proc_pdf = buffer.getvalue()
|
|
||||||
|
|
||||||
try:
|
|
||||||
DataManager.edit_user(
|
|
||||||
user.id,
|
|
||||||
"pages",
|
|
||||||
user.pages + pages
|
|
||||||
)
|
|
||||||
response = requests.post(
|
|
||||||
f"http://{config['server']['ip']}:{config['server']['port']}/printfile",
|
|
||||||
files = {
|
|
||||||
'file' : (f"wp_{user.username}_{user.lastprint_ts}.pdf", proc_pdf)
|
|
||||||
},
|
|
||||||
data = {
|
|
||||||
'secret-key' : config['server']['secret']
|
|
||||||
},
|
|
||||||
timeout = 10
|
|
||||||
)
|
|
||||||
if response.status_code == 200:
|
|
||||||
return redirect("/")
|
|
||||||
else:
|
|
||||||
return render_template(
|
|
||||||
"error.html",
|
|
||||||
error = "Ошибка при отправке PDF файла на печать. Обратитесь к Администратору."
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return render_template(
|
|
||||||
"error.html",
|
|
||||||
error = f"Ошибка при подключении к серверу печати: {str(e)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
@m_print.route('/printpdf', methods=['POST'])
|
|
||||||
@auth_required
|
|
||||||
def print_pdf_proc():
|
|
||||||
user = DataManager.get_user(SessionManager.get_session(session["token"]).user_id)
|
|
||||||
config = ConfigManager.get_config()
|
|
||||||
if user.team not in RENDER_TEMPLATE:
|
|
||||||
return redirect("/")
|
|
||||||
samples = 1
|
|
||||||
file = request.files.get('file')
|
|
||||||
if file.filename == '':
|
|
||||||
return render_template("error.html", error = "Не был прикреплён файл!")
|
|
||||||
if 'samples' in request.form:
|
|
||||||
samples = int(request.form.get('samples'))
|
|
||||||
if samples not in range(1, 16):
|
|
||||||
return render_template(
|
|
||||||
"error.html",
|
|
||||||
error = "Допустимый диапазон копий составляет 1..15!"
|
|
||||||
)
|
|
||||||
DataManager.edit_user(user.id, "lastprint_ts", int(time.time()))
|
|
||||||
try:
|
|
||||||
response = requests.post(
|
|
||||||
f"http://{config['server']['ip']}:{config['server']['port']}/printfile",
|
|
||||||
files = {
|
|
||||||
'file' : (f"wp_{user.username}_{user.lastprint_ts}.pdf", file)
|
|
||||||
},
|
|
||||||
data = {
|
|
||||||
'secret-key' : config['server']['secret'],
|
|
||||||
'samples' : samples
|
|
||||||
},
|
|
||||||
timeout = 10
|
|
||||||
)
|
|
||||||
if response.status_code == 200:
|
|
||||||
return redirect("/")
|
|
||||||
else:
|
|
||||||
return render_template(
|
|
||||||
"error.html",
|
|
||||||
error = "Ошибка при отправке PDF файла на печать. Обратитесь к Администратору."
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return render_template(
|
|
||||||
"error.html",
|
|
||||||
error = f"Ошибка при подключении к серверу печати: {str(e)}"
|
|
||||||
)
|
|
Binary file not shown.
Before Width: | Height: | Size: 2.7 KiB |
@ -1,171 +0,0 @@
|
|||||||
:root {
|
|
||||||
--primary-color: #ff6f00; /* Orange */
|
|
||||||
--secondary-color: #0077ff; /* Blue */
|
|
||||||
--gradient-bg: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
|
||||||
--text-color: #ffffff;
|
|
||||||
--bg-color: #121212; /* Dark background */
|
|
||||||
--card-bg: #1e1e1e; /* Dark card */
|
|
||||||
--input-bg: #2e2e2e; /* Dark input field */
|
|
||||||
--input-border: #444444; /* Input border color */
|
|
||||||
--shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
background-color: var(--bg-color);
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
background: var(--gradient-bg);
|
|
||||||
color: var(--text-color);
|
|
||||||
padding: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: bold;
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 2rem auto;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background: var(--card-bg);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1.5rem;
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
background: var(--gradient-bg);
|
|
||||||
color: var(--text-color);
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:hover {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.5rem;
|
|
||||||
border: 1px solid var(--input-border);
|
|
||||||
border-radius: 5px;
|
|
||||||
background-color: var(--input-bg);
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin-top: 1rem;
|
|
||||||
table-layout: fixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
th, td {
|
|
||||||
padding: 1rem;
|
|
||||||
text-align: left;
|
|
||||||
border: 1px solid #333;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
background: var(--primary-color);
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
td.username {
|
|
||||||
width: 20%;
|
|
||||||
}
|
|
||||||
|
|
||||||
td.team {
|
|
||||||
width: 60%;
|
|
||||||
}
|
|
||||||
|
|
||||||
td.actions {
|
|
||||||
width: 20%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-input-wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-input-label {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
background: var(--gradient-bg);
|
|
||||||
color: var(--text-color);
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-input {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-name {
|
|
||||||
color: var(--text-color);
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: white;
|
|
||||||
text-decoration: none;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
font-family: monospace;
|
|
||||||
width: 100%;
|
|
||||||
background-color: var(--input-bg);
|
|
||||||
color: var(--text-color);
|
|
||||||
border: 1px solid var(--input-border);
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-button {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--primary-color);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
{% extends "general.html" %}
|
|
||||||
|
|
||||||
{% block title %}Произошла ошибка{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="card">
|
|
||||||
<h2 style="color: darkred;">{{ error }}</h2>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@ -1,20 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>WebPrint | {% block title %}Default Title{% endblock %}</title>
|
|
||||||
<link rel="shortcut icon" href="{{ url_for('static', filename='style/fav.png') }}" type="image/x-icon">
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
{% block header %}WebPrint{% endblock %}
|
|
||||||
</header>
|
|
||||||
<div class="container">
|
|
||||||
{% block content %}{% endblock %}
|
|
||||||
</div>
|
|
||||||
{% block script %}
|
|
||||||
{% endblock %}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,3 +0,0 @@
|
|||||||
bind = '127.0.0.1:25565'
|
|
||||||
workers = 2
|
|
||||||
timeout = 120
|
|
@ -1,18 +0,0 @@
|
|||||||
bcrypt==4.2.1
|
|
||||||
blinker==1.9.0
|
|
||||||
certifi==2024.12.14
|
|
||||||
chardet==5.2.0
|
|
||||||
charset-normalizer==3.4.1
|
|
||||||
click==8.1.8
|
|
||||||
Flask==3.1.0
|
|
||||||
gunicorn==23.0.0
|
|
||||||
idna==3.10
|
|
||||||
itsdangerous==2.2.0
|
|
||||||
Jinja2==3.1.5
|
|
||||||
MarkupSafe==3.0.2
|
|
||||||
packaging==24.2
|
|
||||||
pillow==11.1.0
|
|
||||||
reportlab==4.2.5
|
|
||||||
requests==2.32.3
|
|
||||||
urllib3==2.3.0
|
|
||||||
Werkzeug==3.1.3
|
|
@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"font-path": "app/print/static/font.ttf",
|
|
||||||
"server": {
|
|
||||||
"ip": "127.0.0.1",
|
|
||||||
"port": 33333,
|
|
||||||
"secret": "PROJECT_BY_GAZAKBAYEV_AHMET"
|
|
||||||
},
|
|
||||||
"print": {
|
|
||||||
"watermark": " * КОМАНДА $team$ ЛОК $loc$ ВРЕМ $time$ СТРАНИЦА $page$ * ",
|
|
||||||
"limits": {
|
|
||||||
"pages-per-request": 20,
|
|
||||||
"seconds-per-request": 20
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
from app import get_app
|
|
||||||
|
|
||||||
app = get_app()
|
|
Loading…
x
Reference in New Issue
Block a user