Skip to content

Commit 0645c74

Browse files
authored
Data uploader (#309)
1 parent 065c6d4 commit 0645c74

File tree

8 files changed

+250
-2
lines changed

8 files changed

+250
-2
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Generated by Django 4.0.7 on 2022-09-08 20:07
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("app", "0002_notebook_notebookcell"),
10+
]
11+
12+
operations = [
13+
migrations.CreateModel(
14+
name="UploadedData",
15+
fields=[
16+
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
17+
("file_type", models.IntegerField(choices=[(1, "CSV"), (2, "JSON")], default=1)),
18+
("created_at", models.DateTimeField(auto_now_add=True)),
19+
("updated_at", models.DateTimeField(auto_now=True)),
20+
],
21+
),
22+
]

pgml-dashboard/app/models.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
from django.db import models, connection
1+
from django.db import models, connection, transaction
22
from django.template.loader import render_to_string
33
from django.utils.safestring import mark_safe
44
from django.db.utils import ProgrammingError
55
from django.utils import timezone
66
from django.utils.html import strip_tags
77

88
import markdown
9+
import codecs
10+
import csv
911

1012

1113
class Project(models.Model):
@@ -298,3 +300,35 @@ def code(self):
298300

299301
def __str__(self):
300302
return f"{self.notebook} - {self.pk}"
303+
304+
305+
class UploadedData(models.Model):
306+
"""Data uploaded by the user through the dashboard."""
307+
308+
file_type = models.IntegerField(
309+
choices=(
310+
(
311+
1,
312+
"CSV",
313+
),
314+
(2, "JSON"),
315+
),
316+
default=1,
317+
)
318+
created_at = models.DateTimeField(auto_now_add=True)
319+
updated_at = models.DateTimeField(auto_now=True)
320+
321+
def create_table(self, file):
322+
if file.content_type == "text/csv":
323+
reader = csv.reader(codecs.iterdecode(file, "utf-8"))
324+
headers = next(reader)
325+
columns = ", ".join(map(lambda x: f"{x.replace(' ', '_').lower()} FLOAT4", headers))
326+
327+
with transaction.atomic():
328+
sql = f"CREATE TABLE data_{self.pk} (" + columns + ")"
329+
330+
with connection.cursor() as cursor:
331+
cursor.execute(sql)
332+
333+
file.seek(0)
334+
cursor.copy_expert(f"COPY data_{self.pk} FROM STDIN CSV HEADER", file)

