From 508c7ffe4eb4513e4f02e59cc285dbb653614f69 Mon Sep 17 00:00:00 2001 From: veldakiara Date: Sat, 9 May 2026 16:32:01 +0100 Subject: [PATCH] redirects --- config/settings.py | 1 + config/urls.py | 5 + frontend/css/tailwind.css | 147 ------ static/images/spokane.png | Bin 0 -> 25341 bytes templates/about.html | 74 +-- templates/admin/base_site.html | 12 +- templates/admin/login.html | 466 +++++++++--------- templates/events/djangocon.html | 258 ++-------- templates/events/sponsored.html | 112 +---- templates/robots.txt | 4 + templates/socialaccount/login.html | 288 +++++------ website/admin.py | 9 +- .../0006_djangoconedition_logo_url.py | 23 + .../0007_seed_djangocon_editions.py | 149 ++++++ .../0008_backfill_edition_logo_urls.py | 32 ++ .../migrations/0009_fix_edition_logo_urls.py | 32 ++ website/migrations/0010_fix_2017_logo.py | 16 + website/migrations/0011_add_milestone.py | 24 + website/migrations/0012_seed_milestones.py | 29 ++ website/models.py | 14 +- website/sitemaps.py | 45 ++ website/urls.py | 18 +- website/views.py | 21 +- 23 files changed, 853 insertions(+), 926 deletions(-) create mode 100644 static/images/spokane.png create mode 100644 templates/robots.txt create mode 100644 website/migrations/0006_djangoconedition_logo_url.py create mode 100644 website/migrations/0007_seed_djangocon_editions.py create mode 100644 website/migrations/0008_backfill_edition_logo_urls.py create mode 100644 website/migrations/0009_fix_edition_logo_urls.py create mode 100644 website/migrations/0010_fix_2017_logo.py create mode 100644 website/migrations/0011_add_milestone.py create mode 100644 website/migrations/0012_seed_milestones.py create mode 100644 website/sitemaps.py diff --git a/config/settings.py b/config/settings.py index 1adbf23..735c850 100644 --- a/config/settings.py +++ b/config/settings.py @@ -40,6 +40,7 @@ "django.contrib.contenttypes", "django.contrib.messages", "django.contrib.sessions", + "django.contrib.sitemaps", "django.contrib.sites", "django.contrib.staticfiles", ] diff --git a/config/urls.py b/config/urls.py index e541328..6779e62 100644 --- a/config/urls.py +++ b/config/urls.py @@ -1,13 +1,16 @@ from django.conf import settings from django.conf.urls.static import static from django.contrib import admin +from django.contrib.sitemaps.views import sitemap from django.urls import include from django.urls import path from django.urls import re_path +from django.views.generic import TemplateView from django.views.static import serve from config import __version__ from health_check.views import HealthCheckView +from website.sitemaps import SITEMAPS admin_header = f"DEFNA v{__version__}" admin.site.enable_nav_sidebar = False @@ -29,6 +32,8 @@ ] ), ), + path("sitemap.xml", sitemap, {"sitemaps": SITEMAPS}, name="django.contrib.sitemaps.views.sitemap"), + path("robots.txt", TemplateView.as_view(template_name="robots.txt", content_type="text/plain")), ] urlpatterns += [ diff --git a/frontend/css/tailwind.css b/frontend/css/tailwind.css index 5123142..61d10c0 100644 --- a/frontend/css/tailwind.css +++ b/frontend/css/tailwind.css @@ -592,9 +592,6 @@ .inset-0 { inset: calc(var(--spacing) * 0); } - .inset-x-0 { - inset-inline: calc(var(--spacing) * 0); - } .start-1 { inset-inline-start: calc(var(--spacing) * 1); } @@ -610,15 +607,6 @@ .right-2 { right: calc(var(--spacing) * 2); } - .bottom-0 { - bottom: calc(var(--spacing) * 0); - } - .bottom-4 { - bottom: calc(var(--spacing) * 4); - } - .-left-1 { - left: calc(var(--spacing) * -1); - } .-left-1\.5 { left: calc(var(--spacing) * -1.5); } @@ -628,15 +616,9 @@ .left-0 { left: calc(var(--spacing) * 0); } - .left-1 { - left: calc(var(--spacing) * 1); - } .left-1\/2 { left: calc(1/2 * 100%); } - .left-4 { - left: calc(var(--spacing) * 4); - } .isolate { isolation: isolate; } @@ -1347,9 +1329,6 @@ margin-bottom: 0; } } - .mt-0 { - margin-top: calc(var(--spacing) * 0); - } .mt-0\.5 { margin-top: calc(var(--spacing) * 0.5); } @@ -1509,16 +1488,9 @@ .field-sizing-fixed { field-sizing: fixed; } - .aspect-3\/4 { - aspect-ratio: 3/4; - } .aspect-video { aspect-ratio: var(--aspect-video); } - .size-3 { - width: calc(var(--spacing) * 3); - height: calc(var(--spacing) * 3); - } .size-3\.5 { width: calc(var(--spacing) * 3.5); height: calc(var(--spacing) * 3.5); @@ -1823,10 +1795,6 @@ --tw-translate-y: 100%; translate: var(--tw-translate-x) var(--tw-translate-y); } - .-translate-x-1 { - --tw-translate-x: calc(var(--spacing) * -1); - translate: var(--tw-translate-x) var(--tw-translate-y); - } .-translate-x-1\/2 { --tw-translate-x: calc(calc(1/2 * 100%) * -1); translate: var(--tw-translate-x) var(--tw-translate-y); @@ -2551,9 +2519,6 @@ .border-gray-300 { border-color: var(--color-gray-300); } - .border-white { - border-color: var(--color-white); - } .border-white\/20 { border-color: color-mix(in srgb, #fff 20%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -2653,13 +2618,6 @@ .bg-yellow-50 { background-color: var(--color-yellow-50); } - .bg-linear-to-t { - --tw-gradient-position: to top; - @supports (background-image: linear-gradient(in lab, red, red)) { - --tw-gradient-position: to top in oklab; - } - background-image: linear-gradient(var(--tw-gradient-stops)); - } .-bg-conic { --tw-gradient-position: in oklab; background-image: conic-gradient(var(--tw-gradient-stops)); @@ -2678,21 +2636,6 @@ .via-none { --tw-gradient-via-stops: initial; } - .from-black { - --tw-gradient-from: var(--color-black); - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); - } - .from-black\/60 { - --tw-gradient-from: color-mix(in srgb, #000 60%, transparent); - @supports (color: color-mix(in lab, red, red)) { - --tw-gradient-from: color-mix(in oklab, var(--color-black) 60%, transparent); - } - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); - } - .to-transparent { - --tw-gradient-to: transparent; - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); - } .mask-none { mask-image: none; } @@ -3009,15 +2952,9 @@ .object-right-top { object-position: right top; } - .object-top { - object-position: top; - } .p-1 { padding: calc(var(--spacing) * 1); } - .p-2 { - padding: calc(var(--spacing) * 2); - } .p-2\.5 { padding: calc(var(--spacing) * 2.5); } @@ -3060,9 +2997,6 @@ .px-\[0\.3rem\] { padding-inline: 0.3rem; } - .py-0 { - padding-block: calc(var(--spacing) * 0); - } .py-0\.5 { padding-block: calc(var(--spacing) * 0.5); } @@ -3576,9 +3510,6 @@ .opacity-50 { opacity: 50%; } - .opacity-60 { - opacity: 60%; - } .mix-blend-plus-darker { mix-blend-mode: plus-darker; } @@ -3734,10 +3665,6 @@ --tw-duration: 300ms; transition-duration: 300ms; } - .duration-500 { - --tw-duration: 500ms; - transition-duration: 500ms; - } .ease-in { --tw-ease: var(--ease-in); transition-timing-function: var(--ease-in); @@ -3946,13 +3873,6 @@ } } } - .group-hover\:opacity-75 { - &:is(:where(.group):hover *) { - @media (hover: hover) { - opacity: 75%; - } - } - } .group-hover\:opacity-100 { &:is(:where(.group):hover *) { @media (hover: hover) { @@ -3960,14 +3880,6 @@ } } } - .group-hover\:grayscale-0 { - &:is(:where(.group):hover *) { - @media (hover: hover) { - --tw-grayscale: grayscale(0%); - filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); - } - } - } .group-data-\[disabled\=true\]\:pointer-events-none { &:is(:where(.group)[data-disabled="true"] *) { pointer-events: none; @@ -4143,14 +4055,6 @@ } } } - .hover\:shadow-lg { - &:hover { - @media (hover: hover) { - --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - } - } .hover\:shadow-md { &:hover { @media (hover: hover) { @@ -4742,48 +4646,6 @@ inherits: false; initial-value: 0; } -@property --tw-gradient-position { - syntax: "*"; - inherits: false; -} -@property --tw-gradient-from { - syntax: ""; - inherits: false; - initial-value: #0000; -} -@property --tw-gradient-via { - syntax: ""; - inherits: false; - initial-value: #0000; -} -@property --tw-gradient-to { - syntax: ""; - inherits: false; - initial-value: #0000; -} -@property --tw-gradient-stops { - syntax: "*"; - inherits: false; -} -@property --tw-gradient-via-stops { - syntax: "*"; - inherits: false; -} -@property --tw-gradient-from-position { - syntax: ""; - inherits: false; - initial-value: 0%; -} -@property --tw-gradient-via-position { - syntax: ""; - inherits: false; - initial-value: 50%; -} -@property --tw-gradient-to-position { - syntax: ""; - inherits: false; - initial-value: 100%; -} @property --tw-leading { syntax: "*"; inherits: false; @@ -5057,15 +4919,6 @@ --tw-divide-x-reverse: 0; --tw-border-style: solid; --tw-divide-y-reverse: 0; - --tw-gradient-position: initial; - --tw-gradient-from: #0000; - --tw-gradient-via: #0000; - --tw-gradient-to: #0000; - --tw-gradient-stops: initial; - --tw-gradient-via-stops: initial; - --tw-gradient-from-position: 0%; - --tw-gradient-via-position: 50%; - --tw-gradient-to-position: 100%; --tw-leading: initial; --tw-font-weight: initial; --tw-tracking: initial; diff --git a/static/images/spokane.png b/static/images/spokane.png new file mode 100644 index 0000000000000000000000000000000000000000..b525faff76c3ba92cdde131334c0a553cf4c0abb GIT binary patch literal 25341 zcmeFY^;=uZ7e0y>hXTd5K!M`Y;sIK`KxuI+?(P;KrMMR;ZY@&WAwaQ|;_ktvxVr@c z-*C?9IiD+k!2RLod4|2S_spz!)_T`kGm|0w-CH?4Y)Wh-BqTfqd1+N7Bvf)FBxE+s zM~Ej$?=E(ckZ_f)rKH{|NJ-JZb8)n=wlhaUk`GVR!q8UhCC$=PeTs%DE%|0k`EeYP z5nCo z{nmk}ey*Es_Zc_C!ka@a{HRDRk=%(JIcZ313Mb~gKkrKO)}D}VQ$Uc=KL_xRY~rk` zzExKj`e@z&2YUPA(f3_c2deI7!|&^?uw-12k>sCrY`<=#C-|L>1kpoR)e%RMe!ezn zxvrS##n~akw?*Ia@=Zojd)b?e@^&%|``;!Fk0nq(mJV(9^C6FH1&WnOe#Ua&;?Z4Z zB8+(U_~zAKLb9L~<}pK<{Z~Bdgz^c?0#JU!wcy|xp|ZGyD_V&O1;-jvm3~@(_9^K5 zDk!;Gzw?NEs-0U3(l^!a!Lbh=N6HalOHVmL?3(~^st zxhe@TT6oRdQt)wEhJpH$@nByk?n`^ZHw({r_c;h1<>_2G1|k%LQm~yR1B9NUnBavm zt_fEeO9>PO>&%B4Q>8MuL(PU-eY0#JHDl$~tC$Cw2_ zL_S7+6VHEx?}h73i;3EJWWZa!}ji+8Yw z$eE3Xa)t+VAemtDhFL{YEsHmh(c)M5SBk9K&6^6va9;#<2fb}aZP$2BwzA>o9B20> zIpG!A1Oa8A3JiMvv+V14_JdfUlzU+mzccFc$0mrgPy;vh3`zC;)3!t%iXtsBjbVSACFrTfNq3;4XH%-F-XfH6GGoE-EUj z1Zd@TEXq-?=`LNU9V{aH(YHN*@mLo$%Jt?s%wex^QK{u@I`@GWx9~ zJtle_Jw8*Q^~eo}(6G4?X$l5uAkn)=nxEXi64@coy~3Nuxd>z)!Hx+ugm^m<>Z8}M z>}@|9ensbnCr*#~`n44%>ZqL3W5o_yXW8j60-~@WMqYWo_z*J&BI$qusU&*y9-BO( znUDzv35L+mt35h-#Es$eaz39W;x1W3igQH@32lO7; zb)yenQH`!{d7rV(28+wx@O~^ZRvLKX3YjJ+i|kOX#8nF6guJP8DWxpw)?Eg+M!6fe zOp9iO@jHoT>i)@Ibkq)x_$#O7vV zsBl3GSWsT(P%xySqM=pDt+kZzuLS=NW2cO^kU7oGA33q^x2~%tt_9ZCQ{v*u;bn!q zGyNu;D4W~$ESk&>dm(=4n@TR(SpPQ07TH!y+7o4V<#J|2*8q#eji-aVX>eWKGA!LV zU2om~S`XXINuOfnG4L4v1gQ;~ts_+Y8ypdo2PpJIK2`-)1v*EJmPt)iO|5L3?S9$b zo7^7R9-o|YA5|X7EYr&eYS%}G^OmKES#Y_cyGoo=p2Ep*^@_Z@r!*c@7LgO|*fgkyMyOf$^%&l`5}ckro0Q9?yRZ(@|Xow^yinY+nW zIlBs?3}4=zT@Al2f7|$0G{Zq=nEK@yq!8R$bALt4yZ%#W1yg{r)NE0O`p z;`%L;smgJoz2!?kz>WScuAT{vlTGIhdJP4Qj&5y3Q^s;t$yLK%kHu~Qq&~Gi&8Kds z1DErM*P+RQS}{0%l(Uh!f%LvP(>^dn|odS`bNt+A`TYlY1Oy!g)E!RL8jg1%%vhpV9!l%|hm?xg9)``8zsPt2$d zzvWL!65T&?gSp+Ep;`ykcGNCd(_P2i+W3P+xcy9hvOltGb9-@}5tox(gf+{Q$f)o& z^J`j)4W13@C(IB7v$~2m^CjWMb zCyN&-TQFgu%dnreE$q55Ag+^QS3n)7k|?;j3yJn`YL`8Z{_P*2@=D|9oDNnE{=ifk zXX=7@x5LnwunKk{HYg=>m}I~{-BQHyxcTN(__Ur=k~ELoO3$jb(#!ZVXIbWDWKpCm z>AmindhL&`if7@9x)IS4WF!)|%vb3@h@y#fde0qOS{F`e?lP~MV)BWT9kgMM>e@c# z14X6Hd#ijut6i5=u3CTJ1gul^i)uEMO*h9ks8*lMvMp)6EUD8ZEB9}(I7#cOE3M17 zz1J1=TbOZ4KH=Og+9la-Vryql6V$fTZd@@n*Ib`uyDtuj4xvhR@U2m3*dDWJ8$8#G z0)MghZqsN!?*cE|M^9=_uFr+f;eaI;<0@Cr2=?e~JSq1$=B}-i&9tb%0ioikQiJ0%5{N0wR=dtPR z>In<8icPkNg#A9&PUDqbU(Lbh*e0EiqCXVow~CWaCIMf$lmi;78eS^S^an^Jw~T@6 zcDa+=W?{Lo!RSFU78wPr}9pJD0B)Mw^c;}{|Mkl)QdW%*kgW?t zGqFJIk%f?8#QFlwR6*(;E*=uH1M2D_;(LU-wt|JSG7=l&9uo--nGy*dafgg3;>cA0 z-peAhBBB2E90ds})EWuxpE_?5*N4w%M0sfQ_Z2lh1PKH2jR;Y^zM}l8jY|F%_0K)> zgFK{HYElXch^v~Zi@CXjtCgdh>SCJ<;sKVEypAgp66v#tf~=s*bcE=C%3599O z#MIHA%h=4(#GK2^-szzqBrz`$#I3!#n=!qYy`6)rh!=q2uNoqV`-jKe4D^3hakB+5 zXe+;?mvVG5rx)Pj;o@Ns$EK&J7jrSQ5K)zu{iiwN8-T&e&CN-Ko7>aVlgpEz%hAP> zn^#y^n45=>n~#qZQG?Uf+riD)i_^iC@$XLl^doKVYU*O`yIMv5BL*8-Rh~ zLD0XSzj2y-S^p==!S$bEAqL3(@P?b0i--H)z7b8u9-fN4v-UE#(~-8eN9YV8L!3`o zNbIlr|BpBSN&KId+W)l_6z2QimjCnS|K3u=)!aqO(H6(nm4+C;j25ERWET-%txxnVL!x zy%Px9Kt@aXh-4YxBgsEdI8tP2l_&e3XOBNZaFDTasgeJ;;$wM6$$+?! z_?Ukh{!#2BNgxZxVF3-4`UoS?({=4-fssU0Hyj!0aeC#$H?;Y1V~EWtyw z{k+ap?)YYd;f-jS{pawd`(0EhUWvh4MI~%)^WNHrLqPfmx8wHKK^Vp#26+jgJRy&L zu|_9xJj}`lu1tLP9V@sqZHNTse$lX)FAg=$@;l6onL*hi_=m^=dPH9#4eONx zvUsiK`g2<=UV zfxA6>&*tWKExV-s;44>?MGU{*Gc}w3_7ZT&JnZ!pfzcMc{DY3*%}12gAym-2_&fZf zOXaSgK21cso9&VUhf8(M3C>+8s}=Xh3-4V1^Rp<-cScc|AFl6yhQp}brWjV4!Y`Uo zzVb4!f`C5IdxbrR0bf`QN^a**-akTQ5%^@^CEIOq@~nAsX4MO}(HxH+B`>9>zz^+` z?_I~bTjv(kPq<{1PDN?dL>x(^6@np6D616h6s<$wO?l5Xc=v+hX#DSIqjdybm|kTP61CaNX*~d4He2z4}Us*QgFTncd<&N-|2W z(c>+mzjb{?kH*S#AU4usHm#=3L2(ZI0RE71+3K-+hXzuwLwHRsUNzoX;hqsLz%MWooR9D;pf?(MTu8tP*!_&$aU5- zt03|e>7hp^Q9=JiZTbWxbXn#>x!rypa?GqcGr!BVc^#wDR(GAK^8*2nwKmb{sKE~C z&|>c<%Ad{&6cL6s4i*1-Bu~_Ia*2DOGD}RfVwY;k&NF{;Tu+8Y{E6$2Up_`q>grX> zN5LY}SBH0@42N?;b>b+bUhZ^o8d(nzazZPJsp#mYRqDT7@G%n!JtUKalMJmnJHEny zvPaLT6drf=n!DfHHUvkxOe665Q6Lzc(aUwrp%g@*jA z<5%`R(KofZZ-tb+%^EW$RSWcZ5%5ogWn_eZh(YKJ(#hpbWI{a+W8))-TF2s7%o;79 z^E5khYc8?%HdMa)laT`Gk+Gxb8$$gDQZsN$2HPPsPz#AgWQ8PP1j;XKHBbF!F>8W< zgPS-aI6z2z(NGlZF4bHtK~Ko%ey#fhkLM?M^z!Wq=l`HULXkqimXdzq^=z5r(OKic zJ!Ea~v=Aj%j|o8Kb47*AB1e(_B1vRq?+=SULiiE+bRHs0{~7Y8JC|uHGo=Fltzqy} zeOdDb?%gF@Jq>#s#);;uB??Dw-)1Yke=teXAn=QTFSlpXqd>L3>iY8ho+ke90(h%C z?d2>22+6bK0GG}N9Dk4*IU;}oiQCeeB~M@`zn6*cwQz^-#)($V8s$jIn_IljneqP^ zSqK@?H!*IzwT2V(N`;4ZB#t-ac_7Ma;z=kfi-b38^vY|JxBm{{Bf<|L1XEaOM9}0I zvCd$z?`rykk+3_FyPdko zl~>YTx&@xBJQ)I*A+;#>(a)iO@cg5rWxRk7kvMU_?*dlpYHkeo#na;!meOC%2(9dQ zJ|gFm`V*E}5$L6kv*jBEM;+C(Ve0ejg4=kq4_q@0^n`z|C)?F;!2S~lRuG?jcY!Z54#`a-bC6gE?kjaVQR zfYx@@&UzM`MN-y>pO7iEtycnF?r^&`jK~+$_H3k#2KcaF)u@a<{*!1znVm{dR!iFM zugA3v?-|@BL9o^PtF!TJ-<6%yVbDcE`9)F}=yF{`JMDTFjcAe#Io_$CU=h9?>vtlc zdzh9HiT}2Vv-i4OKghWPeEwgJYR zpF_KD?;#Y?3cYL4&NJj zojEG4{IL@jx zQbWZ;|B{UUvl7C9NM~c{vh=((%PFn)Z+r>3_>@Gr8fh2und^19*|vn)l!MqxRORWeN_qfXGJ0 z^)-xw77fVnZ5K2_G5@->Bq_p$kQESxe$!qn;Hlr?vHSDuLx*S2>EPU2G8&QQJJhX2y@Vf23o^cfLI zOd%&@=v@%7-u#`mQy3?A8?cIJr%B@K_E%zTaOVK@E9nQP9YV*J0w*8>|;pUghz15C4ZOA_f?1AYJ zS_GCyUl6svDmwg$XAjejW$?obDMf?{JM8~e1LDmK#B)k=+t4TEw(1RkCO3bfcSmS? zSKpvQ)u=gV`9C*8pnXVDsM3fcIsd=HC8a*11 z+jiTgY+ucvL(4bxY;0|_yM(Ib4(bKU21_nl#ZFoE`peC~UvSU#y05eNpBB!4A^6L+ zfP_M|kk6gVhuPr)1gJM|Er&YuOhhS#L7Jrpy{u3+ESn*sx|ak9}M1gRsrn-&a*G1NOxheFAtg>DLD6^SoFyp0x0R~(gA1}43)M4 zPmv{wp$0tD{a%q7@&U)25;tm!&~kx?4A3wOr0myL=lIP?6QlHb;y$Jl7#Yb7&{82E z?g5*ZXRPaLID2+vM06A<^;5*?Esd@M&WJoTT9+pc_migcx);S~FDNaCO$_{oy)*N~A@ei`OtChThA)myaP zPXzk3+IfCIwPBtC8+gaC<0$am?RA5A&%sBy)a4tZ?f1Cz(geycF5mJ^`kOz!DOSFf z0=L-LPBT)^GQ1v&osBsO&fSyYLaWC5LP}2gA{LjZ2Eq_|u7eX*6Dt>VK6_Q%mbty( zk(QNYtE0qG-E2RRy(Vmau)k`zpWB?+nJOf0KjW%2)T6wYymr!dmway!^V{ks_KdLV zLu5GrV0zsviTLp}as5CD3Fen48;N=I=Jm+vU65vIJS?v)V|XTxU&R)+C=@?)+c z`w`;i>!immZE(l?>j`vCrVt%(nYqk`&|(ChnYPf_!olDCF20@0LLbgcEVS05hoH=# z4U97Us?PDXn2xgYAq_j)0~`mQoID+ljICSemj*YYHPJ_jObQ#*B;ZuxMC)wkR7(|} z60gjKy9jdEN%z9dOOgzqlLm|cb|QIpo*`PH+(2S1CQDWxGqLjc+tO&|pr$h^qGnmw zgV_(87x59B`<)u_y&gehEfdZ6M*5w1R(l_YSO68;1a*gumophLwdgaC3+X?r8tFfC zl0}mJBrhX*&Lm;7If;W?^|bwNsFhX8EYj)vDDQqr)F0L$`}5b?j`&8Ed$h*d>|}~L z8M#)voXPzj$m^@WY89=`lH4J`FpWLV$=n%F}RCePU!n?^S()us4PPkSQ5K1SYLcw zus|+dYzG_RT?^bsdGVb=R&GCf{LPp){frw;nOCj8B>EM+Jqb@ zPjkS{`&-r9seX&KU6=dO2%M@JzWRe5twfow#MHr|GWIzxSChuUD}_#KiVOu#*#L#-<7a{J@%3|($SpNRj+kzIya z_=Ft1^s&Lmq~cZ`;G>~Z7>*FMRzJj)<-yNq;Eg;SApLFp-vGz$s2BU zJd`fPrns=wo6X>gfZW-?@xEb8?C4~4b6RTI0c`-ii!d@&NnOF}S=@)Y7me@@&o;kn zki}mrlicXtJ%S|> zBY#HNBh)@O@FZF1f9l-02fYtaIzVzcUjvK{n&Lv;Z0#Rtk2MRW;>%eKP$I zFU0)ReHT^_&_kT2%(&Zq?S0v9&A&V@rX|5?f#vmt@f43J>`%5N#)6lmSQ7JZIrgg0 ziBhYlcfTua$S2zY)LRb#N@uAynbL1x@XksJuG0h83%d6K!&O&-PpJD%Ot%Gd#V_CF z=rv7xoNvDrmGzN31YYg^lsy|Q^Xo5%nq|X(YRz4q2#Lfd!JY5}*ok>a$blTRltOco zE9iRPHfeBh%{hH7y)L_0fE=Y*ZeaZ?-ruN-0wgk8?7wQhYVg>&kD8wI$Htnw63y5L zeEV^JG3je@Tclf$$!**yNA%otP@de&uYZ)&2`?fQ&R9ljCQ6ac^xA+>~9s2S3u?d1NL4MkSEjUP>2d4g%A zmwh$+ZVjiiw%=gg603%b6(O0kiM_RprMgtX7(IJxs>jzld-#HCdu~UxSg!LnvDI>> z(7(EZTCO5Xqj|!hLL4?vd(-FKBd^MSPTZQ*pteG{DBQA+qj4-`x(9aK5J4+{ZKQ3a zj)oDO(BhHNmi_cM?EZ zK*@DEE==(yfytu4XHa)&-=?%&>B83$GEDWWzUti8eu|#9j$2qAUF`o`dt+de10VO# z7gJ^J$8UsafN|S@t6_njdG@KqIk5DWbM>u(k zNh^vwa~a-Ay8(cb3%{^x$X?i1iboQhk!uaGM`mkY01MOD6vSL}Y({Ae6-9%A>uW=n@ zI9k?H0L}TEv{{)XIPe}7_J*)%#zp|X?PZ5$kjjQf-eDL`E}C7}6t+Q5?0VC=)^QO< zYl|iO@8+Hwn5{~E9Z>H24SIo+tcE_HyFS(Mh3=Os^Q?N8GcSvS;Vvl(?(0{YGi%c| z;Vm<5H|uftR`!B8Q4|*|n*$el&My_iZ`KhFAT^IUJpok5P}LN`0h_Ah1u z=3+*wUVopqqDiKWy)fuBahyD6i_$f4(SKk0rj5~o#CeE8YnHl2g(^J;k7NFVgTtmu-GmK#SNv>s3+unOH7v|VrGimhwiD4B#!FMKJ#?|^e!Va zMq~+*8DOy)#zYoJW``UJvF#6?{cJ2>TbFg+4h%3XephZwdnI8;KS){Tx5as8gf!sC zM;x48&2RNRw9ZWtDmubGvl;`&IIgiVw)U|{iP#bp*Bfd3Ew&^gvP9ciF)hIN@Z4kT zuKO1KVqft3_GFP?(yBd56B+qTz_*w7UE6d1v#at%B$<0<^Xtyf1mm8CXZm+4;6&x) z%Vp{9OCSiNyP&w*t(LOzqL9Z%(~fA7``7cD;2xIk=G#6<`h zxK&@O_u>~aD6&-2Uaryk4cmY^Q7DfCXe(OWOW!;yGaWOnYS#KzqA?udoxU%sXy9V- zQwg^JOkRVp%=8(IrcvVw`Ka0OuWvT8LFC7haz|7#yZ4U`2HkS=yvdXe3U;R8ktI#{e~ScDls z+7Beb`fUD+7{Insrg0G;xY_(EwdZi#|5^4)Wi7Z&n~u>oTEo!ZGpIs+pk!}a?VG9N z-IkZxel9G#3Q~A7O1lf#Fi^SsfY<}Ul@<8yHQKwV_KfjsUq_3s@i(Q?3oRybLb`V? z&X=^2z%pBs$Vl^V6`$D!bj=*Aj`S|MPLdy& zzWJ{WJobtZS~jz_%YptSF>IE|oMm|b<)wvN^Y5B;blFaNEdrCy9J0~IPYZUEWkxjx zzx4JiD*1n-0t6mJa{u5tZ|G@>kY1`D(dr=qa#ZIp>}e9Z`us5LEwGp=2N?bo6&)(w z=7e5MwY<)92TYq(HXA&Ql4E^1OYS_j>p1m#X_asM5cgNh#$r^AmTwY@DUS_dcjViP zEH-}R^FoEWnrZ+Xra$Sl2Nb343#6WqM#t?_NuVD9sF`^9ML89)}Uwoj-$KPF1yIRHxQ9S8u8~zEW!> zF=g=6Gb_R8zp=B(QFM%A_hn1l8*qvg)6{h-zEZhqapcw4n& zZcjETA+l*PRmsW4?}yD&B_nX1@37E%z2InQ(v;=654>wS$AxyI3E)IU&_8kCsG^|L z9T3?{%LrSQVYx~3ZP`Gmfm@s-e5@v<(<#eD12{ zf4Eyx0XqJ!GYndKvDP8;ut&p=V0rkMS|{;4OJX(9->5<41P)Gc#p zzS~sywMcBnP}o}R^<^>2FFwuv1{!Byv_mD)n$6^`smM5zlhWOQM7`h`Ak0Pm-ikU~ z=NGs^+CB{SX_`L$2^HYJ4)%C|5wTV0HM};M_%Px=s-Ka>yG+Hpx1>8MZ=ZREi&Lae z-lan7^DX}ls0aXYxUyv5X~|lW0U$8xi$s>2VsF#9g&jiS^05}dO*Y>*c1wX;LB}Vl zc65n%2v<|J1XPT)2s)6Q__P*z_*{^PT%>rp)zn7s&_%EE+fI`vhvB`4Co4S*d1GkT zh~~k`BYn=f*~eqswOzj^_sQQZ$F3K;JM~fbr=+{~+-}|%tuH^e>jpYD0pzj91n zkeE)4SFuslK6w6O-QKlnwD62eXICkDWp@c;4sGD*TVi(bv!r{>r=nfU$91|mIsZfO zL5r~F(~8Y>(ntLQ4~YJ8#-aMu>$c8kCQ|D6I*VeCjFmb@148dq&XQcuv#J(LMudsZ z7>Uiza$kPc?9p5>3u+$FpA!SALk(;xly-#Yc$$S-hEEW-zsiO#@Ua_0gG=LBxWB2l3{PRHb&-WGt1y z077>w#7@H7w#oZM>?rJ;;K-=d^VmY&nAFHr_g6(mkB#jfjXR^HX3%bUK+3*ln-^_KO4W>^Q9yKUk18n1g&A5-avw zZtM&gKx?O|u0U(Y7uz})YTgEjKyg~M{*DC4$4BeRQpLcw;=JK?_m20{EGfcwp%%AR zk&06pl%3^CRNZw5N5v(ia-WUs)4=ChJ<}7Bb1Pf6z$#32$qgu2Pe~2Z-Xohq%&7m? z3V*k;b^nw}mEETB@R@0x?PQgSzO;Xre!Tw_m@SjjnVbEoc1=G6Xuq+gTw})Je6lce z##Ql*{Ms{EY1hYSUHzNsxi%Oqb50lifjt+H6)vi$Ukxp)>HeIw`60FmT~^~UNy-fJ zYgmS}kGH>lq>!~+bww}KmeF7)!Ieqnd}p8Ey?V!$ly1v;Mz4FoS$me`!`CEu?dHsB zw@4_vunxR_-AK4RVwBXc#H?gt%6a?(cu?Y-ZzBFYnJrUp^$Q@olQt4mypZG<9HSRr zD?{g)UU>H&l&`Wc3{EBXI(Ip6^I^JJ|C#DZ=2(2#fR*8j9K$$e zI{)TnQW?oS*vz|JYt|v!oz^ekKC$hLNH$K7x1gFrV@v2agIzVa@NTNHb;7kP8G&ar zu_GR_IePo7m2-ckIYbgMiKVvVWdR_y3~ z68@|uDZ&0BCJj4ci>RdDs&HY-@8b42%J3OW2?1TF!n%)_d2Hg&$N>OWJFG#cb^Ru> zMZ@&v$u4#yW9nj6#RxAybIe}&4hW1QwO6W5r{}M6T9Wwl7z43lkV=!Lfnp1}R!uA}5*f~tplx9jmLWTR`IbaH&qeIC zjjdPngWDm6l%ItKw?F;r^|AyMPTua)r=+sSJM3hxZ zJ7&R2PQ5Fe=kU?K$ypEyFb;T6A+7OD@$(qC#V=S**6ks&+vk2vvj$Fe^eautUa&Vk zXrTk+Y^DxQC}I)QnWpT+8K{BK!j}4AZ*C;thXeN8f<)s?+Y_ai6`I__L(BDhH2^Z8 z|M@w$;T~mgaV;S2st&y6oS(%gn}$A`f{K@U!>LRyebB3 zKPxK)Jw&pYc6nWL>-WlaAT`#-lJ8CWU)%B3O+}}p3^jYrHlq~i6RB8jwNEMClHg|5 zfy05dUR^{JAm9)7W7~{pH|j%WleyU{ zv6!pv>evg!^a4N(Exa48^S2n1YZ&OU?e^Xkn#%^SiOssJ1ypr^5=ChPJK1sK=C+Dm zmc3HHDK&YShBV{S;!y(s#}Vrp9-CLbgPVc!ViK|BY*$lb7+34bSVon>Wh%eT@i`@Q zDom(W@_tAYQ~R}=ER|EVk2dLenRPXA!a006vH=MOB(26D2$J-{L+oz)ig}Fw4HJ#$ zwk>3n_iX4P)6e(V#HEv3bE(&K!1gHLQKV>>Y=16jo~MW|E*p?c%OTgjE}!O_(>&p) zj2n>lF>Bk2f39|O2Ky*pUnil8m~qWipK?GMS3RGwDAP*cEvCE2lx=IQjN7_gG+Ugf zw@k^7TApbd-de7WS z)oo>bCD5eSSlaRut)NA!A zl_~5pyY~5Jr%|P~4hFJQd4FeE)P@FXZZ0|p$Jk=oc2Vop0E+KPaH2e@`JrR1k(zWd zOGG?mG)3~$JJ5QP!Pfa!G&Z`^|G1)5avW9+#hA5(#teU5&Ga+x zMQni42B>X(GF+MKKc3&6Ulr7F265{FT*UPAmLk>`5R=~h;t^+i1M`lAzcepVmtAG5 z4B8cno*nGUezPF^W9hiH3Gx_|f5`o1*rJih?y{aQM+|6FX2_(ivSj)2JfMtsV%Kuy zc&Ife!(Bqu-!sOu#_o%AQyUI9cY4$GLd^QfbiZ^~EOVRU%zmRgz$$y2Mr+G!6U8({ z>tdR-+ug$N167Urea0NtEeLU1^!~o4bs+``={fM>sOWwsV3-=p8_s%8wH^o%^kLQ*EZ7-BSrweTkLbjj+-y13r5OEx0 ziB;gX;QnH0z2NY!;p%D(_5OSx#4U1G==}U#;uFXoe*oQvUEmHHdhdPR4T3cZyLz@Q zQMTo}RJ)|UiDH}&)xGLDXS#OjC<+dB3hN0TJO26tpHSfL*V6szx?+Xz`Vd;|ZO`?h zP1`h!$CT34F}r2k8TbCwgsz%Ey~|LiT`}E-GO^bf8&8bL7QXZ3qIwzGQYA>Iq7h5_ zEW5^f+ul?#{~HbA9drXS@_hxCWEWLwzI8aeOM5IzS&#;Nqj(#`BVM~UNO(&d>A4N? z^76Mm7PVvf;8^#}p7BFWC9hT~n6$O1On3>gGHoI7TJs$8KUVRxYCKRCub%g5LqM1o z*j-0ULl1FViL+b(UhZmUUtQ09@x`-F5IHt|`U}8zkE-c8o_+YTVLGZ*ddRL`4R3qa zw_)3IVCNxghRq>MS=HRl)kvD4TyMgQgQRvvobFn{uFJO9yIGZ>XkiOlQ`aHSNVtoH z*LkF2Ycs6RmOw>6C|kdq55BjMJ)d-ghX?w80Fa`aC{HS&`~`laJXpmENM5DINKOf6%$y>i6#_?zHU477{1!i zRNHUqpez9_MRIu8T#6~II;_y|*OCH@@P4h29UFNqPWuE!0}07Zl^DP(>tp)3Yx=3u zn7vGw)o$^?K@2)^{+67BOsyh6OMMSQa+Cvk!HE}y-?_rhggz{Of;`rXA&niwk!U}h zr5@2IPj{T`!ffgy=V$~7j_ARb*jWz6xXf8m+qw&~^Wok!mg{v;F@yM`4rH8r|R*tTX}taR$OUDbCt2%UlyWlWR)P-P*lv#SE2kPEHZEE zQQf_KRnLO@T(Xm&*PtMwW zNEYUS^Vm}V&KpPIL!wQfx5Q|jvl}%Kl~+mpi}!&ZaSsuFaxo4 z`k+F{J93pY(BfL0H=IjH%)p~+rAPNsP0zL8l}yIx?S!PNsg#Y%kLF9EF9x@S{ys;s zF~RlGHjZd2_P`=hV6<$m^EtfJEMB*{SQf)Be<$OKLPQJ<$!pZO>9e;IK2CBa-DS zd;Wp}+w`2*cEV7&ALra z!hts+?$Z|)*!tWZUzx^{&8u++CRM|+^}?Twp1E&s>~%=>peEThTB}cQ#!`BGeyB_p zd`B23w` z)|VK6>e?N3ogfM}o1?YkIG2*(7+DR4mODUgU3PSza4o9vX7 z2Orc6C4Mz7CNwu%OSG#@_vUf9FU6nM1%<%cf({8+a~_Z~qU0V#juY~xG$j#|xdz$Y z2r}~6Ih2Atq>b|oyqA>f7UI1Xo;G*TYnZTi?`2DNxYur+^GAhY<1dt(aCm0sLp8#M z#sy-9DprzU6}aOB0&aRf^Q1`{>M~;2s@c%Tc{|B0t4W|%fysMS0iUyk(}mUEo+sI} ze({cVjo>_pRaHe2a%vcO&PA= z)}G!mG!#g9M7o0+75EJE;0!Iqgpq`V{*&YzNgU4^(x)OXE^V^4^91mdCoKqojC)kG z{XSVpD2&7THYJ|#zj(wzxM9|?;{zvzSvp}RMMe;N^w3*bo=svTdtu85Z z$+Vk|r8!b_O4$e70;vTR30&5OHmLpFnDjilx5Q}%zc;S`)>L`RN+(9IUN8&bJ|Jw`S>#)c-M%1lX zD6lt6Q$e<08Xj*on>2(^{KEX}%#zSkWHecsk>tl@CPPyZ5Nv7cAMa7vcqW=T*R=B* zrayuD(uDs$xVLPFkcm#1{Cm&x@NSLabSu1_HSI`vjt(0)pZ=P8yuR%^!~r{-+ZhlH zXt^2$H7}p08(y5U&;gpnK-YD07KT?P{p&b}e)FxDoWJkmZpXZA^vj+1wVs8D(DYiK zZ-<>0dEvt+uZZTJrtX&&I4f7YuTo!~=B7K!YHG21q89{7c|~&l+_rqTvYi!5G51!= zNXaMt!_xZl{s6wCPOT|?xVgA?#%aW1*v$w+ZUnM2|p=4rU+2OkX2rS!-T zJec0*>`bVgyJq{njYzADcn5+3>-$S(uHER-`7eYw#-%-tt1`A~mNbZVjj1@R=)k=7 z2DZt`%25BCT~#}~u=Pw^#WiOvcJ}P2$-DxJS3ryglV|7Oo|f0!st1K8S_aQh!{m_y zj(hT!s?~YFcpMs#=SSE0+Mql`F~yWe548iGCqlWZ4fqwZ>3pyDPJUK^>Uzc{>IT|7<&s_CiT0FU`VF01QcpN8*sSnDm-d6uNk5r9Pa>`pgwGU-cym08P_U){S)f)VaU;zbE81lp{K>zb$ID2 zxmqO&HFD^ivgxjYvwdU1fD3k?BX@Rp3VwZkB?b6>b!)BRGXWYnIa=4tc3UhQWgdT3 zH3gnn`o~Xs81`zx))Yy3*;Q8*MoS#_)|vcN1FQ$nw7CI0B1<^M

pe$eBC)0qI43pG5E& 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 Mission {# Timeline sidebar #} -

+ {% if milestones %} + + {% endif %} @@ -206,36 +195,7 @@

{{ member.name {% else %} - {# Placeholder portrait cards — shown until board members are added via the admin #} -
- - {% for role in board_placeholder_roles %} -
- - {# Portrait photo — fills top of card #} -
- Director placeholder photo - {# Gradient overlay at bottom of photo #} -
- {# Role badge pinned to bottom of photo #} - - {{ role }} - -
- - {# Info below photo #} -
- -

Peter is a Django developer and co-founder of Two Rock Software, a North Carolina based software development company specializing in business workflow automation. He also enjoys solving devops puzzles. When he’s not working he loves exploring new foods, gardening, and movement practices like martial arts, aerial dance, and contact improv.

-
- -
- {% endfor %} - -
+

Board members will appear here once added.

{% endif %} @@ -247,8 +207,8 @@

{{ member.name
-

Emeritus

-

Board Members Emeritus

+

Emeriti

+

Board Members Emeriti

Honoring those who went above and beyond in their service to DEFNA and the Django community.

diff --git a/templates/admin/base_site.html b/templates/admin/base_site.html index f8407fe..e4682e1 100644 --- a/templates/admin/base_site.html +++ b/templates/admin/base_site.html @@ -2,9 +2,9 @@ {% load i18n static %} {% block branding %} - -{% endblock %} +
+ + DEFNA + +
+{% endblock branding %} diff --git a/templates/admin/login.html b/templates/admin/login.html index 2c0df3e..d4e65cb 100644 --- a/templates/admin/login.html +++ b/templates/admin/login.html @@ -1,266 +1,266 @@ -{% load i18n static socialaccount %} +{% load i18n socialaccount static %} - - - - {% translate "Admin Login" %} — DEFNA - - - + .footer-link { + display: block; + text-align: center; + margin-top: 0.75rem; + font-size: 0.78rem; + color: rgba(171,209,198,0.5); + text-decoration: none; + transition: color 0.15s; + } + .footer-link:hover { color: #abd1c6; } + + + -
-
-
+
+
+
-
+
-
- DEFNA -
+
+ DEFNA +
-

{% translate "Staff Login" %}

-

{% translate "Sign in to manage DEFNA" %}

+

{% translate "Staff Login" %}

+

{% translate "Sign in to manage DEFNA" %}

- {% if request.GET.error == "unauthorized" %} -
- - {% translate "Access restricted to authorised staff members." %} -
- {% endif %} + {% if request.GET.error == "unauthorized" %} +
+ + {% translate "Access restricted to authorised staff members." %} +
+ {% endif %} - {% if form.non_field_errors %} -
- - {% for e in form.non_field_errors %}{{ e }}{% endfor %} -
- {% endif %} + {% if form.non_field_errors %} +
+ + {% for e in form.non_field_errors %}{{ e }}{% endfor %} +
+ {% endif %} - {% if user.is_authenticated %} -
- - {% blocktranslate trimmed with username=username %}You are signed in as {{ username }} but lack admin access.{% endblocktranslate %} -
- {% endif %} + {% if user.is_authenticated %} +
+ + {% blocktranslate trimmed with username=username %}You are signed in as {{ username }} but lack admin access.{% endblocktranslate %} +
+ {% endif %} - - - {% translate "Sign in with GitHub" %} - + + + {% translate "Sign in with GitHub" %} + -
{% translate "or use password" %}
+
{% translate "or use password" %}
-
- {% translate "Sign in with username & password ▼" %} -
- {% csrf_token %} - -
- {{ form.username.label_tag }} - {{ form.username }} - {{ form.username.errors }} -
-
- {{ form.password.label_tag }} - {{ form.password }} - {{ form.password.errors }} -
- -
- {% url 'admin_password_reset' as password_reset_url %} - {% if password_reset_url %} - {% translate "Forgot your credentials?" %} - {% endif %} -
+
+ {% translate "Sign in with username & password ▼" %} +
+ {% csrf_token %} + +
+ {{ form.username.label_tag }} + {{ form.username }} + {{ form.username.errors }} +
+
+ {{ form.password.label_tag }} + {{ form.password }} + {{ form.password.errors }} +
+ +
+ {% url 'admin_password_reset' as password_reset_url %} + {% if password_reset_url %} + {% translate "Forgot your credentials?" %} + {% endif %} +
-
+
- + diff --git a/templates/events/djangocon.html b/templates/events/djangocon.html index 67971cd..d74b387 100644 --- a/templates/events/djangocon.html +++ b/templates/events/djangocon.html @@ -139,221 +139,57 @@

All Editions

+ {% for edition in editions %} +
+ + {% if edition.logo_url %} +
+ DjangoCon US {{ edition.year }} logo +
+ {% endif %} - {# 2026 #} -
-
- DjangoCon US 2026 logo -
-
-
-

DjangoCon US 2026

- Upcoming -
-

Chicago, Illinois

-

August 24–28, 2026

-

voco Chicago Downtown — talks, tutorials, and sprints with opportunity grants available.

- - Conference site ↗ - -
-
- - {# 2025 #} -
-
- DjangoCon US 2025 logo -
-
-

DjangoCon US 2025

-

Chicago, Illinois

-

September 8–12, 2025

-

voco Chicago Downtown — celebrating 20 years of Django with talks, tutorials, and sprints.

- - Conference site ↗ - -
-
- - {# 2024 #} -
-
- DjangoCon US 2024 logo -
-
-

DjangoCon US 2024

-

Durham, North Carolina

-

September 22–27, 2024

-

Over 300 attendees and 50+ talks and tutorials.

- - Conference site ↗ - -
-
- - {# 2023 #} -
-
- DjangoCon US 2023 logo -
-
-

DjangoCon US 2023

-

Durham, North Carolina

-

October 16–20, 2023

-

Durham Convention Center.

- - Conference site ↗ - -
-
- - {# 2022 #} -
-
- DjangoCon US 2022 logo -
-
-

DjangoCon US 2022

-

San Diego, California

-

October 16–21, 2022

-

San Diego Marriott Mission Valley.

- - Conference site ↗ - -
-
- - {# 2021 — Virtual #} -
-
- DjangoCon US 2021 logo -
-
-
-

DjangoCon US 2021

- Virtual -
-

Online

-

October 22–23, 2021

-

Fully virtual edition — no tutorials or sprints.

- - Conference site ↗ - -
-
- - {# 2020 — Cancelled #} -
-
- DjangoCon US 2020 logo -
-
-
-

DjangoCon US 2020

- Cancelled -
-

San Diego, California

-

October 11–16, 2020

-

Cancelled due to COVID-19.

- - Conference site ↗ - -
-
- - {# 2019 #} -
-
- DjangoCon US 2019 logo -
-
-

DjangoCon US 2019

-

San Diego, California

-

September 22–27, 2019

-

San Diego Marriott Mission Valley.

- - Conference site ↗ - -
-
- - {# 2018 #} -
-
- DjangoCon US 2018 logo -
-
-

DjangoCon US 2018

-

San Diego, California

-

October 14–19, 2018

-

San Diego Marriott Mission Valley.

- - Conference site ↗ - -
-
- - {# 2017 #} -
-
- DjangoCon US 2017 logo -
-
-

DjangoCon US 2017

-

Spokane, Washington

-

August 13–18, 2017

-

Hotel RL by Red Lion.

- - Conference site ↗ - -
-
- - {# 2016 #} -
-
- DjangoCon US 2016 logo -
-
-

DjangoCon US 2016

-

Philadelphia, Pennsylvania

-

July 17–22, 2016

-

The Wharton School, University of Pennsylvania.

- - Conference site ↗ - -
-
+
+
+

DjangoCon US {{ edition.year }}

+ {% if edition.status == 'upcoming' %} + Upcoming + {% elif edition.status == 'cancelled' %} + Cancelled + {% elif edition.status == 'virtual' %} + Virtual + {% elif forloop.last %} + First DEFNA edition + {% endif %} +
- {# 2015 #} -
-
- DjangoCon US 2015 logo -
-
-
-

DjangoCon US 2015

- First DEFNA edition +

+ {{ edition.location }} +

+

+ + – + +

+ + {% if edition.highlight %} +

{{ edition.highlight }}

+ {% else %} +
+ {% endif %} + + {% if edition.website_url %} + + Conference site ↗ + + {% endif %}
-

September 6–11, 2015

- - Conference site ↗ - -
-
+
+ {% endfor %}
diff --git a/templates/events/sponsored.html b/templates/events/sponsored.html index 484e2cb..b4f5255 100644 --- a/templates/events/sponsored.html +++ b/templates/events/sponsored.html @@ -82,117 +82,7 @@

{{ event.name }} {% else %} -
- -
-
-
- -

Django Meetup

-
- $250 grant -
-

- - Atlanta, GA · Django ATL -

-

A community gathering for Django developers in Atlanta covering best practices, new features, and local project showcases.

-
- -
-
-
- -

Django Workshop

-
- $300 grant -
-

- - Chicago, IL · Chi Python -

-

A full-day hands-on workshop introducing Django to beginners and intermediate developers, with mentors from the local Python community.

-
- -
-
-
- -

Django Sprints

-
- $200 grant -
-

- - New York, NY · BoroPy -

-

An open-source contribution sprint focused on Django core and popular third-party packages, open to all experience levels.

-
- -
-
-
- -

Django Day

-
- $300 grant -
-

- - Austin, TX · Austin Web Devs -

-

A single-day mini-conference with talks on REST APIs, deployment, and Django ORM patterns, drawing developers from across Texas.

-
- -
-
-
- -

Django Meetup

-
- $150 grant -
-

- - Los Angeles, CA · LA Django -

-

Monthly meetup for the LA Django community featuring lightning talks, networking, and a live coding demo on async Django views.

-
- -
-
-
- -

Django Hackathon

-
- $300 grant -
-

- - Denver, CO · PyColorado -

-

A weekend hackathon using Django to build civic tech tools for local nonprofits, with prizes and mentorship from senior engineers.

-
- -
+

No sponsored events yet.

{% endif %} diff --git a/templates/robots.txt b/templates/robots.txt new file mode 100644 index 0000000..7baf73a --- /dev/null +++ b/templates/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: {{ request.scheme }}://{{ request.get_host }}/sitemap.xml diff --git a/templates/socialaccount/login.html b/templates/socialaccount/login.html index 32a4bea..5fc09c8 100644 --- a/templates/socialaccount/login.html +++ b/templates/socialaccount/login.html @@ -1,150 +1,150 @@ {% load i18n static %} - - - - {% trans "Sign In Via GitHub" %} — DEFNA - - - - -
-
-
- -
- -
- DEFNA -
- -
- -
- - {% if process == "connect" %} -

{% blocktrans with provider.name as provider %}Connect {{ provider }}{% endblocktrans %}

-

{% blocktrans with provider.name as provider %}You are about to connect a new third-party account from {{ provider }}.{% endblocktrans %}

- {% else %} -

{% blocktrans with provider.name as provider %}Sign In Via {{ provider }}{% endblocktrans %}

-

{% blocktrans with provider.name as provider %}You are about to sign in using a third-party account from {{ provider }}.{% endblocktrans %}

- {% endif %} - -
- {% csrf_token %} - -
- - {% trans "Back to login" %} - -
- - + .provider-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 56px; + height: 56px; + background: #24292e; + border-radius: 50%; + margin-bottom: 1.25rem; + border: 1px solid rgba(255,255,255,0.12); + } + .provider-icon svg { width: 28px; height: 28px; color: #fff; fill: currentColor; } + + .card-title { + color: #e8e4e6; + font-size: 1.35rem; + font-weight: 700; + letter-spacing: -0.01em; + margin-bottom: 0.5rem; + } + .card-body { + color: #abd1c6; + font-size: 0.9rem; + line-height: 1.6; + margin-bottom: 2rem; + } + + .btn-continue { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.6rem; + width: 100%; + padding: 0.85rem 1.5rem; + background: #f9bc60; + color: #001e1d; + font-size: 0.95rem; + font-weight: 700; + border: none; + border-radius: 10px; + cursor: pointer; + transition: background 0.15s, box-shadow 0.15s; + box-shadow: 0 4px 14px rgba(249,188,96,0.3); + text-decoration: none; + } + .btn-continue:hover { + background: #e8a840; + box-shadow: 0 6px 20px rgba(249,188,96,0.4); + } + + .back-link { + display: inline-block; + margin-top: 1.25rem; + font-size: 0.82rem; + color: rgba(171,209,198,0.6); + text-decoration: none; + transition: color 0.15s; + } + .back-link:hover { color: #abd1c6; } + .back-link::before { content: "← "; } + + + + +
+
+
+ +
+ +
+ DEFNA +
+ +
+ +
+ + {% if process == "connect" %} +

{% blocktranslate with provider=provider.name %}Connect {{ provider }}{% endblocktranslate %}

+

{% blocktranslate with provider=provider.name %}You are about to connect a new third-party account from {{ provider }}.{% endblocktranslate %}

+ {% else %} +

{% blocktranslate with provider=provider.name %}Sign In Via {{ provider }}{% endblocktranslate %}

+

{% blocktranslate with provider=provider.name %}You are about to sign in using a third-party account from {{ provider }}.{% endblocktranslate %}

+ {% endif %} + +
+ {% csrf_token %} + +
+ + {% translate "Back to login" %} + +
+ + diff --git a/website/admin.py b/website/admin.py index 8a97be8..e92b580 100644 --- a/website/admin.py +++ b/website/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin from markdownx.admin import MarkdownxModelAdmin -from website.models import Announcement, BlogPost, BoardMember, DjangoConEdition, GrantCycle, SponsoredEvent +from website.models import Announcement, BlogPost, BoardMember, DjangoConEdition, GrantCycle, Milestone, SponsoredEvent @admin.register(Announcement) @@ -26,6 +26,7 @@ class DjangoConEditionAdmin(admin.ModelAdmin): list_display = ["year", "location", "start_date", "end_date", "status", "website_url"] list_filter = ["status"] ordering = ["-year"] + fields = ["year", "status", "location", "start_date", "end_date", "website_url", "logo_url", "highlight"] @admin.register(SponsoredEvent) @@ -47,6 +48,12 @@ class BlogPostAdmin(MarkdownxModelAdmin): search_fields = ["title", "excerpt", "body", "author"] +@admin.register(Milestone) +class MilestoneAdmin(admin.ModelAdmin): + list_display = ["year", "description"] + ordering = ["year"] + + @admin.register(GrantCycle) class GrantCycleAdmin(admin.ModelAdmin): list_display = ["__str__", "is_open", "max_amount", "deadline"] diff --git a/website/migrations/0006_djangoconedition_logo_url.py b/website/migrations/0006_djangoconedition_logo_url.py new file mode 100644 index 0000000..a523ec0 --- /dev/null +++ b/website/migrations/0006_djangoconedition_logo_url.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.11 on 2026-05-09 13:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('website', '0005_boardmember_is_emeritus'), + ] + + operations = [ + migrations.AddField( + model_name='djangoconedition', + name='logo_url', + field=models.URLField(blank=True, verbose_name='Logo URL'), + ), + migrations.AlterField( + model_name='djangoconedition', + name='highlight', + field=models.TextField(blank=True, help_text='One-line summary shown in the edition card and timeline.'), + ), + ] diff --git a/website/migrations/0007_seed_djangocon_editions.py b/website/migrations/0007_seed_djangocon_editions.py new file mode 100644 index 0000000..72261c5 --- /dev/null +++ b/website/migrations/0007_seed_djangocon_editions.py @@ -0,0 +1,149 @@ +import datetime + +from django.db import migrations + +EDITIONS = [ + { + "year": 2026, + "start_date": datetime.date(2026, 8, 24), + "end_date": datetime.date(2026, 8, 28), + "location": "Chicago, Illinois", + "website_url": "https://2026.djangocon.us", + "logo_url": "https://2026.djangocon.us/assets/img/theme/logo.svg", + "status": "upcoming", + "highlight": "voco Chicago Downtown — talks, tutorials, and sprints with opportunity grants available.", + }, + { + "year": 2025, + "start_date": datetime.date(2025, 9, 8), + "end_date": datetime.date(2025, 9, 12), + "location": "Chicago, Illinois", + "website_url": "https://2025.djangocon.us", + "logo_url": "https://2025.djangocon.us/assets/img/theme/logo.svg", + "status": "completed", + "highlight": "voco Chicago Downtown — celebrating 20 years of Django with talks, tutorials, and sprints.", + }, + { + "year": 2024, + "start_date": datetime.date(2024, 9, 22), + "end_date": datetime.date(2024, 9, 27), + "location": "Durham, North Carolina", + "website_url": "https://2024.djangocon.us", + "logo_url": "https://2024.djangocon.us/assets/img/theme/logo.svg", + "status": "completed", + "highlight": "Over 300 attendees and 50+ talks and tutorials.", + }, + { + "year": 2023, + "start_date": datetime.date(2023, 10, 16), + "end_date": datetime.date(2023, 10, 20), + "location": "Durham, North Carolina", + "website_url": "https://2023.djangocon.us", + "logo_url": "https://2023.djangocon.us/static/img/logo.svg", + "status": "completed", + "highlight": "Durham Convention Center.", + }, + { + "year": 2022, + "start_date": datetime.date(2022, 10, 16), + "end_date": datetime.date(2022, 10, 21), + "location": "San Diego, California", + "website_url": "https://2022.djangocon.us", + "logo_url": "https://2022.djangocon.us/static/img/logo-2022.svg", + "status": "completed", + "highlight": "San Diego Marriott Mission Valley.", + }, + { + "year": 2021, + "start_date": datetime.date(2021, 10, 22), + "end_date": datetime.date(2021, 10, 23), + "location": "Online", + "website_url": "https://2021.djangocon.us", + "logo_url": "https://2021.djangocon.us/static/img/logo.svg", + "status": "virtual", + "highlight": "Fully virtual edition — no tutorials or sprints.", + }, + { + "year": 2020, + "start_date": datetime.date(2020, 10, 11), + "end_date": datetime.date(2020, 10, 16), + "location": "San Diego, California", + "website_url": "https://2020.djangocon.us", + "logo_url": "https://2020.djangocon.us/static/img/logo.svg", + "status": "cancelled", + "highlight": "Cancelled due to COVID-19.", + }, + { + "year": 2019, + "start_date": datetime.date(2019, 9, 22), + "end_date": datetime.date(2019, 9, 27), + "location": "San Diego, California", + "website_url": "https://2019.djangocon.us", + "logo_url": "https://2019.djangocon.us/static/img/logo.svg", + "status": "completed", + "highlight": "San Diego Marriott Mission Valley.", + }, + { + "year": 2018, + "start_date": datetime.date(2018, 10, 14), + "end_date": datetime.date(2018, 10, 19), + "location": "San Diego, California", + "website_url": "https://2018.djangocon.us", + "logo_url": "https://2018.djangocon.us/static/img/logo.svg", + "status": "completed", + "highlight": "San Diego Marriott Mission Valley.", + }, + { + "year": 2017, + "start_date": datetime.date(2017, 8, 13), + "end_date": datetime.date(2017, 8, 18), + "location": "Spokane, Washington", + "website_url": "https://2017.djangocon.us", + "logo_url": "/static/images/spokane.png", + "status": "completed", + "highlight": "Hotel RL by Red Lion.", + }, + { + "year": 2016, + "start_date": datetime.date(2016, 7, 17), + "end_date": datetime.date(2016, 7, 22), + "location": "Philadelphia, Pennsylvania", + "website_url": "https://2016.djangocon.us", + "logo_url": "https://2016.djangocon.us/site_media/static/assets/img/logo-full.59a2e37afda6.png", + "status": "completed", + "highlight": "The Wharton School, University of Pennsylvania.", + }, + { + "year": 2015, + "start_date": datetime.date(2015, 9, 6), + "end_date": datetime.date(2015, 9, 11), + "location": "Austin, Texas", + "website_url": "https://2015.djangocon.us", + "logo_url": "https://2015.djangocon.us/site_media/static/images/logo.png", + "status": "completed", + "highlight": "", + }, +] + + +def seed_editions(apps, schema_editor): + DjangoConEdition = apps.get_model("website", "DjangoConEdition") + for data in EDITIONS: + year = data.pop("year") + DjangoConEdition.objects.update_or_create(year=year, defaults=data) + data["year"] = year + + +def unseed_editions(apps, schema_editor): + DjangoConEdition = apps.get_model("website", "DjangoConEdition") + DjangoConEdition.objects.filter(year__in=[e["year"] for e in EDITIONS]).delete() + + +class Migration(migrations.Migration): + dependencies = [ + ("website", "0006_djangoconedition_logo_url"), + ] + + operations = [ + migrations.RunPython(seed_editions, reverse_code=unseed_editions), + ] diff --git a/website/migrations/0008_backfill_edition_logo_urls.py b/website/migrations/0008_backfill_edition_logo_urls.py new file mode 100644 index 0000000..c259c70 --- /dev/null +++ b/website/migrations/0008_backfill_edition_logo_urls.py @@ -0,0 +1,32 @@ +from django.db import migrations + +LOGO_URLS = { + 2026: "https://2026.djangocon.us/assets/img/theme/logo.svg", + 2025: "https://2025.djangocon.us/assets/img/theme/logo.svg", + 2024: "https://2024.djangocon.us/assets/img/theme/logo.svg", + 2023: "https://2023.djangocon.us/static/img/logo.svg", + 2022: "https://2022.djangocon.us/static/img/logo-2022.svg", + 2021: "https://2021.djangocon.us/static/img/logo.svg", + 2020: "https://2020.djangocon.us/static/img/logo.svg", + 2019: "https://2019.djangocon.us/static/img/logo.svg", + 2018: "https://2018.djangocon.us/static/img/logo.svg", + 2017: "https://2017.djangocon.us/static/img/logo.svg", + 2016: "https://2016.djangocon.us/site_media/static/assets/img/logo-full.59a2e37afda6.png", + 2015: "https://2015.djangocon.us/site_media/static/images/logo.png", +} + + +def backfill_logo_urls(apps, schema_editor): + DjangoConEdition = apps.get_model("website", "DjangoConEdition") + for year, logo_url in LOGO_URLS.items(): + DjangoConEdition.objects.filter(year=year, logo_url="").update(logo_url=logo_url) + + +class Migration(migrations.Migration): + dependencies = [ + ("website", "0007_seed_djangocon_editions"), + ] + + operations = [ + migrations.RunPython(backfill_logo_urls, reverse_code=migrations.RunPython.noop), + ] diff --git a/website/migrations/0009_fix_edition_logo_urls.py b/website/migrations/0009_fix_edition_logo_urls.py new file mode 100644 index 0000000..9265802 --- /dev/null +++ b/website/migrations/0009_fix_edition_logo_urls.py @@ -0,0 +1,32 @@ +from django.db import migrations + +LOGO_URLS = { + 2026: "https://2026.djangocon.us/assets/img/theme/logo.svg", + 2025: "https://2025.djangocon.us/assets/img/theme/logo.svg", + 2024: "https://2024.djangocon.us/assets/img/theme/logo.svg", + 2023: "https://2023.djangocon.us/static/img/logo.svg", + 2022: "https://2022.djangocon.us/static/img/logo-2022.svg", + 2021: "https://2021.djangocon.us/static/img/logo.svg", + 2020: "https://2020.djangocon.us/static/img/logo.svg", + 2019: "https://2019.djangocon.us/static/img/logo.svg", + 2018: "https://2018.djangocon.us/static/img/logo.svg", + 2017: "/static/images/spokane.png", + 2016: "https://2016.djangocon.us/site_media/static/assets/img/logo-full.59a2e37afda6.png", + 2015: "https://2015.djangocon.us/site_media/static/images/logo.png", +} + + +def fix_logo_urls(apps, schema_editor): + DjangoConEdition = apps.get_model("website", "DjangoConEdition") + for year, logo_url in LOGO_URLS.items(): + DjangoConEdition.objects.filter(year=year).update(logo_url=logo_url) + + +class Migration(migrations.Migration): + dependencies = [ + ("website", "0008_backfill_edition_logo_urls"), + ] + + operations = [ + migrations.RunPython(fix_logo_urls, reverse_code=migrations.RunPython.noop), + ] diff --git a/website/migrations/0010_fix_2017_logo.py b/website/migrations/0010_fix_2017_logo.py new file mode 100644 index 0000000..8f67f98 --- /dev/null +++ b/website/migrations/0010_fix_2017_logo.py @@ -0,0 +1,16 @@ +from django.db import migrations + + +def fix_2017_logo(apps, schema_editor): + DjangoConEdition = apps.get_model("website", "DjangoConEdition") + DjangoConEdition.objects.filter(year=2017).update(logo_url="/static/images/spokane.png") + + +class Migration(migrations.Migration): + dependencies = [ + ("website", "0009_fix_edition_logo_urls"), + ] + + operations = [ + migrations.RunPython(fix_2017_logo, reverse_code=migrations.RunPython.noop), + ] diff --git a/website/migrations/0011_add_milestone.py b/website/migrations/0011_add_milestone.py new file mode 100644 index 0000000..f471be8 --- /dev/null +++ b/website/migrations/0011_add_milestone.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.11 on 2026-05-09 15:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('website', '0010_fix_2017_logo'), + ] + + operations = [ + migrations.CreateModel( + name='Milestone', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('year', models.PositiveSmallIntegerField()), + ('description', models.TextField()), + ], + options={ + 'ordering': ['year'], + }, + ), + ] diff --git a/website/migrations/0012_seed_milestones.py b/website/migrations/0012_seed_milestones.py new file mode 100644 index 0000000..1a03ca3 --- /dev/null +++ b/website/migrations/0012_seed_milestones.py @@ -0,0 +1,29 @@ +from django.db import migrations + +MILESTONES = [ + (2015, "DEFNA founded as a California nonprofit at DSF's request. First DjangoCon US held."), + (2017, "Community grants program launched to fund Django events across North America."), + (2021, "DjangoCon US goes fully virtual in response to COVID-19, reaching a global audience."), + (2025, "Celebrated 20 years of Django at DjangoCon US in Chicago."), +] + + +def seed_milestones(apps, schema_editor): + Milestone = apps.get_model("website", "Milestone") + for year, description in MILESTONES: + Milestone.objects.get_or_create(year=year, defaults={"description": description}) + + +def unseed_milestones(apps, schema_editor): + Milestone = apps.get_model("website", "Milestone") + Milestone.objects.filter(year__in=[m[0] for m in MILESTONES]).delete() + + +class Migration(migrations.Migration): + dependencies = [ + ("website", "0011_add_milestone"), + ] + + operations = [ + migrations.RunPython(seed_milestones, reverse_code=unseed_milestones), + ] diff --git a/website/models.py b/website/models.py index 7339c30..eeeb76c 100644 --- a/website/models.py +++ b/website/models.py @@ -52,8 +52,9 @@ class Status(models.TextChoices): end_date = models.DateField() location = models.CharField(max_length=200) website_url = models.URLField(blank=True) + logo_url = models.URLField(blank=True, verbose_name="Logo URL") status = models.CharField(max_length=20, choices=Status.choices, default=Status.UPCOMING) - highlight = models.TextField(blank=True, help_text="One-line summary shown in the timeline.") + highlight = models.TextField(blank=True, help_text="One-line summary shown in the edition card and timeline.") class Meta: ordering = ["-year"] @@ -102,6 +103,17 @@ def get_absolute_url(self): return reverse("blog-detail", kwargs={"slug": self.slug}) +class Milestone(models.Model): + year = models.PositiveSmallIntegerField() + description = models.TextField() + + class Meta: + ordering = ["year"] + + def __str__(self): + return f"{self.year}: {self.description[:60]}" + + class GrantCycle(models.Model): is_open = models.BooleanField(default=False) application_url = models.URLField(blank=True) diff --git a/website/sitemaps.py b/website/sitemaps.py new file mode 100644 index 0000000..ab9ac9e --- /dev/null +++ b/website/sitemaps.py @@ -0,0 +1,45 @@ +from django.contrib.sitemaps import Sitemap +from django.urls import reverse + +from website.models import BlogPost + + +class BlogPostSitemap(Sitemap): + changefreq = "monthly" + priority = 0.8 + + def items(self): + return BlogPost.objects.filter(is_published=True) + + def lastmod(self, obj): + return obj.published_at + + def location(self, obj): + return obj.get_absolute_url() + + +class StaticPageSitemap(Sitemap): + changefreq = "monthly" + priority = 0.6 + + def items(self): + return [ + "home", + "about", + "djangocon", + "sponsored-events", + "donate", + "grants", + "contact", + "venue-host", + "blog", + ] + + def location(self, item): + return reverse(item) + + +SITEMAPS = { + "static": StaticPageSitemap, + "blog": BlogPostSitemap, +} diff --git a/website/urls.py b/website/urls.py index 9bdc909..f7e03aa 100644 --- a/website/urls.py +++ b/website/urls.py @@ -1,4 +1,5 @@ -from django.urls import path +from django.urls import path, re_path +from django.views.generic import RedirectView from website import views @@ -13,4 +14,19 @@ path("events/venue-host/", views.VenueHostView.as_view(), name="venue-host"), path("blog/", views.BlogListView.as_view(), name="blog"), path("blog//", views.BlogDetailView.as_view(), name="blog-detail"), + # Redirects from old site URLs + re_path(r"^events/djangoconus_\d{4}/?$", RedirectView.as_view(url="/events/djangocon/", permanent=True)), + re_path( + r"^announcements/\d+/\d+/\d+/(?P[\w-]+)/$", + RedirectView.as_view(url="/blog/%(slug)s/", permanent=True), + ), + path( + "defna-board-member-update-july", + RedirectView.as_view(url="/blog/defna-board-member-update-july/", permanent=True), + ), + path("blm/", RedirectView.as_view(url="/", permanent=True)), + path("events/", RedirectView.as_view(url="/events/djangocon/", permanent=True)), + path("home-alt/", RedirectView.as_view(url="/", permanent=True)), + path("home-alt-2/", RedirectView.as_view(url="/", permanent=True)), + re_path(r"^category/[\w-]+/$", RedirectView.as_view(url="/blog/", permanent=True)), ] diff --git a/website/views.py b/website/views.py index d6a974d..eee2003 100644 --- a/website/views.py +++ b/website/views.py @@ -7,6 +7,7 @@ from website.models import BoardMember from website.models import DjangoConEdition from website.models import GrantCycle +from website.models import Milestone from website.models import SponsoredEvent @@ -20,19 +21,6 @@ def get_context_data(self, **kwargs): return ctx -BOARD_PLACEHOLDER_ROLES = [ - "President", - "Vice President", - "Secretary", - "Treasurer", - "Director", - "Director", - "Director", - "Director", - "Director", -] - - class AboutView(TemplateView): template_name = "about.html" @@ -41,7 +29,7 @@ def get_context_data(self, **kwargs): ctx["board"] = BoardMember.objects.filter(is_current=True) ctx["emeritus"] = BoardMember.objects.filter(is_current=False, is_emeritus=True) ctx["alumni"] = BoardMember.objects.filter(is_current=False, is_emeritus=False) - ctx["board_placeholder_roles"] = BOARD_PLACEHOLDER_ROLES + ctx["milestones"] = Milestone.objects.all() return ctx @@ -61,6 +49,11 @@ class SponsoredEventsView(ListView): context_object_name = "events" queryset = SponsoredEvent.objects.filter(is_published=True) + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + ctx["grant_cycle"] = GrantCycle.objects.order_by("-id").first() + return ctx + class DonateView(TemplateView): template_name = "donate.html"