Docker Image and first iteration
This commit is contained in:
1 parent
723b64b1c4
commit
bd9209ebb7
40 files changed
+26643
No files matched your search
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import PortfolioEntry, Region
|
||||
|
||||
|
||||
@admin.register(Region)
|
||||
class RegionAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "code", "level", "parent")
|
||||
list_filter = ("level", "parent")
|
||||
search_fields = ("name", "code")
|
||||
|
||||
|
||||
@admin.register(PortfolioEntry)
|
||||
class PortfolioEntryAdmin(admin.ModelAdmin):
|
||||
list_display = ("title", "organization", "entry_type", "is_published", "display_order")
|
||||
list_filter = ("entry_type", "is_published", "regions")
|
||||
search_fields = ("title", "organization", "summary")
|
||||
filter_horizontal = ("regions",)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ShowcaseConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "showcase"
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Region",
|
||||
fields=[
|
||||
("code", models.CharField(max_length=8, primary_key=True, serialize=False)),
|
||||
("name", models.CharField(max_length=120)),
|
||||
(
|
||||
"level",
|
||||
models.CharField(
|
||||
choices=[("country", "Country"), ("subdivision", "Subdivision")],
|
||||
max_length=16,
|
||||
),
|
||||
),
|
||||
(
|
||||
"parent",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="subdivisions",
|
||||
to="showcase.region",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={"ordering": ("name",)},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="PortfolioEntry",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("entry_type", models.CharField(choices=[("work", "Work"), ("cv", "CV")], max_length=8)),
|
||||
("title", models.CharField(max_length=160)),
|
||||
("organization", models.CharField(blank=True, max_length=160)),
|
||||
("start_date", models.DateField(blank=True, null=True)),
|
||||
("end_date", models.DateField(blank=True, null=True)),
|
||||
("summary", models.TextField()),
|
||||
("url", models.URLField(blank=True)),
|
||||
("is_published", models.BooleanField(default=False)),
|
||||
("display_order", models.PositiveIntegerField(default=0)),
|
||||
("regions", models.ManyToManyField(related_name="entries", to="showcase.region")),
|
||||
],
|
||||
options={
|
||||
"verbose_name_plural": "portfolio entries",
|
||||
"ordering": ("display_order", "-start_date", "title"),
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def seed_regions(apps, schema_editor):
|
||||
region_model = apps.get_model("showcase", "Region")
|
||||
seed_path = Path(__file__).with_name("regions.json")
|
||||
regions = json.loads(seed_path.read_text())
|
||||
|
||||
for region in regions["countries"]:
|
||||
region_model.objects.update_or_create(
|
||||
code=region["code"],
|
||||
defaults={"name": region["name"], "level": "country", "parent": None},
|
||||
)
|
||||
|
||||
for region in regions["subdivisions"]:
|
||||
region_model.objects.update_or_create(
|
||||
code=region["code"],
|
||||
defaults={
|
||||
"name": region["name"],
|
||||
"level": "subdivision",
|
||||
"parent_id": region["parentCode"],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def unseed_regions(apps, schema_editor):
|
||||
apps.get_model("showcase", "Region").objects.all().delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [("showcase", "0001_initial")]
|
||||
|
||||
operations = [migrations.RunPython(seed_regions, unseed_regions)]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,49 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Region(models.Model):
|
||||
class Level(models.TextChoices):
|
||||
COUNTRY = "country", "Country"
|
||||
SUBDIVISION = "subdivision", "Subdivision"
|
||||
|
||||
code = models.CharField(max_length=8, primary_key=True)
|
||||
name = models.CharField(max_length=120)
|
||||
level = models.CharField(max_length=16, choices=Level.choices)
|
||||
parent = models.ForeignKey(
|
||||
"self",
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="subdivisions",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ("name",)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class PortfolioEntry(models.Model):
|
||||
class EntryType(models.TextChoices):
|
||||
WORK = "work", "Work"
|
||||
CV = "cv", "CV"
|
||||
|
||||
entry_type = models.CharField(max_length=8, choices=EntryType.choices)
|
||||
title = models.CharField(max_length=160)
|
||||
organization = models.CharField(max_length=160, blank=True)
|
||||
start_date = models.DateField(blank=True, null=True)
|
||||
end_date = models.DateField(blank=True, null=True)
|
||||
summary = models.TextField()
|
||||
url = models.URLField(blank=True)
|
||||
is_published = models.BooleanField(default=False)
|
||||
display_order = models.PositiveIntegerField(default=0)
|
||||
regions = models.ManyToManyField(Region, related_name="entries")
|
||||
|
||||
class Meta:
|
||||
ordering = ("display_order", "-start_date", "title")
|
||||
verbose_name_plural = "portfolio entries"
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,26 @@
|
||||
{% load static %}
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Xavier Bukowski - Atlas</title>
|
||||
<link rel="stylesheet" href="{% static 'showcase/dist/map.css' %}">
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<a class="site-mark" href="/">Xavier Bukowski<span>.</span></a>
|
||||
<nav aria-label="Primary navigation">
|
||||
<a href="#map">Atlas</a>
|
||||
<a href="#work">Work</a>
|
||||
<a href="#contact">Contact</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main class="site-shell">
|
||||
<section id="map" aria-labelledby="map-title">
|
||||
<div id="portfolio-map-root" data-testid="portfolio-map-root"></div>
|
||||
</section>
|
||||
</main>
|
||||
<script type="module" src="{% static 'showcase/dist/map.js' %}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from showcase.models import PortfolioEntry, Region
|
||||
|
||||
|
||||
class MapRegionsViewTests(TestCase):
|
||||
def setUp(self):
|
||||
self.us = Region.objects.get(code="USA")
|
||||
self.california = Region.objects.get(code="US-CA")
|
||||
self.canada = Region.objects.get(code="CAN")
|
||||
|
||||
def regions_by_code(self):
|
||||
response = self.client.get(reverse("map-regions"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
return {region["code"]: region for region in response.json()["regions"]}
|
||||
|
||||
def test_homepage_contains_map_mount(self):
|
||||
response = self.client.get(reverse("home"))
|
||||
|
||||
self.assertContains(response, 'id="portfolio-map-root"')
|
||||
self.assertContains(response, "showcase/dist/map.js")
|
||||
|
||||
def test_api_returns_seeded_regions_with_empty_entries(self):
|
||||
regions = self.regions_by_code()
|
||||
|
||||
self.assertIn("USA", regions)
|
||||
self.assertIn("CAN", regions)
|
||||
self.assertIn("US-CA", regions)
|
||||
self.assertEqual(regions["US-CA"]["parentCode"], "USA")
|
||||
self.assertEqual(regions["USA"]["entries"], [])
|
||||
|
||||
def test_country_aggregates_subdivision_entries_and_deduplicates(self):
|
||||
entry = PortfolioEntry.objects.create(
|
||||
entry_type=PortfolioEntry.EntryType.WORK,
|
||||
title="West Coast delivery",
|
||||
summary="Delivered a regional engagement.",
|
||||
is_published=True,
|
||||
)
|
||||
entry.regions.add(self.us, self.california)
|
||||
|
||||
regions = self.regions_by_code()
|
||||
|
||||
self.assertEqual(len(regions["USA"]["entries"]), 1)
|
||||
self.assertEqual(regions["US-CA"]["entries"][0]["title"], entry.title)
|
||||
|
||||
def test_api_excludes_unpublished_entries(self):
|
||||
entry = PortfolioEntry.objects.create(
|
||||
entry_type=PortfolioEntry.EntryType.CV,
|
||||
title="Draft entry",
|
||||
summary="Not visible yet.",
|
||||
is_published=False,
|
||||
)
|
||||
entry.regions.add(self.canada)
|
||||
|
||||
regions = self.regions_by_code()
|
||||
|
||||
self.assertEqual(regions["CAN"]["entries"], [])
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.home, name="home"),
|
||||
path("api/map/regions/", views.map_regions, name="map-regions"),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import render
|
||||
|
||||
from .models import PortfolioEntry, Region
|
||||
|
||||
|
||||
def home(request):
|
||||
return render(request, "showcase/home.html")
|
||||
|
||||
|
||||
def _serialize_entry(entry):
|
||||
return {
|
||||
"id": entry.id,
|
||||
"type": entry.entry_type,
|
||||
"title": entry.title,
|
||||
"organization": entry.organization,
|
||||
"startDate": entry.start_date.isoformat() if entry.start_date else None,
|
||||
"endDate": entry.end_date.isoformat() if entry.end_date else None,
|
||||
"summary": entry.summary,
|
||||
"url": entry.url,
|
||||
}
|
||||
|
||||
|
||||
def map_regions(request):
|
||||
entries = PortfolioEntry.objects.filter(is_published=True).prefetch_related("regions")
|
||||
entries_by_region = {}
|
||||
for entry in entries:
|
||||
payload = _serialize_entry(entry)
|
||||
for region in entry.regions.all():
|
||||
entries_by_region.setdefault(region.code, {})[entry.id] = payload
|
||||
|
||||
regions = list(Region.objects.select_related("parent").prefetch_related("subdivisions"))
|
||||
payload = []
|
||||
for region in regions:
|
||||
related = dict(entries_by_region.get(region.code, {}))
|
||||
if region.level == Region.Level.COUNTRY:
|
||||
for subdivision in region.subdivisions.all():
|
||||
related.update(entries_by_region.get(subdivision.code, {}))
|
||||
|
||||
payload.append(
|
||||
{
|
||||
"code": region.code,
|
||||
"name": region.name,
|
||||
"level": region.level,
|
||||
"parentCode": region.parent_id,
|
||||
"entries": list(related.values()),
|
||||
}
|
||||
)
|
||||
|
||||
return JsonResponse({"regions": payload})
|
||||
|
||||
Reference in new issue
Block a user