Initial commit
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import DocumentCategory, Document
|
||||
|
||||
|
||||
@admin.register(DocumentCategory)
|
||||
class DocumentCategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ("name",)
|
||||
search_fields = ("name",)
|
||||
|
||||
@admin.register(Document)
|
||||
class DocumentAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
"title",
|
||||
"project",
|
||||
"category",
|
||||
"uploaded_by",
|
||||
"created_at",
|
||||
)
|
||||
|
||||
list_filter = (
|
||||
"project",
|
||||
"category",
|
||||
"created_at",
|
||||
)
|
||||
|
||||
search_fields = (
|
||||
"title",
|
||||
)
|
||||
|
||||
readonly_fields = (
|
||||
"created_at",
|
||||
)
|
||||
|
||||
autocomplete_fields = (
|
||||
"project",
|
||||
"category",
|
||||
"uploaded_by",
|
||||
)
|
||||
|
||||
ordering = (
|
||||
"-created_at",
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class DocumentsConfig(AppConfig):
|
||||
name = 'documents'
|
||||
@@ -0,0 +1,11 @@
|
||||
from .models import Document
|
||||
from core.forms.base import BaseForm
|
||||
|
||||
class DocumentForm(BaseForm):
|
||||
class Meta:
|
||||
model = Document
|
||||
fields = ["title", "category", "file"]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.apply_style()
|
||||
@@ -0,0 +1,36 @@
|
||||
# Generated by Django 6.0.6 on 2026-06-23 22:12
|
||||
|
||||
import documents.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Document',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=255, verbose_name='Name')),
|
||||
('file', models.FileField(upload_to=documents.models.document_upload_path, verbose_name='Datei')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DocumentCategory',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=120, unique=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Dokumentenkategorie',
|
||||
'verbose_name_plural': 'Dokumentenkategorien',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 6.0.6 on 2026-06-23 22:12
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('documents', '0001_initial'),
|
||||
('projects', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='document',
|
||||
name='project',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='projects.project'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='document',
|
||||
name='uploaded_by',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='document',
|
||||
name='category',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='documents.documentcategory', verbose_name='Kategorie'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,32 @@
|
||||
from django.db import models
|
||||
from projects.models import Project
|
||||
import os
|
||||
|
||||
def document_upload_path(instance, filename):
|
||||
return f"projects/{instance.project.id}/documents/{filename}"
|
||||
|
||||
class DocumentCategory(models.Model):
|
||||
name = models.CharField(max_length=120, unique=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Dokumentenkategorie"
|
||||
verbose_name_plural = "Dokumentenkategorien"
|
||||
ordering = ["name"]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Document(models.Model):
|
||||
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="documents")
|
||||
category = models.ForeignKey(DocumentCategory, on_delete=models.CASCADE, verbose_name="Kategorie")
|
||||
title = models.CharField(max_length=255, verbose_name="Name")
|
||||
file = models.FileField(upload_to=document_upload_path, verbose_name="Datei")
|
||||
uploaded_by = models.ForeignKey("auth.User", on_delete=models.SET_NULL, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
@property
|
||||
def extension(self):
|
||||
return os.path.splitext(self.file.name)[1].lower()
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
@@ -0,0 +1,12 @@
|
||||
from masterdata.registry import register_setting
|
||||
|
||||
from .models import (
|
||||
DocumentCategory,
|
||||
)
|
||||
|
||||
|
||||
register_setting(
|
||||
group="Dokumente",
|
||||
name="Kategorien",
|
||||
model=DocumentCategory,
|
||||
)
|
||||
@@ -0,0 +1,36 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{{ project.name }} - Dokumente
|
||||
{% endblock %}
|
||||
|
||||
{% block header_title %}
|
||||
Dokumente hochladen
|
||||
{% endblock %}
|
||||
|
||||
{% block header_description %}
|
||||
Dokumente und Belege hochladen
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
{{ form.as_p }}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="btn mt-4 btn-save">
|
||||
Speichern
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,111 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{{ project.name }} - Dokumente
|
||||
{% endblock %}
|
||||
|
||||
{% block header_title %}
|
||||
Dokumente
|
||||
{% endblock %}
|
||||
|
||||
{% block header_description %}
|
||||
Dateien & Bilder
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mb-8">
|
||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold">Aktionen</h2>
|
||||
<p class="text-slate-400 text-sm mt-1">Dokumente hochladen</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-2">
|
||||
<a href="{% url 'documents:add' project.id %}" class="btn btn-add">+ Dokument</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Kategorien -->
|
||||
{% for cat, docs in grouped.items %}
|
||||
|
||||
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors mb-2" >
|
||||
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h2 class="text-xl font-bold">
|
||||
{{ cat.name }}
|
||||
</h2>
|
||||
|
||||
<span class="text-sm text-slate-400">
|
||||
{{ docs|length }} Dokumente
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% if docs %}
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
|
||||
{% for d in docs %}
|
||||
|
||||
<div class="bg-slate-100 dark:bg-slate-700 rounded-xl shadow overflow-hidden transition">
|
||||
|
||||
<!-- Vorschau klickbar -->
|
||||
<a href="{{ d.file.url }}" target="_blank">
|
||||
|
||||
{% if d.extension == ".jpg" or d.extension == ".jpeg" or d.extension == ".png" or d.extension == ".webp" %}
|
||||
|
||||
<div class="h-64 bg-slate-100 flex items-center justify-center overflow-hidden">
|
||||
<img src="{{ d.file.url }}" class="w-full h-full object-cover">
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
|
||||
<div class="h-64 flex flex-col items-center justify-center bg-slate-100">
|
||||
<div class="text-5xl mb-2">📄</div>
|
||||
<div class="text-slate-400 text-sm">Keine Vorschau verfügbar</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
</a>
|
||||
|
||||
<!-- Infos -->
|
||||
<div class="p-4">
|
||||
<h3 class="font-semibold truncate">{{ d.title }}</h3>
|
||||
<div class="mt-2 text-sm text-slate-400">Hochgeladen von {{ d.uploaded_by }}</div>
|
||||
<div class="text-sm text-slate-400">{{ d.created_at|date:"d.m.Y H:i" }}</div>
|
||||
<div class="mt-4 flex">
|
||||
<form method="post" action="{% url 'documents:delete' project.id d.id %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-delete">Löschen</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<div class="bg-white dark:bg-slate-800 border border-orange-500 rounded-xl p-6 text-orange-500">
|
||||
Keine Dokumente vorhanden.
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{% empty %}
|
||||
<div class="bg-white dark:bg-slate-800 border border-orange-500 rounded-xl p-6 text-orange-500">
|
||||
Noch keine Kategorien vorhanden.
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -0,0 +1,10 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = "documents"
|
||||
|
||||
urlpatterns = [
|
||||
path("<int:project_id>/documents/", views.document_overview, name="overview"),
|
||||
path("<int:project_id>/documents/delete/<int:doc_id>/", views.document_delete, name="delete"),
|
||||
path("<int:project_id>/documents/add/", views.document_add, name="add"),
|
||||
]
|
||||
@@ -0,0 +1,88 @@
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from accounts.decorators import permission_required
|
||||
from projects.models import ProjectMembership
|
||||
from .models import Document, DocumentCategory
|
||||
from .forms import DocumentForm
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("projects_documents_view")
|
||||
def document_overview(request, project_id):
|
||||
|
||||
membership = get_object_or_404(ProjectMembership, user=request.user, project_id=project_id)
|
||||
project = membership.project
|
||||
|
||||
documents = Document.objects.filter(project=project).order_by("-created_at")
|
||||
categories = DocumentCategory.objects.all()
|
||||
|
||||
form = DocumentForm()
|
||||
|
||||
if request.method == "POST":
|
||||
form = DocumentForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
doc = form.save(commit=False)
|
||||
doc.project = project
|
||||
doc.uploaded_by = request.user
|
||||
doc.save()
|
||||
return redirect("documents:overview", project.id)
|
||||
|
||||
# gruppiert nach Kategorie
|
||||
grouped = {}
|
||||
|
||||
for cat in categories:
|
||||
docs = list(documents.filter(category=cat))
|
||||
|
||||
if docs:
|
||||
grouped[cat] = docs
|
||||
|
||||
uncategorized = documents.filter(category__isnull=True)
|
||||
|
||||
return render(request, "documents/overview.html", {
|
||||
"project": project,
|
||||
"form": form,
|
||||
"grouped": grouped,
|
||||
"uncategorized": uncategorized,
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("projects_documents_delete")
|
||||
def document_delete(request, project_id, doc_id):
|
||||
membership = get_object_or_404(ProjectMembership, user=request.user, project_id=project_id)
|
||||
doc = get_object_or_404(Document, id=doc_id, project=membership.project)
|
||||
doc.file.delete()
|
||||
doc.delete()
|
||||
return redirect("documents:overview", project_id)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("projects_documents_add")
|
||||
def document_add(request, project_id):
|
||||
membership = get_object_or_404(ProjectMembership, user=request.user, project_id=project_id)
|
||||
project = membership.project
|
||||
|
||||
documents = Document.objects.filter(project=project).order_by("-created_at")
|
||||
categories = DocumentCategory.objects.all()
|
||||
|
||||
form = DocumentForm()
|
||||
|
||||
if request.method == "POST":
|
||||
form = DocumentForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
doc = form.save(commit=False)
|
||||
doc.project = project
|
||||
doc.uploaded_by = request.user
|
||||
doc.save()
|
||||
return redirect("documents:overview", project.id)
|
||||
else:
|
||||
form = DocumentForm()
|
||||
|
||||
return render(
|
||||
request,
|
||||
"documents/add.html",
|
||||
{
|
||||
"project": project,
|
||||
"form": form,
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user