Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions docker-site/main/admin.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from django.contrib import admin
from .models import UserProfile, Category, Post, Project, PublicationIRIS, UserProfilePublicationIRIS, IRISImportLog, MeetingRoom, RoomReservation, ShortLink, HistoryMilestone, ResearchArea, DashboardCard, ExternalRedirects, WikiPage, WikiPageVersion, WikiPageChangeRequest, WikiImage
from .models import UserProfile, Category, Post, PostAttachment, Project, PublicationIRIS, UserProfilePublicationIRIS, IRISImportLog, MeetingRoom, RoomReservation, ShortLink, HistoryMilestone, ResearchArea, DashboardCard, ExternalRedirects, WikiPage, WikiPageVersion, WikiPageChangeRequest, WikiImage

admin.site.site_header = "AImageLab Admin"
admin.site.site_title = "AImageLab Admin"
Expand Down Expand Up @@ -44,7 +44,15 @@ class CategoryAdmin(admin.ModelAdmin):
pass

class PostAdmin(admin.ModelAdmin):
pass
search_fields = ('title', 'slug', 'description', 'content')


@admin.register(PostAttachment)
class PostAttachmentAdmin(admin.ModelAdmin):
list_display = ('name', 'post', 'uploaded_at')
list_filter = ('uploaded_at',)
search_fields = ('name', 'post__title')
autocomplete_fields = ['post']

class ProjectAdmin(admin.ModelAdmin):
list_display = ('title', 'name', 'project_type', 'start_date', 'end_date')
Expand Down
27 changes: 27 additions & 0 deletions docker-site/main/migrations/0013_postattachment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 5.2.11 on 2026-05-02 00:00

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('main', '0012_externalredirects'),
]

operations = [
migrations.CreateModel(
name='PostAttachment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Display name for this attachment', max_length=200)),
('file', models.FileField(help_text='Attachment file', upload_to='news_attachments/%Y/%m/')),
('uploaded_at', models.DateTimeField(auto_now_add=True)),
('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='main.post')),
],
options={
'ordering': ['name', '-uploaded_at'],
},
),
]
15 changes: 15 additions & 0 deletions docker-site/main/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,21 @@ def __str__(self):
return self.title


class PostAttachment(models.Model):
"""Named file attachment for a news post."""

post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='attachments')
name = models.CharField(max_length=200, help_text="Display name for this attachment")
file = models.FileField(upload_to='news_attachments/%Y/%m/', help_text="Attachment file")
uploaded_at = models.DateTimeField(auto_now_add=True)

class Meta:
ordering = ['name', '-uploaded_at']

def __str__(self):
return f"{self.name} ({self.post.title})"


class ShortLink(models.Model):
"""Short URL redirect model (Go links)"""

Expand Down
6 changes: 3 additions & 3 deletions docker-site/main/templates/main/contacts.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ <h2 class="h2-home mb-2">Connect with Us</h2>
</div>
<div class="flex gap-4 mb-8">
<a href="https://www.linkedin.com/company/aimagelab" target="_blank" aria-label="LinkedIn" class="btn btn-primary">
<i data-lucide="linkedin" class="w-6 h-6"></i> LinkedIn
<svg class="w-5 h-5 mr-1" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 0 1-2.063-2.065 2.064 2.064 0 1 1 2.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg> LinkedIn
</a>
<a href="https://github.com/aimagelab" target="_blank" aria-label="GitHub" class="btn btn-primary">
<i data-lucide="github" class="w-6 h-6"></i> GitHub
<svg class="w-5 h-5 mr-1" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg> GitHub
</a>
<a href="https://www.youtube.com/@aimagelab" target="_blank" aria-label="YouTube" class="btn btn-primary">
<i data-lucide="youtube" class="w-6 h-6"></i> YouTube
<svg class="w-5 h-5 mr-1" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg> YouTube
</a>
</div>

Expand Down
191 changes: 191 additions & 0 deletions docker-site/main/templates/main/post_edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,33 @@ <h3 class="font-bold text-lg" id="modal-title">
</form>
</dialog>

<dialog id="attachment-modal" class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg">Add Attachment</h3>
<p class="py-2 text-sm">Choose a file and assign a display name for this news attachment.</p>

<fieldset class="fieldset mb-3">
<legend class="fieldset-legend form-legend">Attachment Name *</legend>
<input type="text" id="attachment-name" class="input w-full" maxlength="200" placeholder="Example: Slides PDF" required>
</fieldset>

<fieldset class="fieldset mb-2">
<legend class="fieldset-legend form-legend">File *</legend>
<input type="file" id="attachment-file" class="file-input w-full" required>
</fieldset>

<div class="modal-action">
<button type="button" id="add-attachment-btn" class="btn btn-primary">Add Attachment</button>
<form method="dialog">
<button class="btn btn-ghost">Cancel</button>
</form>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>

<form id="post-form" method="post" enctype="multipart/form-data">
{% csrf_token %}

Expand Down Expand Up @@ -175,6 +202,45 @@ <h2 class="card-title text-primary mb-4">
</div>
</div>


<!-- Attachments Card -->
<div class="card bg-base-200">
<div class="card-body">
<h2 class="card-title text-primary mb-4">
<i data-lucide="paperclip" class="w-5 h-5"></i>
Attachments
</h2>

<button type="button" id="open-attachment-modal" class="btn btn-outline btn-sm w-full mb-3">
<i data-lucide="plus" class="w-4 h-4"></i>
Add Attachment
</button>
<p class="label form-copy">Each attachment requires a name and file.</p>

{% with post.attachments.all as current_attachments %}
{% if post and current_attachments %}
<div class="mt-4">
<p class="text-sm font-semibold mb-2">Current Attachments:</p>
<div class="space-y-2">
{% for attachment in current_attachments %}
<label class="label cursor-pointer justify-start gap-3 border border-base-300 rounded-md px-3 py-2">
<input class="checkbox checkbox-primary flex-shrink-0" type="checkbox" name="remove_attachment_ids" value="{{ attachment.id }}" form="post-form">
<span class="label-text text-sm break-words">Remove {{ attachment.name }}</span>
</label>
{% endfor %}
</div>
</div>
{% endif %}
{% endwith %}

<div class="mt-4">
<p class="text-sm font-semibold mb-2">New Attachments (pending save):</p>
<div id="pending-attachments-list" class="space-y-2">
<p id="pending-attachments-empty" class="text-xs opacity-70">No new attachments added yet.</p>
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="card bg-base-200 mt-6">
<div class="card-body">
Expand Down Expand Up @@ -228,6 +294,8 @@ <h2 class="card-title text-primary mb-4">
</div>
</div>
</div>

<div id="attachment-hidden-inputs"></div>
</form>
{% endblock %}

Expand Down Expand Up @@ -462,6 +530,16 @@ <h2 class="card-title text-primary mb-4">
const modalMessage = document.getElementById('modal-message');
const modalCloseBtn = document.getElementById('modal-close-btn');

const attachmentModal = document.getElementById('attachment-modal');
const openAttachmentModalBtn = document.getElementById('open-attachment-modal');
const addAttachmentBtn = document.getElementById('add-attachment-btn');
const attachmentNameInput = document.getElementById('attachment-name');
let attachmentFileInput = document.getElementById('attachment-file');
const pendingAttachmentsList = document.getElementById('pending-attachments-list');
const pendingAttachmentsEmpty = document.getElementById('pending-attachments-empty');
const attachmentHiddenInputs = document.getElementById('attachment-hidden-inputs');
let stagedAttachmentIndex = 0;

function showModal(title, message, showClose = false) {
modalTitle.innerHTML = title;
modalMessage.innerHTML = message;
Expand All @@ -475,6 +553,119 @@ <h2 class="card-title text-primary mb-4">

modalCloseBtn.addEventListener('click', hideModal);

if (openAttachmentModalBtn && attachmentModal) {
openAttachmentModalBtn.addEventListener('click', function() {
attachmentModal.showModal();
});
}

if (addAttachmentBtn) {
addAttachmentBtn.addEventListener('click', function() {
const attachmentName = attachmentNameInput.value.trim();
const attachmentFile = attachmentFileInput.files[0];

if (!attachmentName || !attachmentFile) {
showModal(
'<i data-lucide="alert-circle" class="w-5 h-5 inline text-warning"></i> Attachment Required',
'Please provide both attachment name and file.',
true
);
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
return;
}

const wrapper = document.createElement('div');
const attachmentIndex = stagedAttachmentIndex;
wrapper.dataset.attachmentIndex = attachmentIndex;
wrapper.style.display = 'none';

const hiddenNameInput = document.createElement('input');
hiddenNameInput.type = 'hidden';
hiddenNameInput.name = 'new_attachment_names';
hiddenNameInput.value = attachmentName;

// Move the actual file input (which already holds the selected file) into the
// hidden wrapper so the browser submits it reliably with the form.
// DataTransfer.files assignment is not reliable across all browsers.
const fileInputParent = attachmentFileInput.parentNode;
const fileInputClass = attachmentFileInput.className;
attachmentFileInput.name = 'new_attachments';
wrapper.appendChild(hiddenNameInput);
wrapper.appendChild(attachmentFileInput);
attachmentHiddenInputs.appendChild(wrapper);

// Replace the moved input with a fresh one for the next attachment.
const newFileInput = document.createElement('input');
newFileInput.type = 'file';
newFileInput.id = 'attachment-file';
newFileInput.className = fileInputClass;
newFileInput.required = true;
fileInputParent.appendChild(newFileInput);
attachmentFileInput = newFileInput;

// Build the pending list item using DOM methods to prevent XSS.
const pendingItem = document.createElement('div');
pendingItem.className = 'flex items-center justify-between gap-2 border border-base-300 rounded-md px-3 py-2';
pendingItem.dataset.attachmentIndex = attachmentIndex;

const contentDiv = document.createElement('div');
contentDiv.className = 'min-w-0';

const namePara = document.createElement('p');
namePara.className = 'text-sm font-medium break-words';
namePara.textContent = attachmentName;

const filePara = document.createElement('p');
filePara.className = 'text-xs opacity-70 break-all';
filePara.textContent = attachmentFile.name;

contentDiv.appendChild(namePara);
contentDiv.appendChild(filePara);

const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'btn btn-ghost btn-xs';
removeBtn.dataset.removeStaged = attachmentIndex;
removeBtn.textContent = 'Remove';

pendingItem.appendChild(contentDiv);
pendingItem.appendChild(removeBtn);
pendingAttachmentsList.appendChild(pendingItem);
pendingAttachmentsEmpty.style.display = 'none';

attachmentNameInput.value = '';
attachmentFileInput.value = '';
attachmentModal.close();
stagedAttachmentIndex += 1;
});
}

if (pendingAttachmentsList) {
pendingAttachmentsList.addEventListener('click', function(event) {
const removeBtn = event.target.closest('[data-remove-staged]');
if (!removeBtn) {
return;
}

const targetIndex = removeBtn.getAttribute('data-remove-staged');
const hiddenWrapper = attachmentHiddenInputs.querySelector(`[data-attachment-index="${targetIndex}"]`);
const listItem = pendingAttachmentsList.querySelector(`[data-attachment-index="${targetIndex}"]`);

if (hiddenWrapper) {
hiddenWrapper.remove();
}
if (listItem) {
listItem.remove();
}

if (!pendingAttachmentsList.querySelector('[data-attachment-index]')) {
pendingAttachmentsEmpty.style.display = 'block';
}
});
}

const uploadBtn = document.getElementById('upload-image-btn');
const fileInput = document.getElementById('post-image-upload');

Expand Down
14 changes: 14 additions & 0 deletions docker-site/main/templates/main/single.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,20 @@
<div class="text max-w-none mb-6 space-y-3">
{{ post.content|markdownify|linebreaks }}
</div>

{% if attachments %}
<div class="mb-6">
<h3 class="text-lg font-semibold mb-3">Attachments</h3>
<div class="space-y-2">
{% for attachment in attachments %}
<a href="{{ attachment.file.url }}" class="btn btn-outline btn-sm w-full justify-start" target="_blank" rel="noopener">
<i data-lucide="paperclip" class="w-4 h-4"></i>
{{ attachment.name }}
</a>
{% endfor %}
</div>
</div>
{% endif %}

<div class="divider"></div>

Expand Down
Loading