bump project

This commit is contained in:
2026-05-01 21:24:21 +03:30
parent dc4b1a1400
commit 139b3e26da
83 changed files with 2455 additions and 18349 deletions

5
.env.example Normal file
View File

@@ -0,0 +1,5 @@
APP_ENV=development
SECRET_KEY=replace-with-a-long-random-secret
DATABASE_URL=sqlite:///instance/face_attendance.sqlite3
FACE_MATCH_TOLERANCE=0.55
MAX_CONTENT_LENGTH=8388608

8
.gitignore vendored
View File

@@ -49,6 +49,9 @@ coverage.xml
*.py.cover
.hypothesis/
.pytest_cache/
pytest-cache-files-*/
.pytest_tmp/
tmp_pytest/
cover/
# Translations
@@ -64,6 +67,9 @@ db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
*.sqlite
*.sqlite3
*.db
# Scrapy stuff:
.scrapy
@@ -149,6 +155,8 @@ activemq-data/
# Environments
.env
.env.*
!.env.example
.envrc
.venv
env/

View File

@@ -1,2 +1,74 @@
# face-attendance
Attendance registration project with facial recognition.
# Face Attendance
A modern Flask attendance application with staff authentication, role-based access control, face enrollment, camera-based recognition, and attendance reporting.
## Roles
- `admin`: manages staff accounts, enrolls people, records attendance, and views reports.
- `operator`: enrolls people, uses the attendance kiosk, and views reports.
- `viewer`: read-only access to people and attendance reports.
Only admins can create staff accounts.
## Requirements
- Python 3.11 or 3.12 is recommended for production.
- Native build prerequisites for `face-recognition`/`dlib` may be required by your OS.
- A production WSGI server such as Gunicorn on Linux or Waitress on Windows.
## Setup
```powershell
py -3.12 -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
Copy-Item .env.example .env
```
Edit `.env` and set a strong `SECRET_KEY`. For production, set `APP_ENV=production` and use a durable database URL.
## Database
Initialize the database with migrations:
```powershell
$env:FLASK_APP = "wsgi:app"
flask db upgrade
flask create-admin
```
## Run Locally
```powershell
$env:FLASK_APP = "wsgi:app"
flask run
```
Open `http://127.0.0.1:5000` and sign in with the admin account created above.
## Tests
```powershell
pytest
```
The tests use an isolated SQLite database and disable CSRF only for test requests.
## Production Notes
- Use HTTPS. Production config enables secure cookies.
- Set `SECRET_KEY`, `DATABASE_URL`, and `APP_ENV=production`.
- Run `flask db upgrade` during deployment.
- Run behind a reverse proxy that forwards HTTPS headers correctly.
- Treat face encodings as biometric data. Restrict database access, define a retention policy, and back up securely.
## What Changed
- Replaced the legacy single-file Flask script with an application factory and Blueprints.
- Added SQLAlchemy models and Alembic migrations.
- Added Flask-Login authentication, admin-created accounts, password hashing, and role checks.
- Replaced raw SQL with ORM queries.
- Removed global enrollment state.
- Rebuilt the UI in English with reusable templates and responsive Bootstrap-based layouts.
- Added CSRF protection, secure cookie settings, security headers, validation, and error pages.
- Added pytest coverage for authentication, roles, models, and attendance recording.

123
app/__init__.py Normal file
View File

@@ -0,0 +1,123 @@
from __future__ import annotations
import logging
import os
from pathlib import Path
from flask import Flask, render_template
from app.config import ProductionConfig, config_by_name
from app.extensions import csrf, db, login_manager, migrate
def create_app(config_name: str | None = None, config_overrides: dict | None = None) -> Flask:
try:
from dotenv import load_dotenv
load_dotenv()
except ImportError:
pass
app = Flask(__name__, instance_relative_config=True)
Path(app.instance_path).mkdir(parents=True, exist_ok=True)
selected_config = config_name or os.getenv("APP_ENV", "development")
config_class = config_by_name.get(selected_config, config_by_name["development"])
app.config.from_object(config_class)
if config_overrides:
app.config.update(config_overrides)
if config_class is ProductionConfig:
validate_production_config(app)
configure_logging(app)
init_extensions(app)
register_blueprints(app)
register_error_handlers(app)
register_security_headers(app)
register_template_context(app)
from app.commands import register_commands
register_commands(app)
return app
def init_extensions(app: Flask) -> None:
from app.models import Account
db.init_app(app)
migrate.init_app(app, db)
csrf.init_app(app)
login_manager.login_view = "auth.login"
login_manager.login_message_category = "warning"
login_manager.init_app(app)
@login_manager.user_loader
def load_user(account_id: str) -> Account | None:
if not account_id.isdigit():
return None
return db.session.get(Account, int(account_id))
def register_blueprints(app: Flask) -> None:
from app.admin.routes import bp as admin_bp
from app.attendance.routes import bp as attendance_bp
from app.auth.routes import bp as auth_bp
from app.main.routes import bp as main_bp
from app.people.routes import bp as people_bp
from app.recognition.routes import bp as recognition_bp
app.register_blueprint(auth_bp)
app.register_blueprint(main_bp)
app.register_blueprint(admin_bp)
app.register_blueprint(people_bp)
app.register_blueprint(attendance_bp)
app.register_blueprint(recognition_bp)
def register_error_handlers(app: Flask) -> None:
@app.errorhandler(403)
def forbidden(error):
return render_template("errors/403.html"), 403
@app.errorhandler(404)
def not_found(error):
return render_template("errors/404.html"), 404
@app.errorhandler(500)
def internal_error(error):
db.session.rollback()
app.logger.exception("Unhandled application error: %s", error)
return render_template("errors/500.html"), 500
def register_security_headers(app: Flask) -> None:
@app.after_request
def set_default_security_headers(response):
response.headers.setdefault("X-Content-Type-Options", "nosniff")
response.headers.setdefault("X-Frame-Options", "SAMEORIGIN")
response.headers.setdefault("Referrer-Policy", "strict-origin-when-cross-origin")
response.headers.setdefault("Permissions-Policy", "camera=(self), microphone=()")
return response
def register_template_context(app: Flask) -> None:
from datetime import datetime, timezone
from app.models import Role
@app.context_processor
def inject_globals():
return {"current_year": datetime.now(timezone.utc).year, "Role": Role}
def configure_logging(app: Flask) -> None:
if app.debug or app.testing:
return
logging.basicConfig(level=logging.INFO)
def validate_production_config(app: Flask) -> None:
if app.config["SECRET_KEY"] in {"", "dev-change-me"}:
raise RuntimeError("SECRET_KEY must be set for production.")

1
app/admin/__init__.py Normal file
View File

@@ -0,0 +1 @@

45
app/admin/forms.py Normal file
View File

@@ -0,0 +1,45 @@
from __future__ import annotations
import re
from flask_wtf import FlaskForm
from wtforms import EmailField, PasswordField, SelectField, StringField, SubmitField
from wtforms.validators import DataRequired, Email, EqualTo, Length, ValidationError
from app.models import Account, Role
class AccountCreateForm(FlaskForm):
full_name = StringField("Full name", validators=[DataRequired(), Length(max=160)])
username = StringField("Username", validators=[DataRequired(), Length(min=3, max=80)])
email = EmailField("Email", validators=[DataRequired(), Email(), Length(max=255)])
role = SelectField(
"Role",
choices=[(Role.OPERATOR.value, "Operator"), (Role.VIEWER.value, "Viewer"), (Role.ADMIN.value, "Admin")],
validators=[DataRequired()],
)
password = PasswordField("Password", validators=[DataRequired(), Length(min=12, max=128)])
confirm_password = PasswordField(
"Confirm password",
validators=[DataRequired(), EqualTo("password", message="Passwords must match.")],
)
submit = SubmitField("Create account")
def validate_username(self, field) -> None:
username = field.data.strip().lower()
if not re.fullmatch(r"[a-z0-9_.-]+", username):
raise ValidationError("Use only letters, numbers, dots, hyphens, and underscores.")
if Account.query.filter_by(username=username).first():
raise ValidationError("That username is already in use.")
field.data = username
def validate_email(self, field) -> None:
email = field.data.strip().lower()
if Account.query.filter_by(email=email).first():
raise ValidationError("That email is already in use.")
field.data = email
def validate_password(self, field) -> None:
password = field.data
if not re.search(r"[A-Z]", password) or not re.search(r"[a-z]", password) or not re.search(r"\d", password):
raise ValidationError("Use at least one uppercase letter, one lowercase letter, and one number.")

52
app/admin/routes.py Normal file
View File

@@ -0,0 +1,52 @@
from __future__ import annotations
from flask import Blueprint, abort, flash, redirect, render_template, url_for
from flask_login import current_user
from app.admin.forms import AccountCreateForm
from app.extensions import db
from app.models import Account, Role
from app.services.security import roles_required
bp = Blueprint("admin", __name__, url_prefix="/admin")
@bp.get("/accounts")
@roles_required(Role.ADMIN)
def accounts():
items = Account.query.order_by(Account.created_at.desc()).all()
return render_template("admin/accounts.html", accounts=items)
@bp.route("/accounts/new", methods=["GET", "POST"])
@roles_required(Role.ADMIN)
def new_account():
form = AccountCreateForm()
if form.validate_on_submit():
account = Account(
full_name=form.full_name.data.strip(),
username=form.username.data.strip().lower(),
email=form.email.data.strip().lower(),
role=form.role.data,
)
account.set_password(form.password.data)
db.session.add(account)
db.session.commit()
flash(f"Account created for {account.full_name}.", "success")
return redirect(url_for("admin.accounts"))
return render_template("admin/account_form.html", form=form)
@bp.post("/accounts/<int:account_id>/toggle")
@roles_required(Role.ADMIN)
def toggle_account(account_id: int):
account = db.session.get(Account, account_id)
if not account:
abort(404)
if account.id == current_user.id:
flash("You cannot deactivate your own account.", "warning")
return redirect(url_for("admin.accounts"))
account.active = not account.active
db.session.commit()
flash(f"{account.full_name} is now {'active' if account.active else 'inactive'}.", "success")
return redirect(url_for("admin.accounts"))

View File

@@ -0,0 +1 @@

60
app/attendance/routes.py Normal file
View File

@@ -0,0 +1,60 @@
from __future__ import annotations
from flask import Blueprint, abort, jsonify, redirect, render_template, request, url_for
from flask_login import current_user
from app.extensions import db
from app.models import AttendanceEvent, AttendanceType, Person, Role
from app.services.attendance import last_presence_summary, record_attendance
from app.services.security import roles_required
bp = Blueprint("attendance", __name__, url_prefix="/attendance")
@bp.get("/")
@roles_required(Role.ADMIN, Role.OPERATOR, Role.VIEWER)
def index():
events = AttendanceEvent.query.order_by(AttendanceEvent.occurred_at.desc()).limit(250).all()
return render_template("attendance/index.html", events=events)
@bp.get("/kiosk")
@roles_required(Role.ADMIN, Role.OPERATOR)
def kiosk():
return render_template("attendance/kiosk.html")
@bp.post("/events")
@roles_required(Role.ADMIN, Role.OPERATOR)
def create_event():
person_id = request.form.get("person_id", type=int)
raw_event_type = request.form.get("event_type", "")
confidence = request.form.get("confidence", type=float)
person = db.session.get(Person, person_id) if person_id else None
if not person or not person.active:
abort(404)
try:
event_type = AttendanceType(raw_event_type)
except ValueError:
abort(400)
event = record_attendance(
person,
event_type,
created_by_id=current_user.id,
confidence=confidence,
source="web-kiosk",
)
if request.accept_mimetypes.accept_json:
return jsonify(
{
"id": event.id,
"event_type": event.type_label,
"occurred_at": event.occurred_at.strftime("%Y-%m-%d %H:%M"),
"summary": last_presence_summary(person),
}
)
return redirect(url_for("attendance.index"))

1
app/auth/__init__.py Normal file
View File

@@ -0,0 +1 @@

12
app/auth/forms.py Normal file
View File

@@ -0,0 +1,12 @@
from __future__ import annotations
from flask_wtf import FlaskForm
from wtforms import BooleanField, PasswordField, StringField, SubmitField
from wtforms.validators import DataRequired, Length
class LoginForm(FlaskForm):
username = StringField("Username", validators=[DataRequired(), Length(max=80)])
password = PasswordField("Password", validators=[DataRequired()])
remember = BooleanField("Remember this device")
submit = SubmitField("Sign in")

39
app/auth/routes.py Normal file
View File

@@ -0,0 +1,39 @@
from __future__ import annotations
from urllib.parse import urlsplit
from flask import Blueprint, flash, redirect, render_template, request, url_for
from flask_login import current_user, login_required, login_user, logout_user
from app.auth.forms import LoginForm
from app.models import Account
bp = Blueprint("auth", __name__, url_prefix="/auth")
@bp.route("/login", methods=["GET", "POST"])
def login():
if current_user.is_authenticated:
return redirect(url_for("main.dashboard"))
form = LoginForm()
if form.validate_on_submit():
username = form.username.data.strip().lower()
account = Account.query.filter_by(username=username).first()
if account and account.active and account.check_password(form.password.data):
login_user(account, remember=form.remember.data)
next_url = request.args.get("next")
if not next_url or urlsplit(next_url).netloc:
next_url = url_for("main.dashboard")
return redirect(next_url)
flash("Invalid username or password.", "danger")
return render_template("auth/login.html", form=form)
@bp.post("/logout")
@login_required
def logout():
logout_user()
flash("You have been signed out.", "info")
return redirect(url_for("auth.login"))

28
app/commands.py Normal file
View File

@@ -0,0 +1,28 @@
from __future__ import annotations
import click
from flask import Flask
from app.extensions import db
from app.models import Account, Role
def register_commands(app: Flask) -> None:
@app.cli.command("create-admin")
@click.option("--username", prompt=True)
@click.option("--email", prompt=True)
@click.option("--full-name", prompt=True)
@click.password_option()
def create_admin(username: str, email: str, full_name: str, password: str) -> None:
"""Create an initial administrator account."""
username = username.strip().lower()
email = email.strip().lower()
if Account.query.filter((Account.username == username) | (Account.email == email)).first():
raise click.ClickException("An account with that username or email already exists.")
account = Account(username=username, email=email, full_name=full_name.strip(), role=Role.ADMIN.value)
account.set_password(password)
db.session.add(account)
db.session.commit()
click.echo(f"Created admin account: {username}")

57
app/config.py Normal file
View File

@@ -0,0 +1,57 @@
from __future__ import annotations
import os
from datetime import timedelta
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
class Config:
SECRET_KEY = os.getenv("SECRET_KEY", "dev-change-me")
SQLALCHEMY_DATABASE_URI = os.getenv(
"DATABASE_URL",
f"sqlite:///{BASE_DIR / 'instance' / 'face_attendance.sqlite3'}",
)
SQLALCHEMY_TRACK_MODIFICATIONS = False
MAX_CONTENT_LENGTH = int(os.getenv("MAX_CONTENT_LENGTH", str(8 * 1024 * 1024)))
FACE_MATCH_TOLERANCE = float(os.getenv("FACE_MATCH_TOLERANCE", "0.55"))
PERMANENT_SESSION_LIFETIME = timedelta(hours=12)
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
REMEMBER_COOKIE_HTTPONLY = True
REMEMBER_COOKIE_DURATION = timedelta(days=14)
WTF_CSRF_TIME_LIMIT = None
class DevelopmentConfig(Config):
DEBUG = True
SESSION_COOKIE_SECURE = False
REMEMBER_COOKIE_SECURE = False
class TestingConfig(Config):
TESTING = True
WTF_CSRF_ENABLED = False
SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"
SERVER_NAME = "localhost.test"
SESSION_COOKIE_SECURE = False
REMEMBER_COOKIE_SECURE = False
class ProductionConfig(Config):
DEBUG = False
TESTING = False
SESSION_COOKIE_SECURE = True
REMEMBER_COOKIE_SECURE = True
config_by_name = {
"development": DevelopmentConfig,
"testing": TestingConfig,
"production": ProductionConfig,
}

17
app/extensions.py Normal file
View File

@@ -0,0 +1,17 @@
from __future__ import annotations
from flask_login import LoginManager
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
from flask_wtf import CSRFProtect
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass
db = SQLAlchemy(model_class=Base)
migrate = Migrate()
login_manager = LoginManager()
csrf = CSRFProtect()

1
app/main/__init__.py Normal file
View File

@@ -0,0 +1 @@

28
app/main/routes.py Normal file
View File

@@ -0,0 +1,28 @@
from __future__ import annotations
from flask import Blueprint, redirect, render_template, url_for
from flask_login import current_user, login_required
from app.models import Account, AttendanceEvent, Person
bp = Blueprint("main", __name__)
@bp.get("/")
def index():
if current_user.is_authenticated:
return redirect(url_for("main.dashboard"))
return redirect(url_for("auth.login"))
@bp.get("/dashboard")
@login_required
def dashboard():
stats = {
"people": Person.query.count(),
"active_people": Person.query.filter_by(active=True).count(),
"events": AttendanceEvent.query.count(),
"accounts": Account.query.count(),
}
latest_events = AttendanceEvent.query.order_by(AttendanceEvent.occurred_at.desc()).limit(8).all()
return render_template("dashboard.html", stats=stats, latest_events=latest_events)

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

@@ -0,0 +1,12 @@
from app.models.account import Account, Role
from app.models.attendance import AttendanceEvent, AttendanceType
from app.models.person import FaceEncoding, Person
__all__ = [
"Account",
"AttendanceEvent",
"AttendanceType",
"FaceEncoding",
"Person",
"Role",
]

68
app/models/account.py Normal file
View File

@@ -0,0 +1,68 @@
from __future__ import annotations
from datetime import datetime, timezone
from enum import Enum
from flask_login import UserMixin
from sqlalchemy import Boolean, DateTime, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from werkzeug.security import check_password_hash, generate_password_hash
from app.extensions import db
def utcnow() -> datetime:
return datetime.now(timezone.utc)
class Role(str, Enum):
ADMIN = "admin"
OPERATOR = "operator"
VIEWER = "viewer"
@property
def label(self) -> str:
return self.value.title()
class Account(UserMixin, db.Model):
__tablename__ = "accounts"
id: Mapped[int] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(String(80), unique=True, index=True, nullable=False)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True, nullable=False)
full_name: Mapped[str] = mapped_column(String(160), nullable=False)
role: Mapped[str] = mapped_column(String(20), default=Role.VIEWER.value, nullable=False)
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, nullable=False)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
default=utcnow,
onupdate=utcnow,
nullable=False,
)
attendance_events = relationship("AttendanceEvent", back_populates="created_by")
@property
def is_active(self) -> bool:
return self.active
def set_password(self, password: str) -> None:
self.password_hash = generate_password_hash(password, method="scrypt")
def check_password(self, password: str) -> bool:
return check_password_hash(self.password_hash, password)
def has_role(self, *roles: str | Role) -> bool:
normalized = {role.value if isinstance(role, Role) else role for role in roles}
return self.role in normalized
@property
def is_admin(self) -> bool:
return self.role == Role.ADMIN.value
@property
def can_operate(self) -> bool:
return self.role in {Role.ADMIN.value, Role.OPERATOR.value}

44
app/models/attendance.py Normal file
View File

@@ -0,0 +1,44 @@
from __future__ import annotations
from datetime import datetime, timezone
from enum import Enum
from sqlalchemy import DateTime, Float, ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.extensions import db
def utcnow() -> datetime:
return datetime.now(timezone.utc)
class AttendanceType(str, Enum):
CHECK_IN = "check_in"
CHECK_OUT = "check_out"
@property
def label(self) -> str:
return "Check in" if self is AttendanceType.CHECK_IN else "Check out"
class AttendanceEvent(db.Model):
__tablename__ = "attendance_events"
id: Mapped[int] = mapped_column(primary_key=True)
person_id: Mapped[int] = mapped_column(ForeignKey("people.id"), nullable=False, index=True)
event_type: Mapped[str] = mapped_column(String(20), nullable=False)
occurred_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, nullable=False, index=True)
source: Mapped[str] = mapped_column(String(80), default="web", nullable=False)
confidence: Mapped[float | None] = mapped_column(Float)
created_by_id: Mapped[int | None] = mapped_column(ForeignKey("accounts.id"), index=True)
person = relationship("Person", back_populates="attendance_events")
created_by = relationship("Account", back_populates="attendance_events")
@property
def type_label(self) -> str:
try:
return AttendanceType(self.event_type).label
except ValueError:
return self.event_type

62
app/models/person.py Normal file
View File

@@ -0,0 +1,62 @@
from __future__ import annotations
import json
from datetime import datetime, timezone
from sqlalchemy import Boolean, DateTime, ForeignKey, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.extensions import db
def utcnow() -> datetime:
return datetime.now(timezone.utc)
class Person(db.Model):
__tablename__ = "people"
id: Mapped[int] = mapped_column(primary_key=True)
full_name: Mapped[str] = mapped_column(String(160), nullable=False, index=True)
employee_id: Mapped[str] = mapped_column(String(80), unique=True, index=True, nullable=False)
notes: Mapped[str | None] = mapped_column(Text)
active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, nullable=False)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
default=utcnow,
onupdate=utcnow,
nullable=False,
)
face_encodings: Mapped[list["FaceEncoding"]] = relationship(
back_populates="person",
cascade="all, delete-orphan",
lazy="selectin",
)
attendance_events = relationship(
"AttendanceEvent",
back_populates="person",
cascade="all, delete-orphan",
lazy="selectin",
)
class FaceEncoding(db.Model):
__tablename__ = "face_encodings"
id: Mapped[int] = mapped_column(primary_key=True)
person_id: Mapped[int] = mapped_column(ForeignKey("people.id"), nullable=False, index=True)
image_slot: Mapped[int] = mapped_column(default=1, nullable=False)
vector_json: Mapped[str] = mapped_column(Text, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, nullable=False)
person: Mapped[Person] = relationship(back_populates="face_encodings")
@property
def vector(self) -> list[float]:
return json.loads(self.vector_json)
@vector.setter
def vector(self, value: list[float]) -> None:
self.vector_json = json.dumps([float(item) for item in value])

1
app/people/__init__.py Normal file
View File

@@ -0,0 +1 @@

24
app/people/forms.py Normal file
View File

@@ -0,0 +1,24 @@
from __future__ import annotations
from flask_wtf import FlaskForm
from wtforms import FileField, StringField, SubmitField, TextAreaField
from wtforms.validators import DataRequired, Length, ValidationError
from app.models import Person
class PersonForm(FlaskForm):
full_name = StringField("Full name", validators=[DataRequired(), Length(max=160)])
employee_id = StringField("Employee ID", validators=[DataRequired(), Length(max=80)])
notes = TextAreaField("Notes", validators=[Length(max=2000)])
photo_1 = FileField("Reference photo 1")
photo_2 = FileField("Reference photo 2")
photo_3 = FileField("Reference photo 3")
photo_4 = FileField("Reference photo 4")
submit = SubmitField("Enroll person")
def validate_employee_id(self, field) -> None:
employee_id = field.data.strip()
if Person.query.filter_by(employee_id=employee_id).first():
raise ValidationError("That employee ID is already enrolled.")
field.data = employee_id

84
app/people/routes.py Normal file
View File

@@ -0,0 +1,84 @@
from __future__ import annotations
from flask import Blueprint, abort, flash, redirect, render_template, request, url_for
from app.extensions import db
from app.models import FaceEncoding, Person, Role
from app.people.forms import PersonForm
from app.services.attendance import last_presence_summary
from app.services.recognition import RecognitionUnavailable, encode_uploaded_image
from app.services.security import roles_required
bp = Blueprint("people", __name__, url_prefix="/people")
@bp.get("/")
@roles_required(Role.ADMIN, Role.OPERATOR, Role.VIEWER)
def index():
people = Person.query.order_by(Person.full_name.asc()).all()
return render_template("people/index.html", people=people)
@bp.route("/new", methods=["GET", "POST"])
@roles_required(Role.ADMIN, Role.OPERATOR)
def new_person():
form = PersonForm()
if form.validate_on_submit():
files = [field.data for field in (form.photo_1, form.photo_2, form.photo_3, form.photo_4) if field.data]
files = [file for file in files if file.filename]
if not files:
flash("Upload at least one reference photo.", "danger")
return render_template("people/form.html", form=form)
try:
vectors = [encode_uploaded_image(file) for file in files]
except RecognitionUnavailable as exc:
flash(str(exc), "danger")
return render_template("people/form.html", form=form), 503
except ValueError as exc:
flash(str(exc), "danger")
return render_template("people/form.html", form=form)
person = Person(
full_name=form.full_name.data.strip(),
employee_id=form.employee_id.data.strip(),
notes=form.notes.data.strip() or None,
)
for slot, vector in enumerate(vectors, start=1):
encoding = FaceEncoding(image_slot=slot)
encoding.vector = vector
person.face_encodings.append(encoding)
db.session.add(person)
db.session.commit()
flash(f"{person.full_name} has been enrolled.", "success")
return redirect(url_for("people.detail", person_id=person.id))
return render_template("people/form.html", form=form)
@bp.get("/<int:person_id>")
@roles_required(Role.ADMIN, Role.OPERATOR, Role.VIEWER)
def detail(person_id: int):
person = db.session.get(Person, person_id)
if not person:
abort(404)
events = person.attendance_events[:]
events.sort(key=lambda event: event.occurred_at, reverse=True)
return render_template(
"people/detail.html",
person=person,
events=events[:20],
presence_summary=last_presence_summary(person),
)
@bp.post("/<int:person_id>/toggle")
@roles_required(Role.ADMIN, Role.OPERATOR)
def toggle_person(person_id: int):
person = db.session.get(Person, person_id)
if not person:
abort(404)
person.active = not person.active
db.session.commit()
flash(f"{person.full_name} is now {'active' if person.active else 'inactive'}.", "success")
return redirect(request.referrer or url_for("people.index"))

View File

@@ -0,0 +1 @@

44
app/recognition/routes.py Normal file
View File

@@ -0,0 +1,44 @@
from __future__ import annotations
from flask import Blueprint, current_app, jsonify, request
from app.models import Person, Role
from app.services.attendance import last_presence_summary
from app.services.recognition import RecognitionUnavailable, recognize_faces
from app.services.security import roles_required
bp = Blueprint("recognition", __name__, url_prefix="/recognition")
@bp.post("/identify")
@roles_required(Role.ADMIN, Role.OPERATOR)
def identify():
image = request.form.get("image", "")
if not image:
return jsonify({"error": "No image was provided."}), 400
people = Person.query.filter_by(active=True).all()
try:
result = recognize_faces(image, people, tolerance=current_app.config["FACE_MATCH_TOLERANCE"])
except RecognitionUnavailable as exc:
return jsonify({"error": str(exc)}), 503
except ValueError as exc:
return jsonify({"error": str(exc)}), 400
faces = []
for face in result.faces:
if not face.person:
faces.append({"matched": False, "confidence": None})
continue
faces.append(
{
"matched": True,
"person_id": face.person.id,
"full_name": face.person.full_name,
"employee_id": face.person.employee_id,
"confidence": round(face.confidence, 3) if face.confidence is not None else None,
"last_presence": last_presence_summary(face.person),
}
)
return jsonify({"faces": faces, "annotated_image": result.annotated_image})

View File

@@ -0,0 +1,51 @@
from __future__ import annotations
from datetime import datetime, timezone
from app.extensions import db
from app.models import AttendanceEvent, AttendanceType, Person
def record_attendance(
person: Person,
event_type: AttendanceType,
*,
created_by_id: int | None = None,
confidence: float | None = None,
source: str = "web",
) -> AttendanceEvent:
event = AttendanceEvent(
person=person,
event_type=event_type.value,
occurred_at=datetime.now(timezone.utc),
created_by_id=created_by_id,
confidence=confidence,
source=source,
)
db.session.add(event)
db.session.commit()
return event
def last_presence_summary(person: Person) -> str:
check_in = (
AttendanceEvent.query.filter_by(person_id=person.id, event_type=AttendanceType.CHECK_IN.value)
.order_by(AttendanceEvent.occurred_at.desc())
.first()
)
check_out = (
AttendanceEvent.query.filter_by(person_id=person.id, event_type=AttendanceType.CHECK_OUT.value)
.order_by(AttendanceEvent.occurred_at.desc())
.first()
)
if not check_in and not check_out:
return "No attendance recorded"
check_in_text = _format_event_time(check_in) if check_in else "no check-in"
check_out_text = _format_event_time(check_out) if check_out else "no check-out"
return f"From {check_in_text} to {check_out_text}"
def _format_event_time(event: AttendanceEvent) -> str:
return event.occurred_at.strftime("%Y-%m-%d %H:%M")

125
app/services/recognition.py Normal file
View File

@@ -0,0 +1,125 @@
from __future__ import annotations
import base64
from dataclasses import dataclass
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from werkzeug.datastructures import FileStorage
from app.models import Person
class RecognitionUnavailable(RuntimeError):
pass
@dataclass(frozen=True)
class DetectedFace:
person: "Person | None"
confidence: float | None
@dataclass(frozen=True)
class RecognitionResult:
faces: list[DetectedFace]
annotated_image: str
def encode_uploaded_image(file: "FileStorage") -> list[float]:
cv2, np, face_recognition = _load_dependencies()
raw = file.read()
image = _decode_bytes(raw, cv2, np)
rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
encodings = face_recognition.face_encodings(rgb)
if len(encodings) == 0:
raise ValueError(f"No face was detected in {file.filename}.")
if len(encodings) > 1:
raise ValueError(f"Upload a single-face photo for {file.filename}.")
return [float(value) for value in encodings[0]]
def recognize_faces(data_url: str, people: list["Person"], *, tolerance: float) -> RecognitionResult:
cv2, np, face_recognition = _load_dependencies()
image = _decode_data_url(data_url, cv2, np)
rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
locations = face_recognition.face_locations(rgb)
encodings = face_recognition.face_encodings(rgb, locations)
candidates = [
(person, encoding.vector)
for person in people
for encoding in person.face_encodings
]
faces: list[DetectedFace] = []
for location, face_encoding in zip(locations, encodings, strict=False):
person = None
confidence = None
label = "Unknown"
if candidates:
vectors = [candidate[1] for candidate in candidates]
distances = face_recognition.face_distance(vectors, face_encoding)
best_index = int(np.argmin(distances))
best_distance = float(distances[best_index])
if best_distance <= tolerance:
person = candidates[best_index][0]
confidence = max(0.0, 1.0 - best_distance)
label = person.full_name
faces.append(DetectedFace(person=person, confidence=confidence))
_draw_face_label(image, location, label, cv2)
if not faces:
return RecognitionResult(faces=[DetectedFace(person=None, confidence=None)], annotated_image=_encode_png(image, cv2))
return RecognitionResult(faces=faces, annotated_image=_encode_png(image, cv2))
def _load_dependencies():
try:
import cv2
import face_recognition
import numpy as np
except ImportError as exc:
raise RecognitionUnavailable(
"Face recognition dependencies are not installed. Install requirements on a supported Python version."
) from exc
return cv2, np, face_recognition
def _decode_data_url(data_url: str, cv2, np):
if "," not in data_url:
raise ValueError("Invalid image payload.")
header, encoded = data_url.split(",", 1)
if not header.startswith("data:image/"):
raise ValueError("Only image uploads are supported.")
try:
raw = base64.b64decode(encoded, validate=True)
except ValueError as exc:
raise ValueError("Invalid image encoding.") from exc
return _decode_bytes(raw, cv2, np)
def _decode_bytes(raw: bytes, cv2, np):
if not raw:
raise ValueError("The uploaded image is empty.")
array = np.frombuffer(raw, np.uint8)
image = cv2.imdecode(array, cv2.IMREAD_COLOR)
if image is None:
raise ValueError("The uploaded file is not a readable image.")
return image
def _draw_face_label(image, location, label: str, cv2) -> None:
top, right, bottom, left = location
cv2.rectangle(image, (left, top), (right, bottom), (30, 136, 229), 2)
cv2.rectangle(image, (left, bottom - 28), (right, bottom), (30, 136, 229), cv2.FILLED)
cv2.putText(image, label[:32], (left + 6, bottom - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (255, 255, 255), 1)
def _encode_png(image, cv2) -> str:
ok, buffer = cv2.imencode(".png", image)
if not ok:
raise ValueError("Could not encode processed image.")
return "data:image/png;base64," + base64.b64encode(buffer).decode("ascii")

28
app/services/security.py Normal file
View File

@@ -0,0 +1,28 @@
from __future__ import annotations
from collections.abc import Callable
from functools import wraps
from typing import TypeVar
from flask import abort
from flask_login import current_user, login_required
from app.models import Role
F = TypeVar("F", bound=Callable)
def roles_required(*roles: str | Role) -> Callable[[F], F]:
allowed_roles = {role.value if isinstance(role, Role) else role for role in roles}
def decorator(view: F) -> F:
@wraps(view)
@login_required
def wrapped(*args, **kwargs):
if not current_user.is_authenticated or current_user.role not in allowed_roles:
abort(403)
return view(*args, **kwargs)
return wrapped # type: ignore[return-value]
return decorator

286
app/static/css/app.css Normal file
View File

@@ -0,0 +1,286 @@
:root {
--surface: #ffffff;
--surface-muted: #f6f7f9;
--border: #d8dee6;
--text: #172033;
--muted: #657084;
--accent: #0f6b5f;
--accent-strong: #0b5a50;
--info: #275f9f;
--warning: #a35f00;
}
body {
min-height: 100vh;
background: var(--surface-muted);
color: var(--text);
}
a {
color: var(--info);
}
.btn-primary {
--bs-btn-bg: var(--accent);
--bs-btn-border-color: var(--accent);
--bs-btn-hover-bg: var(--accent-strong);
--bs-btn-hover-border-color: var(--accent-strong);
}
.page-shell {
padding: 2rem 0 3rem;
}
.flash-stack {
margin-bottom: 1.25rem;
}
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1.5rem;
}
.page-header h1 {
margin: 0;
font-size: clamp(1.75rem, 2vw, 2.35rem);
}
.eyebrow {
margin: 0 0 0.35rem;
color: var(--muted);
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.auth-layout {
min-height: calc(100vh - 4rem);
display: grid;
place-items: center;
}
.auth-panel,
.form-section,
.content-section,
.camera-panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 16px 40px rgba(23, 32, 51, 0.06);
}
.auth-panel {
width: min(100%, 440px);
padding: 2rem;
}
.form-section,
.content-section,
.camera-panel {
padding: 1.25rem;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1rem;
}
.section-header h2,
.content-section h2 {
font-size: 1.15rem;
margin: 0 0 1rem;
}
.section-header h2 {
margin: 0;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.stat-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1.1rem;
}
.stat-card span {
display: block;
color: var(--muted);
font-size: 0.9rem;
}
.stat-card strong {
display: block;
margin-top: 0.35rem;
font-size: 2rem;
line-height: 1;
}
.empty-state {
border: 1px dashed var(--border);
border-radius: 8px;
color: var(--muted);
padding: 1.25rem;
text-align: center;
}
.form-actions {
display: flex;
justify-content: flex-end;
margin-top: 1rem;
}
.form-help {
color: var(--muted);
font-size: 0.9rem;
margin: 0.85rem 0 0;
}
.upload-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 1rem;
margin-top: 0.5rem;
}
.detail-grid,
.kiosk-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(360px, 0.7fr);
gap: 1.25rem;
}
.detail-list {
display: grid;
grid-template-columns: 140px 1fr;
gap: 0.65rem 1rem;
margin: 0;
}
.detail-list dt {
color: var(--muted);
font-weight: 600;
}
.detail-list dd {
margin: 0;
}
.event-list,
.result-list {
display: grid;
gap: 0.75rem;
}
.event-row,
.result-card {
border: 1px solid var(--border);
border-radius: 8px;
padding: 0.85rem;
}
.event-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.camera-frame {
position: relative;
overflow: hidden;
aspect-ratio: 16 / 10;
background: #111827;
border-radius: 8px;
}
.camera-frame video,
.camera-frame img,
.camera-frame canvas {
width: 100%;
height: 100%;
object-fit: cover;
}
.camera-frame img,
.camera-frame canvas {
display: none;
}
.camera-frame.has-preview video {
display: none;
}
.camera-frame.has-preview img {
display: block;
}
.camera-actions {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin: 1rem 0;
}
.result-card h3 {
font-size: 1rem;
margin: 0 0 0.35rem;
}
.result-card p {
color: var(--muted);
margin-bottom: 0.75rem;
}
.result-actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.error-page {
max-width: 620px;
margin: 10vh auto;
text-align: center;
}
@media (max-width: 992px) {
.stats-grid,
.upload-grid,
.detail-grid,
.kiosk-grid {
grid-template-columns: 1fr 1fr;
}
}
@media (max-width: 720px) {
.page-header,
.section-header {
align-items: flex-start;
flex-direction: column;
}
.stats-grid,
.upload-grid,
.detail-grid,
.kiosk-grid {
grid-template-columns: 1fr;
}
.detail-list {
grid-template-columns: 1fr;
}
}

146
app/static/js/kiosk.js Normal file
View File

@@ -0,0 +1,146 @@
const kiosk = document.querySelector(".kiosk-grid");
const video = document.getElementById("cameraStream");
const canvas = document.getElementById("snapshotCanvas");
const preview = document.getElementById("snapshotPreview");
const startButton = document.getElementById("startCamera");
const captureButton = document.getElementById("capturePhoto");
const statusText = document.getElementById("cameraStatus");
const results = document.getElementById("recognitionResults");
const frame = document.querySelector(".camera-frame");
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute("content");
let activeStream = null;
let lastFaces = [];
startButton.addEventListener("click", async () => {
try {
activeStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "user" }, audio: false });
video.srcObject = activeStream;
frame.classList.remove("has-preview");
captureButton.disabled = false;
statusText.textContent = "Camera is active.";
} catch (error) {
statusText.textContent = `Camera access failed: ${error.name}`;
}
});
captureButton.addEventListener("click", async () => {
if (!video.videoWidth || !video.videoHeight) {
statusText.textContent = "Camera is not ready yet.";
return;
}
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext("2d").drawImage(video, 0, 0, canvas.width, canvas.height);
const image = canvas.toDataURL("image/png");
await identify(image);
});
async function identify(image) {
setLoading("Identifying faces...");
const formData = new FormData();
formData.append("image", image);
const response = await fetch(kiosk.dataset.identifyUrl, {
method: "POST",
body: formData,
credentials: "same-origin",
headers: { "X-CSRFToken": csrfToken, "Accept": "application/json" },
});
const payload = await response.json();
if (!response.ok) {
showError(payload.error || "Recognition failed.");
return;
}
lastFaces = payload.faces;
preview.src = payload.annotated_image;
frame.classList.add("has-preview");
renderFaces(payload.faces);
}
function renderFaces(faces) {
results.innerHTML = "";
if (!faces.length) {
results.innerHTML = '<div class="empty-state">No faces were detected.</div>';
return;
}
faces.forEach((face) => {
const card = document.createElement("div");
card.className = "result-card";
if (!face.matched) {
card.innerHTML = "<h3>Unknown person</h3><p>No enrolled person matched this face.</p>";
results.appendChild(card);
return;
}
const confidence = face.confidence === null ? "n/a" : `${Math.round(face.confidence * 100)}%`;
card.innerHTML = `
<h3>${escapeHtml(face.full_name)}</h3>
<p>${escapeHtml(face.employee_id)} · Confidence ${confidence}<br>${escapeHtml(face.last_presence)}</p>
<div class="result-actions">
<button class="btn btn-success btn-sm" data-event-type="check_in" data-person-id="${face.person_id}">Check in</button>
<button class="btn btn-warning btn-sm" data-event-type="check_out" data-person-id="${face.person_id}">Check out</button>
</div>
<div class="small text-secondary mt-2" data-status></div>
`;
results.appendChild(card);
});
}
results.addEventListener("click", async (event) => {
const button = event.target.closest("button[data-event-type]");
if (!button) {
return;
}
const face = lastFaces.find((item) => item.person_id === Number(button.dataset.personId));
const status = button.closest(".result-card").querySelector("[data-status]");
button.disabled = true;
status.textContent = "Recording attendance...";
const formData = new FormData();
formData.append("person_id", button.dataset.personId);
formData.append("event_type", button.dataset.eventType);
if (face && face.confidence !== null) {
formData.append("confidence", face.confidence);
}
const response = await fetch(kiosk.dataset.recordUrl, {
method: "POST",
body: formData,
credentials: "same-origin",
headers: { "X-CSRFToken": csrfToken, "Accept": "application/json" },
});
const payload = await response.json();
button.disabled = false;
if (!response.ok) {
status.textContent = "Could not record attendance.";
return;
}
status.textContent = `${payload.event_type} recorded at ${payload.occurred_at}. ${payload.summary}`;
});
function setLoading(message) {
statusText.textContent = message;
results.innerHTML = `<div class="empty-state">${escapeHtml(message)}</div>`;
}
function showError(message) {
statusText.textContent = message;
results.innerHTML = `<div class="empty-state">${escapeHtml(message)}</div>`;
}
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}

View File

@@ -0,0 +1,31 @@
{% extends "base.html" %}
{% from "macros/forms.html" import render_field, render_select %}
{% block title %}Create account · Face Attendance{% endblock %}
{% block content %}
<div class="page-header">
<div>
<p class="eyebrow">Administration</p>
<h1>Create account</h1>
</div>
<a class="btn btn-outline-secondary" href="{{ url_for('admin.accounts') }}">Back</a>
</div>
<section class="form-section">
<form method="post" novalidate>
{{ form.hidden_tag() }}
<div class="row g-3">
<div class="col-md-6">{{ render_field(form.full_name) }}</div>
<div class="col-md-6">{{ render_field(form.username) }}</div>
<div class="col-md-6">{{ render_field(form.email) }}</div>
<div class="col-md-6">{{ render_select(form.role) }}</div>
<div class="col-md-6">{{ render_field(form.password) }}</div>
<div class="col-md-6">{{ render_field(form.confirm_password) }}</div>
</div>
<div class="form-actions">
{{ form.submit(class="btn btn-primary") }}
</div>
</form>
</section>
{% endblock %}

View File

@@ -0,0 +1,53 @@
{% extends "base.html" %}
{% block title %}Accounts · Face Attendance{% endblock %}
{% block content %}
<div class="page-header">
<div>
<p class="eyebrow">Administration</p>
<h1>Accounts</h1>
</div>
<a class="btn btn-primary" href="{{ url_for('admin.new_account') }}">Create account</a>
</div>
<section class="content-section">
{% if accounts %}
<div class="table-responsive">
<table class="table align-middle">
<thead>
<tr>
<th>Name</th>
<th>Username</th>
<th>Email</th>
<th>Role</th>
<th>Status</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
{% for account in accounts %}
<tr>
<td>{{ account.full_name }}</td>
<td>{{ account.username }}</td>
<td>{{ account.email }}</td>
<td>{{ account.role|title }}</td>
<td><span class="badge text-bg-{{ 'success' if account.active else 'secondary' }}">{{ "Active" if account.active else "Inactive" }}</span></td>
<td class="text-end">
{% if account.id != current_user.id %}
<form method="post" action="{{ url_for('admin.toggle_account', account_id=account.id) }}" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-outline-secondary btn-sm" type="submit">{{ "Deactivate" if account.active else "Activate" }}</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="empty-state">No staff accounts exist yet.</div>
{% endif %}
</section>
{% endblock %}

View File

@@ -0,0 +1,48 @@
{% extends "base.html" %}
{% block title %}Attendance · Face Attendance{% endblock %}
{% block content %}
<div class="page-header">
<div>
<p class="eyebrow">Reports</p>
<h1>Attendance</h1>
</div>
{% if current_user.can_operate %}
<a class="btn btn-primary" href="{{ url_for('attendance.kiosk') }}">Open kiosk</a>
{% endif %}
</div>
<section class="content-section">
{% if events %}
<div class="table-responsive">
<table class="table align-middle">
<thead>
<tr>
<th>Person</th>
<th>Employee ID</th>
<th>Type</th>
<th>Time</th>
<th>Source</th>
<th>Recorded by</th>
</tr>
</thead>
<tbody>
{% for event in events %}
<tr>
<td><a href="{{ url_for('people.detail', person_id=event.person.id) }}">{{ event.person.full_name }}</a></td>
<td>{{ event.person.employee_id }}</td>
<td><span class="badge text-bg-{{ 'success' if event.event_type == 'check_in' else 'warning' }}">{{ event.type_label }}</span></td>
<td>{{ event.occurred_at.strftime("%Y-%m-%d %H:%M") }}</td>
<td>{{ event.source }}</td>
<td>{{ event.created_by.full_name if event.created_by else "System" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="empty-state">No attendance has been recorded yet.</div>
{% endif %}
</section>
{% endblock %}

View File

@@ -0,0 +1,39 @@
{% extends "base.html" %}
{% block title %}Kiosk · Face Attendance{% endblock %}
{% block content %}
<div class="page-header">
<div>
<p class="eyebrow">Recognition</p>
<h1>Attendance kiosk</h1>
</div>
<a class="btn btn-outline-secondary" href="{{ url_for('attendance.index') }}">Reports</a>
</div>
<div class="kiosk-grid" data-identify-url="{{ url_for('recognition.identify') }}" data-record-url="{{ url_for('attendance.create_event') }}">
<section class="camera-panel">
<div class="camera-frame">
<video id="cameraStream" autoplay playsinline muted></video>
<img id="snapshotPreview" alt="Processed camera snapshot">
<canvas id="snapshotCanvas"></canvas>
</div>
<div class="camera-actions">
<button class="btn btn-outline-secondary" id="startCamera" type="button">Start camera</button>
<button class="btn btn-primary" id="capturePhoto" type="button" disabled>Capture and identify</button>
</div>
<p class="text-secondary small mb-0" id="cameraStatus">Camera is not active.</p>
</section>
<section class="content-section result-panel">
<h2>Recognition results</h2>
<div id="recognitionResults" class="result-list">
<div class="empty-state">Capture a face to see matching people.</div>
</div>
</section>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/kiosk.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,27 @@
{% extends "base.html" %}
{% from "macros/forms.html" import render_field %}
{% block title %}Sign in · Face Attendance{% endblock %}
{% block content %}
<div class="auth-layout">
<section class="auth-panel">
<div class="mb-4">
<p class="eyebrow">Secure attendance management</p>
<h1 class="h3 mb-2">Sign in</h1>
<p class="text-secondary mb-0">Use your staff account to access the attendance system.</p>
</div>
<form method="post" novalidate>
{{ form.hidden_tag() }}
{{ render_field(form.username, "username") }}
{{ render_field(form.password, "password") }}
<div class="form-check mb-4">
{{ form.remember(class="form-check-input") }}
{{ form.remember.label(class="form-check-label") }}
</div>
{{ form.submit(class="btn btn-primary w-100") }}
</form>
</section>
</div>
{% endblock %}

65
app/templates/base.html Normal file
View File

@@ -0,0 +1,65 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{% block title %}Face Attendance{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/app.css') }}">
</head>
<body>
{% if current_user.is_authenticated %}
<nav class="navbar navbar-expand-lg border-bottom bg-white sticky-top">
<div class="container-fluid px-4">
<a class="navbar-brand fw-semibold" href="{{ url_for('main.dashboard') }}">Face Attendance</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav" aria-controls="mainNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="mainNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item"><a class="nav-link" href="{{ url_for('main.dashboard') }}">Dashboard</a></li>
{% if current_user.can_operate %}
<li class="nav-item"><a class="nav-link" href="{{ url_for('attendance.kiosk') }}">Kiosk</a></li>
{% endif %}
<li class="nav-item"><a class="nav-link" href="{{ url_for('people.index') }}">People</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('attendance.index') }}">Attendance</a></li>
{% if current_user.is_admin %}
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.accounts') }}">Accounts</a></li>
{% endif %}
</ul>
<div class="d-flex align-items-center gap-3">
<span class="small text-secondary">{{ current_user.full_name }} · {{ current_user.role|title }}</span>
<form method="post" action="{{ url_for('auth.logout') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-outline-secondary btn-sm" type="submit">Sign out</button>
</form>
</div>
</div>
</div>
</nav>
{% endif %}
<main class="page-shell">
<div class="container-fluid px-4">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="flash-stack">
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,67 @@
{% extends "base.html" %}
{% block title %}Dashboard · Face Attendance{% endblock %}
{% block content %}
<div class="page-header">
<div>
<p class="eyebrow">Overview</p>
<h1>Dashboard</h1>
</div>
{% if current_user.can_operate %}
<a class="btn btn-primary" href="{{ url_for('attendance.kiosk') }}">Open kiosk</a>
{% endif %}
</div>
<div class="stats-grid">
<div class="stat-card">
<span>Active people</span>
<strong>{{ stats.active_people }}</strong>
</div>
<div class="stat-card">
<span>Total people</span>
<strong>{{ stats.people }}</strong>
</div>
<div class="stat-card">
<span>Attendance events</span>
<strong>{{ stats.events }}</strong>
</div>
<div class="stat-card">
<span>Staff accounts</span>
<strong>{{ stats.accounts }}</strong>
</div>
</div>
<section class="content-section">
<div class="section-header">
<h2>Recent attendance</h2>
<a href="{{ url_for('attendance.index') }}">View all</a>
</div>
{% if latest_events %}
<div class="table-responsive">
<table class="table align-middle">
<thead>
<tr>
<th>Person</th>
<th>Type</th>
<th>Time</th>
<th>Recorded by</th>
</tr>
</thead>
<tbody>
{% for event in latest_events %}
<tr>
<td>{{ event.person.full_name }}</td>
<td><span class="badge text-bg-{{ 'success' if event.event_type == 'check_in' else 'warning' }}">{{ event.type_label }}</span></td>
<td>{{ event.occurred_at.strftime("%Y-%m-%d %H:%M") }}</td>
<td>{{ event.created_by.full_name if event.created_by else "System" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="empty-state">No attendance has been recorded yet.</div>
{% endif %}
</section>
{% endblock %}

View File

@@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block title %}Access denied · Face Attendance{% endblock %}
{% block content %}
<section class="error-page">
<h1>Access denied</h1>
<p>Your account does not have permission to perform this action.</p>
<a class="btn btn-primary" href="{{ url_for('main.dashboard') }}">Go to dashboard</a>
</section>
{% endblock %}

View File

@@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block title %}Not found · Face Attendance{% endblock %}
{% block content %}
<section class="error-page">
<h1>Page not found</h1>
<p>The page or record you requested does not exist.</p>
<a class="btn btn-primary" href="{{ url_for('main.dashboard') }}">Go to dashboard</a>
</section>
{% endblock %}

View File

@@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block title %}Server error · Face Attendance{% endblock %}
{% block content %}
<section class="error-page">
<h1>Server error</h1>
<p>The application could not complete the request. The error has been logged.</p>
<a class="btn btn-primary" href="{{ url_for('main.dashboard') }}">Go to dashboard</a>
</section>
{% endblock %}

View File

@@ -0,0 +1,35 @@
{% macro render_field(field, placeholder='') %}
<div class="mb-3">
{{ field.label(class="form-label") }}
{{ field(class="form-control" + (" is-invalid" if field.errors else ""), placeholder=placeholder) }}
{% if field.errors %}
<div class="invalid-feedback">
{{ field.errors|join(" ") }}
</div>
{% endif %}
</div>
{% endmacro %}
{% macro render_textarea(field, rows=4) %}
<div class="mb-3">
{{ field.label(class="form-label") }}
{{ field(class="form-control" + (" is-invalid" if field.errors else ""), rows=rows) }}
{% if field.errors %}
<div class="invalid-feedback">
{{ field.errors|join(" ") }}
</div>
{% endif %}
</div>
{% endmacro %}
{% macro render_select(field) %}
<div class="mb-3">
{{ field.label(class="form-label") }}
{{ field(class="form-select" + (" is-invalid" if field.errors else "")) }}
{% if field.errors %}
<div class="invalid-feedback">
{{ field.errors|join(" ") }}
</div>
{% endif %}
</div>
{% endmacro %}

View File

@@ -0,0 +1,46 @@
{% extends "base.html" %}
{% block title %}{{ person.full_name }} · Face Attendance{% endblock %}
{% block content %}
<div class="page-header">
<div>
<p class="eyebrow">Person profile</p>
<h1>{{ person.full_name }}</h1>
<p class="text-secondary mb-0">{{ person.employee_id }} · {{ "Active" if person.active else "Inactive" }}</p>
</div>
<a class="btn btn-outline-secondary" href="{{ url_for('people.index') }}">Back</a>
</div>
<div class="detail-grid">
<section class="content-section">
<h2>Profile</h2>
<dl class="detail-list">
<dt>Employee ID</dt>
<dd>{{ person.employee_id }}</dd>
<dt>Face samples</dt>
<dd>{{ person.face_encodings|length }}</dd>
<dt>Last presence</dt>
<dd>{{ presence_summary }}</dd>
<dt>Notes</dt>
<dd>{{ person.notes or "No notes" }}</dd>
</dl>
</section>
<section class="content-section">
<h2>Recent events</h2>
{% if events %}
<div class="event-list">
{% for event in events %}
<div class="event-row">
<span class="badge text-bg-{{ 'success' if event.event_type == 'check_in' else 'warning' }}">{{ event.type_label }}</span>
<span>{{ event.occurred_at.strftime("%Y-%m-%d %H:%M") }}</span>
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-state">No attendance events for this person.</div>
{% endif %}
</section>
</div>
{% endblock %}

View File

@@ -0,0 +1,39 @@
{% extends "base.html" %}
{% from "macros/forms.html" import render_field, render_textarea %}
{% block title %}Enroll person · Face Attendance{% endblock %}
{% block content %}
<div class="page-header">
<div>
<p class="eyebrow">Enrollment</p>
<h1>Enroll person</h1>
</div>
<a class="btn btn-outline-secondary" href="{{ url_for('people.index') }}">Back</a>
</div>
<section class="form-section">
<form method="post" enctype="multipart/form-data" novalidate>
{{ form.hidden_tag() }}
<div class="row g-3">
<div class="col-md-6">{{ render_field(form.full_name) }}</div>
<div class="col-md-6">{{ render_field(form.employee_id) }}</div>
<div class="col-12">{{ render_textarea(form.notes, 3) }}</div>
</div>
<div class="upload-grid">
{% for field in [form.photo_1, form.photo_2, form.photo_3, form.photo_4] %}
<div>
{{ field.label(class="form-label") }}
{{ field(class="form-control", accept="image/png,image/jpeg") }}
</div>
{% endfor %}
</div>
<p class="form-help">Upload one to four clear single-face photos. More samples usually improve recognition quality.</p>
<div class="form-actions">
{{ form.submit(class="btn btn-primary") }}
</div>
</form>
</section>
{% endblock %}

View File

@@ -0,0 +1,53 @@
{% extends "base.html" %}
{% block title %}People · Face Attendance{% endblock %}
{% block content %}
<div class="page-header">
<div>
<p class="eyebrow">Enrollment</p>
<h1>People</h1>
</div>
{% if current_user.can_operate %}
<a class="btn btn-primary" href="{{ url_for('people.new_person') }}">Enroll person</a>
{% endif %}
</div>
<section class="content-section">
{% if people %}
<div class="table-responsive">
<table class="table align-middle">
<thead>
<tr>
<th>Name</th>
<th>Employee ID</th>
<th>Face samples</th>
<th>Status</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
{% for person in people %}
<tr>
<td><a href="{{ url_for('people.detail', person_id=person.id) }}">{{ person.full_name }}</a></td>
<td>{{ person.employee_id }}</td>
<td>{{ person.face_encodings|length }}</td>
<td><span class="badge text-bg-{{ 'success' if person.active else 'secondary' }}">{{ "Active" if person.active else "Inactive" }}</span></td>
<td class="text-end">
{% if current_user.can_operate %}
<form method="post" action="{{ url_for('people.toggle_person', person_id=person.id) }}" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-outline-secondary btn-sm" type="submit">{{ "Deactivate" if person.active else "Activate" }}</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="empty-state">No people have been enrolled yet.</div>
{% endif %}
</section>
{% endblock %}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

215
index.py
View File

@@ -1,214 +1,7 @@
import cv2, os
import sys
import json
import base64
import sqlite3
import numpy as np
import face_recognition
from jdatetime import date
from time import localtime, strftime
from flask import Flask, render_template, request, abort, Response
from app import create_app
app = Flask(__name__)
newimg_li = [0, 0, 0, 0]
database = "/home/FaceAtt/mysite/database.db"
cnt = sqlite3.connect(database)
con = cnt.cursor()
app = create_app()
def ReadSQL(sql):
cnt = sqlite3.connect(database)
con = cnt.cursor()
return list(con.execute(sql))
def ExecSQL(sql):
cnt = sqlite3.connect(database)
con = cnt.cursor()
try:
con.execute(sql)
cnt.commit()
return "1"
except:
return "0"
def get_plast(id):
ini = ReadSQL(
"""SELECT * FROM attendances WHERE "user_id" = '{}' AND "type" = 'ورود' ORDER BY "id" DESC LIMIT 0,1""".format(
id))
out = ReadSQL(
"""SELECT * FROM attendances WHERE "user_id" = '{}' AND "type" = 'خروج' ORDER BY "id" DESC LIMIT 0,1""".format(
id))
ini = ini[0] if len(ini) != 0 else ("", "", "", "00:00", "00-00-00")
out = out[0] if len(out) != 0 else ("", "", "", "00:00", "00-00-00")
return "از " + ini[4] + " " + ini[3] + " تا " + out[4] + " " + out[3]
def readb64(uri):
encoded_data = uri.split(',')[1]
nparr = np.fromstring(base64.b64decode(encoded_data), np.uint8)
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
return img
@app.route("/")
def hello():
return render_template('index.html',
namep="-----",
navp="-----",
timep="از 00:00 تا 00:00",
)
@app.route("/add", methods=['GET', 'POST'])
def add():
if request.method == 'POST':
global newimg_li
newimg_li = [0, 0, 0, 0]
return "1"
return "0"
@app.route("/one_img", methods=['GET', 'POST'])
@app.route("/two_img", methods=['GET', 'POST'])
@app.route("/tre_img", methods=['GET', 'POST'])
@app.route("/for_img", methods=['GET', 'POST'])
def one_img():
try:
if request.method == 'POST':
global newimg_li
data = request.form['image'].split(',')[0]
if "image/png" not in data and "image/jpeg" not in data:
return "لطفا تصویر وارد کنید."
img = readb64(request.form['image'])
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
face_code = face_recognition.face_encodings(img)
if len(face_code) == 0:
return "چهره ای در تصویر شناسایی نشد."
if len(face_code) > 1:
return "لطفا تصویری با یک چهره وارد کنید."
face_code = list(face_code[0])
if "one" in request.url:
newimg_li[0] = face_code
if "two" in request.url:
if newimg_li[0] == 0:
return "تصویر اول را وارد کنید."
newimg_li[1] = face_code
if "tre" in request.url:
if newimg_li[0] == 0:
return "تصویر اول را وارد کنید."
if newimg_li[1] == 0:
return "تصویر دوم را وارد کنید."
newimg_li[2] = face_code
if "for" in request.url:
if newimg_li[0] == 0:
return "تصویر اول را وارد کنید."
if newimg_li[1] == 0:
return "تصویر دوم را وارد کنید."
if newimg_li[2] == 0:
return "تصویر سوم را وارد کنید."
newimg_li[3] = face_code
return "1"
except:
pass
return '0'
@app.route("/savenew", methods=['GET', 'POST'])
def savenew():
try:
if request.method == 'POST':
name = request.form['name']
nave = request.form['nav']
if name == "":
return "نام را وارد کنید."
if nave == "":
return "کد ملی را وارد کنید."
try:
int(nave)
except:
return "کد ملی باید عدد باشد."
naves = ReadSQL("SELECT * FROM users WHERE nave = '{nav}'".format(nav=nave))
if len(naves) != 0:
return "کد ملی تکراری است."
if 0 in newimg_li:
return "تمامی تصاویر را وارد کنید."
return ExecSQL("INSERT INTO users VALUES (null, '{name}', '{nav}', '{code}')".format(name=name, nav=nave,
code=json.dumps(
newimg_li)))
except:
pass
return '0'
@app.route('/search', methods=['GET', 'POST'])
def search():
try:
img = readb64(request.form['image'])
users = ReadSQL("SELECT * FROM users")
face_codes = face_recognition.face_encodings(img)
out_li = []
l = 0
for face_code in face_codes:
l += 1
for user in users:
if True in face_recognition.compare_faces(json.loads(user[3]), face_code):
au = list(user[:3])
au.append(get_plast(user[0]))
out_li.append(au)
continue
if len(out_li) != l:
out_li.append("0")
if len(out_li) == 0:
out_li.append("0")
locs = face_recognition.face_locations(img)
for loc in locs:
rec = cv2.rectangle(img, (loc[3], loc[0]), (loc[1], loc[2]), (255, 0, 255), 3)
retval, buffer = cv2.imencode('.png', img)
img_base64 = "data:image/png;base64," + str(base64.b64encode(buffer))[2:-1]
out_li.append(img_base64)
return json.dumps(out_li)
except:
return '0'
@app.route("/ini/<path:id>")
@app.route("/out/<path:id>")
def init(id):
if id == "0":
return "00:00"
typ = "ورود" if "ini" in request.url else "خروج"
ExecSQL(
"INSERT INTO attendances VALUES (null, '{}', '{}', '{}', '{}')".format(id, typ, strftime("%H:%M", localtime()),
str(date.today())))
return get_plast(id)
@app.route("/users")
def users_show():
theads = ["نام", "کد ملی"]
tbodys = ReadSQL("SELECT name,nave FROM users")
return render_template('table.html', title="کاربران", columns="[0, 1]", theads=theads, tbodys=tbodys)
@app.route("/atts")
def atts_show():
theads = ["نام", "کد ملی", "نوع", "تاریخ", "زمان"]
tbodys = ReadSQL(
"SELECT users.name, users.nave, type, date, time FROM attendances INNER JOIN users ON attendances.user_id = users.id;")
return render_template('table.html', title="ورود و خروج", columns="[0, 1, 2, 3, 4]", theads=theads, tbodys=tbodys)
if __name__ == "__main__":
app.run()

