Primeira versão do Rifalto

This commit is contained in:
Clóvis Fabrício Costa 2023-06-14 09:20:36 -03:00
commit 910beeb64c
67 changed files with 60428 additions and 0 deletions

21
Containerfile Normal file
View File

@ -0,0 +1,21 @@
FROM python:3.11-slim-buster
RUN pip install --no-cache-dir gunicorn
WORKDIR /rifalto
COPY requirements.txt /rifalto
RUN pip install --no-cache-dir -r requirements.txt
RUN rm requirements.txt
EXPOSE 5000
COPY rifaserver ./rifaserver
COPY start.sh .
COPY migrations ./migrations
RUN chmod +x start.sh
ENV FLASK_APP=rifaserver.app
ENV PYTHONUNBUFFERED 1
# CMD ["gunicorn", "-b", ":5000", "rifaserver.app:app", "--access-logfile", "-", "--error-logfile", "-"]
# "--access-logformat", "%({x-forwarded-for}i)s %(l)s %(u)s %(t)s \"%(r)s\" %(s)s %(b)s \"%(f)s\" \"%(a)s\""]
CMD ["./start.sh"]

0
main.py Normal file
View File

1
migrations/README Normal file
View File

@ -0,0 +1 @@
Single-database configuration for Flask.

50
migrations/alembic.ini Normal file
View File

@ -0,0 +1,50 @@
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic,flask_migrate
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[logger_flask_migrate]
level = INFO
handlers =
qualname = flask_migrate
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

110
migrations/env.py Normal file
View File

@ -0,0 +1,110 @@
import logging
from logging.config import fileConfig
from flask import current_app
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
def get_engine():
try:
# this works with Flask-SQLAlchemy<3 and Alchemical
return current_app.extensions['migrate'].db.get_engine()
except TypeError:
# this works with Flask-SQLAlchemy>=3
return current_app.extensions['migrate'].db.engine
def get_engine_url():
try:
return get_engine().url.render_as_string(hide_password=False).replace(
'%', '%%')
except AttributeError:
return str(get_engine().url).replace('%', '%%')
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
config.set_main_option('sqlalchemy.url', get_engine_url())
target_db = current_app.extensions['migrate'].db
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def get_metadata():
if hasattr(target_db, 'metadatas'):
return target_db.metadatas[None]
return target_db.metadata
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=get_metadata(), literal_binds=True
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')
connectable = get_engine()
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=get_metadata(),
process_revision_directives=process_revision_directives,
**current_app.extensions['migrate'].configure_args
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

24
migrations/script.py.mako Normal file
View File

@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,32 @@
"""add remarks to purchases
Revision ID: 45281a94fd78
Revises:
Create Date: 2023-06-09 18:39:54.584636
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '45281a94fd78'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('purchases', schema=None) as batch_op:
batch_op.add_column(sa.Column('remarks', sa.Text(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('purchases', schema=None) as batch_op:
batch_op.drop_column('remarks')
# ### end Alembic commands ###

10
requirements.txt Normal file
View File

@ -0,0 +1,10 @@
pytest
Flask
alembic
SQLAlchemy
click
Werkzeug
flask_sqlalchemy
flask_login
flask_migrate
APScheduler

0
rifaserver/__init__.py Normal file
View File

235
rifaserver/app.py Normal file
View File

@ -0,0 +1,235 @@
import functools
import itertools
import operator
import os
from flask import Flask, render_template, request, url_for, redirect, flash, session, make_response
from flask.cli import with_appcontext
from flask_login import LoginManager, login_user, login_required, logout_user, current_user
from sqlalchemy.exc import IntegrityError
import click
from sqlalchemy.orm import joinedload
from werkzeug.security import generate_password_hash
from flask_migrate import Migrate, upgrade, stamp
from rifaserver.models import Seller, Purchase, Raffle, Ticket, db
from apscheduler.schedulers.background import BackgroundScheduler
from datetime import datetime
import shutil
def backup_db(instance_path):
db_path = os.path.join(instance_path, 'raffle.db')
backup_dir = os.path.join(instance_path, 'backup')
os.makedirs(backup_dir, exist_ok=True)
# Find the most recent backup file
try:
latest_backup = max(os.path.join(backup_dir, name) for name in os.listdir(backup_dir))
except ValueError:
latest_backup = None # No backup files exist yet
if latest_backup and os.path.getmtime(db_path) <= os.path.getmtime(latest_backup):
# print("No changes to database, skipping backup.")
pass
else:
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
shutil.copy(db_path, os.path.join(backup_dir, f'raffle_{timestamp}.db'))
def remove_cache(response):
response = make_response(response)
response.headers['Last-Modified'] = str(datetime.now())
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '-1'
return response
def create_app():
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///raffle.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # optional, but can improve performance
app.config['SECRET_KEY'] = 'idm&#@$8*RSXZpc6Wvb4'
db.init_app(app)
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'
login_manager.login_message = 'Favor entrar com senha para acessar esta página!'
login_manager.login_message_category = 'danger'
@login_manager.user_loader
def load_user(user_id):
return Seller.query.get(user_id)
migrate = Migrate(app, db)
with app.app_context():
if not os.path.isfile(os.path.join(app.instance_path, 'raffle.db')):
db.create_all()
stamp()
scheduler = BackgroundScheduler()
scheduler.add_job(func=backup_db, args=(app.instance_path,), trigger="interval", hours=1)
scheduler.start()
# print('App created!!')
return app
app = create_app()
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
user = Seller.query.filter_by(username=request.form['username']).first()
if user and user.check_password(request.form['password']):
login_user(user)
return redirect(url_for('seller_screen'))
return render_template('login.html')
@app.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('login'))
@app.route('/venda', methods=['GET', 'POST'])
@login_required
def seller_screen():
if request.method == 'POST':
if not request.form['amount'].isdigit() or int(request.form['amount']) <= 0:
flash("Quantidade inválida", 'warning')
return redirect(url_for('seller_screen'))
purchase = Purchase(seller_id=current_user.id, total_numbers=int(request.form['amount']),
numbers_left=int(request.form['amount']), remarks=request.form['remarks'])
purchase.generate_id()
db.session.add(purchase)
db.session.commit()
purchase_link = url_for('numbers_screen', purchase_id=purchase.id, _external=True)
return remove_cache(render_template('purchase_link.html', link=purchase_link))
return render_template('seller_screen.html')
@app.route('/numeros/<purchase_id>', methods=['GET', 'POST'])
def numbers_screen(purchase_id):
purchase = Purchase.query.get_or_404(purchase_id)
buyer_data = session.get('buyer_data', {'name': '', 'contact': ''})
if request.method == 'POST':
_validation_errors = False
if not request.form['raffle_id']:
flash("Rifa não escolhida, escolha uma rifa!", 'warning')
_validation_errors = True
if purchase.numbers_left <= 0:
flash("Acabaram os números para escolher, compre mais!", 'danger')
_validation_errors = True
if not request.form['name']:
flash("Preencher o nome é obrigatório", 'warning')
_validation_errors = True
if not request.form['number'].isdigit() or int(request.form['number']) <= 0:
flash("Número inválido!", 'danger')
_validation_errors = True
if not _validation_errors:
raffle = Raffle.query.get_or_404(request.form['raffle_id'])
ticket = Ticket(purchase_id=purchase.id, raffle_id=raffle.id, buyer_name=request.form['name'],
buyer_contact=request.form['contact'], chosen_number=request.form['number'])
db.session.add(ticket)
try:
purchase.decrease_numbers()
db.session.commit()
flash(f"Número reservado: {request.form['number']}", 'success')
except IntegrityError:
db.session.rollback()
flash(f"O número {request.form['number']} já foi escolhido para {raffle.description}!", 'warning')
session['buyer_data'] = {'name': request.form['name'], 'contact': request.form['contact']}
return redirect(url_for('numbers_screen', purchase_id=purchase_id))
raffles = Raffle.query.all()
tickets = Ticket.query.filter_by(purchase_id=purchase_id).options(joinedload(Ticket.raffle)).order_by(Ticket.raffle_id).all()
grouped_tickets = {k: list(v) for k, v in itertools.groupby(tickets, key=operator.attrgetter('raffle.description'))}
# tickets = Ticket.query.filter_by(purchase_id=purchase_id).all()
return remove_cache(render_template('purchase.html', purchase=purchase, raffles=raffles,
grouped_tickets=grouped_tickets, buyer_data=buyer_data))
@app.route('/vendas', methods=['GET'])
@login_required
def seller_purchases():
seller_id = current_user.id
purchases = Purchase.query.filter_by(seller_id=seller_id).order_by(Purchase.id.desc()).all()
return render_template('seller_purchases.html', purchases=purchases)
@app.cli.command('create-seller')
@click.argument('username')
@click.argument('password')
def create_seller(username, password):
"""Create a new seller"""
from rifaserver.models import Seller, db
# check if seller exists
existing_seller = Seller.query.filter_by(username=username).first()
if existing_seller:
click.echo(f"Username {username} already exists.")
return
seller = Seller(
username=username,
password_hash=generate_password_hash(password)
)
db.session.add(seller)
db.session.commit()
click.echo(f"Seller: {username} created.")
@app.cli.command('cria-rifa')
@click.argument('descricao')
@with_appcontext
def create_raffle(descricao):
"""Create a new raffle"""
from rifaserver.models import db, Raffle
# Create a new raffle
raffle = Raffle(description=descricao)
db.session.add(raffle)
db.session.commit()
click.echo(f"Rifa '{descricao}' criada.")
@app.cli.command('muda-senha')
@click.argument('usuario')
@click.argument('nova_senha')
@with_appcontext
def change_password(usuario, nova_senha):
"""Change password for a seller"""
from rifaserver.models import db, Seller
from werkzeug.security import generate_password_hash
# Find the seller
seller = Seller.query.filter_by(username=usuario).first()
if not seller:
click.echo(f"Vendedor não encontrado: {usuario}")
return
# Change the password and commit the changes
seller.password_hash = generate_password_hash(nova_senha)
db.session.commit()
click.echo(f"Senha para {usuario} foi atualizada.")
@app.cli.command('apply_migrations')
@with_appcontext
def apply_migrations_command():
"""Apply database migrations."""
upgrade()

66
rifaserver/models.py Normal file
View File

@ -0,0 +1,66 @@
from flask_login import UserMixin
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import relationship
from werkzeug.security import generate_password_hash, check_password_hash
from sqlalchemy import UniqueConstraint
import hashlib
import os
db = SQLAlchemy()
class Seller(UserMixin, db.Model):
__tablename__ = 'sellers'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
password_hash = db.Column(db.String(128))
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
class Purchase(db.Model):
__tablename__ = 'purchases'
id = db.Column(db.String(64), primary_key=True)
seller_id = db.Column(db.Integer, db.ForeignKey('sellers.id'))
total_numbers = db.Column(db.Integer, default=0)
numbers_left = db.Column(db.Integer, default=0)
remarks = db.Column(db.Text)
def generate_id(self):
self.id = hashlib.sha256(os.urandom(60)).hexdigest()
def decrease_numbers(self):
if self.numbers_left > 0:
self.numbers_left -= 1
else:
raise Exception('No numbers left for this purchase')
class Raffle(db.Model):
__tablename__ = 'raffles'
id = db.Column(db.Integer, primary_key=True)
description = db.Column(db.String(128))
class Ticket(db.Model):
__tablename__ = 'tickets'
id = db.Column(db.Integer, primary_key=True)
purchase_id = db.Column(db.String(64), db.ForeignKey('purchases.id'))
raffle_id = db.Column(db.Integer, db.ForeignKey('raffles.id'))
buyer_name = db.Column(db.String(64))
buyer_contact = db.Column(db.String(64))
chosen_number = db.Column(db.Integer)
raffle = relationship(Raffle)
__table_args__ = (
UniqueConstraint('raffle_id', 'chosen_number', name='unique_raffle_number'),
)

36
rifaserver/schema.sql Normal file
View File

@ -0,0 +1,36 @@
DROP TABLE IF EXISTS purchases;
DROP TABLE IF EXISTS tickets;
DROP TABLE IF EXISTS sellers;
DROP TABLE IF EXISTS raffles;
CREATE TABLE sellers (
id INTEGER PRIMARY KEY,
username TEXT NOT NULL,
password_hash TEXT NOT NULL
);
CREATE TABLE purchases (
id TEXT PRIMARY KEY,
seller_id INTEGER,
total_numbers INTEGER,
numbers_left INTEGER,
remarks TEXT,
FOREIGN KEY (seller_id) REFERENCES sellers (id)
);
CREATE TABLE raffles (
id INTEGER PRIMARY KEY,
description TEXT
);
CREATE TABLE tickets (
id INTEGER PRIMARY KEY,
purchase_id TEXT,
raffle_id INTEGER,
buyer_name TEXT,
buyer_contact TEXT,
chosen_number INTEGER,
UNIQUE(raffle_id, chosen_number),
FOREIGN KEY (purchase_id) REFERENCES purchases (id),
FOREIGN KEY (raffle_id) REFERENCES raffles (id)
);

4085
rifaserver/static/css/bootstrap-grid.css vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,593 @@
/*!
* Bootstrap Reboot v5.3.0 (https://getbootstrap.com/)
* Copyright 2011-2023 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #adb5bd;
--bs-body-color-rgb: 173, 181, 189;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(173, 181, 189, 0.75);
--bs-secondary-color-rgb: 173, 181, 189;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(173, 181, 189, 0.5);
--bs-tertiary-color-rgb: 173, 181, 189;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-left: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: left;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
outline-offset: -2px;
-webkit-appearance: textfield;
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,590 @@
/*!
* Bootstrap Reboot v5.3.0 (https://getbootstrap.com/)
* Copyright 2011-2023 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #adb5bd;
--bs-body-color-rgb: 173, 181, 189;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(173, 181, 189, 0.75);
--bs-secondary-color-rgb: 173, 181, 189;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(173, 181, 189, 0.5);
--bs-tertiary-color-rgb: 173, 181, 189;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-right: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-right: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: right;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: right;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: right;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
outline-offset: -2px;
-webkit-appearance: textfield;
}
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

12071
rifaserver/static/css/bootstrap.css vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

12035
rifaserver/static/css/bootstrap.rtl.css vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

6306
rifaserver/static/js/bootstrap.bundle.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4439
rifaserver/static/js/bootstrap.esm.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4486
rifaserver/static/js/bootstrap.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

7
rifaserver/static/js/bootstrap.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>{% block title %}{% endblock %}{% if self.title() %} - {% endif %}Rifa do Alto</title>
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet">
</head>
<body>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="container">
{% if self.title() %}
<h2>{{ self.title() }}</h2>
{% endif %}
{% block content %}
{% endblock %}
</div>
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
</body>
</html>

View File

@ -0,0 +1,16 @@
{% extends 'base.html' %}
{% block title %}Login{% endblock %}
{% block content %}
<form method="POST">
<div class="form-group">
<label>Usuário</label>
<input type="text" name="username" class="form-control">
</div>
<div class="form-group">
<label>Senha</label>
<input type="password" name="password" class="form-control">
</div>
<button type="submit" class="btn btn-primary">Entrar</button>
</form>
{% endblock %}

View File

@ -0,0 +1,44 @@
{% extends 'base.html' %}
{% block title %}Reserva de Números{% endblock %}
{% block content %}
<p>Números a escolher: {{ purchase.numbers_left }} de {{ purchase.total_numbers }}</p>
{% if purchase.numbers_left > 0 %}
<form method="POST">
<div class="form-group">
<label>Rifa</label>
<select name="raffle_id" class="form-control">
<option value="">[ Escolha uma Rifa ]</option>
{% for raffle in raffles %}
<option value="{{ raffle.id }}">{{ raffle.description }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label>Seu nome</label>
<input type="text" name="name" class="form-control" value="{{ buyer_data.name }}">
</div>
<div class="form-group">
<label>Contato</label>
<input type="text" name="contact" class="form-control" value="{{ buyer_data.contact }}">
</div>
<div class="form-group">
<label>Número para Sorteio</label>
<input type="number" name="number" class="form-control">
</div>
<button type="submit" class="btn btn-primary">Reservar</button>
</form>
{% endif %}
{% if grouped_tickets %}
<br>
<h4>Escolhidos:</h4>
{% for raffle, tickets in grouped_tickets.items() %}
<h5>{{ raffle }}</h5>
<ul>
{% for ticket in tickets %}
<li>{{ ticket.chosen_number }}</li>
{% endfor %}
</ul>
{% endfor %}
{% endif %}
{% endblock %}

View File

@ -0,0 +1,36 @@
{% extends 'base.html' %}
{% block title %}Link para escolha dos Números{% endblock %}
{% block content %}
<div class="input-group mb-3">
<input type="text" id="purchase-link" value="{{ link }}" class="form-control" readonly>
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" id="copy-button">Copiar</button>
<a class="btn btn-outline-secondary" href="{{ link }}" target="_blank">Abrir</a>
</div>
</div>
<a href="/venda">Voltar</a>
<script>
document.querySelector("#copy-button").addEventListener("click", function() {
// Select the text field
var copyText = document.querySelector("#purchase-link");
copyText.select();
// For mobile devices
copyText.setSelectionRange(0, 99999);
// Copy the text inside the text field
document.execCommand("copy");
// Change button text to "Copied!"
this.textContent = "Copiado!";
// Change it back after 2 seconds
var button = this;
setTimeout(function() {
button.textContent = "Copiar";
}, 2000);
});
</script>
{% endblock %}

View File

@ -0,0 +1,47 @@
{% extends 'base.html' %}
{% block content %}
<h2>Tabela de Links:</h2>
<a href="/venda">Adicionar</a>
<table class="table">
<thead>
<tr>
<th scope="col">Obs</th>
<th scope="col">Números</th>
<th scope="col">Ações</th>
</tr>
</thead>
<tbody>
{% for purchase in purchases %}
<tr>
<td>{{ purchase.remarks }}</td>
<td>{{ purchase.total_numbers }}</td>
<td>
<div class="btn-group" role="group">
<a class="btn btn-primary" href="/numeros/{{ purchase.id }}" target="_blank">Abrir</a>
<button class="btn btn-secondary copy-button" data-link="/numeros/{{ purchase.id }}">Copiar</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<script>
document.querySelectorAll(".copy-button").forEach(function(button) {
button.addEventListener("click", function() {
var link = this.dataset.link;
navigator.clipboard.writeText(link);
// Change button text to "Copied!"
this.textContent = "Copiado!";
// Change it back after 2 seconds
var copyButton = this;
setTimeout(function() {
copyButton.textContent = "Copiar";
}, 2000);
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,17 @@
{% extends 'base.html' %}
{% block title %}Registrar Venda{% endblock %}
{% block content %}
<form method="POST">
<div class="form-group">
<label>Quantidade</label>
<input type="number" name="amount" class="form-control">
</div>
<div class="form-group">
<label>Anotações</label>
<textarea name="remarks" class="form-control"></textarea>
</div>
<button type="submit" class="btn btn-primary">Gerar Link</button>
<a href="/vendas">Ver tabela</a>
</form>
{% endblock %}

5
start.sh Normal file
View File

@ -0,0 +1,5 @@
#!/bin/sh
# flask --app rifaserver.app apply_migrations
exec gunicorn -b :5001 rifaserver.app:app --access-logfile - --error-logfile -
# CMD ["gunicorn", "-b", ":5000", "rifaserver.app:app", "--access-logfile", "-", "--error-logfile", "-"]

16
test/conftest.py Normal file
View File

@ -0,0 +1,16 @@
import pytest
from rifaserver.app import create_app
from rifaserver.models import db as _db
@pytest.fixture(scope='module')
def test_app():
app = create_app('testing')
with app.app_context():
yield app
@pytest.fixture(scope='module')
def test_db(test_app):
_db.app = test_app
_db.create_all()
yield _db
_db.drop_all()

64
test/test_app.py Normal file
View File

@ -0,0 +1,64 @@
def test_new_purchase(test_app, test_db):
from rifaserver.models import Seller, Purchase
seller = Seller(username='test', password_hash='test')
test_db.session.add(seller)
test_db.session.commit()
with test_app.test_client() as client:
resp = client.post('/seller', data={'amount': 5})
assert resp.status_code == 200
assert Purchase.query.count() == 1
purchase = Purchase.query.first()
assert purchase.total_numbers == 5
assert purchase.numbers_left == 5
def test_ticket_purchase(test_app, test_db):
from rifaserver.models import Seller, Purchase, Raffle, Ticket
seller = Seller(username='test', password_hash='test')
test_db.session.add(seller)
raffle = Raffle(description='test raffle')
test_db.session.add(raffle)
test_db.session.commit()
purchase = Purchase(id='test123', seller_id=seller.id, total_numbers=5, numbers_left=5)
test_db.session.add(purchase)
test_db.session.commit()
with test_app.test_client() as client:
resp = client.post(f'/purchase/{purchase.id}', data={'raffle_id': raffle.id, 'name': 'test buyer', 'contact': 'test contact', 'number': 1})
assert resp.status_code == 200
assert purchase.numbers_left == 4
assert Ticket.query.count() == 1
ticket = Ticket.query.first()
assert ticket.buyer_name == 'test buyer'
assert ticket.buyer_contact == 'test contact'
assert ticket.chosen_number == 1
def test_valid_login(test_app, test_db):
from your_flask_app.models import Seller
seller = Seller(username='test', password_hash='test')
test_db.session.add(seller)
test_db.session.commit()
with test_app.test_client() as client:
resp = client.post('/login', data={'username': 'test', 'password': 'test'}, follow_redirects=True)
assert resp.status_code == 200
assert b'Logged in successfully' in resp.data
def test_invalid_login(test_app):
with test_app.test_client() as client:
resp = client.post('/login', data={'username': 'test', 'password': 'wrong_password'}, follow_redirects=True)
assert resp.status_code == 200
assert b'Invalid username or password' in resp.data
def test_protected_route_while_logged_in(test_app, test_db):
from your_flask_app.models import Seller
seller = Seller(username='test', password_hash='test')
test_db.session.add(seller)
test_db.session.commit()
with test_app.test_client() as client:
client.post('/login', data={'username': 'test', 'password': 'test'}, follow_redirects=True)
resp = client.get('/seller', follow_redirects=True)
assert resp.status_code == 200