1.1
This commit is contained in:
parent
92ec45d230
commit
873880f7d1
Binary file not shown.
Binary file not shown.
@ -141,6 +141,9 @@ USE_TZ = True
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||
|
||||
MEDIA_URL = '/media/'
|
||||
MEDIA_ROOT = BASE_DIR / 'media'
|
||||
|
||||
|
||||
STATICFILES_DIRS = [
|
||||
BASE_DIR / 'static',
|
||||
|
||||
@ -16,8 +16,13 @@ Including another URLconf
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("", include("core.urls")),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,6 +1,6 @@
|
||||
from django import forms
|
||||
from django.contrib.auth.forms import UserCreationForm
|
||||
from .models import Company, Tender, Bid
|
||||
from .models import Company, Tender, Bid, Document, Note, Approval
|
||||
|
||||
class SignUpForm(UserCreationForm):
|
||||
class Meta(UserCreationForm.Meta):
|
||||
@ -20,3 +20,18 @@ class BidForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Bid
|
||||
fields = ['amount']
|
||||
|
||||
class DocumentForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Document
|
||||
fields = ['file', 'description']
|
||||
|
||||
class NoteForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Note
|
||||
fields = ['note']
|
||||
|
||||
class ApprovalForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Approval
|
||||
fields = ['status', 'comments']
|
||||
|
||||
18
core/migrations/0002_document_description.py
Normal file
18
core/migrations/0002_document_description.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-15 18:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='document',
|
||||
name='description',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
]
|
||||
21
core/migrations/0003_document_uploaded_by.py
Normal file
21
core/migrations/0003_document_uploaded_by.py
Normal file
@ -0,0 +1,21 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-15 18:58
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0002_document_description'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='document',
|
||||
name='uploaded_by',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
@ -46,7 +46,9 @@ class Bid(models.Model):
|
||||
class Document(models.Model):
|
||||
tender = models.ForeignKey(Tender, on_delete=models.CASCADE, null=True, blank=True)
|
||||
bid = models.ForeignKey(Bid, on_delete=models.CASCADE, null=True, blank=True)
|
||||
uploaded_by = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
|
||||
file = models.FileField(upload_to='documents/')
|
||||
description = models.TextField(blank=True)
|
||||
uploaded_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
|
||||
24
core/templates/core/approve_bid.html
Normal file
24
core/templates/core/approve_bid.html
Normal file
@ -0,0 +1,24 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h1 class="card-title">Approve Bid</h1>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><strong>Tender:</strong> {{ bid.tender.title }}</p>
|
||||
<p><strong>Company:</strong> {{ bid.company.name }}</p>
|
||||
<p><strong>Amount:</strong> ${{ bid.amount }}</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit" class="btn btn-primary">Submit Approval</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<a href="{% url 'tender_detail' bid.tender.id %}" class="btn btn-secondary">Back to Tender</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
19
core/templates/core/delete_bid.html
Normal file
19
core/templates/core/delete_bid.html
Normal file
@ -0,0 +1,19 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h1 class="card-title">Delete Bid</h1>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Are you sure you want to delete this bid of ${{ bid.amount }}?</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger">Delete</button>
|
||||
<a href="{% url 'tender_detail' bid.tender.id %}" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
19
core/templates/core/delete_document.html
Normal file
19
core/templates/core/delete_document.html
Normal file
@ -0,0 +1,19 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h1 class="card-title">Delete Document</h1>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Are you sure you want to delete the document "{{ document.file.name }}"?</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger">Delete</button>
|
||||
<a href="{% url 'tender_detail' document.tender.id %}" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
19
core/templates/core/delete_tender.html
Normal file
19
core/templates/core/delete_tender.html
Normal file
@ -0,0 +1,19 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h1 class="card-title">Delete Tender</h1>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Are you sure you want to delete the tender "{{ tender.title }}"?</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger">Delete</button>
|
||||
<a href="{% url 'tender_detail' tender.id %}" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -14,10 +14,12 @@
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<a href="{% url 'tender_list' tender.company.id %}" class="btn btn-secondary">Back to Tenders</a>
|
||||
<a href="{% url 'update_tender' tender.id %}" class="btn btn-primary">Update</a>
|
||||
<a href="{% url 'delete_tender' tender.id %}" class="btn btn-danger">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h2 class="h4">Bids</h2>
|
||||
<a href="{% url 'create_bid' tender.id %}" class="btn btn-primary">Create Bid</a>
|
||||
@ -27,7 +29,23 @@
|
||||
<ul class="list-group">
|
||||
{% for bid in bids %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<span>{{ bid.company.name }} - ${{ bid.amount }}</span>
|
||||
<div>
|
||||
<span>{{ bid.company.name }} - ${{ bid.amount }}</span>
|
||||
{% with approval=bid.approval_set.first %}
|
||||
{% if approval %}
|
||||
<span class="badge bg-{% if approval.status == 'approved' %}success{% elif approval.status == 'rejected' %}danger{% else %}secondary{% endif %}">{{ approval.get_status_display }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Pending</span>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'approve_bid' bid.id %}" class="btn btn-sm btn-outline-primary">Review</a>
|
||||
{% if request.user.membership_set.first.company == bid.company %}
|
||||
<a href="{% url 'update_bid' bid.id %}" class="btn btn-sm btn-outline-secondary">Update</a>
|
||||
<a href="{% url 'delete_bid' bid.id %}" class="btn btn-sm btn-outline-danger">Delete</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
@ -36,5 +54,57 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h2 class="h4">Documents</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" enctype="multipart/form-data" class="mb-4">
|
||||
{% csrf_token %}
|
||||
{{ doc_form.as_p }}
|
||||
<button type="submit" name="submit_document" class="btn btn-primary">Upload Document</button>
|
||||
</form>
|
||||
|
||||
{% if documents %}
|
||||
<ul class="list-group">
|
||||
{% for doc in documents %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<a href="{{ doc.file.url }}" target="_blank">{{ doc.file.name }}</a>
|
||||
<small class="text-muted">by {{ doc.uploaded_by.username }} on {{ doc.uploaded_at|date:"Y-m-d" }}</small>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>No documents yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="h4">Notes</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" class="mb-4">
|
||||
{% csrf_token %}
|
||||
{{ note_form.as_p }}
|
||||
<button type="submit" name="submit_note" class="btn btn-primary">Add Note</button>
|
||||
</form>
|
||||
|
||||
{% if notes %}
|
||||
<ul class="list-group">
|
||||
{% for note in notes %}
|
||||
<li class="list-group-item">
|
||||
<p class="mb-1">{{ note.note }}</p>
|
||||
<small class="text-muted">by {{ note.user.username }} on {{ note.created_at|date:"Y-m-d" }}</small>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>No notes yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@ -10,8 +10,12 @@
|
||||
{% if tenders %}
|
||||
<ul class="list-group">
|
||||
{% for tender in tenders %}
|
||||
<li class="list-group-item">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<a href="{% url 'tender_detail' tender.id %}">{{ tender.title }}</a>
|
||||
<div>
|
||||
<a href="{% url 'update_tender' tender.id %}" class="btn btn-sm btn-outline-primary">Update</a>
|
||||
<a href="{% url 'delete_tender' tender.id %}" class="btn btn-sm btn-outline-danger">Delete</a>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
21
core/templates/core/update_bid.html
Normal file
21
core/templates/core/update_bid.html
Normal file
@ -0,0 +1,21 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h1 class="card-title">Update Bid</h1>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit" class="btn btn-primary">Update Bid</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<a href="{% url 'tender_detail' bid.tender.id %}" class="btn btn-secondary">Back to Tender</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
21
core/templates/core/update_tender.html
Normal file
21
core/templates/core/update_tender.html
Normal file
@ -0,0 +1,21 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h1 class="card-title">Update Tender</h1>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit" class="btn btn-primary">Update Tender</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<a href="{% url 'tender_detail' tender.id %}" class="btn btn-secondary">Back to Tender</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -11,6 +11,11 @@ urlpatterns = [
|
||||
path('company/<int:company_id>/tenders/', views.tender_list, name='tender_list'),
|
||||
path('tender/<int:tender_id>/', views.tender_detail, name='tender_detail'),
|
||||
path('company/<int:company_id>/create_tender/', views.create_tender, name='create_tender'),
|
||||
path('tender/<int:tender_id>/update/', views.update_tender, name='update_tender'),
|
||||
path('tender/<int:tender_id>/delete/', views.delete_tender, name='delete_tender'),
|
||||
path('tender/<int:tender_id>/create_bid/', views.create_bid, name='create_bid'),
|
||||
path('bid/<int:bid_id>/update/', views.update_bid, name='update_bid'),
|
||||
path('bid/<int:bid_id>/delete/', views.delete_bid, name='delete_bid'),
|
||||
path('bid/<int:bid_id>/approve/', views.approve_bid, name='approve_bid'),
|
||||
path('', views.home, name='home'),
|
||||
]
|
||||
121
core/views.py
121
core/views.py
@ -1,8 +1,8 @@
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.contrib.auth import login, authenticate, logout
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from .forms import SignUpForm, CompanyForm, TenderForm, BidForm
|
||||
from .models import Company, Membership, Tender, Bid
|
||||
from .forms import SignUpForm, CompanyForm, TenderForm, BidForm, DocumentForm, NoteForm, ApprovalForm
|
||||
from .models import Company, Membership, Tender, Bid, Document, Note, Approval
|
||||
|
||||
def home(request):
|
||||
return render(request, "core/index.html")
|
||||
@ -57,9 +57,37 @@ def tender_list(request, company_id):
|
||||
def tender_detail(request, tender_id):
|
||||
tender = get_object_or_404(Tender, pk=tender_id)
|
||||
bids = Bid.objects.filter(tender=tender)
|
||||
documents = Document.objects.filter(tender=tender)
|
||||
notes = Note.objects.filter(tender=tender)
|
||||
|
||||
if request.method == 'POST':
|
||||
if 'submit_document' in request.POST:
|
||||
doc_form = DocumentForm(request.POST, request.FILES)
|
||||
if doc_form.is_valid():
|
||||
document = doc_form.save(commit=False)
|
||||
document.tender = tender
|
||||
document.uploaded_by = request.user
|
||||
document.save()
|
||||
return redirect('tender_detail', tender_id=tender.id)
|
||||
elif 'submit_note' in request.POST:
|
||||
note_form = NoteForm(request.POST)
|
||||
if note_form.is_valid():
|
||||
note = note_form.save(commit=False)
|
||||
note.tender = tender
|
||||
note.user = request.user
|
||||
note.save()
|
||||
return redirect('tender_detail', tender_id=tender.id)
|
||||
|
||||
doc_form = DocumentForm()
|
||||
note_form = NoteForm()
|
||||
|
||||
context = {
|
||||
'tender': tender,
|
||||
'bids': bids
|
||||
'bids': bids,
|
||||
'documents': documents,
|
||||
'notes': notes,
|
||||
'doc_form': doc_form,
|
||||
'note_form': note_form
|
||||
}
|
||||
return render(request, 'core/tender_detail.html', context)
|
||||
|
||||
@ -81,6 +109,34 @@ def create_tender(request, company_id):
|
||||
}
|
||||
return render(request, 'core/create_tender.html', context)
|
||||
|
||||
@login_required
|
||||
def update_tender(request, tender_id):
|
||||
tender = get_object_or_404(Tender, pk=tender_id)
|
||||
if request.method == 'POST':
|
||||
form = TenderForm(request.POST, instance=tender)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect('tender_detail', tender_id=tender.id)
|
||||
else:
|
||||
form = TenderForm(instance=tender)
|
||||
context = {
|
||||
'form': form,
|
||||
'tender': tender
|
||||
}
|
||||
return render(request, 'core/update_tender.html', context)
|
||||
|
||||
@login_required
|
||||
def delete_tender(request, tender_id):
|
||||
tender = get_object_or_404(Tender, pk=tender_id)
|
||||
if request.method == 'POST':
|
||||
company_id = tender.company.id
|
||||
tender.delete()
|
||||
return redirect('tender_list', company_id=company_id)
|
||||
context = {
|
||||
'tender': tender
|
||||
}
|
||||
return render(request, 'core/delete_tender.html', context)
|
||||
|
||||
@login_required
|
||||
def create_bid(request, tender_id):
|
||||
tender = get_object_or_404(Tender, pk=tender_id)
|
||||
@ -108,3 +164,62 @@ def create_bid(request, tender_id):
|
||||
}
|
||||
return render(request, 'core/create_bid.html', context)
|
||||
|
||||
@login_required
|
||||
def update_bid(request, bid_id):
|
||||
bid = get_object_or_404(Bid, pk=bid_id)
|
||||
if request.method == 'POST':
|
||||
form = BidForm(request.POST, instance=bid)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect('tender_detail', tender_id=bid.tender.id)
|
||||
else:
|
||||
form = BidForm(instance=bid)
|
||||
context = {
|
||||
'form': form,
|
||||
'bid': bid
|
||||
}
|
||||
return render(request, 'core/update_bid.html', context)
|
||||
|
||||
@login_required
|
||||
def delete_bid(request, bid_id):
|
||||
bid = get_object_or_404(Bid, pk=bid_id)
|
||||
if request.method == 'POST':
|
||||
tender_id = bid.tender.id
|
||||
bid.delete()
|
||||
return redirect('tender_detail', tender_id=tender_id)
|
||||
context = {
|
||||
'bid': bid
|
||||
}
|
||||
return render(request, 'core/delete_bid.html', context)
|
||||
|
||||
@login_required
|
||||
def approve_bid(request, bid_id):
|
||||
bid = get_object_or_404(Bid, pk=bid_id)
|
||||
approval, created = Approval.objects.get_or_create(bid=bid, approver=request.user)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ApprovalForm(request.POST, instance=approval)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect('tender_detail', tender_id=bid.tender.id)
|
||||
else:
|
||||
form = ApprovalForm(instance=approval)
|
||||
|
||||
context = {
|
||||
'form': form,
|
||||
'bid': bid
|
||||
}
|
||||
return render(request, 'core/approve_bid.html', context)
|
||||
|
||||
@login_required
|
||||
def delete_document(request, document_id):
|
||||
document = get_object_or_404(Document, pk=document_id)
|
||||
if request.method == 'POST':
|
||||
tender_id = document.tender.id
|
||||
document.delete()
|
||||
return redirect('tender_detail', tender_id=tender_id)
|
||||
context = {
|
||||
'document': document
|
||||
}
|
||||
return render(request, 'core/delete_document.html', context)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user