Envio do estado atual do projeto

Projeto ainda em desenvolvimento.
Enviado para o repositório para manter estado atual.
This commit is contained in:
Eberton committed 2026-03-29 15:28:16 -03:00
1 parent e539e83c79
commit 24650484f1
94 files changed
+3575

No files matched your search

View File
Whitespace-only changes.
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
+5
View File
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class CoreConfig(AppConfig):
name = 'core'
@@ -0,0 +1,22 @@
from entities.models import Farm
# Injects the selected farm and the list of farms into the template context for authenticated users.
def selected_farm_processor(request):
selected_farm = None
farms = None
if request.user.is_authenticated:
farms = request.user.farms.only("id", "name")
farm_id = request.session.get('selected_farm_id')
if farm_id:
try:
selected_farm = farms.get(id=farm_id)
except Farm.DoesNotExist:
request.session.pop('selected_farm_id', None)
return {
'SELECTED_FARM': selected_farm,
'farms': farms
}
+18
View File
@@ -0,0 +1,18 @@
from django.shortcuts import redirect
from django.urls import reverse
from functools import wraps
FARM_SELECT_URL = 'entities:user:farm_select'
def farm_required(view_func):
"""
Decorator that ensures the user has a farm selected in the session.
If there isn't one, it redirects to the farm selection screen.
"""
@wraps(view_func)
def wrapper(request, *args, **kwargs):
if 'selected_farm_id' not in request.session:
return redirect(reverse(FARM_SELECT_URL))
return view_func(request, *args, **kwargs)
return wrapper
+24
View File
@@ -0,0 +1,24 @@
from django.shortcuts import redirect
from django.conf import settings
# Forces the user to be logged in the system
# A URL whitelist can be found in the settings.py file
class LoginRequiredMiddleware:
def __init__(self, get_response):
self.get_response = get_response
self.exempt_paths = getattr(settings, 'LOGIN_EXEMPT_PATHS', [])
self.exempt_prefixes = getattr(settings, 'LOGIN_EXEMPT_PREFIX', [])
def __call__(self, request):
path = request.path_info
if any(path.startswith(prefix) for prefix in self.exempt_prefixes):
return self.get_response(request)
if path in self.exempt_paths:
return self.get_response(request)
if not request.user.is_authenticated:
return redirect(settings.LOGIN_URL)
return self.get_response(request)
+3
View File
@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.
Whitespace-only changes.
+29
View File
@@ -0,0 +1,29 @@
{% load static %}
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<title>{{ title }}</title>
<link rel="shortcut icon" href="{% static 'img/favicon/favicon.ico' %}" type="image/x-icon">
<link rel="stylesheet" href="{% static 'css/base/reset.css' %}">
<link rel="stylesheet" href="{% static 'css/base/variables.css' %}">
<link rel="stylesheet" href="{% static 'css/layout/global.css' %}">
<link rel="stylesheet" href="{% static 'css/layout/buttons.css' %}">
<!-- Importing Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css" integrity="sha512-2SwdPD6INVrV/lHTZbO2nodKhrnDdJK9/kg2XD1r9uGqPo1cUbujc+IYdlYdEErWNu69gVcYgdxlmVmzTWnetw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
{% block extra_css %}{% endblock %}
</head>
<body>
{% include 'includes/_sidebar.html' %}
{% include 'includes/_navbar.html' %}
<div class="main-content">
{% block content %}{% endblock %}
</div>
</body>
<script src="{% static 'js/main.js' %}"></script>
</html>
@@ -0,0 +1,48 @@
{% extends 'base.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/layout/entity_home.css' %}">
{% endblock %}
{% block content %}
<div class="entity_title">
<h1>{{entity_name}}</h1>
<form method="GET" action="{{entity_home_url}}" class="form_entity_search">
{% if filter_query %}
<a href="{{entity_home_url}}" class="btn-clear-filter">{{filter_query}}<i class="fa-solid fa-xmark"></i></a>
{% endif %}
<label for="entity_search">Buscar</label>
<input type="text" name="entity_search" required>
<button type="submit"><i class="fa-solid fa-magnifying-glass"></i></button>
</form>
<a href="{{entity_new_url}}">Novo <i class="fa-solid fa-plus"></i></a>
</div>
{% if not entities %}
<p>Nenhum registro encontrado.</p>
{% else %}
<table>
<tr>
<th>Identificador</th>
<th>Idade</th>
<th>Peso</th>
<th>Editar</th>
<th>Excluir</th>
</tr>
{% for entity in entities %}
<tr>
<td>{{ animal.identifier }}</td>
<td>{{ animal.age }}</td>
<td>{{ animal.weight }} Kg</td>
<td><a href="{% url 'entities:animal:animal_update' animal.id %}"><i class="fa-solid fa-pen-to-square"></i></a></td>
<td>
<form method="POST" action="{% url 'entities:animal:animal_delete' animal.id %}">
{% csrf_token %}
<button type="submit" class="btn-icon-trash"><i class="fa-solid fa-trash"></i></button>
</form>
</td>
</tr>
{% endfor %}
</table>
{% endif %}
{% endblock %}
@@ -0,0 +1,59 @@
{% load static %}
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<title>Seleção de Fazenda</title>
<link rel="shortcut icon" href="{% static 'img/favicon/favicon.ico' %}" type="image/x-icon">
<link rel="stylesheet" href="{% static 'css/base/reset.css' %}">
<link rel="stylesheet" href="{% static 'css/base/variables.css' %}">
<link rel="stylesheet" href="{% static 'css/layout/global.css' %}">
<link rel="stylesheet" href="{% static 'css/layout/buttons.css' %}">
<link rel="stylesheet" href="{% static 'user/css/farm_select.css' %}">
<!-- Importing Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css" integrity="sha512-2SwdPD6INVrV/lHTZbO2nodKhrnDdJK9/kg2XD1r9uGqPo1cUbujc+IYdlYdEErWNu69gVcYgdxlmVmzTWnetw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
<body>
<nav class="farm-select-navbar">
<div class="navbar-brand">
MultiFarm
</div>
<div class="navbar-user-info">
<span class="username">Olá, {{ request.user.short_name }}</span>
<a href="{% url 'entities:user:user_logout' %}"><i class="fa-solid fa-right-from-bracket"></i></a>
</div>
</nav>
<div class="farm-select-container">
<h1>Selecione sua fazenda</h1>
{% if farms %}
<ul class="farm-list">
{% for farm in farms %}
<li>
<a href="{% url 'entities:user:farm_set' farm_id=farm.id %}" class="farm-button">
<div class="farm-item">
<h3 class="farm-name">{{ farm.name }}</h3>
<p class="farm-description">
<span class="total-area">Área total: {{ farm.total_area }}m²</span>
<span class="count-piquetes">Piquetes: {{ farm.total_paddocks }}</span>
<span class="count-animais">Animais: {{ farm.total_animals }}</span>
</p>
</div>
</a>
</li>
{% endfor %}
</ul>
{% else %}
<div class="no-farms">
<p>Nenhuma fazenda encontrada</p>
<p><a href="#"><i class="fa-solid fa-plus"></i> Criar nova fazenda</a></p>
</div>
{% endif %}
</div>
</body>
</html>
@@ -0,0 +1,6 @@
<form method="POST" action="{{ action_url }}">
{% csrf_token %}
{{ form }}
<button class="btn-primary" type="submit">Salvar</button>
<a href="{{ return_url }}" class="btn-simple">Cancelar</a>
</form>
@@ -0,0 +1,28 @@
<nav class="navbar">
<div class="w33">
<i class="fa-solid fa-bars" id="sidebar-toggle"></i>
</div>
{% comment %} <div class="navbar-farm-selector w33">
<span class="farm-name"><i class="fa-regular fa-circle-check"></i> {{ SELECTED_FARM.name }}</span>
<a href="{% url 'entities:user:farm_deselect' %}" class="btn-simple">Alterar</a>
</div> {% endcomment %}
<form method="post" action="{% url 'entities:user:farm_set_post' %}" class="navbar-farm-selector w33">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.get_full_path }}">
<select name="farm_id" onchange="this.form.submit()">
{% for farm in farms %}
<option value="{{ farm.id }}" {% if farm.id == SELECTED_FARM.id %}selected{% endif %}>
{{ farm.name }}
</option>
{% endfor %}
</select>
</form>
<div class="navbar-user-info w33">
<span class="username">Olá, {{ request.user.short_name }}</span>
<a href="{% url 'entities:user:farm_deselect' %}"><i class="fa-solid fa-user-pen"></i></a>
<a href="{% url 'entities:user:user_logout' %}"><i class="fa-solid fa-right-from-bracket"></i></a>
</div>
</nav>
@@ -0,0 +1,48 @@
<aside class="sidebar">
<div class="sidebar-brand">
<a href="{% url 'entities:farm:farm_home' %}">MultiFarm</a>
</div>
<ul class="sidebar-menu">
<li class="sidebar-header">ACOMPANHAMENTO</li>
<li class="{% if request.resolver_match.url_name != 'report' %}highlight-hover{% endif %}">
<a href="{% url 'entities:farm:farm_home' %}" class="sidebar-link {% if request.resolver_match.url_name == 'report' %}active{% endif %}"><i class="fa-solid fa-chart-pie"></i> Relatórios</a>
</li>
<li class="sidebar-header">REGISTROS</li>
<li class="{% if request.resolver_match.url_name not in 'rotation_home, rotation_new, rotation_delete' %}highlight-hover{% endif %}">
<a href="{% url 'entities:rotation:rotation_home' %}" class="sidebar-link {% if request.resolver_match.url_name in 'rotation_home, rotation_new, rotation_delete' %}active{% endif %}"><i class="fa-solid fa-arrows-spin"></i> Movimentações</a>
</li>
<li class="sidebar-header">CADASTROS</li>
<li class="{% if request.resolver_match.url_name not in 'animal_home, animal_new, animal_update, animal_delete' %}highlight-hover{% endif %}">
<a href="{% url 'entities:animal:animal_home' %}" class="sidebar-link {% if request.resolver_match.url_name in 'animal_home, animal_new, animal_update, animal_delete' %}active{% endif %}"><i class="fa-solid fa-cow"></i> Animais</a>
</li>
<li class="{% if request.resolver_match.url_name not in 'batch_home, batch_new, batch_update, batch_delete' %}highlight-hover{% endif %}">
<a href="{% url 'entities:animal:batch_home' %}" class="sidebar-link {% if request.resolver_match.url_name in 'batch_home, batch_new, batch_update, batch_delete' %}active{% endif %}"><i class="fa-solid fa-layer-group"></i> Lotes</a>
</li>
<li class="{% if request.resolver_match.url_name not in 'paddock_home, paddock_new, paddock_update, paddock_delete' %}highlight-hover{% endif %}">
<a href="{% url 'entities:farm:paddock_home' %}" class="sidebar-link {% if request.resolver_match.url_name in 'paddock_home, paddock_new, paddock_update, paddock_delete' %}active{% endif %}"><i class="fa-solid fa-leaf"></i> Piquetes</a>
</li>
<li class="{% if request.resolver_match.url_name not in 'paddock_group_home, paddock_group_new, paddock_group_update, paddock_group_delete' %}highlight-hover{% endif %}">
<a href="{% url 'entities:farm:paddock_group_home' %}" class="sidebar-link {% if request.resolver_match.url_name in 'paddock_group_home, paddock_group_new, paddock_group_update, paddock_group_delete' %}active{% endif %}"><i class="fa-solid fa-layer-group"></i> Blocos</a>
</li>
<li class="sidebar-header">CONFIGURAÇÔES</li>
<li class="{% if request.resolver_match.url_name not in 'farm_home, farm_new, farm_update, farm_delete' %}highlight-hover{% endif %}">
<a href="{% url 'entities:farm:farm_home' %}" class="sidebar-link {% if request.resolver_match.url_name in 'farm_home, farm_new, farm_update, farm_delete' %}active{% endif %}"><i class="fa-solid fa-gear"></i> Fazenda</a>
</li>
{% if user.is_staff or user.is_superuser %}
<li class="{% if request.resolver_match.url_name not in 'admin' %}highlight-hover{% endif %}">
<a href="{% url 'admin:index' %}" class="sidebar-link"><i class="fa-solid fa-user-tie"></i> Painel Administrativo</a>
</li>
{% endif %}
</ul>
</aside>
@@ -0,0 +1,39 @@
{% load static %}
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<title>Login</title>
<link rel="shortcut icon" href="{% static 'img/favicon/favicon.ico' %}" type="image/x-icon">
<link rel="stylesheet" href="{% static 'css/base/reset.css' %}">
<link rel="stylesheet" href="{% static 'css/base/variables.css' %}">
<link rel="stylesheet" href="{% static 'css/layout/global.css' %}">
<link rel="stylesheet" href="{% static 'css/layout/buttons.css' %}">
<link rel="stylesheet" href="{% static 'user/css/login.css' %}">
<!-- Importing Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css" integrity="sha512-2SwdPD6INVrV/lHTZbO2nodKhrnDdJK9/kg2XD1r9uGqPo1cUbujc+IYdlYdEErWNu69gVcYgdxlmVmzTWnetw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
<body>
<div class="login-container">
<div class="ilustration-container">
<h1>Multifarm pastoreio</h1>
</div>
<div class="login-form-container">
<h1>Login</h1>
<form method="POST" action="{% url 'entities:user:user_login' %}">
{% csrf_token %}
{{ form }}
{% if fail_login %}
<p class="login-fail"><i class="fa-solid fa-circle-exclamation"></i> Login ou senha incorretos</p>
{% endif %}
<button type="submit">Entrar <i class="fa-solid fa-right-to-bracket"></i></button>
<p>Não possui cadastro? <a href="{% url 'entities:user:user_signin' %}">Solicite acesso</a>.</p>
</form>
</div>
</div>
</body>
</html>
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+3
View File
@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.
Whitespace-only changes.
+18
View File
@@ -0,0 +1,18 @@
from django.contrib import admin
from .models.user import User
from .models.farm import Farm, PaddockGroup, Paddock
from .models.animal import Animal, Batch, BatchAnimal, FeedingRecord
from .models.rotation import Rotation
admin.site.register([
User,
Farm,
PaddockGroup,
Paddock,
Animal,
Batch,
BatchAnimal,
FeedingRecord,
Rotation,
])
+5
View File
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class EntitiesConfig(AppConfig):
name = 'entities'
@@ -0,0 +1,4 @@
from .animal import *
from .farm import *
from .rotation import *
from .user import *
+45
View File
@@ -0,0 +1,45 @@
from django import forms
from ..models.animal import Batch, Animal
class AnimalCreateForm(forms.ModelForm):
class Meta:
model = Animal
fields = ['identifier', 'birth', 'weight']
labels = {
'identifier': 'Identificador',
'birth': 'Data de Nascimento',
'weight': 'Peso (kg)',
}
widgets = {
'birth': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
'weight': forms.NumberInput(attrs={'step': '0.01'})
}
class AnimalEditForm(forms.ModelForm):
class Meta:
model = Animal
fields = ['identifier', 'weight']
labels = {
'identifier': 'Identificador',
'weight': 'Peso (kg)',
}
widgets = {
'weight': forms.NumberInput(attrs={'step': '0.01'})
}
class BatchForm(forms.ModelForm):
class Meta:
model = Batch
fields = ['identifier', 'animal_category', 'batch_type', 'animals']
labels = {
'identifier': 'Identificador',
'animal_category': 'Categoria animal',
'batch_type': 'Tipo',
'animals': 'Animais',
}
BatchCreateForm = BatchEditForm = BatchForm # Alias to BatchForm since its implementations are the same
+37
View File
@@ -0,0 +1,37 @@
from django import forms
from ..models import Farm, PaddockGroup, Paddock
class FarmForm(forms.ModelForm):
class Meta:
model = Farm
fields = ['name', 'total_area']
labels = {
'name': 'Nome',
'total_area': 'Área total',
}
FarmCreateForm = FarmEditForm = FarmForm # Alias since its implementations are the same
class PaddockGroupForm(forms.ModelForm):
class Meta:
model = PaddockGroup
fields = ['identifier', 'description']
labels = {
'identifier': 'Identificador',
'description': 'Descrição',
}
PaddockGroupCreateForm = PaddockGroupEditForm = PaddockGroupForm
class PaddockForm(forms.ModelForm):
class Meta:
model = Paddock
fields = ['paddock_number', 'area', 'load_capacity', 'paddock_group']
labels = {
'paddock_number': 'Número do piquete',
'area': 'Área',
'load_capacity': 'Lotação máxima',
'paddock_group': 'Bloco',
}
PaddockCreateForm = PaddockEditForm = PaddockForm
@@ -0,0 +1,23 @@
from django import forms
from ..models import Rotation
class RotationEntryForm(forms.ModelForm):
class Meta:
model = Rotation
fields = ['batch', 'entry_height', 'paddock']
labels = {
'batch': 'Lote de animais',
'entry_height': 'Altura do pasto',
'paddock': 'Piquete',
}
class RotationExitForm(forms.ModelForm):
class Meta:
model = Rotation
fields = ['batch', 'exit_height']
labels = {
'batch': 'Lote de animais',
'exit_height': 'Altura do pasto',
}
+35
View File
@@ -0,0 +1,35 @@
from django import forms
from ..models import User
class UserCreateForm(forms.ModelForm):
class Meta:
model = User
fields = ['name', 'login', 'email',]
labels = {
'name': 'Nome',
'login': 'Login',
'email': 'E-mail',
}
widgets = {
'email': forms.EmailInput(),
}
class UserEditForm(forms.ModelForm):
class Meta:
model = User
fields = ['name', 'email',]
labels = {
'name': 'Nome',
'email': 'E-mail',
}
widgets = {
'email': forms.EmailInput(),
}
class UserLoginForm(forms.Form):
login = forms.CharField(max_length=45, label='Usuário')
password = forms.CharField(widget=forms.PasswordInput, label='Senha')
@@ -0,0 +1,145 @@
# Generated by Django 6.0.2 on 2026-02-28 19:45
import django.db.models.deletion
import django.utils.timezone
import entities.models.user
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='Animal',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_active', models.BooleanField(default=True)),
('identifier', models.CharField(max_length=45)),
('birth', models.DateField()),
('weight', models.DecimalField(decimal_places=2, max_digits=10)),
],
),
migrations.CreateModel(
name='Batch',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_active', models.BooleanField(default=True)),
('identifier', models.CharField(max_length=45)),
('animal_category', models.CharField(blank=True, max_length=45, null=True)),
('batch_type', models.CharField(blank=True, max_length=45, null=True)),
],
),
migrations.CreateModel(
name='Farm',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_active', models.BooleanField(default=True)),
('name', models.CharField(max_length=45)),
('total_area', models.IntegerField(blank=True, null=True)),
],
),
migrations.CreateModel(
name='PaddockGroup',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_active', models.BooleanField(default=True)),
('identifier', models.CharField(max_length=45)),
('description', models.TextField(blank=True, null=True)),
],
),
migrations.CreateModel(
name='BatchAnimal',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_active', models.BooleanField(default=True)),
('animal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='entities.animal')),
('batch', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='entities.batch')),
],
),
migrations.AddField(
model_name='batch',
name='animals',
field=models.ManyToManyField(related_name='batches', through='entities.BatchAnimal', to='entities.animal'),
),
migrations.AddField(
model_name='animal',
name='farm',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='animals', to='entities.farm'),
),
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('name', models.CharField(max_length=45)),
('login', models.CharField(max_length=45, unique=True)),
('email', models.EmailField(max_length=254, unique=True)),
('is_active', models.BooleanField(default=True)),
('is_superuser', models.BooleanField(default=False)),
('is_staff', models.BooleanField(default=False)),
('date_joined', models.DateTimeField(default=django.utils.timezone.now)),
('last_login', models.DateTimeField(blank=True, null=True)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
('farms', models.ManyToManyField(related_name='users', to='entities.farm')),
],
options={
'verbose_name': 'User',
'verbose_name_plural': 'Users',
},
managers=[
('objects', entities.models.user.CustomUserManager()),
],
),
migrations.CreateModel(
name='Paddock',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_active', models.BooleanField(default=True)),
('paddock_number', models.IntegerField()),
('area', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
('load_capacity', models.IntegerField(blank=True, null=True)),
('occupied', models.BooleanField(default=False)),
('farm', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='paddocks', to='entities.farm')),
('paddock_group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='paddocks', to='entities.paddockgroup')),
],
),
migrations.CreateModel(
name='Rotation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_active', models.BooleanField(default=True)),
('entry_date', models.DateTimeField()),
('entry_height', models.DecimalField(decimal_places=2, max_digits=10)),
('exit_date', models.DateTimeField(blank=True, null=True)),
('exit_height', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
('animal_count', models.IntegerField()),
('livestock_unit', models.DecimalField(decimal_places=2, max_digits=10)),
('batch', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='rotations', to='entities.batch')),
('paddock', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rotations', to='entities.paddock')),
],
),
migrations.CreateModel(
name='FeedingRecord',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('feed_quantity', models.DecimalField(decimal_places=2, max_digits=10)),
('animal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feeding_record', to='entities.animal')),
('rotation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feeding_record', to='entities.rotation')),
],
),
migrations.AddConstraint(
model_name='batchanimal',
constraint=models.UniqueConstraint(condition=models.Q(('is_active', True)), fields=('animal',), name='unique_active_animal_batch_link'),
),
migrations.AddConstraint(
model_name='animal',
constraint=models.UniqueConstraint(condition=models.Q(('is_active', True)), fields=('identifier', 'farm'), name='unique_active_farm_animal'),
),
]
Whitespace-only changes.
@@ -0,0 +1,4 @@
from .user import *
from .farm import *
from .animal import *
from .rotation import *
+103
View File
@@ -0,0 +1,103 @@
from datetime import date
from django.db import models
from django.db.models import Q
class Animal(models.Model):
is_active = models.BooleanField(default=True)
identifier = models.CharField(max_length=45)
birth = models.DateField()
weight = models.DecimalField(max_digits=10, decimal_places=2)
farm = models.ForeignKey('entities.Farm', on_delete=models.CASCADE, related_name='animals')
def __str__(self):
return f"Animal {self.identifier} - birth {self.birth} - weight {self.weight} - farm {self.farm} - is_active {self.is_active}"
@property
def age(self):
today = date.today()
birth_date = self.birth
years = today.year - birth_date.year
months = today.month - birth_date.month
if today.day < birth_date.day:
months -= 1
if months < 0:
months += 12
years -= 1
if years > 0 and months > 0:
return f"{years} anos e {months} meses" # YYYY years and MM months
elif years > 0:
return f"{years} anos" # YYYY years
elif months > 0:
return f"{months} meses" # MM months
else:
return "Menos de um mês" # Less than a month
class Meta:
constraints = [
models.UniqueConstraint(
fields=['identifier', 'farm'],
condition=Q(is_active=True),
name='unique_active_farm_animal'
)
]
class Batch(models.Model):
is_active = models.BooleanField(default=True)
identifier = models.CharField(max_length=45)
animal_category = models.CharField(max_length=45, null=True, blank=True)
batch_type = models.CharField(max_length=45, null=True, blank=True)
animals = models.ManyToManyField(Animal, through='BatchAnimal', related_name='batches')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__original_is_active = self.is_active
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
is_active_changed = self.__original_is_active != self.is_active
if is_active_changed:
if self.is_active is False:
self.batchanimal_set.all().update(is_active=False)
elif self.is_active is True:
self.batchanimal_set.all().update(is_active=True)
self.__original_is_active = self.is_active
def __str__(self):
return self.identifier
class BatchAnimal(models.Model):
animal = models.ForeignKey(Animal, on_delete=models.CASCADE)
batch = models.ForeignKey(Batch, on_delete=models.CASCADE)
is_active = models.BooleanField(default=True)
# When reactivating a batch, all its animals must not belong to any other active batch.
# Otherwise, the database will raise an IntegrityError due to the unique constraint on active animals.
# This is a rare use case, but any future code regarding it must implement error treatment
class Meta:
constraints = [
models.UniqueConstraint(
fields=['animal'],
condition=Q(is_active=True),
name='unique_active_animal_batch_link'
)
]
def __str__(self):
return f"{self.animal.identifier} in {self.batch.identifier}"
class FeedingRecord(models.Model):
animal = models.ForeignKey(Animal, on_delete=models.CASCADE, related_name='feeding_record')
rotation = models.ForeignKey('entities.Rotation', on_delete=models.CASCADE, related_name='feeding_record')
feed_quantity = models.DecimalField(max_digits=10, decimal_places=2)
def __str__(self):
return f"Consumo de {self.animal.identifier} - {self.feed_quantity} kg"
+30
View File
@@ -0,0 +1,30 @@
from django.db import models
class Farm(models.Model):
is_active = models.BooleanField(default=True)
name = models.CharField(max_length=45)
total_area = models.IntegerField(null=True, blank=True)
def __str__(self):
return self.name
class PaddockGroup(models.Model):
is_active = models.BooleanField(default=True)
identifier = models.CharField(max_length=45)
description = models.TextField(null=True, blank=True)
def __str__(self):
return f"Bloco {self.id}"
class Paddock(models.Model):
is_active = models.BooleanField(default=True)
paddock_number = models.IntegerField()
area = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
load_capacity = models.IntegerField(null=True, blank=True)
occupied = models.BooleanField(default=False)
farm = models.ForeignKey(Farm, on_delete=models.CASCADE, related_name='paddocks')
paddock_group = models.ForeignKey(PaddockGroup, on_delete=models.SET_NULL, related_name='paddocks', null=True, blank=True)
def __str__(self):
return f"Piquete {self.paddock_number} - Bloco {self.paddock_group.id}"
@@ -0,0 +1,15 @@
from django.db import models
class Rotation(models.Model):
is_active = models.BooleanField(default=True)
entry_date = models.DateTimeField()
entry_height = models.DecimalField(max_digits=10, decimal_places=2)
exit_date = models.DateTimeField(null=True, blank=True)
exit_height = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
animal_count = models.IntegerField()
livestock_unit = models.DecimalField(max_digits=10, decimal_places=2)
batch = models.ForeignKey('entities.Batch', on_delete=models.SET_NULL, related_name='rotations', null=True, blank=True)
paddock = models.ForeignKey('entities.Paddock', on_delete=models.CASCADE, related_name='rotations')
def __str__(self):
return f"Movimentação {self.id} - Lote {self.batch.identifier}"
+63
View File
@@ -0,0 +1,63 @@
from django.db import models
from django.contrib.auth.models import UserManager, AbstractBaseUser, PermissionsMixin
from django.utils import timezone
class CustomUserManager(UserManager):
def _create_user(self, name, login, email, password, **extra_fields):
if not login:
raise ValueError("Information provided is not a valid login")
if not email:
raise ValueError("You must inform an e-mail adress")
email = self.normalize_email(email)
user = self.model(
name = name,
login = login,
email = email,
**extra_fields)
user.set_password(password)
user.save(using=self.db)
return user
def create_user(self, name=None, login=None, email=None, password=None, **extra_fields):
extra_fields.setdefault('is_staff', False)
extra_fields.setdefault('is_superuser', False)
return self._create_user(name, login, email, password, **extra_fields)
def create_superuser(self, name=None, login=None, email=None, password=None, **extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
return self._create_user(name, login, email, password, **extra_fields)
class User(AbstractBaseUser, PermissionsMixin):
name = models.CharField(max_length=45)
login = models.CharField(max_length=45, unique=True)
email = models.EmailField(unique=True)
farms = models.ManyToManyField('entities.Farm', related_name='users')
is_active = models.BooleanField(default=True)
is_superuser = models.BooleanField(default=False)
is_staff = models.BooleanField(default=False)
date_joined = models.DateTimeField(default=timezone.now)
last_login = models.DateTimeField(blank=True, null=True)
objects = CustomUserManager()
USERNAME_FIELD = 'login'
EMAIL_FIELD = 'email'
REQUIRED_FIELDS = ['name', 'email']
class Meta:
verbose_name = 'User'
verbose_name_plural = 'Users'
@property
def full_name(self):
return self.name
@property
def short_name(self):
return self.name.split(' ')[0] or self.email.split('@')[0]
Whitespace-only changes.
@@ -0,0 +1,227 @@
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.db.models import Prefetch
from ..models import Animal, Batch
from ..forms import AnimalCreateForm, AnimalEditForm, BatchForm
from entities.models import Farm
def get_selected_farm(request):
return Farm.objects.get(id=request.session['selected_farm_id'])
# Animal Services
def get_animal_home_context(request):
selected_farm = get_selected_farm(request)
context = {}
context['title'] = 'Animais'
context['farm'] = selected_farm
animals = Animal.objects.filter(
farm=selected_farm,
is_active=True
)
filter_query = request.GET.get('animal_search')
if filter_query:
context['filter_query'] = filter_query
animals = animals.filter(
identifier__icontains=filter_query
)
context['animals'] = animals
return context
def get_animal_new_context():
form = AnimalCreateForm()
context = {}
context['title'] = 'Novo animal'
context['form'] = form
context['action_url'] = reverse('entities:animal:animal_new')
context['return_url'] = reverse('entities:animal:animal_home')
return context
def create_animal(request):
form = AnimalCreateForm(request.POST)
selected_farm = get_selected_farm(request)
if form.is_valid():
animal = form.save(commit=False)
animal.farm = selected_farm
animal.save()
return True
return False
def update_animal(request, animal_id):
farm_id = request.session['selected_farm_id']
animal = get_object_or_404(Animal, pk=animal_id, farm_id=farm_id)
context = {}
context['title'] = 'Editar animal'
context['action_url'] = reverse(
'entities:animal:animal_update',
kwargs={'animal_id': animal.pk}
)
context['return_url'] = reverse('entities:animal:animal_home')
if request.method == 'GET':
form = AnimalEditForm(instance=animal)
elif request.method == 'POST':
form = AnimalEditForm(request.POST, instance=animal)
if form.is_valid():
form.save()
return {'redirect': 'entities:animal:animal_home'}
context['form'] = form
return {'context': context}
def delete_animal(request, animal_id):
farm_id = request.session['selected_farm_id']
animal = get_object_or_404(Animal, pk=animal_id, farm_id=farm_id)
animal.is_active = False
animal.save()
# Batch Services
def get_batch_home_context(request):
selected_farm = get_selected_farm(request)
context = {}
context['title'] = 'Lotes'
context['farm'] = selected_farm
all_batches = Batch.objects.filter(
batchanimal__animal__farm=selected_farm,
is_active=True
).distinct().prefetch_related(
Prefetch('animals', queryset=Animal.objects.filter(is_active=True))
)
filter_query = request.GET.get('batch_search')
if filter_query:
context['filter_query'] = filter_query
all_batches = all_batches.filter(
identifier__icontains=filter_query
)
context['batches'] = all_batches
return context
def create_batch(request):
selected_farm = get_selected_farm(request)
if request.method == 'GET':
form = BatchForm()
form.fields['animals'].queryset = Animal.objects.filter(
farm=selected_farm,
is_active=True
)
context = {
'title': 'Novo Lote',
'form': form,
'action_url': reverse('entities:animal:batch_new'),
'return_url': reverse('entities:animal:batch_home')
}
return {'context': context}
elif request.method == 'POST':
form = BatchForm(request.POST)
if form.is_valid():
batch = form.save(commit=False)
batch.save()
form.save_m2m()
return {'redirect': 'entities:animal:batch_home'}
form.fields['animals'].queryset = Animal.objects.filter(
farm=selected_farm,
is_active=True
)
context = {
'title': 'Novo Lote',
'form': form,
'action_url': reverse('entities:animal:batch_new'),
'return_url': reverse('entities:animal:batch_home')
}
return {'context': context}
def update_batch(request, batch_id):
batch_old = get_object_or_404(Batch, pk=batch_id, is_active=True)
selected_farm = get_selected_farm(request)
context = {
'title': f'Editar Lote {batch_old.identifier}',
'return_url': reverse('entities:animal:batch_home')
}
if request.method == 'GET':
form = BatchForm(instance=batch_old)
form.fields['animals'].queryset = Animal.objects.filter(
farm=selected_farm,
is_active=True
)
context['action_url'] = reverse(
'entities:animal:batch_update',
kwargs={'batch_id': batch_old.pk}
)
elif request.method == 'POST':
form = BatchForm(request.POST)
if form.is_valid():
batch_new = form.save(commit=False)
batch_new.is_active = True
batch_old.is_active = False
batch_old.save()
batch_new.save()
form.save_m2m()
return {'redirect': 'entities:animal:batch_home'}
form.fields['animals'].queryset = Animal.objects.filter(
farm=selected_farm,
is_active=True
)
context['action_url'] = reverse(
'entities:animal:batch_update',
kwargs={'batch_id': batch_old.pk}
)
context['form'] = form
return {'context': context}
def delete_batch(batch_id):
batch = get_object_or_404(Batch, pk=batch_id, is_active=True)
batch.is_active = False
batch.save()
+293
View File
@@ -0,0 +1,293 @@
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.db.models import Prefetch
from ..models import Farm, Paddock, PaddockGroup
from ..forms import FarmCreateForm, FarmEditForm, PaddockCreateForm, PaddockEditForm, PaddockGroupCreateForm, PaddockGroupEditForm
def get_selected_farm(request):
return Farm.objects.get(id=request.session['selected_farm_id'])
# Farm Services
def get_farm_home_context(request):
authenticated_user = request.user
context = {}
context['title'] = 'Fazenda'
farms = Farm.objects.filter(
users=authenticated_user, # Verificar se da para deixar apenas = ou deve usar um is in ou algo do tipo
is_active=True
)
context['farms'] = farms
return context
def get_farm_new_context():
form = FarmCreateForm()
context = {}
context['title'] = 'Nova fazenda'
context['form'] = form
context['action_url'] = reverse('entities:farm:farm_new')
context['return_url'] = reverse('entities:farm:farm_home')
return context
def create_farm(request):
form = FarmCreateForm(request.POST)
authenticated_user = request.user
if form.is_valid():
farm = form.save()
farm.users.add(authenticated_user)
return True
return False
def update_farm(request, farm_id):
farm = get_object_or_404(Farm, pk=farm_id, is_active=True)
context = {}
context['title'] = 'Editar fazenda'
context['action_url'] = reverse(
'entities:farm:farm_update',
kwargs={'farm_id': farm.pk}
)
context['return_url'] = reverse('entities:farm:farm_home')
if request.method == 'GET':
form = FarmEditForm(instance=farm)
elif request.method == 'POST':
form = FarmEditForm(request.POST, instance=farm)
if form.is_valid():
form.save()
return {'redirect': 'entities:farm:farm_home'}
context['form'] = form
return {'context': context}
def delete_farm(farm_id):
farm = get_object_or_404(Farm, pk=farm_id, is_active=True)
farm.is_active = False
farm.save()
# Paddock Services
def get_paddock_home_context(request):
selected_farm = get_selected_farm(request)
context = {}
context['title'] = 'Piquete'
context['farm'] = selected_farm
all_paddocks = Paddock.objects.filter(
farm=selected_farm,
is_active=True
)
filter_query = request.GET.get('paddock_search')
if filter_query:
context['filter_query'] = filter_query
all_paddocks = all_paddocks.filter(
identifier__icontains=filter_query
)
context['paddocks'] = all_paddocks
return context
def create_paddock(request):
selected_farm = get_selected_farm(request)
if request.method == 'GET':
form = PaddockCreateForm()
context = {
'title': 'Novo Piquete',
'form': form,
'action_url': reverse('entities:farm:paddock_new'),
'return_url': reverse('entities:farm:paddock_home')
}
return {'context': context}
elif request.method == 'POST':
form = PaddockCreateForm(request.POST)
if form.is_valid():
paddock = form.save(commit=False)
paddock.farm = selected_farm
paddock.save()
return {'redirect': 'entities:farm:paddock_home'}
context = {
'title': 'Novo Piquete',
'form': form,
'action_url': reverse('entities:farm:paddock_new'),
'return_url': reverse('entities:farm:paddock_home')
}
return {'context': context}
def update_paddock(request, paddock_id):
farm_id = request.session['selected_farm_id']
paddock = get_object_or_404(Paddock, pk=paddock_id, farm_id=farm_id, is_active=True)
context = {}
context['title'] = 'Editar piquete'
context['action_url'] = reverse(
'entities:farm:paddock_update',
kwargs={'paddock_id': paddock.pk}
)
context['return_url'] = reverse('entities:farm:paddock_home')
if request.method == 'GET':
form = PaddockEditForm(instance=paddock)
elif request.method == 'POST':
form = PaddockEditForm(request.POST, instance=paddock)
if form.is_valid():
form.save()
return {'redirect': 'entities:farm:paddock_home'}
context['form'] = form
return {'context': context}
def delete_paddock(paddock_id):
paddock = get_object_or_404(Paddock, pk=paddock_id, is_active=True)
paddock.is_active = False
paddock.save()
# Paddock Group Services
def get_paddock_group_home_context(request):
selected_farm = get_selected_farm(request)
context = {}
context['title'] = 'Blocos'
context['farm'] = selected_farm
all_paddock_groups = PaddockGroup.objects.filter(
paddocks__farm=selected_farm,
is_active=True
).distinct().prefetch_related(
Prefetch('paddocks', queryset=Paddock.objects.filter(is_active=True))
)
filter_query = request.GET.get('paddock_group_search')
if filter_query:
context['filter_query'] = filter_query
all_paddock_groups = all_paddock_groups.filter(
identifier__icontains=filter_query
)
context['paddock_groups'] = all_paddock_groups
return context
def create_paddock_group(request):
selected_farm = get_selected_farm(request)
if request.method == 'GET':
form = PaddockGroupCreateForm()
form.fields['paddocks'].queryset = Paddock.objects.filter(
farm=selected_farm,
is_active=True
)
if request.method == 'POST':
form = PaddockGroupCreateForm(request.POST)
if form.is_valid():
paddock_group = form.save(commit=False)
paddock_group.save()
return {'redirect': 'entities:farm:paddock_group_home'}
form.fields['paddocks'].queryset = Paddock.objects.filter(
farm=selected_farm,
is_active=True
)
context = {
'title': 'Novo Bloco',
'form': form,
'action_url': reverse('entities:farm:paddock_group_new'),
'return_url': reverse('entities:farm:paddock_group_home')
}
return {'context': context}
def update_paddock_group(request, paddock_group_id):
selected_farm = get_selected_farm(request)
paddock_group = get_object_or_404(PaddockGroup, pk=paddock_group_id, is_active=True)
context = {
'title': f'Editar Lote {paddock_group.identifier}',
'return_url': reverse('entities:farm:paddock_group_home')
}
if request.method == 'GET':
form = PaddockGroupEditForm(instance=paddock_group)
form.fields['paddocks'].queryset = Paddock.objects.filter(
farm=selected_farm,
is_active=True
)
context['action_url'] = reverse(
'entities:farm:paddock_group_update',
kwargs={'paddock_group_id': paddock_group.pk}
)
elif request.method == 'POST':
form = PaddockGroupEditForm(request.POST, instance=paddock_group)
if form.is_valid():
paddock_group = form.save()
paddock_group.save()
return {'redirect': 'entities:farm:paddock_group_home'}
form.fields['paddocks'].queryset = Paddock.objects.filter(
farm=selected_farm,
is_active=True
)
context['action_url'] = reverse(
'entities:farm:paddock_group_update',
kwargs={'paddock_group_id': paddock_group.pk}
)
context['form'] = form
return {'context': context}
def delete_paddock_group(paddock_group_id):
paddock_group = get_object_or_404(PaddockGroup, pk=paddock_group_id, is_active=True)
paddock_group.is_active = False
paddock_group.save()
@@ -0,0 +1,88 @@
from django.shortcuts import redirect
from django.contrib.auth import authenticate, login, logout
from django.db.models import Count
from ..forms import UserLoginForm
def user_profile(request): # TO DO
...
def user_login(request):
if request.user.is_authenticated:
return {'redirect': 'entities:user:farm_select'}
context = {}
context['fail_login'] = False
form = UserLoginForm()
if request.method == "POST":
form = UserLoginForm(request.POST)
if form.is_valid():
username = form.cleaned_data['login']
password = form.cleaned_data['password']
user = authenticate(request, username=username, password=password)
if user is not None:
login(request, user)
context['title'] = 'Fazendas'
return {'redirect': 'entities:user:farm_select'}
else:
context['fail_login'] = True
else:
context['fail_login'] = True
context['form'] = form
return {'context': context}
def user_logout(request):
logout(request)
return redirect('entities:user:user_login')
def user_signin(request): # TO DO
...
def farm_select(request):
if 'selected_farm_id' in request.session:
return {'redirect': 'entities:farm:farm_home'}
farms_query = request.user.farms.filter(is_active=True)
farms_with_counts = farms_query.annotate(
total_animals=Count('animals'),
total_paddocks=Count('paddocks')
)
context = {
'farms': farms_with_counts
}
return {'context': context}
def farm_set(request, farm_id=None):
# Allows setting the selected farm either via URL parameter or POST data
farm_id = farm_id or request.POST.get("farm_id")
next_url = request.POST.get("next")
if not farm_id:
return redirect('entities:user:farm_select')
try:
farm = request.user.farms.get(id=farm_id)
request.session['selected_farm_id'] = farm.id
return redirect(next_url or 'entities:farm:farm_home')
except request.user.farms.model.DoesNotExist:
return redirect('entities:user:farm_select')
def farm_deselect(request):
if 'selected_farm_id' in request.session:
del request.session['selected_farm_id']
return redirect('entities:user:farm_select')
@@ -0,0 +1,152 @@
/* ===== PAGE HEADER ===== */
.animal_title {
display: flex;
align-items: center;
gap: var(--space-md);
margin-bottom: var(--space-lg);
}
.animal_title h1 {
font-size: var(--font-xl);
color: var(--color-text-dark);
margin: 0;
margin-right: var(--space-lg);
}
/* ===== SEARCH FORM ===== */
.form_animal_search {
display: flex;
align-items: center;
gap: var(--space-xs);
margin-left: auto;
}
.form_animal_search label {
font-size: var(--font-sm);
color: var(--color-text-dark);
}
/* Input com altura igual ao botão */
.form_animal_search input {
height: 32px;
padding: 0 var(--space-sm);
font-size: var(--font-sm);
border: 1px solid var(--color-border-subtle);
border-radius: var(--border-radius-sm);
outline: none;
}
.form_animal_search input:focus {
border-color: var(--color-primary);
}
/* Botão buscar */
.form_animal_search button {
height: 32px;
padding: 0 var(--space-sm);
border: none;
border-radius: var(--border-radius-sm);
background: var(--color-primary);
color: var(--color-text-light);
cursor: pointer;
transition: background var(--transition-speed);
}
.form_animal_search button:hover {
background: var(--color-secondary);
}
/* ===== CLEAR FILTER ===== */
.btn-clear-filter {
display: flex;
align-items: center;
gap: 4px;
height: 32px;
padding: 0 var(--space-sm);
background: var(--color-danger-light);
color: var(--color-danger);
border-radius: var(--border-radius-sm);
text-decoration: none;
font-size: var(--font-sm);
}
.btn-clear-filter:hover {
background: var(--color-danger);
color: var(--color-text-light);
}
/* ===== NEW BUTTON ===== */
.animal_title > a {
height: 32px;
padding: 0 var(--space-md);
background: var(--color-success);
color: var(--color-text-light);
border-radius: var(--border-radius-sm);
text-decoration: none;
font-size: var(--font-sm);
display: flex;
align-items: center;
gap: var(--space-xs);
transition: background var(--transition-speed);
}
.animal_title > a:hover {
background: var(--color-success);
opacity: 0.85;
}
/* ===== TABLE ===== */
table {
width: 100%;
border-collapse: collapse;
background: var(--color-bg-white);
border-radius: var(--border-radius-sm);
overflow: hidden;
}
table th {
text-align: left;
padding: var(--space-sm);
font-size: var(--font-sm);
background: var(--color-bg-dark);
color: var(--color-text-light);
font-weight: 500;
}
table td {
padding: var(--space-sm);
font-size: var(--font-md);
border-bottom: 1px solid var(--color-border-subtle);
}
table tr:last-child td {
border-bottom: none;
}
table tbody tr:hover {
background: var(--color-bg-light);
}
/* ===== ACTION ICONS ===== */
table a {
color: var(--color-primary);
transition: color var(--transition-speed);
}
table a:hover {
color: var(--color-secondary);
}
/* DELETE BUTTON */
.btn-icon-trash {
background: transparent;
border: none;
color: var(--color-danger);
cursor: pointer;
transition: color var(--transition-speed);
}
.btn-icon-trash:hover {
color: var(--color-danger);
opacity: 0.7;
}
@@ -0,0 +1,150 @@
/* ===== PAGE HEADER ===== */
.batch_title {
display: flex;
align-items: center;
gap: var(--space-md);
margin-bottom: var(--space-lg);
}
.batch_title h1 {
font-size: var(--font-xl);
color: var(--color-text-dark);
margin: 0;
margin-right: var(--space-lg);
}
/* ===== SEARCH FORM ===== */
.form_batch_search {
display: flex;
align-items: center;
gap: var(--space-xs);
margin-left: auto;
}
.form_batch_search label {
font-size: var(--font-sm);
color: var(--color-text-dark);
}
.form_batch_search input {
height: 32px;
padding: 0 var(--space-sm);
font-size: var(--font-sm);
border: 1px solid var(--color-border-subtle);
border-radius: var(--border-radius-sm);
outline: none;
}
.form_batch_search input:focus {
border-color: var(--color-primary);
}
.form_batch_search button {
height: 32px;
padding: 0 var(--space-sm);
border: none;
border-radius: var(--border-radius-sm);
background: var(--color-primary);
color: var(--color-text-light);
cursor: pointer;
transition: background var(--transition-speed);
}
.form_batch_search button:hover {
background: var(--color-secondary);
}
/* ===== CLEAR FILTER ===== */
.btn-clear-filter {
display: flex;
align-items: center;
gap: 4px;
height: 32px;
padding: 0 var(--space-sm);
background: var(--color-danger-light);
color: var(--color-danger);
border-radius: var(--border-radius-sm);
text-decoration: none;
font-size: var(--font-sm);
}
.btn-clear-filter:hover {
background: var(--color-danger);
color: var(--color-text-light);
}
/* ===== NEW BUTTON ===== */
.batch_title > a {
height: 32px;
padding: 0 var(--space-md);
background: var(--color-success);
color: var(--color-text-light);
border-radius: var(--border-radius-sm);
text-decoration: none;
font-size: var(--font-sm);
display: flex;
align-items: center;
gap: var(--space-xs);
transition: background var(--transition-speed);
}
.batch_title > a:hover {
background: var(--color-success);
opacity: 0.85;
}
/* ===== TABLE ===== */
table {
width: 100%;
border-collapse: collapse;
background: var(--color-bg-white);
border-radius: var(--border-radius-sm);
overflow: hidden;
}
table th {
text-align: left;
padding: var(--space-sm);
font-size: var(--font-sm);
background: var(--color-bg-dark);
color: var(--color-text-light);
font-weight: 500;
}
table td {
padding: var(--space-sm);
font-size: var(--font-md);
border-bottom: 1px solid var(--color-border-subtle);
}
table tr:last-child td {
border-bottom: none;
}
table tbody tr:hover {
background: var(--color-bg-light);
}
/* ===== ACTION ICONS ===== */
table a {
color: var(--color-primary);
transition: color var(--transition-speed);
}
table a:hover {
color: var(--color-secondary);
}
/* DELETE BUTTON */
.btn-icon-trash {
background: transparent;
border: none;
color: var(--color-danger);
cursor: pointer;
transition: color var(--transition-speed);
}
.btn-icon-trash:hover {
color: var(--color-danger);
opacity: 0.7;
}
Whitespace-only changes.
@@ -0,0 +1,94 @@
/* ===== PAGE HEADER ===== */
.paddock_title {
display: flex;
align-items: center;
gap: var(--space-md);
margin-bottom: var(--space-lg);
}
.paddock_title h1 {
font-size: var(--font-xl);
color: var(--color-text-dark);
margin: 0;
margin-right: var(--space-lg);
}
/* ===== SEARCH FORM ===== */
.form_paddock_search {
display: flex;
align-items: center;
gap: var(--space-xs);
margin-left: auto;
}
.form_paddock_search label {
font-size: var(--font-sm);
color: var(--color-text-dark);
}
.form_paddock_search input {
height: 32px;
padding: 0 var(--space-sm);
font-size: var(--font-sm);
border: 1px solid var(--color-border-subtle);
border-radius: var(--border-radius-sm);
outline: none;
}
.form_paddock_search input:focus {
border-color: var(--color-primary);
}
.form_paddock_search button {
height: 32px;
padding: 0 var(--space-sm);
border: none;
border-radius: var(--border-radius-sm);
background: var(--color-primary);
color: var(--color-text-light);
cursor: pointer;
transition: background var(--transition-speed);
}
.form_paddock_search button:hover {
background: var(--color-secondary);
}
/* ===== CLEAR FILTER ===== */
.btn-clear-filter {
display: flex;
align-items: center;
gap: 4px;
height: 32px;
padding: 0 var(--space-sm);
background: var(--color-danger-light);
color: var(--color-danger);
border-radius: var(--border-radius-sm);
text-decoration: none;
font-size: var(--font-sm);
}
.btn-clear-filter:hover {
background: var(--color-danger);
color: var(--color-text-light);
}
/* ===== NEW BUTTON ===== */
.paddock_title > a {
height: 32px;
padding: 0 var(--space-md);
background: var(--color-success);
color: var(--color-text-light);
border-radius: var(--border-radius-sm);
text-decoration: none;
font-size: var(--font-sm);
display: flex;
align-items: center;
gap: var(--space-xs);
transition: background var(--transition-speed);
}
.paddock_title > a:hover {
background: var(--color-success);
opacity: 0.85;
}
@@ -0,0 +1,75 @@
/* ===== PAGE HEADER ===== */
.paddock_group_title {
display: flex;
align-items: center;
gap: var(--space-md);
margin-bottom: var(--space-lg);
}
.paddock_group_title h1 {
font-size: var(--font-xl);
color: var(--color-text-dark);
margin: 0;
margin-right: var(--space-lg);
}
/* ===== SEARCH FORM ===== */
.form_paddock_group_search {
display: flex;
align-items: center;
gap: var(--space-xs);
margin-left: auto;
}
.form_paddock_group_search label {
font-size: var(--font-sm);
color: var(--color-text-dark);
}
.form_paddock_group_search input {
height: 32px;
padding: 0 var(--space-sm);
font-size: var(--font-sm);
border: 1px solid var(--color-border-subtle);
border-radius: var(--border-radius-sm);
outline: none;
}
.form_paddock_group_search input:focus {
border-color: var(--color-primary);
}
.form_paddock_group_search button {
height: 32px;
padding: 0 var(--space-sm);
border: none;
border-radius: var(--border-radius-sm);
background: var(--color-primary);
color: var(--color-text-light);
cursor: pointer;
transition: background var(--transition-speed);
}
.form_paddock_group_search button:hover {
background: var(--color-secondary);
}
/* ===== NEW BUTTON ===== */
.paddock_group_title > a {
height: 32px;
padding: 0 var(--space-md);
background: var(--color-success);
color: var(--color-text-light);
border-radius: var(--border-radius-sm);
text-decoration: none;
font-size: var(--font-sm);
display: flex;
align-items: center;
gap: var(--space-xs);
transition: background var(--transition-speed);
}
.paddock_group_title > a:hover {
background: var(--color-success);
opacity: 0.85;
}
Whitespace-only changes.
@@ -0,0 +1,162 @@
/* CONTENT */
body {
display: block;
min-height: 100vh;
margin: 0;
padding-top: var(--header-height);
}
/* NAVBAR */
.farm-select-navbar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: var(--header-height);
background-color: var(--color-bg-light);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 var(--space-lg);
z-index: 950;
}
/* SITE'S TITLE */
.farm-select-navbar .navbar-brand {
font-weight: bold;
font-size: 1.5rem;
color: var(--color-bg-sidebar, #444);
cursor: default;
}
/* .navbar-user-info and .username uses gap and align style from global.css */
/* SELECT CONTAINER */
.farm-select-container {
width: 100%;
max-width: 700px;
margin: var(--space-xl) auto;
text-align: center;
padding: 0;
}
.farm-select-container h1 {
font-size: var(--font-xl);
color: #1f2937;
margin-bottom: var(--space-xl);
}
/* LIST OF FARMS */
.farm-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
.farm-button {
text-decoration: none;
display: block;
width: 100%;
}
.farm-item {
padding: var(--space-md);
text-align: left;
background-color: #ffffff;
border: 1px solid #e0e0e0;
border-left: 5px solid transparent;
border-radius: 6px;
transition: all 0.3s ease;
}
.farm-item:hover {
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
border-color: var(--color-farm-select-hover);
border-left-color: var(--color-farm-select-hover); /* Cor primária */
}
.farm-name {
font-size: 1.25rem;
color: var(--color-text-dark);
margin: 0 0 var(--space-xs);
font-weight: 700;
}
.farm-description {
display: flex;
justify-content: flex-start;
gap: var(--space-sm);
align-items: center;
margin-top: var(--space-sm);
font-size: var(--font-sm);
flex-wrap: wrap;
}
/* DATA SEPARATOR */
.total-area, .count-piquetes, .count-animais {
padding: 0;
background-color: transparent;
color: var(--color-farm-select-info);
font-weight: 500;
}
.total-area:after, .count-piquetes:after {
content: "|";
margin-left: var(--space-xs);
color: var(--color-simple-light-gray);
}
/* WHEN NO FARMS */
.no-farms {
padding: var(--space-lg);
margin-top: var(--space-lg);
border: 2px dashed #ccc;
border-radius: var(--border-radius-sm);
background-color: var(--color-bg-white);
}
.no-farms p {
color: var(--color-farm-select-info);
font-size: var(--font-md);
margin: var(--space-xs) 0;
}
/* NEW FARM BUTTON */
.no-farms a {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
margin-top: var(--space-md);
padding: var(--space-xs) var(--space-md);
background-color: var(--color-new-farm-button);
color: white;
text-decoration: none;
font-weight: 500;
border-radius: 4px;
transition: background-color 0.2s;
}
.no-farms a:hover {
background-color: var(--color-new-farm-hover);
}
@media (max-width: 768px) {
body {
padding: 0;
}
.farm-select-navbar {
padding: 0 var(--space-md);
}
.farm-select-container {
padding: var(--space-md) var(--space-sm);
margin: var(--header-height) 0 0 0;
max-width: 100%;
box-shadow: none;
border-radius: 0;
}
}
@@ -0,0 +1,145 @@
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
background-color: var(--color-bg-light);
color: var(--color-text-dark);
}
.login-container {
display: flex;
width: 850px;
max-width: 90%;
min-height: 450px;
border-radius: var(--border-radius-lg, 10px);
overflow: hidden;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15);
background-color: #ffffff;
}
.ilustration-container {
flex: 1;
background: linear-gradient(135deg, var(--login-background-primary), var(--login-background-secondary));
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: var(--space-lg);
color: var(--color-text-light);
text-align: center;
font-size: var(--font-lg, 1.5rem);
font-weight: bold;
}
.ilustration-container h1 {
margin: 0;
line-height: 1.2;
}
.login-form-container {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
padding: var(--space-xl);
}
.login-form-container h1 {
font-size: var(--font-xl);
margin-bottom: var(--space-md);
color: var(--color-text-dark);
text-align: center;
}
.login-form-container form {
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
.login-form-container input[type="text"],
.login-form-container input[type="password"] {
padding: var(--space-sm);
font-size: var(--font-md);
border-radius: var(--border-radius-sm);
border: 1px solid var(--color-border-subtle);
width: 100%;
transition: border-color var(--transition-speed) ease;
}
.login-form-container input:focus {
border-color: var(--color-primary);
outline: none;
box-shadow: 0 0 0 1px var(--color-primary-light, rgba(0, 123, 255, 0.2));
}
.login-form-container button[type="submit"] {
background-color: var(--color-login-button);
color: white;
padding: var(--space-xs);
font-size: var(--font-lg);
font-weight: bold;
border: none;
border-radius: var(--border-radius-sm);
cursor: pointer;
margin-top: var(--space-sm);
transition: background-color var(--transition-speed) ease;
}
.login-form-container button[type="submit"]:hover {
background-color: var(--color-login-button-focus);
}
.login-fail {
color: var(--color-danger);
background-color: var(--color-danger-light);
border: 1px solid var(--color-danger);
padding: var(--space-xs);
border-radius: var(--border-radius-sm);
font-size: var(--font-sm);
text-align: center;
margin-top: var(--space-sm);
}
.login-fail i {
margin-right: var(--space-xs);
}
.login-form-container p {
text-align: center;
font-size: var(--font-sm);
margin-top: var(--space-md);
}
.login-form-container a {
color: var(--color-primary);
text-decoration: none;
}
.login-form-container a:hover {
text-decoration: underline;
}
@media (max-width: 800px) {
.login-container {
flex-direction: column;
height: auto;
max-width: 95%;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.ilustration-container, .login-form-container {
flex: none;
width: 100%;
}
.ilustration-container {
padding: var(--space-md);
}
.ilustration-container h1 {
font-size: var(--font-md);
}
.login-form-container {
padding: var(--space-lg) var(--space-md);
}
}
@@ -0,0 +1,48 @@
{% extends 'base.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'animal/css/animal.css' %}">
{% endblock %}
{% block content %}
<div class="animal_title">
<h1>Animais</h1>
<form method="GET" action="{% url 'entities:animal:animal_home' %}" class="form_animal_search">
{% if filter_query %}
<a href="{% url 'entities:animal:animal_home' %}" class="btn-clear-filter">{{filter_query}}<i class="fa-solid fa-xmark"></i></a>
{% endif %}
<label for="animal_search">Buscar</label>
<input type="text" name="animal_search" required>
<button type="submit"><i class="fa-solid fa-magnifying-glass"></i></button>
</form>
<a href="{% url 'entities:animal:animal_new' %}">Novo <i class="fa-solid fa-plus"></i></a>
</div>
{% if not animals %}
<p>Nenhum animal encontrado.</p>
{% else %}
<table>
<tr>
<th>Identificador</th>
<th>Idade</th>
<th>Peso</th>
<th>Editar</th>
<th>Excluir</th>
</tr>
{% for animal in animals %}
<tr>
<td>{{ animal.identifier }}</td>
<td>{{ animal.age }}</td>
<td>{{ animal.weight }} Kg</td>
<td><a href="{% url 'entities:animal:animal_update' animal.id %}"><i class="fa-solid fa-pen-to-square"></i></a></td>
<td>
<form method="POST" action="{% url 'entities:animal:animal_delete' animal.id %}">
{% csrf_token %}
<button type="submit" class="btn-icon-trash"><i class="fa-solid fa-trash"></i></button>
</form>
</td>
</tr>
{% endfor %}
</table>
{% endif %}
{% endblock %}
@@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'animal/css/animal.css' %}">
{% endblock %}
{% block content %}
NOVO ANIMAL <br>
{% include 'includes/_form.html' %}
{% endblock %}
@@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'animal/css/animal.css' %}">
{% endblock %}
{% block content %}
EDITAR ANIMAL <br>
{% include 'includes/_form.html' %}
{% endblock %}
@@ -0,0 +1,48 @@
{% extends 'base.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'animal/css/batch.css' %}">
{% endblock %}
{% block content %}
<div class="batch_title">
<h1>Lotes</h1>
<form method="GET" action="{% url 'entities:animal:batch_home' %}" class="form_batch_search">
{% if filter_query %}
<a href="{% url 'entities:animal:batch_home' %}" class="btn-clear-filter">{{filter_query}}<i class="fa-solid fa-xmark"></i></a>
{% endif %}
<label for="batch_search">Buscar</label>
<input type="text" name="batch_search" required>
<button type="submit"><i class="fa-solid fa-magnifying-glass"></i></button>
</form>
<a href="{% url 'entities:animal:batch_new' %}">Novo <i class="fa-solid fa-plus"></i></a>
</div>
{% if not batches %}
<p>Nenhum lote encontrado.</p>
{% else %}
<table>
<tr>
<th>Identificador</th>
<th>Categoria animal</th>
<th>Tipo de lote</th>
<th>Editar</th>
<th>Excluir</th>
</tr>
{% for batch in batches %}
<tr>
<td>{{ batch.identifier }}</td>
<td>{{ batch.animal_category }}</td>
<td>{{ batch.batch_type }} Kg</td>
<td><a href="{% url 'entities:animal:batch_update' batch.id %}"><i class="fa-solid fa-pen-to-square"></i></a></td>
<td>
<form method="POST" action="{% url 'entities:animal:batch_delete' batch.id %}">
{% csrf_token %}
<button type="submit" class="btn-icon-trash"><i class="fa-solid fa-trash"></i></button>
</form>
</td>
</tr>
{% endfor %}
</table>
{% endif %}
{% endblock %}
@@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'animal/css/batch.css' %}">
{% endblock %}
{% block content %}
NOVO LOTE <br>
{% include 'includes/_form.html' %}
{% endblock %}
@@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'animal/css/batch.css' %}">
{% endblock %}
{% block content %}
EDITAR LOTE <br>
{% include 'includes/_form.html' %}
{% endblock %}
@@ -0,0 +1,46 @@
{% extends 'base.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'farm/css/home.css' %}">
{% endblock %}
{% block content %}
<div class="farm_title">
<h1>Fazendas</h1>
<form method="GET" action="{% url 'entities:farm:farm_home' %}" class="form_farm_search">
{% if filter_query %}
<a href="{% url 'entities:farm:farm_home' %}" class="btn-clear-filter">{{filter_query}}<i class="fa-solid fa-xmark"></i></a>
{% endif %}
<label for="farm_search">Buscar</label>
<input type="text" name="farm_search" required>
<button type="submit"><i class="fa-solid fa-magnifying-glass"></i></button>
</form>
<a href="{% url 'entities:farm:farm_new' %}">Novo <i class="fa-solid fa-plus"></i></a>
</div>
{% if not farms %}
<p>Nenhuma fazenda encontrado.</p>
{% else %}
<table>
<tr>
<th>Nome</th>
<th>Área total</th>
<th>Editar</th>
<th>Excluir</th>
</tr>
{% for farm in farms %}
<tr>
<td>{{ farm.name }}</td>
<td>{{ farm.total_area }} m2</td>
<td><a href="{% url 'entities:farm:farm_update' farm.id %}"><i class="fa-solid fa-pen-to-square"></i></a></td>
<td>
<form method="POST" action="{% url 'entities:farm:farm_delete' farm.id %}">
{% csrf_token %}
<button type="submit" class="btn-icon-trash"><i class="fa-solid fa-trash"></i></button>
</form>
</td>
</tr>
{% endfor %}
</table>
{% endif %}
{% endblock %}
@@ -0,0 +1,10 @@
{% extends 'base.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'farm/css/home.css' %}">
{% endblock %}
{% block content %}
New - Farm
{% endblock %}
@@ -0,0 +1,10 @@
{% extends 'base.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'farm/css/home.css' %}">
{% endblock %}
{% block content %}
Update - Farm
{% endblock %}
@@ -0,0 +1,85 @@
{% extends 'base.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'farm/css/paddock.css' %}">
{% endblock %}
{% block content %}
<div class="paddock_title">
<h1>Piquetes</h1>
<form method="GET" action="{% url 'entities:farm:paddock_home' %}" class="form_paddock_search">
{% if filter_query %}
<a href="{% url 'entities:farm:paddock_home' %}" class="btn-clear-filter">
{{filter_query}}<i class="fa-solid fa-xmark"></i>
</a>
{% endif %}
<label for="paddock_search">Buscar</label>
<input type="text" name="paddock_search" required>
<button type="submit">
<i class="fa-solid fa-magnifying-glass"></i>
</button>
</form>
<a href="{% url 'entities:farm:paddock_new' %}">
Novo <i class="fa-solid fa-plus"></i>
</a>
</div>
{% if not paddocks %}
<p>Nenhum piquete encontrado.</p>
{% else %}
<table>
<tr>
<th>Número</th>
<th>Área</th>
<th>Capacidade</th>
<th>Ocupado</th>
<th>Bloco</th>
<th>Editar</th>
<th>Excluir</th>
</tr>
{% for paddock in paddocks %}
<tr>
<td>{{ paddock.paddock_number }}</td>
<td>{{ paddock.area }} ha</td>
<td>{{ paddock.load_capacity }}</td>
<td>
{% if paddock.occupied %}
Sim
{% else %}
Não
{% endif %}
</td>
<td>
{% if paddock.paddock_group %}
{{ paddock.paddock_group.identifier }}
{% else %}
-
{% endif %}
</td>
<td>
<a href="{% url 'entities:farm:paddock_update' paddock.id %}">
<i class="fa-solid fa-pen-to-square"></i>
</a>
</td>
<td>
<form method="POST" action="{% url 'entities:farm:paddock_delete' paddock.id %}">
{% csrf_token %}
<button type="submit" class="btn-icon-trash">
<i class="fa-solid fa-trash"></i>
</button>
</form>
</td>
</tr>
{% endfor %}
</table>
{% endif %}
{% endblock %}
@@ -0,0 +1,10 @@
{% extends 'base.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'farm/css/home.css' %}">
{% endblock %}
{% block content %}
New - Paddock
{% endblock %}
@@ -0,0 +1,10 @@
{% extends 'base.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'farm/css/home.css' %}">
{% endblock %}
{% block content %}
Delete - Paddock
{% endblock %}
@@ -0,0 +1,76 @@
{% extends 'base.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'farm/css/paddock_group.css' %}">
{% endblock %}
{% block content %}
<div class="paddock_group_title">
<h1>Blocos</h1>
<form method="GET" action="{% url 'entities:farm:paddock_group_home' %}" class="form_paddock_group_search">
{% if filter_query %}
<a href="{% url 'entities:farm:paddock_group_home' %}" class="btn-clear-filter">
{{filter_query}}<i class="fa-solid fa-xmark"></i>
</a>
{% endif %}
<label for="paddock_group_search">Buscar</label>
<input type="text" name="paddock_group_search" required>
<button type="submit">
<i class="fa-solid fa-magnifying-glass"></i>
</button>
</form>
<a href="{% url 'entities:farm:paddock_group_new' %}">
Novo <i class="fa-solid fa-plus"></i>
</a>
</div>
{% if not paddock_groups %}
<p>Nenhum bloco encontrado.</p>
{% else %}
<table>
<tr>
<th>Identificador</th>
<th>Descrição</th>
<th>Piquetes</th>
<th>Editar</th>
<th>Excluir</th>
</tr>
{% for group in paddock_groups %}
<tr>
<td>{{ group.identifier }}</td>
<td>{{ group.description }}</td>
<td>
{% for paddock in group.paddocks.all %}
{{ paddock.paddock_number }}{% if not forloop.last %}, {% endif %}
{% empty %}
-
{% endfor %}
</td>
<td>
<a href="{% url 'entities:farm:paddock_group_update' group.id %}">
<i class="fa-solid fa-pen-to-square"></i>
</a>
</td>
<td>
<form method="POST" action="{% url 'entities:farm:paddock_group_delete' group.id %}">
{% csrf_token %}
<button type="submit" class="btn-icon-trash">
<i class="fa-solid fa-trash"></i>
</button>
</form>
</td>
</tr>
{% endfor %}
</table>
{% endif %}
{% endblock %}
@@ -0,0 +1,10 @@
{% extends 'base.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'farm/css/home.css' %}">
{% endblock %}
{% block content %}
New - Paddock Group
{% endblock %}
@@ -0,0 +1,10 @@
{% extends 'base.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'farm/css/home.css' %}">
{% endblock %}
{% block content %}
Delete - Paddock Group
{% endblock %}
@@ -0,0 +1,10 @@
{% extends 'base.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'farm/css/home.css' %}">
{% endblock %}
{% block content %}
Home - Rotation
{% endblock %}
@@ -0,0 +1,10 @@
{% extends 'base.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'farm/css/home.css' %}">
{% endblock %}
{% block content %}
New - Rotation
{% endblock %}
@@ -0,0 +1,10 @@
{% extends 'base.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'farm/css/home.css' %}">
{% endblock %}
{% block content %}
Delete - Rotation
{% endblock %}
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
@@ -0,0 +1,10 @@
from django.urls import include, path
app_name = "entities"
urlpatterns = [
path("animal/", include("entities.urls.animal", namespace='animal')),
path("farm/", include("entities.urls.farm", namespace='farm')),
path("rotation/", include("entities.urls.rotation", namespace='rotation')),
path("user/", include("entities.urls.user", namespace='user')),
]
+17
View File
@@ -0,0 +1,17 @@
from django.urls import path
from ..views import animal as views
app_name = 'animal'
urlpatterns = [
path('', views.animal_home, name='animal_home'),
path('batch/', views.batch_home, name='batch_home'),
path('new/', views.animal_new, name='animal_new'),
path('update/<int:animal_id>', views.animal_update, name='animal_update'),
path('delete/<int:animal_id>', views.animal_delete, name='animal_delete'),
path('batch/new/', views.batch_new, name='batch_new'),
path('batch/update/<int:batch_id>', views.batch_update, name='batch_update'),
path('batch/delete/<int:batch_id>', views.batch_delete, name='batch_delete'),
]
+22
View File
@@ -0,0 +1,22 @@
from django.urls import path
from ..views import farm as views
app_name = 'farm'
urlpatterns = [
path('', views.farm_home, name='farm_home'),
path('paddock/', views.paddock_home, name='paddock_home'),
path("paddock/group/", views.paddock_group_home, name="paddock_group_home"),
path('new/', views.farm_new, name='farm_new'),
path('update/<int:farm_id>', views.farm_update, name='farm_update'),
path('delete/<int:farm_id>', views.farm_delete, name='farm_delete'),
path('paddock/new/', views.paddock_new, name='paddock_new'),
path('paddock/update/<int:paddock_id>', views.paddock_update, name='paddock_update'),
path('paddock/delete/<int:paddock_id>', views.paddock_delete, name='paddock_delete'),
path('paddock_group/new/', views.paddock_group_new, name='paddock_group_new'),
path('paddock_group/update/<int:paddock_group_id>', views.paddock_group_update, name='paddock_group_update'),
path('paddock_group/delete/<int:paddock_group_id>', views.paddock_group_delete, name='paddock_group_delete'),
]
@@ -0,0 +1,14 @@
from django.urls import path
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required
from ..views import rotation as views
app_name = 'rotation'
urlpatterns = [
path('', views.rotation_home, name='rotation_home'),
path('new/', views.rotation_new, name='rotation_new'),
path('delete/<int:rotation_id>', views.rotation_delete, name='rotation_delete'),
]
+18
View File
@@ -0,0 +1,18 @@
from django.urls import path
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required
from ..views import user as views
app_name = 'user'
urlpatterns = [
path('', views.user_profile, name='user_profile'),
path('login/', views.user_login, name='user_login'),
path('logout/', views.user_logout, name='user_logout'),
path('signin/', views.user_signin, name='user_signin'),
path('farm-select/', views.farm_select, name='farm_select'),
path('farm-set/<int:farm_id>/', views.farm_set, name='farm_set'),
path('farm-set/', views.farm_set, name='farm_set_post'),
path('farm-deselect/', views.farm_deselect, name='farm_deselect'),
]
@@ -0,0 +1,3 @@
from .animal import *
from .farm import *
from .rotation import *
+76
View File
@@ -0,0 +1,76 @@
from django.views.decorators.http import require_http_methods
from django.shortcuts import redirect, render
from core.decorators import farm_required
from ..services import animal as services
# Animal Views
@farm_required
def animal_home(request):
context = services.get_animal_home_context(request)
return render(request, 'animal/animal_home.html', context)
@farm_required
def animal_new(request):
if request.method == 'GET':
context = services.get_animal_new_context()
return render(request, 'animal/animal_new.html', context)
elif request.method == 'POST':
success = services.create_animal(request)
if success:
return redirect('entities:animal:animal_home')
return redirect('entities:animal:animal_new')
@farm_required
def animal_update(request, animal_id):
result = services.update_animal(request, animal_id)
if result.get('redirect'):
return redirect(result['redirect'])
return render(request, 'animal/animal_update.html', result['context'])
@farm_required
@require_http_methods(["POST"])
def animal_delete(request, animal_id):
services.delete_animal(request, animal_id)
return redirect('entities:animal:animal_home')
# Batch Views
@farm_required
def batch_home(request):
context = services.get_batch_home_context(request)
return render(request, 'batch/batch_home.html', context)
@farm_required
def batch_new(request):
result = services.create_batch(request)
if result.get('redirect'):
return redirect(result['redirect'])
return render(request, 'batch/batch_new.html', result['context'])
@farm_required
def batch_update(request, batch_id):
result = services.update_batch(request, batch_id)
if result.get('redirect'):
return redirect(result['redirect'])
return render(request, 'batch/batch_update.html', result['context'])
@farm_required
@require_http_methods(["POST"])
def batch_delete(request, batch_id):
services.delete_batch(batch_id)
return redirect('entities:animal:batch_home')
+122
View File
@@ -0,0 +1,122 @@
from django.views.decorators.http import require_http_methods
from django.shortcuts import redirect, render
from core.decorators import farm_required
from ..services import farm as services
# Farm Views
@farm_required
def farm_home(request):
context = services.get_farm_home_context(request)
return render(request, 'farm/farm_home.html', context)
@farm_required
def farm_new(request):
if request.method == 'GET':
context = services.get_farm_new_context()
return render(request, 'farm/farm_new.html', context)
elif request.method == 'POST':
success = services.create_farm(request)
if success:
return redirect('entities:farm:farm_home')
return redirect('entities:farm:farm_new')
@farm_required
def farm_update(request, farm_id):
result = services.update_farm(request, farm_id)
if result.get('redirect'):
return redirect(result['redirect'])
return render(request, 'farm/farm_update.html', result['context'])
@farm_required
@require_http_methods(["POST"])
def farm_delete(request, farm_id):
services.delete_farm(request, farm_id)
return redirect('entities:farm:farm_home')
# Paddock Views
@farm_required
def paddock_home(request):
context = {}
context['title'] = 'Piquete'
return render(request, 'paddock/paddock_home.html', context)
@farm_required
def paddock_new(request):
if request.method == 'GET':
context = services.get_paddock_new_context()
return render(request, 'paddock/paddock_new.html', context)
elif request.method == 'POST':
success = services.create_paddock(request)
if success:
return redirect('entities:farm:paddock_home')
return redirect('entities:farm:paddock_new')
@farm_required
def paddock_update(request, paddock_id):
result = services.update_paddock(request, paddock_id)
if result.get('redirect'):
return redirect(result['redirect'])
return render(request, 'paddock/paddock_update.html', result['context'])
@farm_required
@require_http_methods(["POST"])
def paddock_delete(request, paddock_id):
services.delete_paddock(request, paddock_id)
return redirect('entities:farm:paddock_home')
# Paddock Group Views
@farm_required
def paddock_group_home(request):
context = {}
context['title'] = 'Bloco'
return render(request, 'paddock_group/paddock_group_home.html', context)
@farm_required
def paddock_group_new(request):
if request.method == 'GET':
context = services.get_paddock_group_new_context()
return render(request, 'paddock_group/paddock_group_new.html', context)
elif request.method == 'POST':
success = services.create_paddock_group(request)
if success:
return redirect('entities:farm:paddock_group_home')
return redirect('entities:farm:paddock_group_new')
@farm_required
def paddock_group_update(request, paddock_group_id):
result = services.update_paddock_group(request, paddock_group_id)
if result.get('redirect'):
return redirect(result['redirect'])
return render(request, 'paddock_group/paddock_group_update.html', result['context'])
@farm_required
@require_http_methods(["POST"])
def paddock_group_delete(request, paddock_group_id):
services.delete_paddock_group(request, paddock_group_id)
return redirect('entities:farm:paddock_group_home')
@@ -0,0 +1,10 @@
from django.shortcuts import render
def rotation_home(request):
return render(request, 'rotation/rotation_home.html')
def rotation_new(request):
return render(request, 'rotation/rotation_new.html')
def rotation_delete(request):
return render(request, 'rotation/rotation_delete.html')
+40
View File
@@ -0,0 +1,40 @@
from django.shortcuts import redirect, render
from ..services import user as services
def user_profile(request): # TO DO
return services.user_profile(request)
def user_login(request):
result = services.user_login(request)
if result.get('redirect'):
return redirect(result['redirect'])
return render(request, 'login.html', result['context'])
def user_logout(request):
return services.user_logout(request)
def user_signin(request): # TO DO
return services.user_signin(request)
def farm_select(request):
result = services.farm_select(request)
if result.get('redirect'):
return redirect(result['redirect'])
return render(request, 'farm_select.html', result['context'])
def farm_set(request, farm_id=None):
return services.farm_set(request, farm_id)
def farm_deselect(request):
return services.farm_deselect(request)
View File
Whitespace-only changes.
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
+5
View File
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class ReportConfig(AppConfig):
name = 'report'
+3
View File
@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+13
View File
@@ -0,0 +1,13 @@
from django.urls import path
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required
app_name = 'report'
@login_required(login_url='/user/login/')
def home(request):
return HttpResponse("Home app page")
urlpatterns = [
path('', home),
]
+3
View File
@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'multigranja.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()
View File
Whitespace-only changes.
+7
View File
@@ -0,0 +1,7 @@
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'multigranja.settings')
application = get_asgi_application()
+148
View File
@@ -0,0 +1,148 @@
from pathlib import Path
import sys
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
sys.path.append(str(BASE_DIR / "apps"))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-aziq*utn^c^-nha$#r4veem)7iew70x47zo!9wfsb(y)pav_6v'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ['*']
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# My apps
'core',
'entities',
'report',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
# My middlewares
'core.middlewares.LoginRequiredMiddleware',
]
# Defines the system login URL
LOGIN_URL = '/user/login/'
# Defines URL paths that do not require login
LOGIN_EXEMPT_PATHS = [
'/user/login/',
'/user/signin/',
]
# Defines prefixes for URL paths that do not require login
LOGIN_EXEMPT_PREFIX = [
'/admin/',
]
ROOT_URLCONF = 'multigranja.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR/'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
# My processors
'core.context_processors.selected_farm_processor',
],
},
},
]
WSGI_APPLICATION = 'multigranja.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
# DATABASES = {
# 'default': {
# 'ENGINE': 'django.db.backends.postgresql',
# 'NAME': 'multifarmdb',
# 'USER': 'postgres',
# 'PASSWORD': 'root',
# 'HOST': 'localhost',
# 'PORT': '5432',
# }
# }
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
AUTH_USER_MODEL = 'entities.User'
# Internationalization
# https://docs.djangoproject.com/en/6.0/topics/i18n/
LANGUAGE_CODE = 'pt-br'
TIME_ZONE = 'America/Sao_Paulo'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.2/howto/static-files/
STATIC_URL = 'static/'
STATIC_DIR = [BASE_DIR / "static"]
STATICFILES_DIRS = [
BASE_DIR / "static",
]
# Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+13
View File
@@ -0,0 +1,13 @@
from django.contrib import admin
from django.shortcuts import redirect
from django.urls import path, include
def home(request):
return redirect('entities:user:user_login')
urlpatterns = [
path('admin/', admin.site.urls),
path('', home),
path('', include('entities.urls')),
path('report/', include('report.urls')),
]
+16
View File
@@ -0,0 +1,16 @@
"""
WSGI config for multigranja project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'multigranja.settings')
application = get_wsgi_application()
+42
View File
@@ -0,0 +1,42 @@
/*
* File: static/css/base/reset.css
* Purpose: Normalize and reset default browser styles.
*/
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 16px;
font-family: Arial, sans-serif;
line-height: 1.5;
}
ul, ol {
list-style: none;
}
a {
color: inherit;
text-decoration: none;
}
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
}
input,
button,
textarea,
select {
font: inherit;
}
+50
View File
@@ -0,0 +1,50 @@
:root {
/* SPECIFIC COLORS */
--login-background-primary: #343a40;
--login-background-secondary: #6b7480;
--color-login-button: #343a40;
--color-login-button-focus: #6b7480;
--color-farm-select-hover: #343a40;
--color-farm-select-info: #6d6d6d;
--color-new-farm-button: #343a40;
--color-new-farm-hover: #6b7480;
/* GENERIC COLORS */
--color-simple-light-gray: #ccc;
--color-border-subtle: #ccc;
/* PRIMARY COLORS */
--color-primary: #007bff;
--color-secondary: #6c757d;
--color-success: #28a745;
--color-danger: #dc3545;
--color-danger-light: #f8d7da;
--color-warn: #dca235;
/* BACKGROUND/TEXT COLORS */
--color-bg-light: #f0f0f0;
--color-bg-white: #fafafa;
--color-bg-dark: #343a40;
--color-bg-sidebar: #343a40;
--color-text-dark: #151422;
--color-text-light: #f8f9fa;
/* SPACING DIMENSIONS */
--space-xs: 8px;
--space-sm: 12px;
--space-md: 16px;
--space-lg: 24px;
--space-xl: 32px;
--header-height: 60px;
/* FONT DIMENSIONS */
--font-xs: 12px;
--font-sm: 14px;
--font-md: 16px;
--font-lg: 24px;
--font-xl: 32px;
--border-radius-sm: 4px;
--transition-speed: 0.4s;
--transition-speed-long: 1.0s;
}
+54
View File
@@ -0,0 +1,54 @@
/* BUTTON STYLES */
.btn-simple {
color: var(--color-text-dark);
padding: 0 var(--space-xs);
border-radius: var(--border-radius-sm);
}
.btn-simple:hover {
color: var(--color-text-light);
background-color: var(--color-bg-dark);
transition: all var(--transition-speed) ease;
}
/* PRIMARY BUTTON (Main Action) */
.btn-primary {
background-color: var(--color-primary);
padding: var(--space-sm);
border-radius: var(--border-radius-sm);
color: var(--color-text-light); /* White text */
}
.btn-primary:hover {
background-color: #0056b3; /* Darker blue on hover (a specific dark value often works best) */
}
/* SECONDARY BUTTON (Neutral Action) */
.btn-secondary {
background-color: var(--color-secondary);
padding: var(--space-sm);
border-radius: var(--border-radius-sm);
color: var(--color-text-light);
}
.btn-secondary:hover {
background-color: #5a6268;
}
/* SUCCESS/DANGER BUTTONS */
.btn-success {
background-color: var(--color-success);
color: var(--color-text-light);
}
.btn-danger {
background-color: var(--color-danger);
color: var(--color-text-light);
}
/* HOVER EFFECTS */
.highlight-hover:hover {
background-color: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(8px);
transition: all var(--transition-speed) ease;
}
Whitespace-only changes.
+151
View File
@@ -0,0 +1,151 @@
/* DEFAULTS */
body {
background-color: var(--color-bg-light);
}
.w33 {
width: 33%;
}
/* SIDE BAR */
.sidebar {
position: fixed;
top: 0;
left: 0;
height: 100vh;
width: 250px;
background-color: var(--color-bg-sidebar);
padding: 0 var(--space-md);
overflow-y: auto;
overflow-x: hidden;
z-index: 1000;
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
}
.sidebar-brand {
text-align: center;
padding: var(--space-md);
color: var(--color-text-light);
font-weight: bold;
font-size: var(--font-lg);
display: block;
margin-bottom: var(--space-sm);
}
.sidebar-menu {
margin: var(--space-sm) 0;
}
.sidebar-header {
color: var(--color-text-light);
font-size: var(--font-sm);
margin-top: var(--space-xs);
padding: var(--space-xs) 0;
text-transform: uppercase;
opacity: 0.6;
}
.sidebar-link {
color: var(--color-text-light);
padding: var(--space-xs) var(--space-xs);
border-radius: var(--border-radius-sm);
width: 100%;
display: block;
opacity: 1;
transition: opacity var(--transition-speed-long) cubic-bezier(0.2, 0.85, 0.75, 1.7), transform var(--transition-speed) ease;
}
.active {
background-color: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(8px);
box-shadow: inset -1px 0 0 0 rgba(255, 255, 255, 0.2), inset 0 -1px 0 0 rgba(255, 255, 255, 0.2);
}
/* SIDEBAR TOGGLING */
body.sidebar-collapsed .sidebar {
transform: translateX(-250px);
}
body.sidebar-collapsed .sidebar-link {
opacity: -20;
transform: translateX(260px);
}
body.sidebar-collapsed .navbar,
body.sidebar-collapsed .main-content {
transform: translateX(0);
width: 100%;
}
.sidebar,
.navbar,
.main-content {
transition: all var(--transition-speed) ease;
}
/* NAV BAR */
.navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
transform: translateX(250px);
height: var(--header-height);
width: calc(100% - 250px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 var(--space-md);
z-index: 900;
}
#sidebar-toggle {
cursor: pointer;
}
/* Inicio do css do seletor antigo */
.navbar-farm-selector {
display: flex;
flex-direction: column;
align-items: center;
cursor: default;
opacity: 0.75;
transition: all var(--transition-speed) ease;
}
.navbar-farm-selector:hover {
opacity: 1;
}
.farm-name {
font-size: var(--font-md);
font-weight: 500;
margin: 0;
white-space: nowrap;
}
/* Fim do css do seletor antigo */
.navbar-user-info {
display: flex;
justify-content: flex-end;
gap: var(--space-md);
}
.username {
cursor: default;
}
/* MAIN CONTENT */
.main-content {
position: absolute;
top: var(--header-height);
left: 0;
bottom: 0;
right: 0;
transform: translateX(250px);
padding: var(--space-lg);
width: calc(100% - 250px);
overflow-y: auto;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+19
View File
@@ -0,0 +1,19 @@
// SIDEBAR TOGGLER
document.addEventListener("DOMContentLoaded", () => {
const button = document.getElementById("sidebar-toggle");
const body = document.body;
if (!button) return;
const savedState = localStorage.getItem("sidebarCollapsed");
if (savedState === "true") {
body.classList.add("sidebar-collapsed");
}
button.addEventListener("click", () => {
body.classList.toggle("sidebar-collapsed");
const isCollapsed = body.classList.contains("sidebar-collapsed");
localStorage.setItem("sidebarCollapsed", isCollapsed);
});
});
BIN
View File
Binary file not shown.