diff --git a/config/__pycache__/__init__.cpython-311.pyc b/config/__pycache__/__init__.cpython-311.pyc index 423a636..9072756 100644 Binary files a/config/__pycache__/__init__.cpython-311.pyc and b/config/__pycache__/__init__.cpython-311.pyc differ diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 96bce55..1fcc2e1 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 0b85e94..bdcfacd 100644 Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ diff --git a/config/__pycache__/wsgi.cpython-311.pyc b/config/__pycache__/wsgi.cpython-311.pyc index 9c49e09..e26c2ee 100644 Binary files a/config/__pycache__/wsgi.cpython-311.pyc and b/config/__pycache__/wsgi.cpython-311.pyc differ diff --git a/core/__pycache__/__init__.cpython-311.pyc b/core/__pycache__/__init__.cpython-311.pyc index 74b1112..274adaa 100644 Binary files a/core/__pycache__/__init__.cpython-311.pyc and b/core/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index a5ed392..1ca0848 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/apps.cpython-311.pyc b/core/__pycache__/apps.cpython-311.pyc index 6f131d4..272d326 100644 Binary files a/core/__pycache__/apps.cpython-311.pyc and b/core/__pycache__/apps.cpython-311.pyc differ diff --git a/core/__pycache__/context_processors.cpython-311.pyc b/core/__pycache__/context_processors.cpython-311.pyc index 75bf223..5845507 100644 Binary files a/core/__pycache__/context_processors.cpython-311.pyc and b/core/__pycache__/context_processors.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index e061640..5385e1e 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 5a69659..79b3e70 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 2a36fd6..b84be70 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 8c38f3f..65c56bd 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,60 @@ from django.contrib import admin +from .models import ( + Department, Shift, Employee, BiometricDevice, + AttendanceLog, DailyAttendance, LeaveType, + LeaveRequest, LeaveBalance +) -# Register your models here. +@admin.register(Department) +class DepartmentAdmin(admin.ModelAdmin): + list_display = ('name', 'code') + +@admin.register(Shift) +class ShiftAdmin(admin.ModelAdmin): + list_display = ('name', 'start_time', 'end_time') + +@admin.register(Employee) +class EmployeeAdmin(admin.ModelAdmin): + list_display = ('employee_id', 'biometric_id', 'first_name', 'last_name', 'department', 'position', 'is_active') + list_filter = ('department', 'shift', 'is_active') + search_fields = ('employee_id', 'biometric_id', 'first_name', 'last_name', 'position') + +@admin.register(BiometricDevice) +class BiometricDeviceAdmin(admin.ModelAdmin): + list_display = ('name', 'ip_address', 'port', 'is_active', 'last_sync') + list_filter = ('is_active',) + +@admin.register(AttendanceLog) +class AttendanceLogAdmin(admin.ModelAdmin): + list_display = ('employee', 'timestamp', 'device') + list_filter = ('device', 'timestamp') + search_fields = ('employee__first_name', 'employee__last_name', 'employee__employee_id', 'employee__biometric_id') + +@admin.register(DailyAttendance) +class DailyAttendanceAdmin(admin.ModelAdmin): + list_display = ('employee', 'date', 'check_in', 'check_out', 'status', 'is_late', 'worked_hours') + list_filter = ('date', 'status', 'is_late') + search_fields = ('employee__first_name', 'employee__last_name', 'employee__employee_id') + +@admin.register(LeaveType) +class LeaveTypeAdmin(admin.ModelAdmin): + list_display = ('name', 'total_days', 'color_code') + +@admin.register(LeaveRequest) +class LeaveRequestAdmin(admin.ModelAdmin): + list_display = ('employee', 'leave_type', 'start_date', 'end_date', 'status', 'applied_on') + list_filter = ('status', 'leave_type', 'start_date') + actions = ['approve_requests', 'reject_requests'] + + def approve_requests(self, request, queryset): + queryset.update(status='approved', approved_by=request.user) + approve_requests.short_description = "Approve selected leave requests" + + def reject_requests(self, request, queryset): + queryset.update(status='rejected') + reject_requests.short_description = "Reject selected leave requests" + +@admin.register(LeaveBalance) +class LeaveBalanceAdmin(admin.ModelAdmin): + list_display = ('employee', 'leave_type', 'allocated', 'used', 'remaining') + list_filter = ('leave_type',) diff --git a/core/management/__init__.py b/core/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/management/__pycache__/__init__.cpython-311.pyc b/core/management/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..a8238d3 Binary files /dev/null and b/core/management/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/management/commands/__init__.py b/core/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/management/commands/__pycache__/__init__.cpython-311.pyc b/core/management/commands/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..35af27f Binary files /dev/null and b/core/management/commands/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/management/commands/__pycache__/setup_demo.cpython-311.pyc b/core/management/commands/__pycache__/setup_demo.cpython-311.pyc new file mode 100644 index 0000000..cb7685a Binary files /dev/null and b/core/management/commands/__pycache__/setup_demo.cpython-311.pyc differ diff --git a/core/management/commands/fetch_attendance.py b/core/management/commands/fetch_attendance.py new file mode 100644 index 0000000..7c46a35 --- /dev/null +++ b/core/management/commands/fetch_attendance.py @@ -0,0 +1,62 @@ +from django.core.management.base import BaseCommand +from core.models import Employee, BiometricDevice, AttendanceLog +from core.utils import sync_all_daily_summaries +from zk import ZK, const +from django.utils import timezone +import datetime + +class Command(BaseCommand): + help = 'Fetches attendance logs from ZKTeco biometric devices' + + def handle(self, *args, **options): + devices = BiometricDevice.objects.filter(is_active=True) + if not devices.exists(): + self.stdout.write(self.style.WARNING('No active biometric devices found.')) + return + + for device in devices: + self.stdout.write(f'Connecting to device: {device.name} ({device.ip_address})...') + zk = ZK(device.ip_address, port=device.port, timeout=5, password=0, force_udp=False, ommit_ping=False) + conn = None + try: + conn = zk.connect() + # Disable device while fetching data + conn.disable_device() + + logs = conn.get_attendance() + count = 0 + for log in logs: + try: + employee = Employee.objects.get(biometric_id=log.user_id) + # Avoid duplicates using unique_together constraint logic + obj, created = AttendanceLog.objects.get_or_create( + employee=employee, + timestamp=log.timestamp, + defaults={ + 'device': device, + 'uid': log.uid + } + ) + if created: + count += 1 + except Employee.DoesNotExist: + # self.stdout.write(self.style.WARNING(f'Employee with biometric ID {log.user_id} not found.')) + continue + + device.last_sync = timezone.now() + device.save() + + self.stdout.write(self.style.SUCCESS(f'Successfully synced {count} new logs from {device.name}.')) + + # Re-enable device + conn.enable_device() + except Exception as e: + self.stdout.write(self.style.ERROR(f'Failed to connect to {device.name}: {str(e)}')) + finally: + if conn: + conn.disconnect() + + # After all devices synced, update daily summaries + self.stdout.write('Updating daily attendance summaries...') + sync_all_daily_summaries() + self.stdout.write(self.style.SUCCESS('All summaries updated.')) diff --git a/core/management/commands/setup_demo.py b/core/management/commands/setup_demo.py new file mode 100644 index 0000000..2c840b5 --- /dev/null +++ b/core/management/commands/setup_demo.py @@ -0,0 +1,81 @@ +from django.core.management.base import BaseCommand +from core.models import Employee, Department, Shift, DailyAttendance, LeaveType, LeaveRequest +from django.utils import timezone +import datetime + +class Command(BaseCommand): + help = 'Populates the database with demo data' + + def handle(self, *args, **options): + # 1. Create Department + dept, _ = Department.objects.get_or_create(name='IT Department', code='IT', defaults={'description': 'Information Technology'}) + + # 2. Create Shift + shift, _ = Shift.objects.get_or_create( + name='General Shift', + defaults={'start_time': datetime.time(9, 0), 'end_time': datetime.time(17, 0)} + ) + + # 3. Create Employees + emp1, _ = Employee.objects.get_or_create( + employee_id='EMP001', + defaults={ + 'first_name': 'John', + 'last_name': 'Doe', + 'department': dept, + 'shift': shift, + 'position': 'Software Engineer', + 'joined_date': datetime.date(2023, 1, 1), + 'is_active': True + } + ) + + emp2, _ = Employee.objects.get_or_create( + employee_id='EMP002', + defaults={ + 'first_name': 'Jane', + 'last_name': 'Smith', + 'department': dept, + 'shift': shift, + 'position': 'Product Manager', + 'joined_date': datetime.date(2023, 2, 1), + 'is_active': True + } + ) + + # 4. Create Leave Types + lt, _ = LeaveType.objects.get_or_create(name='Sick Leave', defaults={'total_days': 12, 'color_code': '#e74c3c'}) + + # 5. Create Daily Attendance for today + today = timezone.now().date() + DailyAttendance.objects.get_or_create( + employee=emp1, + date=today, + defaults={ + 'check_in': datetime.time(9, 5), + 'status': 'present', + 'worked_hours': 8.0 + } + ) + + DailyAttendance.objects.get_or_create( + employee=emp2, + date=today, + defaults={ + 'check_in': datetime.time(9, 45), + 'status': 'late', + 'is_late': True, + 'worked_hours': 7.25 + } + ) + + # 6. Create Pending Leave + LeaveRequest.objects.get_or_create( + employee=emp1, + leave_type=lt, + start_date=today + datetime.timedelta(days=5), + end_date=today + datetime.timedelta(days=7), + defaults={'reason': 'Medical checkup', 'status': 'pending'} + ) + + self.stdout.write(self.style.SUCCESS('Successfully populated demo data.')) diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..4bd8198 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,50 @@ +# Generated by Django 5.2.7 on 2026-02-17 05:58 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Department', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('code', models.CharField(max_length=10, unique=True)), + ('description', models.TextField(blank=True)), + ], + ), + migrations.CreateModel( + name='Shift', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50)), + ('start_time', models.TimeField()), + ('end_time', models.TimeField()), + ], + ), + migrations.CreateModel( + name='Employee', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('employee_id', models.CharField(max_length=20, unique=True)), + ('first_name', models.CharField(max_length=50)), + ('last_name', models.CharField(max_length=50)), + ('position', models.CharField(max_length=100)), + ('joined_date', models.DateField()), + ('is_active', models.BooleanField(default=True)), + ('department', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.department')), + ('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('shift', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.shift')), + ], + ), + ] diff --git a/core/migrations/0002_biometricdevice_employee_biometric_id_attendancelog.py b/core/migrations/0002_biometricdevice_employee_biometric_id_attendancelog.py new file mode 100644 index 0000000..9d80242 --- /dev/null +++ b/core/migrations/0002_biometricdevice_employee_biometric_id_attendancelog.py @@ -0,0 +1,44 @@ +# Generated by Django 5.2.7 on 2026-02-17 06:12 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='BiometricDevice', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('ip_address', models.GenericIPAddressField()), + ('port', models.IntegerField(default=4370)), + ('is_active', models.BooleanField(default=True)), + ('last_sync', models.DateTimeField(blank=True, null=True)), + ], + ), + migrations.AddField( + model_name='employee', + name='biometric_id', + field=models.CharField(blank=True, help_text='ID on the ZKTeco device', max_length=20, null=True, unique=True), + ), + migrations.CreateModel( + name='AttendanceLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('timestamp', models.DateTimeField()), + ('uid', models.IntegerField(help_text='Internal ID from device log')), + ('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.employee')), + ('device', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.biometricdevice')), + ], + options={ + 'ordering': ['-timestamp'], + 'unique_together': {('employee', 'timestamp')}, + }, + ), + ] diff --git a/core/migrations/0003_leavetype_leaverequest_dailyattendance_leavebalance.py b/core/migrations/0003_leavetype_leaverequest_dailyattendance_leavebalance.py new file mode 100644 index 0000000..9b04f23 --- /dev/null +++ b/core/migrations/0003_leavetype_leaverequest_dailyattendance_leavebalance.py @@ -0,0 +1,68 @@ +# Generated by Django 5.2.7 on 2026-02-17 06:18 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_biometricdevice_employee_biometric_id_attendancelog'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='LeaveType', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50)), + ('color_code', models.CharField(default='#3498db', max_length=7)), + ('total_days', models.IntegerField(default=0)), + ], + ), + migrations.CreateModel( + name='LeaveRequest', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('start_date', models.DateField()), + ('end_date', models.DateField()), + ('reason', models.TextField()), + ('status', models.CharField(choices=[('pending', 'Pending'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='pending', max_length=20)), + ('applied_on', models.DateTimeField(auto_now_add=True)), + ('approved_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='approver', to=settings.AUTH_USER_MODEL)), + ('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.employee')), + ('leave_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.leavetype')), + ], + ), + migrations.CreateModel( + name='DailyAttendance', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateField()), + ('check_in', models.TimeField(blank=True, null=True)), + ('check_out', models.TimeField(blank=True, null=True)), + ('status', models.CharField(choices=[('present', 'Present'), ('absent', 'Absent'), ('late', 'Late'), ('half_day', 'Half Day'), ('on_leave', 'On Leave')], default='absent', max_length=20)), + ('is_late', models.BooleanField(default=False)), + ('worked_hours', models.DecimalField(decimal_places=2, default=0.0, max_digits=5)), + ('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.employee')), + ], + options={ + 'unique_together': {('employee', 'date')}, + }, + ), + migrations.CreateModel( + name='LeaveBalance', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('allocated', models.IntegerField()), + ('used', models.IntegerField(default=0)), + ('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.employee')), + ('leave_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.leavetype')), + ], + options={ + 'unique_together': {('employee', 'leave_type')}, + }, + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..d5de818 Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0002_biometricdevice_employee_biometric_id_attendancelog.cpython-311.pyc b/core/migrations/__pycache__/0002_biometricdevice_employee_biometric_id_attendancelog.cpython-311.pyc new file mode 100644 index 0000000..80cd338 Binary files /dev/null and b/core/migrations/__pycache__/0002_biometricdevice_employee_biometric_id_attendancelog.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0003_leavetype_leaverequest_dailyattendance_leavebalance.cpython-311.pyc b/core/migrations/__pycache__/0003_leavetype_leaverequest_dailyattendance_leavebalance.cpython-311.pyc new file mode 100644 index 0000000..1e9605e Binary files /dev/null and b/core/migrations/__pycache__/0003_leavetype_leaverequest_dailyattendance_leavebalance.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/__init__.cpython-311.pyc b/core/migrations/__pycache__/__init__.cpython-311.pyc index 9c833c8..af534ed 100644 Binary files a/core/migrations/__pycache__/__init__.cpython-311.pyc and b/core/migrations/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..c9fc53c 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,114 @@ from django.db import models +from django.contrib.auth.models import User -# Create your models here. +class Department(models.Model): + name = models.CharField(max_length=100) + code = models.CharField(max_length=10, unique=True) + description = models.TextField(blank=True) + + def __str__(self): + return self.name + +class Shift(models.Model): + name = models.CharField(max_length=50) # e.g., Morning, Night, General + start_time = models.TimeField() + end_time = models.TimeField() + + def __str__(self): + return f"{self.name} ({self.start_time} - {self.end_time})" + +class Employee(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE, null=True, blank=True) + employee_id = models.CharField(max_length=20, unique=True) + biometric_id = models.CharField(max_length=20, unique=True, null=True, blank=True, help_text="ID on the ZKTeco device") + first_name = models.CharField(max_length=50) + last_name = models.CharField(max_length=50) + department = models.ForeignKey(Department, on_delete=models.SET_NULL, null=True) + shift = models.ForeignKey(Shift, on_delete=models.SET_NULL, null=True) + position = models.CharField(max_length=100) + joined_date = models.DateField() + is_active = models.BooleanField(default=True) + + def __str__(self): + return f"{self.first_name} {self.last_name} ({self.employee_id})" + +class BiometricDevice(models.Model): + name = models.CharField(max_length=100) + ip_address = models.GenericIPAddressField() + port = models.IntegerField(default=4370) + is_active = models.BooleanField(default=True) + last_sync = models.DateTimeField(null=True, blank=True) + + def __str__(self): + return f"{self.name} ({self.ip_address})" + +class AttendanceLog(models.Model): + employee = models.ForeignKey(Employee, on_delete=models.CASCADE) + timestamp = models.DateTimeField() + device = models.ForeignKey(BiometricDevice, on_delete=models.SET_NULL, null=True) + uid = models.IntegerField(help_text="Internal ID from device log") + + class Meta: + unique_together = ('employee', 'timestamp') + ordering = ['-timestamp'] + + def __str__(self): + return f"{self.employee} - {self.timestamp}" + +class DailyAttendance(models.Model): + STATUS_CHOICES = ( + ('present', 'Present'), + ('absent', 'Absent'), + ('late', 'Late'), + ('half_day', 'Half Day'), + ('on_leave', 'On Leave'), + ) + employee = models.ForeignKey(Employee, on_delete=models.CASCADE) + date = models.DateField() + check_in = models.TimeField(null=True, blank=True) + check_out = models.TimeField(null=True, blank=True) + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='absent') + is_late = models.BooleanField(default=False) + worked_hours = models.DecimalField(max_digits=5, decimal_places=2, default=0.00) + + class Meta: + unique_together = ('employee', 'date') + +class LeaveType(models.Model): + name = models.CharField(max_length=50) + color_code = models.CharField(max_length=7, default='#3498db') # Hex code for UI + total_days = models.IntegerField(default=0) + + def __str__(self): + return self.name + +class LeaveRequest(models.Model): + STATUS_CHOICES = ( + ('pending', 'Pending'), + ('approved', 'Approved'), + ('rejected', 'Rejected'), + ) + employee = models.ForeignKey(Employee, on_delete=models.CASCADE) + leave_type = models.ForeignKey(LeaveType, on_delete=models.CASCADE) + start_date = models.DateField() + end_date = models.DateField() + reason = models.TextField() + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending') + applied_on = models.DateTimeField(auto_now_add=True) + approved_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='approver') + + def __str__(self): + return f"{self.employee} - {self.leave_type} ({self.start_date})" + +class LeaveBalance(models.Model): + employee = models.ForeignKey(Employee, on_delete=models.CASCADE) + leave_type = models.ForeignKey(LeaveType, on_delete=models.CASCADE) + allocated = models.IntegerField() + used = models.IntegerField(default=0) + + @property + def remaining(self): + return self.allocated - self.used + + class Meta: + unique_together = ('employee', 'leave_type') diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..cfdb406 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -3,23 +3,70 @@
-