pgml-dashboard/app/static/css/base.css

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,3 +652,36 @@ main turbo-frame:first-of-type .notebook-cell {
652652
.CodeMirror {
653653
font-size: 1rem;
654654
}
655+
656+
/*
657+
* Uploader
658+
*/
659+
body.uploader section p, body.uploader section li {
660+
margin: 0.5rem 0;
661+
}
662+
663+
body.uploader section ol, body.uploader section ul {
664+
margin: 1rem 0;
665+
}
666+
667+
body.uploader section .markdown-body{
668+
margin: 1rem 0;
669+
}
670+
671+
body.uploader ul {
672+
list-style-type: disc;
673+
list-style-position: inside;
674+
}
675+
676+
body.uploader ol {
677+
list-style-type: decimal;
678+
list-style-position: inside;
679+
}
680+
681+
body.uploader section li {
682+
margin-left: 1rem;
683+
}
684+
685+
body.uploader strong {
686+
font-weight: bold;
687+
}

pgml-dashboard/app/templates/base.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@
3232
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.css" integrity="sha512-uf06llspW44/LZpHzHT6qBOIVODjWtv4MxCricRxkzvopAlSWnTf6hpZTFxuuZcuNE9CBQhqE0Seu1CoRk84nQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
3333
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/addon/hint/show-hint.min.css" integrity="sha512-OmcLQEy8iGiD7PSm85s06dnR7G7C9C0VqahIPAj/KHk5RpOCmnC6R2ob1oK4/uwYhWa9BF1GC6tzxsC8TIx7Jg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
3434

35+
<!-- Papa Parse <3 -->
36+
<!-- <script defer async src="https://cdn.jsdelivr.net/npm/papaparse@5.3.2/papaparse.min.js"></script> -->
37+
38+
<!-- CSV preview -->
39+
<!-- <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/kktsvetkov/heiho@latest/heiho.css" /> -->
40+
<!-- <script async defer src="https://cdn.jsdelivr.net/gh/kktsvetkov/heiho@latest/heiho.js"></script> -->
41+
3542
<script defer src="https://unpkg.com/es-module-shims@1.2.0/dist/es-module-shims.js"></script>
3643
<script type="importmap-shim">
3744
{
@@ -92,6 +99,7 @@
9299
<li{% if topic == "deployments" %} class="selected"{% endif %}><a href="{% url 'deployments' %}"><span class="material-symbols-outlined">inventory</span>Deployments</a></li>
93100
<li{% if topic == "snapshots" %} class="selected"{% endif %}><a href="{% url 'snapshots' %}"><span class="material-symbols-outlined">storage</span>Snapshots</a></li>
94101
<li{% if topic == "console" %} class="selected"{% endif %}><a href="{% url 'console' %}"><span class="material-symbols-outlined">terminal</span>Console</a></li>
102+
<li{% if topic == "uploader" %} class="selected"{% endif %}><a href="{% url 'uploader' %}"><span class="material-symbols-outlined">cloud_upload</span>Upload Data</a></li>
95103
<li><a href="https://postgresml.org/user_guides/training/overview/" data-turbo="false" target="_blank"><span class="material-symbols-outlined">menu_book</span>Docs</a></li>
96104
</ul>
97105
</nav>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{% extends "base.html" %}
2+
{% load humanize %}
3+
4+
{% block main %}
5+
6+
<section>
7+
{% if error %}
8+
<h1><span class="material-symbols-outlined" style="color: var(--highlite-red)">cloud_upload</span>Error</h1>
9+
{% else %}
10+
<h1><span class="material-symbols-outlined">cloud_upload</span>Upload Data</h1>
11+
{% endif %}
12+
13+
{% if error %}
14+
<p style="margin-bottom: 1rem;">Hmm, something went wrong. Please make sure:</p>
15+
{% else %}
16+
<p style="margin-bottom: 1rem;">You can upload your datasets using the CSV format. Before uploading, please make sure:</p>
17+
{% endif %}
18+
19+
<ol>
20+
<li>The data is numeric (i.e. only floats or integers and no text)</li>
21+
<li>The CSV includes headers on the first line</li>
22+
<li>The headers are alphanumeric, contain no spaces and don't start with a number</li>
23+
<li>The CSV is comma (<code>,</code>) delimited</li>
24+
</ol>
25+
26+
{% if error %}
27+
<h4>Error: </h4>
28+
<div class="markdown-body">
29+
<pre><code>{{ error }}</code></pre>
30+
</div>
31+
{% endif %}
32+
33+
<p>If you are exporting data from a PostgreSQL database, you can use <code>psql</code> to generate a valid CSV file:</p>
34+
<div class="markdown-body">
35+
<pre><code class="language-sql">\copy your_table_name TO 'output.csv' CSV HEADER</code></pre>
36+
</div>
37+
38+
</section>
39+
40+
<section>
41+
<form action="{% url 'uploader' %}" method="post" enctype="multipart/form-data">
42+
{% csrf_token %}
43+
<div class="flex">
44+
<input id="file" type="file" name="file" accept="text/csv,application/json" required="true" />
45+
</div>
46+
47+
<div class="button-container">
48+
<button type="submit">Upload</button>
49+
</div>
50+
</form>
51+
</section>
52+
{% endblock %}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{% extends "base.html" %}
2+
{% load humanize %}
3+
4+
{% block main %}
5+
6+
<section>
7+
<h1><span class="material-symbols-outlined" style="color: var(--highlite-green)">cloud_upload</span>Upload Successful</h1>
8+
</section>
9+
10+
<section>
11+
<h2><span class="material-symbols-outlined">data_array</span>Preview</h2>
12+
13+
{% include 'projects/sample.html' %}
14+
</section>
15+
16+
<section>
17+
<h2><span class="material-symbols-outlined">table_rows</span>Next Steps</h2>
18+
<p>Your data has been saved in <strong>pgml.{{ table_name }}</strong> table.</p>
19+
<p>You can now build a model using a <a href="{% url 'notebooks' %}">Notebook</a> or browse the data in the <a href="{% url 'console' %}">Console</a>:</p>
20+
<div class="markdown-body">
21+
<pre><code class="language-sql">SELECT * FROM pgml.{{ table_name }}
22+
LIMIT 10</code></pre>
23+
</section>
24+
25+
{% endblock %}

pgml-dashboard/app/urls.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from django.urls import path
22
from rest_framework import routers
33

4-
from app.views import root, projects, models, snapshots, deployments, console, notebooks
4+
from app.views import root, projects, models, snapshots, deployments, console, notebooks, uploader
55

66

77
router = routers.DefaultRouter()
@@ -26,6 +26,8 @@
2626
path("projects/<int:pk>", projects.ProjectView.as_view(), name="project"),
2727
path("snapshots/", snapshots.index, name="snapshots"),
2828
path("snapshots/<int:id>", snapshots.snapshot, name="snapshot"),
29+
path("uploader/", uploader.index, name="uploader"),
30+
path("uploader/uploaded/<int:pk>/", uploader.uploaded, name="uploader/uploaded"),
2931
path("console/", console.ConsoleView.as_view(), name="console"),
3032
path("console/run/", console.run_sql, name="console/run-sql"),
3133
path("set-auth-cookie/", root.set_auth_cookie, name="set-auth-cookie"),

pgml-dashboard/app/views/uploader.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from django.db import connection
2+
from django.shortcuts import render, get_object_or_404
3+
from django.utils.safestring import SafeString
4+
from django.http import HttpResponse, HttpResponseRedirect
5+
from django.urls import reverse_lazy
6+
from django import forms
7+
8+
from app.models import UploadedData
9+
10+
import csv
11+
import json
12+
import codecs
13+
14+
15+
class UploadForm(forms.Form):
16+
file = forms.FileField()
17+
18+
19+
def index(request):
20+
if request.method == "POST":
21+
form = UploadForm(request.POST, request.FILES)
22+
if not form.is_valid():
23+
return HttpResponse(status=400)
24+
25+
file = request.FILES.get("file")
26+
if file.content_type not in ["text/csv", "application/json"]:
27+
return HttpResponse(status=400)
28+
else:
29+
try:
30+
upload = UploadedData.objects.create(
31+
file_type=1 if file.content_type == "text/csv" else 2,
32+
)
33+
34+
upload.create_table(file)
35+
except Exception as e:
36+
return render(
37+
request,
38+
"uploader/index.html",
39+
{
40+
"error": str(e),
41+
"topic": "uploader",
42+
},
43+
status=400,
44+
)
45+
return HttpResponseRedirect(reverse_lazy("uploader/uploaded", kwargs={"pk": upload.pk}))
46+
else:
47+
return render(
48+
request,
49+
"uploader/index.html",
50+
{
51+
"topic": "uploader",
52+
},
53+
)
54+
55+
56+
def uploaded(request, pk):
57+
upload = UploadedData.objects.get(pk=pk)
58+
with connection.cursor() as cursor:
59+
cursor.execute(f"SELECT * FROM data_{upload.pk} LIMIT 11")
60+
columns = [col[0] for col in cursor.description]
61+
rows = cursor.fetchall()
62+
return render(
63+
request,
64+
"uploader/uploaded.html",
65+
{
66+
"columns": columns,
67+
"rows": rows[:10],
68+
"table_name": f"data_{upload.pk}",
69+
"redacted": len(rows) > 10,
70+
"topic": "uploader",
71+
},
72+
)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy