Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
c1c945a
Add basic vue + uppy functionality
msc5 Jan 27, 2026
ff04beb
Working on upload feature
msc5 Jan 27, 2026
b48bdfb
Fix CSRFToken issue
msc5 Mar 2, 2026
142242f
Add a basic attachments table
msc5 Mar 2, 2026
cd6137f
Add view and download buttons/views
msc5 Mar 2, 2026
559f33b
Some more convenience things, table updating
msc5 Mar 2, 2026
f43afcd
Dockerfile and dep
msc5 Mar 3, 2026
e0a1ec7
Add terraform
msc5 Mar 3, 2026
f320873
Fix some upload logic
msc5 Mar 3, 2026
0bfb623
Add bashrc
msc5 Mar 3, 2026
da94bc6
Use config/.env.example
msc5 Mar 3, 2026
a4bac20
No uv lock
msc5 Mar 3, 2026
9ac3bbe
Add uv.lock
msc5 Mar 3, 2026
e653bf1
Build vue
msc5 Mar 3, 2026
afcdb80
No dev
msc5 Mar 3, 2026
eccd4bc
Move deploy to different dep. group
msc5 Mar 3, 2026
c36e183
No-sync
msc5 Mar 3, 2026
bbeff26
Use secret not secret version
msc5 Mar 3, 2026
98cc3f1
Update secrets, config not managed
msc5 Mar 3, 2026
d710232
fix iam
msc5 Mar 3, 2026
e241bf1
Add whitenoise
msc5 Mar 3, 2026
b59522a
Update AWS_STORAGE_BUCKET_NAME
msc5 Mar 3, 2026
4077854
AWS_STORAGE_BUCKET_NAME default
msc5 Mar 3, 2026
7dd37d6
S3 bucket settings
msc5 Mar 3, 2026
8a1eaef
Remove storage backend
msc5 Mar 3, 2026
ccbaa51
djm no sync
msc5 Mar 3, 2026
183f46c
New App
msc5 Mar 4, 2026
c796a96
Working on form file uploads
msc5 Mar 4, 2026
cd02177
Add permissions
msc5 Mar 5, 2026
006bb85
Crispy Form
msc5 Mar 5, 2026
705add4
Add permissions and roles
msc5 Mar 5, 2026
d124984
Work on attachments (need to refactor vue state)
msc5 Mar 5, 2026
71db85f
Still working on attachments initial value
msc5 Mar 5, 2026
4faef06
Working on state and hydration
msc5 Mar 6, 2026
77bc6aa
Ok State pattern
msc5 Mar 6, 2026
e886bd5
State and serializers
msc5 Mar 6, 2026
5256033
Lots of improvements to linking
msc5 Mar 6, 2026
9286e1f
Only show navbar items if user is authenticated
msc5 Mar 6, 2026
3f7e692
Add basic delete attachment functionality
msc5 Mar 6, 2026
85821db
Selectable
msc5 Mar 8, 2026
7a492d8
Bundle everything
msc5 Mar 8, 2026
6863a9f
Update selection and bundling
msc5 Mar 8, 2026
0c211b3
Some tweaks
msc5 Mar 8, 2026
d923cff
Fix bootstrap icons
msc5 Mar 8, 2026
646de28
Auto select when uploading
msc5 Mar 8, 2026
5cfa6bd
Remove bootstrap icons scss
msc5 Mar 8, 2026
3b4668f
Add humanize
msc5 Mar 9, 2026
ffd365a
Organization
msc5 Mar 9, 2026
b75a23e
Datetime formatting
msc5 Mar 9, 2026
5e18e65
Update deps
msc5 Mar 10, 2026
ce16246
Remove ruff
msc5 Mar 10, 2026
4e7a8e9
Update page titles
msc5 Mar 10, 2026
4247070
Add vue and ts_ls
msc5 Mar 10, 2026
4e46b5e
Tweaks
msc5 Mar 10, 2026
f4731dd
Update props
msc5 Mar 10, 2026
c268e7f
tableRowClass
msc5 Mar 10, 2026
6f845e9
Remove spaces
msc5 Mar 10, 2026
b44cd26
Some bugfixes
msc5 Mar 11, 2026
888a946
Fix attachment view/download
msc5 Mar 11, 2026
8f21db4
Add github routes
msc5 Mar 11, 2026
eb46265
Vite config
msc5 Mar 16, 2026
e75edf2
Add a bunch of feature flags
msc5 Mar 16, 2026
3c89380
Add feature flags
msc5 Mar 16, 2026
18cee79
Add all terraform feature flags
msc5 Mar 16, 2026
ab52940
Remove terraform lock files
msc5 Mar 16, 2026
b4894cd
Feature flags
msc5 Mar 17, 2026
806587e
Remove some dep files
msc5 Mar 17, 2026
6175f4a
Remove terraform and bashrc
msc5 Apr 3, 2026
96247c6
Remove scss
msc5 Apr 3, 2026
6a6c9c5
Working on addressing PR comments
msc5 Apr 24, 2026
c0805f1
Fix json script
msc5 Apr 24, 2026
5767181
Remove json_script
msc5 Apr 27, 2026
a1902fe
Some fixes from review
msc5 Apr 27, 2026
c2b241c
Remove unused imports
msc5 Apr 27, 2026
41658e4
Merge branch 'main' into msc/direct-upload
msc5 May 13, 2026
dfc9e98
Work on code review
msc5 May 13, 2026
50e50b5
Code Review
msc5 May 13, 2026
5e51a94
Code Review
msc5 May 14, 2026
c70895b
Add comment
msc5 May 14, 2026
5f73860
Safer jsonify
msc5 May 14, 2026
40d575b
Use round-trip json parse
msc5 May 15, 2026
b4445b4
Switch create_presigned_url to PUT instead of POST
msc5 May 15, 2026
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
18 changes: 18 additions & 0 deletions .deploy/state.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"terraform": {
"staging": {
"cluster_id": "arn:aws:ecs:us-east-1:745415963933:cluster/deploy-staging-cluster",
"ecr_image_uri": "745415963933.dkr.ecr.us-east-1.amazonaws.com/deploy-staging-ecr:latest",
"ecr_repository_name": "deploy-staging-ecr",
"s3_bucket_name": "deploy-staging-staging-deploy.dev.zagaran.com",
"web_log_group_name": "deploy-staging-web",
"web_network_configuration_security_group": "sg-07b0e269a067068e6",
"web_network_configuration_subnet": "subnet-00553ef2b3e969274",
"web_service_name": "deploy-staging-web",
"web_task_definition_arn": "arn:aws:ecs:us-east-1:745415963933:task-definition/deploy-staging-web:2",
"worker_log_group_name": "deploy-staging-worker",
"worker_service_name": "deploy-staging-worker",
"worker_task_desired_count": 0
}
}
}
8 changes: 6 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ venv.bak/
node_modules/

# Compiled Files
static/js/dist*
staticfiles/
staticfiles/*
# START_FEATURE sass_bootstrap
Expand All @@ -65,7 +66,10 @@ static/webpack_bundles/
webpack-stats.json
# END_FEATURE django_react


# Locally uploaded files
media/

# START_FEATURE ecs
.terraform.lock.hcl
.terraform/
# END_FEATURE ecs
# END_FEATURE ecs
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12
Empty file added app/__init__.py
Empty file.
3 changes: 3 additions & 0 deletions app/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.contrib import admin

# Register your models here.
6 changes: 6 additions & 0 deletions app/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class AppConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'app'
5 changes: 5 additions & 0 deletions app/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
SAMPLE_OBJECT_PK_URL_KWARG = "sample_object_id"

# START_FEATURE direct_upload
ATTACHMENT_PK_URL_KWARG = "attachment_id"
# END_FEATURE direct_upload
46 changes: 46 additions & 0 deletions app/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from crispy_forms.helper import Layout
from crispy_forms.layout import Fieldset
from django import forms
from django.http import HttpRequest
from app.models import Attachment, SampleObject
from common.fields import DirectUploadFileField
from common.forms import ActionFormMixin, CrispyFormMixin


class SampleObjectBaseForm(CrispyFormMixin, ActionFormMixin, forms.ModelForm):
request: HttpRequest

# START_FEATURE direct_upload
attachments = DirectUploadFileField(queryset=Attachment.objects.filter(deleted_on=None), required=False)
# END_FEATURE direct_upload

class Meta:
model = SampleObject
exclude = ['created_by']

layout = Layout(
Fieldset(
"Details",
"name",
"description"
),
# START_FEATURE direct_upload
"attachments"
# END_FEATURE direct_upload
)

def __init__(self, request: HttpRequest, *args, **kwargs):
super().__init__(*args, **kwargs)
self.request = request


class SampleObjectCreateForm(SampleObjectBaseForm):
action_title = "Create Sample Object"

def save(self, commit=True):
self.instance.created_by = self.request.user
return super().save(commit)


class SampleObjectEditForm(SampleObjectBaseForm):
action_title = "Edit {instance}"
44 changes: 44 additions & 0 deletions app/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Generated by Django 5.2.12 on 2026-03-16 18:26

import common.models
import uuid
from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
]

operations = [
migrations.CreateModel(
name='Attachment',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_on', models.DateTimeField(auto_now_add=True)),
('updated_on', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=512)),
('file', models.FileField(max_length=1024, upload_to=common.models.get_upload_prefix)),
('upload_completed_on', models.DateTimeField(null=True)),
('deleted_on', models.DateTimeField(null=True)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='SampleObject',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_on', models.DateTimeField(auto_now_add=True)),
('updated_on', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=512, unique=True)),
('description', models.TextField(blank=True, default='')),
],
options={
'abstract': False,
},
),
]
33 changes: 33 additions & 0 deletions app/migrations/0002_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Generated by Django 5.2.12 on 2026-03-16 18:26

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


class Migration(migrations.Migration):

initial = True

dependencies = [
('app', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.AddField(
model_name='attachment',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='files', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='sampleobject',
name='attachments',
field=models.ManyToManyField(related_name='sample_objects', to='app.attachment'),
),
migrations.AddField(
model_name='sampleobject',
name='created_by',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='sample_objects', to=settings.AUTH_USER_MODEL),
),
]
Empty file added app/migrations/__init__.py
Empty file.
30 changes: 30 additions & 0 deletions app/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from django.db import models

from common.models import TimestampedModel, UploadFile, User


class SampleObject(TimestampedModel):
created_by = models.ForeignKey(User, related_name="sample_objects", on_delete=models.PROTECT)

# START_FEATURE direct_upload
attachments = models.ManyToManyField("Attachment", related_name="sample_objects")
Comment thread
msc5 marked this conversation as resolved.
# END_FEATURE direct_upload

name = models.CharField(max_length=512, unique=True)
description = models.TextField(default="", blank=True)

def __str__(self) -> str:
return f'Sample Object {self.name}'

def get_attachments(self):
qs = self.attachments.prefetch_related('user')
return [
attachment for attachment in qs
if not attachment.deleted_on and attachment.upload_completed_on
]


# START_FEATURE direct_upload
class Attachment(UploadFile):
pass
# END_FEATURE direct_upload
45 changes: 45 additions & 0 deletions app/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# START_FEATURE direct_upload
from app.constants import ATTACHMENT_PK_URL_KWARG
from django.utils.formats import date_format
from common.serializers import UserSerializer
from django.urls import reverse
from rest_framework import serializers

from app.models import Attachment


class AttachmentSerializer(serializers.ModelSerializer):
user = UserSerializer()
view_url = serializers.SerializerMethodField()
download_url = serializers.SerializerMethodField()
delete_url = serializers.SerializerMethodField()
created_on = serializers.SerializerMethodField()
upload_completed_on = serializers.SerializerMethodField()
size = serializers.SerializerMethodField()
path = serializers.SerializerMethodField()

class Meta:
model = Attachment
exclude = []

def get_view_url(self, instance):
return reverse('attachment_open', kwargs={ATTACHMENT_PK_URL_KWARG: instance.id})

def get_download_url(self, instance):
return reverse('attachment_download', kwargs={ATTACHMENT_PK_URL_KWARG: instance.id})

def get_delete_url(self, instance):
return reverse('attachment_delete', kwargs={ATTACHMENT_PK_URL_KWARG: instance.id})

def get_created_on(self, instance):
return date_format(instance.created_on, format="DATETIME_FORMAT")

def get_upload_completed_on(self, instance):
return date_format(instance.upload_completed_on, format="DATETIME_FORMAT")

def get_size(self, instance):
return instance.file.size

def get_path(self, instance):
return instance.file.name
# END_FEATURE direct_upload
91 changes: 91 additions & 0 deletions app/templates/app/dashboard.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
{% extends "base_templates/base.html" %}
{% load vue %}


{% block title %}Dashboard{% endblock %}

{% block head %}

{% endblock %}
Comment thread
msc5 marked this conversation as resolved.

{% block body %}
<div id="app">
<h2>Dashboard</h2>
<p>Hello <b>{{ user.email }}</b>!</p>

<div class="my-4 card">
<div class="card-body">
<div class="d-flex gap-3">
<h3 class="card-title">Sample Objects</h3>
<a href="{% url 'sample-object-create' %}">
<button class="btn btn-sm btn-success">
<i class="bi bi-plus"></i> Create
</button>
</a>
</div>
<div class="my-2">
{% if sample_objects %}
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Created By</th>
<th>Created On</th>
{# START_FEATURE direct_upload #}
<th>Attachments</th>
{# END_FEATURE direct_upload #}
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for sample_object in sample_objects %}
<tr>
<td>{{ sample_object.name }}</td>
<td>{{ sample_object.created_by }}</td>
<td>{{ sample_object.created_on }}</td>
{# START_FEATURE direct_upload #}
<td>
{% for attachment in sample_object.get_attachments %}
<span class="badge bg-info">{{ attachment.name }}</span>
{% endfor %}
</td>
<td>
<div class="d-flex gap-2">
<a href="{% url 'sample-object' sample_object.id %}">
<button class="btn btn-sm btn-secondary">View</button>
</a>
<a href="{% url 'sample-object-edit' sample_object.id %}">
<button class="btn btn-sm btn-secondary">Edit</button>
</a>
</div>
</td>
{# END_FEATURE direct_upload #}
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="alert alert-light">
<i class="bi bi-exclamation-square me-2"></i>
No Objects
</div>
{% endif %}
</div>
</div>
</div>

{# START_FEATURE direct_upload #}
<div class="my-4 card">
<div class="card-body">
<h3 class="card-title">All Attachments</h3>
<file-upload-dashboard
:files="{{ attachments|jsonify }}"
storage-backend="{{ settings.DEFAULT_STORAGE_TYPE }}"
upload-start-url="{% url 'attachment_upload_start' %}"
:multiple="true"
></file-upload-dashboard>
</div>
</div>
{# END_FEATURE direct_upload #}
</div>
{% endblock %}
Loading