3
migrations/README Normal file
View File

@@ -0,0 +1,3 @@
Alembic migrations for Face Attendance.
Use `flask db migrate` after model changes and `flask db upgrade` to apply migrations.

43
migrations/alembic.ini Normal file
View File

@@ -0,0 +1,43 @@
[alembic]
script_location = migrations
prepend_sys_path = .
version_path_separator = os
[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

52
migrations/env.py Normal file
View File

@@ -0,0 +1,52 @@
from __future__ import annotations
from logging.config import fileConfig
from alembic import context
from flask import current_app
config = context.config
if config.config_file_name is not None:
fileConfig(config.config_file_name)
target_db = current_app.extensions["migrate"].db
target_metadata = target_db.metadata
def get_engine():
try:
return target_db.get_engine()
except TypeError:
return target_db.engine
def get_engine_url() -> str:
return str(get_engine().url).replace("%", "%%")
def run_migrations_offline() -> None:
context.configure(
url=get_engine_url(),
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
compare_type=True,
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
with get_engine().connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata, compare_type=True)
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 = ${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,93 @@
"""Initial schema.
Revision ID: 0001_initial_schema
Revises:
Create Date: 2026-05-01
"""
from alembic import op
import sqlalchemy as sa
revision = "0001_initial_schema"
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_table(
"accounts",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("username", sa.String(length=80), nullable=False),
sa.Column("email", sa.String(length=255), nullable=False),
sa.Column("full_name", sa.String(length=160), nullable=False),
sa.Column("role", sa.String(length=20), nullable=False),
sa.Column("password_hash", sa.String(length=255), nullable=False),
sa.Column("active", sa.Boolean(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("email"),
sa.UniqueConstraint("username"),
)
op.create_index(op.f("ix_accounts_email"), "accounts", ["email"], unique=False)
op.create_index(op.f("ix_accounts_username"), "accounts", ["username"], unique=False)
op.create_table(
"people",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("full_name", sa.String(length=160), nullable=False),
sa.Column("employee_id", sa.String(length=80), nullable=False),
sa.Column("notes", sa.Text(), nullable=True),
sa.Column("active", sa.Boolean(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("employee_id"),
)
op.create_index(op.f("ix_people_employee_id"), "people", ["employee_id"], unique=False)
op.create_index(op.f("ix_people_full_name"), "people", ["full_name"], unique=False)
op.create_table(
"attendance_events",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("person_id", sa.Integer(), nullable=False),
sa.Column("event_type", sa.String(length=20), nullable=False),
sa.Column("occurred_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("source", sa.String(length=80), nullable=False),
sa.Column("confidence", sa.Float(), nullable=True),
sa.Column("created_by_id", sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(["created_by_id"], ["accounts.id"]),
sa.ForeignKeyConstraint(["person_id"], ["people.id"]),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_attendance_events_created_by_id"), "attendance_events", ["created_by_id"], unique=False)
op.create_index(op.f("ix_attendance_events_occurred_at"), "attendance_events", ["occurred_at"], unique=False)
op.create_index(op.f("ix_attendance_events_person_id"), "attendance_events", ["person_id"], unique=False)
op.create_table(
"face_encodings",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("person_id", sa.Integer(), nullable=False),
sa.Column("image_slot", sa.Integer(), nullable=False),
sa.Column("vector_json", sa.Text(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["person_id"], ["people.id"]),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_face_encodings_person_id"), "face_encodings", ["person_id"], unique=False)
def downgrade() -> None:
op.drop_index(op.f("ix_face_encodings_person_id"), table_name="face_encodings")
op.drop_table("face_encodings")
op.drop_index(op.f("ix_attendance_events_person_id"), table_name="attendance_events")
op.drop_index(op.f("ix_attendance_events_occurred_at"), table_name="attendance_events")
op.drop_index(op.f("ix_attendance_events_created_by_id"), table_name="attendance_events")
op.drop_table("attendance_events")
op.drop_index(op.f("ix_people_full_name"), table_name="people")
op.drop_index(op.f("ix_people_employee_id"), table_name="people")
op.drop_table("people")
op.drop_index(op.f("ix_accounts_username"), table_name="accounts")
op.drop_index(op.f("ix_accounts_email"), table_name="accounts")
op.drop_table("accounts")

10
pyproject.toml Normal file
View File

@@ -0,0 +1,10 @@
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-q -p no:cacheprovider"
[tool.ruff]
line-length = 120
target-version = "py311"
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B"]

16
requirements.txt Normal file
View File

@@ -0,0 +1,16 @@
Flask>=3.0,<4.0
Flask-SQLAlchemy>=3.1,<4.0
Flask-Migrate>=4.0,<5.0
Flask-Login>=0.6,<1.0
Flask-WTF>=1.2,<2.0
WTForms>=3.1,<4.0
email-validator>=2.1,<3.0
python-dotenv>=1.0,<2.0
pytest>=8.0,<9.0
numpy>=1.26,<3.0
opencv-python>=4.9,<5.0
face-recognition==1.3.0
gunicorn>=22.0,<24.0; platform_system != "Windows"
waitress>=3.0,<4.0; platform_system == "Windows"

Binary file not shown.

1
resume.sh Normal file
View File

@@ -0,0 +1 @@
codex resume 019de462-2a30-7452-9bf9-4654918530a5

Binary file not shown.

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,396 +0,0 @@
/*
* The MIT License
* Copyright (c) 2012 Matias Meno <m@tias.me>
*/
@-webkit-keyframes passing-through {
0% {
opacity: 0;
-webkit-transform: translateY(40px);
-moz-transform: translateY(40px);
-ms-transform: translateY(40px);
-o-transform: translateY(40px);
transform: translateY(40px); }
30%, 70% {
opacity: 1;
-webkit-transform: translateY(0px);
-moz-transform: translateY(0px);
-ms-transform: translateY(0px);
-o-transform: translateY(0px);
transform: translateY(0px); }
100% {
opacity: 0;
-webkit-transform: translateY(-40px);
-moz-transform: translateY(-40px);
-ms-transform: translateY(-40px);
-o-transform: translateY(-40px);
transform: translateY(-40px); } }
@-moz-keyframes passing-through {
0% {
opacity: 0;
-webkit-transform: translateY(40px);
-moz-transform: translateY(40px);
-ms-transform: translateY(40px);
-o-transform: translateY(40px);
transform: translateY(40px); }
30%, 70% {
opacity: 1;
-webkit-transform: translateY(0px);
-moz-transform: translateY(0px);
-ms-transform: translateY(0px);
-o-transform: translateY(0px);
transform: translateY(0px); }
100% {
opacity: 0;
-webkit-transform: translateY(-40px);
-moz-transform: translateY(-40px);
-ms-transform: translateY(-40px);
-o-transform: translateY(-40px);
transform: translateY(-40px); } }
@keyframes passing-through {
0% {
opacity: 0;
-webkit-transform: translateY(40px);
-moz-transform: translateY(40px);
-ms-transform: translateY(40px);
-o-transform: translateY(40px);
transform: translateY(40px); }
30%, 70% {
opacity: 1;
-webkit-transform: translateY(0px);
-moz-transform: translateY(0px);
-ms-transform: translateY(0px);
-o-transform: translateY(0px);
transform: translateY(0px); }
100% {
opacity: 0;
-webkit-transform: translateY(-40px);
-moz-transform: translateY(-40px);
-ms-transform: translateY(-40px);
-o-transform: translateY(-40px);
transform: translateY(-40px); } }
@-webkit-keyframes slide-in {
0% {
opacity: 0;
-webkit-transform: translateY(40px);
-moz-transform: translateY(40px);
-ms-transform: translateY(40px);
-o-transform: translateY(40px);
transform: translateY(40px); }
30% {
opacity: 1;
-webkit-transform: translateY(0px);
-moz-transform: translateY(0px);
-ms-transform: translateY(0px);
-o-transform: translateY(0px);
transform: translateY(0px); } }
@-moz-keyframes slide-in {
0% {
opacity: 0;
-webkit-transform: translateY(40px);
-moz-transform: translateY(40px);
-ms-transform: translateY(40px);
-o-transform: translateY(40px);
transform: translateY(40px); }
30% {
opacity: 1;
-webkit-transform: translateY(0px);
-moz-transform: translateY(0px);
-ms-transform: translateY(0px);
-o-transform: translateY(0px);
transform: translateY(0px); } }
@keyframes slide-in {
0% {
opacity: 0;
-webkit-transform: translateY(40px);
-moz-transform: translateY(40px);
-ms-transform: translateY(40px);
-o-transform: translateY(40px);
transform: translateY(40px); }
30% {
opacity: 1;
-webkit-transform: translateY(0px);
-moz-transform: translateY(0px);
-ms-transform: translateY(0px);
-o-transform: translateY(0px);
transform: translateY(0px); } }
@-webkit-keyframes pulse {
0% {
-webkit-transform: scale(1);
-moz-transform: scale(1);
-ms-transform: scale(1);
-o-transform: scale(1);
transform: scale(1); }
10% {
-webkit-transform: scale(1.1);
-moz-transform: scale(1.1);
-ms-transform: scale(1.1);
-o-transform: scale(1.1);
transform: scale(1.1); }
20% {
-webkit-transform: scale(1);
-moz-transform: scale(1);
-ms-transform: scale(1);
-o-transform: scale(1);
transform: scale(1); } }
@-moz-keyframes pulse {
0% {
-webkit-transform: scale(1);
-moz-transform: scale(1);
-ms-transform: scale(1);
-o-transform: scale(1);
transform: scale(1); }
10% {
-webkit-transform: scale(1.1);
-moz-transform: scale(1.1);
-ms-transform: scale(1.1);
-o-transform: scale(1.1);
transform: scale(1.1); }
20% {
-webkit-transform: scale(1);
-moz-transform: scale(1);
-ms-transform: scale(1);
-o-transform: scale(1);
transform: scale(1); } }
@keyframes pulse {
0% {
-webkit-transform: scale(1);
-moz-transform: scale(1);
-ms-transform: scale(1);
-o-transform: scale(1);
transform: scale(1); }
10% {
-webkit-transform: scale(1.1);
-moz-transform: scale(1.1);
-ms-transform: scale(1.1);
-o-transform: scale(1.1);
transform: scale(1.1); }
20% {
-webkit-transform: scale(1);
-moz-transform: scale(1);
-ms-transform: scale(1);
-o-transform: scale(1);
transform: scale(1); } }
.dropzone, .dropzone * {
box-sizing: border-box; }
.dropzone {
min-height: 150px;
border: 2px solid rgba(0, 0, 0, 0.3);
background: white;
padding: 20px 20px; }
.dropzone.dz-clickable {
cursor: pointer; }
.dropzone.dz-clickable * {
cursor: default; }
.dropzone.dz-clickable .dz-message, .dropzone.dz-clickable .dz-message * {
cursor: pointer; }
.dropzone.dz-started .dz-message {
display: none; }
.dropzone.dz-drag-hover {
border-style: solid; }
.dropzone.dz-drag-hover .dz-message {
opacity: 0.5; }
.dropzone .dz-message {
text-align: center;
margin: 2em 0; }
.dropzone .dz-message .dz-button {
background: none;
color: inherit;
border: none;
padding: 0;
font: inherit;
cursor: pointer;
outline: inherit; }
.dropzone .dz-preview {
position: relative;
display: inline-block;
vertical-align: top;
margin: 16px;
min-height: 100px; }
.dropzone .dz-preview:hover {
z-index: 1000; }
.dropzone .dz-preview:hover .dz-details {
opacity: 1; }
.dropzone .dz-preview.dz-file-preview .dz-image {
border-radius: 20px;
background: #999;
background: linear-gradient(to bottom, #eee, #ddd); }
.dropzone .dz-preview.dz-file-preview .dz-details {
opacity: 1; }
.dropzone .dz-preview.dz-image-preview {
background: white; }
.dropzone .dz-preview.dz-image-preview .dz-details {
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
-ms-transition: opacity 0.2s linear;
-o-transition: opacity 0.2s linear;
transition: opacity 0.2s linear; }
.dropzone .dz-preview .dz-remove {
font-size: 14px;
text-align: center;
display: block;
cursor: pointer;
border: none; }
.dropzone .dz-preview .dz-remove:hover {
text-decoration: underline; }
.dropzone .dz-preview:hover .dz-details {
opacity: 1; }
.dropzone .dz-preview .dz-details {
z-index: 20;
position: absolute;
top: 0;
left: 0;
opacity: 0;
font-size: 13px;
min-width: 100%;
max-width: 100%;
padding: 2em 1em;
text-align: center;
color: rgba(0, 0, 0, 0.9);
line-height: 150%; }
.dropzone .dz-preview .dz-details .dz-size {
margin-bottom: 1em;
font-size: 16px; }
.dropzone .dz-preview .dz-details .dz-filename {
white-space: nowrap; }
.dropzone .dz-preview .dz-details .dz-filename:hover span {
border: 1px solid rgba(200, 200, 200, 0.8);
background-color: rgba(255, 255, 255, 0.8); }
.dropzone .dz-preview .dz-details .dz-filename:not(:hover) {
overflow: hidden;
text-overflow: ellipsis; }
.dropzone .dz-preview .dz-details .dz-filename:not(:hover) span {
border: 1px solid transparent; }
.dropzone .dz-preview .dz-details .dz-filename span, .dropzone .dz-preview .dz-details .dz-size span {
background-color: rgba(255, 255, 255, 0.4);
padding: 0 0.4em;
border-radius: 3px; }
.dropzone .dz-preview:hover .dz-image img {
-webkit-transform: scale(1.05, 1.05);
-moz-transform: scale(1.05, 1.05);
-ms-transform: scale(1.05, 1.05);
-o-transform: scale(1.05, 1.05);
transform: scale(1.05, 1.05);
-webkit-filter: blur(8px);
filter: blur(8px); }
.dropzone .dz-preview .dz-image {
border-radius: 20px;
overflow: hidden;
width: 120px;
height: 120px;
position: relative;
display: block;
z-index: 10; }
.dropzone .dz-preview .dz-image img {
display: block; }
.dropzone .dz-preview.dz-success .dz-success-mark {
-webkit-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
-moz-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
-ms-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
-o-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); }
.dropzone .dz-preview.dz-error .dz-error-mark {
opacity: 1;
-webkit-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
-moz-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
-ms-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
-o-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); }
.dropzone .dz-preview .dz-success-mark, .dropzone .dz-preview .dz-error-mark {
pointer-events: none;
opacity: 0;
z-index: 500;
position: absolute;
display: block;
top: 50%;
left: 50%;
margin-left: -27px;
margin-top: -27px; }
.dropzone .dz-preview .dz-success-mark svg, .dropzone .dz-preview .dz-error-mark svg {
display: block;
width: 54px;
height: 54px; }
.dropzone .dz-preview.dz-processing .dz-progress {
opacity: 1;
-webkit-transition: all 0.2s linear;
-moz-transition: all 0.2s linear;
-ms-transition: all 0.2s linear;
-o-transition: all 0.2s linear;
transition: all 0.2s linear; }
.dropzone .dz-preview.dz-complete .dz-progress {
opacity: 0;
-webkit-transition: opacity 0.4s ease-in;
-moz-transition: opacity 0.4s ease-in;
-ms-transition: opacity 0.4s ease-in;
-o-transition: opacity 0.4s ease-in;
transition: opacity 0.4s ease-in; }
.dropzone .dz-preview:not(.dz-processing) .dz-progress {
-webkit-animation: pulse 6s ease infinite;
-moz-animation: pulse 6s ease infinite;
-ms-animation: pulse 6s ease infinite;
-o-animation: pulse 6s ease infinite;
animation: pulse 6s ease infinite; }
.dropzone .dz-preview .dz-progress {
opacity: 1;
z-index: 1000;
pointer-events: none;
position: absolute;
height: 16px;
left: 50%;
top: 50%;
margin-top: -8px;
width: 80px;
margin-left: -40px;
background: rgba(255, 255, 255, 0.9);
-webkit-transform: scale(1);
border-radius: 8px;
overflow: hidden; }
.dropzone .dz-preview .dz-progress .dz-upload {
background: #333;
background: linear-gradient(to bottom, #666, #444);
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 0;
-webkit-transition: width 300ms ease-in-out;
-moz-transition: width 300ms ease-in-out;
-ms-transition: width 300ms ease-in-out;
-o-transition: width 300ms ease-in-out;
transition: width 300ms ease-in-out; }
.dropzone .dz-preview.dz-error .dz-error-message {
display: block; }
.dropzone .dz-preview.dz-error:hover .dz-error-message {
opacity: 1;
pointer-events: auto; }
.dropzone .dz-preview .dz-error-message {
pointer-events: none;
z-index: 1000;
position: absolute;
display: block;
display: none;
opacity: 0;
-webkit-transition: opacity 0.3s ease;
-moz-transition: opacity 0.3s ease;
-ms-transition: opacity 0.3s ease;
-o-transition: opacity 0.3s ease;
transition: opacity 0.3s ease;
border-radius: 8px;
font-size: 13px;
top: 130px;
left: -10px;
width: 140px;
background: #be2626;
background: linear-gradient(to bottom, #be2626, #a92222);
padding: 0.5em 1.2em;
color: white; }
.dropzone .dz-preview .dz-error-message:after {
content: '';
position: absolute;
top: -6px;
left: 64px;
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid #be2626; }

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 KiB

View File

@@ -1,178 +0,0 @@
textarea {
width: calc(100% - 12px);
padding: 5px;
}
.testbox {
display: flex;
justify-content: center;
align-items: center;
height: inherit;
padding: 20px;
}
form {
width: 100%;
padding: 20px;
border-radius: 6px;
background: #fff;
box-shadow: 0 0 8px #000;
}
.banner {
position: relative;
height: 300px;
background-image: url("/uploads/media/default/0001/02/e2502bb5e1dab7d5cc9b011c745033821aad632c.jpeg");
background-size: cover;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
.banner::after {
content: "";
background-color: rgba(0, 0, 0, 0.2);
position: absolute;
width: 100%;
height: 100%;
}
input, select, textarea {
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 3px;
}
input {
width: calc(100% - 10px);
padding: 5px;
}
input[type="date"] {
padding: 4px 5px;
}
textarea {
width: calc(100% - 12px);
padding: 5px;
}
.item:hover p, .item:hover i, .question:hover p, .question label:hover, input:hover::placeholder {
color: #000;
}
.item input:hover, .item select:hover, .item textarea:hover {
border: 1px solid transparent;
box-shadow: 0 0 3px 0 #000;
color: #000;
}
.item {
position: relative;
margin: 10px 0;
}
.item span {
color: red;
}
input[type="date"]::-webkit-inner-spin-button {
display: none;
}
.item i, input[type="date"]::-webkit-calendar-picker-indicator {
position: absolute;
font-size: 20px;
color: #00b33c;
}
.item i {
right: 1%;
top: 30px;
z-index: 1;
}
.week {
display:flex;
justfiy-content:space-between;
}
.colums {
display:flex;
justify-content:space-between;
flex-direction:row;
flex-wrap:wrap;
}
.colums div {
width:48%;
}
[type="date"]::-webkit-calendar-picker-indicator {
right: 1%;
z-index: 2;
opacity: 0;
cursor: pointer;
}
input[type=radio], input[type=checkbox] {
display: none;
}
label.radio {
position: relative;
display: inline-block;
margin: 5px 20px 15px 0;
cursor: pointer;
}
.question span {
margin-left: 30px;
}
.question-answer label {
display: block;
}
label.radio:before {
content: "";
position: absolute;
left: 0;
width: 17px;
height: 17px;
border-radius: 50%;
border: 2px solid #ccc;
}
input[type=radio]:checked + label:before, label.radio:hover:before {
border: 2px solid #000;
}
label.radio:after {
content: "";
position: absolute;
top: 6px;
left: 5px;
width: 8px;
height: 4px;
border: 3px solid #000;
border-top: none;
border-right: none;
transform: rotate(-45deg);
opacity: 0;
}
input[type=radio]:checked + label:after {
opacity: 1;
}
.flax {
display:flex;
justify-content:space-around;
}
.btn-block {
margin-top: 10px;
text-align: center;
}
button {
width: 150px;
padding: 10px;
border: none;
border-radius: 5px;
background: #000;
font-size: 16px;
color: #fff;
cursor: pointer;
}
button:hover {
background: #00b33c;
}
@media (min-width: 568px) {
.name-item, .city-item {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.name-item input, .name-item div {
width: calc(50% - 20px);
}
.name-item div input {
width:97%;}
.name-item div label {
display:block;
padding-bottom:5px;
}
}

View File

@@ -1,23 +0,0 @@
/* fallback */
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url(/static/icons.woff2) format('woff2');
}
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-moz-font-feature-settings: 'liga';
-moz-osx-font-smoothing: grayscale;
}

Binary file not shown.

View File

@@ -1,275 +0,0 @@
document.addEventListener('DOMContentLoaded', function () {
cheangpage();
// References to all the element we will need.
var video = document.querySelector('#camera-stream'),
image = document.querySelector('#snap'),
start_camera = document.querySelector('#start-camera'),
controls = document.querySelector('.controls'),
take_photo_btn = document.querySelector('#take-photo'),
delete_photo_btn = document.querySelector('#delete-photo'),
error_message = document.querySelector('#error-message');
// The getUserMedia interface is used for handling camera input.
// Some browsers need a prefix so here we're covering all the options
navigator.getMedia = ( navigator.getUserMedia ||
navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia ||
navigator.msGetUserMedia);
if(!navigator.getMedia){
displayErrorMessage("Your browser doesn't have support for the navigator.getUserMedia interface.");
}
else{
// Request the camera.
navigator.getMedia(
{
video: true
},
// Success Callback
function(stream){
// Create an object URL for the video stream and
// set it as src of our HTLM video element.
//video.src = window.URL.createObjectURL(stream);
video.srcObject = stream
// Play the video element to start the stream.
video.play();
video.onplay = function() {
showVideo();
};
},
// Error Callback
function(err){
displayErrorMessage("There was an error with accessing the camera stream: " + err.name, err);
}
);
}
// Mobile browsers cannot play video without user input,
// so here we're using a button to start it manually.
start_camera.addEventListener("click", function(e){
e.preventDefault();
// Start video playback manually.
video.play();
showVideo();
});
take_photo_btn.addEventListener("click", function(e){
e.preventDefault();
var snap = takeSnapshot();
// Show image.
image.setAttribute('src', snap);
image.classList.add("visible");
// Enable delete and save buttons
delete_photo_btn.classList.remove("disabled");
// Pause video playback of stream.
video.pause();
search(snap);
});
delete_photo_btn.addEventListener("click", function(e){
e.preventDefault();
// Hide image.
image.setAttribute('src', "");
image.classList.remove("visible");
// Disable delete and save buttons
delete_photo_btn.classList.add("disabled");
// Resume playback of stream.
video.play();
});
function showVideo(){
// Display the video stream and the controls.
hideUI();
video.classList.add("visible");
controls.classList.add("visible");
}
function takeSnapshot(){
// Here we're using a trick that involves a hidden canvas element.
var hidden_canvas = document.querySelector('canvas'),
context = hidden_canvas.getContext('2d');
var width = video.videoWidth,
height = video.videoHeight;
if (width && height) {
// Setup a canvas with the same dimensions as the video.
hidden_canvas.width = width;
hidden_canvas.height = height;
// Make a copy of the current frame in the video on the canvas.
context.drawImage(video, 0, 0, width, height);
// Turn the canvas image into a dataURL that can be used as a src for our photo.
return hidden_canvas.toDataURL('image/png');
}
}
function displayErrorMessage(error_msg, error){
error = error || "";
if(error){
console.error(error);
}
error_message.innerText = error_msg;
hideUI();
error_message.classList.add("visible");
}
function hideUI(){
// Helper function for clearing the app UI.
controls.classList.remove("visible");
start_camera.classList.remove("visible");
video.classList.remove("visible");
snap.classList.remove("visible");
error_message.classList.remove("visible");
}
});
function cheangpage(){
var add_page = document.getElementById('add_page');
var search_page = document.getElementById('search_page');
var add_icon = document.getElementsByClassName('add_icon')[0];
if (add_icon.innerHTML == "add")
{
add_icon.innerHTML = "search";
add_page.style.display = "";
search_page.style.display = "none";
var xhr = new XMLHttpRequest();
xhr.open('POST', '/add', true);
var formData = new FormData();
xhr.onload = function(e) { };
xhr.send(formData);
}else{
add_icon.innerHTML = "add";
add_page.style.display = "none";
search_page.style.display = "";
}
};
function setinit(typ, pid){
if (pid != "0"){
var xhr = new XMLHttpRequest();
xhr.open('GET', '/' + typ + '/' + pid, true);
xhr.onload = function(e) {
document.getElementById("plast" + pid).innerHTML = "آخرین حضور: " + xhr.response;
};
xhr.send();
}};
function cer_per (name, nave, last, pid) {
var per = document.createElement("DIV");
var pname = document.createElement("P");
var pnave = document.createElement("P");
var plast = document.createElement("P");
var lex = document.createElement("DIV");
var ini = document.createElement("A");
var out = document.createElement("A");
pname.innerHTML = "نام: " + name;
pnave.innerHTML = "کد ملی: " + nave;
plast.innerHTML = "آخرین حضور: " + last;
plast.id = "plast" + pid;
lex.className = "flex";
lex.dir = "ltr";
var aa = '<a class="btn effect01" onclick="setinit(\'out\', ' + pid + ')"><span>ثبت خروج</span></a>';
aa = aa + '<a class="btn effect01" onclick="setinit(\'ini\', ' + pid + ')"><span>ثبت ورود</span></a>';
lex.innerHTML = aa;
per.appendChild(pname);
per.appendChild(pnave);
per.appendChild(plast);
lex.appendChild(out);
lex.appendChild(ini);
per.appendChild(lex);
Persons.appendChild(per);
};
function search (data) {
Persons.innerHTML = "<h4>مشخصات فرد</h4>";
// define data and connections
var blob = new Blob([data]);
var url = URL.createObjectURL(blob);
var xhr = new XMLHttpRequest();
xhr.open('POST', '/search', true);
// define new form
var formData = new FormData();
formData.append('image', data);
// action after uploading happens
xhr.onload = function(e) {
var text = xhr.response;
if (text == "0" || text == ""){return;}
var users = JSON.parse(text);
var img = users.pop();
snap.src = img;
for (index in users){
var user = users[index];
if (user == "0") {cer_per("یافت نشد.", "", "00:00", "0");}
else {cer_per(user[1], user[2], user[3], user[0]);}
}
};
// do the uploading
xhr.send(formData);
};
function savenew(){
var fname = document.getElementById("fname");
var fnav = document.getElementById("fnav");
var add_result = document.getElementById("add_result");
var xhr = new XMLHttpRequest();
var formData = new FormData();
formData.append('name', fname.value);
formData.append('nav', fnav.value);
xhr.onload = function(e) {
var text = xhr.response;
if (text == "1") {add_result.innerHTML = "با موفقیت ثبت شد.";add_result.style.color = "green";}
else if (text == "0") {add_result.innerHTML = "مشکلی پیش آمد دوبار امتحان کنید.";add_result.style.color = "red";}
else {add_result.innerHTML = text;add_result.style.color = "red";}
};
xhr.open('POST', '/savenew', true);
xhr.send(formData);
};

View File

@@ -1,341 +0,0 @@
@font-face {
font-family: 'IRANSansWeb';
src: url('/static/IRANSansWeb/IRANSansWeb.eot');
src: url('/static/IRANSansWeb/IRANSansWeb.eot?#iefix') format('embedded-opentype'),
url('/static/IRANSansWeb/IRANSansWeb.svg#IRANSansWeb') format('svg'),
url('/static/IRANSansWeb/IRANSansWeb.ttf') format('truetype'),
url('/static/IRANSansWeb/IRANSansWeb.woff') format('woff'),
url('/static/IRANSansWeb/IRANSansWeb.woff2') format('woff2');
font-weight: normal;
font-style: normal;
}
*{
box-sizing: border-box;
margin: 0;
padding: 0;
}
html{
background-color: #fff;
font-family: 'IRANSansWeb';
color: #333;
}
h3, h4{
font-size: 30px;
color: #2c3e50;
margin: 2% 0;
text-align: center;
}
input{
margin-top: 10px;
font-family: IRANSansWeb;
font-size: 20px;
}
.fleft{
float:left;
}
.fright{
float:right;
}
.tright{
text-align:right;
}
.file{
margin: 20px;
min-width: 200px;
max-width: 200px;
}
.file form{
margin-top: 20px;
}
.container{
min-width: 45%;
max-width: 45%;
margin: 2%;
padding: 20px;
background-color: #efefef;
}
.one_container{
min-width: 90%;
max-width: 90%;
margin: 5%;
padding: 20px;
background-color: #efefef;
}
@media only screen and (max-width: 1010px) {
.container {
min-width: 90%;
max-width: 90%;
margin: 2% auto;
}
.fone{
float:initial;
}
h3, h4{
font-size: 20px;
}
}
.app{
width: 100%;
position: relative;
}
.app #start-camera{
display: none;
border-radius: 3px;
max-width: 400px;
color: #fff;
background-color: #448AFF;
text-decoration: none;
padding: 15px;
opacity: 0.8;
margin: 50px auto;
text-align: center;
}
.app video#camera-stream{
display: none;
width: 100%;
}
.app img#snap{
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 10;
display: none;
}
.app #error-message{
width: 100%;
background-color: #ccc;
color: #9b9b9b;
font-size: 28px;
padding: 200px 100px;
text-align: center;
display: none;
}
.app .controls{
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 20;
display: flex;
align-items: flex-end;
justify-content: space-between;
padding: 30px;
display: none;
}
.app .controls a{
border-radius: 50%;
color: #fff;
background-color: #111;
text-decoration: none;
padding: 15px;
line-height: 0;
opacity: 0.7;
outline: none;
-webkit-tap-highlight-color: transparent;
}
.app .controls a:hover{
opacity: 1;
}
.app .controls a.disabled{
background-color: #555;
opacity: 0.5;
cursor: default;
pointer-events: none;
}
.app .controls a.disabled:hover{
opacity: 0.5;
}
.app .controls a i{
font-size: 18px;
}
.app .controls #take-photo i{
font-size: 32px;
}
.app canvas{
display: none;
}
.app video#camera-stream.visible,
.app img#snap.visible,
.app #error-message.visible
{
display: block;
}
.app .controls.visible{
display: flex;
}
@media(max-width: 1000px){
.container{
margin: 40px;
}
.app #start-camera.visible{
display: block;
}
.app .controls a i{
font-size: 16px;
}
.app .controls #take-photo i{
font-size: 24px;
}
}
@media(max-width: 600px){
.container{
margin: 10px;
}
.app #error-message{
padding: 80px 50px;
font-size: 18px;
}
.app .controls a i{
font-size: 12px;
}
.app .controls #take-photo i{
font-size: 18px;
}
}
//////////////////////////////////
.buttons {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
text-align: center;
width: 100%;
height: 100%;
margin: 0 auto;
/* padding: 2em 0em; */
}
.flex {
display: flex;
margin-top: 20px;
text-align: center;
}
.btn {
user-select: none;
cursor: pointer;
font-size: 14px;
font-weight: 400;
line-height: 45px;
max-width: 160px;
position: relative;
text-decoration: none;
width: 100%;
margin: 5px;
}
.btn:hover {
text-decoration: none;
}
/*btn_background*/
.effect01 {
color: #FFF;
border: 4px solid #000;
box-shadow:0px 0px 0px 1px #000 inset;
background-color: #000;
overflow: hidden;
position: relative;
transition: all 0.3s ease-in-out;
}
.effect01:hover {
border: 4px solid #666;
background-color: #FFF;
box-shadow:0px 0px 0px 4px #EEE inset;
}
/*btn_text*/
.effect01 span {
transition: all 0.2s ease-out;
z-index: 2;
}
.effect01:hover span{
letter-spacing: 0.13em;
color: #333;
}
/*highlight*/
.effect01:after {
background: #FFF;
border: 0px solid #000;
content: "";
height: 155px;
left: -75px;
opacity: .8;
position: absolute;
top: -50px;
-webkit-transform: rotate(35deg);
transform: rotate(35deg);
width: 50px;
transition: all 1s cubic-bezier(0.075, 0.82, 0.165, 1);/*easeOutCirc*/
z-index: 1;
}
.effect01:hover:after {
background: #FFF;
border: 20px solid #000;
opacity: 0;
left: 120%;
-webkit-transform: rotate(40deg);
transform: rotate(40deg);
}
.add_icon_Circle{
cursor: pointer;
border-radius: 50%;
right: 20px;
bottom: 20px;
position: fixed;
user-select: none;
box-shadow: 0px 1px 18px 0px rgba(0,0,0,0.4),
0px 3px 5px -1px rgba(0,0,0,0.2);
background-color: #fff;
width: 50px;
height: 50px;
}
.add_icon{
font-size: 40px;
margin: 5px;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,35 +0,0 @@
/*!
Buttons for DataTables 1.2.1
©2016 SpryMedia Ltd - datatables.net/license
*/
(function(d){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(o){return d(o,window,document)}):"object"===typeof exports?module.exports=function(o,n){o||(o=window);if(!n||!n.fn.dataTable)n=require("datatables.net")(o,n).$;return d(n,o,o.document)}:d(jQuery,window,document)})(function(d,o,n,l){var i=d.fn.dataTable,u=0,v=0,k=i.ext.buttons,m=function(a,b){!0===b&&(b={});d.isArray(b)&&(b={buttons:b});this.c=d.extend(!0,{},m.defaults,b);b.buttons&&(this.c.buttons=b.buttons);
this.s={dt:new i.Api(a),buttons:[],listenKeys:"",namespace:"dtb"+u++};this.dom={container:d("<"+this.c.dom.container.tag+"/>").addClass(this.c.dom.container.className)};this._constructor()};d.extend(m.prototype,{action:function(a,b){var c=this._nodeToButton(a);if(b===l)return c.conf.action;c.conf.action=b;return this},active:function(a,b){var c=this._nodeToButton(a),e=this.c.dom.button.active,c=d(c.node);if(b===l)return c.hasClass(e);c.toggleClass(e,b===l?!0:b);return this},add:function(a,b){var c=
this.s.buttons;if("string"===typeof b){for(var e=b.split("-"),c=this.s,d=0,h=e.length-1;d<h;d++)c=c.buttons[1*e[d]];c=c.buttons;b=1*e[e.length-1]}this._expandButton(c,a,!1,b);this._draw();return this},container:function(){return this.dom.container},disable:function(a){a=this._nodeToButton(a);d(a.node).addClass(this.c.dom.button.disabled);return this},destroy:function(){d("body").off("keyup."+this.s.namespace);var a=this.s.buttons.slice(),b,c;b=0;for(c=a.length;b<c;b++)this.remove(a[b].node);this.dom.container.remove();
a=this.s.dt.settings()[0];b=0;for(c=a.length;b<c;b++)if(a.inst===this){a.splice(b,1);break}return this},enable:function(a,b){if(!1===b)return this.disable(a);var c=this._nodeToButton(a);d(c.node).removeClass(this.c.dom.button.disabled);return this},name:function(){return this.c.name},node:function(a){a=this._nodeToButton(a);return d(a.node)},remove:function(a){var b=this._nodeToButton(a),c=this._nodeToHost(a),e=this.s.dt;if(b.buttons.length)for(var g=b.buttons.length-1;0<=g;g--)this.remove(b.buttons[g].node);
b.conf.destroy&&b.conf.destroy.call(e.button(a),e,d(a),b.conf);this._removeKey(b.conf);d(b.node).remove();a=d.inArray(b,c);c.splice(a,1);return this},text:function(a,b){var c=this._nodeToButton(a),e=this.c.dom.collection.buttonLiner,e=c.inCollection&&e&&e.tag?e.tag:this.c.dom.buttonLiner.tag,g=this.s.dt,h=d(c.node),f=function(a){return"function"===typeof a?a(g,h,c.conf):a};if(b===l)return f(c.conf.text);c.conf.text=b;e?h.children(e).html(f(b)):h.html(f(b));return this},_constructor:function(){var a=
this,b=this.s.dt,c=b.settings()[0],e=this.c.buttons;c._buttons||(c._buttons=[]);c._buttons.push({inst:this,name:this.c.name});for(var c=0,g=e.length;c<g;c++)this.add(e[c]);b.on("destroy",function(){a.destroy()});d("body").on("keyup."+this.s.namespace,function(b){if(!n.activeElement||n.activeElement===n.body){var c=String.fromCharCode(b.keyCode).toLowerCase();a.s.listenKeys.toLowerCase().indexOf(c)!==-1&&a._keypress(c,b)}})},_addKey:function(a){a.key&&(this.s.listenKeys+=d.isPlainObject(a.key)?a.key.key:
a.key)},_draw:function(a,b){a||(a=this.dom.container,b=this.s.buttons);a.children().detach();for(var c=0,d=b.length;c<d;c++)a.append(b[c].inserter),b[c].buttons&&b[c].buttons.length&&this._draw(b[c].collection,b[c].buttons)},_expandButton:function(a,b,c,e){for(var g=this.s.dt,h=0,b=!d.isArray(b)?[b]:b,f=0,q=b.length;f<q;f++){var j=this._resolveExtends(b[f]);if(j)if(d.isArray(j))this._expandButton(a,j,c,e);else{var p=this._buildButton(j,c);if(p){e!==l?(a.splice(e,0,p),e++):a.push(p);if(p.conf.buttons){var s=
this.c.dom.collection;p.collection=d("<"+s.tag+"/>").addClass(s.className);p.conf._collection=p.collection;this._expandButton(p.buttons,p.conf.buttons,!0,e)}j.init&&j.init.call(g.button(p.node),g,d(p.node),j);h++}}}},_buildButton:function(a,b){var c=this.c.dom.button,e=this.c.dom.buttonLiner,g=this.c.dom.collection,h=this.s.dt,f=function(b){return"function"===typeof b?b(h,j,a):b};b&&g.button&&(c=g.button);b&&g.buttonLiner&&(e=g.buttonLiner);if(a.available&&!a.available(h,a))return!1;var q=function(a,
b,c,e){e.action.call(b.button(c),a,b,c,e);d(b.table().node()).triggerHandler("buttons-action.dt",[b.button(c),b,c,e])},j=d("<"+c.tag+"/>").addClass(c.className).attr("tabindex",this.s.dt.settings()[0].iTabIndex).attr("aria-controls",this.s.dt.table().node().id).on("click.dtb",function(b){b.preventDefault();!j.hasClass(c.disabled)&&a.action&&q(b,h,j,a);j.blur()}).on("keyup.dtb",function(b){b.keyCode===13&&!j.hasClass(c.disabled)&&a.action&&q(b,h,j,a)});"a"===c.tag.toLowerCase()&&j.attr("href","#");
e.tag?(g=d("<"+e.tag+"/>").html(f(a.text)).addClass(e.className),"a"===e.tag.toLowerCase()&&g.attr("href","#"),j.append(g)):j.html(f(a.text));!1===a.enabled&&j.addClass(c.disabled);a.className&&j.addClass(a.className);a.titleAttr&&j.attr("title",a.titleAttr);a.namespace||(a.namespace=".dt-button-"+v++);e=(e=this.c.dom.buttonContainer)&&e.tag?d("<"+e.tag+"/>").addClass(e.className).append(j):j;this._addKey(a);return{conf:a,node:j.get(0),inserter:e,buttons:[],inCollection:b,collection:null}},_nodeToButton:function(a,
b){b||(b=this.s.buttons);for(var c=0,d=b.length;c<d;c++){if(b[c].node===a)return b[c];if(b[c].buttons.length){var g=this._nodeToButton(a,b[c].buttons);if(g)return g}}},_nodeToHost:function(a,b){b||(b=this.s.buttons);for(var c=0,d=b.length;c<d;c++){if(b[c].node===a)return b;if(b[c].buttons.length){var g=this._nodeToHost(a,b[c].buttons);if(g)return g}}},_keypress:function(a,b){var c=function(e){for(var g=0,h=e.length;g<h;g++){var f=e[g].conf,q=e[g].node;if(f.key)if(f.key===a)d(q).click();else if(d.isPlainObject(f.key)&&
f.key.key===a&&(!f.key.shiftKey||b.shiftKey))if(!f.key.altKey||b.altKey)if(!f.key.ctrlKey||b.ctrlKey)(!f.key.metaKey||b.metaKey)&&d(q).click();e[g].buttons.length&&c(e[g].buttons)}};c(this.s.buttons)},_removeKey:function(a){if(a.key){var b=d.isPlainObject(a.key)?a.key.key:a.key,a=this.s.listenKeys.split(""),b=d.inArray(b,a);a.splice(b,1);this.s.listenKeys=a.join("")}},_resolveExtends:function(a){for(var b=this.s.dt,c,e,g=function(c){for(var e=0;!d.isPlainObject(c)&&!d.isArray(c);){if(c===l)return;
if("function"===typeof c){if(c=c(b,a),!c)return!1}else if("string"===typeof c){if(!k[c])throw"Unknown button type: "+c;c=k[c]}e++;if(30<e)throw"Buttons: Too many iterations";}return d.isArray(c)?c:d.extend({},c)},a=g(a);a&&a.extend;){if(!k[a.extend])throw"Cannot extend unknown button type: "+a.extend;var h=g(k[a.extend]);if(d.isArray(h))return h;if(!h)return!1;c=h.className;a=d.extend({},h,a);c&&a.className!==c&&(a.className=c+" "+a.className);var f=a.postfixButtons;if(f){a.buttons||(a.buttons=[]);
c=0;for(e=f.length;c<e;c++)a.buttons.push(f[c]);a.postfixButtons=null}if(f=a.prefixButtons){a.buttons||(a.buttons=[]);c=0;for(e=f.length;c<e;c++)a.buttons.splice(c,0,f[c]);a.prefixButtons=null}a.extend=h.extend}return a}});m.background=function(a,b,c){c===l&&(c=400);a?d("<div/>").addClass(b).css("display","none").appendTo("body").fadeIn(c):d("body > div."+b).fadeOut(c,function(){d(this).remove()})};m.instanceSelector=function(a,b){if(!a)return d.map(b,function(a){return a.inst});var c=[],e=d.map(b,
function(a){return a.name}),g=function(a){if(d.isArray(a))for(var f=0,q=a.length;f<q;f++)g(a[f]);else"string"===typeof a?-1!==a.indexOf(",")?g(a.split(",")):(a=d.inArray(d.trim(a),e),-1!==a&&c.push(b[a].inst)):"number"===typeof a&&c.push(b[a].inst)};g(a);return c};m.buttonSelector=function(a,b){for(var c=[],e=function(a,b,c){for(var d,g,f=0,h=b.length;f<h;f++)if(d=b[f])g=c!==l?c+f:f+"",a.push({node:d.node,name:d.conf.name,idx:g}),d.buttons&&e(a,d.buttons,g+"-")},g=function(a,b){var f,h,i=[];e(i,b.s.buttons);
f=d.map(i,function(a){return a.node});if(d.isArray(a)||a instanceof d){f=0;for(h=a.length;f<h;f++)g(a[f],b)}else if(null===a||a===l||"*"===a){f=0;for(h=i.length;f<h;f++)c.push({inst:b,node:i[f].node})}else if("number"===typeof a)c.push({inst:b,node:b.s.buttons[a].node});else if("string"===typeof a)if(-1!==a.indexOf(",")){i=a.split(",");f=0;for(h=i.length;f<h;f++)g(d.trim(i[f]),b)}else if(a.match(/^\d+(\-\d+)*$/))f=d.map(i,function(a){return a.idx}),c.push({inst:b,node:i[d.inArray(a,f)].node});else if(-1!==
a.indexOf(":name")){var k=a.replace(":name","");f=0;for(h=i.length;f<h;f++)i[f].name===k&&c.push({inst:b,node:i[f].node})}else d(f).filter(a).each(function(){c.push({inst:b,node:this})});else"object"===typeof a&&a.nodeName&&(i=d.inArray(a,f),-1!==i&&c.push({inst:b,node:f[i]}))},h=0,f=a.length;h<f;h++)g(b,a[h]);return c};m.defaults={buttons:["copy","excel","csv","pdf","print"],name:"main",tabIndex:0,dom:{container:{tag:"div",className:"dt-buttons"},collection:{tag:"div",className:"dt-button-collection"},
button:{tag:"a",className:"dt-button",active:"active",disabled:"disabled"},buttonLiner:{tag:"span",className:""}}};m.version="1.2.1";d.extend(k,{collection:{text:function(a){return a.i18n("buttons.collection","Collection")},className:"buttons-collection",action:function(a,b,c,e){var a=c.offset(),g=d(b.table().container()),h=!1;d("div.dt-button-background").length&&(h=d("div.dt-button-collection").offset(),d("body").trigger("click.dtb-collection"));e._collection.addClass(e.collectionLayout).css("display",
"none").appendTo("body").fadeIn(e.fade);var f=e._collection.css("position");h&&"absolute"===f?e._collection.css({top:h.top+5,left:h.left+5}):"absolute"===f?(e._collection.css({top:a.top+c.outerHeight(),left:a.left}),c=a.left+e._collection.outerWidth(),g=g.offset().left+g.width(),c>g&&e._collection.css("left",a.left-(c-g))):(a=e._collection.height()/2,a>d(o).height()/2&&(a=d(o).height()/2),e._collection.css("marginTop",-1*a));e.background&&m.background(!0,e.backgroundClassName,e.fade);setTimeout(function(){d("div.dt-button-background").on("click.dtb-collection",
function(){});d("body").on("click.dtb-collection",function(a){if(!d(a.target).parents().andSelf().filter(e._collection).length){e._collection.fadeOut(e.fade,function(){e._collection.detach()});d("div.dt-button-background").off("click.dtb-collection");m.background(false,e.backgroundClassName,e.fade);d("body").off("click.dtb-collection");b.off("buttons-action.b-internal")}})},10);if(e.autoClose)b.on("buttons-action.b-internal",function(){d("div.dt-button-background").click()})},background:!0,collectionLayout:"",
backgroundClassName:"dt-button-background",autoClose:!1,fade:400},copy:function(a,b){if(k.copyHtml5)return"copyHtml5";if(k.copyFlash&&k.copyFlash.available(a,b))return"copyFlash"},csv:function(a,b){if(k.csvHtml5&&k.csvHtml5.available(a,b))return"csvHtml5";if(k.csvFlash&&k.csvFlash.available(a,b))return"csvFlash"},excel:function(a,b){if(k.excelHtml5&&k.excelHtml5.available(a,b))return"excelHtml5";if(k.excelFlash&&k.excelFlash.available(a,b))return"excelFlash"},pdf:function(a,b){if(k.pdfHtml5&&k.pdfHtml5.available(a,
b))return"pdfHtml5";if(k.pdfFlash&&k.pdfFlash.available(a,b))return"pdfFlash"},pageLength:function(a){var a=a.settings()[0].aLengthMenu,b=d.isArray(a[0])?a[0]:a,c=d.isArray(a[0])?a[1]:a,e=function(a){return a.i18n("buttons.pageLength",{"-1":"Show all rows",_:"Show %d rows"},a.page.len())};return{extend:"collection",text:e,className:"buttons-page-length",autoClose:!0,buttons:d.map(b,function(a,b){return{text:c[b],action:function(b,c){c.page.len(a).draw()},init:function(b,c,d){var e=this,c=function(){e.active(b.page.len()===
a)};b.on("length.dt"+d.namespace,c);c()},destroy:function(a,b,c){a.off("length.dt"+c.namespace)}}}),init:function(a,b,c){var d=this;a.on("length.dt"+c.namespace,function(){d.text(e(a))})},destroy:function(a,b,c){a.off("length.dt"+c.namespace)}}}});i.Api.register("buttons()",function(a,b){b===l&&(b=a,a=l);return this.iterator(!0,"table",function(c){if(c._buttons)return m.buttonSelector(m.instanceSelector(a,c._buttons),b)},!0)});i.Api.register("button()",function(a,b){var c=this.buttons(a,b);1<c.length&&
c.splice(1,c.length);return c});i.Api.registerPlural("buttons().active()","button().active()",function(a){return a===l?this.map(function(a){return a.inst.active(a.node)}):this.each(function(b){b.inst.active(b.node,a)})});i.Api.registerPlural("buttons().action()","button().action()",function(a){return a===l?this.map(function(a){return a.inst.action(a.node)}):this.each(function(b){b.inst.action(b.node,a)})});i.Api.register(["buttons().enable()","button().enable()"],function(a){return this.each(function(b){b.inst.enable(b.node,
a)})});i.Api.register(["buttons().disable()","button().disable()"],function(){return this.each(function(a){a.inst.disable(a.node)})});i.Api.registerPlural("buttons().nodes()","button().node()",function(){var a=d();d(this.each(function(b){a=a.add(b.inst.node(b.node))}));return a});i.Api.registerPlural("buttons().text()","button().text()",function(a){return a===l?this.map(function(a){return a.inst.text(a.node)}):this.each(function(b){b.inst.text(b.node,a)})});i.Api.registerPlural("buttons().trigger()",
"button().trigger()",function(){return this.each(function(a){a.inst.node(a.node).trigger("click")})});i.Api.registerPlural("buttons().containers()","buttons().container()",function(){var a=d();d(this.each(function(b){a=a.add(b.inst.container())}));return a});i.Api.register("button().add()",function(a,b){1===this.length&&this[0].inst.add(b,a);return this.button(a)});i.Api.register("buttons().destroy()",function(){this.pluck("inst").unique().each(function(a){a.destroy()});return this});i.Api.registerPlural("buttons().remove()",
"buttons().remove()",function(){this.each(function(a){a.inst.remove(a.node)});return this});var r;i.Api.register("buttons.info()",function(a,b,c){var e=this;if(!1===a)return d("#datatables_buttons_info").fadeOut(function(){d(this).remove()}),clearTimeout(r),r=null,this;r&&clearTimeout(r);d("#datatables_buttons_info").length&&d("#datatables_buttons_info").remove();d('<div id="datatables_buttons_info" class="dt-button-info"/>').html(a?"<h2>"+a+"</h2>":"").append(d("<div/>")["string"===typeof b?"html":
"append"](b)).css("display","none").appendTo("body").fadeIn();c!==l&&0!==c&&(r=setTimeout(function(){e.buttons.info(!1)},c));return this});i.Api.register("buttons.exportData()",function(a){if(this.context.length){for(var b=new i.Api(this.context[0]),c=d.extend(!0,{},{rows:null,columns:"",modifier:{search:"applied",order:"applied"},orthogonal:"display",stripHtml:!0,stripNewlines:!0,decodeEntities:!0,trim:!0,format:{header:function(a){return e(a)},footer:function(a){return e(a)},body:function(a){return e(a)}}},
a),e=function(a){if("string"!==typeof a)return a;c.stripHtml&&(a=a.replace(/<[^>]*>/g,""));c.trim&&(a=a.replace(/^\s+|\s+$/g,""));c.stripNewlines&&(a=a.replace(/\n/g," "));c.decodeEntities&&(t.innerHTML=a,a=t.value);return a},a=b.columns(c.columns).indexes().map(function(a){return c.format.header(b.column(a).header().innerHTML,a)}).toArray(),g=b.table().footer()?b.columns(c.columns).indexes().map(function(a){var d=b.column(a).footer();return c.format.footer(d?d.innerHTML:"",a)}).toArray():null,h=
b.rows(c.rows,c.modifier).indexes().toArray(),h=b.cells(h,c.columns).render(c.orthogonal).toArray(),f=a.length,k=0<f?h.length/f:0,j=Array(k),m=0,l=0;l<k;l++){for(var o=Array(f),n=0;n<f;n++)o[n]=c.format.body(h[m],n,l),m++;j[l]=o}return{header:a,footer:g,body:j}}});var t=d("<textarea/>")[0];d.fn.dataTable.Buttons=m;d.fn.DataTable.Buttons=m;d(n).on("init.dt plugin-init.dt",function(a,b){if("dt"===a.namespace){var c=b.oInit.buttons||i.defaults.buttons;c&&!b._buttons&&(new m(b,c)).container()}});i.ext.feature.push({fnInit:function(a){var a=
new i.Api(a),b=a.init().buttons||i.defaults.buttons;return(new m(a,b)).container()},cFeature:"B"});return m});

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,166 +0,0 @@
/*!
DataTables 1.10.12
©2008-2015 SpryMedia Ltd - datatables.net/license
*/
(function(h){"function"===typeof define&&define.amd?define(["jquery"],function(D){return h(D,window,document)}):"object"===typeof exports?module.exports=function(D,I){D||(D=window);I||(I="undefined"!==typeof window?require("jquery"):require("jquery")(D));return h(I,D,D.document)}:h(jQuery,window,document)})(function(h,D,I,k){function X(a){var b,c,d={};h.each(a,function(e){if((b=e.match(/^([^A-Z]+?)([A-Z])/))&&-1!=="a aa ai ao as b fn i m o s ".indexOf(b[1]+" "))c=e.replace(b[0],b[2].toLowerCase()),
d[c]=e,"o"===b[1]&&X(a[e])});a._hungarianMap=d}function K(a,b,c){a._hungarianMap||X(a);var d;h.each(b,function(e){d=a._hungarianMap[e];if(d!==k&&(c||b[d]===k))"o"===d.charAt(0)?(b[d]||(b[d]={}),h.extend(!0,b[d],b[e]),K(a[d],b[d],c)):b[d]=b[e]})}function Da(a){var b=m.defaults.oLanguage,c=a.sZeroRecords;!a.sEmptyTable&&(c&&"هیچ اطلاعاتی در این جدول نیست."===b.sEmptyTable)&&E(a,a,"sZeroRecords","sEmptyTable");!a.sLoadingRecords&&(c&&"Loading..."===b.sLoadingRecords)&&E(a,a,"sZeroRecords","sLoadingRecords");
a.sInfoThousands&&(a.sThousands=a.sInfoThousands);(a=a.sDecimal)&&db(a)}function eb(a){A(a,"ordering","bSort");A(a,"orderMulti","bSortMulti");A(a,"orderClasses","bSortClasses");A(a,"orderCellsTop","bSortCellsTop");A(a,"order","aaSorting");A(a,"orderFixed","aaSortingFixed");A(a,"paging","bPaginate");A(a,"pagingType","sPaginationType");A(a,"pageLength","iDisplayLength");A(a,"searching","bFilter");"boolean"===typeof a.sScrollX&&(a.sScrollX=a.sScrollX?"100%":"");"boolean"===typeof a.scrollX&&(a.scrollX=
a.scrollX?"100%":"");if(a=a.aoSearchCols)for(var b=0,c=a.length;b<c;b++)a[b]&&K(m.models.oSearch,a[b])}function fb(a){A(a,"orderable","bSortable");A(a,"orderData","aDataSort");A(a,"orderSequence","asSorting");A(a,"orderDataType","sortDataType");var b=a.aDataSort;b&&!h.isArray(b)&&(a.aDataSort=[b])}function gb(a){if(!m.__browser){var b={};m.__browser=b;var c=h("<div/>").css({position:"fixed",top:0,left:0,height:1,width:1,overflow:"hidden"}).append(h("<div/>").css({position:"absolute",top:1,left:1,
width:100,overflow:"scroll"}).append(h("<div/>").css({width:"100%",height:10}))).appendTo("body"),d=c.children(),e=d.children();b.barWidth=d[0].offsetWidth-d[0].clientWidth;b.bScrollOversize=100===e[0].offsetWidth&&100!==d[0].clientWidth;b.bScrollbarLeft=1!==Math.round(e.offset().left);b.bBounding=c[0].getBoundingClientRect().width?!0:!1;c.remove()}h.extend(a.oBrowser,m.__browser);a.oScroll.iBarWidth=m.__browser.barWidth}function hb(a,b,c,d,e,f){var g,j=!1;c!==k&&(g=c,j=!0);for(;d!==e;)a.hasOwnProperty(d)&&
(g=j?b(g,a[d],d,a):a[d],j=!0,d+=f);return g}function Ea(a,b){var c=m.defaults.column,d=a.aoColumns.length,c=h.extend({},m.models.oColumn,c,{nTh:b?b:I.createElement("th"),sTitle:c.sTitle?c.sTitle:b?b.innerHTML:"",aDataSort:c.aDataSort?c.aDataSort:[d],mData:c.mData?c.mData:d,idx:d});a.aoColumns.push(c);c=a.aoPreSearchCols;c[d]=h.extend({},m.models.oSearch,c[d]);ja(a,d,h(b).data())}function ja(a,b,c){var b=a.aoColumns[b],d=a.oClasses,e=h(b.nTh);if(!b.sWidthOrig){b.sWidthOrig=e.attr("width")||null;var f=
(e.attr("style")||"").match(/width:\s*(\d+[pxem%]+)/);f&&(b.sWidthOrig=f[1])}c!==k&&null!==c&&(fb(c),K(m.defaults.column,c),c.mDataProp!==k&&!c.mData&&(c.mData=c.mDataProp),c.sType&&(b._sManualType=c.sType),c.className&&!c.sClass&&(c.sClass=c.className),h.extend(b,c),E(b,c,"sWidth","sWidthOrig"),c.iDataSort!==k&&(b.aDataSort=[c.iDataSort]),E(b,c,"aDataSort"));var g=b.mData,j=Q(g),i=b.mRender?Q(b.mRender):null,c=function(a){return"string"===typeof a&&-1!==a.indexOf("@")};b._bAttrSrc=h.isPlainObject(g)&&
(c(g.sort)||c(g.type)||c(g.filter));b._setter=null;b.fnGetData=function(a,b,c){var d=j(a,b,k,c);return i&&b?i(d,b,a,c):d};b.fnSetData=function(a,b,c){return R(g)(a,b,c)};"number"!==typeof g&&(a._rowReadObject=!0);a.oFeatures.bSort||(b.bSortable=!1,e.addClass(d.sSortableNone));a=-1!==h.inArray("asc",b.asSorting);c=-1!==h.inArray("desc",b.asSorting);!b.bSortable||!a&&!c?(b.sSortingClass=d.sSortableNone,b.sSortingClassJUI=""):a&&!c?(b.sSortingClass=d.sSortableAsc,b.sSortingClassJUI=d.sSortJUIAscAllowed):
!a&&c?(b.sSortingClass=d.sSortableDesc,b.sSortingClassJUI=d.sSortJUIDescAllowed):(b.sSortingClass=d.sSortable,b.sSortingClassJUI=d.sSortJUI)}function Y(a){if(!1!==a.oFeatures.bAutoWidth){var b=a.aoColumns;Fa(a);for(var c=0,d=b.length;c<d;c++)b[c].nTh.style.width=b[c].sWidth}b=a.oScroll;(""!==b.sY||""!==b.sX)&&ka(a);u(a,null,"column-sizing",[a])}function Z(a,b){var c=la(a,"bVisible");return"number"===typeof c[b]?c[b]:null}function $(a,b){var c=la(a,"bVisible"),c=h.inArray(b,c);return-1!==c?c:null}
function aa(a){var b=0;h.each(a.aoColumns,function(a,d){d.bVisible&&"none"!==h(d.nTh).css("display")&&b++});return b}function la(a,b){var c=[];h.map(a.aoColumns,function(a,e){a[b]&&c.push(e)});return c}function Ga(a){var b=a.aoColumns,c=a.aoData,d=m.ext.type.detect,e,f,g,j,i,h,l,q,t;e=0;for(f=b.length;e<f;e++)if(l=b[e],t=[],!l.sType&&l._sManualType)l.sType=l._sManualType;else if(!l.sType){g=0;for(j=d.length;g<j;g++){i=0;for(h=c.length;i<h;i++){t[i]===k&&(t[i]=B(a,i,e,"type"));q=d[g](t[i],a);if(!q&&
g!==d.length-1)break;if("html"===q)break}if(q){l.sType=q;break}}l.sType||(l.sType="string")}}function ib(a,b,c,d){var e,f,g,j,i,n,l=a.aoColumns;if(b)for(e=b.length-1;0<=e;e--){n=b[e];var q=n.targets!==k?n.targets:n.aTargets;h.isArray(q)||(q=[q]);f=0;for(g=q.length;f<g;f++)if("number"===typeof q[f]&&0<=q[f]){for(;l.length<=q[f];)Ea(a);d(q[f],n)}else if("number"===typeof q[f]&&0>q[f])d(l.length+q[f],n);else if("string"===typeof q[f]){j=0;for(i=l.length;j<i;j++)("_all"==q[f]||h(l[j].nTh).hasClass(q[f]))&&
d(j,n)}}if(c){e=0;for(a=c.length;e<a;e++)d(e,c[e])}}function N(a,b,c,d){var e=a.aoData.length,f=h.extend(!0,{},m.models.oRow,{src:c?"dom":"data",idx:e});f._aData=b;a.aoData.push(f);for(var g=a.aoColumns,j=0,i=g.length;j<i;j++)g[j].sType=null;a.aiDisplayMaster.push(e);b=a.rowIdFn(b);b!==k&&(a.aIds[b]=f);(c||!a.oFeatures.bDeferRender)&&Ha(a,e,c,d);return e}function ma(a,b){var c;b instanceof h||(b=h(b));return b.map(function(b,e){c=Ia(a,e);return N(a,c.data,e,c.cells)})}function B(a,b,c,d){var e=a.iDraw,
f=a.aoColumns[c],g=a.aoData[b]._aData,j=f.sDefaultContent,i=f.fnGetData(g,d,{settings:a,row:b,col:c});if(i===k)return a.iDrawError!=e&&null===j&&(L(a,0,"Requested unknown parameter "+("function"==typeof f.mData?"{function}":"'"+f.mData+"'")+" for row "+b+", column "+c,4),a.iDrawError=e),j;if((i===g||null===i)&&null!==j&&d!==k)i=j;else if("function"===typeof i)return i.call(g);return null===i&&"display"==d?"":i}function jb(a,b,c,d){a.aoColumns[c].fnSetData(a.aoData[b]._aData,d,{settings:a,row:b,col:c})}
function Ja(a){return h.map(a.match(/(\\.|[^\.])+/g)||[""],function(a){return a.replace(/\\./g,".")})}function Q(a){if(h.isPlainObject(a)){var b={};h.each(a,function(a,c){c&&(b[a]=Q(c))});return function(a,c,f,g){var j=b[c]||b._;return j!==k?j(a,c,f,g):a}}if(null===a)return function(a){return a};if("function"===typeof a)return function(b,c,f,g){return a(b,c,f,g)};if("string"===typeof a&&(-1!==a.indexOf(".")||-1!==a.indexOf("[")||-1!==a.indexOf("("))){var c=function(a,b,f){var g,j;if(""!==f){j=Ja(f);
for(var i=0,n=j.length;i<n;i++){f=j[i].match(ba);g=j[i].match(U);if(f){j[i]=j[i].replace(ba,"");""!==j[i]&&(a=a[j[i]]);g=[];j.splice(0,i+1);j=j.join(".");if(h.isArray(a)){i=0;for(n=a.length;i<n;i++)g.push(c(a[i],b,j))}a=f[0].substring(1,f[0].length-1);a=""===a?g:g.join(a);break}else if(g){j[i]=j[i].replace(U,"");a=a[j[i]]();continue}if(null===a||a[j[i]]===k)return k;a=a[j[i]]}}return a};return function(b,e){return c(b,e,a)}}return function(b){return b[a]}}function R(a){if(h.isPlainObject(a))return R(a._);
if(null===a)return function(){};if("function"===typeof a)return function(b,d,e){a(b,"set",d,e)};if("string"===typeof a&&(-1!==a.indexOf(".")||-1!==a.indexOf("[")||-1!==a.indexOf("("))){var b=function(a,d,e){var e=Ja(e),f;f=e[e.length-1];for(var g,j,i=0,n=e.length-1;i<n;i++){g=e[i].match(ba);j=e[i].match(U);if(g){e[i]=e[i].replace(ba,"");a[e[i]]=[];f=e.slice();f.splice(0,i+1);g=f.join(".");if(h.isArray(d)){j=0;for(n=d.length;j<n;j++)f={},b(f,d[j],g),a[e[i]].push(f)}else a[e[i]]=d;return}j&&(e[i]=e[i].replace(U,
""),a=a[e[i]](d));if(null===a[e[i]]||a[e[i]]===k)a[e[i]]={};a=a[e[i]]}if(f.match(U))a[f.replace(U,"")](d);else a[f.replace(ba,"")]=d};return function(c,d){return b(c,d,a)}}return function(b,d){b[a]=d}}function Ka(a){return G(a.aoData,"_aData")}function na(a){a.aoData.length=0;a.aiDisplayMaster.length=0;a.aiDisplay.length=0;a.aIds={}}function oa(a,b,c){for(var d=-1,e=0,f=a.length;e<f;e++)a[e]==b?d=e:a[e]>b&&a[e]--; -1!=d&&c===k&&a.splice(d,1)}function ca(a,b,c,d){var e=a.aoData[b],f,g=function(c,d){for(;c.childNodes.length;)c.removeChild(c.firstChild);
c.innerHTML=B(a,b,d,"display")};if("dom"===c||(!c||"auto"===c)&&"dom"===e.src)e._aData=Ia(a,e,d,d===k?k:e._aData).data;else{var j=e.anCells;if(j)if(d!==k)g(j[d],d);else{c=0;for(f=j.length;c<f;c++)g(j[c],c)}}e._aSortData=null;e._aFilterData=null;g=a.aoColumns;if(d!==k)g[d].sType=null;else{c=0;for(f=g.length;c<f;c++)g[c].sType=null;La(a,e)}}function Ia(a,b,c,d){var e=[],f=b.firstChild,g,j,i=0,n,l=a.aoColumns,q=a._rowReadObject,d=d!==k?d:q?{}:[],t=function(a,b){if("string"===typeof a){var c=a.indexOf("@");
-1!==c&&(c=a.substring(c+1),R(a)(d,b.getAttribute(c)))}},S=function(a){if(c===k||c===i)j=l[i],n=h.trim(a.innerHTML),j&&j._bAttrSrc?(R(j.mData._)(d,n),t(j.mData.sort,a),t(j.mData.type,a),t(j.mData.filter,a)):q?(j._setter||(j._setter=R(j.mData)),j._setter(d,n)):d[i]=n;i++};if(f)for(;f;){g=f.nodeName.toUpperCase();if("TD"==g||"TH"==g)S(f),e.push(f);f=f.nextSibling}else{e=b.anCells;f=0;for(g=e.length;f<g;f++)S(e[f])}if(b=b.firstChild?b:b.nTr)(b=b.getAttribute("id"))&&R(a.rowId)(d,b);return{data:d,cells:e}}
function Ha(a,b,c,d){var e=a.aoData[b],f=e._aData,g=[],j,i,n,l,q;if(null===e.nTr){j=c||I.createElement("tr");e.nTr=j;e.anCells=g;j._DT_RowIndex=b;La(a,e);l=0;for(q=a.aoColumns.length;l<q;l++){n=a.aoColumns[l];i=c?d[l]:I.createElement(n.sCellType);i._DT_CellIndex={row:b,column:l};g.push(i);if((!c||n.mRender||n.mData!==l)&&(!h.isPlainObject(n.mData)||n.mData._!==l+".display"))i.innerHTML=B(a,b,l,"display");n.sClass&&(i.className+=" "+n.sClass);n.bVisible&&!c?j.appendChild(i):!n.bVisible&&c&&i.parentNode.removeChild(i);
n.fnCreatedCell&&n.fnCreatedCell.call(a.oInstance,i,B(a,b,l),f,b,l)}u(a,"aoRowCreatedCallback",null,[j,f,b])}e.nTr.setAttribute("role","row")}function La(a,b){var c=b.nTr,d=b._aData;if(c){var e=a.rowIdFn(d);e&&(c.id=e);d.DT_RowClass&&(e=d.DT_RowClass.split(" "),b.__rowc=b.__rowc?pa(b.__rowc.concat(e)):e,h(c).removeClass(b.__rowc.join(" ")).addClass(d.DT_RowClass));d.DT_RowAttr&&h(c).attr(d.DT_RowAttr);d.DT_RowData&&h(c).data(d.DT_RowData)}}function kb(a){var b,c,d,e,f,g=a.nTHead,j=a.nTFoot,i=0===
h("th, td",g).length,n=a.oClasses,l=a.aoColumns;i&&(e=h("<tr/>").appendTo(g));b=0;for(c=l.length;b<c;b++)f=l[b],d=h(f.nTh).addClass(f.sClass),i&&d.appendTo(e),a.oFeatures.bSort&&(d.addClass(f.sSortingClass),!1!==f.bSortable&&(d.attr("tabindex",a.iTabIndex).attr("aria-controls",a.sTableId),Ma(a,f.nTh,b))),f.sTitle!=d[0].innerHTML&&d.html(f.sTitle),Na(a,"header")(a,d,f,n);i&&da(a.aoHeader,g);h(g).find(">tr").attr("role","row");h(g).find(">tr>th, >tr>td").addClass(n.sHeaderTH);h(j).find(">tr>th, >tr>td").addClass(n.sFooterTH);
if(null!==j){a=a.aoFooter[0];b=0;for(c=a.length;b<c;b++)f=l[b],f.nTf=a[b].cell,f.sClass&&h(f.nTf).addClass(f.sClass)}}function ea(a,b,c){var d,e,f,g=[],j=[],i=a.aoColumns.length,n;if(b){c===k&&(c=!1);d=0;for(e=b.length;d<e;d++){g[d]=b[d].slice();g[d].nTr=b[d].nTr;for(f=i-1;0<=f;f--)!a.aoColumns[f].bVisible&&!c&&g[d].splice(f,1);j.push([])}d=0;for(e=g.length;d<e;d++){if(a=g[d].nTr)for(;f=a.firstChild;)a.removeChild(f);f=0;for(b=g[d].length;f<b;f++)if(n=i=1,j[d][f]===k){a.appendChild(g[d][f].cell);
for(j[d][f]=1;g[d+i]!==k&&g[d][f].cell==g[d+i][f].cell;)j[d+i][f]=1,i++;for(;g[d][f+n]!==k&&g[d][f].cell==g[d][f+n].cell;){for(c=0;c<i;c++)j[d+c][f+n]=1;n++}h(g[d][f].cell).attr("rowspan",i).attr("colspan",n)}}}}function O(a){var b=u(a,"aoPreDrawCallback","preDraw",[a]);if(-1!==h.inArray(!1,b))C(a,!1);else{var b=[],c=0,d=a.asStripeClasses,e=d.length,f=a.oLanguage,g=a.iInitDisplayStart,j="ssp"==y(a),i=a.aiDisplay;a.bDrawing=!0;g!==k&&-1!==g&&(a._iDisplayStart=j?g:g>=a.fnRecordsDisplay()?0:g,a.iInitDisplayStart=
-1);var g=a._iDisplayStart,n=a.fnDisplayEnd();if(a.bDeferLoading)a.bDeferLoading=!1,a.iDraw++,C(a,!1);else if(j){if(!a.bDestroying&&!lb(a))return}else a.iDraw++;if(0!==i.length){f=j?a.aoData.length:n;for(j=j?0:g;j<f;j++){var l=i[j],q=a.aoData[l];null===q.nTr&&Ha(a,l);l=q.nTr;if(0!==e){var t=d[c%e];q._sRowStripe!=t&&(h(l).removeClass(q._sRowStripe).addClass(t),q._sRowStripe=t)}u(a,"aoRowCallback",null,[l,q._aData,c,j]);b.push(l);c++}}else c=f.sZeroRecords,1==a.iDraw&&"ajax"==y(a)?c=f.sLoadingRecords:
f.sEmptyTable&&0===a.fnRecordsTotal()&&(c=f.sEmptyTable),b[0]=h("<tr/>",{"class":e?d[0]:""}).append(h("<td />",{valign:"top",colSpan:aa(a),"class":a.oClasses.sRowEmpty}).html(c))[0];u(a,"aoHeaderCallback","header",[h(a.nTHead).children("tr")[0],Ka(a),g,n,i]);u(a,"aoFooterCallback","footer",[h(a.nTFoot).children("tr")[0],Ka(a),g,n,i]);d=h(a.nTBody);d.children().detach();d.append(h(b));u(a,"aoDrawCallback","draw",[a]);a.bSorted=!1;a.bFiltered=!1;a.bDrawing=!1}}function T(a,b){var c=a.oFeatures,d=c.bFilter;
c.bSort&&mb(a);d?fa(a,a.oPreviousSearch):a.aiDisplay=a.aiDisplayMaster.slice();!0!==b&&(a._iDisplayStart=0);a._drawHold=b;O(a);a._drawHold=!1}function nb(a){var b=a.oClasses,c=h(a.nTable),c=h("<div/>").insertBefore(c),d=a.oFeatures,e=h("<div/>",{id:a.sTableId+"_wrapper","class":b.sWrapper+(a.nTFoot?"":" "+b.sNoFooter)});a.nHolding=c[0];a.nTableWrapper=e[0];a.nTableReinsertBefore=a.nTable.nextSibling;for(var f=a.sDom.split(""),g,j,i,n,l,q,t=0;t<f.length;t++){g=null;j=f[t];if("<"==j){i=h("<div/>")[0];
n=f[t+1];if("'"==n||'"'==n){l="";for(q=2;f[t+q]!=n;)l+=f[t+q],q++;"H"==l?l=b.sJUIHeader:"F"==l&&(l=b.sJUIFooter);-1!=l.indexOf(".")?(n=l.split("."),i.id=n[0].substr(1,n[0].length-1),i.className=n[1]):"#"==l.charAt(0)?i.id=l.substr(1,l.length-1):i.className=l;t+=q}e.append(i);e=h(i)}else if(">"==j)e=e.parent();else if("l"==j&&d.bPaginate&&d.bLengthChange)g=ob(a);else if("f"==j&&d.bFilter)g=pb(a);else if("r"==j&&d.bProcessing)g=qb(a);else if("t"==j)g=rb(a);else if("i"==j&&d.bInfo)g=sb(a);else if("p"==
j&&d.bPaginate)g=tb(a);else if(0!==m.ext.feature.length){i=m.ext.feature;q=0;for(n=i.length;q<n;q++)if(j==i[q].cFeature){g=i[q].fnInit(a);break}}g&&(i=a.aanFeatures,i[j]||(i[j]=[]),i[j].push(g),e.append(g))}c.replaceWith(e);a.nHolding=null}function da(a,b){var c=h(b).children("tr"),d,e,f,g,j,i,n,l,q,t;a.splice(0,a.length);f=0;for(i=c.length;f<i;f++)a.push([]);f=0;for(i=c.length;f<i;f++){d=c[f];for(e=d.firstChild;e;){if("TD"==e.nodeName.toUpperCase()||"TH"==e.nodeName.toUpperCase()){l=1*e.getAttribute("colspan");
q=1*e.getAttribute("rowspan");l=!l||0===l||1===l?1:l;q=!q||0===q||1===q?1:q;g=0;for(j=a[f];j[g];)g++;n=g;t=1===l?!0:!1;for(j=0;j<l;j++)for(g=0;g<q;g++)a[f+g][n+j]={cell:e,unique:t},a[f+g].nTr=d}e=e.nextSibling}}}function qa(a,b,c){var d=[];c||(c=a.aoHeader,b&&(c=[],da(c,b)));for(var b=0,e=c.length;b<e;b++)for(var f=0,g=c[b].length;f<g;f++)if(c[b][f].unique&&(!d[f]||!a.bSortCellsTop))d[f]=c[b][f].cell;return d}function ra(a,b,c){u(a,"aoServerParams","serverParams",[b]);if(b&&h.isArray(b)){var d={},
e=/(.*?)\[\]$/;h.each(b,function(a,b){var c=b.name.match(e);c?(c=c[0],d[c]||(d[c]=[]),d[c].push(b.value)):d[b.name]=b.value});b=d}var f,g=a.ajax,j=a.oInstance,i=function(b){u(a,null,"xhr",[a,b,a.jqXHR]);c(b)};if(h.isPlainObject(g)&&g.data){f=g.data;var n=h.isFunction(f)?f(b,a):f,b=h.isFunction(f)&&n?n:h.extend(!0,b,n);delete g.data}n={data:b,success:function(b){var c=b.error||b.sError;c&&L(a,0,c);a.json=b;i(b)},dataType:"json",cache:!1,type:a.sServerMethod,error:function(b,c){var d=u(a,null,"xhr",
[a,null,a.jqXHR]);-1===h.inArray(!0,d)&&("parsererror"==c?L(a,0,"Invalid JSON response",1):4===b.readyState&&L(a,0,"Ajax error",7));C(a,!1)}};a.oAjaxData=b;u(a,null,"preXhr",[a,b]);a.fnServerData?a.fnServerData.call(j,a.sAjaxSource,h.map(b,function(a,b){return{name:b,value:a}}),i,a):a.sAjaxSource||"string"===typeof g?a.jqXHR=h.ajax(h.extend(n,{url:g||a.sAjaxSource})):h.isFunction(g)?a.jqXHR=g.call(j,b,i,a):(a.jqXHR=h.ajax(h.extend(n,g)),g.data=f)}function lb(a){return a.bAjaxDataGet?(a.iDraw++,C(a,
!0),ra(a,ub(a),function(b){vb(a,b)}),!1):!0}function ub(a){var b=a.aoColumns,c=b.length,d=a.oFeatures,e=a.oPreviousSearch,f=a.aoPreSearchCols,g,j=[],i,n,l,q=V(a);g=a._iDisplayStart;i=!1!==d.bPaginate?a._iDisplayLength:-1;var k=function(a,b){j.push({name:a,value:b})};k("sEcho",a.iDraw);k("iColumns",c);k("sColumns",G(b,"sName").join(","));k("iDisplayStart",g);k("iDisplayLength",i);var S={draw:a.iDraw,columns:[],order:[],start:g,length:i,search:{value:e.sSearch,regex:e.bRegex}};for(g=0;g<c;g++)n=b[g],
l=f[g],i="function"==typeof n.mData?"function":n.mData,S.columns.push({data:i,name:n.sName,searchable:n.bSearchable,orderable:n.bSortable,search:{value:l.sSearch,regex:l.bRegex}}),k("mDataProp_"+g,i),d.bFilter&&(k("sSearch_"+g,l.sSearch),k("bRegex_"+g,l.bRegex),k("bSearchable_"+g,n.bSearchable)),d.bSort&&k("bSortable_"+g,n.bSortable);d.bFilter&&(k("sSearch",e.sSearch),k("bRegex",e.bRegex));d.bSort&&(h.each(q,function(a,b){S.order.push({column:b.col,dir:b.dir});k("iSortCol_"+a,b.col);k("sSortDir_"+
a,b.dir)}),k("iSortingCols",q.length));b=m.ext.legacy.ajax;return null===b?a.sAjaxSource?j:S:b?j:S}function vb(a,b){var c=sa(a,b),d=b.sEcho!==k?b.sEcho:b.draw,e=b.iTotalRecords!==k?b.iTotalRecords:b.recordsTotal,f=b.iTotalDisplayRecords!==k?b.iTotalDisplayRecords:b.recordsFiltered;if(d){if(1*d<a.iDraw)return;a.iDraw=1*d}na(a);a._iRecordsTotal=parseInt(e,10);a._iRecordsDisplay=parseInt(f,10);d=0;for(e=c.length;d<e;d++)N(a,c[d]);a.aiDisplay=a.aiDisplayMaster.slice();a.bAjaxDataGet=!1;O(a);a._bInitComplete||
ta(a,b);a.bAjaxDataGet=!0;C(a,!1)}function sa(a,b){var c=h.isPlainObject(a.ajax)&&a.ajax.dataSrc!==k?a.ajax.dataSrc:a.sAjaxDataProp;return"data"===c?b.aaData||b[c]:""!==c?Q(c)(b):b}function pb(a){var b=a.oClasses,c=a.sTableId,d=a.oLanguage,e=a.oPreviousSearch,f=a.aanFeatures,g='<input type="search" class="'+b.sFilterInput+'"/>',j=d.sSearch,j=j.match(/_INPUT_/)?j.replace("_INPUT_",g):j+g,b=h("<div/>",{id:!f.f?c+"_filter":null,"class":b.sFilter}).append(h("<label/>").append(j)),f=function(){var b=!this.value?
"":this.value;b!=e.sSearch&&(fa(a,{sSearch:b,bRegex:e.bRegex,bSmart:e.bSmart,bCaseInsensitive:e.bCaseInsensitive}),a._iDisplayStart=0,O(a))},g=null!==a.searchDelay?a.searchDelay:"ssp"===y(a)?400:0,i=h("input",b).val(e.sSearch).attr("placeholder",d.sSearchPlaceholder).bind("keyup.DT search.DT input.DT paste.DT cut.DT",g?Oa(f,g):f).bind("keypress.DT",function(a){if(13==a.keyCode)return!1}).attr("aria-controls",c);h(a.nTable).on("search.dt.DT",function(b,c){if(a===c)try{i[0]!==I.activeElement&&i.val(e.sSearch)}catch(d){}});
return b[0]}function fa(a,b,c){var d=a.oPreviousSearch,e=a.aoPreSearchCols,f=function(a){d.sSearch=a.sSearch;d.bRegex=a.bRegex;d.bSmart=a.bSmart;d.bCaseInsensitive=a.bCaseInsensitive};Ga(a);if("ssp"!=y(a)){wb(a,b.sSearch,c,b.bEscapeRegex!==k?!b.bEscapeRegex:b.bRegex,b.bSmart,b.bCaseInsensitive);f(b);for(b=0;b<e.length;b++)xb(a,e[b].sSearch,b,e[b].bEscapeRegex!==k?!e[b].bEscapeRegex:e[b].bRegex,e[b].bSmart,e[b].bCaseInsensitive);yb(a)}else f(b);a.bFiltered=!0;u(a,null,"search",[a])}function yb(a){for(var b=
m.ext.search,c=a.aiDisplay,d,e,f=0,g=b.length;f<g;f++){for(var j=[],i=0,n=c.length;i<n;i++)e=c[i],d=a.aoData[e],b[f](a,d._aFilterData,e,d._aData,i)&&j.push(e);c.length=0;h.merge(c,j)}}function xb(a,b,c,d,e,f){if(""!==b)for(var g=a.aiDisplay,d=Pa(b,d,e,f),e=g.length-1;0<=e;e--)b=a.aoData[g[e]]._aFilterData[c],d.test(b)||g.splice(e,1)}function wb(a,b,c,d,e,f){var d=Pa(b,d,e,f),e=a.oPreviousSearch.sSearch,f=a.aiDisplayMaster,g;0!==m.ext.search.length&&(c=!0);g=zb(a);if(0>=b.length)a.aiDisplay=f.slice();
else{if(g||c||e.length>b.length||0!==b.indexOf(e)||a.bSorted)a.aiDisplay=f.slice();b=a.aiDisplay;for(c=b.length-1;0<=c;c--)d.test(a.aoData[b[c]]._sFilterRow)||b.splice(c,1)}}function Pa(a,b,c,d){a=b?a:Qa(a);c&&(a="^(?=.*?"+h.map(a.match(/"[^"]+"|[^ ]+/g)||[""],function(a){if('"'===a.charAt(0))var b=a.match(/^"(.*)"$/),a=b?b[1]:a;return a.replace('"',"")}).join(")(?=.*?")+").*$");return RegExp(a,d?"i":"")}function zb(a){var b=a.aoColumns,c,d,e,f,g,j,i,h,l=m.ext.type.search;c=!1;d=0;for(f=a.aoData.length;d<
f;d++)if(h=a.aoData[d],!h._aFilterData){j=[];e=0;for(g=b.length;e<g;e++)c=b[e],c.bSearchable?(i=B(a,d,e,"filter"),l[c.sType]&&(i=l[c.sType](i)),null===i&&(i=""),"string"!==typeof i&&i.toString&&(i=i.toString())):i="",i.indexOf&&-1!==i.indexOf("&")&&(ua.innerHTML=i,i=Zb?ua.textContent:ua.innerText),i.replace&&(i=i.replace(/[\r\n]/g,"")),j.push(i);h._aFilterData=j;h._sFilterRow=j.join(" ");c=!0}return c}function Ab(a){return{search:a.sSearch,smart:a.bSmart,regex:a.bRegex,caseInsensitive:a.bCaseInsensitive}}
function Bb(a){return{sSearch:a.search,bSmart:a.smart,bRegex:a.regex,bCaseInsensitive:a.caseInsensitive}}function sb(a){var b=a.sTableId,c=a.aanFeatures.i,d=h("<div/>",{"class":a.oClasses.sInfo,id:!c?b+"_info":null});c||(a.aoDrawCallback.push({fn:Cb,sName:"information"}),d.attr("role","status").attr("aria-live","polite"),h(a.nTable).attr("aria-describedby",b+"_info"));return d[0]}function Cb(a){var b=a.aanFeatures.i;if(0!==b.length){var c=a.oLanguage,d=a._iDisplayStart+1,e=a.fnDisplayEnd(),f=a.fnRecordsTotal(),
g=a.fnRecordsDisplay(),j=g?c.sInfo:c.sInfoEmpty;g!==f&&(j+=" "+c.sInfoFiltered);j+=c.sInfoPostFix;j=Db(a,j);c=c.fnInfoCallback;null!==c&&(j=c.call(a.oInstance,a,d,e,f,g,j));h(b).html(j)}}function Db(a,b){var c=a.fnFormatNumber,d=a._iDisplayStart+1,e=a._iDisplayLength,f=a.fnRecordsDisplay(),g=-1===e;return b.replace(/_START_/g,c.call(a,d)).replace(/_END_/g,c.call(a,a.fnDisplayEnd())).replace(/_MAX_/g,c.call(a,a.fnRecordsTotal())).replace(/_TOTAL_/g,c.call(a,f)).replace(/_PAGE_/g,c.call(a,g?1:Math.ceil(d/
e))).replace(/_PAGES_/g,c.call(a,g?1:Math.ceil(f/e)))}function ga(a){var b,c,d=a.iInitDisplayStart,e=a.aoColumns,f;c=a.oFeatures;var g=a.bDeferLoading;if(a.bInitialised){nb(a);kb(a);ea(a,a.aoHeader);ea(a,a.aoFooter);C(a,!0);c.bAutoWidth&&Fa(a);b=0;for(c=e.length;b<c;b++)f=e[b],f.sWidth&&(f.nTh.style.width=x(f.sWidth));u(a,null,"preInit",[a]);T(a);e=y(a);if("ssp"!=e||g)"ajax"==e?ra(a,[],function(c){var f=sa(a,c);for(b=0;b<f.length;b++)N(a,f[b]);a.iInitDisplayStart=d;T(a);C(a,!1);ta(a,c)},a):(C(a,!1),
ta(a))}else setTimeout(function(){ga(a)},200)}function ta(a,b){a._bInitComplete=!0;(b||a.oInit.aaData)&&Y(a);u(a,null,"plugin-init",[a,b]);u(a,"aoInitComplete","init",[a,b])}function Ra(a,b){var c=parseInt(b,10);a._iDisplayLength=c;Sa(a);u(a,null,"length",[a,c])}function ob(a){for(var b=a.oClasses,c=a.sTableId,d=a.aLengthMenu,e=h.isArray(d[0]),f=e?d[0]:d,d=e?d[1]:d,e=h("<select/>",{name:c+"_length","aria-controls":c,"class":b.sLengthSelect}),g=0,j=f.length;g<j;g++)e[0][g]=new Option(d[g],f[g]);var i=
h("<div><label/></div>").addClass(b.sLength);a.aanFeatures.l||(i[0].id=c+"_length");i.children().append(a.oLanguage.sLengthMenu.replace("_MENU_",e[0].outerHTML));h("select",i).val(a._iDisplayLength).bind("change.DT",function(){Ra(a,h(this).val());O(a)});h(a.nTable).bind("length.dt.DT",function(b,c,d){a===c&&h("select",i).val(d)});return i[0]}function tb(a){var b=a.sPaginationType,c=m.ext.pager[b],d="function"===typeof c,e=function(a){O(a)},b=h("<div/>").addClass(a.oClasses.sPaging+b)[0],f=a.aanFeatures;
d||c.fnInit(a,b,e);f.p||(b.id=a.sTableId+"_paginate",a.aoDrawCallback.push({fn:function(a){if(d){var b=a._iDisplayStart,i=a._iDisplayLength,h=a.fnRecordsDisplay(),l=-1===i,b=l?0:Math.ceil(b/i),i=l?1:Math.ceil(h/i),h=c(b,i),k,l=0;for(k=f.p.length;l<k;l++)Na(a,"pageButton")(a,f.p[l],l,h,b,i)}else c.fnUpdate(a,e)},sName:"pagination"}));return b}function Ta(a,b,c){var d=a._iDisplayStart,e=a._iDisplayLength,f=a.fnRecordsDisplay();0===f||-1===e?d=0:"number"===typeof b?(d=b*e,d>f&&(d=0)):"first"==b?d=0:
"previous"==b?(d=0<=e?d-e:0,0>d&&(d=0)):"next"==b?d+e<f&&(d+=e):"last"==b?d=Math.floor((f-1)/e)*e:L(a,0,"Unknown paging action: "+b,5);b=a._iDisplayStart!==d;a._iDisplayStart=d;b&&(u(a,null,"page",[a]),c&&O(a));return b}function qb(a){return h("<div/>",{id:!a.aanFeatures.r?a.sTableId+"_processing":null,"class":a.oClasses.sProcessing}).html(a.oLanguage.sProcessing).insertBefore(a.nTable)[0]}function C(a,b){a.oFeatures.bProcessing&&h(a.aanFeatures.r).css("display",b?"block":"none");u(a,null,"processing",
[a,b])}function rb(a){var b=h(a.nTable);b.attr("role","grid");var c=a.oScroll;if(""===c.sX&&""===c.sY)return a.nTable;var d=c.sX,e=c.sY,f=a.oClasses,g=b.children("caption"),j=g.length?g[0]._captionSide:null,i=h(b[0].cloneNode(!1)),n=h(b[0].cloneNode(!1)),l=b.children("tfoot");l.length||(l=null);i=h("<div/>",{"class":f.sScrollWrapper}).append(h("<div/>",{"class":f.sScrollHead}).css({overflow:"hidden",position:"relative",border:0,width:d?!d?null:x(d):"100%"}).append(h("<div/>",{"class":f.sScrollHeadInner}).css({"box-sizing":"content-box",
width:c.sXInner||"100%"}).append(i.removeAttr("id").css("margin-left",0).append("top"===j?g:null).append(b.children("thead"))))).append(h("<div/>",{"class":f.sScrollBody}).css({position:"relative",overflow:"auto",width:!d?null:x(d)}).append(b));l&&i.append(h("<div/>",{"class":f.sScrollFoot}).css({overflow:"hidden",border:0,width:d?!d?null:x(d):"100%"}).append(h("<div/>",{"class":f.sScrollFootInner}).append(n.removeAttr("id").css("margin-left",0).append("bottom"===j?g:null).append(b.children("tfoot")))));
var b=i.children(),k=b[0],f=b[1],t=l?b[2]:null;if(d)h(f).on("scroll.DT",function(){var a=this.scrollLeft;k.scrollLeft=a;l&&(t.scrollLeft=a)});h(f).css(e&&c.bCollapse?"max-height":"height",e);a.nScrollHead=k;a.nScrollBody=f;a.nScrollFoot=t;a.aoDrawCallback.push({fn:ka,sName:"scrolling"});return i[0]}function ka(a){var b=a.oScroll,c=b.sX,d=b.sXInner,e=b.sY,b=b.iBarWidth,f=h(a.nScrollHead),g=f[0].style,j=f.children("div"),i=j[0].style,n=j.children("table"),j=a.nScrollBody,l=h(j),q=j.style,t=h(a.nScrollFoot).children("div"),
m=t.children("table"),o=h(a.nTHead),F=h(a.nTable),p=F[0],r=p.style,u=a.nTFoot?h(a.nTFoot):null,Eb=a.oBrowser,Ua=Eb.bScrollOversize,s=G(a.aoColumns,"nTh"),P,v,w,y,z=[],A=[],B=[],C=[],D,E=function(a){a=a.style;a.paddingTop="0";a.paddingBottom="0";a.borderTopWidth="0";a.borderBottomWidth="0";a.height=0};v=j.scrollHeight>j.clientHeight;if(a.scrollBarVis!==v&&a.scrollBarVis!==k)a.scrollBarVis=v,Y(a);else{a.scrollBarVis=v;F.children("thead, tfoot").remove();u&&(w=u.clone().prependTo(F),P=u.find("tr"),w=
w.find("tr"));y=o.clone().prependTo(F);o=o.find("tr");v=y.find("tr");y.find("th, td").removeAttr("tabindex");c||(q.width="100%",f[0].style.width="100%");h.each(qa(a,y),function(b,c){D=Z(a,b);c.style.width=a.aoColumns[D].sWidth});u&&J(function(a){a.style.width=""},w);f=F.outerWidth();if(""===c){r.width="100%";if(Ua&&(F.find("tbody").height()>j.offsetHeight||"scroll"==l.css("overflow-y")))r.width=x(F.outerWidth()-b);f=F.outerWidth()}else""!==d&&(r.width=x(d),f=F.outerWidth());J(E,v);J(function(a){B.push(a.innerHTML);
z.push(x(h(a).css("width")))},v);J(function(a,b){if(h.inArray(a,s)!==-1)a.style.width=z[b]},o);h(v).height(0);u&&(J(E,w),J(function(a){C.push(a.innerHTML);A.push(x(h(a).css("width")))},w),J(function(a,b){a.style.width=A[b]},P),h(w).height(0));J(function(a,b){a.innerHTML='<div class="dataTables_sizing" style="height:0;overflow:hidden;">'+B[b]+"</div>";a.style.width=z[b]},v);u&&J(function(a,b){a.innerHTML='<div class="dataTables_sizing" style="height:0;overflow:hidden;">'+C[b]+"</div>";a.style.width=
A[b]},w);if(F.outerWidth()<f){P=j.scrollHeight>j.offsetHeight||"scroll"==l.css("overflow-y")?f+b:f;if(Ua&&(j.scrollHeight>j.offsetHeight||"scroll"==l.css("overflow-y")))r.width=x(P-b);(""===c||""!==d)&&L(a,1,"Possible column misalignment",6)}else P="100%";q.width=x(P);g.width=x(P);u&&(a.nScrollFoot.style.width=x(P));!e&&Ua&&(q.height=x(p.offsetHeight+b));c=F.outerWidth();n[0].style.width=x(c);i.width=x(c);d=F.height()>j.clientHeight||"scroll"==l.css("overflow-y");e="padding"+(Eb.bScrollbarLeft?"Left":
"Right");i[e]=d?b+"px":"0px";u&&(m[0].style.width=x(c),t[0].style.width=x(c),t[0].style[e]=d?b+"px":"0px");F.children("colgroup").insertBefore(F.children("thead"));l.scroll();if((a.bSorted||a.bFiltered)&&!a._drawHold)j.scrollTop=0}}function J(a,b,c){for(var d=0,e=0,f=b.length,g,j;e<f;){g=b[e].firstChild;for(j=c?c[e].firstChild:null;g;)1===g.nodeType&&(c?a(g,j,d):a(g,d),d++),g=g.nextSibling,j=c?j.nextSibling:null;e++}}function Fa(a){var b=a.nTable,c=a.aoColumns,d=a.oScroll,e=d.sY,f=d.sX,g=d.sXInner,
j=c.length,i=la(a,"bVisible"),n=h("th",a.nTHead),l=b.getAttribute("width"),k=b.parentNode,t=!1,m,o,p=a.oBrowser,d=p.bScrollOversize;(m=b.style.width)&&-1!==m.indexOf("%")&&(l=m);for(m=0;m<i.length;m++)o=c[i[m]],null!==o.sWidth&&(o.sWidth=Fb(o.sWidthOrig,k),t=!0);if(d||!t&&!f&&!e&&j==aa(a)&&j==n.length)for(m=0;m<j;m++)i=Z(a,m),null!==i&&(c[i].sWidth=x(n.eq(m).width()));else{j=h(b).clone().css("visibility","hidden").removeAttr("id");j.find("tbody tr").remove();var r=h("<tr/>").appendTo(j.find("tbody"));
j.find("thead, tfoot").remove();j.append(h(a.nTHead).clone()).append(h(a.nTFoot).clone());j.find("tfoot th, tfoot td").css("width","");n=qa(a,j.find("thead")[0]);for(m=0;m<i.length;m++)o=c[i[m]],n[m].style.width=null!==o.sWidthOrig&&""!==o.sWidthOrig?x(o.sWidthOrig):"",o.sWidthOrig&&f&&h(n[m]).append(h("<div/>").css({width:o.sWidthOrig,margin:0,padding:0,border:0,height:1}));if(a.aoData.length)for(m=0;m<i.length;m++)t=i[m],o=c[t],h(Gb(a,t)).clone(!1).append(o.sContentPadding).appendTo(r);h("[name]",
j).removeAttr("name");o=h("<div/>").css(f||e?{position:"absolute",top:0,left:0,height:1,right:0,overflow:"hidden"}:{}).append(j).appendTo(k);f&&g?j.width(g):f?(j.css("width","auto"),j.removeAttr("width"),j.width()<k.clientWidth&&l&&j.width(k.clientWidth)):e?j.width(k.clientWidth):l&&j.width(l);for(m=e=0;m<i.length;m++)k=h(n[m]),g=k.outerWidth()-k.width(),k=p.bBounding?Math.ceil(n[m].getBoundingClientRect().width):k.outerWidth(),e+=k,c[i[m]].sWidth=x(k-g);b.style.width=x(e);o.remove()}l&&(b.style.width=
x(l));if((l||f)&&!a._reszEvt)b=function(){h(D).bind("resize.DT-"+a.sInstance,Oa(function(){Y(a)}))},d?setTimeout(b,1E3):b(),a._reszEvt=!0}function Fb(a,b){if(!a)return 0;var c=h("<div/>").css("width",x(a)).appendTo(b||I.body),d=c[0].offsetWidth;c.remove();return d}function Gb(a,b){var c=Hb(a,b);if(0>c)return null;var d=a.aoData[c];return!d.nTr?h("<td/>").html(B(a,c,b,"display"))[0]:d.anCells[b]}function Hb(a,b){for(var c,d=-1,e=-1,f=0,g=a.aoData.length;f<g;f++)c=B(a,f,b,"display")+"",c=c.replace($b,
""),c=c.replace(/&nbsp;/g," "),c.length>d&&(d=c.length,e=f);return e}function x(a){return null===a?"0px":"number"==typeof a?0>a?"0px":a+"px":a.match(/\d$/)?a+"px":a}function V(a){var b,c,d=[],e=a.aoColumns,f,g,j,i;b=a.aaSortingFixed;c=h.isPlainObject(b);var n=[];f=function(a){a.length&&!h.isArray(a[0])?n.push(a):h.merge(n,a)};h.isArray(b)&&f(b);c&&b.pre&&f(b.pre);f(a.aaSorting);c&&b.post&&f(b.post);for(a=0;a<n.length;a++){i=n[a][0];f=e[i].aDataSort;b=0;for(c=f.length;b<c;b++)g=f[b],j=e[g].sType||
"string",n[a]._idx===k&&(n[a]._idx=h.inArray(n[a][1],e[g].asSorting)),d.push({src:i,col:g,dir:n[a][1],index:n[a]._idx,type:j,formatter:m.ext.type.order[j+"-pre"]})}return d}function mb(a){var b,c,d=[],e=m.ext.type.order,f=a.aoData,g=0,j,i=a.aiDisplayMaster,h;Ga(a);h=V(a);b=0;for(c=h.length;b<c;b++)j=h[b],j.formatter&&g++,Ib(a,j.col);if("ssp"!=y(a)&&0!==h.length){b=0;for(c=i.length;b<c;b++)d[i[b]]=b;g===h.length?i.sort(function(a,b){var c,e,g,j,i=h.length,k=f[a]._aSortData,m=f[b]._aSortData;for(g=
0;g<i;g++)if(j=h[g],c=k[j.col],e=m[j.col],c=c<e?-1:c>e?1:0,0!==c)return"asc"===j.dir?c:-c;c=d[a];e=d[b];return c<e?-1:c>e?1:0}):i.sort(function(a,b){var c,g,j,i,k=h.length,m=f[a]._aSortData,p=f[b]._aSortData;for(j=0;j<k;j++)if(i=h[j],c=m[i.col],g=p[i.col],i=e[i.type+"-"+i.dir]||e["string-"+i.dir],c=i(c,g),0!==c)return c;c=d[a];g=d[b];return c<g?-1:c>g?1:0})}a.bSorted=!0}function Jb(a){for(var b,c,d=a.aoColumns,e=V(a),a=a.oLanguage.oAria,f=0,g=d.length;f<g;f++){c=d[f];var j=c.asSorting;b=c.sTitle.replace(/<.*?>/g,
"");var i=c.nTh;i.removeAttribute("aria-sort");c.bSortable&&(0<e.length&&e[0].col==f?(i.setAttribute("aria-sort","asc"==e[0].dir?"ascending":"descending"),c=j[e[0].index+1]||j[0]):c=j[0],b+="asc"===c?a.sSortAscending:a.sSortDescending);i.setAttribute("aria-label",b)}}function Va(a,b,c,d){var e=a.aaSorting,f=a.aoColumns[b].asSorting,g=function(a,b){var c=a._idx;c===k&&(c=h.inArray(a[1],f));return c+1<f.length?c+1:b?null:0};"number"===typeof e[0]&&(e=a.aaSorting=[e]);c&&a.oFeatures.bSortMulti?(c=h.inArray(b,
G(e,"0")),-1!==c?(b=g(e[c],!0),null===b&&1===e.length&&(b=0),null===b?e.splice(c,1):(e[c][1]=f[b],e[c]._idx=b)):(e.push([b,f[0],0]),e[e.length-1]._idx=0)):e.length&&e[0][0]==b?(b=g(e[0]),e.length=1,e[0][1]=f[b],e[0]._idx=b):(e.length=0,e.push([b,f[0]]),e[0]._idx=0);T(a);"function"==typeof d&&d(a)}function Ma(a,b,c,d){var e=a.aoColumns[c];Wa(b,{},function(b){!1!==e.bSortable&&(a.oFeatures.bProcessing?(C(a,!0),setTimeout(function(){Va(a,c,b.shiftKey,d);"ssp"!==y(a)&&C(a,!1)},0)):Va(a,c,b.shiftKey,d))})}
function va(a){var b=a.aLastSort,c=a.oClasses.sSortColumn,d=V(a),e=a.oFeatures,f,g;if(e.bSort&&e.bSortClasses){e=0;for(f=b.length;e<f;e++)g=b[e].src,h(G(a.aoData,"anCells",g)).removeClass(c+(2>e?e+1:3));e=0;for(f=d.length;e<f;e++)g=d[e].src,h(G(a.aoData,"anCells",g)).addClass(c+(2>e?e+1:3))}a.aLastSort=d}function Ib(a,b){var c=a.aoColumns[b],d=m.ext.order[c.sSortDataType],e;d&&(e=d.call(a.oInstance,a,b,$(a,b)));for(var f,g=m.ext.type.order[c.sType+"-pre"],j=0,i=a.aoData.length;j<i;j++)if(c=a.aoData[j],
c._aSortData||(c._aSortData=[]),!c._aSortData[b]||d)f=d?e[j]:B(a,j,b,"sort"),c._aSortData[b]=g?g(f):f}function wa(a){if(a.oFeatures.bStateSave&&!a.bDestroying){var b={time:+new Date,start:a._iDisplayStart,length:a._iDisplayLength,order:h.extend(!0,[],a.aaSorting),search:Ab(a.oPreviousSearch),columns:h.map(a.aoColumns,function(b,d){return{visible:b.bVisible,search:Ab(a.aoPreSearchCols[d])}})};u(a,"aoStateSaveParams","stateSaveParams",[a,b]);a.oSavedState=b;a.fnStateSaveCallback.call(a.oInstance,a,
b)}}function Kb(a){var b,c,d=a.aoColumns;if(a.oFeatures.bStateSave){var e=a.fnStateLoadCallback.call(a.oInstance,a);if(e&&e.time&&(b=u(a,"aoStateLoadParams","stateLoadParams",[a,e]),-1===h.inArray(!1,b)&&(b=a.iStateDuration,!(0<b&&e.time<+new Date-1E3*b)&&d.length===e.columns.length))){a.oLoadedState=h.extend(!0,{},e);e.start!==k&&(a._iDisplayStart=e.start,a.iInitDisplayStart=e.start);e.length!==k&&(a._iDisplayLength=e.length);e.order!==k&&(a.aaSorting=[],h.each(e.order,function(b,c){a.aaSorting.push(c[0]>=
d.length?[0,c[1]]:c)}));e.search!==k&&h.extend(a.oPreviousSearch,Bb(e.search));b=0;for(c=e.columns.length;b<c;b++){var f=e.columns[b];f.visible!==k&&(d[b].bVisible=f.visible);f.search!==k&&h.extend(a.aoPreSearchCols[b],Bb(f.search))}u(a,"aoStateLoaded","stateLoaded",[a,e])}}}function xa(a){var b=m.settings,a=h.inArray(a,G(b,"nTable"));return-1!==a?b[a]:null}function L(a,b,c,d){c="DataTables warning: "+(a?"table id="+a.sTableId+" - ":"")+c;d&&(c+=". For more information about this error, please see http://datatables.net/tn/"+
d);if(b)D.console&&console.log&&console.log(c);else if(b=m.ext,b=b.sErrMode||b.errMode,a&&u(a,null,"error",[a,d,c]),"alert"==b)alert(c);else{if("throw"==b)throw Error(c);"function"==typeof b&&b(a,d,c)}}function E(a,b,c,d){h.isArray(c)?h.each(c,function(c,d){h.isArray(d)?E(a,b,d[0],d[1]):E(a,b,d)}):(d===k&&(d=c),b[c]!==k&&(a[d]=b[c]))}function Lb(a,b,c){var d,e;for(e in b)b.hasOwnProperty(e)&&(d=b[e],h.isPlainObject(d)?(h.isPlainObject(a[e])||(a[e]={}),h.extend(!0,a[e],d)):a[e]=c&&"data"!==e&&"aaData"!==
e&&h.isArray(d)?d.slice():d);return a}function Wa(a,b,c){h(a).bind("click.DT",b,function(b){a.blur();c(b)}).bind("keypress.DT",b,function(a){13===a.which&&(a.preventDefault(),c(a))}).bind("selectstart.DT",function(){return!1})}function z(a,b,c,d){c&&a[b].push({fn:c,sName:d})}function u(a,b,c,d){var e=[];b&&(e=h.map(a[b].slice().reverse(),function(b){return b.fn.apply(a.oInstance,d)}));null!==c&&(b=h.Event(c+".dt"),h(a.nTable).trigger(b,d),e.push(b.result));return e}function Sa(a){var b=a._iDisplayStart,
c=a.fnDisplayEnd(),d=a._iDisplayLength;b>=c&&(b=c-d);b-=b%d;if(-1===d||0>b)b=0;a._iDisplayStart=b}function Na(a,b){var c=a.renderer,d=m.ext.renderer[b];return h.isPlainObject(c)&&c[b]?d[c[b]]||d._:"string"===typeof c?d[c]||d._:d._}function y(a){return a.oFeatures.bServerSide?"ssp":a.ajax||a.sAjaxSource?"ajax":"dom"}function ya(a,b){var c=[],c=Mb.numbers_length,d=Math.floor(c/2);b<=c?c=W(0,b):a<=d?(c=W(0,c-2),c.push("ellipsis"),c.push(b-1)):(a>=b-1-d?c=W(b-(c-2),b):(c=W(a-d+2,a+d-1),c.push("ellipsis"),
c.push(b-1)),c.splice(0,0,"ellipsis"),c.splice(0,0,0));c.DT_el="span";return c}function db(a){h.each({num:function(b){return za(b,a)},"num-fmt":function(b){return za(b,a,Xa)},"html-num":function(b){return za(b,a,Aa)},"html-num-fmt":function(b){return za(b,a,Aa,Xa)}},function(b,c){v.type.order[b+a+"-pre"]=c;b.match(/^html\-/)&&(v.type.search[b+a]=v.type.search.html)})}function Nb(a){return function(){var b=[xa(this[m.ext.iApiIndex])].concat(Array.prototype.slice.call(arguments));return m.ext.internal[a].apply(this,
b)}}var m=function(a){this.$=function(a,b){return this.api(!0).$(a,b)};this._=function(a,b){return this.api(!0).rows(a,b).data()};this.api=function(a){return a?new r(xa(this[v.iApiIndex])):new r(this)};this.fnAddData=function(a,b){var c=this.api(!0),d=h.isArray(a)&&(h.isArray(a[0])||h.isPlainObject(a[0]))?c.rows.add(a):c.row.add(a);(b===k||b)&&c.draw();return d.flatten().toArray()};this.fnAdjustColumnSizing=function(a){var b=this.api(!0).columns.adjust(),c=b.settings()[0],d=c.oScroll;a===k||a?b.draw(!1):
(""!==d.sX||""!==d.sY)&&ka(c)};this.fnClearTable=function(a){var b=this.api(!0).clear();(a===k||a)&&b.draw()};this.fnClose=function(a){this.api(!0).row(a).child.hide()};this.fnDeleteRow=function(a,b,c){var d=this.api(!0),a=d.rows(a),e=a.settings()[0],h=e.aoData[a[0][0]];a.remove();b&&b.call(this,e,h);(c===k||c)&&d.draw();return h};this.fnDestroy=function(a){this.api(!0).destroy(a)};this.fnDraw=function(a){this.api(!0).draw(a)};this.fnFilter=function(a,b,c,d,e,h){e=this.api(!0);null===b||b===k?e.search(a,
c,d,h):e.column(b).search(a,c,d,h);e.draw()};this.fnGetData=function(a,b){var c=this.api(!0);if(a!==k){var d=a.nodeName?a.nodeName.toLowerCase():"";return b!==k||"td"==d||"th"==d?c.cell(a,b).data():c.row(a).data()||null}return c.data().toArray()};this.fnGetNodes=function(a){var b=this.api(!0);return a!==k?b.row(a).node():b.rows().nodes().flatten().toArray()};this.fnGetPosition=function(a){var b=this.api(!0),c=a.nodeName.toUpperCase();return"TR"==c?b.row(a).index():"TD"==c||"TH"==c?(a=b.cell(a).index(),
[a.row,a.columnVisible,a.column]):null};this.fnIsOpen=function(a){return this.api(!0).row(a).child.isShown()};this.fnOpen=function(a,b,c){return this.api(!0).row(a).child(b,c).show().child()[0]};this.fnPageChange=function(a,b){var c=this.api(!0).page(a);(b===k||b)&&c.draw(!1)};this.fnSetColumnVis=function(a,b,c){a=this.api(!0).column(a).visible(b);(c===k||c)&&a.columns.adjust().draw()};this.fnSettings=function(){return xa(this[v.iApiIndex])};this.fnSort=function(a){this.api(!0).order(a).draw()};this.fnSortListener=
function(a,b,c){this.api(!0).order.listener(a,b,c)};this.fnUpdate=function(a,b,c,d,e){var h=this.api(!0);c===k||null===c?h.row(b).data(a):h.cell(b,c).data(a);(e===k||e)&&h.columns.adjust();(d===k||d)&&h.draw();return 0};this.fnVersionCheck=v.fnVersionCheck;var b=this,c=a===k,d=this.length;c&&(a={});this.oApi=this.internal=v.internal;for(var e in m.ext.internal)e&&(this[e]=Nb(e));this.each(function(){var e={},e=1<d?Lb(e,a,!0):a,g=0,j,i=this.getAttribute("id"),n=!1,l=m.defaults,q=h(this);if("table"!=
this.nodeName.toLowerCase())L(null,0,"Non-table node initialisation ("+this.nodeName+")",2);else{eb(l);fb(l.column);K(l,l,!0);K(l.column,l.column,!0);K(l,h.extend(e,q.data()));var t=m.settings,g=0;for(j=t.length;g<j;g++){var p=t[g];if(p.nTable==this||p.nTHead.parentNode==this||p.nTFoot&&p.nTFoot.parentNode==this){g=e.bRetrieve!==k?e.bRetrieve:l.bRetrieve;if(c||g)return p.oInstance;if(e.bDestroy!==k?e.bDestroy:l.bDestroy){p.oInstance.fnDestroy();break}else{L(p,0,"Cannot reinitialise DataTable",3);
return}}if(p.sTableId==this.id){t.splice(g,1);break}}if(null===i||""===i)this.id=i="DataTables_Table_"+m.ext._unique++;var o=h.extend(!0,{},m.models.oSettings,{sDestroyWidth:q[0].style.width,sInstance:i,sTableId:i});o.nTable=this;o.oApi=b.internal;o.oInit=e;t.push(o);o.oInstance=1===b.length?b:q.dataTable();eb(e);e.oLanguage&&Da(e.oLanguage);e.aLengthMenu&&!e.iDisplayLength&&(e.iDisplayLength=h.isArray(e.aLengthMenu[0])?e.aLengthMenu[0][0]:e.aLengthMenu[0]);e=Lb(h.extend(!0,{},l),e);E(o.oFeatures,
e,"bPaginate bLengthChange bFilter bSort bSortMulti bInfo bProcessing bAutoWidth bSortClasses bServerSide bDeferRender".split(" "));E(o,e,["asStripeClasses","ajax","fnServerData","fnFormatNumber","sServerMethod","aaSorting","aaSortingFixed","aLengthMenu","sPaginationType","sAjaxSource","sAjaxDataProp","iStateDuration","sDom","bSortCellsTop","iTabIndex","fnStateLoadCallback","fnStateSaveCallback","renderer","searchDelay","rowId",["iCookieDuration","iStateDuration"],["oSearch","oPreviousSearch"],["aoSearchCols",
"aoPreSearchCols"],["iDisplayLength","_iDisplayLength"],["bJQueryUI","bJUI"]]);E(o.oScroll,e,[["sScrollX","sX"],["sScrollXInner","sXInner"],["sScrollY","sY"],["bScrollCollapse","bCollapse"]]);E(o.oLanguage,e,"fnInfoCallback");z(o,"aoDrawCallback",e.fnDrawCallback,"user");z(o,"aoServerParams",e.fnServerParams,"user");z(o,"aoStateSaveParams",e.fnStateSaveParams,"user");z(o,"aoStateLoadParams",e.fnStateLoadParams,"user");z(o,"aoStateLoaded",e.fnStateLoaded,"user");z(o,"aoRowCallback",e.fnRowCallback,
"user");z(o,"aoRowCreatedCallback",e.fnCreatedRow,"user");z(o,"aoHeaderCallback",e.fnHeaderCallback,"user");z(o,"aoFooterCallback",e.fnFooterCallback,"user");z(o,"aoInitComplete",e.fnInitComplete,"user");z(o,"aoPreDrawCallback",e.fnPreDrawCallback,"user");o.rowIdFn=Q(e.rowId);gb(o);i=o.oClasses;e.bJQueryUI?(h.extend(i,m.ext.oJUIClasses,e.oClasses),e.sDom===l.sDom&&"lfrtip"===l.sDom&&(o.sDom='<"H"lfr>t<"F"ip>'),o.renderer)?h.isPlainObject(o.renderer)&&!o.renderer.header&&(o.renderer.header="jqueryui"):
o.renderer="jqueryui":h.extend(i,m.ext.classes,e.oClasses);q.addClass(i.sTable);o.iInitDisplayStart===k&&(o.iInitDisplayStart=e.iDisplayStart,o._iDisplayStart=e.iDisplayStart);null!==e.iDeferLoading&&(o.bDeferLoading=!0,g=h.isArray(e.iDeferLoading),o._iRecordsDisplay=g?e.iDeferLoading[0]:e.iDeferLoading,o._iRecordsTotal=g?e.iDeferLoading[1]:e.iDeferLoading);var r=o.oLanguage;h.extend(!0,r,e.oLanguage);""!==r.sUrl&&(h.ajax({dataType:"json",url:r.sUrl,success:function(a){Da(a);K(l.oLanguage,a);h.extend(true,
r,a);ga(o)},error:function(){ga(o)}}),n=!0);null===e.asStripeClasses&&(o.asStripeClasses=[i.sStripeOdd,i.sStripeEven]);var g=o.asStripeClasses,v=q.children("tbody").find("tr").eq(0);-1!==h.inArray(!0,h.map(g,function(a){return v.hasClass(a)}))&&(h("tbody tr",this).removeClass(g.join(" ")),o.asDestroyStripes=g.slice());t=[];g=this.getElementsByTagName("thead");0!==g.length&&(da(o.aoHeader,g[0]),t=qa(o));if(null===e.aoColumns){p=[];g=0;for(j=t.length;g<j;g++)p.push(null)}else p=e.aoColumns;g=0;for(j=
p.length;g<j;g++)Ea(o,t?t[g]:null);ib(o,e.aoColumnDefs,p,function(a,b){ja(o,a,b)});if(v.length){var s=function(a,b){return a.getAttribute("data-"+b)!==null?b:null};h(v[0]).children("th, td").each(function(a,b){var c=o.aoColumns[a];if(c.mData===a){var d=s(b,"sort")||s(b,"order"),e=s(b,"filter")||s(b,"search");if(d!==null||e!==null){c.mData={_:a+".display",sort:d!==null?a+".@data-"+d:k,type:d!==null?a+".@data-"+d:k,filter:e!==null?a+".@data-"+e:k};ja(o,a)}}})}var w=o.oFeatures;e.bStateSave&&(w.bStateSave=
!0,Kb(o,e),z(o,"aoDrawCallback",wa,"state_save"));if(e.aaSorting===k){t=o.aaSorting;g=0;for(j=t.length;g<j;g++)t[g][1]=o.aoColumns[g].asSorting[0]}va(o);w.bSort&&z(o,"aoDrawCallback",function(){if(o.bSorted){var a=V(o),b={};h.each(a,function(a,c){b[c.src]=c.dir});u(o,null,"order",[o,a,b]);Jb(o)}});z(o,"aoDrawCallback",function(){(o.bSorted||y(o)==="ssp"||w.bDeferRender)&&va(o)},"sc");g=q.children("caption").each(function(){this._captionSide=q.css("caption-side")});j=q.children("thead");0===j.length&&
(j=h("<thead/>").appendTo(this));o.nTHead=j[0];j=q.children("tbody");0===j.length&&(j=h("<tbody/>").appendTo(this));o.nTBody=j[0];j=q.children("tfoot");if(0===j.length&&0<g.length&&(""!==o.oScroll.sX||""!==o.oScroll.sY))j=h("<tfoot/>").appendTo(this);0===j.length||0===j.children().length?q.addClass(i.sNoFooter):0<j.length&&(o.nTFoot=j[0],da(o.aoFooter,o.nTFoot));if(e.aaData)for(g=0;g<e.aaData.length;g++)N(o,e.aaData[g]);else(o.bDeferLoading||"dom"==y(o))&&ma(o,h(o.nTBody).children("tr"));o.aiDisplay=
o.aiDisplayMaster.slice();o.bInitialised=!0;!1===n&&ga(o)}});b=null;return this},v,r,p,s,Ya={},Ob=/[\r\n]/g,Aa=/<.*?>/g,ac=/^[\w\+\-]/,bc=/[\w\+\-]$/,cc=RegExp("(\\/|\\.|\\*|\\+|\\?|\\||\\(|\\)|\\[|\\]|\\{|\\}|\\\\|\\$|\\^|\\-)","g"),Xa=/[',$£€¥%\u2009\u202F\u20BD\u20a9\u20BArfk]/gi,M=function(a){return!a||!0===a||"-"===a?!0:!1},Pb=function(a){var b=parseInt(a,10);return!isNaN(b)&&isFinite(a)?b:null},Qb=function(a,b){Ya[b]||(Ya[b]=RegExp(Qa(b),"g"));return"string"===typeof a&&"."!==b?a.replace(/\./g,
"").replace(Ya[b],"."):a},Za=function(a,b,c){var d="string"===typeof a;if(M(a))return!0;b&&d&&(a=Qb(a,b));c&&d&&(a=a.replace(Xa,""));return!isNaN(parseFloat(a))&&isFinite(a)},Rb=function(a,b,c){return M(a)?!0:!(M(a)||"string"===typeof a)?null:Za(a.replace(Aa,""),b,c)?!0:null},G=function(a,b,c){var d=[],e=0,f=a.length;if(c!==k)for(;e<f;e++)a[e]&&a[e][b]&&d.push(a[e][b][c]);else for(;e<f;e++)a[e]&&d.push(a[e][b]);return d},ha=function(a,b,c,d){var e=[],f=0,g=b.length;if(d!==k)for(;f<g;f++)a[b[f]][c]&&
e.push(a[b[f]][c][d]);else for(;f<g;f++)e.push(a[b[f]][c]);return e},W=function(a,b){var c=[],d;b===k?(b=0,d=a):(d=b,b=a);for(var e=b;e<d;e++)c.push(e);return c},Sb=function(a){for(var b=[],c=0,d=a.length;c<d;c++)a[c]&&b.push(a[c]);return b},pa=function(a){var b=[],c,d,e=a.length,f,g=0;d=0;a:for(;d<e;d++){c=a[d];for(f=0;f<g;f++)if(b[f]===c)continue a;b.push(c);g++}return b};m.util={throttle:function(a,b){var c=b!==k?b:200,d,e;return function(){var b=this,g=+new Date,h=arguments;d&&g<d+c?(clearTimeout(e),
e=setTimeout(function(){d=k;a.apply(b,h)},c)):(d=g,a.apply(b,h))}},escapeRegex:function(a){return a.replace(cc,"\\$1")}};var A=function(a,b,c){a[b]!==k&&(a[c]=a[b])},ba=/\[.*?\]$/,U=/\(\)$/,Qa=m.util.escapeRegex,ua=h("<div>")[0],Zb=ua.textContent!==k,$b=/<.*?>/g,Oa=m.util.throttle,Tb=[],w=Array.prototype,dc=function(a){var b,c,d=m.settings,e=h.map(d,function(a){return a.nTable});if(a){if(a.nTable&&a.oApi)return[a];if(a.nodeName&&"table"===a.nodeName.toLowerCase())return b=h.inArray(a,e),-1!==b?[d[b]]:
null;if(a&&"function"===typeof a.settings)return a.settings().toArray();"string"===typeof a?c=h(a):a instanceof h&&(c=a)}else return[];if(c)return c.map(function(){b=h.inArray(this,e);return-1!==b?d[b]:null}).toArray()};r=function(a,b){if(!(this instanceof r))return new r(a,b);var c=[],d=function(a){(a=dc(a))&&(c=c.concat(a))};if(h.isArray(a))for(var e=0,f=a.length;e<f;e++)d(a[e]);else d(a);this.context=pa(c);b&&h.merge(this,b);this.selector={rows:null,cols:null,opts:null};r.extend(this,this,Tb)};
m.Api=r;h.extend(r.prototype,{any:function(){return 0!==this.count()},concat:w.concat,context:[],count:function(){return this.flatten().length},each:function(a){for(var b=0,c=this.length;b<c;b++)a.call(this,this[b],b,this);return this},eq:function(a){var b=this.context;return b.length>a?new r(b[a],this[a]):null},filter:function(a){var b=[];if(w.filter)b=w.filter.call(this,a,this);else for(var c=0,d=this.length;c<d;c++)a.call(this,this[c],c,this)&&b.push(this[c]);return new r(this.context,b)},flatten:function(){var a=
[];return new r(this.context,a.concat.apply(a,this.toArray()))},join:w.join,indexOf:w.indexOf||function(a,b){for(var c=b||0,d=this.length;c<d;c++)if(this[c]===a)return c;return-1},iterator:function(a,b,c,d){var e=[],f,g,h,i,n,l=this.context,m,t,p=this.selector;"string"===typeof a&&(d=c,c=b,b=a,a=!1);g=0;for(h=l.length;g<h;g++){var o=new r(l[g]);if("table"===b)f=c.call(o,l[g],g),f!==k&&e.push(f);else if("columns"===b||"rows"===b)f=c.call(o,l[g],this[g],g),f!==k&&e.push(f);else if("column"===b||"column-rows"===
b||"row"===b||"cell"===b){t=this[g];"column-rows"===b&&(m=Ba(l[g],p.opts));i=0;for(n=t.length;i<n;i++)f=t[i],f="cell"===b?c.call(o,l[g],f.row,f.column,g,i):c.call(o,l[g],f,g,i,m),f!==k&&e.push(f)}}return e.length||d?(a=new r(l,a?e.concat.apply([],e):e),b=a.selector,b.rows=p.rows,b.cols=p.cols,b.opts=p.opts,a):this},lastIndexOf:w.lastIndexOf||function(a,b){return this.indexOf.apply(this.toArray.reverse(),arguments)},length:0,map:function(a){var b=[];if(w.map)b=w.map.call(this,a,this);else for(var c=
0,d=this.length;c<d;c++)b.push(a.call(this,this[c],c));return new r(this.context,b)},pluck:function(a){return this.map(function(b){return b[a]})},pop:w.pop,push:w.push,reduce:w.reduce||function(a,b){return hb(this,a,b,0,this.length,1)},reduceRight:w.reduceRight||function(a,b){return hb(this,a,b,this.length-1,-1,-1)},reverse:w.reverse,selector:null,shift:w.shift,sort:w.sort,splice:w.splice,toArray:function(){return w.slice.call(this)},to$:function(){return h(this)},toJQuery:function(){return h(this)},
unique:function(){return new r(this.context,pa(this))},unshift:w.unshift});r.extend=function(a,b,c){if(c.length&&b&&(b instanceof r||b.__dt_wrapper)){var d,e,f,g=function(a,b,c){return function(){var d=b.apply(a,arguments);r.extend(d,d,c.methodExt);return d}};d=0;for(e=c.length;d<e;d++)f=c[d],b[f.name]="function"===typeof f.val?g(a,f.val,f):h.isPlainObject(f.val)?{}:f.val,b[f.name].__dt_wrapper=!0,r.extend(a,b[f.name],f.propExt)}};r.register=p=function(a,b){if(h.isArray(a))for(var c=0,d=a.length;c<
d;c++)r.register(a[c],b);else for(var e=a.split("."),f=Tb,g,j,c=0,d=e.length;c<d;c++){g=(j=-1!==e[c].indexOf("()"))?e[c].replace("()",""):e[c];var i;a:{i=0;for(var n=f.length;i<n;i++)if(f[i].name===g){i=f[i];break a}i=null}i||(i={name:g,val:{},methodExt:[],propExt:[]},f.push(i));c===d-1?i.val=b:f=j?i.methodExt:i.propExt}};r.registerPlural=s=function(a,b,c){r.register(a,c);r.register(b,function(){var a=c.apply(this,arguments);return a===this?this:a instanceof r?a.length?h.isArray(a[0])?new r(a.context,
a[0]):a[0]:k:a})};p("tables()",function(a){var b;if(a){b=r;var c=this.context;if("number"===typeof a)a=[c[a]];else var d=h.map(c,function(a){return a.nTable}),a=h(d).filter(a).map(function(){var a=h.inArray(this,d);return c[a]}).toArray();b=new b(a)}else b=this;return b});p("table()",function(a){var a=this.tables(a),b=a.context;return b.length?new r(b[0]):a});s("tables().nodes()","table().node()",function(){return this.iterator("table",function(a){return a.nTable},1)});s("tables().body()","table().body()",
function(){return this.iterator("table",function(a){return a.nTBody},1)});s("tables().header()","table().header()",function(){return this.iterator("table",function(a){return a.nTHead},1)});s("tables().footer()","table().footer()",function(){return this.iterator("table",function(a){return a.nTFoot},1)});s("tables().containers()","table().container()",function(){return this.iterator("table",function(a){return a.nTableWrapper},1)});p("draw()",function(a){return this.iterator("table",function(b){"page"===
a?O(b):("string"===typeof a&&(a="full-hold"===a?!1:!0),T(b,!1===a))})});p("page()",function(a){return a===k?this.page.info().page:this.iterator("table",function(b){Ta(b,a)})});p("page.info()",function(){if(0===this.context.length)return k;var a=this.context[0],b=a._iDisplayStart,c=a.oFeatures.bPaginate?a._iDisplayLength:-1,d=a.fnRecordsDisplay(),e=-1===c;return{page:e?0:Math.floor(b/c),pages:e?1:Math.ceil(d/c),start:b,end:a.fnDisplayEnd(),length:c,recordsTotal:a.fnRecordsTotal(),recordsDisplay:d,
serverSide:"ssp"===y(a)}});p("page.len()",function(a){return a===k?0!==this.context.length?this.context[0]._iDisplayLength:k:this.iterator("table",function(b){Ra(b,a)})});var Ub=function(a,b,c){if(c){var d=new r(a);d.one("draw",function(){c(d.ajax.json())})}if("ssp"==y(a))T(a,b);else{C(a,!0);var e=a.jqXHR;e&&4!==e.readyState&&e.abort();ra(a,[],function(c){na(a);for(var c=sa(a,c),d=0,e=c.length;d<e;d++)N(a,c[d]);T(a,b);C(a,!1)})}};p("ajax.json()",function(){var a=this.context;if(0<a.length)return a[0].json});
p("ajax.params()",function(){var a=this.context;if(0<a.length)return a[0].oAjaxData});p("ajax.reload()",function(a,b){return this.iterator("table",function(c){Ub(c,!1===b,a)})});p("ajax.url()",function(a){var b=this.context;if(a===k){if(0===b.length)return k;b=b[0];return b.ajax?h.isPlainObject(b.ajax)?b.ajax.url:b.ajax:b.sAjaxSource}return this.iterator("table",function(b){h.isPlainObject(b.ajax)?b.ajax.url=a:b.ajax=a})});p("ajax.url().load()",function(a,b){return this.iterator("table",function(c){Ub(c,
!1===b,a)})});var $a=function(a,b,c,d,e){var f=[],g,j,i,n,l,m;i=typeof b;if(!b||"string"===i||"function"===i||b.length===k)b=[b];i=0;for(n=b.length;i<n;i++){j=b[i]&&b[i].split?b[i].split(","):[b[i]];l=0;for(m=j.length;l<m;l++)(g=c("string"===typeof j[l]?h.trim(j[l]):j[l]))&&g.length&&(f=f.concat(g))}a=v.selector[a];if(a.length){i=0;for(n=a.length;i<n;i++)f=a[i](d,e,f)}return pa(f)},ab=function(a){a||(a={});a.filter&&a.search===k&&(a.search=a.filter);return h.extend({search:"none",order:"current",
page:"all"},a)},bb=function(a){for(var b=0,c=a.length;b<c;b++)if(0<a[b].length)return a[0]=a[b],a[0].length=1,a.length=1,a.context=[a.context[b]],a;a.length=0;return a},Ba=function(a,b){var c,d,e,f=[],g=a.aiDisplay;c=a.aiDisplayMaster;var j=b.search;d=b.order;e=b.page;if("ssp"==y(a))return"removed"===j?[]:W(0,c.length);if("current"==e){c=a._iDisplayStart;for(d=a.fnDisplayEnd();c<d;c++)f.push(g[c])}else if("current"==d||"applied"==d)f="none"==j?c.slice():"applied"==j?g.slice():h.map(c,function(a){return-1===
h.inArray(a,g)?a:null});else if("index"==d||"original"==d){c=0;for(d=a.aoData.length;c<d;c++)"none"==j?f.push(c):(e=h.inArray(c,g),(-1===e&&"removed"==j||0<=e&&"applied"==j)&&f.push(c))}return f};p("rows()",function(a,b){a===k?a="":h.isPlainObject(a)&&(b=a,a="");var b=ab(b),c=this.iterator("table",function(c){var e=b;return $a("row",a,function(a){var b=Pb(a);if(b!==null&&!e)return[b];var j=Ba(c,e);if(b!==null&&h.inArray(b,j)!==-1)return[b];if(!a)return j;if(typeof a==="function")return h.map(j,function(b){var e=
c.aoData[b];return a(b,e._aData,e.nTr)?b:null});b=Sb(ha(c.aoData,j,"nTr"));if(a.nodeName){if(a._DT_RowIndex!==k)return[a._DT_RowIndex];if(a._DT_CellIndex)return[a._DT_CellIndex.row];b=h(a).closest("*[data-dt-row]");return b.length?[b.data("dt-row")]:[]}if(typeof a==="string"&&a.charAt(0)==="#"){j=c.aIds[a.replace(/^#/,"")];if(j!==k)return[j.idx]}return h(b).filter(a).map(function(){return this._DT_RowIndex}).toArray()},c,e)},1);c.selector.rows=a;c.selector.opts=b;return c});p("rows().nodes()",function(){return this.iterator("row",
function(a,b){return a.aoData[b].nTr||k},1)});p("rows().data()",function(){return this.iterator(!0,"rows",function(a,b){return ha(a.aoData,b,"_aData")},1)});s("rows().cache()","row().cache()",function(a){return this.iterator("row",function(b,c){var d=b.aoData[c];return"search"===a?d._aFilterData:d._aSortData},1)});s("rows().invalidate()","row().invalidate()",function(a){return this.iterator("row",function(b,c){ca(b,c,a)})});s("rows().indexes()","row().index()",function(){return this.iterator("row",
function(a,b){return b},1)});s("rows().ids()","row().id()",function(a){for(var b=[],c=this.context,d=0,e=c.length;d<e;d++)for(var f=0,g=this[d].length;f<g;f++){var h=c[d].rowIdFn(c[d].aoData[this[d][f]]._aData);b.push((!0===a?"#":"")+h)}return new r(c,b)});s("rows().remove()","row().remove()",function(){var a=this;this.iterator("row",function(b,c,d){var e=b.aoData,f=e[c],g,h,i,n,l;e.splice(c,1);g=0;for(h=e.length;g<h;g++)if(i=e[g],l=i.anCells,null!==i.nTr&&(i.nTr._DT_RowIndex=g),null!==l){i=0;for(n=
l.length;i<n;i++)l[i]._DT_CellIndex.row=g}oa(b.aiDisplayMaster,c);oa(b.aiDisplay,c);oa(a[d],c,!1);Sa(b);c=b.rowIdFn(f._aData);c!==k&&delete b.aIds[c]});this.iterator("table",function(a){for(var c=0,d=a.aoData.length;c<d;c++)a.aoData[c].idx=c});return this});p("rows.add()",function(a){var b=this.iterator("table",function(b){var c,f,g,h=[];f=0;for(g=a.length;f<g;f++)c=a[f],c.nodeName&&"TR"===c.nodeName.toUpperCase()?h.push(ma(b,c)[0]):h.push(N(b,c));return h},1),c=this.rows(-1);c.pop();h.merge(c,b);
return c});p("row()",function(a,b){return bb(this.rows(a,b))});p("row().data()",function(a){var b=this.context;if(a===k)return b.length&&this.length?b[0].aoData[this[0]]._aData:k;b[0].aoData[this[0]]._aData=a;ca(b[0],this[0],"data");return this});p("row().node()",function(){var a=this.context;return a.length&&this.length?a[0].aoData[this[0]].nTr||null:null});p("row.add()",function(a){a instanceof h&&a.length&&(a=a[0]);var b=this.iterator("table",function(b){return a.nodeName&&"TR"===a.nodeName.toUpperCase()?
ma(b,a)[0]:N(b,a)});return this.row(b[0])});var cb=function(a,b){var c=a.context;if(c.length&&(c=c[0].aoData[b!==k?b:a[0]])&&c._details)c._details.remove(),c._detailsShow=k,c._details=k},Vb=function(a,b){var c=a.context;if(c.length&&a.length){var d=c[0].aoData[a[0]];if(d._details){(d._detailsShow=b)?d._details.insertAfter(d.nTr):d._details.detach();var e=c[0],f=new r(e),g=e.aoData;f.off("draw.dt.DT_details column-visibility.dt.DT_details destroy.dt.DT_details");0<G(g,"_details").length&&(f.on("draw.dt.DT_details",
function(a,b){e===b&&f.rows({page:"current"}).eq(0).each(function(a){a=g[a];a._detailsShow&&a._details.insertAfter(a.nTr)})}),f.on("column-visibility.dt.DT_details",function(a,b){if(e===b)for(var c,d=aa(b),f=0,h=g.length;f<h;f++)c=g[f],c._details&&c._details.children("td[colspan]").attr("colspan",d)}),f.on("destroy.dt.DT_details",function(a,b){if(e===b)for(var c=0,d=g.length;c<d;c++)g[c]._details&&cb(f,c)}))}}};p("row().child()",function(a,b){var c=this.context;if(a===k)return c.length&&this.length?
c[0].aoData[this[0]]._details:k;if(!0===a)this.child.show();else if(!1===a)cb(this);else if(c.length&&this.length){var d=c[0],c=c[0].aoData[this[0]],e=[],f=function(a,b){if(h.isArray(a)||a instanceof h)for(var c=0,k=a.length;c<k;c++)f(a[c],b);else a.nodeName&&"tr"===a.nodeName.toLowerCase()?e.push(a):(c=h("<tr><td/></tr>").addClass(b),h("td",c).addClass(b).html(a)[0].colSpan=aa(d),e.push(c[0]))};f(a,b);c._details&&c._details.remove();c._details=h(e);c._detailsShow&&c._details.insertAfter(c.nTr)}return this});
p(["row().child.show()","row().child().show()"],function(){Vb(this,!0);return this});p(["row().child.hide()","row().child().hide()"],function(){Vb(this,!1);return this});p(["row().child.remove()","row().child().remove()"],function(){cb(this);return this});p("row().child.isShown()",function(){var a=this.context;return a.length&&this.length?a[0].aoData[this[0]]._detailsShow||!1:!1});var ec=/^(.+):(name|visIdx|visible)$/,Wb=function(a,b,c,d,e){for(var c=[],d=0,f=e.length;d<f;d++)c.push(B(a,e[d],b));
return c};p("columns()",function(a,b){a===k?a="":h.isPlainObject(a)&&(b=a,a="");var b=ab(b),c=this.iterator("table",function(c){var e=a,f=b,g=c.aoColumns,j=G(g,"sName"),i=G(g,"nTh");return $a("column",e,function(a){var b=Pb(a);if(a==="")return W(g.length);if(b!==null)return[b>=0?b:g.length+b];if(typeof a==="function"){var e=Ba(c,f);return h.map(g,function(b,f){return a(f,Wb(c,f,0,0,e),i[f])?f:null})}var k=typeof a==="string"?a.match(ec):"";if(k)switch(k[2]){case "visIdx":case "visible":b=parseInt(k[1],
10);if(b<0){var m=h.map(g,function(a,b){return a.bVisible?b:null});return[m[m.length+b]]}return[Z(c,b)];case "name":return h.map(j,function(a,b){return a===k[1]?b:null});default:return[]}if(a.nodeName&&a._DT_CellIndex)return[a._DT_CellIndex.column];b=h(i).filter(a).map(function(){return h.inArray(this,i)}).toArray();if(b.length||!a.nodeName)return b;b=h(a).closest("*[data-dt-column]");return b.length?[b.data("dt-column")]:[]},c,f)},1);c.selector.cols=a;c.selector.opts=b;return c});s("columns().header()",
"column().header()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTh},1)});s("columns().footer()","column().footer()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTf},1)});s("columns().data()","column().data()",function(){return this.iterator("column-rows",Wb,1)});s("columns().dataSrc()","column().dataSrc()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].mData},1)});s("columns().cache()","column().cache()",
function(a){return this.iterator("column-rows",function(b,c,d,e,f){return ha(b.aoData,f,"search"===a?"_aFilterData":"_aSortData",c)},1)});s("columns().nodes()","column().nodes()",function(){return this.iterator("column-rows",function(a,b,c,d,e){return ha(a.aoData,e,"anCells",b)},1)});s("columns().visible()","column().visible()",function(a,b){var c=this.iterator("column",function(b,c){if(a===k)return b.aoColumns[c].bVisible;var f=b.aoColumns,g=f[c],j=b.aoData,i,n,l;if(a!==k&&g.bVisible!==a){if(a){var m=
h.inArray(!0,G(f,"bVisible"),c+1);i=0;for(n=j.length;i<n;i++)l=j[i].nTr,f=j[i].anCells,l&&l.insertBefore(f[c],f[m]||null)}else h(G(b.aoData,"anCells",c)).detach();g.bVisible=a;ea(b,b.aoHeader);ea(b,b.aoFooter);wa(b)}});a!==k&&(this.iterator("column",function(c,e){u(c,null,"column-visibility",[c,e,a,b])}),(b===k||b)&&this.columns.adjust());return c});s("columns().indexes()","column().index()",function(a){return this.iterator("column",function(b,c){return"visible"===a?$(b,c):c},1)});p("columns.adjust()",
function(){return this.iterator("table",function(a){Y(a)},1)});p("column.index()",function(a,b){if(0!==this.context.length){var c=this.context[0];if("fromVisible"===a||"toData"===a)return Z(c,b);if("fromData"===a||"toVisible"===a)return $(c,b)}});p("column()",function(a,b){return bb(this.columns(a,b))});p("cells()",function(a,b,c){h.isPlainObject(a)&&(a.row===k?(c=a,a=null):(c=b,b=null));h.isPlainObject(b)&&(c=b,b=null);if(null===b||b===k)return this.iterator("table",function(b){var d=a,e=ab(c),f=
b.aoData,g=Ba(b,e),j=Sb(ha(f,g,"anCells")),i=h([].concat.apply([],j)),l,n=b.aoColumns.length,m,p,r,u,v,s;return $a("cell",d,function(a){var c=typeof a==="function";if(a===null||a===k||c){m=[];p=0;for(r=g.length;p<r;p++){l=g[p];for(u=0;u<n;u++){v={row:l,column:u};if(c){s=f[l];a(v,B(b,l,u),s.anCells?s.anCells[u]:null)&&m.push(v)}else m.push(v)}}return m}if(h.isPlainObject(a))return[a];c=i.filter(a).map(function(a,b){return{row:b._DT_CellIndex.row,column:b._DT_CellIndex.column}}).toArray();if(c.length||
!a.nodeName)return c;s=h(a).closest("*[data-dt-row]");return s.length?[{row:s.data("dt-row"),column:s.data("dt-column")}]:[]},b,e)});var d=this.columns(b,c),e=this.rows(a,c),f,g,j,i,n,l=this.iterator("table",function(a,b){f=[];g=0;for(j=e[b].length;g<j;g++){i=0;for(n=d[b].length;i<n;i++)f.push({row:e[b][g],column:d[b][i]})}return f},1);h.extend(l.selector,{cols:b,rows:a,opts:c});return l});s("cells().nodes()","cell().node()",function(){return this.iterator("cell",function(a,b,c){return(a=a.aoData[b])&&
a.anCells?a.anCells[c]:k},1)});p("cells().data()",function(){return this.iterator("cell",function(a,b,c){return B(a,b,c)},1)});s("cells().cache()","cell().cache()",function(a){a="search"===a?"_aFilterData":"_aSortData";return this.iterator("cell",function(b,c,d){return b.aoData[c][a][d]},1)});s("cells().render()","cell().render()",function(a){return this.iterator("cell",function(b,c,d){return B(b,c,d,a)},1)});s("cells().indexes()","cell().index()",function(){return this.iterator("cell",function(a,
b,c){return{row:b,column:c,columnVisible:$(a,c)}},1)});s("cells().invalidate()","cell().invalidate()",function(a){return this.iterator("cell",function(b,c,d){ca(b,c,a,d)})});p("cell()",function(a,b,c){return bb(this.cells(a,b,c))});p("cell().data()",function(a){var b=this.context,c=this[0];if(a===k)return b.length&&c.length?B(b[0],c[0].row,c[0].column):k;jb(b[0],c[0].row,c[0].column,a);ca(b[0],c[0].row,"data",c[0].column);return this});p("order()",function(a,b){var c=this.context;if(a===k)return 0!==
c.length?c[0].aaSorting:k;"number"===typeof a?a=[[a,b]]:a.length&&!h.isArray(a[0])&&(a=Array.prototype.slice.call(arguments));return this.iterator("table",function(b){b.aaSorting=a.slice()})});p("order.listener()",function(a,b,c){return this.iterator("table",function(d){Ma(d,a,b,c)})});p("order.fixed()",function(a){if(!a){var b=this.context,b=b.length?b[0].aaSortingFixed:k;return h.isArray(b)?{pre:b}:b}return this.iterator("table",function(b){b.aaSortingFixed=h.extend(!0,{},a)})});p(["columns().order()",
"column().order()"],function(a){var b=this;return this.iterator("table",function(c,d){var e=[];h.each(b[d],function(b,c){e.push([c,a])});c.aaSorting=e})});p("search()",function(a,b,c,d){var e=this.context;return a===k?0!==e.length?e[0].oPreviousSearch.sSearch:k:this.iterator("table",function(e){e.oFeatures.bFilter&&fa(e,h.extend({},e.oPreviousSearch,{sSearch:a+"",bRegex:null===b?!1:b,bSmart:null===c?!0:c,bCaseInsensitive:null===d?!0:d}),1)})});s("columns().search()","column().search()",function(a,
b,c,d){return this.iterator("column",function(e,f){var g=e.aoPreSearchCols;if(a===k)return g[f].sSearch;e.oFeatures.bFilter&&(h.extend(g[f],{sSearch:a+"",bRegex:null===b?!1:b,bSmart:null===c?!0:c,bCaseInsensitive:null===d?!0:d}),fa(e,e.oPreviousSearch,1))})});p("state()",function(){return this.context.length?this.context[0].oSavedState:null});p("state.clear()",function(){return this.iterator("table",function(a){a.fnStateSaveCallback.call(a.oInstance,a,{})})});p("state.loaded()",function(){return this.context.length?
this.context[0].oLoadedState:null});p("state.save()",function(){return this.iterator("table",function(a){wa(a)})});m.versionCheck=m.fnVersionCheck=function(a){for(var b=m.version.split("."),a=a.split("."),c,d,e=0,f=a.length;e<f;e++)if(c=parseInt(b[e],10)||0,d=parseInt(a[e],10)||0,c!==d)return c>d;return!0};m.isDataTable=m.fnIsDataTable=function(a){var b=h(a).get(0),c=!1;h.each(m.settings,function(a,e){var f=e.nScrollHead?h("table",e.nScrollHead)[0]:null,g=e.nScrollFoot?h("table",e.nScrollFoot)[0]:
null;if(e.nTable===b||f===b||g===b)c=!0});return c};m.tables=m.fnTables=function(a){var b=!1;h.isPlainObject(a)&&(b=a.api,a=a.visible);var c=h.map(m.settings,function(b){if(!a||a&&h(b.nTable).is(":visible"))return b.nTable});return b?new r(c):c};m.camelToHungarian=K;p("$()",function(a,b){var c=this.rows(b).nodes(),c=h(c);return h([].concat(c.filter(a).toArray(),c.find(a).toArray()))});h.each(["on","one","off"],function(a,b){p(b+"()",function(){var a=Array.prototype.slice.call(arguments);a[0].match(/\.dt\b/)||
(a[0]+=".dt");var d=h(this.tables().nodes());d[b].apply(d,a);return this})});p("clear()",function(){return this.iterator("table",function(a){na(a)})});p("settings()",function(){return new r(this.context,this.context)});p("init()",function(){var a=this.context;return a.length?a[0].oInit:null});p("data()",function(){return this.iterator("table",function(a){return G(a.aoData,"_aData")}).flatten()});p("destroy()",function(a){a=a||!1;return this.iterator("table",function(b){var c=b.nTableWrapper.parentNode,
d=b.oClasses,e=b.nTable,f=b.nTBody,g=b.nTHead,j=b.nTFoot,i=h(e),f=h(f),k=h(b.nTableWrapper),l=h.map(b.aoData,function(a){return a.nTr}),p;b.bDestroying=!0;u(b,"aoDestroyCallback","destroy",[b]);a||(new r(b)).columns().visible(!0);k.unbind(".DT").find(":not(tbody *)").unbind(".DT");h(D).unbind(".DT-"+b.sInstance);e!=g.parentNode&&(i.children("thead").detach(),i.append(g));j&&e!=j.parentNode&&(i.children("tfoot").detach(),i.append(j));b.aaSorting=[];b.aaSortingFixed=[];va(b);h(l).removeClass(b.asStripeClasses.join(" "));
h("th, td",g).removeClass(d.sSortable+" "+d.sSortableAsc+" "+d.sSortableDesc+" "+d.sSortableNone);b.bJUI&&(h("th span."+d.sSortIcon+", td span."+d.sSortIcon,g).detach(),h("th, td",g).each(function(){var a=h("div."+d.sSortJUIWrapper,this);h(this).append(a.contents());a.detach()}));f.children().detach();f.append(l);g=a?"remove":"detach";i[g]();k[g]();!a&&c&&(c.insertBefore(e,b.nTableReinsertBefore),i.css("width",b.sDestroyWidth).removeClass(d.sTable),(p=b.asDestroyStripes.length)&&f.children().each(function(a){h(this).addClass(b.asDestroyStripes[a%
p])}));c=h.inArray(b,m.settings);-1!==c&&m.settings.splice(c,1)})});h.each(["column","row","cell"],function(a,b){p(b+"s().every()",function(a){var d=this.selector.opts,e=this;return this.iterator(b,function(f,g,h,i,n){a.call(e[b](g,"cell"===b?h:d,"cell"===b?d:k),g,h,i,n)})})});p("i18n()",function(a,b,c){var d=this.context[0],a=Q(a)(d.oLanguage);a===k&&(a=b);c!==k&&h.isPlainObject(a)&&(a=a[c]!==k?a[c]:a._);return a.replace("%d",c)});m.version="1.10.12";m.settings=[];m.models={};m.models.oSearch={bCaseInsensitive:!0,
sSearch:"",bRegex:!1,bSmart:!0};m.models.oRow={nTr:null,anCells:null,_aData:[],_aSortData:null,_aFilterData:null,_sFilterRow:null,_sRowStripe:"",src:null,idx:-1};m.models.oColumn={idx:null,aDataSort:null,asSorting:null,bSearchable:null,bSortable:null,bVisible:null,_sManualType:null,_bAttrSrc:!1,fnCreatedCell:null,fnGetData:null,fnSetData:null,mData:null,mRender:null,nTh:null,nTf:null,sClass:null,sContentPadding:null,sDefaultContent:null,sName:null,sSortDataType:"std",sSortingClass:null,sSortingClassJUI:null,
sTitle:null,sType:null,sWidth:null,sWidthOrig:null};m.defaults={aaData:null,aaSorting:[[0,"asc"]],aaSortingFixed:[],ajax:null,aLengthMenu:[10,25,50,100],aoColumns:null,aoColumnDefs:null,aoSearchCols:[],asStripeClasses:null,bAutoWidth:!0,bDeferRender:!1,bDestroy:!1,bFilter:!0,bInfo:!0,bJQueryUI:!1,bLengthChange:!0,bPaginate:!0,bProcessing:!1,bRetrieve:!1,bScrollCollapse:!1,bServerSide:!1,bSort:!0,bSortMulti:!0,bSortCellsTop:!1,bSortClasses:!0,bStateSave:!1,fnCreatedRow:null,fnDrawCallback:null,fnFooterCallback:null,
fnFormatNumber:function(a){return a.toString().replace(/\B(?=(\d{3})+(?!\d))/g,this.oLanguage.sThousands)},fnHeaderCallback:null,fnInfoCallback:null,fnInitComplete:null,fnPreDrawCallback:null,fnRowCallback:null,fnServerData:null,fnServerParams:null,fnStateLoadCallback:function(a){try{return JSON.parse((-1===a.iStateDuration?sessionStorage:localStorage).getItem("DataTables_"+a.sInstance+"_"+location.pathname))}catch(b){}},fnStateLoadParams:null,fnStateLoaded:null,fnStateSaveCallback:function(a,b){try{(-1===
a.iStateDuration?sessionStorage:localStorage).setItem("DataTables_"+a.sInstance+"_"+location.pathname,JSON.stringify(b))}catch(c){}},fnStateSaveParams:null,iStateDuration:7200,iDeferLoading:null,iDisplayLength:10,iDisplayStart:0,iTabIndex:0,oClasses:{},oLanguage:{oAria:{sSortAscending:": activate to sort column ascending",sSortDescending:": activate to sort column descending"},oPaginate:{sFirst:"First",sLast:"Last",sNext:"Next",sPrevious:"Previous"},sEmptyTable:"هیچ اطلاعاتی در این جدول نیست.",sInfo:"نمایش _START_ تا _END_ از _TOTAL_ تا مورد",
sInfoEmpty:"نمایش 0 تا 0 از 0 تا مورد",sInfoFiltered:"(filtered from _MAX_ total entries)",sInfoPostFix:"",sDecimal:"",sThousands:",",sLengthMenu:"نمایش _MENU_ مورد",sLoadingRecords:"Loading...",sProcessing:"Processing...",sSearch:"جست و جو:",sSearchPlaceholder:"",sUrl:"",sZeroRecords:"No matching records found"},oSearch:h.extend({},m.models.oSearch),sAjaxDataProp:"data",sAjaxSource:null,sDom:"lfrtip",searchDelay:null,sPaginationType:"simple_numbers",sScrollX:"",sScrollXInner:"",sScrollY:"",sServerMethod:"GET",
renderer:null,rowId:"DT_RowId"};X(m.defaults);m.defaults.column={aDataSort:null,iDataSort:-1,asSorting:["asc","desc"],bSearchable:!0,bSortable:!0,bVisible:!0,fnCreatedCell:null,mData:null,mRender:null,sCellType:"td",sClass:"",sContentPadding:"",sDefaultContent:null,sName:"",sSortDataType:"std",sTitle:null,sType:null,sWidth:null};X(m.defaults.column);m.models.oSettings={oFeatures:{bAutoWidth:null,bDeferRender:null,bFilter:null,bInfo:null,bLengthChange:null,bPaginate:null,bProcessing:null,bServerSide:null,
bSort:null,bSortMulti:null,bSortClasses:null,bStateSave:null},oScroll:{bCollapse:null,iBarWidth:0,sX:null,sXInner:null,sY:null},oLanguage:{fnInfoCallback:null},oBrowser:{bScrollOversize:!1,bScrollbarLeft:!1,bBounding:!1,barWidth:0},ajax:null,aanFeatures:[],aoData:[],aiDisplay:[],aiDisplayMaster:[],aIds:{},aoColumns:[],aoHeader:[],aoFooter:[],oPreviousSearch:{},aoPreSearchCols:[],aaSorting:null,aaSortingFixed:[],asStripeClasses:null,asDestroyStripes:[],sDestroyWidth:0,aoRowCallback:[],aoHeaderCallback:[],
aoFooterCallback:[],aoDrawCallback:[],aoRowCreatedCallback:[],aoPreDrawCallback:[],aoInitComplete:[],aoStateSaveParams:[],aoStateLoadParams:[],aoStateLoaded:[],sTableId:"",nTable:null,nTHead:null,nTFoot:null,nTBody:null,nTableWrapper:null,bDeferLoading:!1,bInitialised:!1,aoOpenRows:[],sDom:null,searchDelay:null,sPaginationType:"two_button",iStateDuration:0,aoStateSave:[],aoStateLoad:[],oSavedState:null,oLoadedState:null,sAjaxSource:null,sAjaxDataProp:null,bAjaxDataGet:!0,jqXHR:null,json:k,oAjaxData:k,
fnServerData:null,aoServerParams:[],sServerMethod:null,fnFormatNumber:null,aLengthMenu:null,iDraw:0,bDrawing:!1,iDrawError:-1,_iDisplayLength:10,_iDisplayStart:0,_iRecordsTotal:0,_iRecordsDisplay:0,bJUI:null,oClasses:{},bFiltered:!1,bSorted:!1,bSortCellsTop:null,oInit:null,aoDestroyCallback:[],fnRecordsTotal:function(){return"ssp"==y(this)?1*this._iRecordsTotal:this.aiDisplayMaster.length},fnRecordsDisplay:function(){return"ssp"==y(this)?1*this._iRecordsDisplay:this.aiDisplay.length},fnDisplayEnd:function(){var a=
this._iDisplayLength,b=this._iDisplayStart,c=b+a,d=this.aiDisplay.length,e=this.oFeatures,f=e.bPaginate;return e.bServerSide?!1===f||-1===a?b+d:Math.min(b+a,this._iRecordsDisplay):!f||c>d||-1===a?d:c},oInstance:null,sInstance:null,iTabIndex:0,nScrollHead:null,nScrollFoot:null,aLastSort:[],oPlugins:{},rowIdFn:null,rowId:null};m.ext=v={buttons:{},classes:{},builder:"-source-",errMode:"alert",feature:[],search:[],selector:{cell:[],column:[],row:[]},internal:{},legacy:{ajax:null},pager:{},renderer:{pageButton:{},
header:{}},order:{},type:{detect:[],search:{},order:{}},_unique:0,fnVersionCheck:m.fnVersionCheck,iApiIndex:0,oJUIClasses:{},sVersion:m.version};h.extend(v,{afnFiltering:v.search,aTypes:v.type.detect,ofnSearch:v.type.search,oSort:v.type.order,afnSortData:v.order,aoFeatures:v.feature,oApi:v.internal,oStdClasses:v.classes,oPagination:v.pager});h.extend(m.ext.classes,{sTable:"dataTable",sNoFooter:"no-footer",sPageButton:"paginate_button",sPageButtonActive:"current",sPageButtonDisabled:"disabled",sStripeOdd:"odd",
sStripeEven:"even",sRowEmpty:"dataTables_empty",sWrapper:"dataTables_wrapper",sFilter:"dataTables_filter",sInfo:"dataTables_info",sPaging:"dataTables_paginate paging_",sLength:"dataTables_length",sProcessing:"dataTables_processing",sSortAsc:"sorting_asc",sSortDesc:"sorting_desc",sSortable:"sorting",sSortableAsc:"sorting_asc_disabled",sSortableDesc:"sorting_desc_disabled",sSortableNone:"sorting_disabled",sSortColumn:"sorting_",sFilterInput:"",sLengthSelect:"",sScrollWrapper:"dataTables_scroll",sScrollHead:"dataTables_scrollHead",
sScrollHeadInner:"dataTables_scrollHeadInner",sScrollBody:"dataTables_scrollBody",sScrollFoot:"dataTables_scrollFoot",sScrollFootInner:"dataTables_scrollFootInner",sHeaderTH:"",sFooterTH:"",sSortJUIAsc:"",sSortJUIDesc:"",sSortJUI:"",sSortJUIAscAllowed:"",sSortJUIDescAllowed:"",sSortJUIWrapper:"",sSortIcon:"",sJUIHeader:"",sJUIFooter:""});var Ca="",Ca="",H=Ca+"ui-state-default",ia=Ca+"css_right ui-icon ui-icon-",Xb=Ca+"fg-toolbar ui-toolbar ui-widget-header ui-helper-clearfix";h.extend(m.ext.oJUIClasses,
m.ext.classes,{sPageButton:"fg-button ui-button "+H,sPageButtonActive:"ui-state-disabled",sPageButtonDisabled:"ui-state-disabled",sPaging:"dataTables_paginate fg-buttonset ui-buttonset fg-buttonset-multi ui-buttonset-multi paging_",sSortAsc:H+" sorting_asc",sSortDesc:H+" sorting_desc",sSortable:H+" sorting",sSortableAsc:H+" sorting_asc_disabled",sSortableDesc:H+" sorting_desc_disabled",sSortableNone:H+" sorting_disabled",sSortJUIAsc:ia+"triangle-1-n",sSortJUIDesc:ia+"triangle-1-s",sSortJUI:ia+"carat-2-n-s",
sSortJUIAscAllowed:ia+"carat-1-n",sSortJUIDescAllowed:ia+"carat-1-s",sSortJUIWrapper:"DataTables_sort_wrapper",sSortIcon:"DataTables_sort_icon",sScrollHead:"dataTables_scrollHead "+H,sScrollFoot:"dataTables_scrollFoot "+H,sHeaderTH:H,sFooterTH:H,sJUIHeader:Xb+" ui-corner-tl ui-corner-tr",sJUIFooter:Xb+" ui-corner-bl ui-corner-br"});var Mb=m.ext.pager;h.extend(Mb,{simple:function(){return["previous","next"]},full:function(){return["first","previous","next","last"]},numbers:function(a,b){return[ya(a,
b)]},simple_numbers:function(a,b){return["previous",ya(a,b),"next"]},full_numbers:function(a,b){return["first","previous",ya(a,b),"next","last"]},_numbers:ya,numbers_length:7});h.extend(!0,m.ext.renderer,{pageButton:{_:function(a,b,c,d,e,f){var g=a.oClasses,j=a.oLanguage.oPaginate,i=a.oLanguage.oAria.paginate||{},k,l,m=0,p=function(b,d){var o,r,u,s,v=function(b){Ta(a,b.data.action,true)};o=0;for(r=d.length;o<r;o++){s=d[o];if(h.isArray(s)){u=h("<"+(s.DT_el||"div")+"/>").appendTo(b);p(u,s)}else{k=null;
l="";switch(s){case "ellipsis":b.append('<span class="ellipsis">&#x2026;</span>');break;case "first":k=j.sFirst;l=s+(e>0?"":" "+g.sPageButtonDisabled);break;case "previous":k=j.sPrevious;l=s+(e>0?"":" "+g.sPageButtonDisabled);break;case "next":k=j.sNext;l=s+(e<f-1?"":" "+g.sPageButtonDisabled);break;case "last":k=j.sLast;l=s+(e<f-1?"":" "+g.sPageButtonDisabled);break;default:k=s+1;l=e===s?g.sPageButtonActive:""}if(k!==null){u=h("<a>",{"class":g.sPageButton+" "+l,"aria-controls":a.sTableId,"aria-label":i[s],
"data-dt-idx":m,tabindex:a.iTabIndex,id:c===0&&typeof s==="string"?a.sTableId+"_"+s:null}).html(k).appendTo(b);Wa(u,{action:s},v);m++}}}},r;try{r=h(b).find(I.activeElement).data("dt-idx")}catch(o){}p(h(b).empty(),d);r&&h(b).find("[data-dt-idx="+r+"]").focus()}}});h.extend(m.ext.type.detect,[function(a,b){var c=b.oLanguage.sDecimal;return Za(a,c)?"num"+c:null},function(a){if(a&&!(a instanceof Date)&&(!ac.test(a)||!bc.test(a)))return null;var b=Date.parse(a);return null!==b&&!isNaN(b)||M(a)?"date":
null},function(a,b){var c=b.oLanguage.sDecimal;return Za(a,c,!0)?"num-fmt"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Rb(a,c)?"html-num"+c:null},function(a,b){var c=b.oLanguage.sDecimal;return Rb(a,c,!0)?"html-num-fmt"+c:null},function(a){return M(a)||"string"===typeof a&&-1!==a.indexOf("<")?"html":null}]);h.extend(m.ext.type.search,{html:function(a){return M(a)?a:"string"===typeof a?a.replace(Ob," ").replace(Aa,""):""},string:function(a){return M(a)?a:"string"===typeof a?a.replace(Ob,
" "):a}});var za=function(a,b,c,d){if(0!==a&&(!a||"-"===a))return-Infinity;b&&(a=Qb(a,b));a.replace&&(c&&(a=a.replace(c,"")),d&&(a=a.replace(d,"")));return 1*a};h.extend(v.type.order,{"date-pre":function(a){return Date.parse(a)||0},"html-pre":function(a){return M(a)?"":a.replace?a.replace(/<.*?>/g,"").toLowerCase():a+""},"string-pre":function(a){return M(a)?"":"string"===typeof a?a.toLowerCase():!a.toString?"":a.toString()},"string-asc":function(a,b){return a<b?-1:a>b?1:0},"string-desc":function(a,
b){return a<b?1:a>b?-1:0}});db("");h.extend(!0,m.ext.renderer,{header:{_:function(a,b,c,d){h(a.nTable).on("order.dt.DT",function(e,f,g,h){if(a===f){e=c.idx;b.removeClass(c.sSortingClass+" "+d.sSortAsc+" "+d.sSortDesc).addClass(h[e]=="asc"?d.sSortAsc:h[e]=="desc"?d.sSortDesc:c.sSortingClass)}})},jqueryui:function(a,b,c,d){h("<div/>").addClass(d.sSortJUIWrapper).append(b.contents()).append(h("<span/>").addClass(d.sSortIcon+" "+c.sSortingClassJUI)).appendTo(b);h(a.nTable).on("order.dt.DT",function(e,
f,g,h){if(a===f){e=c.idx;b.removeClass(d.sSortAsc+" "+d.sSortDesc).addClass(h[e]=="asc"?d.sSortAsc:h[e]=="desc"?d.sSortDesc:c.sSortingClass);b.find("span."+d.sSortIcon).removeClass(d.sSortJUIAsc+" "+d.sSortJUIDesc+" "+d.sSortJUI+" "+d.sSortJUIAscAllowed+" "+d.sSortJUIDescAllowed).addClass(h[e]=="asc"?d.sSortJUIAsc:h[e]=="desc"?d.sSortJUIDesc:c.sSortingClassJUI)}})}}});var Yb=function(a){return"string"===typeof a?a.replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"):a};m.render={number:function(a,
b,c,d,e){return{display:function(f){if("number"!==typeof f&&"string"!==typeof f)return f;var g=0>f?"-":"",h=parseFloat(f);if(isNaN(h))return Yb(f);f=Math.abs(h);h=parseInt(f,10);f=c?b+(f-h).toFixed(c).substring(2):"";return g+(d||"")+h.toString().replace(/\B(?=(\d{3})+(?!\d))/g,a)+f+(e||"")}}},text:function(){return{display:Yb}}};h.extend(m.ext.internal,{_fnExternApiFunc:Nb,_fnBuildAjax:ra,_fnAjaxUpdate:lb,_fnAjaxParameters:ub,_fnAjaxUpdateDraw:vb,_fnAjaxDataSrc:sa,_fnAddColumn:Ea,_fnColumnOptions:ja,
_fnAdjustColumnSizing:Y,_fnVisibleToColumnIndex:Z,_fnColumnIndexToVisible:$,_fnVisbleColumns:aa,_fnGetColumns:la,_fnColumnTypes:Ga,_fnApplyColumnDefs:ib,_fnHungarianMap:X,_fnCamelToHungarian:K,_fnLanguageCompat:Da,_fnBrowserDetect:gb,_fnAddData:N,_fnAddTr:ma,_fnNodeToDataIndex:function(a,b){return b._DT_RowIndex!==k?b._DT_RowIndex:null},_fnNodeToColumnIndex:function(a,b,c){return h.inArray(c,a.aoData[b].anCells)},_fnGetCellData:B,_fnSetCellData:jb,_fnSplitObjNotation:Ja,_fnGetObjectDataFn:Q,_fnSetObjectDataFn:R,
_fnGetDataMaster:Ka,_fnClearTable:na,_fnDeleteIndex:oa,_fnInvalidate:ca,_fnGetRowElements:Ia,_fnCreateTr:Ha,_fnBuildHead:kb,_fnDrawHead:ea,_fnDraw:O,_fnReDraw:T,_fnAddOptionsHtml:nb,_fnDetectHeader:da,_fnGetUniqueThs:qa,_fnFeatureHtmlFilter:pb,_fnFilterComplete:fa,_fnFilterCustom:yb,_fnFilterColumn:xb,_fnFilter:wb,_fnFilterCreateSearch:Pa,_fnEscapeRegex:Qa,_fnFilterData:zb,_fnFeatureHtmlInfo:sb,_fnUpdateInfo:Cb,_fnInfoMacros:Db,_fnInitialise:ga,_fnInitComplete:ta,_fnLengthChange:Ra,_fnFeatureHtmlLength:ob,
_fnFeatureHtmlPaginate:tb,_fnPageChange:Ta,_fnFeatureHtmlProcessing:qb,_fnProcessingDisplay:C,_fnFeatureHtmlTable:rb,_fnScrollDraw:ka,_fnApplyToChildren:J,_fnCalculateColumnWidths:Fa,_fnThrottle:Oa,_fnConvertToWidth:Fb,_fnGetWidestNode:Gb,_fnGetMaxLenString:Hb,_fnStringToCss:x,_fnSortFlatten:V,_fnSort:mb,_fnSortAria:Jb,_fnSortListener:Va,_fnSortAttachListener:Ma,_fnSortingClasses:va,_fnSortData:Ib,_fnSaveState:wa,_fnLoadState:Kb,_fnSettingsFromNode:xa,_fnLog:L,_fnMap:E,_fnBindAction:Wa,_fnCallbackReg:z,
_fnCallbackFire:u,_fnLengthOverflow:Sa,_fnRenderer:Na,_fnDataSource:y,_fnRowAttributes:La,_fnCalculateEnd:function(){}});h.fn.dataTable=m;m.$=h;h.fn.dataTableSettings=m.settings;h.fn.dataTableExt=m.ext;h.fn.DataTable=function(a){return h(this).dataTable(a).api()};h.each(m,function(a,b){h.fn.DataTable[a]=b});return h.fn.dataTable});

File diff suppressed because one or more lines are too long

View File

@@ -1,126 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>سامنه ثبت ورود و خروج</title>
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
<link rel="stylesheet" href="/static/icon.css">
<link rel="stylesheet" href="/static/form.css">
<link rel="stylesheet" href="/static/styles.css">
<link rel="stylesheet" href="/static/dropzone.css">
</head>
<body dir="rtl">
<div id="search_page">
<h3>ثبت ورود و خروج</h3>
<div class="container fleft fone">
<div class="app">
<a href="#" id="start-camera" class="visible">Touch here to start the app.</a>
<video id="camera-stream"></video>
<img id="snap">
<p id="error-message"></p>
<div class="controls">
<a href="#" id="take-photo" title="Take Photo"><i class="material-icons">camera_alt</i></a>
<a href="#" id="delete-photo" title="Delete Photo" class="disabled"><i class="material-icons">live_tv</i></a>
</div>
<!-- Hidden canvas element. Used for taking snapshot of video. -->
<canvas></canvas>
</div>
</div>
<div class="container fright fone tright" id="Persons">
<h4>مشخصات فرد</h4>
<div>
<p>نام: {{ namep }}</p>
<p>کد ملی: {{ navp }}</p>
<p>آخرین حضور: {{ timep }}</p>
<div class="flex" dir="ltr">
<a class="btn effect01"><span>ثبت خروج</span></a>
<a class="btn effect01"><span>ثبت ورود</span></a>
</div>
</div>
</div>
</div>
<div id="add_page">
<h3>افزودن کاربر</h3>
<div class="one_container">
<h4>مشخصات فرد</h4>
<label for="fname">نام و نام خانوادگی:</label>
<input id="fname" type="text" name="fname" />
<label for="fnav">کد ملی:</label>
<input id="fnav" type="text" name="fnav" />
<div>
<span class="fright" id="add_result"></span>
<div class="flex" dir="ltr">
<a class="btn effect01" onclick="savenew()"><span>ثبت</span></a>
</div>
</div>
<div class="fright">
<div class="fright file">
<label for="one_img">تصویر اول:</label>
<form action="/one_img"
class="dropzone files"
id="one_img"></form>
</div>
<div class="fright file">
<label for="tow_img">تصویر دوم:</label>
<form action="/two_img"
class="dropzone files"
id="tow_img"></form>
</div>
<div class="fright file">
<label for="tre_img">تصویر سوم:</label>
<form action="/tre_img"
class="dropzone files"
id="tre_img"></form>
</div>
<div class="fright file">
<label for="for_img">تصویر چهارم:</label>
<form action="/for_img"
class="dropzone"
id="for_img"></form>
</div>
</div>
</div>
</div>
<div class="add_icon_Circle" onclick="cheangpage()">
<i class="material-icons add_icon">add</i>
</div>
<style>
</style>
<script src="/static/script.js"></script>
<script src="/static/dropzone.js"></script>
<script>
cheangpage();
Persons.innerHTML = "<h4>مشخصات فرد</h4>";
</script>
</body>
</html>

View File

@@ -1,96 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ title }}</title>
<link rel="stylesheet" href="/static/styles.css">
<link rel="stylesheet" href="/static/tadel/jquery.dataTables.min.css" />
<link rel="stylesheet" href="/static/tadel/buttons.dataTables.min.css" />
<Script type="text/javascript" src="/static/tadel/jquery-1.12.3.js"></Script>
<Script type="text/javascript" src="/static/tadel/jquery.dataTables.min.js"></Script>
<Script type="text/javascript" src="/static/tadel/jszip.min.js"></Script>
<Script type="text/javascript" src="/static/tadel/buttons.html5.min.js"></Script>
<Script type="text/javascript" src="/static/tadel/dataTables.buttons.min.js"></Script>
<style>
th,td{
color: black;
border-color: black;
}
</style>
</head>
<body>
<header class="masthead bg-primary text-white text-center" dir="rtl">
<div class="container d-flex align-items-center flex-column" style="min-width:96%;">
<h2>{{ title }}</h2>
<hr class="dropdown-divider">
<script>
var col = document.getElementsByTagName('th').length-1;
$(document).ready(function () {
$(document).ready(function () {
$('table').DataTable({
dom: 'Blfrtip',
buttons: [{
text: 'دریافت فایل Excel',
extend: 'excelHtml5',
exportOptions: {
modifier: {
selected: true
},
columns: {{ columns }},
format: {
header: function (data, columnIdx) {
return data;
},
body: function (data, column, row) {
// Strip $ from salary column to make it numeric
debugger;
return column === col ? "" : data;
}
}
},
footer: false,
customize: function (xlsx) {
var sheet = xlsx.xl.worksheets['sheet1.xml'];
//$('c[r=A1] t', sheet).text( 'Custom text' );
//$('row c[r^="C"]', sheet).attr('s', '2');
}
}]
});
});
});
</script>
<table>
<thead>
<tr>
{% for thead in theads %}
<th scope="col">{{ thead }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for tbody in tbodys %}
<tr>
{% for td in tbody %}
<td scope="col">{{ td }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</header>
</body>
</html>

49
tests/conftest.py Normal file
View File

@@ -0,0 +1,49 @@
from __future__ import annotations
import pytest
from app import create_app
from app.extensions import db
from app.models import Account, Role
@pytest.fixture()
def app():
app = create_app(
"testing",
{
"SECRET_KEY": "test-secret",
"SQLALCHEMY_DATABASE_URI": "sqlite:///:memory:",
},
)
with app.app_context():
db.create_all()
yield app
db.session.remove()
db.drop_all()
@pytest.fixture()
def client(app):
return app.test_client()
def create_account(username: str = "admin", role: str = Role.ADMIN.value) -> Account:
account = Account(
username=username,
email=f"{username}@example.com",
full_name=username.title(),
role=role,
)
account.set_password("Password12345")
db.session.add(account)
db.session.commit()
return account
def login(client, username: str = "admin", password: str = "Password12345"):
return client.post(
"/auth/login",
data={"username": username, "password": password},
follow_redirects=True,
)

44
tests/test_attendance.py Normal file
View File

@@ -0,0 +1,44 @@
from __future__ import annotations
from app.extensions import db
from app.models import AttendanceEvent, AttendanceType, Person, Role
from tests.conftest import create_account, login
def test_operator_can_record_attendance(app, client):
with app.app_context():
create_account(username="operator", role=Role.OPERATOR.value)
person = Person(full_name="Ada Lovelace", employee_id="EMP-001")
db.session.add(person)
db.session.commit()
person_id = person.id
login(client, "operator")
response = client.post(
"/attendance/events",
data={"person_id": person_id, "event_type": AttendanceType.CHECK_IN.value},
headers={"Accept": "application/json"},
)
assert response.status_code == 200
assert response.get_json()["event_type"] == "Check in"
with app.app_context():
assert AttendanceEvent.query.count() == 1
def test_viewer_cannot_record_attendance(app, client):
with app.app_context():
create_account(username="viewer", role=Role.VIEWER.value)
person = Person(full_name="Grace Hopper", employee_id="EMP-002")
db.session.add(person)
db.session.commit()
person_id = person.id
login(client, "viewer")
response = client.post(
"/attendance/events",
data={"person_id": person_id, "event_type": AttendanceType.CHECK_OUT.value},
headers={"Accept": "application/json"},
)
assert response.status_code == 403

55
tests/test_auth.py Normal file
View File

@@ -0,0 +1,55 @@
from __future__ import annotations
from app.models import Account, Role
from tests.conftest import create_account, login
def test_passwords_are_hashed(app):
with app.app_context():
account = create_account()
assert account.password_hash != "Password12345"
assert account.check_password("Password12345")
def test_login_redirects_to_dashboard(app, client):
with app.app_context():
create_account()
response = login(client)
assert response.status_code == 200
assert b"Dashboard" in response.data
def test_admin_can_create_operator_account(app, client):
with app.app_context():
create_account()
login(client)
response = client.post(
"/admin/accounts/new",
data={
"full_name": "Operations User",
"username": "operator",
"email": "operator@example.com",
"role": Role.OPERATOR.value,
"password": "Password12345",
"confirm_password": "Password12345",
},
follow_redirects=True,
)
assert response.status_code == 200
with app.app_context():
account = Account.query.filter_by(username="operator").one()
assert account.role == Role.OPERATOR.value
def test_operator_cannot_manage_accounts(app, client):
with app.app_context():
create_account(username="operator", role=Role.OPERATOR.value)
login(client, "operator")
response = client.get("/admin/accounts")
assert response.status_code == 403

3
wsgi.py Normal file
View File

@@ -0,0 +1,3 @@
from app import create_app
app = create_app()