Overview
The Visit Tracking system logs all library visits, capturing visitor information, entry times, and activities. This feature provides valuable usage statistics and helps track library traffic patterns without requiring user authentication.
Purpose
Usage Analytics Track library traffic patterns and peak hours
Activity Insights Understand what activities users engage in (study, research, computer use)
Compliance Meet institutional reporting requirements for library utilization
Resource Planning Make data-driven decisions about staffing and resource allocation
LibraryLog Model
The LibraryLog model captures visit information:
class LibraryLog ( db . Model ):
id = db.Column(db.Integer, primary_key = True )
visitor_name = db.Column(db.String( 100 ), nullable = False )
visitor_id = db.Column(db.String( 20 ), nullable = False )
role = db.Column(db.String( 20 ), nullable = False )
entry_time = db.Column(db.DateTime, default = datetime.utcnow)
activity = db.Column(db.String( 50 ), nullable = False )
Fields Explained
Field Type Description Example visitor_nameString Full name of visitor ”María García” visitor_idString Document ID number ”123456789” roleString User role or “Visitante" "cliente”, “premium”, “Visitante” entry_timeDateTime Timestamp of visit (UTC) 2024-03-15 14:30:00 activityString Purpose of visit ”Estudio”, “Computadores”, “Lectura”
Visit Registration Route
The visit registration route handles both registered users and walk-in visitors:
@bp.route ( '/visit' , methods = [ 'GET' , 'POST' ])
def register_visit ():
form = VisitForm()
if form.validate_on_submit():
document_id = form.document_id.data.strip()
activity = form.activity.data.strip()
manual_name = form.visitor_name.data.strip() if form.visitor_name.data else ''
# Check if user is registered
user = User.query.filter_by( document_id = document_id).first()
if user:
# Registered user - use account info
name, role = user.full_name, user.role
else :
# Unregistered visitor - require manual name
if not manual_name:
flash( 'Documento no registrado. Por favor ingrese su Nombre.' , 'warning' )
return render_template( 'main/visit.html' , form = form, pre_doc = document_id)
name, role = manual_name, 'Visitante'
# Create visit log entry
visit = LibraryLog(
visitor_name = name,
visitor_id = document_id,
role = role,
activity = activity
)
db.session.add(visit)
db.session.commit()
flash( f 'Bienvenido/a { name } . Actividad: { activity } ' , 'success' )
return redirect(url_for( 'main.register_visit' ))
return render_template( 'main/visit.html' , form = form)
User Flows
Registered User Flow
Enter Document ID
User enters their registered document ID number
Automatic Lookup
System retrieves user’s full name and role from database
Select Activity
User chooses visit purpose from predefined list
Confirmation
System logs visit and displays welcome message with user’s name
# Registered user lookup
user = User.query.filter_by( document_id = document_id).first()
if user:
name, role = user.full_name, user.role
Unregistered Visitor Flow
Enter Document ID
Visitor enters their document ID (not in system)
Manual Name Entry
System prompts for manual name entry
Select Activity
Visitor chooses visit purpose
Guest Registration
System logs visit with role “Visitante”
# Unregistered visitor handling
if not user:
if not manual_name:
flash( 'Documento no registrado. Por favor ingrese su Nombre.' , 'warning' )
return render_template( 'main/visit.html' , form = form, pre_doc = document_id)
name, role = manual_name, 'Visitante'
Guest Access : The system allows non-registered visitors to log their visits, making it useful for public libraries or open-access study spaces.
class VisitForm ( FlaskForm ):
document_id = StringField( 'Documento' , validators = [DataRequired()])
activity = StringField( 'Actividad' , validators = [DataRequired()])
submit = SubmitField( 'Registrar Visita' )
Form Fields : The form uses simple StringField for activity to allow free-text entry. The template (main/visit.html) includes an additional visitor_name field for unregistered visitors, which is accessed in the route handler via form.visitor_name.data.
Activity Examples
Common activities logged by visitors include:
General study and homework activities
Reading books, magazines, or other materials
Using library computers or internet access
Academic research and reference consultation
Looking up specific materials or resources
Other activities not listed
Template Implementation
< form method = "POST" action = "{{ url_for('main.register_visit') }}" >
{{ form.hidden_tag() }}
< div class = "form-group" >
{{ form.document_id.label }}
{{ form.document_id(class="form-control",
placeholder="Ingrese su documento",
value=pre_doc if pre_doc else '') }}
</ div >
< div class = "form-group" >
{{ form.visitor_name.label }}
{{ form.visitor_name(class="form-control",
placeholder="Solo si no está registrado") }}
</ div >
< div class = "form-group" >
{{ form.activity.label }}
{{ form.activity(class="form-control") }}
</ div >
{{ form.submit(class="btn btn-primary") }}
</ form >
Welcome Message
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
< div class = "alert alert-{{ category }}" role = "alert" >
{{ message }}
{# e.g., "Bienvenido/a María García. Actividad: Estudio" #}
</ div >
{% endfor %}
{% endwith %}
Analytics and Reporting
Visit Count by Date
from sqlalchemy import func, Date
# Daily visit counts
daily_visits = db.session.query(
func.date(LibraryLog.entry_time).label( 'date' ),
func.count(LibraryLog.id).label( 'count' )
).group_by(func.date(LibraryLog.entry_time)).all()
Visit Count by Activity
# Activity distribution
activity_stats = db.session.query(
LibraryLog.activity,
func.count(LibraryLog.id).label( 'count' )
).group_by(LibraryLog.activity).order_by(
func.count(LibraryLog.id).desc()
).all()
Visit Count by Role
# User type breakdown
role_distribution = db.session.query(
LibraryLog.role,
func.count(LibraryLog.id).label( 'count' )
).group_by(LibraryLog.role).all()
Peak Hours Analysis
from sqlalchemy import extract
# Hourly traffic patterns
hourly_traffic = db.session.query(
extract( 'hour' , LibraryLog.entry_time).label( 'hour' ),
func.count(LibraryLog.id).label( 'count' )
).group_by(extract( 'hour' , LibraryLog.entry_time)).order_by( 'hour' ).all()
Use Cases
Kiosk-Style Entry Terminal
Deploy on a tablet or touchscreen at library entrance:
Setup :
- Touchscreen tablet mounted at entrance
- Browser locked to /visit route
- Large, simple UI for quick entry
- Optional barcode scanner for document IDs
Workflow :
- User scans ID card or enters manually
- Taps activity button
- Receives welcome message
- Total time : < 10 seconds
Staffed Reception Desk
Librarian logs visitors manually:
Setup :
- Desktop computer at reception
- /visit route open in browser tab
Workflow :
- Visitor states document ID
- Librarian enters ID and activity
- Librarian verifies welcome message shows correct name
- Visitor proceeds into library
Reporting Dashboard
Generate usage reports for administrators:
@bp.route ( '/admin/visit_stats' )
@role_required ( 'admin' , 'bibliotecario' )
def visit_statistics ():
# Get date range from request
start_date = request.args.get( 'start' , default_start)
end_date = request.args.get( 'end' , default_end)
# Query visits in range
visits = LibraryLog.query.filter(
LibraryLog.entry_time >= start_date,
LibraryLog.entry_time <= end_date
).all()
# Calculate statistics
total_visits = len (visits)
unique_visitors = len ( set (v.visitor_id for v in visits))
activity_breakdown = { ... }
return render_template( 'admin/visit_stats.html' ,
total = total_visits,
unique = unique_visitors,
activities = activity_breakdown)
Data Privacy Considerations
Personal Data : Visit logs contain personally identifiable information (names and document IDs). Ensure compliance with local data protection regulations.
Best Practices
Data Retention Policy Implement automatic deletion of logs older than necessary (e.g., 1 year)
Access Control Restrict visit log viewing to admin and bibliotecario roles only
Anonymization For public reports, aggregate data without showing individual names
Informed Consent Display privacy notice at visit registration terminal
Implementing Data Retention
from datetime import datetime, timedelta
@ staticmethod
def purge_old_visits ( days = 365 ):
"""Delete visit logs older than specified days"""
cutoff_date = datetime.utcnow() - timedelta( days = days)
old_visits = LibraryLog.query.filter(
LibraryLog.entry_time < cutoff_date
).delete()
db.session.commit()
return old_visits
Integration with Other Features
Linking to User Accounts
# Optional: Add relationship to User model
class User ( UserMixin , db . Model ):
# ... existing fields ...
visits = db.relationship( 'LibraryLog' ,
primaryjoin = 'User.document_id==LibraryLog.visitor_id' ,
foreign_keys = 'LibraryLog.visitor_id' ,
backref = 'registered_user' ,
lazy = 'dynamic' )
Combining with Loan Data
# User profile showing both loans and visits
@bp.route ( '/profile' )
@login_required
def profile ():
loans = Loan.query.filter_by( user_id = current_user.id).all()
visits = LibraryLog.query.filter_by( visitor_id = current_user.document_id).all()
return render_template( 'main/profile.html' ,
loans = loans,
visits = visits)
Activity-Based Recommendations
# Suggest resources based on visit activities
frequent_activities = db.session.query(LibraryLog.activity).filter(
LibraryLog.visitor_id == user.document_id
).group_by(LibraryLog.activity).order_by(
func.count(LibraryLog.activity).desc()
).limit( 3 ).all()
if 'Computadores' in [a[ 0 ] for a in frequent_activities]:
# Recommend computing equipment catalog
recommendations = Catalog.query.filter_by( category = 'computo' ).all()
Customization Examples
Adding Custom Activities
Extend the activity dropdown with institution-specific options:
activity = SelectField(
'Actividad' ,
choices = [
( 'Estudio' , 'Estudio' ),
( 'Lectura' , 'Lectura' ),
( 'Computadores' , 'Uso de Computadores' ),
( 'Investigación' , 'Investigación' ),
( 'Consulta' , 'Consulta de Material' ),
( 'Tutoría' , 'Sesión de Tutoría' ), # Custom
( 'Grupo' , 'Trabajo en Grupo' ), # Custom
( 'Impresión' , 'Impresión/Escaneo' ), # Custom
( 'Otro' , 'Otro' )
],
validators = [DataRequired()]
)
Exit Time Tracking
Modify the model to track both entry and exit:
class LibraryLog ( db . Model ):
# ... existing fields ...
exit_time = db.Column(db.DateTime, nullable = True )
@ property
def duration_minutes ( self ):
if self .exit_time and self .entry_time:
delta = self .exit_time - self .entry_time
return delta.total_seconds() / 60
return None
Troubleshooting
Duplicate Visits Being Logged
Check if form is being submitted multiple times. Implement redirect-after-POST pattern (already in place) and consider adding a cooldown period.
Visitor Name Not Pre-Filling
Ensure the pre_doc variable is passed to template on validation failure: render_template('main/visit.html', form=form, pre_doc=document_id)
Timezone Issues in Reports
All entry_time values are stored in UTC. Convert to local timezone when displaying: entry_time_local = log.entry_time - timedelta(hours=5)
Performance Issues with Large Datasets
Add database indexes on frequently queried columns: db.Index('idx_entry_time', LibraryLog.entry_time) and db.Index('idx_visitor_id', LibraryLog.visitor_id)