From 6898ae7106d1e1b64e961474385bea67e3259389 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 14 Oct 2021 11:36:13 -0400 Subject: [PATCH 01/35] Fixes #7544: Fix multi-value filtering of custom field objects --- docs/release-notes/version-3.0.md | 4 ++++ netbox/extras/filtersets.py | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index 25295b621..f0bb726d8 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -2,6 +2,10 @@ ## v3.0.8 (FUTURE) +### Bug Fixes + +* [#7544](https://github.com/netbox-community/netbox/issues/7544) - Fix multi-value filtering of custom field objects + --- ## v3.0.7 (2021-10-08) diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index 25fd32f0d..af8d904f4 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -15,6 +15,7 @@ from .models import * __all__ = ( 'ConfigContextFilterSet', 'ContentTypeFilterSet', + 'CustomFieldFilterSet', 'CustomLinkFilterSet', 'ExportTemplateFilterSet', 'ImageAttachmentFilterSet', @@ -47,7 +48,7 @@ class WebhookFilterSet(BaseFilterSet): ] -class CustomFieldFilterSet(django_filters.FilterSet): +class CustomFieldFilterSet(BaseFilterSet): content_types = ContentTypeFilter() class Meta: From b95773938d50acdb5404a63c346ca93bee5fdbb9 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 14 Oct 2021 12:24:29 -0400 Subject: [PATCH 02/35] Fixes #7534: Avoid exception when utilizing "create and add another" twice in succession --- docs/release-notes/version-3.0.md | 1 + netbox/netbox/views/generic.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index f0bb726d8..4e7d19cdd 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -4,6 +4,7 @@ ### Bug Fixes +* [#7534](https://github.com/netbox-community/netbox/issues/7534) - Avoid exception when utilizing "create and add another" twice in succession * [#7544](https://github.com/netbox-community/netbox/issues/7544) - Fix multi-value filtering of custom field objects --- diff --git a/netbox/netbox/views/generic.py b/netbox/netbox/views/generic.py index 4baf2e0e9..75e978e2a 100644 --- a/netbox/netbox/views/generic.py +++ b/netbox/netbox/views/generic.py @@ -282,11 +282,11 @@ class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): messages.success(request, mark_safe(msg)) if '_addanother' in request.POST: - redirect_url = request.get_full_path() + redirect_url = request.path # If the object has clone_fields, pre-populate a new instance of the form if hasattr(obj, 'clone_fields'): - redirect_url += f"{'&' if '?' in redirect_url else '?'}{prepare_cloned_fields(obj)}" + redirect_url += f"?{prepare_cloned_fields(obj)}" return redirect(redirect_url) From a7b6c40596fb1291243de8b5697cc1f7f24099e2 Mon Sep 17 00:00:00 2001 From: miaow2 Date: Thu, 14 Oct 2021 20:35:21 +0300 Subject: [PATCH 03/35] Fixing display of webhook types --- netbox/templates/extras/webhook.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/templates/extras/webhook.html b/netbox/templates/extras/webhook.html index f1cf876c1..c92ec4c99 100644 --- a/netbox/templates/extras/webhook.html +++ b/netbox/templates/extras/webhook.html @@ -47,7 +47,7 @@ Update - {% if object.type_create %} + {% if object.type_update %} {% else %} @@ -57,7 +57,7 @@ Delete - {% if object.type_create %} + {% if object.type_delete %} {% else %} From e16942dea5d6f60f099b850c15d00c3f52898fd0 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 14 Oct 2021 13:44:54 -0400 Subject: [PATCH 04/35] Fixes #7529: Restore horizontal scrolling for tables in narrow viewports --- docs/release-notes/version-3.0.md | 2 + netbox/templates/inc/table.html | 68 ++++++++++++++++--------------- 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index 4e7d19cdd..b49eb15ae 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -4,8 +4,10 @@ ### Bug Fixes +* [#7529](https://github.com/netbox-community/netbox/issues/7529) - Restore horizontal scrolling for tables in narrow viewports * [#7534](https://github.com/netbox-community/netbox/issues/7534) - Avoid exception when utilizing "create and add another" twice in succession * [#7544](https://github.com/netbox-community/netbox/issues/7544) - Fix multi-value filtering of custom field objects +* [#7545](https://github.com/netbox-community/netbox/issues/7545) - Fix incorrect display of update/delete events for webhooks --- diff --git a/netbox/templates/inc/table.html b/netbox/templates/inc/table.html index 3710af846..c38f50222 100644 --- a/netbox/templates/inc/table.html +++ b/netbox/templates/inc/table.html @@ -1,41 +1,43 @@ {% load django_tables2 %} - +
+ {% if table.show_header %} - - - {% for column in table.columns %} - {% if column.orderable %} - {{ column.header }} - {% else %} - {{ column.header }} - {% endif %} - {% endfor %} - - + + + {% for column in table.columns %} + {% if column.orderable %} + {{ column.header }} + {% else %} + {{ column.header }} + {% endif %} + {% endfor %} + + {% endif %} - {% for row in table.page.object_list|default:table.rows %} - - {% for column, cell in row.items %} - {{ cell }} - {% endfor %} - - {% empty %} - {% if table.empty_text %} - - — {{ table.empty_text }} — - - {% endif %} - {% endfor %} + {% for row in table.page.object_list|default:table.rows %} + + {% for column, cell in row.items %} + {{ cell }} + {% endfor %} + + {% empty %} + {% if table.empty_text %} + + — {{ table.empty_text }} — + + {% endif %} + {% endfor %} {% if table.has_footer %} - - - {% for column in table.columns %} - {{ column.footer }} - {% endfor %} - - + + + {% for column in table.columns %} + {{ column.footer }} + {% endfor %} + + {% endif %} - + +
From f1f0d9cd0d4b8098a9204a9de7061e5a5349ce47 Mon Sep 17 00:00:00 2001 From: thatmattlove Date: Fri, 15 Oct 2021 15:02:50 -0700 Subject: [PATCH 05/35] Fixes #7495: Fix sidenav overlapping elements --- docs/release-notes/version-3.0.md | 1 + netbox/project-static/dist/netbox-dark.css | Bin 788267 -> 788707 bytes netbox/project-static/dist/netbox-light.css | Bin 493227 -> 493565 bytes netbox/project-static/dist/netbox-print.css | Bin 1622455 -> 1623301 bytes netbox/project-static/dist/netbox.js | Bin 322556 -> 322534 bytes netbox/project-static/dist/netbox.js.map | Bin 310813 -> 310793 bytes netbox/project-static/src/sidenav.ts | 6 ++-- netbox/project-static/styles/sidenav.scss | 30 ++++++++++++++++++-- 8 files changed, 30 insertions(+), 7 deletions(-) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index b49eb15ae..82770e856 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -4,6 +4,7 @@ ### Bug Fixes +* [#7495](https://github.com/netbox-community/netbox/issues/7495) - Fix navigation UI issue that caused improper element overlap * [#7529](https://github.com/netbox-community/netbox/issues/7529) - Restore horizontal scrolling for tables in narrow viewports * [#7534](https://github.com/netbox-community/netbox/issues/7534) - Avoid exception when utilizing "create and add another" twice in succession * [#7544](https://github.com/netbox-community/netbox/issues/7544) - Fix multi-value filtering of custom field objects diff --git a/netbox/project-static/dist/netbox-dark.css b/netbox/project-static/dist/netbox-dark.css index 48745de125ca9d55e5264e8424d5c1a17c8fba0d..4a6912458cc957244337335aac5de2cb0470ca60 100644 GIT binary patch delta 327 zcmZ48XYhEUK|>2;3sVbo3rh=Y3tJ0&3&$4D3E9&R%CL(}_ddYQKfS+$Q+fLRJWiqM zADTE>r}H;*vQ4)*z^#Ok*nTvd^EK0Sp5JUz(>c9Zd8hA?VV6}kG{`MhNKGtG)y>S) z%`YvnE-6aPE6yy*%+J$JNzF;Dw9+#yp6*!4sXJY+fKza~cLgiYbUAZQw(UImoRK2a z&7ZR=PZwz7)Pmc~I(>Hor|I&byPg;n5Auqw?fPQ~ITpra>$6a$;3w7tHCGlF$`P&vC0C(Inf V$&7ZY?E+n#K+LsWpo?3&763E?Z^{4w delta 122 zcmV-=0EPeKkua-{Fo1*sgaU*Egam{Iga(8Mgb1_=kZYF^p9>_H5N!z&w@PdY>jIZi zN(nKSkZ=hYx7%+CY!sI-fe94@EipBh(0B(Xm%xDu5|Wc}7>Wd1u>Wd5cd>*+hJ^%m! diff --git a/netbox/project-static/dist/netbox-light.css b/netbox/project-static/dist/netbox-light.css index 6ca1d7884d19cf22c2390e609cc385b786f53998..2c5aa81f04fc95fb043f2fe60980ed77e0c7c1b3 100644 GIT binary patch delta 305 zcmZ48EBCiwuAzmog{g(Pg=GtC+`j37Wh`RTuN-ETnEq!UD?f8`e%|zl8q5eGj_GIi zv5HLR?_*`#-o1~Nm1%lm8jIBQSuhujTE_Zc)L1J=dNu`y6^<>3fVHLQ2R;78x zsaPBUbm8=gXMi^3oo5x;-g|-d0JC&KKE#byML;)}WR|5`!%Q`7PrSmqJ@E>g*Cqgj Cymcx7 delta 120 zcmV-;0Ehqmj~=Uy9)N@agaU*Egaot&UcR@%z6CM@3}k6@aBN|DI&N=nWtV}z1rwKG z!UYMpc)#9m)ph#3YRO=1sb>f a(FL3Xlj0N=mr&LPB!}451-IDM1|_oDmM?z* diff --git a/netbox/project-static/dist/netbox-print.css b/netbox/project-static/dist/netbox-print.css index a159c81ec9a89b19b6db58d0ced00165e28fdcbe..741f7ea7431acdaa8bc82267f975768d3aae91c4 100644 GIT binary patch delta 545 zcmZvYO-lk{5XZ4v+g@CC$I1jliVhLRda#2+ht5&aCvdYR1aqTZ4`!gyRM4d@!NEgs z6wwP=kg!o+x^?sm)Hevab*ml|OUT0?o*DlCnR$NZdURtunneq?U@NvEhgNJy9y_oT zyRaK=D4-o3=tQvu_bIyb*j@BwfS-IB;K{fIj%Kc5Fuw}!-)e*$1?VJW4Q#Y?3Y;ZP zyM|?uDh3Svn1tJ}M2s}lm+%~%O*uBF6X-V@bvZnZB3BQ`u!}^Mx&|-Q_ zP52j-xSR@&1vEPTviH)_Ch%q@123aAeFj2K8nRO46vTW99Q1WjTkfULBHLoiHeQ>P zL~H(}(OLaNz1t))K@+!Z-p^2GJ*>WXlJ>z$*++(8{G1V~m%;i0e~w^n=34=TYQRgr zW9a{HtM&i9lWBEXj>hyqU1=Z_N+vX=2`a`iC-=MRR6O*410ISL ifiwGx;1p;s*e&KCz`HeD1|}C%4^uBwpE+B0FLK{0-^uO( delta 195 zcmZqeO5Wa_+|a_vzBG4k(BD6(#N9FYB z58|TJKU4|}PUo!_R+^S0%rV`dN?2e!kk`jL-Fd69!t{i4VXo;0&B8s~*EI?UYD-v` z6eZ>rr{x#rT7g+Pi6yBTx`w9H8$|iVrx$Dn8nX&$j6;<$|8}3X!Z#VG8|)C~+HSv5 l*nn^Pr8I7l>8C%6i?+|-FAT&YKn!Aw0kQb@`THf(SOL%mM=JmT diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index cc12e4855709dd686515e62961911c0a21a59033..6a60ff56def395459f1244adc618e97ed9a518a2 100644 GIT binary patch delta 28 kcmex!UHI8`;f5B*7N!>FEiC8nP5*e6MQ^+CeU_Iz0L$tPUjP6A delta 51 zcmaEMUHH#+;f5B*7N!>FEiC8n2^6Q6gk#=DVEdnY HEU$O~J*E{q diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index a67c6cbd814471217b565895629019f22877c9f2..ba7d8cd2f7dca3b93a9f961f1393011110461af4 100644 GIT binary patch delta 46 zcmV+}0MY-Q{1S=$5`csOgaU*Ev;<$cmu|QO90E^Nw~@F65di}^Q)rh@y95@uySW5n E1gd-y0{{R3 delta 60 zcmeDDBQ*DqP(ur23sVd87MA!OeBq9c;X0m { - this.bodyRemove('hide'); - this.bodyAdd('hidden'); - }, 300); + this.bodyRemove('hide'); + this.bodyAdd('hidden'); } } diff --git a/netbox/project-static/styles/sidenav.scss b/netbox/project-static/styles/sidenav.scss index ffc366c16..9dfdd855a 100644 --- a/netbox/project-static/styles/sidenav.scss +++ b/netbox/project-static/styles/sidenav.scss @@ -105,6 +105,11 @@ // Navbar brand .sidenav-brand { margin-right: 0; + transition: opacity 0.1s ease-in-out; + } + + .sidenav-brand-icon { + transition: opacity 0.1s ease-in-out; } .sidenav-inner { @@ -141,7 +146,17 @@ } .sidenav-toggle { - display: none; + // The sidenav toggle's default state is "hidden". Because modifying the `display` property + // isn't ideal for smooth transitions, combine opacity 0 (transparent) and position absolute + // to yield a similar result. + position: absolute; + display: inline-block; + opacity: 0; + // The transition itself is largely irrelevant, but CSS needs *something* to transition in + // order to apply a delay. + transition: opacity 10ms ease-in-out; + // Offset the transition delay so the icon isn't visible during the logo transition. + transition-delay: 0.1s; } .sidenav-collapse { @@ -350,13 +365,21 @@ .sidenav-brand { position: absolute; opacity: 0; - transform: translateX(-150%); } .sidenav-brand-icon { opacity: 1; } + .sidenav-toggle { + // Immediately hide the toggle when the sidenav is closed, so it doesn't linger and overlap + // with the logo elements. + opacity: 0; + position: absolute; + transition: unset; + transition-delay: 0ms; + } + .navbar-nav > .nav-item { > .nav-link { &:after { @@ -402,7 +425,8 @@ @include media-breakpoint-up(lg) { .sidenav-toggle { - display: inline-block; + position: relative; + opacity: 1; } } } From 84c14aadc7c9329a426e6598af55f6c61129b8e8 Mon Sep 17 00:00:00 2001 From: thatmattlove Date: Fri, 15 Oct 2021 17:07:54 -0700 Subject: [PATCH 06/35] Fixes #7300: Fix incorrect Device LLDP interface row coloring & improve related JS --- docs/release-notes/version-3.0.md | 1 + netbox/project-static/dist/lldp.js | Bin 109095 -> 109291 bytes netbox/project-static/dist/lldp.js.map | Bin 106320 -> 106584 bytes netbox/project-static/src/device/lldp.ts | 98 +++++++++++++++++------ 4 files changed, 74 insertions(+), 25 deletions(-) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index 82770e856..70a9a0dac 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -4,6 +4,7 @@ ### Bug Fixes +* [#7300](https://github.com/netbox-community/netbox/issues/7300) - Fix incorrect Device LLDP interface row coloring * [#7495](https://github.com/netbox-community/netbox/issues/7495) - Fix navigation UI issue that caused improper element overlap * [#7529](https://github.com/netbox-community/netbox/issues/7529) - Restore horizontal scrolling for tables in narrow viewports * [#7534](https://github.com/netbox-community/netbox/issues/7534) - Avoid exception when utilizing "create and add another" twice in succession diff --git a/netbox/project-static/dist/lldp.js b/netbox/project-static/dist/lldp.js index 7fac1012a96c1fbfd21177b565f29c9a2cb3fe7c..2b3934742c7a3b9294c91b81efcf0817e51c021a 100644 GIT binary patch delta 6474 zcmZWu33wD$p8x)p1CaX&3E@hbCRRhG!yz(sY(kDs$bBb}5JGiz^^sJ%nyTt_I-Q6P z4sys5p2PD1Mg_%PXWYed?Ac{-U{&r{ur`na&dY8DIU@Y{X%l>)Ic>!6YB zVCLe;XP9iLC2ugRp@~dSnumxDe|yRLBsQ&9VI6)}5LsWg-5->sf=-2X>W?OkO@Si) z$mqkPphd5m^bREVdpQ^R=h20_XUfx~pj4lf`C)qUS}zwMwUcV}@+GI{LbE=pAu_h4 zSjboWR#jAdyyWW_?NvhTWQ~H|B?_`jY;GtomU{xSU-WBStzZ}ZKSM+0bjMv#OvbI> zI5{L(IK}R_#wQO9G?HEGj}+7hteaCTmccx3S{1nuAITT!pN4OLeb2~*=3kO8mouy^lP5xkyQ&da^n$3YY$D>^g z=HHY~AJKY$4>kINP&q&e`PT#Kp~W574} zd|=cvi>(3eyr7$Wylp0|CMnyuBGxkX(vm$!`y4lUEO9pojkLz$37hL=M`>vBML{akQZ~ z4(2kr?LQv703Q9_#|vS!Byxh^Vv|Vrp~d+B9uhGus+dX9Gp3?b*dG+vbpi=idITc`|9(gLy z_&jrD=NKp>D~__al_`Ukm;%?3?xV|ZTQf}7>%Tqf0;ty~>l0yKn_^!q-pN%MpQT*4 z&u-Eb-mm%$R~7v(6Q`-9_<0rD^yu@C1Bhhp3!gxRUJ}g%MpO0RA2O1_M|K@MhQAem z{0KyS>G9bBep3Hpx=B{;dF!m!1$mr;Et_qfYoR%hEArXV;n(`@i#e5Se{nJJ`X66B zYshWrWH$)<^^*%3#4In*hjhQnnM@csU3R5FP%&_bTuhqcD<7khYfn8rT8@q2RQ<3~@JYjvRL-q$81qo;>n|JJZJ`7f_8bE$R{FNum~;=I^&s@4S6@3U{Pnj>Cya03^> z9;;#fxqQH9;(AEn^qeY>YTqD8vMQP*{XQJjIZV3}fFlO@gzgw^js%RQdPnvPW7EyM zL=)FU&YfO{hCk`ee@FZI^P5(TBG=wz$26&yd`Z-@M6&qD>B(WF=%za%gLJ%Amei?Q zeti*^Q@I#Y$=~0i$qp46+jJ&pxzjjDP}5|;i7PbPs&$xbM3(SSg$5_J*mcXHLgd1k z1=EX1cy3e&&5>GTWSGo)dm-ag$;zu4^x#;SO0>6GCZy8G@57`PreE%anS0Nqk{{nT zVfZ(lU5?nr`yieExB|wEvY@@yvtj+-vzbW^RdIdUD4DqC7(-RYHa__$4AtP! zLPe27RiYcU(>F{u_&i>uOH})^MT@v{^4Wh^LLFK0?i1NfqTQs#dmB`Hq`gM%ur-NX zdmnzgwL#8Iet37*rfNK5w-|QGA-_4&jQe&W&||QJsZCVkSobg{iF_|=*ci7OF-;B% zURC22EsnJgV?FZlu%clI{Uko91(AU4Ggf!WEyLUPkD$Rod|)X;E`|5pvcXz}B$h3BZ#r=W@u2b5oK`oR6G5myQnq-nrbFGLM=a z;l}UsVeXX`)x;?z`+ORxWchg(eS5?CLI9cCSxDFa?LrXag`W5R+$2!a0- z5^L&TeDn%4VU21lQ@KuiprBs0wdpT?8Zjbt!Dp)hE4{7%Cy&JwYM2hXw1Os``TQzC zCHek~za}@U7#GidnS;3nzM6)zp8nO~c&8ZiAIY!_i9Ghz?N}$i_ElbIkhgD8y>iGL zaf;aGPLOEvG1`T+7wsfLdgG)^9PSSwkT5YEZ;nVjF4C%@b>W&ARY=R%%jfr?R=r}} z-#mtfjZQ8LODjp@1FFyOF>#ILxv!tVVy*cb6aMynvw~?9$(WZ|{roprK6%L3S9f4N zyy@F#nIKPx_Ch8p|L%S$)ZhAUI;0eexF^|jZ3aoXwhZHS{f{PevknvWc5Fu z#<5@iBOjPTkxsq?GE=-fmQ8f>Yz$Hxt{*~aB;P2(%B=iGJ~ZmTx^a?84)9!K6pM}k z%;-d}F*>>wIbehX6X(=o;_RevGfEpJ#Ks&=(%Jtftg-6KGifU1+wRL@%FW>r63+6bK_gs zOcF-d1i^JvR|*7@SM&A={j?IYqu-~140OjRm_XmChcsq2PhV6aGkRnc%z+j>EhFjkJ9{G(b4|?0D!#4}gQ7N`qxk8~q^- zUS*({zMKJ%Os^F!1)A5V+MHG+MzKa4#iq@G|3JYn&x9aT%hTPNFgw{RbG6aanQ%4* zTIt~|a6l3LBn$e}T2Q<_vWu5;oxIR(=*YzNN5k2W#V{>sdh;QJ>hoX<^wU%Gpp7)l?8VwY}C~7eS3q{ceGq@qM6jwsqiY;d1Dr0t4YPiu_dS*VX8rO_M9^Qil-knC5 zsGqosm{A z!Fl^|GP5z}yR)NjEdx0zx!$mzk`-{d$d5dssARWW;QhTkrUh5r5B(CJt>mX=fh-Lt zM3;ugFEJvYLc49S1XOy^25nh9j)=P=j=->(Akh_Lv_LcRVe#}J!U-{sEaax0`H()- zg?)(nm_doMSkggqP~rk~NeNuX zjJU`F<9nhBMk?eWvdAz6Rwyz82b}TUG`M6H{u>)PsK`o_lKl;PbQYf_aNLDYe zvgj2>vW#_%pY4>any6~5&+5l3eG4xIMZ6F?=*%*>7i+cyWnckM^wl!hV>s2C3aHO$ zit9$h6*M0@gA&(F6Fpf0)4)mJset)VNPnn+#n2eFR>I4G&h~W`v_mLbTa8f`7d}w~ zX;4hxse$j8gs@19XOZHYtqQzf$U!V2stoH)?3L^D`|(1|RZ*yev>7$n71I}%g4pcg z{XrhTzwCG}bq8?S%82z7mXn~ZFXt6}->9sID+SCY4 z3bNO_2Gs(^(SvQU(3tz5?XV-YM(VIw&{*&h_9- z#Y4X0f|n-tNtTFFqT_=F=IknZQG_}4_aZJ>6~%Ti3Fo!?fMaq0fdSFl>BT}(Y1HuP z2*(%WbH`7yV)~2^170!xz=u(!n3i^-LaU;G>4GR=WZfbGWx%;F1T|4f1{G{2D2#v{ zFjh5j-K5pHw#p4ZYDGVCs<}7&>gbG(sJjMQun|7NRIxxoU$3T;0)HRdh3mwx zCf&4M1s+Ekf`t#A{&e zH%D6O)jpU9YiVW}S!$+thVdAcbX^$ClQtsbx8(COx+e^2GuI%B(M1)}#F|kI0Xcq? zHE}i36JhWGhI2Lo&%v7L`3M|mpeXwA-EepEgffZqTVt0z^qP%y{${Ac4VBGMj~l+c z83d*!=I7I>{XRGZHO94vZYq_y*n6SV>h}3v>{`iUb<2u_7rZP!^ZCV4Rt-Mg;rotp zF{%;c*YSGfQRAH$yRm^fjJ5mXRX^1 z1d2wQ20@yBKl*At_1+J6qpf^+Kima8HE+dn&D$WG9^HyRyD>i)G1E`aY=x=x?r|`l z2Dd>b_@jHaK^vr%phOIpQtWX7*7`*@NPpZ8IVsqMN#oufFqf9^KsPC&gFE1~SrO2; z<5Tcm*0__Sx>UiEujJ$;lpxT+1CWIk#Lxr4K(Z`w5~Vxg!A)*Fm?3VNSJ65GaffjW zC=A!Ighp~ez(^=w#AEkG!{_i?Z(xC&SP*Z9F{lS2w=4v)5KbVTVFY7c0_yBWEMAGn zCOP6{tP+m_y$Hm$7$d_8a(Kxm06lh*0Os5NXV{R`L z$i~yt=mWdqJcAu4>roSd2;4@u?7=9|OZV)>Fd9qtAMb%;rb(bj3t%Ss(|40;+YlB* z=y!^C{&dr;SV2O*38 zFb~G>y_gdHgupr`#U*fs0=-ZK^XcJzFq`h&hnKu}jn+w{HI61Q@X(F>U{=&ZVFqBy zTl^3fa6H}i5FEoA)bTJ7BTzkzfu}Dz<`FoLFQ)YJqga{p^vr&Y9Od-te)tf|qwgO8 zPfDlpe(A->YV^wn(c|a~uzBm*_QKq@4&#T0j7Ib?5;D+I+c;Cn)b#*j)2=jL;^6av3bnTO{e>AS? zHkPK>pM=~AtPA-U1_pYKkA>QwLZb=LO;5o;VQ#P-fx?u4!s0FZ#Iangl@1(%?acoJ D^S{oE delta 6269 zcmZWt33L=yx;}rU6F~MQBKN9s3`J$3XDE@ire$K9#ls3{ngz8Jf731>;B)p zb(eqn{{Pme|Cx5=bXpe6S{-Yn{k)RZr8w%`-J+n`1V!XEG2Z~Ck*rLnQyIxlPxXr` zS27aLX1M;5@Ir`;Jm`Stk$3XJP12zNB+>#D+n<4LdZ)trxqgM!tX8fh;P5-vO2o!& z1Si?aEWnYEG1<^WUS-N5LT08cM8uB&C1g_yo8F|bB>`3vS%0=85R#;VE`@E@A50ma z3O)Lfv4_V%xn4cxEikqDI1l;lSf}1K?dTXN)Thk;FvHa97KI>E{*j({!k^3afz?A~;wpju$PoMN>O4=bV;QUY1xus0MCG`}3kDiTm18Yf!g ziVir$p`qw7C*vH6rG|!@bgi?}1l8m(!fI60w}Joyad|$55GfG{!9$)Cp8_u#^5!G{ zXKyy*XT0+e`OYiM4T&ArVb0ZIGcguC(~r0ZSxF;J&hch3S+ z)jeGw=-I%GuTwbD>qU#(!}_`2Vg`JAb})m1B7KeG!v}dp>x3X-LJsI9>q0A;E|Kis zHg{UD$XYqS_1gJ5$)V6%-28=*nA;_?S|ljS-YhNKVGe|P-J)VH(CnRvb3A&)aQ;=j z)Do@#Pf)Ed30D9NkUt#AAio=)KzaB> zu2WBmy_3wodmp}V>~1py9{u#lYM5G%VPuW^y=)IY9S(;;=8~JBEv~0m4^GhsNiIM+ zIeJeiI>qPrd<8tYaIZ3k7dzHEUA*qv_a#7^UUOeMkVcZJ58nSiOe$2a(|;?G9()l7 zlM|+sP5bAgs}1j82(9|V`vFXYIGV8gv&l;bGBG~?`@jQGN}>ntz|)Y#=rF%_D0L{|Z{j(}#ZxEqd3#Ob7DR;R*WIM=bzV`eR3S zje|b2<`{cJpE7KXo3M=Z99w-u*{HZ)|JP$4fc5%ReKIU;P#nv}o49u4v6AcYJItEG z2UNciq+-Bh<|-9Z^n?lxdhCgZ0W`AV$xonNFOKB_Q>o~oKWC-@hQL!#;eX}RAAzQ? zd}bbipENw1VGgK{yp1;7k~~hvmfgP5v(%Eu75W_*>Kg)%<(xuxJi8oJ{pn{<8+sc# z-UE_;<@iztG3yJ9Aj7Y6W;0Tz$DtHRDpH2X#kDEE_%Rx|?!?itf%phc(SJIT0f=S1 zTo1DDdwG%xV?6xIw}!9Duf4L$t2)fQBr2Mj^I+4h+LF?s-?7i>TIoNz}4Lvizr+rhb&@mYX1xbi7fP(xqDOJrAp? zTm)(4{WqwoQ^k#KIhC`z**Hf?)8v4ea~o~dHYz%COL!=^v6Ck3y6#YQ{N#>(I#VLKbiaHQl?oYYcFNegX3X3(cWa4I+Z?r2d1-r`sFT|v-eaQ`RPqF62JNM zYQ)ap0U7kCH85_B75%M&?bmNRJv*hbD6t=bSLBc?N@lJlzMNd9q4OQb%r=rN= zYSD|Z(>d>=BW3)#9^9 z8|E6x4{y)iV!mJ1Kq)8vrgQ#D@E z5?J{t)+>(=Ya4|yP7;F(5edqEV|I_+I=XD@H8d1V3>-wrqwrnVEl`S(B=hl^EK$FV z``CbxAjSQv1x*ykTF7Vb%-T|gI#MOS;Zl9r9My8JUlV(C<6&F&3pl=(nOpyhj(|9T zYHs)i9#q7BGuMuI@=Up6KWYvs0aUW}OpZz89cHrsjCo9!zqiZG`N=zHa_0mPv|Za! zz~jfnD=Vs*lS%g3bWq6Zvn+=9=Ce+K0CljCq5s>t5K@Mo_wIrev~b<|e9US)&sU~K zL^LN|HW5ASBM(d@Kc1fl7Bcrj1(|bUJPBTy2i^Ma3)`5K0d*;P?~`<5`(QjH^0Xrh zGAGDb#C6%^04sBYr1OJ9QgAUXMPp^U*$XqFl^p+IacaAY35zsbm@%$L#o}Y_)$VeV zeQ`d?x|l{vE>=&fS6!M7U8y5q%*kOXKM_MScCi%M@W+ckLL<5IVVJ2@$t~wH$?lKJ z8JRbl`WGL)$V@6z?R_fO=?E6ASM3e@^PffynlAXP9I(*a_J8ttJfns=p<646=*;Jr z0IB`^FaB=YpdvY*`7#HS3w$*L6+ZK;;fc*+JcuO2Ie7BWSBtSseEF-qPJwr9R(*2V z5^WZ-E0h$`?c=f+Wgj|AQuHK5k2u=zLm=sAINlQFd7Pw8Ll?t2ky=RW*Q*x=(XKu* z5p=zVjg3w&3o9#0;)AL`fF9r^fBgCptkqh+G2{QhH)|NDNX9+S>Sw>f8mgCkeQ76_ z!&|<6oDq0BvKMBPitp|OxBkX=Ga=P2;+ka74@+u_l{L2>^Z+uu5eJ>MIN%=pH z;@B_$kq?Ypq*HH%*{L2LYbQE&9x~PDD~C`UrXP#3JgfLIADsHHe>~2Zd_3ojVcijg zSzWkmq)3k<2MtcZgK2SfcF{Mn5KQaj9V@sJ%BFyMUH?^$xi!>9!6rEWP-U+L7y=m_4JGh%+N@$7!M20N`>r&HN0a3Yb{6+Wo}{( zyIDexO$uB$^`t`3)WbWX^wTQHj(wjBGBDjdJ>CG>)Henu(Z8xNDRyKG%m-65&xwZB zrpL~Wg$4#{sck&`FSuiKCSY~|Cv{DPM(B?{J`sA*yJNE_!w)bq%v-I=^54SKD^p-G zt(*$MX?47{KrE=>?Zv!ZENJBI_4Jjguo9}DoCa+S48+#YfL?%3dLkWGK~wC9ba;t@ z68b_WJTS9Kv=(SSziM~e43=VzHrGV8S@0jI_0_W>#5D2r?%6QUFwEzoku|^AcVfG--gpL)T%*<8AoodjCp;CHk5v-fA0Tn#D1_{1BgRaN{ z%jA};K30W8tEiX*H$oqMAO||3j9$)xm5bKnkYr$1VO*9Pb5>*WOl(^Yis-}*v~e*k zTGVbhN&@%boleGRth@%fWlzM+X>|W$m0X zTZMlwE`_cwl|gF?R)SC;Jc3VfiS$GqT4grQ%;(-r@`GzdteNztRB z0*VddlWC6~R)9he+Mzv5#Sw`}#1Y7fNf9G4E=x2sAC}J)5Kb!c+C(ARnGYFryx5m4 z6Ko#8+Ur-<q$upLrks2UH@xenCk-QGz!Z`=4?SB5IgD4L=az#7a0RpBvJ$9( zARR1$_VixKk++G*3wo|=V>q{Ci6zg^bR_KkrR0eyDh+1C>4VjUISu~tM z^J63^38O^l@k*Eh&GfBGSOjkRLnSN+XUtXwF91f`*VW*Hx>#Ke@+(gKTrH$Sn7&mD z->;~{8ZD7O!dF|Bc)gHASV0uwYPRHBx&A-^@55XXg?dPzRgGP7Q(+~D&0anb;<0@6 zY4msm%&~@gWCN(9&!{kmKGFd5=+*|LsaG3tWoPVv8lV+Y>qM(HN>6TpnX%?3U{Z01 zZR^o85RM&eho#2re|5pmv}&otk-eO*+lclHZiG2d6uWC990e$%t2P0LuempYI}OkI zq6eOzTq{|lMv;ymD==Xf(eonAr+*T0!lD?qLn-)Pn;$q9*B=@ZZJj7lzuHB!dd}0n|rO4jQwX zxndNMbgs&3!eGTl6t!U#xz*gOeHC<8ADXY87WBas6^#9s zXgR$!07$-cb_BPyf!-9sW3~6Y!v`#iLQ$}$2iD+#{=QbunCb-70h#4=(i}7ZG=2c-* zT6#Aoc(*|MPR^ir-3@0M?7;eNIt@nQ2D)_*Qb7sbvlr zQ)&AMRz$^#f*U!Wj==egpxf_*r?C1gxgUr@Q}-k7)W*g=0B7-wDgEF_+d<1WyXi}{0fov;2zn2hsUOfL5UYcHg6N_a^|*oxcJ<`4y$X^ zvfMQtOIQ~^@;a7U@%xq8vfQ!)KT5@&mYN@h{bO++FV0gzPdo~_)BBO`g`puYK6p&K f#)HBU)U%Hk9f98>hyQ-_Zs`K?$`u-^0za))8(avU#WI#nca2B3ks=&H*Ec|pRbcW?TFf810 zDi*MEV1OedH8k2rwt)j94wwnQd-_~5B(_a&jglSXq@u!9_tFAdlB54HNG6MYbDL`a zT{X*^^VsQHUK&6nrbwr|Y+?=AsO!FwP8&>_e?kvmPPHDR;B3}I UW_gV0SFJ>mCfw`Fb25?q0Lz=q!~g&Q delta 426 zcmY+9!AiqW5Jha#oqn=2_9D7+LvV8K)CXT^ay) zmOQ|*#K`T1D|}+huV=H|PRR#ir6Jwg+8KlQSe2|?%}o0tcl9y+V|2uLU?s&XYBsSH zf^RGV2-KHQ0PgEekS2*&emQ3mm=gF?Phr$#hB|O1nWuCD>{?N|s2jwR+Z8xkGucDf zm}L*J4riJkQKW}dxYrFEGWtinynLFQ;~oESUGm4KO)=1@M5A*qKj&tv$32N_^h_N? iUFe$idZ``!{_otHdvK5H&H<%BsyLtw!K-~SZSxIf)Oc3_ diff --git a/netbox/project-static/src/device/lldp.ts b/netbox/project-static/src/device/lldp.ts index 6baaa9b38..ebf71138c 100644 --- a/netbox/project-static/src/device/lldp.ts +++ b/netbox/project-static/src/device/lldp.ts @@ -1,6 +1,17 @@ import { createToast } from '../bs'; import { getNetboxData, apiGetBase, hasError, isTruthy, toggleLoader } from '../util'; +// Match an interface name that begins with a capital letter and is followed by at least one other +// alphabetic character, and ends with a forward-slash-separated numeric sequence such as 0/1/2. +const CISCO_IOS_PATTERN = new RegExp(/^([A-Z][A-Za-z]+)[^0-9]*([0-9/]+)$/); + +// Mapping of overrides to default Cisco IOS interface alias behavior (default behavior is to use +// the first two characters). +const CISCO_IOS_OVERRIDES = new Map([ + // Cisco IOS abbreviates 25G (TwentyFiveGigE) interfaces as 'Twe'. + ['TwentyFiveGigE', 'Twe'], +]); + /** * Get an attribute from a row's cell. * @@ -12,6 +23,40 @@ function getData(row: HTMLTableRowElement, query: string, attr: string): string return row.querySelector(query)?.getAttribute(attr) ?? null; } +/** + * Get preconfigured alias for given interface. Primarily for matching long-form Cisco IOS + * interface names with short-form Cisco IOS interface names. For example, `GigabitEthernet0/1/2` + * would become `Gi0/1/2`. + * + * This should probably be replaced with something in the primary application (Django), such as + * a database field attached to given interface types. However, this is a temporary measure to + * replace the functionality of this one-liner: + * + * @see https://github.com/netbox-community/netbox/blob/9cc4992fad2fe04ef0211d998c517414e8871d8c/netbox/templates/dcim/device/lldp_neighbors.html#L69 + * + * @param name Long-form/original interface name. + */ +function getInterfaceAlias(name: string | null): string | null { + if (name === null) { + return name; + } + if (name.match(CISCO_IOS_PATTERN)) { + // Extract the base name and numeric portions of the interface. For example, an input interface + // of `GigabitEthernet0/0/1` would result in an array of `['GigabitEthernet', '0/0/1']`. + const [base, numeric] = (name.match(CISCO_IOS_PATTERN) ?? []).slice(1, 3); + + if (isTruthy(base) && isTruthy(numeric)) { + // Check the override map and use its value if the base name is present in the map. + // Otherwise, use the first two characters of the base name. For example, + // `GigabitEthernet0/0/1` would become `Gi0/0/1`, but `TwentyFiveGigE0/0/1` would become + // `Twe0/0/1`. + const aliasBase = CISCO_IOS_OVERRIDES.get(base) || base.slice(0, 2); + return `${aliasBase}${numeric}`; + } + } + return name; +} + /** * Update row styles based on LLDP neighbor data. */ @@ -23,38 +68,41 @@ function updateRowStyle(data: LLDPNeighborDetail) { if (row !== null) { for (const neighbor of neighbors) { - const cellDevice = row.querySelector('td.device'); - const cellInterface = row.querySelector('td.interface'); - const cDevice = getData(row, 'td.configured_device', 'data'); - const cChassis = getData(row, 'td.configured_chassis', 'data-chassis'); - const cInterface = getData(row, 'td.configured_interface', 'data'); + const deviceCell = row.querySelector('td.device'); + const interfaceCell = row.querySelector('td.interface'); + const configuredDevice = getData(row, 'td.configured_device', 'data'); + const configuredChassis = getData(row, 'td.configured_chassis', 'data-chassis'); + const configuredIface = getData(row, 'td.configured_interface', 'data'); - let cInterfaceShort = null; - if (isTruthy(cInterface)) { - cInterfaceShort = cInterface.replace(/^([A-Z][a-z])[^0-9]*([0-9/]+)$/, '$1$2'); + const interfaceAlias = getInterfaceAlias(configuredIface); + + const remoteName = neighbor.remote_system_name ?? ''; + const remotePort = neighbor.remote_port ?? ''; + const [neighborDevice] = remoteName.split('.'); + const [neighborIface] = remotePort.split('.'); + + if (deviceCell !== null) { + deviceCell.innerText = neighborDevice; } - const nHost = neighbor.remote_system_name ?? ''; - const nPort = neighbor.remote_port ?? ''; - const [nDevice] = nHost.split('.'); - const [nInterface] = nPort.split('.'); - - if (cellDevice !== null) { - cellDevice.innerText = nDevice; + if (interfaceCell !== null) { + interfaceCell.innerText = neighborIface; } - if (cellInterface !== null) { - cellInterface.innerText = nInterface; - } + // Interface has an LLDP neighbor, but the neighbor is not configured in NetBox. + const nonConfiguredDevice = !isTruthy(configuredDevice) && isTruthy(neighborDevice); - if (!isTruthy(cDevice) && isTruthy(nDevice)) { + // NetBox device or chassis matches LLDP neighbor. + const validNode = + configuredDevice === neighborDevice || configuredChassis === neighborDevice; + + // NetBox configured interface matches LLDP neighbor interface. + const validInterface = + configuredIface === neighborIface || interfaceAlias === neighborIface; + + if (nonConfiguredDevice) { row.classList.add('info'); - } else if ( - (cDevice === nDevice || cChassis === nDevice) && - cInterfaceShort === nInterface - ) { - row.classList.add('success'); - } else if (cDevice === nDevice || cChassis === nDevice) { + } else if (validNode && validInterface) { row.classList.add('success'); } else { row.classList.add('danger'); From 811c21ec7e57575390f86bb1a146a5304371aad2 Mon Sep 17 00:00:00 2001 From: thatmattlove Date: Fri, 15 Oct 2021 17:21:36 -0700 Subject: [PATCH 07/35] Minor Style Improvement: Add vertical spacing to Device Type component navigation & fix inconsistent component active color --- netbox/project-static/dist/netbox-dark.css | Bin 788707 -> 788707 bytes netbox/project-static/styles/theme-dark.scss | 1 + netbox/templates/dcim/devicetype.html | 2 +- 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/netbox/project-static/dist/netbox-dark.css b/netbox/project-static/dist/netbox-dark.css index 4a6912458cc957244337335aac5de2cb0470ca60..4bdce502af23e8d6896603b8f092ac2ba0aec533 100644 GIT binary patch delta 115 zcmaDn(ctk!gN7Ey7N!>F7M3lniP_UVMHpqK-*;zJnI4eI%CdcFHtRCa=@QnAVv`$A zNP{J|FWAaj9EB=2y?~dEfBFXjHsk3NF7M3lniP_U5`x(V2H(X+!e&3x@WqLp+E6et!*{sVvCr8MM zO>Q_L4Hn_E%`#GF9PwSA8OxB3qN DS9dFR diff --git a/netbox/project-static/styles/theme-dark.scss b/netbox/project-static/styles/theme-dark.scss index c7c0cd76e..c5fb5dcf1 100644 --- a/netbox/project-static/styles/theme-dark.scss +++ b/netbox/project-static/styles/theme-dark.scss @@ -74,6 +74,7 @@ $btn-link-disabled-color: $gray-300; // Forms $component-active-bg: $primary; +$component-active-color: $black; $form-text-color: $text-muted; $input-bg: $gray-900; $input-disabled-bg: $gray-700; diff --git a/netbox/templates/dcim/devicetype.html b/netbox/templates/dcim/devicetype.html index 2a9f4a93b..bb5743ace 100644 --- a/netbox/templates/dcim/devicetype.html +++ b/netbox/templates/dcim/devicetype.html @@ -137,7 +137,7 @@
-
- {% include 'circuits/inc/circuit_termination.html' with termination=object.termination_a side='A' %} - {% include 'circuits/inc/circuit_termination.html' with termination=object.termination_z side='Z' %} - {% include 'inc/image_attachments_panel.html' %} - {% plugin_right_page object %} -
+ {% include 'circuits/inc/circuit_termination.html' with termination=object.termination_a side='A' %} + {% include 'circuits/inc/circuit_termination.html' with termination=object.termination_z side='Z' %} + {% include 'inc/contacts_panel.html' %} + {% include 'inc/image_attachments_panel.html' %} + {% plugin_right_page object %} +
diff --git a/netbox/templates/circuits/provider.html b/netbox/templates/circuits/provider.html index 4d35da0e6..af883e56f 100644 --- a/netbox/templates/circuits/provider.html +++ b/netbox/templates/circuits/provider.html @@ -47,12 +47,13 @@
+ {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='circuits:provider_list' %} {% plugin_left_page object %}
{% include 'inc/custom_fields_panel.html' %} - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='circuits:provider_list' %} {% include 'inc/comments_panel.html' %} + {% include 'inc/contacts_panel.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index ec1ea3fa1..9ae9df7d4 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -296,6 +296,7 @@
{% endif %} + {% include 'inc/contacts_panel.html' %} {% include 'inc/image_attachments_panel.html' %}
diff --git a/netbox/templates/dcim/location.html b/netbox/templates/dcim/location.html index cd0f2a92a..459880ca8 100644 --- a/netbox/templates/dcim/location.html +++ b/netbox/templates/dcim/location.html @@ -72,6 +72,7 @@
{% include 'inc/custom_fields_panel.html' %} + {% include 'inc/contacts_panel.html' %} {% include 'inc/image_attachments_panel.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/dcim/manufacturer.html b/netbox/templates/dcim/manufacturer.html index 85d76f14f..2a56b57cc 100644 --- a/netbox/templates/dcim/manufacturer.html +++ b/netbox/templates/dcim/manufacturer.html @@ -38,6 +38,7 @@
{% include 'inc/custom_fields_panel.html' %} + {% include 'inc/contacts_panel.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/dcim/powerpanel.html b/netbox/templates/dcim/powerpanel.html index b1367aa1e..10975fa1b 100644 --- a/netbox/templates/dcim/powerpanel.html +++ b/netbox/templates/dcim/powerpanel.html @@ -44,6 +44,7 @@
{% include 'inc/custom_fields_panel.html' %} + {% include 'inc/contacts_panel.html' %} {% include 'inc/image_attachments_panel.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index 5d44e2125..0196a9a18 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -332,6 +332,7 @@ {% endif %} + {% include 'inc/contacts_panel.html' %} {% plugin_right_page object %} diff --git a/netbox/templates/dcim/region.html b/netbox/templates/dcim/region.html index b46c905c3..1ee21a60e 100644 --- a/netbox/templates/dcim/region.html +++ b/netbox/templates/dcim/region.html @@ -46,6 +46,7 @@ {% include 'inc/custom_fields_panel.html' %} + {% include 'inc/contacts_panel.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html index 1ee8cfce0..92023f8d6 100644 --- a/netbox/templates/dcim/site.html +++ b/netbox/templates/dcim/site.html @@ -76,6 +76,10 @@ Facility {{ object.facility|placeholder }} + + Description + {{ object.description|placeholder }} + AS Number {{ object.asn|placeholder }} @@ -91,19 +95,6 @@ {% endif %} - - Description - {{ object.description|placeholder }} - - -
- -
-
- Contact Info -
-
- - - - - - - - - - - - -
Physical Address @@ -138,33 +129,57 @@ {% endif %}
Contact Name{{ object.contact_name|placeholder }}
Contact Phone - {% if object.contact_phone %} - {{ object.contact_phone }} - {% else %} - - {% endif %} -
Contact E-Mail - {% if object.contact_email %} - {{ object.contact_email }} - {% else %} - - {% endif %} -
+ {% include 'inc/contacts_panel.html' %} +
+
Contact Info
+
+ {% with deprecation_warning="This field will be removed in a future release. Please migrate this data to contact objects." %} + + + + + + + + + + + + + +
Contact Name + {% if object.contact_name %} +
+ +
+ {% endif %} + {{ object.contact_name|placeholder }} +
Contact Phone + {% if object.contact_phone %} +
+ +
+ {{ object.contact_phone }} + {% else %} + + {% endif %} +
Contact E-Mail + {% if object.contact_email %} +
+ +
+ {{ object.contact_email }} + {% else %} + + {% endif %} +
+ {% endwith %} +
+
{% include 'inc/custom_fields_panel.html' %} {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='dcim:site_list' %} {% include 'inc/comments_panel.html' %} diff --git a/netbox/templates/dcim/sitegroup.html b/netbox/templates/dcim/sitegroup.html index 856a86d64..610917078 100644 --- a/netbox/templates/dcim/sitegroup.html +++ b/netbox/templates/dcim/sitegroup.html @@ -46,6 +46,7 @@ {% include 'inc/custom_fields_panel.html' %} + {% include 'inc/contacts_panel.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/inc/contacts_panel.html b/netbox/templates/inc/contacts_panel.html new file mode 100644 index 000000000..33788a561 --- /dev/null +++ b/netbox/templates/inc/contacts_panel.html @@ -0,0 +1,49 @@ +{% load helpers %} + +
+
Contacts
+
+ {% with contacts=object.contacts.all %} + {% if contacts.exists %} + + + + + + + + {% for contact in contacts %} + + + + + + + {% endfor %} +
NameRolePriority
+ {{ contact.contact }} + {{ contact.role|placeholder }}{{ contact.get_priority_display|placeholder }} + {% if perms.tenancy.change_contactassignment %} + + + + {% endif %} + {% if perms.tenancy.delete_contactassignment %} + + + + {% endif %} +
+ {% else %} +
None
+ {% endif %} + {% endwith %} +
+ {% if perms.tenancy.add_contactassignment %} + + {% endif %} +
diff --git a/netbox/templates/tenancy/tenant.html b/netbox/templates/tenancy/tenant.html index dee7f7ce7..54b29e946 100644 --- a/netbox/templates/tenancy/tenant.html +++ b/netbox/templates/tenancy/tenant.html @@ -38,6 +38,7 @@ {% include 'inc/custom_fields_panel.html' %} {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='tenancy:tenant_list' %} {% include 'inc/comments_panel.html' %} + {% include 'inc/contacts_panel.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/virtualization/cluster.html b/netbox/templates/virtualization/cluster.html index 769ae431f..fa8cad039 100644 --- a/netbox/templates/virtualization/cluster.html +++ b/netbox/templates/virtualization/cluster.html @@ -62,6 +62,7 @@
{% include 'inc/custom_fields_panel.html' %} {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='virtualization:cluster_list' %} + {% include 'inc/contacts_panel.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/virtualization/clustergroup.html b/netbox/templates/virtualization/clustergroup.html index f7e8cbe5b..fd83c10f3 100644 --- a/netbox/templates/virtualization/clustergroup.html +++ b/netbox/templates/virtualization/clustergroup.html @@ -32,6 +32,7 @@
{% include 'inc/custom_fields_panel.html' %} + {% include 'inc/contacts_panel.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index 249ef91e4..0ef590112 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -173,6 +173,7 @@ {% endif %} + {% include 'inc/contacts_panel.html' %} {% plugin_right_page object %} diff --git a/netbox/tenancy/forms/models.py b/netbox/tenancy/forms/models.py index 6c0640d53..c0aec0aa8 100644 --- a/netbox/tenancy/forms/models.py +++ b/netbox/tenancy/forms/models.py @@ -1,11 +1,15 @@ +from django import forms + from extras.forms import CustomFieldModelForm from extras.models import Tag from tenancy.models import * from utilities.forms import ( BootstrapMixin, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField, SmallTextarea, + StaticSelect, ) __all__ = ( + 'ContactAssignmentForm', 'ContactForm', 'ContactGroupForm', 'ContactRoleForm', @@ -100,3 +104,25 @@ class ContactForm(BootstrapMixin, CustomFieldModelForm): widgets = { 'address': SmallTextarea(attrs={'rows': 3}), } + + +class ContactAssignmentForm(BootstrapMixin, forms.ModelForm): + group = DynamicModelChoiceField( + queryset=ContactGroup.objects.all(), + required=False + ) + contact = DynamicModelChoiceField( + queryset=Contact.objects.all() + ) + role = DynamicModelChoiceField( + queryset=ContactRole.objects.all() + ) + + class Meta: + model = ContactAssignment + fields = ( + 'group', 'contact', 'role', 'priority', + ) + widgets = { + 'priority': StaticSelect(), + } diff --git a/netbox/tenancy/models.py b/netbox/tenancy/models.py index f5e66b753..f416d55b5 100644 --- a/netbox/tenancy/models.py +++ b/netbox/tenancy/models.py @@ -1,4 +1,4 @@ -from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.models import ContentType from django.db import models from django.urls import reverse @@ -86,6 +86,11 @@ class Tenant(PrimaryModel): blank=True ) + # Generic relations + contacts = GenericRelation( + to='tenancy.ContactAssignment' + ) + objects = RestrictedQuerySet.as_manager() clone_fields = [ diff --git a/netbox/tenancy/urls.py b/netbox/tenancy/urls.py index 807af161e..14047603d 100644 --- a/netbox/tenancy/urls.py +++ b/netbox/tenancy/urls.py @@ -67,4 +67,9 @@ urlpatterns = [ path('contacts//changelog/', ObjectChangeLogView.as_view(), name='contact_changelog', kwargs={'model': Contact}), path('contacts//journal/', ObjectJournalView.as_view(), name='contact_journal', kwargs={'model': Contact}), + # Contact assignments + path('contact-assignments/add/', views.ContactAssignmentEditView.as_view(), name='contactassignment_add'), + path('contact-assignments//edit/', views.ContactAssignmentEditView.as_view(), name='contactassignment_edit'), + path('contact-assignments//delete/', views.ContactAssignmentDeleteView.as_view(), name='contactassignment_delete'), + ] diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index f4772b288..e7034ed5f 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -1,9 +1,12 @@ +from django.contrib.contenttypes.models import ContentType +from django.http import Http404 +from django.shortcuts import get_object_or_404 + from circuits.models import Circuit from dcim.models import Site, Rack, Device, RackReservation from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF from netbox.views import generic from utilities.tables import paginate_table -from utilities.utils import count_related from virtualization.models import VirtualMachine, Cluster from . import filtersets, forms, tables from .models import * @@ -309,3 +312,33 @@ class ContactBulkDeleteView(generic.BulkDeleteView): queryset = Contact.objects.prefetch_related('group') filterset = filtersets.ContactFilterSet table = tables.ContactTable + + +# +# Contact assignments +# + +class ContactAssignmentEditView(generic.ObjectEditView): + queryset = ContactAssignment.objects.all() + model_form = forms.ContactAssignmentForm + + def alter_obj(self, instance, request, args, kwargs): + if not instance.pk: + # Assign the object based on URL kwargs + try: + app_label, model = request.GET.get('content_type').split('.') + except (AttributeError, ValueError): + raise Http404("Content type not specified") + content_type = get_object_or_404(ContentType, app_label=app_label, model=model) + instance.object = get_object_or_404(content_type.model_class(), pk=request.GET.get('object_id')) + return instance + + def get_return_url(self, request, obj=None): + return obj.object.get_absolute_url() if obj else super().get_return_url(request) + + +class ContactAssignmentDeleteView(generic.ObjectDeleteView): + queryset = ContactAssignment.objects.all() + + def get_return_url(self, request, obj=None): + return obj.object.get_absolute_url() if obj else super().get_return_url(request) diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index 3408cedbc..d91a39549 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -81,12 +81,17 @@ class ClusterGroup(OrganizationalModel): max_length=200, blank=True ) + + # Generic relations vlan_groups = GenericRelation( to='ipam.VLANGroup', content_type_field='scope_type', object_id_field='scope_id', related_query_name='cluster_group' ) + contacts = GenericRelation( + to='tenancy.ContactAssignment' + ) objects = RestrictedQuerySet.as_manager() @@ -142,12 +147,17 @@ class Cluster(PrimaryModel): comments = models.TextField( blank=True ) + + # Generic relations vlan_groups = GenericRelation( to='ipam.VLANGroup', content_type_field='scope_type', object_id_field='scope_id', related_query_name='cluster' ) + contacts = GenericRelation( + to='tenancy.ContactAssignment' + ) objects = RestrictedQuerySet.as_manager() @@ -268,6 +278,11 @@ class VirtualMachine(PrimaryModel, ConfigContextModel): blank=True ) + # Generic relation + contacts = GenericRelation( + to='tenancy.ContactAssignment' + ) + objects = ConfigContextModelQuerySet.as_manager() clone_fields = [ From faf1e6a43d75cf826c9b460356a70814eb8427d2 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 18 Oct 2021 15:30:28 -0400 Subject: [PATCH 12/35] Add contact/role assignment tables --- netbox/templates/tenancy/contact.html | 13 +++++++++++++ netbox/templates/tenancy/contactrole.html | 6 ++++++ netbox/tenancy/tables.py | 23 ++++++++++++++++++----- netbox/tenancy/views.py | 21 +++++++++++++++++++-- 4 files changed, 56 insertions(+), 7 deletions(-) diff --git a/netbox/templates/tenancy/contact.html b/netbox/templates/tenancy/contact.html index 2ead52e5a..ca46fdb31 100644 --- a/netbox/templates/tenancy/contact.html +++ b/netbox/templates/tenancy/contact.html @@ -46,6 +46,12 @@ Address {{ object.address|linebreaksbr|placeholder }} + + Assignments + + {{ assignment_count }} + + @@ -60,6 +66,13 @@
+
+
Assignments
+
+ {% include 'inc/table.html' with table=contacts_table %} +
+
+ {% include 'inc/paginator.html' with paginator=contacts_table.paginator page=contacts_table.page %} {% plugin_full_width_page object %}
diff --git a/netbox/templates/tenancy/contactrole.html b/netbox/templates/tenancy/contactrole.html index 688c58177..f081afc34 100644 --- a/netbox/templates/tenancy/contactrole.html +++ b/netbox/templates/tenancy/contactrole.html @@ -21,6 +21,12 @@ Description {{ object.description|placeholder }} + + Assignments + + {{ assignment_count }} + + diff --git a/netbox/tenancy/tables.py b/netbox/tenancy/tables.py index 3401c8fe4..5b254842b 100644 --- a/netbox/tenancy/tables.py +++ b/netbox/tenancy/tables.py @@ -1,7 +1,7 @@ import django_tables2 as tables from utilities.tables import ( - BaseTable, ButtonsColumn, LinkedCountColumn, MarkdownColumn, MPTTColumn, TagColumn, ToggleColumn, + BaseTable, ButtonsColumn, ContentTypeColumn, LinkedCountColumn, MarkdownColumn, MPTTColumn, TagColumn, ToggleColumn, ) from .models import * @@ -126,23 +126,36 @@ class ContactTable(BaseTable): linkify=True ) comments = MarkdownColumn() + assignment_count = tables.Column( + verbose_name='Assignments' + ) tags = TagColumn( url_name='tenancy:tenant_list' ) class Meta(BaseTable.Meta): model = Contact - fields = ('pk', 'name', 'group', 'title', 'phone', 'email', 'address', 'comments', 'tags') - default_columns = ('pk', 'name', 'group', 'title', 'phone', 'email') + fields = ('pk', 'name', 'group', 'title', 'phone', 'email', 'address', 'comments', 'assignment_count', 'tags') + default_columns = ('pk', 'name', 'group', 'assignment_count', 'title', 'phone', 'email') class ContactAssignmentTable(BaseTable): pk = ToggleColumn() + content_type = ContentTypeColumn( + verbose_name='Object Type' + ) + object = tables.Column( + linkify=True, + orderable=False + ) contact = tables.Column( linkify=True ) + role = tables.Column( + linkify=True + ) class Meta(BaseTable.Meta): model = ContactAssignment - fields = ('pk', 'contact', 'role', 'priority') - default_columns = ('pk', 'contact', 'role', 'priority') + fields = ('pk', 'content_type', 'object', 'contact', 'role', 'priority') + default_columns = ('pk', 'object', 'contact', 'role', 'priority') diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index e7034ed5f..cdbaebdb1 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -7,6 +7,7 @@ from dcim.models import Site, Rack, Device, RackReservation from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF from netbox.views import generic from utilities.tables import paginate_table +from utilities.utils import count_related from virtualization.models import VirtualMachine, Cluster from . import filtersets, forms, tables from .models import * @@ -236,11 +237,12 @@ class ContactRoleView(generic.ObjectView): role=instance ) contacts_table = tables.ContactAssignmentTable(contact_assignments) + contacts_table.columns.hide('role') paginate_table(contacts_table, request) return { 'contacts_table': contacts_table, - 'contact_count': ContactAssignment.objects.filter(role=instance).count(), + 'assignment_count': ContactAssignment.objects.filter(role=instance).count(), } @@ -276,7 +278,9 @@ class ContactRoleBulkDeleteView(generic.BulkDeleteView): # class ContactListView(generic.ObjectListView): - queryset = Contact.objects.all() + queryset = Contact.objects.annotate( + assignment_count=count_related(ContactAssignment, 'contact') + ) filterset = filtersets.ContactFilterSet filterset_form = forms.ContactFilterForm table = tables.ContactTable @@ -285,6 +289,19 @@ class ContactListView(generic.ObjectListView): class ContactView(generic.ObjectView): queryset = Contact.objects.all() + def get_extra_context(self, request, instance): + contact_assignments = ContactAssignment.objects.restrict(request.user, 'view').filter( + contact=instance + ) + contacts_table = tables.ContactAssignmentTable(contact_assignments) + contacts_table.columns.hide('contact') + paginate_table(contacts_table, request) + + return { + 'contacts_table': contacts_table, + 'assignment_count': ContactAssignment.objects.filter(contact=instance).count(), + } + class ContactEditView(generic.ObjectEditView): queryset = Contact.objects.all() From f485a47b4849c1e756f3d3c5c56c3ccb49688a03 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 18 Oct 2021 15:41:29 -0400 Subject: [PATCH 13/35] Tweak uniqueness constraints --- netbox/tenancy/migrations/0003_contacts.py | 11 +++++++---- netbox/tenancy/models.py | 12 ++++++++---- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/netbox/tenancy/migrations/0003_contacts.py b/netbox/tenancy/migrations/0003_contacts.py index dc6f6c668..6f77810f3 100644 --- a/netbox/tenancy/migrations/0003_contacts.py +++ b/netbox/tenancy/migrations/0003_contacts.py @@ -1,5 +1,3 @@ -# Generated by Django 3.2.8 on 2021-10-18 16:12 - import django.core.serializers.json from django.db import migrations, models import django.db.models.deletion @@ -56,8 +54,8 @@ class Migration(migrations.Migration): ('last_updated', models.DateTimeField(auto_now=True, null=True)), ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)), ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=100, unique=True)), - ('slug', models.SlugField(max_length=100, unique=True)), + ('name', models.CharField(max_length=100)), + ('slug', models.SlugField(max_length=100)), ('description', models.CharField(blank=True, max_length=200)), ('lft', models.PositiveIntegerField(editable=False)), ('rght', models.PositiveIntegerField(editable=False)), @@ -67,6 +65,7 @@ class Migration(migrations.Migration): ], options={ 'ordering': ['name'], + 'unique_together': {('parent', 'name')}, }, ), migrations.CreateModel( @@ -95,4 +94,8 @@ class Migration(migrations.Migration): name='tags', field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), ), + migrations.AlterUniqueTogether( + name='contact', + unique_together={('group', 'name')}, + ), ] diff --git a/netbox/tenancy/models.py b/netbox/tenancy/models.py index f416d55b5..f53f7d0e6 100644 --- a/netbox/tenancy/models.py +++ b/netbox/tenancy/models.py @@ -117,12 +117,10 @@ class ContactGroup(NestedGroupModel): An arbitrary collection of Contacts. """ name = models.CharField( - max_length=100, - unique=True + max_length=100 ) slug = models.SlugField( - max_length=100, - unique=True + max_length=100 ) parent = TreeForeignKey( to='self', @@ -139,6 +137,9 @@ class ContactGroup(NestedGroupModel): class Meta: ordering = ['name'] + unique_together = ( + ('parent', 'name') + ) def get_absolute_url(self): return reverse('tenancy:contactgroup', args=[self.pk]) @@ -216,6 +217,9 @@ class Contact(PrimaryModel): class Meta: ordering = ['name'] + unique_together = ( + ('group', 'name') + ) def __str__(self): return self.name From 487d67768bf6a52c9e96e8a1a3aebda17640f0fe Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 18 Oct 2021 16:20:31 -0400 Subject: [PATCH 14/35] Cleanup and documentation for #1344 --- docs/core-functionality/contacts.md | 5 +++++ docs/models/tenancy/contact.md | 31 +++++++++++++++++++++++++++++ docs/models/tenancy/contactgroup.md | 3 +++ docs/models/tenancy/contactrole.md | 3 +++ mkdocs.yml | 1 + netbox/tenancy/api/serializers.py | 7 +++++-- netbox/tenancy/filtersets.py | 9 +++++---- netbox/tenancy/forms/bulk_edit.py | 15 ++++++++++++++ netbox/tenancy/forms/models.py | 10 ++++++++-- netbox/tenancy/models.py | 3 +++ 10 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 docs/core-functionality/contacts.md create mode 100644 docs/models/tenancy/contact.md create mode 100644 docs/models/tenancy/contactgroup.md create mode 100644 docs/models/tenancy/contactrole.md diff --git a/docs/core-functionality/contacts.md b/docs/core-functionality/contacts.md new file mode 100644 index 000000000..76a005fc0 --- /dev/null +++ b/docs/core-functionality/contacts.md @@ -0,0 +1,5 @@ +# Contacts + +{!models/tenancy/contact.md!} +{!models/tenancy/contactgroup.md!} +{!models/tenancy/contactrole.md!} diff --git a/docs/models/tenancy/contact.md b/docs/models/tenancy/contact.md new file mode 100644 index 000000000..9d81e2d85 --- /dev/null +++ b/docs/models/tenancy/contact.md @@ -0,0 +1,31 @@ +# Contacts + +A contact represent an individual or group that has been associated with an object in NetBox for administrative reasons. For example, you might assign one or more operational contacts to each site. Contacts can be arranged within nested contact groups. + +Each contact must include a name, which is unique to its parent group (if any). The following optional descriptors are also available: + +* Title +* Phone +* Email +* Address + +## Contact Assignment + +Each contact can be assigned to one or more objects, allowing for the efficient reuse of contact information. When assigning a contact to an object, the user may optionally specify a role and/or priority (primary, secondary, tertiary, or inactive) to better convey the nature of the contact's relationship to the assigned object. + +The following models support the assignment of contacts: + +* circuits.Circuit +* circuits.Provider +* dcim.Device +* dcim.Location +* dcim.Manufacturer +* dcim.PowerPanel +* dcim.Rack +* dcim.Region +* dcim.Site +* dcim.SiteGroup +* tenancy.Tenant +* virtualization.Cluster +* virtualization.ClusterGroup +* virtualization.VirtualMachine diff --git a/docs/models/tenancy/contactgroup.md b/docs/models/tenancy/contactgroup.md new file mode 100644 index 000000000..ea566c58a --- /dev/null +++ b/docs/models/tenancy/contactgroup.md @@ -0,0 +1,3 @@ +# Contact Groups + +Contacts can be organized into arbitrary groups. These groups can be recursively nested for convenience. Each contact within a group must have a unique name, but other attributes can be repeated. diff --git a/docs/models/tenancy/contactrole.md b/docs/models/tenancy/contactrole.md new file mode 100644 index 000000000..23642ca03 --- /dev/null +++ b/docs/models/tenancy/contactrole.md @@ -0,0 +1,3 @@ +# Contact Roles + +Contacts can be organized by functional roles, which are fully customizable by the user. For example, you might create roles for administrative, operational, or emergency contacts. diff --git a/mkdocs.yml b/mkdocs.yml index 7244c36d6..72750d6f5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -62,6 +62,7 @@ nav: - Circuits: 'core-functionality/circuits.md' - Power Tracking: 'core-functionality/power.md' - Tenancy: 'core-functionality/tenancy.md' + - Contacts: 'core-functionality/contacts.md' - Customization: - Custom Fields: 'customization/custom-fields.md' - Custom Validation: 'customization/custom-validation.md' diff --git a/netbox/tenancy/api/serializers.py b/netbox/tenancy/api/serializers.py index 2dfb59455..27a14b350 100644 --- a/netbox/tenancy/api/serializers.py +++ b/netbox/tenancy/api/serializers.py @@ -1,8 +1,9 @@ from django.contrib.auth.models import ContentType from rest_framework import serializers -from netbox.api import ContentTypeField +from netbox.api import ChoiceField, ContentTypeField from netbox.api.serializers import NestedGroupModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer +from tenancy.choices import ContactPriorityChoices from tenancy.models import * from .nested_serializers import * @@ -93,9 +94,11 @@ class ContactAssignmentSerializer(PrimaryModelSerializer): ) contact = NestedContactSerializer() role = NestedContactRoleSerializer(required=False, allow_null=True) + priority = ChoiceField(choices=ContactPriorityChoices, required=False) class Meta: model = ContactAssignment fields = [ - 'id', 'url', 'display', 'content_type', 'object_id', 'contact', 'role', 'created', 'last_updated', + 'id', 'url', 'display', 'content_type', 'object_id', 'contact', 'role', 'priority', 'created', + 'last_updated', ] diff --git a/netbox/tenancy/filtersets.py b/netbox/tenancy/filtersets.py index 75f9e351d..f6d0ac72e 100644 --- a/netbox/tenancy/filtersets.py +++ b/netbox/tenancy/filtersets.py @@ -2,8 +2,8 @@ import django_filters from django.db.models import Q from extras.filters import TagFilter -from netbox.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet -from utilities.filters import TreeNodeMultipleChoiceFilter +from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet +from utilities.filters import ContentTypeFilter, TreeNodeMultipleChoiceFilter from .models import * @@ -168,7 +168,8 @@ class ContactFilterSet(PrimaryModelFilterSet): ) -class ContactAssignmentFilterSet(OrganizationalModelFilterSet): +class ContactAssignmentFilterSet(ChangeLoggedModelFilterSet): + content_type = ContentTypeFilter() contact_id = django_filters.ModelMultipleChoiceFilter( queryset=Contact.objects.all(), label='Contact (ID)', @@ -186,4 +187,4 @@ class ContactAssignmentFilterSet(OrganizationalModelFilterSet): class Meta: model = ContactAssignment - fields = ['id', 'priority'] + fields = ['id', 'content_type_id', 'priority'] diff --git a/netbox/tenancy/forms/bulk_edit.py b/netbox/tenancy/forms/bulk_edit.py index 0d414d2a5..a34b8def1 100644 --- a/netbox/tenancy/forms/bulk_edit.py +++ b/netbox/tenancy/forms/bulk_edit.py @@ -96,6 +96,21 @@ class ContactBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBul queryset=ContactGroup.objects.all(), required=False ) + title = forms.CharField( + max_length=100, + required=False + ) + phone = forms.CharField( + max_length=50, + required=False + ) + email = forms.EmailField( + required=False + ) + address = forms.CharField( + max_length=200, + required=False + ) class Meta: nullable_fields = ['group', 'title', 'phone', 'email', 'address', 'comments'] diff --git a/netbox/tenancy/forms/models.py b/netbox/tenancy/forms/models.py index c0aec0aa8..b15065705 100644 --- a/netbox/tenancy/forms/models.py +++ b/netbox/tenancy/forms/models.py @@ -109,10 +109,16 @@ class ContactForm(BootstrapMixin, CustomFieldModelForm): class ContactAssignmentForm(BootstrapMixin, forms.ModelForm): group = DynamicModelChoiceField( queryset=ContactGroup.objects.all(), - required=False + required=False, + initial_params={ + 'contacts': '$contact' + } ) contact = DynamicModelChoiceField( - queryset=Contact.objects.all() + queryset=Contact.objects.all(), + query_params={ + 'group_id': '$group' + } ) role = DynamicModelChoiceField( queryset=ContactRole.objects.all() diff --git a/netbox/tenancy/models.py b/netbox/tenancy/models.py index f53f7d0e6..20708f74a 100644 --- a/netbox/tenancy/models.py +++ b/netbox/tenancy/models.py @@ -259,3 +259,6 @@ class ContactAssignment(ChangeLoggedModel): class Meta: ordering = ('priority', 'contact') + + def __str__(self): + return f"{self.contact} ({self.get_priority_display()})" if self.priority else self.name From b44a5ea60956f63a5514f7b221a9914e3f510012 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 18 Oct 2021 16:33:31 -0400 Subject: [PATCH 15/35] Prevent duplicate contact assignments --- netbox/tenancy/migrations/0003_contacts.py | 54 +++++++++------------- netbox/tenancy/models.py | 1 + 2 files changed, 23 insertions(+), 32 deletions(-) diff --git a/netbox/tenancy/migrations/0003_contacts.py b/netbox/tenancy/migrations/0003_contacts.py index 6f77810f3..35e568ab1 100644 --- a/netbox/tenancy/migrations/0003_contacts.py +++ b/netbox/tenancy/migrations/0003_contacts.py @@ -14,24 +14,6 @@ class Migration(migrations.Migration): ] operations = [ - migrations.CreateModel( - name='Contact', - fields=[ - ('created', models.DateField(auto_now_add=True, null=True)), - ('last_updated', models.DateTimeField(auto_now=True, null=True)), - ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)), - ('id', models.BigAutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=100)), - ('title', models.CharField(blank=True, max_length=100)), - ('phone', models.CharField(blank=True, max_length=50)), - ('email', models.EmailField(blank=True, max_length=254)), - ('address', models.CharField(blank=True, max_length=200)), - ('comments', models.TextField(blank=True)), - ], - options={ - 'ordering': ['name'], - }, - ), migrations.CreateModel( name='ContactRole', fields=[ @@ -68,6 +50,27 @@ class Migration(migrations.Migration): 'unique_together': {('parent', 'name')}, }, ), + migrations.CreateModel( + name='Contact', + fields=[ + ('created', models.DateField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)), + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=100)), + ('title', models.CharField(blank=True, max_length=100)), + ('phone', models.CharField(blank=True, max_length=50)), + ('email', models.EmailField(blank=True, max_length=254)), + ('address', models.CharField(blank=True, max_length=200)), + ('comments', models.TextField(blank=True)), + ('group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='contacts', to='tenancy.contactgroup')), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ], + options={ + 'ordering': ['name'], + 'unique_together': {('group', 'name')}, + }, + ), migrations.CreateModel( name='ContactAssignment', fields=[ @@ -82,20 +85,7 @@ class Migration(migrations.Migration): ], options={ 'ordering': ('priority', 'contact'), + 'unique_together': {('content_type', 'object_id', 'contact', 'role', 'priority')}, }, ), - migrations.AddField( - model_name='contact', - name='group', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='contacts', to='tenancy.contactgroup'), - ), - migrations.AddField( - model_name='contact', - name='tags', - field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), - ), - migrations.AlterUniqueTogether( - name='contact', - unique_together={('group', 'name')}, - ), ] diff --git a/netbox/tenancy/models.py b/netbox/tenancy/models.py index 20708f74a..35c10938b 100644 --- a/netbox/tenancy/models.py +++ b/netbox/tenancy/models.py @@ -259,6 +259,7 @@ class ContactAssignment(ChangeLoggedModel): class Meta: ordering = ('priority', 'contact') + unique_together = ('content_type', 'object_id', 'contact', 'role', 'priority') def __str__(self): return f"{self.contact} ({self.get_priority_display()})" if self.priority else self.name From 554b44b9f2dfc8d10ddc2e4b3dd492c57bc81d4b Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 18 Oct 2021 16:47:49 -0400 Subject: [PATCH 16/35] Fix string repr for ContactAssignment --- netbox/tenancy/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/netbox/tenancy/models.py b/netbox/tenancy/models.py index 35c10938b..c709236e2 100644 --- a/netbox/tenancy/models.py +++ b/netbox/tenancy/models.py @@ -262,4 +262,6 @@ class ContactAssignment(ChangeLoggedModel): unique_together = ('content_type', 'object_id', 'contact', 'role', 'priority') def __str__(self): - return f"{self.contact} ({self.get_priority_display()})" if self.priority else self.name + if self.priority: + return f"{self.contact} ({self.get_priority_display()})" + return str(self.contact) From 79cee12b1eac59e07145925c75d4d8f355cebe4e Mon Sep 17 00:00:00 2001 From: PieterL75 <74899468+PieterL75@users.noreply.github.com> Date: Tue, 19 Oct 2021 16:23:05 +0200 Subject: [PATCH 17/35] Updated release notes with #7556 --- docs/release-notes/version-3.0.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index 70a9a0dac..8b0e99b4f 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -10,6 +10,7 @@ * [#7534](https://github.com/netbox-community/netbox/issues/7534) - Avoid exception when utilizing "create and add another" twice in succession * [#7544](https://github.com/netbox-community/netbox/issues/7544) - Fix multi-value filtering of custom field objects * [#7545](https://github.com/netbox-community/netbox/issues/7545) - Fix incorrect display of update/delete events for webhooks +* [#7556](https://github.com/netbox-community/netbox/issues/7556) - Fix display of New Version --- From 0afd3e61894aaf792e4d09c17c493138f097ee54 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 19 Oct 2021 12:33:17 -0400 Subject: [PATCH 18/35] Closes #6715: Add tenant assignment for cables --- docs/release-notes/version-3.1.md | 5 ++++ netbox/dcim/api/serializers.py | 5 ++-- netbox/dcim/filtersets.py | 10 +------- netbox/dcim/forms/bulk_edit.py | 6 ++++- netbox/dcim/forms/bulk_import.py | 8 ++++++- netbox/dcim/forms/connections.py | 17 ++++++++------ netbox/dcim/forms/filtersets.py | 10 ++------ netbox/dcim/forms/models.py | 4 ++-- ...n_tenant.py => 0135_tenancy_extensions.py} | 5 ++++ netbox/dcim/migrations/0136_device_airflow.py | 2 +- netbox/dcim/models/cables.py | 7 ++++++ netbox/dcim/tables/cables.py | 4 +++- netbox/dcim/tests/test_filtersets.py | 23 ++++++++++--------- netbox/templates/dcim/cable.html | 13 +++++++++++ netbox/templates/dcim/inc/cable_form.html | 4 +++- 15 files changed, 79 insertions(+), 44 deletions(-) rename netbox/dcim/migrations/{0135_location_tenant.py => 0135_tenancy_extensions.py} (67%) diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index c49552edd..630e46b5b 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -3,11 +3,16 @@ !!! warning "PostgreSQL 10 Required" NetBox v3.1 requires PostgreSQL 10 or later. +### Breaking Changes + +* The `tenant` and `tenant_id` filters for the Cable model now filter on the tenant assigned directly to each cable, rather than on the parent object of either termination. + ### Enhancements * [#1337](https://github.com/netbox-community/netbox/issues/1337) - Add WWN field to interfaces * [#3839](https://github.com/netbox-community/netbox/issues/3839) - Add `airflow` field for devices types and devices * [#6711](https://github.com/netbox-community/netbox/issues/6711) - Add `longtext` custom field type with Markdown support +* [#6715](https://github.com/netbox-community/netbox/issues/6715) - Add tenant assignment for cables * [#6874](https://github.com/netbox-community/netbox/issues/6874) - Add tenant assignment for locations ### Other Changes diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 9d261d9e8..14a1af8f0 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -758,14 +758,15 @@ class CableSerializer(PrimaryModelSerializer): termination_a = serializers.SerializerMethodField(read_only=True) termination_b = serializers.SerializerMethodField(read_only=True) status = ChoiceField(choices=CableStatusChoices, required=False) + tenant = NestedTenantSerializer(required=False, allow_null=True) length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False) class Meta: model = Cable fields = [ 'id', 'url', 'display', 'termination_a_type', 'termination_a_id', 'termination_a', 'termination_b_type', - 'termination_b_id', 'termination_b', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags', - 'custom_fields', + 'termination_b_id', 'termination_b', 'type', 'status', 'tenant', 'label', 'color', 'length', 'length_unit', + 'tags', 'custom_fields', ] def _get_termination(self, obj, side): diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index c3de7cb08..c66397029 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -1189,7 +1189,7 @@ class VirtualChassisFilterSet(PrimaryModelFilterSet): return queryset.filter(qs_filter).distinct() -class CableFilterSet(PrimaryModelFilterSet): +class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -1230,14 +1230,6 @@ class CableFilterSet(PrimaryModelFilterSet): method='filter_device', field_name='device__site__slug' ) - tenant_id = MultiValueNumberFilter( - method='filter_device', - field_name='device__tenant_id' - ) - tenant = MultiValueNumberFilter( - method='filter_device', - field_name='device__tenant__slug' - ) tag = TagFilter() class Meta: diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 289057be9..06ccc958c 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -468,6 +468,10 @@ class CableBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkE widget=StaticSelect(), initial='' ) + tenant = DynamicModelChoiceField( + queryset=Tenant.objects.all(), + required=False + ) label = forms.CharField( max_length=100, required=False @@ -488,7 +492,7 @@ class CableBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkE class Meta: nullable_fields = [ - 'type', 'status', 'label', 'color', 'length', + 'type', 'status', 'tenant', 'label', 'color', 'length', ] def clean(self): diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index bd9e8cd4a..720ea8dbd 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -821,6 +821,12 @@ class CableCSVForm(CustomFieldModelCSVForm): required=False, help_text='Physical medium classification' ) + tenant = CSVModelChoiceField( + queryset=Tenant.objects.all(), + required=False, + to_field_name='name', + help_text='Assigned tenant' + ) length_unit = CSVChoiceField( choices=CableLengthUnitChoices, required=False, @@ -831,7 +837,7 @@ class CableCSVForm(CustomFieldModelCSVForm): model = Cable fields = [ 'side_a_device', 'side_a_type', 'side_a_name', 'side_b_device', 'side_b_type', 'side_b_name', 'type', - 'status', 'label', 'color', 'length', 'length_unit', + 'status', 'tenant', 'label', 'color', 'length', 'length_unit', ] help_texts = { 'color': mark_safe('RGB color in hexadecimal (e.g. 00ff00)'), diff --git a/netbox/dcim/forms/connections.py b/netbox/dcim/forms/connections.py index a2ceea6cf..770dc211b 100644 --- a/netbox/dcim/forms/connections.py +++ b/netbox/dcim/forms/connections.py @@ -2,6 +2,7 @@ from circuits.models import Circuit, CircuitTermination, Provider from dcim.models import * from extras.forms import CustomFieldModelForm from extras.models import Tag +from tenancy.forms import TenancyForm from utilities.forms import BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect __all__ = ( @@ -17,7 +18,7 @@ __all__ = ( ) -class ConnectCableToDeviceForm(BootstrapMixin, CustomFieldModelForm): +class ConnectCableToDeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): """ Base form for connecting a Cable to a Device component """ @@ -78,7 +79,8 @@ class ConnectCableToDeviceForm(BootstrapMixin, CustomFieldModelForm): model = Cable fields = [ 'termination_b_region', 'termination_b_site', 'termination_b_rack', 'termination_b_device', - 'termination_b_id', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags', + 'termination_b_id', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color', 'length', 'length_unit', + 'tags', ] widgets = { 'status': StaticSelect, @@ -169,7 +171,7 @@ class ConnectCableToRearPortForm(ConnectCableToDeviceForm): ) -class ConnectCableToCircuitTerminationForm(BootstrapMixin, CustomFieldModelForm): +class ConnectCableToCircuitTerminationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): termination_b_provider = DynamicModelChoiceField( queryset=Provider.objects.all(), label='Provider', @@ -219,7 +221,8 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, CustomFieldModelForm) model = Cable fields = [ 'termination_b_provider', 'termination_b_region', 'termination_b_site', 'termination_b_circuit', - 'termination_b_id', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags', + 'termination_b_id', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color', 'length', 'length_unit', + 'tags', ] def clean_termination_b_id(self): @@ -227,7 +230,7 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, CustomFieldModelForm) return getattr(self.cleaned_data['termination_b_id'], 'pk', None) -class ConnectCableToPowerFeedForm(BootstrapMixin, CustomFieldModelForm): +class ConnectCableToPowerFeedForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): termination_b_region = DynamicModelChoiceField( queryset=Region.objects.all(), label='Region', @@ -280,8 +283,8 @@ class ConnectCableToPowerFeedForm(BootstrapMixin, CustomFieldModelForm): class Meta: model = Cable fields = [ - 'termination_b_location', 'termination_b_powerpanel', 'termination_b_id', 'type', 'status', 'label', - 'color', 'length', 'length_unit', 'tags', + 'termination_b_location', 'termination_b_powerpanel', 'termination_b_id', 'type', 'status', 'tenant_group', + 'tenant', 'label', 'color', 'length', 'length_unit', 'tags', ] def clean_termination_b_id(self): diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 94e7bce05..501e78b18 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -691,13 +691,13 @@ class VirtualChassisFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldMod tag = TagFilterField(model) -class CableFilterForm(BootstrapMixin, CustomFieldModelFilterForm): +class CableFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): model = Cable field_groups = [ ['q', 'tag'], ['site_id', 'rack_id', 'device_id'], ['type', 'status', 'color'], - ['tenant_id'], + ['tenant_group_id', 'tenant_id'], ] q = forms.CharField( required=False, @@ -719,12 +719,6 @@ class CableFilterForm(BootstrapMixin, CustomFieldModelFilterForm): label=_('Site'), fetch_trigger='open' ) - tenant_id = DynamicModelMultipleChoiceField( - queryset=Tenant.objects.all(), - required=False, - label=_('Tenant'), - fetch_trigger='open' - ) rack_id = DynamicModelMultipleChoiceField( queryset=Rack.objects.all(), required=False, diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py index cb690840f..8236b1a97 100644 --- a/netbox/dcim/forms/models.py +++ b/netbox/dcim/forms/models.py @@ -601,7 +601,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): self.fields['position'].widget.choices = [(position, f'U{position}')] -class CableForm(BootstrapMixin, CustomFieldModelForm): +class CableForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False @@ -610,7 +610,7 @@ class CableForm(BootstrapMixin, CustomFieldModelForm): class Meta: model = Cable fields = [ - 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags', + 'type', 'status', 'tenant_group', 'tenant', 'label', 'color', 'length', 'length_unit', 'tags', ] widgets = { 'status': StaticSelect, diff --git a/netbox/dcim/migrations/0135_location_tenant.py b/netbox/dcim/migrations/0135_tenancy_extensions.py similarity index 67% rename from netbox/dcim/migrations/0135_location_tenant.py rename to netbox/dcim/migrations/0135_tenancy_extensions.py index 0b1f429f9..673b5027f 100644 --- a/netbox/dcim/migrations/0135_location_tenant.py +++ b/netbox/dcim/migrations/0135_tenancy_extensions.py @@ -15,4 +15,9 @@ class Migration(migrations.Migration): name='tenant', field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='locations', to='tenancy.tenant'), ), + migrations.AddField( + model_name='cable', + name='tenant', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='cables', to='tenancy.tenant'), + ), ] diff --git a/netbox/dcim/migrations/0136_device_airflow.py b/netbox/dcim/migrations/0136_device_airflow.py index a0887a0b4..94cc89f3f 100644 --- a/netbox/dcim/migrations/0136_device_airflow.py +++ b/netbox/dcim/migrations/0136_device_airflow.py @@ -4,7 +4,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('dcim', '0135_location_tenant'), + ('dcim', '0135_tenancy_extensions'), ] operations = [ diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index c3f8cac3f..bddce93b9 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -67,6 +67,13 @@ class Cable(PrimaryModel): choices=CableStatusChoices, default=CableStatusChoices.STATUS_CONNECTED ) + tenant = models.ForeignKey( + to='tenancy.Tenant', + on_delete=models.PROTECT, + related_name='cables', + blank=True, + null=True + ) label = models.CharField( max_length=100, blank=True diff --git a/netbox/dcim/tables/cables.py b/netbox/dcim/tables/cables.py index 14cf34505..87913cbfd 100644 --- a/netbox/dcim/tables/cables.py +++ b/netbox/dcim/tables/cables.py @@ -2,6 +2,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor from dcim.models import Cable +from tenancy.tables import TenantColumn from utilities.tables import BaseTable, ChoiceFieldColumn, ColorColumn, TagColumn, TemplateColumn, ToggleColumn from .template_code import CABLE_LENGTH, CABLE_TERMINATION_PARENT @@ -45,6 +46,7 @@ class CableTable(BaseTable): verbose_name='Termination B' ) status = ChoiceFieldColumn() + tenant = TenantColumn() length = TemplateColumn( template_code=CABLE_LENGTH, order_by='_abs_length' @@ -58,7 +60,7 @@ class CableTable(BaseTable): model = Cable fields = ( 'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b', - 'status', 'type', 'color', 'length', 'tags', + 'status', 'type', 'tenant', 'color', 'length', 'tags', ) default_columns = ( 'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b', diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index fcee2914b..ce78e0470 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -2819,6 +2819,7 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests): tenants = ( Tenant(name='Tenant 1', slug='tenant-1'), Tenant(name='Tenant 2', slug='tenant-2'), + Tenant(name='Tenant 3', slug='tenant-3'), ) Tenant.objects.bulk_create(tenants) @@ -2834,9 +2835,9 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests): device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1') devices = ( - Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], position=1, tenant=tenants[0]), - Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], position=2, tenant=tenants[0]), - Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[1], rack=racks[1], position=1, tenant=tenants[1]), + Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], position=1), + Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], position=2), + Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[1], rack=racks[1], position=1), Device(name='Device 4', device_type=device_type, device_role=device_role, site=sites[1], rack=racks[1], position=2), Device(name='Device 5', device_type=device_type, device_role=device_role, site=sites[2], rack=racks[2], position=1), Device(name='Device 6', device_type=device_type, device_role=device_role, site=sites[2], rack=racks[2], position=2), @@ -2863,12 +2864,12 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests): console_server_port = ConsoleServerPort.objects.create(device=devices[0], name='Console Server Port 1') # Cables - Cable(termination_a=interfaces[1], termination_b=interfaces[2], label='Cable 1', type=CableTypeChoices.TYPE_CAT3, status=CableStatusChoices.STATUS_CONNECTED, color='aa1409', length=10, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() - Cable(termination_a=interfaces[3], termination_b=interfaces[4], label='Cable 2', type=CableTypeChoices.TYPE_CAT3, status=CableStatusChoices.STATUS_CONNECTED, color='aa1409', length=20, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() - Cable(termination_a=interfaces[5], termination_b=interfaces[6], label='Cable 3', type=CableTypeChoices.TYPE_CAT5E, status=CableStatusChoices.STATUS_CONNECTED, color='f44336', length=30, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() - Cable(termination_a=interfaces[7], termination_b=interfaces[8], label='Cable 4', type=CableTypeChoices.TYPE_CAT5E, status=CableStatusChoices.STATUS_PLANNED, color='f44336', length=40, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() - Cable(termination_a=interfaces[9], termination_b=interfaces[10], label='Cable 5', type=CableTypeChoices.TYPE_CAT6, status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=10, length_unit=CableLengthUnitChoices.UNIT_METER).save() - Cable(termination_a=interfaces[11], termination_b=interfaces[0], label='Cable 6', type=CableTypeChoices.TYPE_CAT6, status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=20, length_unit=CableLengthUnitChoices.UNIT_METER).save() + Cable(termination_a=interfaces[1], termination_b=interfaces[2], label='Cable 1', type=CableTypeChoices.TYPE_CAT3, tenant=tenants[0], status=CableStatusChoices.STATUS_CONNECTED, color='aa1409', length=10, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() + Cable(termination_a=interfaces[3], termination_b=interfaces[4], label='Cable 2', type=CableTypeChoices.TYPE_CAT3, tenant=tenants[0], status=CableStatusChoices.STATUS_CONNECTED, color='aa1409', length=20, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() + Cable(termination_a=interfaces[5], termination_b=interfaces[6], label='Cable 3', type=CableTypeChoices.TYPE_CAT5E, tenant=tenants[1], status=CableStatusChoices.STATUS_CONNECTED, color='f44336', length=30, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() + Cable(termination_a=interfaces[7], termination_b=interfaces[8], label='Cable 4', type=CableTypeChoices.TYPE_CAT5E, tenant=tenants[1], status=CableStatusChoices.STATUS_PLANNED, color='f44336', length=40, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() + Cable(termination_a=interfaces[9], termination_b=interfaces[10], label='Cable 5', type=CableTypeChoices.TYPE_CAT6, tenant=tenants[2], status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=10, length_unit=CableLengthUnitChoices.UNIT_METER).save() + Cable(termination_a=interfaces[11], termination_b=interfaces[0], label='Cable 6', type=CableTypeChoices.TYPE_CAT6, tenant=tenants[2], status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=20, length_unit=CableLengthUnitChoices.UNIT_METER).save() Cable(termination_a=console_port, termination_b=console_server_port, label='Cable 7').save() def test_label(self): @@ -2921,9 +2922,9 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests): def test_tenant(self): tenant = Tenant.objects.all()[:2] params = {'tenant_id': [tenant[0].pk, tenant[1].pk]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) params = {'tenant': [tenant[0].slug, tenant[1].slug]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) def test_termination_types(self): params = {'termination_a_type': 'dcim.consoleport'} diff --git a/netbox/templates/dcim/cable.html b/netbox/templates/dcim/cable.html index c7cd71b65..e9cde6e00 100644 --- a/netbox/templates/dcim/cable.html +++ b/netbox/templates/dcim/cable.html @@ -23,6 +23,19 @@ {{ object.get_status_display }} + + Tenant + + {% if object.tenant %} + {% if object.tenant.group %} + {{ object.tenant.group }} / + {% endif %} + {{ object.tenant }} + {% else %} + None + {% endif %} + + Label {{ object.label|placeholder }} diff --git a/netbox/templates/dcim/inc/cable_form.html b/netbox/templates/dcim/inc/cable_form.html index 05929821c..0f11ac3cb 100644 --- a/netbox/templates/dcim/inc/cable_form.html +++ b/netbox/templates/dcim/inc/cable_form.html @@ -2,6 +2,8 @@ {% render_field form.status %} {% render_field form.type %} +{% render_field form.tenant_group %} +{% render_field form.tenant %} {% render_field form.label %} {% render_field form.color %}
@@ -17,7 +19,7 @@ {% render_field form.tags %} {% if form.custom_fields %}
-
+
Custom Fields
{% render_custom_fields form %} From 8375995680947986900f9f9abefd56e54513d15c Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 19 Oct 2021 13:06:41 -0400 Subject: [PATCH 19/35] Closes #1943: Relax uniqueness constraint on cluster names --- docs/models/virtualization/cluster.md | 2 +- docs/release-notes/version-3.1.md | 1 + netbox/virtualization/api/serializers.py | 4 ++-- .../0024_cluster_relax_uniqueness.py | 21 +++++++++++++++++++ netbox/virtualization/models.py | 7 +++++-- 5 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 netbox/virtualization/migrations/0024_cluster_relax_uniqueness.py diff --git a/docs/models/virtualization/cluster.md b/docs/models/virtualization/cluster.md index 3311ad42d..7fc9bfc06 100644 --- a/docs/models/virtualization/cluster.md +++ b/docs/models/virtualization/cluster.md @@ -1,5 +1,5 @@ # Clusters -A cluster is a logical grouping of physical resources within which virtual machines run. A cluster must be assigned a type (technological classification), and may optionally be assigned to a cluster group, site, and/or tenant. +A cluster is a logical grouping of physical resources within which virtual machines run. A cluster must be assigned a type (technological classification), and may optionally be assigned to a cluster group, site, and/or tenant. Each cluster must have a unique name within its assigned group and/or site, if any. Physical devices may be associated with clusters as hosts. This allows users to track on which host(s) a particular virtual machine may reside. However, NetBox does not support pinning a specific VM within a cluster to a particular host device. diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index 630e46b5b..172fd3ed8 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -10,6 +10,7 @@ ### Enhancements * [#1337](https://github.com/netbox-community/netbox/issues/1337) - Add WWN field to interfaces +* [#1943](https://github.com/netbox-community/netbox/issues/1943) - Relax uniqueness constraint on cluster names * [#3839](https://github.com/netbox-community/netbox/issues/3839) - Add `airflow` field for devices types and devices * [#6711](https://github.com/netbox-community/netbox/issues/6711) - Add `longtext` custom field type with Markdown support * [#6715](https://github.com/netbox-community/netbox/issues/6715) - Add tenant assignment for cables diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py index adad9bf4d..1928960a9 100644 --- a/netbox/virtualization/api/serializers.py +++ b/netbox/virtualization/api/serializers.py @@ -44,9 +44,9 @@ class ClusterGroupSerializer(OrganizationalModelSerializer): class ClusterSerializer(PrimaryModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail') type = NestedClusterTypeSerializer() - group = NestedClusterGroupSerializer(required=False, allow_null=True) + group = NestedClusterGroupSerializer(required=False, allow_null=True, default=None) tenant = NestedTenantSerializer(required=False, allow_null=True) - site = NestedSiteSerializer(required=False, allow_null=True) + site = NestedSiteSerializer(required=False, allow_null=True, default=None) device_count = serializers.IntegerField(read_only=True) virtualmachine_count = serializers.IntegerField(read_only=True) diff --git a/netbox/virtualization/migrations/0024_cluster_relax_uniqueness.py b/netbox/virtualization/migrations/0024_cluster_relax_uniqueness.py new file mode 100644 index 000000000..5ff214d29 --- /dev/null +++ b/netbox/virtualization/migrations/0024_cluster_relax_uniqueness.py @@ -0,0 +1,21 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0136_device_airflow'), + ('virtualization', '0023_virtualmachine_natural_ordering'), + ] + + operations = [ + migrations.AlterField( + model_name='cluster', + name='name', + field=models.CharField(max_length=100), + ), + migrations.AlterUniqueTogether( + name='cluster', + unique_together={('site', 'name'), ('group', 'name')}, + ), + ] diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index d91a39549..11792944a 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -115,8 +115,7 @@ class Cluster(PrimaryModel): A cluster of VirtualMachines. Each Cluster may optionally be associated with one or more Devices. """ name = models.CharField( - max_length=100, - unique=True + max_length=100 ) type = models.ForeignKey( to=ClusterType, @@ -167,6 +166,10 @@ class Cluster(PrimaryModel): class Meta: ordering = ['name'] + unique_together = ( + ('group', 'name'), + ('site', 'name'), + ) def __str__(self): return self.name From 8d0ed99bcd9fba25c4469a789b1a80a2c1b22383 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 19 Oct 2021 13:32:43 -0400 Subject: [PATCH 20/35] Clean up UniqueTogetherValidator workarounds --- netbox/dcim/api/serializers.py | 44 +++++++--------------------------- netbox/ipam/api/serializers.py | 34 ++++---------------------- 2 files changed, 12 insertions(+), 66 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 14a1af8f0..22ea903bb 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -2,7 +2,6 @@ from django.conf import settings from django.contrib.contenttypes.models import ContentType from drf_yasg.utils import swagger_serializer_method from rest_framework import serializers -from rest_framework.validators import UniqueTogetherValidator from timezone_field.rest_framework import TimeZoneSerializerField from dcim.choices import * @@ -170,6 +169,8 @@ class RackSerializer(PrimaryModelSerializer): status = ChoiceField(choices=RackStatusChoices, required=False) role = NestedRackRoleSerializer(required=False, allow_null=True) type = ChoiceField(choices=RackTypeChoices, allow_blank=True, required=False) + facility_id = serializers.CharField(max_length=50, allow_blank=True, allow_null=True, label='Facility ID', + default=None) width = ChoiceField(choices=RackWidthChoices, required=False) outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False) device_count = serializers.IntegerField(read_only=True) @@ -182,23 +183,6 @@ class RackSerializer(PrimaryModelSerializer): 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count', ] - # Omit the UniqueTogetherValidator that would be automatically added to validate (location, facility_id). This - # prevents facility_id from being interpreted as a required field. - validators = [ - UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('location', 'name')) - ] - - def validate(self, data): - - # Validate uniqueness of (location, facility_id) since we omitted the automatically-created validator from Meta. - if data.get('facility_id', None): - validator = UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('location', 'facility_id')) - validator(data, self) - - # Enforce model validation - super().validate(data) - - return data class RackUnitSerializer(serializers.Serializer): @@ -458,12 +442,13 @@ class DeviceSerializer(PrimaryModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail') device_type = NestedDeviceTypeSerializer() device_role = NestedDeviceRoleSerializer() - tenant = NestedTenantSerializer(required=False, allow_null=True) + tenant = NestedTenantSerializer(required=False, allow_null=True, default=None) platform = NestedPlatformSerializer(required=False, allow_null=True) site = NestedSiteSerializer() location = NestedLocationSerializer(required=False, allow_null=True, default=None) - rack = NestedRackSerializer(required=False, allow_null=True) - face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, required=False) + rack = NestedRackSerializer(required=False, allow_null=True, default=None) + face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, default='') + position = serializers.IntegerField(allow_null=True, label='Position (U)', min_value=1, default=None) status = ChoiceField(choices=DeviceStatusChoices, required=False) airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False) primary_ip = NestedIPAddressSerializer(read_only=True) @@ -471,7 +456,8 @@ class DeviceSerializer(PrimaryModelSerializer): primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True) parent_device = serializers.SerializerMethodField() cluster = NestedClusterSerializer(required=False, allow_null=True) - virtual_chassis = NestedVirtualChassisSerializer(required=False, allow_null=True) + virtual_chassis = NestedVirtualChassisSerializer(required=False, allow_null=True, default=None) + vc_position = serializers.IntegerField(allow_null=True, max_value=255, min_value=0, default=None) class Meta: model = Device @@ -481,19 +467,6 @@ class DeviceSerializer(PrimaryModelSerializer): 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', ] - validators = [] - - def validate(self, data): - - # Validate uniqueness of (rack, position, face) since we omitted the automatically-created validator from Meta. - if data.get('rack') and data.get('position') and data.get('face'): - validator = UniqueTogetherValidator(queryset=Device.objects.all(), fields=('rack', 'position', 'face')) - validator(data, self) - - # Enforce model validation - super().validate(data) - - return data @swagger_serializer_method(serializer_or_field=NestedDeviceSerializer) def get_parent_device(self, obj): @@ -730,7 +703,6 @@ class DeviceBaySerializer(PrimaryModelSerializer): class InventoryItemSerializer(PrimaryModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail') device = NestedDeviceSerializer() - # Provide a default value to satisfy UniqueTogetherValidator parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None) manufacturer = NestedManufacturerSerializer(required=False, allow_null=True, default=None) _depth = serializers.IntegerField(source='level', read_only=True) diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 9f3139793..183c45b2a 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -3,7 +3,6 @@ from collections import OrderedDict from django.contrib.contenttypes.models import ContentType from drf_yasg.utils import swagger_serializer_method from rest_framework import serializers -from rest_framework.validators import UniqueTogetherValidator from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer from ipam.choices import * @@ -117,8 +116,10 @@ class VLANGroupSerializer(OrganizationalModelSerializer): queryset=ContentType.objects.filter( model__in=VLANGROUP_SCOPE_TYPES ), - required=False + required=False, + default=None ) + scope_id = serializers.IntegerField(allow_null=True, required=False, default=None) scope = serializers.SerializerMethodField(read_only=True) vlan_count = serializers.IntegerField(read_only=True) @@ -130,19 +131,6 @@ class VLANGroupSerializer(OrganizationalModelSerializer): ] validators = [] - def validate(self, data): - - # Validate uniqueness of name and slug if a site has been assigned. - if data.get('site', None): - for field in ['name', 'slug']: - validator = UniqueTogetherValidator(queryset=VLANGroup.objects.all(), fields=('site', field)) - validator(data, self) - - # Enforce model validation - super().validate(data) - - return data - def get_scope(self, obj): if obj.scope_id is None: return None @@ -155,7 +143,7 @@ class VLANGroupSerializer(OrganizationalModelSerializer): class VLANSerializer(PrimaryModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail') site = NestedSiteSerializer(required=False, allow_null=True) - group = NestedVLANGroupSerializer(required=False, allow_null=True) + group = NestedVLANGroupSerializer(required=False, allow_null=True, default=None) tenant = NestedTenantSerializer(required=False, allow_null=True) status = ChoiceField(choices=VLANStatusChoices, required=False) role = NestedRoleSerializer(required=False, allow_null=True) @@ -167,20 +155,6 @@ class VLANSerializer(PrimaryModelSerializer): 'id', 'url', 'display', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'prefix_count', ] - validators = [] - - def validate(self, data): - - # Validate uniqueness of vid and name if a group has been assigned. - if data.get('group', None): - for field in ['vid', 'name']: - validator = UniqueTogetherValidator(queryset=VLAN.objects.all(), fields=('group', field)) - validator(data, self) - - # Enforce model validation - super().validate(data) - - return data # From 7c56b21095689667cc25125f5888526803449471 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 19 Oct 2021 13:46:35 -0400 Subject: [PATCH 21/35] Closes #7354: Relax uniqueness constraints on region, site group, and location names --- docs/models/dcim/location.md | 3 +- docs/models/dcim/rack.md | 2 +- docs/models/dcim/region.md | 2 + docs/models/dcim/sitegroup.md | 2 + docs/release-notes/version-3.1.md | 1 + netbox/dcim/api/serializers.py | 4 +- .../0137_relax_uniqueness_constraints.py | 45 +++++++++++++++++++ netbox/dcim/models/sites.py | 32 ++++++++----- 8 files changed, 75 insertions(+), 16 deletions(-) create mode 100644 netbox/dcim/migrations/0137_relax_uniqueness_constraints.py diff --git a/docs/models/dcim/location.md b/docs/models/dcim/location.md index 16df208ac..901a68acf 100644 --- a/docs/models/dcim/location.md +++ b/docs/models/dcim/location.md @@ -2,4 +2,5 @@ Racks and devices can be grouped by location within a site. A location may represent a floor, room, cage, or similar organizational unit. Locations can be nested to form a hierarchy. For example, you may have floors within a site, and rooms within a floor. -The name and facility ID of each rack within a location must be unique. (Racks not assigned to the same location may have identical names and/or facility IDs.) +Each location must have a name that is unique within its parent site and location, if any. + diff --git a/docs/models/dcim/rack.md b/docs/models/dcim/rack.md index 90c9cfe6e..9465a828c 100644 --- a/docs/models/dcim/rack.md +++ b/docs/models/dcim/rack.md @@ -1,6 +1,6 @@ # Racks -The rack model represents a physical two- or four-post equipment rack in which devices can be installed. Each rack must be assigned to a site, and may optionally be assigned to a location and/or tenant. Racks can also be organized by user-defined functional roles. +The rack model represents a physical two- or four-post equipment rack in which devices can be installed. Each rack must be assigned to a site, and may optionally be assigned to a location and/or tenant. Racks can also be organized by user-defined functional roles. The name and facility ID of each rack within a location must be unique. Rack height is measured in *rack units* (U); racks are commonly between 42U and 48U tall, but NetBox allows you to define racks of arbitrary height. A toggle is provided to indicate whether rack units are in ascending (from the ground up) or descending order. diff --git a/docs/models/dcim/region.md b/docs/models/dcim/region.md index 734467500..bac186264 100644 --- a/docs/models/dcim/region.md +++ b/docs/models/dcim/region.md @@ -1,3 +1,5 @@ # Regions Sites can be arranged geographically using regions. A region might represent a continent, country, city, campus, or other area depending on your use case. Regions can be nested recursively to construct a hierarchy. For example, you might define several country regions, and within each of those several state or city regions to which sites are assigned. + +Each region must have a name that is unique within its parent region, if any. diff --git a/docs/models/dcim/sitegroup.md b/docs/models/dcim/sitegroup.md index 3c1ed11bd..04ebcc1a5 100644 --- a/docs/models/dcim/sitegroup.md +++ b/docs/models/dcim/sitegroup.md @@ -1,3 +1,5 @@ # Site Groups Like regions, site groups can be used to organize sites. Whereas regions are intended to provide geographic organization, site groups can be used to classify sites by role or function. Also like regions, site groups can be nested to form a hierarchy. Sites which belong to a child group are also considered to be members of any of its parent groups. + +Each site group must have a name that is unique within its parent group, if any. diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index 172fd3ed8..a94f749c7 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -15,6 +15,7 @@ * [#6711](https://github.com/netbox-community/netbox/issues/6711) - Add `longtext` custom field type with Markdown support * [#6715](https://github.com/netbox-community/netbox/issues/6715) - Add tenant assignment for cables * [#6874](https://github.com/netbox-community/netbox/issues/6874) - Add tenant assignment for locations +* [#7354](https://github.com/netbox-community/netbox/issues/7354) - Relax uniqueness constraints on region, site group, and location names ### Other Changes diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 22ea903bb..9b0e7f5b3 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -81,7 +81,7 @@ class ConnectedEndpointSerializer(serializers.ModelSerializer): class RegionSerializer(NestedGroupModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail') - parent = NestedRegionSerializer(required=False, allow_null=True) + parent = NestedRegionSerializer(required=False, allow_null=True, default=None) site_count = serializers.IntegerField(read_only=True) class Meta: @@ -94,7 +94,7 @@ class RegionSerializer(NestedGroupModelSerializer): class SiteGroupSerializer(NestedGroupModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail') - parent = NestedSiteGroupSerializer(required=False, allow_null=True) + parent = NestedSiteGroupSerializer(required=False, allow_null=True, default=None) site_count = serializers.IntegerField(read_only=True) class Meta: diff --git a/netbox/dcim/migrations/0137_relax_uniqueness_constraints.py b/netbox/dcim/migrations/0137_relax_uniqueness_constraints.py new file mode 100644 index 000000000..8f7d40026 --- /dev/null +++ b/netbox/dcim/migrations/0137_relax_uniqueness_constraints.py @@ -0,0 +1,45 @@ +# Generated by Django 3.2.8 on 2021-10-19 17:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0136_device_airflow'), + ] + + operations = [ + migrations.AlterField( + model_name='region', + name='name', + field=models.CharField(max_length=100), + ), + migrations.AlterField( + model_name='region', + name='slug', + field=models.SlugField(max_length=100), + ), + migrations.AlterField( + model_name='sitegroup', + name='name', + field=models.CharField(max_length=100), + ), + migrations.AlterField( + model_name='sitegroup', + name='slug', + field=models.SlugField(max_length=100), + ), + migrations.AlterUniqueTogether( + name='location', + unique_together={('site', 'parent', 'name'), ('site', 'parent', 'slug')}, + ), + migrations.AlterUniqueTogether( + name='region', + unique_together={('parent', 'slug'), ('parent', 'name')}, + ), + migrations.AlterUniqueTogether( + name='sitegroup', + unique_together={('parent', 'slug'), ('parent', 'name')}, + ), + ] diff --git a/netbox/dcim/models/sites.py b/netbox/dcim/models/sites.py index 0d9816b0b..ab9d8e82d 100644 --- a/netbox/dcim/models/sites.py +++ b/netbox/dcim/models/sites.py @@ -41,12 +41,10 @@ class Region(NestedGroupModel): db_index=True ) name = models.CharField( - max_length=100, - unique=True + max_length=100 ) slug = models.SlugField( - max_length=100, - unique=True + max_length=100 ) description = models.CharField( max_length=200, @@ -64,6 +62,12 @@ class Region(NestedGroupModel): to='tenancy.ContactAssignment' ) + class Meta: + unique_together = ( + ('parent', 'name'), + ('parent', 'slug'), + ) + def get_absolute_url(self): return reverse('dcim:region', args=[self.pk]) @@ -94,12 +98,10 @@ class SiteGroup(NestedGroupModel): db_index=True ) name = models.CharField( - max_length=100, - unique=True + max_length=100 ) slug = models.SlugField( - max_length=100, - unique=True + max_length=100 ) description = models.CharField( max_length=200, @@ -117,6 +119,12 @@ class SiteGroup(NestedGroupModel): to='tenancy.ContactAssignment' ) + class Meta: + unique_together = ( + ('parent', 'name'), + ('parent', 'slug'), + ) + def get_absolute_url(self): return reverse('dcim:sitegroup', args=[self.pk]) @@ -325,10 +333,10 @@ class Location(NestedGroupModel): class Meta: ordering = ['site', 'name'] - unique_together = [ - ['site', 'name'], - ['site', 'slug'], - ] + unique_together = ([ + ('site', 'parent', 'name'), + ('site', 'parent', 'slug'), + ]) def get_absolute_url(self): return reverse('dcim:location', args=[self.pk]) From 38bc5de3e8047363dc4342841d50d4e88b7b394d Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 19 Oct 2021 13:56:33 -0400 Subject: [PATCH 22/35] Changelog for #1344 --- docs/release-notes/version-3.1.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index a94f749c7..abf9c7d25 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -7,6 +7,14 @@ * The `tenant` and `tenant_id` filters for the Cable model now filter on the tenant assigned directly to each cable, rather than on the parent object of either termination. +#### Contacts ([#1344](https://github.com/netbox-community/netbox/issues/1344)) + +A set of new models for tracking contact information has been introduced within the tenancy app. Users may now create individual contact objects to be associated with various models within NetBox. Each contact has a name, title, email address, etc. Contacts can be arranged in hierarchical groups for ease of management. + +When assigning a contact to an object, the user must select a predefined role (e.g. "billing" or "technical") and may optionally indicate a priority relative to other contacts associated with the object. There is no limit on how many contacts can be assigned to an object, nor on how many objects to which a contact can be assigned. + +#### + ### Enhancements * [#1337](https://github.com/netbox-community/netbox/issues/1337) - Add WWN field to interfaces @@ -20,3 +28,21 @@ ### Other Changes * [#7318](https://github.com/netbox-community/netbox/issues/7318) - Raise minimum required PostgreSQL version from 9.6 to 10 + +### REST API Changes + +* Added the following endpoints for contacts: + * `/api/tenancy/contact-assignments/` + * `/api/tenancy/contact-groups/` + * `/api/tenancy/contact-roles/` + * `/api/tenancy/contacts/` +* dcim.Cable + * Added `tenant` field +* dcim.Device + * Added `airflow` field +* dcim.DeviceType + * Added `airflow` field +* dcim.Interface + * Added `wwn` field +* dcim.Location + * Added `tenant` field From f04dc5503014f12f77ba0cc3af00e6edc3ea7cbb Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 19 Oct 2021 14:21:31 -0400 Subject: [PATCH 23/35] Reorganize panel inclusion templates --- netbox/templates/circuits/circuit.html | 10 +-- netbox/templates/circuits/circuittype.html | 2 +- netbox/templates/circuits/provider.html | 8 +- .../templates/circuits/providernetwork.html | 6 +- netbox/templates/dcim/cable.html | 4 +- netbox/templates/dcim/consoleport.html | 4 +- netbox/templates/dcim/consoleserverport.html | 4 +- netbox/templates/dcim/device.html | 10 +-- netbox/templates/dcim/devicebay.html | 4 +- netbox/templates/dcim/devicerole.html | 2 +- netbox/templates/dcim/devicetype.html | 6 +- netbox/templates/dcim/frontport.html | 4 +- netbox/templates/dcim/interface.html | 4 +- netbox/templates/dcim/inventoryitem.html | 4 +- netbox/templates/dcim/location.html | 6 +- netbox/templates/dcim/manufacturer.html | 4 +- netbox/templates/dcim/platform.html | 2 +- netbox/templates/dcim/powerfeed.html | 6 +- netbox/templates/dcim/poweroutlet.html | 4 +- netbox/templates/dcim/powerpanel.html | 8 +- netbox/templates/dcim/powerport.html | 4 +- netbox/templates/dcim/rack.html | 10 +-- netbox/templates/dcim/rackreservation.html | 4 +- netbox/templates/dcim/rackrole.html | 2 +- netbox/templates/dcim/rearport.html | 4 +- netbox/templates/dcim/region.html | 4 +- netbox/templates/dcim/site.html | 10 +-- netbox/templates/dcim/sitegroup.html | 4 +- netbox/templates/dcim/virtualchassis.html | 4 +- netbox/templates/extras/journalentry.html | 2 +- .../comments.html} | 0 .../contacts.html} | 0 .../custom_fields.html} | 0 .../image_attachments.html} | 0 .../tags_panel.html => inc/panels/tags.html} | 0 netbox/templates/ipam/aggregate.html | 4 +- netbox/templates/ipam/ipaddress.html | 4 +- netbox/templates/ipam/iprange.html | 4 +- netbox/templates/ipam/prefix.html | 4 +- netbox/templates/ipam/rir.html | 2 +- netbox/templates/ipam/role.html | 2 +- netbox/templates/ipam/routetarget.html | 80 +++++++++---------- netbox/templates/ipam/service.html | 4 +- netbox/templates/ipam/vlan.html | 4 +- netbox/templates/ipam/vlangroup.html | 2 +- netbox/templates/ipam/vrf.html | 4 +- netbox/templates/tenancy/contact.html | 6 +- netbox/templates/tenancy/contactgroup.html | 2 +- netbox/templates/tenancy/contactrole.html | 2 +- netbox/templates/tenancy/tenant.html | 8 +- netbox/templates/tenancy/tenantgroup.html | 2 +- netbox/templates/virtualization/cluster.html | 8 +- .../virtualization/clustergroup.html | 4 +- .../templates/virtualization/clustertype.html | 2 +- .../virtualization/virtualmachine.html | 8 +- .../templates/virtualization/vminterface.html | 4 +- 56 files changed, 154 insertions(+), 156 deletions(-) rename netbox/templates/inc/{comments_panel.html => panels/comments.html} (100%) rename netbox/templates/inc/{contacts_panel.html => panels/contacts.html} (100%) rename netbox/templates/inc/{custom_fields_panel.html => panels/custom_fields.html} (100%) rename netbox/templates/inc/{image_attachments_panel.html => panels/image_attachments.html} (100%) rename netbox/templates/{extras/inc/tags_panel.html => inc/panels/tags.html} (100%) diff --git a/netbox/templates/circuits/circuit.html b/netbox/templates/circuits/circuit.html index 3a8096351..b61dac6fc 100644 --- a/netbox/templates/circuits/circuit.html +++ b/netbox/templates/circuits/circuit.html @@ -64,16 +64,16 @@
- {% include 'inc/custom_fields_panel.html' %} - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='circuits:circuit_list' %} - {% include 'inc/comments_panel.html' %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all url='circuits:circuit_list' %} + {% include 'inc/panels/comments.html' %} {% plugin_left_page object %}
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_a side='A' %} {% include 'circuits/inc/circuit_termination.html' with termination=object.termination_z side='Z' %} - {% include 'inc/contacts_panel.html' %} - {% include 'inc/image_attachments_panel.html' %} + {% include 'inc/panels/contacts.html' %} + {% include 'inc/panels/image_attachments.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/circuits/circuittype.html b/netbox/templates/circuits/circuittype.html index 899ba83c3..ad81de7e1 100644 --- a/netbox/templates/circuits/circuittype.html +++ b/netbox/templates/circuits/circuittype.html @@ -31,7 +31,7 @@ {% plugin_left_page object %}
- {% include 'inc/custom_fields_panel.html' %} + {% include 'inc/panels/custom_fields.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/circuits/provider.html b/netbox/templates/circuits/provider.html index af883e56f..d353e4f37 100644 --- a/netbox/templates/circuits/provider.html +++ b/netbox/templates/circuits/provider.html @@ -47,13 +47,13 @@ - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='circuits:provider_list' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all url='circuits:provider_list' %} {% plugin_left_page object %}
- {% include 'inc/custom_fields_panel.html' %} - {% include 'inc/comments_panel.html' %} - {% include 'inc/contacts_panel.html' %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/comments.html' %} + {% include 'inc/panels/contacts.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/circuits/providernetwork.html b/netbox/templates/circuits/providernetwork.html index a5eac1f78..18a11e115 100644 --- a/netbox/templates/circuits/providernetwork.html +++ b/netbox/templates/circuits/providernetwork.html @@ -37,9 +37,9 @@ {% plugin_left_page object %}
- {% include 'inc/custom_fields_panel.html' %} - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='circuits:providernetwork_list' %} - {% include 'inc/comments_panel.html' %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all url='circuits:providernetwork_list' %} + {% include 'inc/panels/comments.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/dcim/cable.html b/netbox/templates/dcim/cable.html index e9cde6e00..c5d1f7906 100644 --- a/netbox/templates/dcim/cable.html +++ b/netbox/templates/dcim/cable.html @@ -63,8 +63,8 @@ - {% include 'inc/custom_fields_panel.html' %} - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='dcim:cable_list' %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all url='dcim:cable_list' %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/consoleport.html b/netbox/templates/dcim/consoleport.html index ee8b56980..c340cbc5c 100644 --- a/netbox/templates/dcim/consoleport.html +++ b/netbox/templates/dcim/consoleport.html @@ -40,8 +40,8 @@
- {% include 'inc/custom_fields_panel.html' %} - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/consoleserverport.html b/netbox/templates/dcim/consoleserverport.html index 8eb84993c..91de60252 100644 --- a/netbox/templates/dcim/consoleserverport.html +++ b/netbox/templates/dcim/consoleserverport.html @@ -40,8 +40,8 @@
- {% include 'inc/custom_fields_panel.html' %} - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 9ae9df7d4..869ab1ec7 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -220,9 +220,9 @@
- {% include 'inc/custom_fields_panel.html' %} - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='dcim:device_list' %} - {% include 'inc/comments_panel.html' %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all url='dcim:device_list' %} + {% include 'inc/panels/comments.html' %} {% plugin_left_page object %}
@@ -296,8 +296,8 @@
{% endif %} - {% include 'inc/contacts_panel.html' %} - {% include 'inc/image_attachments_panel.html' %} + {% include 'inc/panels/contacts.html' %} + {% include 'inc/panels/image_attachments.html' %}
Related Devices diff --git a/netbox/templates/dcim/devicebay.html b/netbox/templates/dcim/devicebay.html index cc19413b1..918b6b022 100644 --- a/netbox/templates/dcim/devicebay.html +++ b/netbox/templates/dcim/devicebay.html @@ -32,8 +32,8 @@
- {% include 'inc/custom_fields_panel.html' %} - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/devicerole.html b/netbox/templates/dcim/devicerole.html index 382cbc4ee..2c2d7fe6f 100644 --- a/netbox/templates/dcim/devicerole.html +++ b/netbox/templates/dcim/devicerole.html @@ -61,7 +61,7 @@ {% plugin_left_page object %}
- {% include 'inc/custom_fields_panel.html' %} + {% include 'inc/panels/custom_fields.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/dcim/devicetype.html b/netbox/templates/dcim/devicetype.html index 40955f5d6..4239f9eb2 100644 --- a/netbox/templates/dcim/devicetype.html +++ b/netbox/templates/dcim/devicetype.html @@ -130,9 +130,9 @@ {% plugin_left_page object %}
- {% include 'inc/custom_fields_panel.html' %} - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='dcim:devicetype_list' %} - {% include 'inc/comments_panel.html' %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all url='dcim:devicetype_list' %} + {% include 'inc/panels/comments.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/dcim/frontport.html b/netbox/templates/dcim/frontport.html index 43ded0c6a..c6b6cea48 100644 --- a/netbox/templates/dcim/frontport.html +++ b/netbox/templates/dcim/frontport.html @@ -52,8 +52,8 @@ - {% include 'inc/custom_fields_panel.html' %} - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index f9a9b0425..0715bec58 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -102,8 +102,8 @@
- {% include 'inc/custom_fields_panel.html' %} - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/inventoryitem.html b/netbox/templates/dcim/inventoryitem.html index 545e8f1e4..e55d441d4 100644 --- a/netbox/templates/dcim/inventoryitem.html +++ b/netbox/templates/dcim/inventoryitem.html @@ -64,8 +64,8 @@
- {% include 'inc/custom_fields_panel.html' %} - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/location.html b/netbox/templates/dcim/location.html index 459880ca8..eeb891daf 100644 --- a/netbox/templates/dcim/location.html +++ b/netbox/templates/dcim/location.html @@ -71,9 +71,9 @@ {% plugin_left_page object %}
- {% include 'inc/custom_fields_panel.html' %} - {% include 'inc/contacts_panel.html' %} - {% include 'inc/image_attachments_panel.html' %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/contacts.html' %} + {% include 'inc/panels/image_attachments.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/dcim/manufacturer.html b/netbox/templates/dcim/manufacturer.html index 2a56b57cc..792a3e127 100644 --- a/netbox/templates/dcim/manufacturer.html +++ b/netbox/templates/dcim/manufacturer.html @@ -37,8 +37,8 @@ {% plugin_left_page object %}
- {% include 'inc/custom_fields_panel.html' %} - {% include 'inc/contacts_panel.html' %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/contacts.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/dcim/platform.html b/netbox/templates/dcim/platform.html index 7229d8078..bbdf809dd 100644 --- a/netbox/templates/dcim/platform.html +++ b/netbox/templates/dcim/platform.html @@ -66,7 +66,7 @@
{{ object.napalm_args }}
- {% include 'inc/custom_fields_panel.html' %} + {% include 'inc/panels/custom_fields.html' %} {% plugin_right_page object %} diff --git a/netbox/templates/dcim/powerfeed.html b/netbox/templates/dcim/powerfeed.html index b4fb06081..f29a127e3 100644 --- a/netbox/templates/dcim/powerfeed.html +++ b/netbox/templates/dcim/powerfeed.html @@ -107,8 +107,8 @@ - {% include 'inc/custom_fields_panel.html' %} - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='dcim:powerfeed_list' %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all url='dcim:powerfeed_list' %} {% plugin_left_page object %}
@@ -182,7 +182,7 @@
{% endif %} - {% include 'inc/comments_panel.html' %} + {% include 'inc/panels/comments.html' %} {% plugin_right_page object %} diff --git a/netbox/templates/dcim/poweroutlet.html b/netbox/templates/dcim/poweroutlet.html index f8973c79b..1f960e0d5 100644 --- a/netbox/templates/dcim/poweroutlet.html +++ b/netbox/templates/dcim/poweroutlet.html @@ -44,8 +44,8 @@ - {% include 'inc/custom_fields_panel.html' %} - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/powerpanel.html b/netbox/templates/dcim/powerpanel.html index 10975fa1b..a99aabf32 100644 --- a/netbox/templates/dcim/powerpanel.html +++ b/netbox/templates/dcim/powerpanel.html @@ -39,13 +39,13 @@
- {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='dcim:powerpanel_list' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all url='dcim:powerpanel_list' %} {% plugin_left_page object %}
- {% include 'inc/custom_fields_panel.html' %} - {% include 'inc/contacts_panel.html' %} - {% include 'inc/image_attachments_panel.html' %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/contacts.html' %} + {% include 'inc/panels/image_attachments.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/dcim/powerport.html b/netbox/templates/dcim/powerport.html index db367df1f..74ad9603b 100644 --- a/netbox/templates/dcim/powerport.html +++ b/netbox/templates/dcim/powerport.html @@ -44,8 +44,8 @@ - {% include 'inc/custom_fields_panel.html' %} - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index 0196a9a18..586d31771 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -162,9 +162,9 @@
- {% include 'inc/custom_fields_panel.html' %} - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='dcim:rack_list' %} - {% include 'inc/comments_panel.html' %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all url='dcim:rack_list' %} + {% include 'inc/panels/comments.html' %} {% if power_feeds %}
@@ -206,7 +206,7 @@
{% endif %} - {% include 'inc/image_attachments_panel.html' %} + {% include 'inc/panels/image_attachments.html' %}
Reservations @@ -332,7 +332,7 @@
{% endif %} - {% include 'inc/contacts_panel.html' %} + {% include 'inc/panels/contacts.html' %} {% plugin_right_page object %} diff --git a/netbox/templates/dcim/rackreservation.html b/netbox/templates/dcim/rackreservation.html index 9d1b4deea..07ca55f7c 100644 --- a/netbox/templates/dcim/rackreservation.html +++ b/netbox/templates/dcim/rackreservation.html @@ -83,8 +83,8 @@ - {% include 'inc/custom_fields_panel.html' %} - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='dcim:rackreservation_list' %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all url='dcim:rackreservation_list' %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/rackrole.html b/netbox/templates/dcim/rackrole.html index 703e7e3d2..2668905f4 100644 --- a/netbox/templates/dcim/rackrole.html +++ b/netbox/templates/dcim/rackrole.html @@ -37,7 +37,7 @@ {% plugin_left_page object %}
- {% include 'inc/custom_fields_panel.html' %} + {% include 'inc/panels/custom_fields.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/dcim/rearport.html b/netbox/templates/dcim/rearport.html index 1104bd988..b60e04882 100644 --- a/netbox/templates/dcim/rearport.html +++ b/netbox/templates/dcim/rearport.html @@ -46,8 +46,8 @@ - {% include 'inc/custom_fields_panel.html' %} - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/region.html b/netbox/templates/dcim/region.html index 1ee21a60e..c03b11e7d 100644 --- a/netbox/templates/dcim/region.html +++ b/netbox/templates/dcim/region.html @@ -45,8 +45,8 @@
- {% include 'inc/custom_fields_panel.html' %} - {% include 'inc/contacts_panel.html' %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/contacts.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html index 92023f8d6..8442ae41e 100644 --- a/netbox/templates/dcim/site.html +++ b/netbox/templates/dcim/site.html @@ -132,7 +132,7 @@
- {% include 'inc/contacts_panel.html' %} + {% include 'inc/panels/contacts.html' %}
Contact Info
@@ -180,9 +180,9 @@ {% endwith %}
- {% include 'inc/custom_fields_panel.html' %} - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='dcim:site_list' %} - {% include 'inc/comments_panel.html' %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all url='dcim:site_list' %} + {% include 'inc/panels/comments.html' %} {% plugin_left_page object %}
@@ -257,7 +257,7 @@ {% endif %}
- {% include 'inc/image_attachments_panel.html' %} + {% include 'inc/panels/image_attachments.html' %} {% plugin_right_page object %} diff --git a/netbox/templates/dcim/sitegroup.html b/netbox/templates/dcim/sitegroup.html index 610917078..dbee2c835 100644 --- a/netbox/templates/dcim/sitegroup.html +++ b/netbox/templates/dcim/sitegroup.html @@ -45,8 +45,8 @@ - {% include 'inc/custom_fields_panel.html' %} - {% include 'inc/contacts_panel.html' %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/contacts.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/virtualchassis.html b/netbox/templates/dcim/virtualchassis.html index 12088e892..fd31be60d 100644 --- a/netbox/templates/dcim/virtualchassis.html +++ b/netbox/templates/dcim/virtualchassis.html @@ -38,8 +38,8 @@
- {% include 'inc/custom_fields_panel.html' %} - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='dcim:virtualchassis_list' %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all url='dcim:virtualchassis_list' %} {% plugin_left_page object %}
diff --git a/netbox/templates/extras/journalentry.html b/netbox/templates/extras/journalentry.html index 925d98b41..2e7fcbbf5 100644 --- a/netbox/templates/extras/journalentry.html +++ b/netbox/templates/extras/journalentry.html @@ -45,7 +45,7 @@
- {% include 'inc/comments_panel.html' %} + {% include 'inc/panels/comments.html' %}
{% endblock %} diff --git a/netbox/templates/inc/comments_panel.html b/netbox/templates/inc/panels/comments.html similarity index 100% rename from netbox/templates/inc/comments_panel.html rename to netbox/templates/inc/panels/comments.html diff --git a/netbox/templates/inc/contacts_panel.html b/netbox/templates/inc/panels/contacts.html similarity index 100% rename from netbox/templates/inc/contacts_panel.html rename to netbox/templates/inc/panels/contacts.html diff --git a/netbox/templates/inc/custom_fields_panel.html b/netbox/templates/inc/panels/custom_fields.html similarity index 100% rename from netbox/templates/inc/custom_fields_panel.html rename to netbox/templates/inc/panels/custom_fields.html diff --git a/netbox/templates/inc/image_attachments_panel.html b/netbox/templates/inc/panels/image_attachments.html similarity index 100% rename from netbox/templates/inc/image_attachments_panel.html rename to netbox/templates/inc/panels/image_attachments.html diff --git a/netbox/templates/extras/inc/tags_panel.html b/netbox/templates/inc/panels/tags.html similarity index 100% rename from netbox/templates/extras/inc/tags_panel.html rename to netbox/templates/inc/panels/tags.html diff --git a/netbox/templates/ipam/aggregate.html b/netbox/templates/ipam/aggregate.html index c254d9d63..202b6e41c 100644 --- a/netbox/templates/ipam/aggregate.html +++ b/netbox/templates/ipam/aggregate.html @@ -64,8 +64,8 @@ {% plugin_left_page object %}
- {% include 'inc/custom_fields_panel.html' %} - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='ipam:aggregate_list' %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:aggregate_list' %} {% plugin_right_page object %}
diff --git a/netbox/templates/ipam/ipaddress.html b/netbox/templates/ipam/ipaddress.html index 668290458..d98544de4 100644 --- a/netbox/templates/ipam/ipaddress.html +++ b/netbox/templates/ipam/ipaddress.html @@ -107,7 +107,7 @@ - {% include 'inc/custom_fields_panel.html' %} + {% include 'inc/panels/custom_fields.html' %} {% plugin_left_page object %} @@ -145,7 +145,7 @@
- {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='ipam:ipaddress_list' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:ipaddress_list' %}
diff --git a/netbox/templates/ipam/iprange.html b/netbox/templates/ipam/iprange.html index 729f1ed42..e3d37a87a 100644 --- a/netbox/templates/ipam/iprange.html +++ b/netbox/templates/ipam/iprange.html @@ -82,8 +82,8 @@ {% plugin_left_page object %}
- {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='ipam:prefix_list' %} - {% include 'inc/custom_fields_panel.html' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:prefix_list' %} + {% include 'inc/panels/custom_fields.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/ipam/prefix.html b/netbox/templates/ipam/prefix.html index 4e3fd2edf..877ed49e0 100644 --- a/netbox/templates/ipam/prefix.html +++ b/netbox/templates/ipam/prefix.html @@ -121,8 +121,8 @@ {% plugin_left_page object %}
- {% include 'inc/custom_fields_panel.html' %} - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='ipam:prefix_list' %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:prefix_list' %} {% plugin_right_page object %}
diff --git a/netbox/templates/ipam/rir.html b/netbox/templates/ipam/rir.html index d9d13e110..26d5e71da 100644 --- a/netbox/templates/ipam/rir.html +++ b/netbox/templates/ipam/rir.html @@ -41,7 +41,7 @@ {% plugin_left_page object %}
- {% include 'inc/custom_fields_panel.html' %} + {% include 'inc/panels/custom_fields.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/ipam/role.html b/netbox/templates/ipam/role.html index 72a4767c9..7fc967047 100644 --- a/netbox/templates/ipam/role.html +++ b/netbox/templates/ipam/role.html @@ -35,7 +35,7 @@ {% plugin_left_page object %}
- {% include 'inc/custom_fields_panel.html' %} + {% include 'inc/panels/custom_fields.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/ipam/routetarget.html b/netbox/templates/ipam/routetarget.html index 94eec6a15..f615d2d50 100644 --- a/netbox/templates/ipam/routetarget.html +++ b/netbox/templates/ipam/routetarget.html @@ -3,50 +3,48 @@ {% load plugins %} {% block content %} -
-
-
-
- Route Target -
-
- - - - - - - - - - - - - -
Name{{ object.name }}
Tenant - {% if object.tenant %} - {{ object.tenant }} - {% else %} - None - {% endif %} -
Description{{ object.description|placeholder }}
-
+
+
+
+
Route Target
+
+ + + + + + + + + + + + + +
Name{{ object.name }}
Tenant + {% if object.tenant %} + {{ object.tenant }} + {% else %} + None + {% endif %} +
Description{{ object.description|placeholder }}
- {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='ipam:routetarget_list' %} - {% include 'inc/custom_fields_panel.html' %} - {% plugin_left_page object %} -
-
-
+
+ {% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:routetarget_list' %} + {% include 'inc/panels/custom_fields.html' %} + {% plugin_left_page object %} +
+
+
{% include 'inc/panel_table.html' with table=importing_vrfs_table heading="Importing VRFs" %} -
- {% include 'inc/panel_table.html' with table=exporting_vrfs_table heading="Exporting VRFs" %} - {% plugin_right_page object %} +
+ {% include 'inc/panel_table.html' with table=exporting_vrfs_table heading="Exporting VRFs" %} + {% plugin_right_page object %}
-
-
+
+
- {% plugin_full_width_page object %} + {% plugin_full_width_page object %}
-
+
{% endblock %} diff --git a/netbox/templates/ipam/service.html b/netbox/templates/ipam/service.html index 6083d1b5e..7609a280b 100644 --- a/netbox/templates/ipam/service.html +++ b/netbox/templates/ipam/service.html @@ -60,8 +60,8 @@ {% plugin_left_page object %}
- {% include 'inc/custom_fields_panel.html' %} - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='ipam:service_list' %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:service_list' %} {% plugin_right_page object %}
diff --git a/netbox/templates/ipam/vlan.html b/netbox/templates/ipam/vlan.html index 5ecd6efed..e8c514cca 100644 --- a/netbox/templates/ipam/vlan.html +++ b/netbox/templates/ipam/vlan.html @@ -82,8 +82,8 @@ {% plugin_left_page object %}
- {% include 'inc/custom_fields_panel.html' %} - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='ipam:vlan_list' %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:vlan_list' %} {% plugin_right_page object %}
diff --git a/netbox/templates/ipam/vlangroup.html b/netbox/templates/ipam/vlangroup.html index a46bef3b0..2d31feb22 100644 --- a/netbox/templates/ipam/vlangroup.html +++ b/netbox/templates/ipam/vlangroup.html @@ -57,7 +57,7 @@ {% plugin_left_page object %}
- {% include 'inc/custom_fields_panel.html' %} + {% include 'inc/panels/custom_fields.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/ipam/vrf.html b/netbox/templates/ipam/vrf.html index 863753c0d..b320fe6b8 100644 --- a/netbox/templates/ipam/vrf.html +++ b/netbox/templates/ipam/vrf.html @@ -60,8 +60,8 @@ {% plugin_left_page object %}
- {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='ipam:vrf_list' %} - {% include 'inc/custom_fields_panel.html' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:vrf_list' %} + {% include 'inc/panels/custom_fields.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/tenancy/contact.html b/netbox/templates/tenancy/contact.html index ca46fdb31..8bdf6c030 100644 --- a/netbox/templates/tenancy/contact.html +++ b/netbox/templates/tenancy/contact.html @@ -55,12 +55,12 @@ - {% include 'inc/comments_panel.html' %} + {% include 'inc/panels/comments.html' %} {% plugin_left_page object %}
- {% include 'inc/custom_fields_panel.html' %} - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='tenancy:tenant_list' %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all url='tenancy:tenant_list' %} {% plugin_right_page object %}
diff --git a/netbox/templates/tenancy/contactgroup.html b/netbox/templates/tenancy/contactgroup.html index 1511565c3..0eef750eb 100644 --- a/netbox/templates/tenancy/contactgroup.html +++ b/netbox/templates/tenancy/contactgroup.html @@ -48,7 +48,7 @@ {% plugin_left_page object %}
- {% include 'inc/custom_fields_panel.html' %} + {% include 'inc/panels/custom_fields.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/tenancy/contactrole.html b/netbox/templates/tenancy/contactrole.html index f081afc34..4ddde3624 100644 --- a/netbox/templates/tenancy/contactrole.html +++ b/netbox/templates/tenancy/contactrole.html @@ -33,7 +33,7 @@ {% plugin_left_page object %}
- {% include 'inc/custom_fields_panel.html' %} + {% include 'inc/panels/custom_fields.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/tenancy/tenant.html b/netbox/templates/tenancy/tenant.html index 54b29e946..dc51b48c5 100644 --- a/netbox/templates/tenancy/tenant.html +++ b/netbox/templates/tenancy/tenant.html @@ -35,10 +35,10 @@ - {% include 'inc/custom_fields_panel.html' %} - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='tenancy:tenant_list' %} - {% include 'inc/comments_panel.html' %} - {% include 'inc/contacts_panel.html' %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all url='tenancy:tenant_list' %} + {% include 'inc/panels/comments.html' %} + {% include 'inc/panels/contacts.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/tenancy/tenantgroup.html b/netbox/templates/tenancy/tenantgroup.html index 06fd07522..31a756d9e 100644 --- a/netbox/templates/tenancy/tenantgroup.html +++ b/netbox/templates/tenancy/tenantgroup.html @@ -48,7 +48,7 @@ {% plugin_left_page object %}
- {% include 'inc/custom_fields_panel.html' %} + {% include 'inc/panels/custom_fields.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/virtualization/cluster.html b/netbox/templates/virtualization/cluster.html index fa8cad039..84b8235ad 100644 --- a/netbox/templates/virtualization/cluster.html +++ b/netbox/templates/virtualization/cluster.html @@ -56,13 +56,13 @@ - {% include 'inc/comments_panel.html' %} + {% include 'inc/panels/comments.html' %} {% plugin_left_page object %}
- {% include 'inc/custom_fields_panel.html' %} - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='virtualization:cluster_list' %} - {% include 'inc/contacts_panel.html' %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all url='virtualization:cluster_list' %} + {% include 'inc/panels/contacts.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/virtualization/clustergroup.html b/netbox/templates/virtualization/clustergroup.html index fd83c10f3..b367d97f7 100644 --- a/netbox/templates/virtualization/clustergroup.html +++ b/netbox/templates/virtualization/clustergroup.html @@ -31,8 +31,8 @@ {% plugin_left_page object %}
- {% include 'inc/custom_fields_panel.html' %} - {% include 'inc/contacts_panel.html' %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/contacts.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/virtualization/clustertype.html b/netbox/templates/virtualization/clustertype.html index 9ef1abb8e..e3c050a1b 100644 --- a/netbox/templates/virtualization/clustertype.html +++ b/netbox/templates/virtualization/clustertype.html @@ -31,7 +31,7 @@ {% plugin_left_page object %}
- {% include 'inc/custom_fields_panel.html' %} + {% include 'inc/panels/custom_fields.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index 0ef590112..0d9ea4a22 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -89,9 +89,9 @@ - {% include 'inc/custom_fields_panel.html' %} - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='virtualization:virtualmachine_list' %} - {% include 'inc/comments_panel.html' %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all url='virtualization:virtualmachine_list' %} + {% include 'inc/panels/comments.html' %} {% plugin_left_page object %}
@@ -173,7 +173,7 @@
{% endif %} - {% include 'inc/contacts_panel.html' %} + {% include 'inc/panels/contacts.html' %} {% plugin_right_page object %} diff --git a/netbox/templates/virtualization/vminterface.html b/netbox/templates/virtualization/vminterface.html index 6a618a1be..ef12b63a1 100644 --- a/netbox/templates/virtualization/vminterface.html +++ b/netbox/templates/virtualization/vminterface.html @@ -69,8 +69,8 @@ {% plugin_left_page object %}
- {% include 'inc/custom_fields_panel.html' %} - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all %} + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all %} {% plugin_right_page object %}
From c1720505f36eeb0f247da32549a68fcd61e8ef96 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 19 Oct 2021 15:22:22 -0400 Subject: [PATCH 24/35] Fixes #7584: Fix alignment of object identifier under object view --- docs/release-notes/version-3.0.md | 3 ++- netbox/templates/generic/object.html | 21 ++++++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index 8b0e99b4f..77da55606 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -10,7 +10,8 @@ * [#7534](https://github.com/netbox-community/netbox/issues/7534) - Avoid exception when utilizing "create and add another" twice in succession * [#7544](https://github.com/netbox-community/netbox/issues/7544) - Fix multi-value filtering of custom field objects * [#7545](https://github.com/netbox-community/netbox/issues/7545) - Fix incorrect display of update/delete events for webhooks -* [#7556](https://github.com/netbox-community/netbox/issues/7556) - Fix display of New Version +* [#7556](https://github.com/netbox-community/netbox/issues/7556) - Fix display of version when new release is available +* [#7584](https://github.com/netbox-community/netbox/issues/7584) - Fix alignment of object identifier under object view --- diff --git a/netbox/templates/generic/object.html b/netbox/templates/generic/object.html index 24285846f..40c0e09ce 100644 --- a/netbox/templates/generic/object.html +++ b/netbox/templates/generic/object.html @@ -6,9 +6,17 @@ {% load plugins %} {% block header %} - {# Breadcrumbs #} - + {{ block.super }} {% endblock %} From 96015aa59031bbbb96b1ef634c684c52d0ac2e83 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 19 Oct 2021 15:31:07 -0400 Subject: [PATCH 25/35] Fixes #7582: Fix rendering of CustomLink context data table --- docs/customization/custom-links.md | 1 - mkdocs.yml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 docs/customization/custom-links.md diff --git a/docs/customization/custom-links.md b/docs/customization/custom-links.md deleted file mode 100644 index 1ee366cfd..000000000 --- a/docs/customization/custom-links.md +++ /dev/null @@ -1 +0,0 @@ -{!models/extras/customlink.md!} diff --git a/mkdocs.yml b/mkdocs.yml index 7244c36d6..d12ef734f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -65,7 +65,7 @@ nav: - Customization: - Custom Fields: 'customization/custom-fields.md' - Custom Validation: 'customization/custom-validation.md' - - Custom Links: 'customization/custom-links.md' + - Custom Links: 'models/extras/customlink.md' - Export Templates: 'customization/export-templates.md' - Custom Scripts: 'customization/custom-scripts.md' - Reports: 'customization/reports.md' From 39430e01de814ef21ea7dc71f6f84cc3776e771f Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 19 Oct 2021 15:41:19 -0400 Subject: [PATCH 26/35] Fixes #7550: Fix rendering of UTF8-encoded data in change records --- docs/release-notes/version-3.0.md | 1 + netbox/templates/extras/objectchange.html | 10 +++++----- netbox/utilities/templatetags/helpers.py | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index 77da55606..9497f1c7a 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -10,6 +10,7 @@ * [#7534](https://github.com/netbox-community/netbox/issues/7534) - Avoid exception when utilizing "create and add another" twice in succession * [#7544](https://github.com/netbox-community/netbox/issues/7544) - Fix multi-value filtering of custom field objects * [#7545](https://github.com/netbox-community/netbox/issues/7545) - Fix incorrect display of update/delete events for webhooks +* [#7550](https://github.com/netbox-community/netbox/issues/7550) - Fix rendering of UTF8-encoded data in change records * [#7556](https://github.com/netbox-community/netbox/issues/7556) - Fix display of version when new release is available * [#7584](https://github.com/netbox-community/netbox/issues/7584) - Fix alignment of object identifier under object view diff --git a/netbox/templates/extras/objectchange.html b/netbox/templates/extras/objectchange.html index b7bc12446..e8d72810e 100644 --- a/netbox/templates/extras/objectchange.html +++ b/netbox/templates/extras/objectchange.html @@ -130,12 +130,12 @@
{% if object.postchange_data %} -
{% for k, v in object.postchange_data.items %}{% spaceless %}
-                    {{ k }}: {{ v|render_json }}
-                    {% endspaceless %}{% endfor %}
-                
+
{% for k, v in object.postchange_data.items %}{% spaceless %}
+                        {{ k }}: {{ v|render_json }}
+                        {% endspaceless %}{% endfor %}
+                    
{% else %} - None + None {% endif %}
diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index a900d59e2..1695c8257 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -58,7 +58,7 @@ def render_json(value): """ Render a dictionary as formatted JSON. """ - return json.dumps(value, indent=4, sort_keys=True) + return json.dumps(value, ensure_ascii=False, indent=4, sort_keys=True) @register.filter() From eb4b4a6c8d238530e51df7455a179e404404ab90 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 19 Oct 2021 15:51:39 -0400 Subject: [PATCH 27/35] Closes #7561: Add a utilization column to the IP ranges table --- docs/release-notes/version-3.0.md | 4 ++++ netbox/ipam/tables/ip.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index 9497f1c7a..f27a23f2c 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -2,6 +2,10 @@ ## v3.0.8 (FUTURE) +### Enhancements + +* [#7561](https://github.com/netbox-community/netbox/issues/7561) - Add a utilization column to the IP ranges table + ### Bug Fixes * [#7300](https://github.com/netbox-community/netbox/issues/7300) - Fix incorrect Device LLDP interface row coloring diff --git a/netbox/ipam/tables/ip.py b/netbox/ipam/tables/ip.py index 485e4a123..ddad6c573 100644 --- a/netbox/ipam/tables/ip.py +++ b/netbox/ipam/tables/ip.py @@ -260,11 +260,16 @@ class IPRangeTable(BaseTable): linkify=True ) tenant = TenantColumn() + utilization = UtilizationColumn( + accessor='utilization', + orderable=False + ) class Meta(BaseTable.Meta): model = IPRange fields = ( 'pk', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description', + 'utilization', ) default_columns = ( 'pk', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description', From 73f2f9fc6375e7a6d97d5a776dcb7775eec9006e Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 19 Oct 2021 15:57:02 -0400 Subject: [PATCH 28/35] Closes #7551: Add UI field to filter interfaces by kind --- docs/release-notes/version-3.0.md | 1 + netbox/dcim/choices.py | 12 ++++++++++++ netbox/dcim/forms/filtersets.py | 7 ++++++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index f27a23f2c..d50c511b5 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -4,6 +4,7 @@ ### Enhancements +* [#7551](https://github.com/netbox-community/netbox/issues/7551) - Add UI field to filter interfaces by kind * [#7561](https://github.com/netbox-community/netbox/issues/7561) - Add a utilization column to the IP ranges table ### Bug Fixes diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index acea294f8..2f6228751 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -685,6 +685,18 @@ class PowerOutletFeedLegChoices(ChoiceSet): # Interfaces # +class InterfaceKindChoices(ChoiceSet): + KIND_PHYSICAL = 'physical' + KIND_VIRTUAL = 'virtual' + KIND_WIRELESS = 'wireless' + + CHOICES = ( + (KIND_PHYSICAL, 'Physical'), + (KIND_VIRTUAL, 'Virtual'), + (KIND_WIRELESS, 'Wireless'), + ) + + class InterfaceTypeChoices(ChoiceSet): # Virtual diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 95ff9aa3d..4ef53c469 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -957,9 +957,14 @@ class InterfaceFilterForm(DeviceComponentFilterForm): model = Interface field_groups = [ ['q', 'tag'], - ['name', 'label', 'type', 'enabled', 'mgmt_only', 'mac_address'], + ['name', 'label', 'kind', 'type', 'enabled', 'mgmt_only', 'mac_address'], ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], ] + kind = forms.MultipleChoiceField( + choices=InterfaceKindChoices, + required=False, + widget=StaticSelectMultiple() + ) type = forms.MultipleChoiceField( choices=InterfaceTypeChoices, required=False, From fc5a23cc88f04dcfea756b2a761df70a89487692 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 20 Oct 2021 09:31:12 -0400 Subject: [PATCH 29/35] Release v3.0.8 --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yaml | 2 +- docs/release-notes/version-3.0.md | 2 +- netbox/netbox/settings.py | 2 +- requirements.txt | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 2a1ecd5d0..318a9b7ad 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -17,7 +17,7 @@ body: What version of NetBox are you currently running? (If you don't have access to the most recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/) before opening a bug report to see if your issue has already been addressed.) - placeholder: v3.0.7 + placeholder: v3.0.8 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 6a3f81e1e..be89acfad 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.0.7 + placeholder: v3.0.8 validations: required: true - type: dropdown diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index d50c511b5..83299dc86 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -1,6 +1,6 @@ # NetBox v3.0 -## v3.0.8 (FUTURE) +## v3.0.8 (2021-10-20) ### Enhancements diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 3df9a855a..515ca5709 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -16,7 +16,7 @@ from django.core.validators import URLValidator # Environment setup # -VERSION = '3.0.8-dev' +VERSION = '3.0.8' # Hostname HOSTNAME = platform.node() diff --git a/requirements.txt b/requirements.txt index 8aa3b8a5c..7cad262b7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,11 +18,11 @@ gunicorn==20.1.0 Jinja2==3.0.2 Markdown==3.3.4 markdown-include==0.6.0 -mkdocs-material==7.3.2 +mkdocs-material==7.3.4 netaddr==0.8.0 -Pillow==8.3.2 +Pillow==8.4.0 psycopg2-binary==2.9.1 -PyYAML==5.4.1 +PyYAML==6.0 svgwrite==1.4.1 tablib==3.0.0 From 090df051934c88a0ed118694e082afb540b5bb4e Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 20 Oct 2021 09:59:33 -0400 Subject: [PATCH 30/35] PRVB --- docs/release-notes/version-3.0.md | 4 ++++ netbox/netbox/settings.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index 83299dc86..69d8b8456 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -1,5 +1,9 @@ # NetBox v3.0 +## v3.0.9 (FUTURE) + +--- + ## v3.0.8 (2021-10-20) ### Enhancements diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 515ca5709..35e0c6714 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -16,7 +16,7 @@ from django.core.validators import URLValidator # Environment setup # -VERSION = '3.0.8' +VERSION = '3.0.9-dev' # Hostname HOSTNAME = platform.node() From 7b70129974213a03aa53e8a9219007677a99d9b3 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 20 Oct 2021 14:22:11 -0400 Subject: [PATCH 31/35] Refactor device component views --- netbox/dcim/views.py | 224 ++++++++++--------------------------------- 1 file changed, 53 insertions(+), 171 deletions(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 16f88b9c3..9b48e0bd3 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -36,6 +36,29 @@ from .models import ( ) +class DeviceComponentsView(generic.ObjectView): + queryset = Device.objects.all() + model = None + table = None + + def get_components(self, request, instance): + return self.model.objects.restrict(request.user, 'view').filter(device=instance) + + def get_extra_context(self, request, instance): + components = self.get_components(request, instance) + table = self.table(data=components, user=request.user) + change_perm = f'{self.model._meta.app_label}.change_{self.model._meta.model_name}' + delete_perm = f'{self.model._meta.app_label}.delete_{self.model._meta.model_name}' + if request.user.has_perm(change_perm) or request.user.has_perm(delete_perm): + table.columns.show('pk') + paginate_table(table, request) + + return { + f'{self.model._meta.model_name}_table': table, + 'active_tab': f"{self.model._meta.verbose_name_plural.replace(' ', '-')}", + } + + class BulkDisconnectView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): """ An extendable view for disconnection console/power/interface components in bulk. @@ -1306,206 +1329,65 @@ class DeviceView(generic.ObjectView): } -class DeviceConsolePortsView(generic.ObjectView): - queryset = Device.objects.all() +class DeviceConsolePortsView(DeviceComponentsView): + model = ConsolePort + table = tables.DeviceConsolePortTable template_name = 'dcim/device/consoleports.html' - def get_extra_context(self, request, instance): - consoleports = ConsolePort.objects.restrict(request.user, 'view').filter(device=instance).prefetch_related( - 'cable', '_path__destination', - ) - consoleport_table = tables.DeviceConsolePortTable( - data=consoleports, - user=request.user - ) - if request.user.has_perm('dcim.change_consoleport') or request.user.has_perm('dcim.delete_consoleport'): - consoleport_table.columns.show('pk') - paginate_table(consoleport_table, request) - return { - 'consoleport_table': consoleport_table, - 'active_tab': 'console-ports', - } - - -class DeviceConsoleServerPortsView(generic.ObjectView): - queryset = Device.objects.all() +class DeviceConsoleServerPortsView(DeviceComponentsView): + model = ConsoleServerPort + table = tables.DeviceConsoleServerPortTable template_name = 'dcim/device/consoleserverports.html' - def get_extra_context(self, request, instance): - consoleserverports = ConsoleServerPort.objects.restrict(request.user, 'view').filter( - device=instance - ).prefetch_related( - 'cable', '_path__destination', - ) - consoleserverport_table = tables.DeviceConsoleServerPortTable( - data=consoleserverports, - user=request.user - ) - if request.user.has_perm('dcim.change_consoleserverport') or \ - request.user.has_perm('dcim.delete_consoleserverport'): - consoleserverport_table.columns.show('pk') - paginate_table(consoleserverport_table, request) - return { - 'consoleserverport_table': consoleserverport_table, - 'active_tab': 'console-server-ports', - } - - -class DevicePowerPortsView(generic.ObjectView): - queryset = Device.objects.all() +class DevicePowerPortsView(DeviceComponentsView): + model = PowerPort + table = tables.DevicePowerPortTable template_name = 'dcim/device/powerports.html' - def get_extra_context(self, request, instance): - powerports = PowerPort.objects.restrict(request.user, 'view').filter(device=instance).prefetch_related( - 'cable', '_path__destination', - ) - powerport_table = tables.DevicePowerPortTable( - data=powerports, - user=request.user - ) - if request.user.has_perm('dcim.change_powerport') or request.user.has_perm('dcim.delete_powerport'): - powerport_table.columns.show('pk') - paginate_table(powerport_table, request) - return { - 'powerport_table': powerport_table, - 'active_tab': 'power-ports', - } - - -class DevicePowerOutletsView(generic.ObjectView): - queryset = Device.objects.all() +class DevicePowerOutletsView(DeviceComponentsView): + model = PowerOutlet + table = tables.DevicePowerOutletTable template_name = 'dcim/device/poweroutlets.html' - def get_extra_context(self, request, instance): - poweroutlets = PowerOutlet.objects.restrict(request.user, 'view').filter(device=instance).prefetch_related( - 'cable', 'power_port', '_path__destination', - ) - poweroutlet_table = tables.DevicePowerOutletTable( - data=poweroutlets, - user=request.user - ) - if request.user.has_perm('dcim.change_poweroutlet') or request.user.has_perm('dcim.delete_poweroutlet'): - poweroutlet_table.columns.show('pk') - paginate_table(poweroutlet_table, request) - return { - 'poweroutlet_table': poweroutlet_table, - 'active_tab': 'power-outlets', - } - - -class DeviceInterfacesView(generic.ObjectView): - queryset = Device.objects.all() +class DeviceInterfacesView(DeviceComponentsView): + model = Interface + table = tables.DeviceInterfaceTable template_name = 'dcim/device/interfaces.html' - def get_extra_context(self, request, instance): - interfaces = instance.vc_interfaces().restrict(request.user, 'view').prefetch_related( + def get_components(self, request, instance): + return instance.vc_interfaces().restrict(request.user, 'view').prefetch_related( Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)), - Prefetch('member_interfaces', queryset=Interface.objects.restrict(request.user)), - 'lag', 'cable', '_path__destination', 'tags', + Prefetch('member_interfaces', queryset=Interface.objects.restrict(request.user)) ) - interface_table = tables.DeviceInterfaceTable( - data=interfaces, - user=request.user - ) - if request.user.has_perm('dcim.change_interface') or request.user.has_perm('dcim.delete_interface'): - interface_table.columns.show('pk') - paginate_table(interface_table, request) - - return { - 'interface_table': interface_table, - 'active_tab': 'interfaces', - } -class DeviceFrontPortsView(generic.ObjectView): - queryset = Device.objects.all() +class DeviceFrontPortsView(DeviceComponentsView): + model = FrontPort + table = tables.DeviceFrontPortTable template_name = 'dcim/device/frontports.html' - def get_extra_context(self, request, instance): - frontports = FrontPort.objects.restrict(request.user, 'view').filter(device=instance).prefetch_related( - 'rear_port', 'cable', - ) - frontport_table = tables.DeviceFrontPortTable( - data=frontports, - user=request.user - ) - if request.user.has_perm('dcim.change_frontport') or request.user.has_perm('dcim.delete_frontport'): - frontport_table.columns.show('pk') - paginate_table(frontport_table, request) - return { - 'frontport_table': frontport_table, - 'active_tab': 'front-ports', - } - - -class DeviceRearPortsView(generic.ObjectView): - queryset = Device.objects.all() +class DeviceRearPortsView(DeviceComponentsView): + model = RearPort + table = tables.DeviceRearPortTable template_name = 'dcim/device/rearports.html' - def get_extra_context(self, request, instance): - rearports = RearPort.objects.restrict(request.user, 'view').filter(device=instance).prefetch_related('cable') - rearport_table = tables.DeviceRearPortTable( - data=rearports, - user=request.user - ) - if request.user.has_perm('dcim.change_rearport') or request.user.has_perm('dcim.delete_rearport'): - rearport_table.columns.show('pk') - paginate_table(rearport_table, request) - return { - 'rearport_table': rearport_table, - 'active_tab': 'rear-ports', - } - - -class DeviceDeviceBaysView(generic.ObjectView): - queryset = Device.objects.all() +class DeviceDeviceBaysView(DeviceComponentsView): + model = DeviceBay + table = tables.DeviceDeviceBayTable template_name = 'dcim/device/devicebays.html' - def get_extra_context(self, request, instance): - devicebays = DeviceBay.objects.restrict(request.user, 'view').filter(device=instance).prefetch_related( - 'installed_device__device_type__manufacturer', - ) - devicebay_table = tables.DeviceDeviceBayTable( - data=devicebays, - user=request.user - ) - if request.user.has_perm('dcim.change_devicebay') or request.user.has_perm('dcim.delete_devicebay'): - devicebay_table.columns.show('pk') - paginate_table(devicebay_table, request) - return { - 'devicebay_table': devicebay_table, - 'active_tab': 'device-bays', - } - - -class DeviceInventoryView(generic.ObjectView): - queryset = Device.objects.all() +class DeviceInventoryView(DeviceComponentsView): + model = InventoryItem + table = tables.DeviceInventoryItemTable template_name = 'dcim/device/inventory.html' - def get_extra_context(self, request, instance): - inventoryitems = InventoryItem.objects.restrict(request.user, 'view').filter( - device=instance - ).prefetch_related('manufacturer') - inventoryitem_table = tables.DeviceInventoryItemTable( - data=inventoryitems, - user=request.user - ) - if request.user.has_perm('dcim.change_inventoryitem') or request.user.has_perm('dcim.delete_inventoryitem'): - inventoryitem_table.columns.show('pk') - paginate_table(inventoryitem_table, request) - - return { - 'inventoryitem_table': inventoryitem_table, - 'active_tab': 'inventory', - } - class DeviceStatusView(generic.ObjectView): additional_permissions = ['dcim.napalm_read_device'] From 8c058dcd45004362ef69b2f3b0393ed408d4dbf2 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 20 Oct 2021 15:04:40 -0400 Subject: [PATCH 32/35] Closes #7530: Move device type component lists to separate views --- docs/release-notes/version-3.1.md | 1 + netbox/dcim/tests/test_views.py | 110 ++++++++++++++++ netbox/dcim/urls.py | 8 ++ netbox/dcim/views.py | 102 ++++++++------- .../templates/dcim/device/consoleports.html | 6 +- .../dcim/device/consoleserverports.html | 6 +- netbox/templates/dcim/device/devicebays.html | 6 +- netbox/templates/dcim/device/frontports.html | 6 +- netbox/templates/dcim/device/interfaces.html | 6 +- netbox/templates/dcim/device/inventory.html | 6 +- .../templates/dcim/device/poweroutlets.html | 6 +- netbox/templates/dcim/device/powerports.html | 6 +- netbox/templates/dcim/device/rearports.html | 6 +- netbox/templates/dcim/devicetype.html | 117 +---------------- netbox/templates/dcim/devicetype/base.html | 119 ++++++++++++++++++ .../component_templates.html} | 11 +- .../dcim/inc/device_component_table.html | 42 ------- 17 files changed, 323 insertions(+), 241 deletions(-) create mode 100644 netbox/templates/dcim/devicetype/base.html rename netbox/templates/dcim/{inc/devicetype_component_table.html => devicetype/component_templates.html} (93%) delete mode 100644 netbox/templates/dcim/inc/device_component_table.html diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index abf9c7d25..291831500 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -24,6 +24,7 @@ When assigning a contact to an object, the user must select a predefined role (e * [#6715](https://github.com/netbox-community/netbox/issues/6715) - Add tenant assignment for cables * [#6874](https://github.com/netbox-community/netbox/issues/6874) - Add tenant assignment for locations * [#7354](https://github.com/netbox-community/netbox/issues/7354) - Relax uniqueness constraints on region, site group, and location names +* [#7530](https://github.com/netbox-community/netbox/issues/7530) - Move device type component lists to separate views ### Other Changes diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 545a56f81..a9c191679 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -435,6 +435,116 @@ class DeviceTypeTestCase( 'is_full_depth': False, } + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) + def test_devicetype_consoleports(self): + devicetype = DeviceType.objects.first() + console_ports = ( + ConsolePortTemplate(device_type=devicetype, name='Console Port 1'), + ConsolePortTemplate(device_type=devicetype, name='Console Port 2'), + ConsolePortTemplate(device_type=devicetype, name='Console Port 3'), + ) + ConsolePortTemplate.objects.bulk_create(console_ports) + + url = reverse('dcim:devicetype_consoleports', kwargs={'pk': devicetype.pk}) + self.assertHttpStatus(self.client.get(url), 200) + + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) + def test_devicetype_consoleserverports(self): + devicetype = DeviceType.objects.first() + console_server_ports = ( + ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port 1'), + ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port 2'), + ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port 3'), + ) + ConsoleServerPortTemplate.objects.bulk_create(console_server_ports) + + url = reverse('dcim:devicetype_consoleserverports', kwargs={'pk': devicetype.pk}) + self.assertHttpStatus(self.client.get(url), 200) + + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) + def test_devicetype_powerports(self): + devicetype = DeviceType.objects.first() + power_ports = ( + PowerPortTemplate(device_type=devicetype, name='Power Port 1'), + PowerPortTemplate(device_type=devicetype, name='Power Port 2'), + PowerPortTemplate(device_type=devicetype, name='Power Port 3'), + ) + PowerPortTemplate.objects.bulk_create(power_ports) + + url = reverse('dcim:devicetype_powerports', kwargs={'pk': devicetype.pk}) + self.assertHttpStatus(self.client.get(url), 200) + + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) + def test_devicetype_poweroutlets(self): + devicetype = DeviceType.objects.first() + power_outlets = ( + PowerOutletTemplate(device_type=devicetype, name='Power Outlet 1'), + PowerOutletTemplate(device_type=devicetype, name='Power Outlet 2'), + PowerOutletTemplate(device_type=devicetype, name='Power Outlet 3'), + ) + PowerOutletTemplate.objects.bulk_create(power_outlets) + + url = reverse('dcim:devicetype_poweroutlets', kwargs={'pk': devicetype.pk}) + self.assertHttpStatus(self.client.get(url), 200) + + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) + def test_devicetype_interfaces(self): + devicetype = DeviceType.objects.first() + interfaces = ( + InterfaceTemplate(device_type=devicetype, name='Interface 1'), + InterfaceTemplate(device_type=devicetype, name='Interface 2'), + InterfaceTemplate(device_type=devicetype, name='Interface 3'), + ) + InterfaceTemplate.objects.bulk_create(interfaces) + + url = reverse('dcim:devicetype_interfaces', kwargs={'pk': devicetype.pk}) + self.assertHttpStatus(self.client.get(url), 200) + + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) + def test_devicetype_rearports(self): + devicetype = DeviceType.objects.first() + rear_ports = ( + RearPortTemplate(device_type=devicetype, name='Rear Port 1'), + RearPortTemplate(device_type=devicetype, name='Rear Port 2'), + RearPortTemplate(device_type=devicetype, name='Rear Port 3'), + ) + RearPortTemplate.objects.bulk_create(rear_ports) + + url = reverse('dcim:devicetype_rearports', kwargs={'pk': devicetype.pk}) + self.assertHttpStatus(self.client.get(url), 200) + + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) + def test_devicetype_frontports(self): + devicetype = DeviceType.objects.first() + rear_ports = ( + RearPortTemplate(device_type=devicetype, name='Rear Port 1'), + RearPortTemplate(device_type=devicetype, name='Rear Port 2'), + RearPortTemplate(device_type=devicetype, name='Rear Port 3'), + ) + RearPortTemplate.objects.bulk_create(rear_ports) + front_ports = ( + FrontPortTemplate(device_type=devicetype, name='Front Port 1', rear_port=rear_ports[0], rear_port_position=1), + FrontPortTemplate(device_type=devicetype, name='Front Port 2', rear_port=rear_ports[1], rear_port_position=1), + FrontPortTemplate(device_type=devicetype, name='Front Port 3', rear_port=rear_ports[2], rear_port_position=1), + ) + FrontPortTemplate.objects.bulk_create(front_ports) + + url = reverse('dcim:devicetype_frontports', kwargs={'pk': devicetype.pk}) + self.assertHttpStatus(self.client.get(url), 200) + + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) + def test_devicetype_devicebays(self): + devicetype = DeviceType.objects.first() + device_bays = ( + DeviceBayTemplate(device_type=devicetype, name='Device Bay 1'), + DeviceBayTemplate(device_type=devicetype, name='Device Bay 2'), + DeviceBayTemplate(device_type=devicetype, name='Device Bay 3'), + ) + DeviceBayTemplate.objects.bulk_create(device_bays) + + url = reverse('dcim:devicetype_devicebays', kwargs={'pk': devicetype.pk}) + self.assertHttpStatus(self.client.get(url), 200) + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_import_objects(self): """ diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 01e470e5c..dd81ca2ba 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -109,6 +109,14 @@ urlpatterns = [ path('device-types/edit/', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'), path('device-types/delete/', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'), path('device-types//', views.DeviceTypeView.as_view(), name='devicetype'), + path('device-types//console-ports/', views.DeviceTypeConsolePortsView.as_view(), name='devicetype_consoleports'), + path('device-types//console-server-ports/', views.DeviceTypeConsoleServerPortsView.as_view(), name='devicetype_consoleserverports'), + path('device-types//power-ports/', views.DeviceTypePowerPortsView.as_view(), name='devicetype_powerports'), + path('device-types//power-outlets/', views.DeviceTypePowerOutletsView.as_view(), name='devicetype_poweroutlets'), + path('device-types//interfaces/', views.DeviceTypeInterfacesView.as_view(), name='devicetype_interfaces'), + path('device-types//front-ports/', views.DeviceTypeFrontPortsView.as_view(), name='devicetype_frontports'), + path('device-types//rear-ports/', views.DeviceTypeRearPortsView.as_view(), name='devicetype_rearports'), + path('device-types//device-bays/', views.DeviceTypeDeviceBaysView.as_view(), name='devicetype_devicebays'), path('device-types//edit/', views.DeviceTypeEditView.as_view(), name='devicetype_edit'), path('device-types//delete/', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'), path('device-types//changelog/', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 9b48e0bd3..5079e01a5 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -54,11 +54,19 @@ class DeviceComponentsView(generic.ObjectView): paginate_table(table, request) return { - f'{self.model._meta.model_name}_table': table, + 'table': table, 'active_tab': f"{self.model._meta.verbose_name_plural.replace(' ', '-')}", } +class DeviceTypeComponentsView(DeviceComponentsView): + queryset = DeviceType.objects.all() + template_name = 'dcim/devicetype/component_templates.html' + + def get_components(self, request, instance): + return self.model.objects.restrict(request.user, 'view').filter(device_type=instance) + + class BulkDisconnectView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): """ An extendable view for disconnection console/power/interface components in bulk. @@ -782,62 +790,52 @@ class DeviceTypeView(generic.ObjectView): def get_extra_context(self, request, instance): instance_count = Device.objects.restrict(request.user).filter(device_type=instance).count() - # Component tables - consoleport_table = tables.ConsolePortTemplateTable( - ConsolePortTemplate.objects.restrict(request.user, 'view').filter(device_type=instance), - orderable=False - ) - consoleserverport_table = tables.ConsoleServerPortTemplateTable( - ConsoleServerPortTemplate.objects.restrict(request.user, 'view').filter(device_type=instance), - orderable=False - ) - powerport_table = tables.PowerPortTemplateTable( - PowerPortTemplate.objects.restrict(request.user, 'view').filter(device_type=instance), - orderable=False - ) - poweroutlet_table = tables.PowerOutletTemplateTable( - PowerOutletTemplate.objects.restrict(request.user, 'view').filter(device_type=instance), - orderable=False - ) - interface_table = tables.InterfaceTemplateTable( - list(InterfaceTemplate.objects.restrict(request.user, 'view').filter(device_type=instance)), - orderable=False - ) - front_port_table = tables.FrontPortTemplateTable( - FrontPortTemplate.objects.restrict(request.user, 'view').filter(device_type=instance), - orderable=False - ) - rear_port_table = tables.RearPortTemplateTable( - RearPortTemplate.objects.restrict(request.user, 'view').filter(device_type=instance), - orderable=False - ) - devicebay_table = tables.DeviceBayTemplateTable( - DeviceBayTemplate.objects.restrict(request.user, 'view').filter(device_type=instance), - orderable=False - ) - if request.user.has_perm('dcim.change_devicetype'): - consoleport_table.columns.show('pk') - consoleserverport_table.columns.show('pk') - powerport_table.columns.show('pk') - poweroutlet_table.columns.show('pk') - interface_table.columns.show('pk') - front_port_table.columns.show('pk') - rear_port_table.columns.show('pk') - devicebay_table.columns.show('pk') - return { 'instance_count': instance_count, - 'consoleport_table': consoleport_table, - 'consoleserverport_table': consoleserverport_table, - 'powerport_table': powerport_table, - 'poweroutlet_table': poweroutlet_table, - 'interface_table': interface_table, - 'front_port_table': front_port_table, - 'rear_port_table': rear_port_table, - 'devicebay_table': devicebay_table, + 'active_tab': 'devicetype', } +class DeviceTypeConsolePortsView(DeviceTypeComponentsView): + model = ConsolePortTemplate + table = tables.ConsolePortTemplateTable + + +class DeviceTypeConsoleServerPortsView(DeviceTypeComponentsView): + model = ConsoleServerPortTemplate + table = tables.ConsoleServerPortTemplateTable + + +class DeviceTypePowerPortsView(DeviceTypeComponentsView): + model = PowerPortTemplate + table = tables.PowerPortTemplateTable + + +class DeviceTypePowerOutletsView(DeviceTypeComponentsView): + model = PowerOutletTemplate + table = tables.PowerOutletTemplateTable + + +class DeviceTypeInterfacesView(DeviceTypeComponentsView): + model = InterfaceTemplate + table = tables.InterfaceTemplateTable + + +class DeviceTypeFrontPortsView(DeviceTypeComponentsView): + model = FrontPortTemplate + table = tables.FrontPortTemplateTable + + +class DeviceTypeRearPortsView(DeviceTypeComponentsView): + model = RearPortTemplate + table = tables.RearPortTemplateTable + + +class DeviceTypeDeviceBaysView(DeviceTypeComponentsView): + model = DeviceBayTemplate + table = tables.DeviceBayTemplateTable + + class DeviceTypeEditView(generic.ObjectEditView): queryset = DeviceType.objects.all() model_form = forms.DeviceTypeForm diff --git a/netbox/templates/dcim/device/consoleports.html b/netbox/templates/dcim/device/consoleports.html index 4a7bab4d4..6cf736523 100644 --- a/netbox/templates/dcim/device/consoleports.html +++ b/netbox/templates/dcim/device/consoleports.html @@ -7,7 +7,7 @@
{% csrf_token %} {% include 'inc/table_controls.html' with table_modal="DeviceConsolePortTable_config" %} - {% render_table consoleport_table 'inc/table.html' %} + {% render_table table 'inc/table.html' %}
{% if perms.dcim.change_consoleport %} @@ -36,6 +36,6 @@ {% endif %}
- {% include 'inc/paginator.html' with paginator=consoleport_table.paginator page=consoleport_table.page %} - {% table_config_form consoleport_table %} + {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} + {% table_config_form table %} {% endblock %} diff --git a/netbox/templates/dcim/device/consoleserverports.html b/netbox/templates/dcim/device/consoleserverports.html index 4e97039f3..ca159029e 100644 --- a/netbox/templates/dcim/device/consoleserverports.html +++ b/netbox/templates/dcim/device/consoleserverports.html @@ -7,7 +7,7 @@
{% csrf_token %} {% include 'inc/table_controls.html' with table_modal="DeviceConsoleServerPortTable_config" %} - {% render_table consoleserverport_table 'inc/table.html' %} + {% render_table table 'inc/table.html' %}
{% if perms.dcim.change_consoleserverport %} @@ -36,6 +36,6 @@ {% endif %}
- {% include 'inc/paginator.html' with paginator=consoleserverport_table.paginator page=consoleserverport_table.page %} - {% table_config_form consoleserverport_table %} + {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} + {% table_config_form table %} {% endblock %} diff --git a/netbox/templates/dcim/device/devicebays.html b/netbox/templates/dcim/device/devicebays.html index 31ea9b249..b72625005 100644 --- a/netbox/templates/dcim/device/devicebays.html +++ b/netbox/templates/dcim/device/devicebays.html @@ -7,7 +7,7 @@
{% csrf_token %} {% include 'inc/table_controls.html' with table_modal="DeviceDeviceBayTable_config" %} - {% render_table devicebay_table 'inc/table.html' %} + {% render_table table 'inc/table.html' %}
{% if perms.dcim.change_devicebay %} @@ -33,6 +33,6 @@ {% endif %}
- {% include 'inc/paginator.html' with paginator=devicebay_table.paginator page=devicebay_table.page %} - {% table_config_form devicebay_table %} + {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} + {% table_config_form table %} {% endblock %} diff --git a/netbox/templates/dcim/device/frontports.html b/netbox/templates/dcim/device/frontports.html index 4d15dde1b..5833a1c78 100644 --- a/netbox/templates/dcim/device/frontports.html +++ b/netbox/templates/dcim/device/frontports.html @@ -7,7 +7,7 @@
{% csrf_token %} {% include 'inc/table_controls.html' with table_modal="DeviceFrontPortTable_config" %} - {% render_table frontport_table 'inc/table.html' %} + {% render_table table 'inc/table.html' %}
{% if perms.dcim.change_frontport %} @@ -36,6 +36,6 @@ {% endif %}
- {% include 'inc/paginator.html' with paginator=frontport_table.paginator page=frontport_table.page %} - {% table_config_form frontport_table %} + {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} + {% table_config_form table %} {% endblock %} diff --git a/netbox/templates/dcim/device/interfaces.html b/netbox/templates/dcim/device/interfaces.html index 03c8a8913..1d1e7e81b 100644 --- a/netbox/templates/dcim/device/interfaces.html +++ b/netbox/templates/dcim/device/interfaces.html @@ -34,7 +34,7 @@
- {% render_table interface_table 'inc/table.html' %} + {% render_table table 'inc/table.html' %}
{% if perms.dcim.change_interface %} @@ -63,6 +63,6 @@ {% endif %}
- {% include 'inc/paginator.html' with paginator=interface_table.paginator page=interface_table.page %} - {% table_config_form interface_table %} + {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} + {% table_config_form table %} {% endblock %} diff --git a/netbox/templates/dcim/device/inventory.html b/netbox/templates/dcim/device/inventory.html index 6c9fdb17b..2aad68984 100644 --- a/netbox/templates/dcim/device/inventory.html +++ b/netbox/templates/dcim/device/inventory.html @@ -7,7 +7,7 @@
{% csrf_token %} {% include 'inc/table_controls.html' with table_modal="DeviceInventoryItemTable_config" %} - {% render_table inventoryitem_table 'inc/table.html' %} + {% render_table table 'inc/table.html' %}
{% if perms.dcim.change_inventoryitem %} @@ -33,6 +33,6 @@ {% endif %}
- {% include 'inc/paginator.html' with paginator=inventoryitem_table.paginator page=inventoryitem_table.page %} - {% table_config_form inventoryitem_table %} + {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} + {% table_config_form table %} {% endblock %} diff --git a/netbox/templates/dcim/device/poweroutlets.html b/netbox/templates/dcim/device/poweroutlets.html index f9937bf27..df936742e 100644 --- a/netbox/templates/dcim/device/poweroutlets.html +++ b/netbox/templates/dcim/device/poweroutlets.html @@ -7,7 +7,7 @@
{% csrf_token %} {% include 'inc/table_controls.html' with table_modal="DevicePowerOutletTable_config" %} - {% render_table poweroutlet_table 'inc/table.html' %} + {% render_table table 'inc/table.html' %}
{% if perms.dcim.change_powerport %} @@ -36,6 +36,6 @@ {% endif %}
- {% include 'inc/paginator.html' with paginator=poweroutlet_table.paginator page=poweroutlet_table.page %} - {% table_config_form poweroutlet_table %} + {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} + {% table_config_form table %} {% endblock %} diff --git a/netbox/templates/dcim/device/powerports.html b/netbox/templates/dcim/device/powerports.html index 7d219979c..5a502dc57 100644 --- a/netbox/templates/dcim/device/powerports.html +++ b/netbox/templates/dcim/device/powerports.html @@ -7,7 +7,7 @@
{% csrf_token %} {% include 'inc/table_controls.html' with table_modal="DevicePowerPortTable_config" %} - {% render_table powerport_table 'inc/table.html' %} + {% render_table table 'inc/table.html' %}
{% if perms.dcim.change_powerport %} @@ -36,6 +36,6 @@ {% endif %}
- {% include 'inc/paginator.html' with paginator=powerport_table.paginator page=powerport_table.page %} - {% table_config_form powerport_table %} + {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} + {% table_config_form table %} {% endblock %} diff --git a/netbox/templates/dcim/device/rearports.html b/netbox/templates/dcim/device/rearports.html index f0ec37b80..d0ff55ec9 100644 --- a/netbox/templates/dcim/device/rearports.html +++ b/netbox/templates/dcim/device/rearports.html @@ -7,7 +7,7 @@
{% csrf_token %} {% include 'inc/table_controls.html' with table_modal="DeviceRearPortTable_config" %} - {% render_table rearport_table 'inc/table.html' %} + {% render_table table 'inc/table.html' %}
{% if perms.dcim.change_rearport %} @@ -36,6 +36,6 @@ {% endif %}
- {% include 'inc/paginator.html' with paginator=rearport_table.paginator page=rearport_table.page %} - {% table_config_form rearport_table %} + {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} + {% table_config_form table %} {% endblock %} diff --git a/netbox/templates/dcim/devicetype.html b/netbox/templates/dcim/devicetype.html index 77db7ed18..74a3e73d7 100644 --- a/netbox/templates/dcim/devicetype.html +++ b/netbox/templates/dcim/devicetype.html @@ -1,51 +1,8 @@ -{% extends 'generic/object.html' %} +{% extends 'dcim/devicetype/base.html' %} {% load buttons %} {% load helpers %} {% load plugins %} -{% block title %}{{ object.manufacturer }} {{ object.model }}{% endblock %} - -{% block breadcrumbs %} - {{ block.super }} - -{% endblock %} - -{% block extra_controls %} - {% if perms.dcim.change_devicetype %} - - {% endif %} -{% endblock %} - {% block content %}
@@ -141,76 +98,4 @@ {% plugin_full_width_page object %}
-
-
- -
-
- {% include 'dcim/inc/devicetype_component_table.html' with table=interface_table title='Interfaces' tab='interfaces' %} -
-
- {% include 'dcim/inc/devicetype_component_table.html' with table=front_port_table title='Front Ports' tab='frontports' %} -
-
- {% include 'dcim/inc/devicetype_component_table.html' with table=rear_port_table title='Rear Ports' tab='rearports' %} -
-
- {% include 'dcim/inc/devicetype_component_table.html' with table=consoleport_table title='Console Ports' tab='consoleports' %} -
-
- {% include 'dcim/inc/devicetype_component_table.html' with table=consoleserverport_table title='Console Server Ports' tab='consoleserverports' %} -
-
- {% include 'dcim/inc/devicetype_component_table.html' with table=powerport_table title='Power Ports' tab='powerports' %} -
-
- {% include 'dcim/inc/devicetype_component_table.html' with table=poweroutlet_table title='Power Outlets' tab='poweroutlets' %} -
-
- {% include 'dcim/inc/devicetype_component_table.html' with table=devicebay_table title='Device Bays' tab='devicebays' %} -
-
-
-
{% endblock %} diff --git a/netbox/templates/dcim/devicetype/base.html b/netbox/templates/dcim/devicetype/base.html new file mode 100644 index 000000000..a06886de5 --- /dev/null +++ b/netbox/templates/dcim/devicetype/base.html @@ -0,0 +1,119 @@ +{% extends 'generic/object.html' %} +{% load buttons %} +{% load helpers %} +{% load plugins %} + +{% block title %}{{ object.manufacturer }} {{ object.model }}{% endblock %} + +{% block breadcrumbs %} + {{ block.super }} + +{% endblock %} + +{% block extra_controls %} + {% if perms.dcim.change_devicetype %} + + {% endif %} +{% endblock %} + +{% block tab_items %} + + + {% with interface_count=object.interfacetemplates.count %} + {% if interface_count %} + + {% endif %} + {% endwith %} + + {% with frontport_count=object.frontporttemplates.count %} + {% if frontport_count %} + + {% endif %} + {% endwith %} + + {% with rearport_count=object.rearporttemplates.count %} + {% if rearport_count %} + + {% endif %} + {% endwith %} + + {% with consoleport_count=object.consoleporttemplates.count %} + {% if consoleport_count %} + + {% endif %} + {% endwith %} + + {% with consoleserverport_count=object.consoleserverporttemplates.count %} + {% if consoleserverport_count %} + + {% endif %} + {% endwith %} + + {% with powerport_count=object.powerporttemplates.count %} + {% if powerport_count %} + + {% endif %} + {% endwith %} + + {% with poweroutlet_count=object.poweroutlettemplates.count %} + {% if poweroutlet_count %} + + {% endif %} + {% endwith %} + + {% with devicebay_count=object.devicebaytemplates.count %} + {% if devicebay_count %} + + {% endif %} + {% endwith %} +{% endblock %} diff --git a/netbox/templates/dcim/inc/devicetype_component_table.html b/netbox/templates/dcim/devicetype/component_templates.html similarity index 93% rename from netbox/templates/dcim/inc/devicetype_component_table.html rename to netbox/templates/dcim/devicetype/component_templates.html index 900e0f818..d83a232cd 100644 --- a/netbox/templates/dcim/inc/devicetype_component_table.html +++ b/netbox/templates/dcim/devicetype/component_templates.html @@ -1,7 +1,9 @@ -{% load helpers %} +{% extends 'dcim/devicetype/base.html' %} {% load render_table from django_tables2 %} +{% load helpers %} -{% if perms.dcim.change_devicetype %} +{% block content %} + {% if perms.dcim.change_devicetype %}
{% csrf_token %}
@@ -33,7 +35,7 @@
-{% else %} + {% else %}
{{ title }} @@ -42,4 +44,5 @@ {% render_table table 'inc/table.html' %}
-{% endif %} + {% endif %} +{% endblock content %} diff --git a/netbox/templates/dcim/inc/device_component_table.html b/netbox/templates/dcim/inc/device_component_table.html deleted file mode 100644 index b272e2731..000000000 --- a/netbox/templates/dcim/inc/device_component_table.html +++ /dev/null @@ -1,42 +0,0 @@ -{% load helpers %} -{% load perms %} -
- {% csrf_token %} -
-
- {{ title }} -
-
- - {% for obj in components %} - {% include component_template %} - {% endfor %} -
-
- {% if components and perms.dcim.change_consoleport %} - - {% endif %} -
-
From cfb3897047ff6fc38586466f56468eb9bb3ccba4 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 21 Oct 2021 10:51:02 -0400 Subject: [PATCH 33/35] Add tags to organizational & nested group models --- docs/development/models.md | 4 +- docs/models/extras/tag.md | 3 -- netbox/circuits/api/serializers.py | 8 ++- netbox/circuits/api/views.py | 2 +- netbox/circuits/forms/bulk_edit.py | 2 +- netbox/circuits/forms/models.py | 6 ++- .../migrations/0003_extend_tag_support.py | 20 ++++++++ netbox/circuits/models.py | 2 +- netbox/circuits/tables.py | 5 +- netbox/circuits/tests/test_views.py | 3 ++ netbox/dcim/api/serializers.py | 33 ++++++------ netbox/dcim/api/views.py | 14 +++--- netbox/dcim/forms/bulk_edit.py | 14 +++--- netbox/dcim/forms/models.py | 44 +++++++++++++--- .../migrations/0138_extend_tag_support.py | 50 +++++++++++++++++++ netbox/dcim/models/devices.py | 6 +-- netbox/dcim/models/racks.py | 2 +- netbox/dcim/models/sites.py | 6 +-- netbox/dcim/tables/devices.py | 12 ++++- netbox/dcim/tables/devicetypes.py | 6 ++- netbox/dcim/tables/racks.py | 5 +- netbox/dcim/tables/sites.py | 17 +++++-- netbox/dcim/tests/test_views.py | 21 ++++++++ netbox/ipam/api/serializers.py | 17 +++---- netbox/ipam/api/views.py | 6 +-- netbox/ipam/forms/bulk_edit.py | 6 +-- netbox/ipam/forms/models.py | 20 ++++++-- .../migrations/0051_extend_tag_support.py | 30 +++++++++++ netbox/ipam/models/ip.py | 4 +- netbox/ipam/models/vlans.py | 2 +- netbox/ipam/tables/ip.py | 10 +++- netbox/ipam/tables/vlans.py | 5 +- netbox/ipam/tests/test_views.py | 9 ++++ netbox/netbox/api/serializers.py | 11 +--- netbox/netbox/graphql/types.py | 1 + netbox/netbox/models.py | 21 +++++--- netbox/tenancy/api/serializers.py | 14 +++--- netbox/tenancy/api/views.py | 14 ++---- netbox/tenancy/forms/bulk_edit.py | 6 +-- netbox/tenancy/forms/models.py | 18 +++++-- .../migrations/0004_extend_tag_support.py | 30 +++++++++++ netbox/tenancy/models.py | 6 +-- netbox/tenancy/tables.py | 10 +++- netbox/tenancy/tests/test_views.py | 9 ++++ netbox/virtualization/api/serializers.py | 10 ++-- netbox/virtualization/api/views.py | 4 +- netbox/virtualization/forms/bulk_edit.py | 4 +- netbox/virtualization/forms/models.py | 20 +++++--- .../migrations/0025_extend_tag_support.py | 25 ++++++++++ netbox/virtualization/models.py | 4 +- netbox/virtualization/tables.py | 10 +++- netbox/virtualization/tests/test_views.py | 6 +++ 52 files changed, 463 insertions(+), 154 deletions(-) create mode 100644 netbox/circuits/migrations/0003_extend_tag_support.py create mode 100644 netbox/dcim/migrations/0138_extend_tag_support.py create mode 100644 netbox/ipam/migrations/0051_extend_tag_support.py create mode 100644 netbox/tenancy/migrations/0004_extend_tag_support.py create mode 100644 netbox/virtualization/migrations/0025_extend_tag_support.py diff --git a/docs/development/models.md b/docs/development/models.md index 93a10fff6..59e795cf7 100644 --- a/docs/development/models.md +++ b/docs/development/models.md @@ -19,8 +19,8 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/ | Type | Change Logging | Webhooks | Custom Fields | Export Templates | Tags | Journaling | Nesting | | ------------------ | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | | Primary | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | -| Organizational | :material-check: | :material-check: | :material-check: | :material-check: | | | | -| Nested Group | :material-check: | :material-check: | :material-check: | :material-check: | | | :material-check: | +| Organizational | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | | +| Nested Group | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | :material-check: | | Component | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | | | Component Template | :material-check: | :material-check: | :material-check: | | | | | diff --git a/docs/models/extras/tag.md b/docs/models/extras/tag.md index 29cc8b757..fe6a1ef36 100644 --- a/docs/models/extras/tag.md +++ b/docs/models/extras/tag.md @@ -15,6 +15,3 @@ The `tag` filter can be specified multiple times to match only objects which hav ```no-highlight GET /api/dcim/devices/?tag=monitored&tag=deprecated ``` - -!!! note - Tags have changed substantially in NetBox v2.9. They are no longer created on-demand when editing an object, and their representation in the REST API now includes a complete depiction of the tag rather than only its label. diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index ac6285610..0033e1425 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -5,9 +5,7 @@ from circuits.models import * from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer from dcim.api.serializers import CableTerminationSerializer from netbox.api import ChoiceField -from netbox.api.serializers import ( - OrganizationalModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer -) +from netbox.api.serializers import PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer from tenancy.api.nested_serializers import NestedTenantSerializer from .nested_serializers import * @@ -48,14 +46,14 @@ class ProviderNetworkSerializer(PrimaryModelSerializer): # Circuits # -class CircuitTypeSerializer(OrganizationalModelSerializer): +class CircuitTypeSerializer(PrimaryModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail') circuit_count = serializers.IntegerField(read_only=True) class Meta: model = CircuitType fields = [ - 'id', 'url', 'display', 'name', 'slug', 'description', 'custom_fields', 'created', 'last_updated', + 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count', ] diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index 3bceb2de0..2b3e3b122 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -34,7 +34,7 @@ class ProviderViewSet(CustomFieldModelViewSet): # class CircuitTypeViewSet(CustomFieldModelViewSet): - queryset = CircuitType.objects.annotate( + queryset = CircuitType.objects.prefetch_related('tags').annotate( circuit_count=count_related(Circuit, 'type') ) serializer_class = serializers.CircuitTypeSerializer diff --git a/netbox/circuits/forms/bulk_edit.py b/netbox/circuits/forms/bulk_edit.py index 638426a5e..7bf5644b9 100644 --- a/netbox/circuits/forms/bulk_edit.py +++ b/netbox/circuits/forms/bulk_edit.py @@ -79,7 +79,7 @@ class ProviderNetworkBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomField ] -class CircuitTypeBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): +class CircuitTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=CircuitType.objects.all(), widget=forms.MultipleHiddenInput diff --git a/netbox/circuits/forms/models.py b/netbox/circuits/forms/models.py index 659939293..5679dbc94 100644 --- a/netbox/circuits/forms/models.py +++ b/netbox/circuits/forms/models.py @@ -75,11 +75,15 @@ class ProviderNetworkForm(BootstrapMixin, CustomFieldModelForm): class CircuitTypeForm(BootstrapMixin, CustomFieldModelForm): slug = SlugField() + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) class Meta: model = CircuitType fields = [ - 'name', 'slug', 'description', + 'name', 'slug', 'description', 'tags', ] diff --git a/netbox/circuits/migrations/0003_extend_tag_support.py b/netbox/circuits/migrations/0003_extend_tag_support.py new file mode 100644 index 000000000..e5e6ee262 --- /dev/null +++ b/netbox/circuits/migrations/0003_extend_tag_support.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.8 on 2021-10-21 14:50 + +from django.db import migrations +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0062_clear_secrets_changelog'), + ('circuits', '0002_squashed_0029'), + ] + + operations = [ + migrations.AddField( + model_name='circuittype', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + ] diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index 3d213b48d..e6e03052d 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -128,7 +128,7 @@ class ProviderNetwork(PrimaryModel): return reverse('circuits:providernetwork', args=[self.pk]) -@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class CircuitType(OrganizationalModel): """ Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index 2e31237b6..d0b0797e2 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -82,6 +82,9 @@ class CircuitTypeTable(BaseTable): name = tables.Column( linkify=True ) + tags = TagColumn( + url_name='circuits:circuittype_list' + ) circuit_count = tables.Column( verbose_name='Circuits' ) @@ -89,7 +92,7 @@ class CircuitTypeTable(BaseTable): class Meta(BaseTable.Meta): model = CircuitType - fields = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions') + fields = ('pk', 'name', 'circuit_count', 'description', 'slug', 'tags', 'actions') default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions') diff --git a/netbox/circuits/tests/test_views.py b/netbox/circuits/tests/test_views.py index ccb4a869a..851d52ae8 100644 --- a/netbox/circuits/tests/test_views.py +++ b/netbox/circuits/tests/test_views.py @@ -64,10 +64,13 @@ class CircuitTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase): CircuitType(name='Circuit Type 3', slug='circuit-type-3'), ]) + tags = create_tags('Alpha', 'Bravo', 'Charlie') + cls.form_data = { 'name': 'Circuit Type X', 'slug': 'circuit-type-x', 'description': 'A new circuit type', + 'tags': [t.pk for t in tags], } cls.csv_data = ( diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 9b0e7f5b3..ef4f49247 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -11,8 +11,7 @@ from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSer from ipam.models import VLAN from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField from netbox.api.serializers import ( - NestedGroupModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer, - WritableNestedSerializer, + NestedGroupModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer, ) from tenancy.api.nested_serializers import NestedTenantSerializer from users.api.nested_serializers import NestedUserSerializer @@ -87,8 +86,8 @@ class RegionSerializer(NestedGroupModelSerializer): class Meta: model = Region fields = [ - 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated', - 'site_count', '_depth', + 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', + 'last_updated', 'site_count', '_depth', ] @@ -100,8 +99,8 @@ class SiteGroupSerializer(NestedGroupModelSerializer): class Meta: model = SiteGroup fields = [ - 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated', - 'site_count', '_depth', + 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', + 'last_updated', 'site_count', '_depth', ] @@ -144,20 +143,20 @@ class LocationSerializer(NestedGroupModelSerializer): class Meta: model = Location fields = [ - 'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'tenant', 'description', 'custom_fields', + 'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'tenant', 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count', '_depth', ] -class RackRoleSerializer(OrganizationalModelSerializer): +class RackRoleSerializer(PrimaryModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail') rack_count = serializers.IntegerField(read_only=True) class Meta: model = RackRole fields = [ - 'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'custom_fields', 'created', 'last_updated', - 'rack_count', + 'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created', + 'last_updated', 'rack_count', ] @@ -254,7 +253,7 @@ class RackElevationDetailFilterSerializer(serializers.Serializer): # Device types # -class ManufacturerSerializer(OrganizationalModelSerializer): +class ManufacturerSerializer(PrimaryModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail') devicetype_count = serializers.IntegerField(read_only=True) inventoryitem_count = serializers.IntegerField(read_only=True) @@ -263,7 +262,7 @@ class ManufacturerSerializer(OrganizationalModelSerializer): class Meta: model = Manufacturer fields = [ - 'id', 'url', 'display', 'name', 'slug', 'description', 'custom_fields', 'created', 'last_updated', + 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'devicetype_count', 'inventoryitem_count', 'platform_count', ] @@ -411,7 +410,7 @@ class DeviceBayTemplateSerializer(ValidatedModelSerializer): # Devices # -class DeviceRoleSerializer(OrganizationalModelSerializer): +class DeviceRoleSerializer(PrimaryModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail') device_count = serializers.IntegerField(read_only=True) virtualmachine_count = serializers.IntegerField(read_only=True) @@ -419,12 +418,12 @@ class DeviceRoleSerializer(OrganizationalModelSerializer): class Meta: model = DeviceRole fields = [ - 'id', 'url', 'display', 'name', 'slug', 'color', 'vm_role', 'description', 'custom_fields', 'created', - 'last_updated', 'device_count', 'virtualmachine_count', + 'id', 'url', 'display', 'name', 'slug', 'color', 'vm_role', 'description', 'tags', 'custom_fields', + 'created', 'last_updated', 'device_count', 'virtualmachine_count', ] -class PlatformSerializer(OrganizationalModelSerializer): +class PlatformSerializer(PrimaryModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail') manufacturer = NestedManufacturerSerializer(required=False, allow_null=True) device_count = serializers.IntegerField(read_only=True) @@ -434,7 +433,7 @@ class PlatformSerializer(OrganizationalModelSerializer): model = Platform fields = [ 'id', 'url', 'display', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', - 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count', + 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count', ] diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 2b9d9734c..799a5e703 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -110,7 +110,7 @@ class RegionViewSet(CustomFieldModelViewSet): 'region', 'site_count', cumulative=True - ) + ).prefetch_related('tags') serializer_class = serializers.RegionSerializer filterset_class = filtersets.RegionFilterSet @@ -126,7 +126,7 @@ class SiteGroupViewSet(CustomFieldModelViewSet): 'group', 'site_count', cumulative=True - ) + ).prefetch_related('tags') serializer_class = serializers.SiteGroupSerializer filterset_class = filtersets.SiteGroupFilterSet @@ -167,7 +167,7 @@ class LocationViewSet(CustomFieldModelViewSet): 'location', 'rack_count', cumulative=True - ).prefetch_related('site') + ).prefetch_related('site', 'tags') serializer_class = serializers.LocationSerializer filterset_class = filtersets.LocationFilterSet @@ -177,7 +177,7 @@ class LocationViewSet(CustomFieldModelViewSet): # class RackRoleViewSet(CustomFieldModelViewSet): - queryset = RackRole.objects.annotate( + queryset = RackRole.objects.prefetch_related('tags').annotate( rack_count=count_related(Rack, 'role') ) serializer_class = serializers.RackRoleSerializer @@ -261,7 +261,7 @@ class RackReservationViewSet(ModelViewSet): # class ManufacturerViewSet(CustomFieldModelViewSet): - queryset = Manufacturer.objects.annotate( + queryset = Manufacturer.objects.prefetch_related('tags').annotate( devicetype_count=count_related(DeviceType, 'manufacturer'), inventoryitem_count=count_related(InventoryItem, 'manufacturer'), platform_count=count_related(Platform, 'manufacturer') @@ -340,7 +340,7 @@ class DeviceBayTemplateViewSet(ModelViewSet): # class DeviceRoleViewSet(CustomFieldModelViewSet): - queryset = DeviceRole.objects.annotate( + queryset = DeviceRole.objects.prefetch_related('tags').annotate( device_count=count_related(Device, 'device_role'), virtualmachine_count=count_related(VirtualMachine, 'role') ) @@ -353,7 +353,7 @@ class DeviceRoleViewSet(CustomFieldModelViewSet): # class PlatformViewSet(CustomFieldModelViewSet): - queryset = Platform.objects.annotate( + queryset = Platform.objects.prefetch_related('tags').annotate( device_count=count_related(Device, 'platform'), virtualmachine_count=count_related(VirtualMachine, 'platform') ) diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 06ccc958c..d08692c26 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -51,7 +51,7 @@ __all__ = ( ) -class RegionBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): +class RegionBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Region.objects.all(), widget=forms.MultipleHiddenInput @@ -69,7 +69,7 @@ class RegionBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): nullable_fields = ['parent', 'description'] -class SiteGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): +class SiteGroupBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=SiteGroup.objects.all(), widget=forms.MultipleHiddenInput @@ -132,7 +132,7 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEd ] -class LocationBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): +class LocationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Location.objects.all(), widget=forms.MultipleHiddenInput @@ -161,7 +161,7 @@ class LocationBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): nullable_fields = ['parent', 'tenant', 'description'] -class RackRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): +class RackRoleBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=RackRole.objects.all(), widget=forms.MultipleHiddenInput @@ -303,7 +303,7 @@ class RackReservationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomField nullable_fields = [] -class ManufacturerBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): +class ManufacturerBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Manufacturer.objects.all(), widget=forms.MultipleHiddenInput @@ -345,7 +345,7 @@ class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModel nullable_fields = ['airflow'] -class DeviceRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): +class DeviceRoleBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=DeviceRole.objects.all(), widget=forms.MultipleHiddenInput @@ -367,7 +367,7 @@ class DeviceRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): nullable_fields = ['color', 'description'] -class PlatformBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): +class PlatformBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Platform.objects.all(), widget=forms.MultipleHiddenInput diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py index 8236b1a97..a3dac09dd 100644 --- a/netbox/dcim/forms/models.py +++ b/netbox/dcim/forms/models.py @@ -70,11 +70,15 @@ class RegionForm(BootstrapMixin, CustomFieldModelForm): required=False ) slug = SlugField() + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) class Meta: model = Region fields = ( - 'parent', 'name', 'slug', 'description', + 'parent', 'name', 'slug', 'description', 'tags', ) @@ -84,11 +88,15 @@ class SiteGroupForm(BootstrapMixin, CustomFieldModelForm): required=False ) slug = SlugField() + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) class Meta: model = SiteGroup fields = ( - 'parent', 'name', 'slug', 'description', + 'parent', 'name', 'slug', 'description', 'tags', ) @@ -187,15 +195,19 @@ class LocationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): } ) slug = SlugField() + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) class Meta: model = Location fields = ( - 'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', 'tenant_group', 'tenant', + 'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', 'tenant_group', 'tenant', 'tags', ) fieldsets = ( ('Location', ( - 'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', + 'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', 'tags', )), ('Tenancy', ('tenant_group', 'tenant')), ) @@ -203,11 +215,15 @@ class LocationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): class RackRoleForm(BootstrapMixin, CustomFieldModelForm): slug = SlugField() + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) class Meta: model = RackRole fields = [ - 'name', 'slug', 'color', 'description', + 'name', 'slug', 'color', 'description', 'tags', ] @@ -343,11 +359,15 @@ class RackReservationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): class ManufacturerForm(BootstrapMixin, CustomFieldModelForm): slug = SlugField() + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) class Meta: model = Manufacturer fields = [ - 'name', 'slug', 'description', + 'name', 'slug', 'description', 'tags', ] @@ -392,11 +412,15 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldModelForm): class DeviceRoleForm(BootstrapMixin, CustomFieldModelForm): slug = SlugField() + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) class Meta: model = DeviceRole fields = [ - 'name', 'slug', 'color', 'vm_role', 'description', + 'name', 'slug', 'color', 'vm_role', 'description', 'tags', ] @@ -408,11 +432,15 @@ class PlatformForm(BootstrapMixin, CustomFieldModelForm): slug = SlugField( max_length=64 ) + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) class Meta: model = Platform fields = [ - 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', + 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'tags', ] widgets = { 'napalm_args': SmallTextarea(), diff --git a/netbox/dcim/migrations/0138_extend_tag_support.py b/netbox/dcim/migrations/0138_extend_tag_support.py new file mode 100644 index 000000000..763b53c50 --- /dev/null +++ b/netbox/dcim/migrations/0138_extend_tag_support.py @@ -0,0 +1,50 @@ +# Generated by Django 3.2.8 on 2021-10-21 14:50 + +from django.db import migrations +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0062_clear_secrets_changelog'), + ('dcim', '0137_relax_uniqueness_constraints'), + ] + + operations = [ + migrations.AddField( + model_name='devicerole', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AddField( + model_name='location', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AddField( + model_name='manufacturer', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AddField( + model_name='platform', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AddField( + model_name='rackrole', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AddField( + model_name='region', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AddField( + model_name='sitegroup', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + ] diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 308a094c3..2b3b80d24 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -36,7 +36,7 @@ __all__ = ( # Device Types # -@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class Manufacturer(OrganizationalModel): """ A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell. @@ -351,7 +351,7 @@ class DeviceType(PrimaryModel): # Devices # -@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class DeviceRole(OrganizationalModel): """ Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a @@ -391,7 +391,7 @@ class DeviceRole(OrganizationalModel): return reverse('dcim:devicerole', args=[self.pk]) -@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class Platform(OrganizationalModel): """ Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos". diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index 47fcd42e4..a6be069b6 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -35,7 +35,7 @@ __all__ = ( # Racks # -@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class RackRole(OrganizationalModel): """ Racks can be organized by functional role, similar to Devices. diff --git a/netbox/dcim/models/sites.py b/netbox/dcim/models/sites.py index ab9d8e82d..a978e69e6 100644 --- a/netbox/dcim/models/sites.py +++ b/netbox/dcim/models/sites.py @@ -25,7 +25,7 @@ __all__ = ( # Regions # -@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class Region(NestedGroupModel): """ A region represents a geographic collection of sites. For example, you might create regions representing countries, @@ -82,7 +82,7 @@ class Region(NestedGroupModel): # Site groups # -@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class SiteGroup(NestedGroupModel): """ A site group is an arbitrary grouping of sites. For example, you might have corporate sites and customer sites; and @@ -278,7 +278,7 @@ class Site(PrimaryModel): # Locations # -@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class Location(NestedGroupModel): """ A Location represents a subgroup of Racks and/or Devices within a Site. A Location may represent a building within a diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index a2d3f3da2..f47073848 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -84,11 +84,16 @@ class DeviceRoleTable(BaseTable): ) color = ColorColumn() vm_role = BooleanColumn() + tags = TagColumn( + url_name='dcim:devicerole_list' + ) actions = ButtonsColumn(DeviceRole) class Meta(BaseTable.Meta): model = DeviceRole - fields = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'actions') + fields = ( + 'pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'tags', 'actions', + ) default_columns = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'actions') @@ -111,13 +116,16 @@ class PlatformTable(BaseTable): url_params={'platform_id': 'pk'}, verbose_name='VMs' ) + tags = TagColumn( + url_name='dcim:platform_list' + ) actions = ButtonsColumn(Platform) class Meta(BaseTable.Meta): model = Platform fields = ( 'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'napalm_args', - 'description', 'actions', + 'description', 'tags', 'actions', ) default_columns = ( 'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'napalm_driver', 'description', 'actions', diff --git a/netbox/dcim/tables/devicetypes.py b/netbox/dcim/tables/devicetypes.py index b3310d5d2..9631b5709 100644 --- a/netbox/dcim/tables/devicetypes.py +++ b/netbox/dcim/tables/devicetypes.py @@ -41,12 +41,16 @@ class ManufacturerTable(BaseTable): verbose_name='Platforms' ) slug = tables.Column() + tags = TagColumn( + url_name='dcim:manufacturer_list' + ) actions = ButtonsColumn(Manufacturer) class Meta(BaseTable.Meta): model = Manufacturer fields = ( - 'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', 'actions', + 'pk', 'name', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'description', 'slug', 'tags', + 'actions', ) diff --git a/netbox/dcim/tables/racks.py b/netbox/dcim/tables/racks.py index fcc3ed4d2..bdc5ae713 100644 --- a/netbox/dcim/tables/racks.py +++ b/netbox/dcim/tables/racks.py @@ -24,11 +24,14 @@ class RackRoleTable(BaseTable): name = tables.Column(linkify=True) rack_count = tables.Column(verbose_name='Racks') color = ColorColumn() + tags = TagColumn( + url_name='dcim:rackrole_list' + ) actions = ButtonsColumn(RackRole) class Meta(BaseTable.Meta): model = RackRole - fields = ('pk', 'name', 'rack_count', 'color', 'description', 'slug', 'actions') + fields = ('pk', 'name', 'rack_count', 'color', 'description', 'slug', 'tags', 'actions') default_columns = ('pk', 'name', 'rack_count', 'color', 'description', 'actions') diff --git a/netbox/dcim/tables/sites.py b/netbox/dcim/tables/sites.py index 3ff6ab75b..65419e9c8 100644 --- a/netbox/dcim/tables/sites.py +++ b/netbox/dcim/tables/sites.py @@ -29,11 +29,14 @@ class RegionTable(BaseTable): url_params={'region_id': 'pk'}, verbose_name='Sites' ) + tags = TagColumn( + url_name='dcim:region_list' + ) actions = ButtonsColumn(Region) class Meta(BaseTable.Meta): model = Region - fields = ('pk', 'name', 'slug', 'site_count', 'description', 'actions') + fields = ('pk', 'name', 'slug', 'site_count', 'description', 'tags', 'actions') default_columns = ('pk', 'name', 'site_count', 'description', 'actions') @@ -51,11 +54,14 @@ class SiteGroupTable(BaseTable): url_params={'group_id': 'pk'}, verbose_name='Sites' ) + tags = TagColumn( + url_name='dcim:sitegroup_list' + ) actions = ButtonsColumn(SiteGroup) class Meta(BaseTable.Meta): model = SiteGroup - fields = ('pk', 'name', 'slug', 'site_count', 'description', 'actions') + fields = ('pk', 'name', 'slug', 'site_count', 'description', 'tags', 'actions') default_columns = ('pk', 'name', 'site_count', 'description', 'actions') @@ -114,6 +120,9 @@ class LocationTable(BaseTable): url_params={'location_id': 'pk'}, verbose_name='Devices' ) + tags = TagColumn( + url_name='dcim:location_list' + ) actions = ButtonsColumn( model=Location, prepend_template=LOCATION_ELEVATIONS @@ -121,5 +130,7 @@ class LocationTable(BaseTable): class Meta(BaseTable.Meta): model = Location - fields = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'actions') + fields = ( + 'pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'tags', 'actions', + ) default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'actions') diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index a9c191679..4565c898b 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -31,11 +31,14 @@ class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase): for region in regions: region.save() + tags = create_tags('Alpha', 'Bravo', 'Charlie') + cls.form_data = { 'name': 'Region X', 'slug': 'region-x', 'parent': regions[2].pk, 'description': 'A new region', + 'tags': [t.pk for t in tags], } cls.csv_data = ( @@ -65,11 +68,14 @@ class SiteGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase): for sitegroup in sitegroups: sitegroup.save() + tags = create_tags('Alpha', 'Bravo', 'Charlie') + cls.form_data = { 'name': 'Site Group X', 'slug': 'site-group-x', 'parent': sitegroups[2].pk, 'description': 'A new site group', + 'tags': [t.pk for t in tags], } cls.csv_data = ( @@ -169,12 +175,15 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase): for location in locations: location.save() + tags = create_tags('Alpha', 'Bravo', 'Charlie') + cls.form_data = { 'name': 'Location X', 'slug': 'location-x', 'site': site.pk, 'tenant': tenant.pk, 'description': 'A new location', + 'tags': [t.pk for t in tags], } cls.csv_data = ( @@ -201,11 +210,14 @@ class RackRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase): RackRole(name='Rack Role 3', slug='rack-role-3'), ]) + tags = create_tags('Alpha', 'Bravo', 'Charlie') + cls.form_data = { 'name': 'Rack Role X', 'slug': 'rack-role-x', 'color': 'c0c0c0', 'description': 'New role', + 'tags': [t.pk for t in tags], } cls.csv_data = ( @@ -368,10 +380,13 @@ class ManufacturerTestCase(ViewTestCases.OrganizationalObjectViewTestCase): Manufacturer(name='Manufacturer 3', slug='manufacturer-3'), ]) + tags = create_tags('Alpha', 'Bravo', 'Charlie') + cls.form_data = { 'name': 'Manufacturer X', 'slug': 'manufacturer-x', 'description': 'A new manufacturer', + 'tags': [t.pk for t in tags], } cls.csv_data = ( @@ -1034,12 +1049,15 @@ class DeviceRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase): DeviceRole(name='Device Role 3', slug='device-role-3'), ]) + tags = create_tags('Alpha', 'Bravo', 'Charlie') + cls.form_data = { 'name': 'Devie Role X', 'slug': 'device-role-x', 'color': 'c0c0c0', 'vm_role': False, 'description': 'New device role', + 'tags': [t.pk for t in tags], } cls.csv_data = ( @@ -1069,6 +1087,8 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase): Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturer), ]) + tags = create_tags('Alpha', 'Bravo', 'Charlie') + cls.form_data = { 'name': 'Platform X', 'slug': 'platform-x', @@ -1076,6 +1096,7 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase): 'napalm_driver': 'junos', 'napalm_args': None, 'description': 'A new platform', + 'tags': [t.pk for t in tags], } cls.csv_data = ( diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 183c45b2a..2b221fdab 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -9,7 +9,6 @@ from ipam.choices import * from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES from ipam.models import * from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField -from netbox.api.serializers import OrganizationalModelSerializer from netbox.api.serializers import PrimaryModelSerializer from tenancy.api.nested_serializers import NestedTenantSerializer from utilities.api import get_serializer_for_model @@ -66,14 +65,14 @@ class RouteTargetSerializer(PrimaryModelSerializer): # RIRs/aggregates # -class RIRSerializer(OrganizationalModelSerializer): +class RIRSerializer(PrimaryModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail') aggregate_count = serializers.IntegerField(read_only=True) class Meta: model = RIR fields = [ - 'id', 'url', 'display', 'name', 'slug', 'is_private', 'description', 'custom_fields', 'created', + 'id', 'url', 'display', 'name', 'slug', 'is_private', 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'aggregate_count', ] @@ -97,7 +96,7 @@ class AggregateSerializer(PrimaryModelSerializer): # VLANs # -class RoleSerializer(OrganizationalModelSerializer): +class RoleSerializer(PrimaryModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail') prefix_count = serializers.IntegerField(read_only=True) vlan_count = serializers.IntegerField(read_only=True) @@ -105,12 +104,12 @@ class RoleSerializer(OrganizationalModelSerializer): class Meta: model = Role fields = [ - 'id', 'url', 'display', 'name', 'slug', 'weight', 'description', 'custom_fields', 'created', 'last_updated', - 'prefix_count', 'vlan_count', + 'id', 'url', 'display', 'name', 'slug', 'weight', 'description', 'tags', 'custom_fields', 'created', + 'last_updated', 'prefix_count', 'vlan_count', ] -class VLANGroupSerializer(OrganizationalModelSerializer): +class VLANGroupSerializer(PrimaryModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail') scope_type = ContentTypeField( queryset=ContentType.objects.filter( @@ -126,8 +125,8 @@ class VLANGroupSerializer(OrganizationalModelSerializer): class Meta: model = VLANGroup fields = [ - 'id', 'url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'description', 'custom_fields', - 'created', 'last_updated', 'vlan_count', + 'id', 'url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'description', 'tags', + 'custom_fields', 'created', 'last_updated', 'vlan_count', ] validators = [] diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index 69b6d97f0..a043bd88c 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -48,7 +48,7 @@ class RouteTargetViewSet(CustomFieldModelViewSet): class RIRViewSet(CustomFieldModelViewSet): queryset = RIR.objects.annotate( aggregate_count=count_related(Aggregate, 'rir') - ) + ).prefetch_related('tags') serializer_class = serializers.RIRSerializer filterset_class = filtersets.RIRFilterSet @@ -71,7 +71,7 @@ class RoleViewSet(CustomFieldModelViewSet): queryset = Role.objects.annotate( prefix_count=count_related(Prefix, 'role'), vlan_count=count_related(VLAN, 'role') - ) + ).prefetch_related('tags') serializer_class = serializers.RoleSerializer filterset_class = filtersets.RoleFilterSet @@ -126,7 +126,7 @@ class IPAddressViewSet(CustomFieldModelViewSet): class VLANGroupViewSet(CustomFieldModelViewSet): queryset = VLANGroup.objects.annotate( vlan_count=count_related(VLAN, 'group') - ) + ).prefetch_related('tags') serializer_class = serializers.VLANGroupSerializer filterset_class = filtersets.VLANGroupFilterSet diff --git a/netbox/ipam/forms/bulk_edit.py b/netbox/ipam/forms/bulk_edit.py index 895dbe200..43bf40f88 100644 --- a/netbox/ipam/forms/bulk_edit.py +++ b/netbox/ipam/forms/bulk_edit.py @@ -71,7 +71,7 @@ class RouteTargetBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldMode ] -class RIRBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): +class RIRBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=RIR.objects.all(), widget=forms.MultipleHiddenInput @@ -120,7 +120,7 @@ class AggregateBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelB } -class RoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): +class RoleBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Role.objects.all(), widget=forms.MultipleHiddenInput @@ -280,7 +280,7 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelB ] -class VLANGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): +class VLANGroupBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=VLANGroup.objects.all(), widget=forms.MultipleHiddenInput diff --git a/netbox/ipam/forms/models.py b/netbox/ipam/forms/models.py index d28f7b3ae..a9c8a0910 100644 --- a/netbox/ipam/forms/models.py +++ b/netbox/ipam/forms/models.py @@ -82,11 +82,15 @@ class RouteTargetForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): class RIRForm(BootstrapMixin, CustomFieldModelForm): slug = SlugField() + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) class Meta: model = RIR fields = [ - 'name', 'slug', 'is_private', 'description', + 'name', 'slug', 'is_private', 'description', 'tags', ] @@ -120,11 +124,15 @@ class AggregateForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): class RoleForm(BootstrapMixin, CustomFieldModelForm): slug = SlugField() + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) class Meta: model = Role fields = [ - 'name', 'slug', 'weight', 'description', + 'name', 'slug', 'weight', 'description', 'tags', ] @@ -530,15 +538,19 @@ class VLANGroupForm(BootstrapMixin, CustomFieldModelForm): } ) slug = SlugField() + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) class Meta: model = VLANGroup fields = [ 'name', 'slug', 'description', 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', - 'clustergroup', 'cluster', + 'clustergroup', 'cluster', 'tags', ] fieldsets = ( - ('VLAN Group', ('name', 'slug', 'description')), + ('VLAN Group', ('name', 'slug', 'description', 'tags')), ('Scope', ('scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster')), ) widgets = { diff --git a/netbox/ipam/migrations/0051_extend_tag_support.py b/netbox/ipam/migrations/0051_extend_tag_support.py new file mode 100644 index 000000000..ea31a6645 --- /dev/null +++ b/netbox/ipam/migrations/0051_extend_tag_support.py @@ -0,0 +1,30 @@ +# Generated by Django 3.2.8 on 2021-10-21 14:50 + +from django.db import migrations +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0062_clear_secrets_changelog'), + ('ipam', '0050_iprange'), + ] + + operations = [ + migrations.AddField( + model_name='rir', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AddField( + model_name='role', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AddField( + model_name='vlangroup', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + ] diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index 4fc2b5dbb..514e87a62 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -31,7 +31,7 @@ __all__ = ( ) -@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class RIR(OrganizationalModel): """ A Regional Internet Registry (RIR) is responsible for the allocation of a large portion of the global IP address @@ -168,7 +168,7 @@ class Aggregate(PrimaryModel): return min(utilization, 100) -@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class Role(OrganizationalModel): """ A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or diff --git a/netbox/ipam/models/vlans.py b/netbox/ipam/models/vlans.py index 4ba8d7041..14eaa7ccc 100644 --- a/netbox/ipam/models/vlans.py +++ b/netbox/ipam/models/vlans.py @@ -21,7 +21,7 @@ __all__ = ( ) -@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class VLANGroup(OrganizationalModel): """ A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique. diff --git a/netbox/ipam/tables/ip.py b/netbox/ipam/tables/ip.py index ddad6c573..a2a0c67b1 100644 --- a/netbox/ipam/tables/ip.py +++ b/netbox/ipam/tables/ip.py @@ -85,11 +85,14 @@ class RIRTable(BaseTable): url_params={'rir_id': 'pk'}, verbose_name='Aggregates' ) + tags = TagColumn( + url_name='ipam:rir_list' + ) actions = ButtonsColumn(RIR) class Meta(BaseTable.Meta): model = RIR - fields = ('pk', 'name', 'slug', 'is_private', 'aggregate_count', 'description', 'actions') + fields = ('pk', 'name', 'slug', 'is_private', 'aggregate_count', 'description', 'tags', 'actions') default_columns = ('pk', 'name', 'is_private', 'aggregate_count', 'description', 'actions') @@ -144,11 +147,14 @@ class RoleTable(BaseTable): url_params={'role_id': 'pk'}, verbose_name='VLANs' ) + tags = TagColumn( + url_name='ipam:role_list' + ) actions = ButtonsColumn(Role) class Meta(BaseTable.Meta): model = Role - fields = ('pk', 'name', 'slug', 'prefix_count', 'vlan_count', 'description', 'weight', 'actions') + fields = ('pk', 'name', 'slug', 'prefix_count', 'vlan_count', 'description', 'weight', 'tags', 'actions') default_columns = ('pk', 'name', 'prefix_count', 'vlan_count', 'description', 'actions') diff --git a/netbox/ipam/tables/vlans.py b/netbox/ipam/tables/vlans.py index fd1e92be8..4c0d5d729 100644 --- a/netbox/ipam/tables/vlans.py +++ b/netbox/ipam/tables/vlans.py @@ -74,6 +74,9 @@ class VLANGroupTable(BaseTable): url_params={'group_id': 'pk'}, verbose_name='VLANs' ) + tags = TagColumn( + url_name='ipam:vlangroup_list' + ) actions = ButtonsColumn( model=VLANGroup, prepend_template=VLANGROUP_ADD_VLAN @@ -81,7 +84,7 @@ class VLANGroupTable(BaseTable): class Meta(BaseTable.Meta): model = VLANGroup - fields = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'slug', 'description', 'actions') + fields = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'slug', 'description', 'tags', 'actions') default_columns = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'description', 'actions') diff --git a/netbox/ipam/tests/test_views.py b/netbox/ipam/tests/test_views.py index 2a0bfdf32..5440efcb6 100644 --- a/netbox/ipam/tests/test_views.py +++ b/netbox/ipam/tests/test_views.py @@ -104,11 +104,14 @@ class RIRTestCase(ViewTestCases.OrganizationalObjectViewTestCase): RIR(name='RIR 3', slug='rir-3'), ]) + tags = create_tags('Alpha', 'Bravo', 'Charlie') + cls.form_data = { 'name': 'RIR X', 'slug': 'rir-x', 'is_private': True, 'description': 'A new RIR', + 'tags': [t.pk for t in tags], } cls.csv_data = ( @@ -177,11 +180,14 @@ class RoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase): Role(name='Role 3', slug='role-3'), ]) + tags = create_tags('Alpha', 'Bravo', 'Charlie') + cls.form_data = { 'name': 'Role X', 'slug': 'role-x', 'weight': 200, 'description': 'A new role', + 'tags': [t.pk for t in tags], } cls.csv_data = ( @@ -384,10 +390,13 @@ class VLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase): VLANGroup(name='VLAN Group 3', slug='vlan-group-3', scope=sites[0]), ]) + tags = create_tags('Alpha', 'Bravo', 'Charlie') + cls.form_data = { 'name': 'VLAN Group X', 'slug': 'vlan-group-x', 'description': 'A new VLAN group', + 'tags': [t.pk for t in tags], } cls.csv_data = ( diff --git a/netbox/netbox/api/serializers.py b/netbox/netbox/api/serializers.py index d17751e25..9f51d475d 100644 --- a/netbox/netbox/api/serializers.py +++ b/netbox/netbox/api/serializers.py @@ -147,13 +147,6 @@ class NestedTagSerializer(WritableNestedSerializer): # Base model serializers # -class OrganizationalModelSerializer(CustomFieldModelSerializer): - """ - Adds support for custom fields. - """ - pass - - class PrimaryModelSerializer(CustomFieldModelSerializer): """ Adds support for custom fields and tags. @@ -189,9 +182,9 @@ class PrimaryModelSerializer(CustomFieldModelSerializer): return instance -class NestedGroupModelSerializer(CustomFieldModelSerializer): +class NestedGroupModelSerializer(PrimaryModelSerializer): """ - Extends OrganizationalModelSerializer to include MPTT support. + Extends PrimaryModelSerializer to include MPTT support. """ _depth = serializers.IntegerField(source='level', read_only=True) diff --git a/netbox/netbox/graphql/types.py b/netbox/netbox/graphql/types.py index 181b9a0c6..7d71bd1fb 100644 --- a/netbox/netbox/graphql/types.py +++ b/netbox/netbox/graphql/types.py @@ -41,6 +41,7 @@ class ObjectType( class OrganizationalObjectType( ChangelogMixin, CustomFieldsMixin, + TagsMixin, BaseObjectType ): """ diff --git a/netbox/netbox/models.py b/netbox/netbox/models.py index 317548921..95cea6a93 100644 --- a/netbox/netbox/models.py +++ b/netbox/netbox/models.py @@ -143,6 +143,18 @@ class CustomValidationMixin(models.Model): post_clean.send(sender=self.__class__, instance=self) +class TagsMixin(models.Model): + """ + Enable the assignment of Tags. + """ + tags = TaggableManager( + through='extras.TaggedItem' + ) + + class Meta: + abstract = True + + # # Base model classes @@ -166,7 +178,7 @@ class ChangeLoggedModel(ChangeLoggingMixin, CustomValidationMixin, BigIDModel): abstract = True -class PrimaryModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, BigIDModel): +class PrimaryModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, TagsMixin, BigIDModel): """ Primary models represent real objects within the infrastructure being modeled. """ @@ -175,15 +187,12 @@ class PrimaryModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, object_id_field='assigned_object_id', content_type_field='assigned_object_type' ) - tags = TaggableManager( - through='extras.TaggedItem' - ) class Meta: abstract = True -class NestedGroupModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, BigIDModel, MPTTModel): +class NestedGroupModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, TagsMixin, BigIDModel, MPTTModel): """ Base model for objects which are used to form a hierarchy (regions, locations, etc.). These models nest recursively using MPTT. Within each parent, each child instance must have a unique name. @@ -225,7 +234,7 @@ class NestedGroupModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMi }) -class OrganizationalModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, BigIDModel): +class OrganizationalModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, TagsMixin, BigIDModel): """ Organizational models are those which are used solely to categorize and qualify other objects, and do not convey any real information about the infrastructure being modeled (for example, functional device roles). Organizational diff --git a/netbox/tenancy/api/serializers.py b/netbox/tenancy/api/serializers.py index 27a14b350..90c13725c 100644 --- a/netbox/tenancy/api/serializers.py +++ b/netbox/tenancy/api/serializers.py @@ -2,7 +2,7 @@ from django.contrib.auth.models import ContentType from rest_framework import serializers from netbox.api import ChoiceField, ContentTypeField -from netbox.api.serializers import NestedGroupModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer +from netbox.api.serializers import NestedGroupModelSerializer, PrimaryModelSerializer from tenancy.choices import ContactPriorityChoices from tenancy.models import * from .nested_serializers import * @@ -20,8 +20,8 @@ class TenantGroupSerializer(NestedGroupModelSerializer): class Meta: model = TenantGroup fields = [ - 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated', - 'tenant_count', '_depth', + 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', + 'last_updated', 'tenant_count', '_depth', ] @@ -60,18 +60,18 @@ class ContactGroupSerializer(NestedGroupModelSerializer): class Meta: model = ContactGroup fields = [ - 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated', - 'contact_count', '_depth', + 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', + 'last_updated', 'contact_count', '_depth', ] -class ContactRoleSerializer(OrganizationalModelSerializer): +class ContactRoleSerializer(PrimaryModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactrole-detail') class Meta: model = ContactRole fields = [ - 'id', 'url', 'display', 'name', 'slug', 'description', 'custom_fields', 'created', 'last_updated', + 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', ] diff --git a/netbox/tenancy/api/views.py b/netbox/tenancy/api/views.py index 7ce16c143..8c7c33aba 100644 --- a/netbox/tenancy/api/views.py +++ b/netbox/tenancy/api/views.py @@ -30,7 +30,7 @@ class TenantGroupViewSet(CustomFieldModelViewSet): 'group', 'tenant_count', cumulative=True - ) + ).prefetch_related('tags') serializer_class = serializers.TenantGroupSerializer filterset_class = filtersets.TenantGroupFilterSet @@ -64,28 +64,24 @@ class ContactGroupViewSet(CustomFieldModelViewSet): 'group', 'contact_count', cumulative=True - ) + ).prefetch_related('tags') serializer_class = serializers.ContactGroupSerializer filterset_class = filtersets.ContactGroupFilterSet class ContactRoleViewSet(CustomFieldModelViewSet): - queryset = ContactRole.objects.all() + queryset = ContactRole.objects.prefetch_related('tags') serializer_class = serializers.ContactRoleSerializer filterset_class = filtersets.ContactRoleFilterSet class ContactViewSet(CustomFieldModelViewSet): - queryset = Contact.objects.prefetch_related( - 'group', 'tags' - ) + queryset = Contact.objects.prefetch_related('group', 'tags') serializer_class = serializers.ContactSerializer filterset_class = filtersets.ContactFilterSet class ContactAssignmentViewSet(CustomFieldModelViewSet): - queryset = ContactAssignment.objects.prefetch_related( - 'contact', 'role' - ) + queryset = ContactAssignment.objects.prefetch_related('contact', 'role') serializer_class = serializers.ContactAssignmentSerializer filterset_class = filtersets.ContactAssignmentFilterSet diff --git a/netbox/tenancy/forms/bulk_edit.py b/netbox/tenancy/forms/bulk_edit.py index a34b8def1..f461fe73c 100644 --- a/netbox/tenancy/forms/bulk_edit.py +++ b/netbox/tenancy/forms/bulk_edit.py @@ -17,7 +17,7 @@ __all__ = ( # Tenants # -class TenantGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): +class TenantGroupBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=TenantGroup.objects.all(), widget=forms.MultipleHiddenInput @@ -55,7 +55,7 @@ class TenantBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulk # Contacts # -class ContactGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): +class ContactGroupBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=ContactGroup.objects.all(), widget=forms.MultipleHiddenInput @@ -73,7 +73,7 @@ class ContactGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): nullable_fields = ['parent', 'description'] -class ContactRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): +class ContactRoleBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=ContactRole.objects.all(), widget=forms.MultipleHiddenInput diff --git a/netbox/tenancy/forms/models.py b/netbox/tenancy/forms/models.py index b15065705..0237e4ef8 100644 --- a/netbox/tenancy/forms/models.py +++ b/netbox/tenancy/forms/models.py @@ -28,11 +28,15 @@ class TenantGroupForm(BootstrapMixin, CustomFieldModelForm): required=False ) slug = SlugField() + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) class Meta: model = TenantGroup fields = [ - 'parent', 'name', 'slug', 'description', + 'parent', 'name', 'slug', 'description', 'tags', ] @@ -68,18 +72,26 @@ class ContactGroupForm(BootstrapMixin, CustomFieldModelForm): required=False ) slug = SlugField() + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) class Meta: model = ContactGroup - fields = ['parent', 'name', 'slug', 'description'] + fields = ('parent', 'name', 'slug', 'description', 'tags') class ContactRoleForm(BootstrapMixin, CustomFieldModelForm): slug = SlugField() + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) class Meta: model = ContactRole - fields = ['name', 'slug', 'description'] + fields = ('name', 'slug', 'description', 'tags') class ContactForm(BootstrapMixin, CustomFieldModelForm): diff --git a/netbox/tenancy/migrations/0004_extend_tag_support.py b/netbox/tenancy/migrations/0004_extend_tag_support.py new file mode 100644 index 000000000..942be38b5 --- /dev/null +++ b/netbox/tenancy/migrations/0004_extend_tag_support.py @@ -0,0 +1,30 @@ +# Generated by Django 3.2.8 on 2021-10-21 14:50 + +from django.db import migrations +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0062_clear_secrets_changelog'), + ('tenancy', '0003_contacts'), + ] + + operations = [ + migrations.AddField( + model_name='contactgroup', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AddField( + model_name='contactrole', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AddField( + model_name='tenantgroup', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + ] diff --git a/netbox/tenancy/models.py b/netbox/tenancy/models.py index c709236e2..01ea2d0d5 100644 --- a/netbox/tenancy/models.py +++ b/netbox/tenancy/models.py @@ -24,7 +24,7 @@ __all__ = ( # Tenants # -@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class TenantGroup(NestedGroupModel): """ An arbitrary collection of Tenants. @@ -111,7 +111,7 @@ class Tenant(PrimaryModel): # Contacts # -@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class ContactGroup(NestedGroupModel): """ An arbitrary collection of Contacts. @@ -145,7 +145,7 @@ class ContactGroup(NestedGroupModel): return reverse('tenancy:contactgroup', args=[self.pk]) -@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class ContactRole(OrganizationalModel): """ Functional role for a Contact assigned to an object. diff --git a/netbox/tenancy/tables.py b/netbox/tenancy/tables.py index 5b254842b..02c431846 100644 --- a/netbox/tenancy/tables.py +++ b/netbox/tenancy/tables.py @@ -55,11 +55,14 @@ class TenantGroupTable(BaseTable): url_params={'group_id': 'pk'}, verbose_name='Tenants' ) + tags = TagColumn( + url_name='tenancy:tenantgroup_list' + ) actions = ButtonsColumn(TenantGroup) class Meta(BaseTable.Meta): model = TenantGroup - fields = ('pk', 'name', 'tenant_count', 'description', 'slug', 'actions') + fields = ('pk', 'name', 'tenant_count', 'description', 'slug', 'tags', 'actions') default_columns = ('pk', 'name', 'tenant_count', 'description', 'actions') @@ -96,11 +99,14 @@ class ContactGroupTable(BaseTable): url_params={'role_id': 'pk'}, verbose_name='Contacts' ) + tags = TagColumn( + url_name='tenancy:contactgroup_list' + ) actions = ButtonsColumn(ContactGroup) class Meta(BaseTable.Meta): model = ContactGroup - fields = ('pk', 'name', 'contact_count', 'description', 'slug', 'actions') + fields = ('pk', 'name', 'contact_count', 'description', 'slug', 'tags', 'actions') default_columns = ('pk', 'name', 'contact_count', 'description', 'actions') diff --git a/netbox/tenancy/tests/test_views.py b/netbox/tenancy/tests/test_views.py index fb7ff3ce3..dcfcc1652 100644 --- a/netbox/tenancy/tests/test_views.py +++ b/netbox/tenancy/tests/test_views.py @@ -16,10 +16,13 @@ class TenantGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase): for tenanantgroup in tenant_groups: tenanantgroup.save() + tags = create_tags('Alpha', 'Bravo', 'Charlie') + cls.form_data = { 'name': 'Tenant Group X', 'slug': 'tenant-group-x', 'description': 'A new tenant group', + 'tags': [t.pk for t in tags], } cls.csv_data = ( @@ -90,10 +93,13 @@ class ContactGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase): for tenanantgroup in contact_groups: tenanantgroup.save() + tags = create_tags('Alpha', 'Bravo', 'Charlie') + cls.form_data = { 'name': 'Contact Group X', 'slug': 'contact-group-x', 'description': 'A new contact group', + 'tags': [t.pk for t in tags], } cls.csv_data = ( @@ -120,10 +126,13 @@ class ContactRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase): ContactRole(name='Contact Role 3', slug='contact-role-3'), ]) + tags = create_tags('Alpha', 'Bravo', 'Charlie') + cls.form_data = { 'name': 'Devie Role X', 'slug': 'contact-role-x', 'description': 'New contact role', + 'tags': [t.pk for t in tags], } cls.csv_data = ( diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py index 1928960a9..ef8c975d3 100644 --- a/netbox/virtualization/api/serializers.py +++ b/netbox/virtualization/api/serializers.py @@ -6,7 +6,7 @@ from dcim.choices import InterfaceModeChoices from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer from ipam.models import VLAN from netbox.api import ChoiceField, SerializedPKRelatedField -from netbox.api.serializers import OrganizationalModelSerializer, PrimaryModelSerializer +from netbox.api.serializers import PrimaryModelSerializer from tenancy.api.nested_serializers import NestedTenantSerializer from virtualization.choices import * from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface @@ -17,26 +17,26 @@ from .nested_serializers import * # Clusters # -class ClusterTypeSerializer(OrganizationalModelSerializer): +class ClusterTypeSerializer(PrimaryModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail') cluster_count = serializers.IntegerField(read_only=True) class Meta: model = ClusterType fields = [ - 'id', 'url', 'display', 'name', 'slug', 'description', 'custom_fields', 'created', 'last_updated', + 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'cluster_count', ] -class ClusterGroupSerializer(OrganizationalModelSerializer): +class ClusterGroupSerializer(PrimaryModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail') cluster_count = serializers.IntegerField(read_only=True) class Meta: model = ClusterGroup fields = [ - 'id', 'url', 'display', 'name', 'slug', 'description', 'custom_fields', 'created', 'last_updated', + 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'cluster_count', ] diff --git a/netbox/virtualization/api/views.py b/netbox/virtualization/api/views.py index 8eebd2120..d07ace3d5 100644 --- a/netbox/virtualization/api/views.py +++ b/netbox/virtualization/api/views.py @@ -23,7 +23,7 @@ class VirtualizationRootView(APIRootView): class ClusterTypeViewSet(CustomFieldModelViewSet): queryset = ClusterType.objects.annotate( cluster_count=count_related(Cluster, 'type') - ) + ).prefetch_related('tags') serializer_class = serializers.ClusterTypeSerializer filterset_class = filtersets.ClusterTypeFilterSet @@ -31,7 +31,7 @@ class ClusterTypeViewSet(CustomFieldModelViewSet): class ClusterGroupViewSet(CustomFieldModelViewSet): queryset = ClusterGroup.objects.annotate( cluster_count=count_related(Cluster, 'group') - ) + ).prefetch_related('tags') serializer_class = serializers.ClusterGroupSerializer filterset_class = filtersets.ClusterGroupFilterSet diff --git a/netbox/virtualization/forms/bulk_edit.py b/netbox/virtualization/forms/bulk_edit.py index c140fbc73..d18d432cd 100644 --- a/netbox/virtualization/forms/bulk_edit.py +++ b/netbox/virtualization/forms/bulk_edit.py @@ -23,7 +23,7 @@ __all__ = ( ) -class ClusterTypeBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): +class ClusterTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=ClusterType.objects.all(), widget=forms.MultipleHiddenInput @@ -37,7 +37,7 @@ class ClusterTypeBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): nullable_fields = ['description'] -class ClusterGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): +class ClusterGroupBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=ClusterGroup.objects.all(), widget=forms.MultipleHiddenInput diff --git a/netbox/virtualization/forms/models.py b/netbox/virtualization/forms/models.py index d66bc9f1f..88ebc9e83 100644 --- a/netbox/virtualization/forms/models.py +++ b/netbox/virtualization/forms/models.py @@ -28,22 +28,30 @@ __all__ = ( class ClusterTypeForm(BootstrapMixin, CustomFieldModelForm): slug = SlugField() + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) class Meta: model = ClusterType - fields = [ - 'name', 'slug', 'description', - ] + fields = ( + 'name', 'slug', 'description', 'tags', + ) class ClusterGroupForm(BootstrapMixin, CustomFieldModelForm): slug = SlugField() + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) class Meta: model = ClusterGroup - fields = [ - 'name', 'slug', 'description', - ] + fields = ( + 'name', 'slug', 'description', 'tags', + ) class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): diff --git a/netbox/virtualization/migrations/0025_extend_tag_support.py b/netbox/virtualization/migrations/0025_extend_tag_support.py new file mode 100644 index 000000000..c77aee194 --- /dev/null +++ b/netbox/virtualization/migrations/0025_extend_tag_support.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.8 on 2021-10-21 14:50 + +from django.db import migrations +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0062_clear_secrets_changelog'), + ('virtualization', '0024_cluster_relax_uniqueness'), + ] + + operations = [ + migrations.AddField( + model_name='clustergroup', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AddField( + model_name='clustertype', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + ] diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index 11792944a..bd64f56cf 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -30,7 +30,7 @@ __all__ = ( # Cluster types # -@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class ClusterType(OrganizationalModel): """ A type of Cluster. @@ -64,7 +64,7 @@ class ClusterType(OrganizationalModel): # Cluster groups # -@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') +@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class ClusterGroup(OrganizationalModel): """ An organizational group of Clusters. diff --git a/netbox/virtualization/tables.py b/netbox/virtualization/tables.py index b0e922e71..64b376e1d 100644 --- a/netbox/virtualization/tables.py +++ b/netbox/virtualization/tables.py @@ -40,11 +40,14 @@ class ClusterTypeTable(BaseTable): cluster_count = tables.Column( verbose_name='Clusters' ) + tags = TagColumn( + url_name='virtualization:clustertype_list' + ) actions = ButtonsColumn(ClusterType) class Meta(BaseTable.Meta): model = ClusterType - fields = ('pk', 'name', 'slug', 'cluster_count', 'description', 'actions') + fields = ('pk', 'name', 'slug', 'cluster_count', 'description', 'tags', 'actions') default_columns = ('pk', 'name', 'cluster_count', 'description', 'actions') @@ -60,11 +63,14 @@ class ClusterGroupTable(BaseTable): cluster_count = tables.Column( verbose_name='Clusters' ) + tags = TagColumn( + url_name='virtualization:clustergroup_list' + ) actions = ButtonsColumn(ClusterGroup) class Meta(BaseTable.Meta): model = ClusterGroup - fields = ('pk', 'name', 'slug', 'cluster_count', 'description', 'actions') + fields = ('pk', 'name', 'slug', 'cluster_count', 'description', 'tags', 'actions') default_columns = ('pk', 'name', 'cluster_count', 'description', 'actions') diff --git a/netbox/virtualization/tests/test_views.py b/netbox/virtualization/tests/test_views.py index 020c9ebc5..138b1afae 100644 --- a/netbox/virtualization/tests/test_views.py +++ b/netbox/virtualization/tests/test_views.py @@ -22,10 +22,13 @@ class ClusterGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase): ClusterGroup(name='Cluster Group 3', slug='cluster-group-3'), ]) + tags = create_tags('Alpha', 'Bravo', 'Charlie') + cls.form_data = { 'name': 'Cluster Group X', 'slug': 'cluster-group-x', 'description': 'A new cluster group', + 'tags': [t.pk for t in tags], } cls.csv_data = ( @@ -52,10 +55,13 @@ class ClusterTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase): ClusterType(name='Cluster Type 3', slug='cluster-type-3'), ]) + tags = create_tags('Alpha', 'Bravo', 'Charlie') + cls.form_data = { 'name': 'Cluster Type X', 'slug': 'cluster-type-x', 'description': 'A new cluster type', + 'tags': [t.pk for t in tags], } cls.csv_data = ( From 6f05f17c62ee075a7a997554e981ad81489ec2f6 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 21 Oct 2021 11:23:31 -0400 Subject: [PATCH 34/35] Standardize & simplify tags panel inclusion --- netbox/templates/circuits/circuit.html | 2 +- netbox/templates/circuits/circuittype.html | 1 + netbox/templates/circuits/provider.html | 2 +- netbox/templates/circuits/providernetwork.html | 2 +- netbox/templates/dcim/cable.html | 2 +- netbox/templates/dcim/consoleport.html | 2 +- netbox/templates/dcim/consoleserverport.html | 2 +- netbox/templates/dcim/device.html | 2 +- netbox/templates/dcim/devicebay.html | 2 +- netbox/templates/dcim/devicerole.html | 1 + netbox/templates/dcim/devicetype.html | 2 +- netbox/templates/dcim/frontport.html | 2 +- netbox/templates/dcim/interface.html | 2 +- netbox/templates/dcim/inventoryitem.html | 2 +- netbox/templates/dcim/location.html | 1 + netbox/templates/dcim/manufacturer.html | 1 + netbox/templates/dcim/platform.html | 1 + netbox/templates/dcim/powerfeed.html | 2 +- netbox/templates/dcim/poweroutlet.html | 2 +- netbox/templates/dcim/powerpanel.html | 2 +- netbox/templates/dcim/powerport.html | 2 +- netbox/templates/dcim/rack.html | 2 +- netbox/templates/dcim/rackreservation.html | 2 +- netbox/templates/dcim/rackrole.html | 1 + netbox/templates/dcim/rearport.html | 2 +- netbox/templates/dcim/region.html | 1 + netbox/templates/dcim/site.html | 3 +-- netbox/templates/dcim/sitegroup.html | 1 + netbox/templates/dcim/virtualchassis.html | 2 +- netbox/templates/inc/panels/tags.html | 15 +++++++++------ netbox/templates/ipam/aggregate.html | 2 +- netbox/templates/ipam/ipaddress.html | 2 +- netbox/templates/ipam/iprange.html | 2 +- netbox/templates/ipam/prefix.html | 2 +- netbox/templates/ipam/rir.html | 1 + netbox/templates/ipam/role.html | 1 + netbox/templates/ipam/routetarget.html | 2 +- netbox/templates/ipam/service.html | 2 +- netbox/templates/ipam/vlan.html | 2 +- netbox/templates/ipam/vlangroup.html | 1 + netbox/templates/ipam/vrf.html | 2 +- netbox/templates/tenancy/contact.html | 2 +- netbox/templates/tenancy/contactgroup.html | 1 + netbox/templates/tenancy/contactrole.html | 1 + netbox/templates/tenancy/tenant.html | 2 +- netbox/templates/tenancy/tenantgroup.html | 1 + netbox/templates/virtualization/cluster.html | 2 +- netbox/templates/virtualization/clustergroup.html | 1 + netbox/templates/virtualization/clustertype.html | 1 + .../templates/virtualization/virtualmachine.html | 2 +- netbox/templates/virtualization/vminterface.html | 4 ++-- 51 files changed, 60 insertions(+), 42 deletions(-) diff --git a/netbox/templates/circuits/circuit.html b/netbox/templates/circuits/circuit.html index b61dac6fc..22713b592 100644 --- a/netbox/templates/circuits/circuit.html +++ b/netbox/templates/circuits/circuit.html @@ -65,7 +65,7 @@
{% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' with tags=object.tags.all url='circuits:circuit_list' %} + {% include 'inc/panels/tags.html' %} {% include 'inc/panels/comments.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/circuits/circuittype.html b/netbox/templates/circuits/circuittype.html index ad81de7e1..57737a6d1 100644 --- a/netbox/templates/circuits/circuittype.html +++ b/netbox/templates/circuits/circuittype.html @@ -28,6 +28,7 @@
+ {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/circuits/provider.html b/netbox/templates/circuits/provider.html index d353e4f37..c16afa421 100644 --- a/netbox/templates/circuits/provider.html +++ b/netbox/templates/circuits/provider.html @@ -47,7 +47,7 @@
- {% include 'inc/panels/tags.html' with tags=object.tags.all url='circuits:provider_list' %} + {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/circuits/providernetwork.html b/netbox/templates/circuits/providernetwork.html index 18a11e115..9641c9934 100644 --- a/netbox/templates/circuits/providernetwork.html +++ b/netbox/templates/circuits/providernetwork.html @@ -38,7 +38,7 @@
{% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' with tags=object.tags.all url='circuits:providernetwork_list' %} + {% include 'inc/panels/tags.html' %} {% include 'inc/panels/comments.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/dcim/cable.html b/netbox/templates/dcim/cable.html index c5d1f7906..00704e6ca 100644 --- a/netbox/templates/dcim/cable.html +++ b/netbox/templates/dcim/cable.html @@ -64,7 +64,7 @@ {% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' with tags=object.tags.all url='dcim:cable_list' %} + {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/consoleport.html b/netbox/templates/dcim/consoleport.html index c340cbc5c..60711eb9d 100644 --- a/netbox/templates/dcim/consoleport.html +++ b/netbox/templates/dcim/consoleport.html @@ -41,7 +41,7 @@
{% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' with tags=object.tags.all %} + {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/consoleserverport.html b/netbox/templates/dcim/consoleserverport.html index 91de60252..f65af3285 100644 --- a/netbox/templates/dcim/consoleserverport.html +++ b/netbox/templates/dcim/consoleserverport.html @@ -41,7 +41,7 @@
{% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' with tags=object.tags.all %} + {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 869ab1ec7..ea0c795c5 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -221,7 +221,7 @@
{% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' with tags=object.tags.all url='dcim:device_list' %} + {% include 'inc/panels/tags.html' %} {% include 'inc/panels/comments.html' %} {% plugin_left_page object %} diff --git a/netbox/templates/dcim/devicebay.html b/netbox/templates/dcim/devicebay.html index 918b6b022..ff8f90db2 100644 --- a/netbox/templates/dcim/devicebay.html +++ b/netbox/templates/dcim/devicebay.html @@ -33,7 +33,7 @@ {% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' with tags=object.tags.all %} + {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/devicerole.html b/netbox/templates/dcim/devicerole.html index 2c2d7fe6f..22385ae27 100644 --- a/netbox/templates/dcim/devicerole.html +++ b/netbox/templates/dcim/devicerole.html @@ -58,6 +58,7 @@
+ {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/devicetype.html b/netbox/templates/dcim/devicetype.html index 74a3e73d7..21a04e7d0 100644 --- a/netbox/templates/dcim/devicetype.html +++ b/netbox/templates/dcim/devicetype.html @@ -88,7 +88,7 @@
{% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' with tags=object.tags.all url='dcim:devicetype_list' %} + {% include 'inc/panels/tags.html' %} {% include 'inc/panels/comments.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/dcim/frontport.html b/netbox/templates/dcim/frontport.html index c6b6cea48..6cc3d482f 100644 --- a/netbox/templates/dcim/frontport.html +++ b/netbox/templates/dcim/frontport.html @@ -53,7 +53,7 @@ {% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' with tags=object.tags.all %} + {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index 0715bec58..af038326d 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -103,7 +103,7 @@
{% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' with tags=object.tags.all %} + {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/inventoryitem.html b/netbox/templates/dcim/inventoryitem.html index e55d441d4..163d8edb3 100644 --- a/netbox/templates/dcim/inventoryitem.html +++ b/netbox/templates/dcim/inventoryitem.html @@ -65,7 +65,7 @@
{% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' with tags=object.tags.all %} + {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/location.html b/netbox/templates/dcim/location.html index eeb891daf..434253d43 100644 --- a/netbox/templates/dcim/location.html +++ b/netbox/templates/dcim/location.html @@ -68,6 +68,7 @@
+ {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/manufacturer.html b/netbox/templates/dcim/manufacturer.html index 792a3e127..d43a206c6 100644 --- a/netbox/templates/dcim/manufacturer.html +++ b/netbox/templates/dcim/manufacturer.html @@ -34,6 +34,7 @@
+ {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/platform.html b/netbox/templates/dcim/platform.html index bbdf809dd..8cd26a116 100644 --- a/netbox/templates/dcim/platform.html +++ b/netbox/templates/dcim/platform.html @@ -55,6 +55,7 @@
+ {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/powerfeed.html b/netbox/templates/dcim/powerfeed.html index f29a127e3..1824cac19 100644 --- a/netbox/templates/dcim/powerfeed.html +++ b/netbox/templates/dcim/powerfeed.html @@ -108,7 +108,7 @@
{% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' with tags=object.tags.all url='dcim:powerfeed_list' %} + {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/poweroutlet.html b/netbox/templates/dcim/poweroutlet.html index 1f960e0d5..396ef42a8 100644 --- a/netbox/templates/dcim/poweroutlet.html +++ b/netbox/templates/dcim/poweroutlet.html @@ -45,7 +45,7 @@
{% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' with tags=object.tags.all %} + {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/powerpanel.html b/netbox/templates/dcim/powerpanel.html index a99aabf32..021fa1133 100644 --- a/netbox/templates/dcim/powerpanel.html +++ b/netbox/templates/dcim/powerpanel.html @@ -39,7 +39,7 @@
- {% include 'inc/panels/tags.html' with tags=object.tags.all url='dcim:powerpanel_list' %} + {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/powerport.html b/netbox/templates/dcim/powerport.html index 74ad9603b..dfe428c50 100644 --- a/netbox/templates/dcim/powerport.html +++ b/netbox/templates/dcim/powerport.html @@ -45,7 +45,7 @@
{% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' with tags=object.tags.all %} + {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index 586d31771..93bd21fd9 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -163,7 +163,7 @@
{% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' with tags=object.tags.all url='dcim:rack_list' %} + {% include 'inc/panels/tags.html' %} {% include 'inc/panels/comments.html' %} {% if power_feeds %}
diff --git a/netbox/templates/dcim/rackreservation.html b/netbox/templates/dcim/rackreservation.html index 07ca55f7c..1e16af675 100644 --- a/netbox/templates/dcim/rackreservation.html +++ b/netbox/templates/dcim/rackreservation.html @@ -84,7 +84,7 @@
{% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' with tags=object.tags.all url='dcim:rackreservation_list' %} + {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/rackrole.html b/netbox/templates/dcim/rackrole.html index 2668905f4..2f4661c9f 100644 --- a/netbox/templates/dcim/rackrole.html +++ b/netbox/templates/dcim/rackrole.html @@ -34,6 +34,7 @@
+ {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/rearport.html b/netbox/templates/dcim/rearport.html index b60e04882..b3ecce3ad 100644 --- a/netbox/templates/dcim/rearport.html +++ b/netbox/templates/dcim/rearport.html @@ -47,7 +47,7 @@
{% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' with tags=object.tags.all %} + {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/dcim/region.html b/netbox/templates/dcim/region.html index c03b11e7d..7452e594e 100644 --- a/netbox/templates/dcim/region.html +++ b/netbox/templates/dcim/region.html @@ -45,6 +45,7 @@
+ {% include 'inc/panels/tags.html' %} {% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/contacts.html' %} {% plugin_left_page object %} diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html index 8442ae41e..a17c505a9 100644 --- a/netbox/templates/dcim/site.html +++ b/netbox/templates/dcim/site.html @@ -169,7 +169,6 @@
- {{ object.contact_email }} {% else %} @@ -181,7 +180,7 @@ {% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' with tags=object.tags.all url='dcim:site_list' %} + {% include 'inc/panels/tags.html' %} {% include 'inc/panels/comments.html' %} {% plugin_left_page object %} diff --git a/netbox/templates/dcim/sitegroup.html b/netbox/templates/dcim/sitegroup.html index dbee2c835..d04330413 100644 --- a/netbox/templates/dcim/sitegroup.html +++ b/netbox/templates/dcim/sitegroup.html @@ -45,6 +45,7 @@ + {% include 'inc/panels/tags.html' %} {% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/contacts.html' %} {% plugin_left_page object %} diff --git a/netbox/templates/dcim/virtualchassis.html b/netbox/templates/dcim/virtualchassis.html index fd31be60d..8399576f5 100644 --- a/netbox/templates/dcim/virtualchassis.html +++ b/netbox/templates/dcim/virtualchassis.html @@ -39,7 +39,7 @@ {% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' with tags=object.tags.all url='dcim:virtualchassis_list' %} + {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/inc/panels/tags.html b/netbox/templates/inc/panels/tags.html index e67098c0f..c309afdf0 100644 --- a/netbox/templates/inc/panels/tags.html +++ b/netbox/templates/inc/panels/tags.html @@ -1,11 +1,14 @@ {% load helpers %} +
-
- Tags -
+
Tags
- {% for tag in tags.all %} {% tag tag url %} {% empty %} - No tags assigned - {% endfor %} + {% with url=object|validated_viewname:"list" %} + {% for tag in object.tags.all %} + {% tag tag url %} + {% empty %} + No tags assigned + {% endfor %} + {% endwith %}
diff --git a/netbox/templates/ipam/aggregate.html b/netbox/templates/ipam/aggregate.html index 202b6e41c..aca89a526 100644 --- a/netbox/templates/ipam/aggregate.html +++ b/netbox/templates/ipam/aggregate.html @@ -65,7 +65,7 @@
{% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:aggregate_list' %} + {% include 'inc/panels/tags.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/ipam/ipaddress.html b/netbox/templates/ipam/ipaddress.html index d98544de4..31782bdd7 100644 --- a/netbox/templates/ipam/ipaddress.html +++ b/netbox/templates/ipam/ipaddress.html @@ -145,7 +145,7 @@
- {% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:ipaddress_list' %} + {% include 'inc/panels/tags.html' %}
diff --git a/netbox/templates/ipam/iprange.html b/netbox/templates/ipam/iprange.html index e3d37a87a..b549ec7c5 100644 --- a/netbox/templates/ipam/iprange.html +++ b/netbox/templates/ipam/iprange.html @@ -82,7 +82,7 @@ {% plugin_left_page object %}
- {% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:prefix_list' %} + {% include 'inc/panels/tags.html' %} {% include 'inc/panels/custom_fields.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/ipam/prefix.html b/netbox/templates/ipam/prefix.html index 877ed49e0..eaea4e1ec 100644 --- a/netbox/templates/ipam/prefix.html +++ b/netbox/templates/ipam/prefix.html @@ -122,7 +122,7 @@
{% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:prefix_list' %} + {% include 'inc/panels/tags.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/ipam/rir.html b/netbox/templates/ipam/rir.html index 26d5e71da..c2f88c278 100644 --- a/netbox/templates/ipam/rir.html +++ b/netbox/templates/ipam/rir.html @@ -38,6 +38,7 @@ + {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/ipam/role.html b/netbox/templates/ipam/role.html index 7fc967047..5579010fa 100644 --- a/netbox/templates/ipam/role.html +++ b/netbox/templates/ipam/role.html @@ -32,6 +32,7 @@
+ {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/ipam/routetarget.html b/netbox/templates/ipam/routetarget.html index f615d2d50..71d6f9601 100644 --- a/netbox/templates/ipam/routetarget.html +++ b/netbox/templates/ipam/routetarget.html @@ -30,7 +30,7 @@
- {% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:routetarget_list' %} + {% include 'inc/panels/tags.html' %} {% include 'inc/panels/custom_fields.html' %} {% plugin_left_page object %} diff --git a/netbox/templates/ipam/service.html b/netbox/templates/ipam/service.html index 7609a280b..5a47e44f0 100644 --- a/netbox/templates/ipam/service.html +++ b/netbox/templates/ipam/service.html @@ -61,7 +61,7 @@
{% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:service_list' %} + {% include 'inc/panels/tags.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/ipam/vlan.html b/netbox/templates/ipam/vlan.html index e8c514cca..367ae3641 100644 --- a/netbox/templates/ipam/vlan.html +++ b/netbox/templates/ipam/vlan.html @@ -83,7 +83,7 @@
{% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:vlan_list' %} + {% include 'inc/panels/tags.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/ipam/vlangroup.html b/netbox/templates/ipam/vlangroup.html index 2d31feb22..1c36e92f6 100644 --- a/netbox/templates/ipam/vlangroup.html +++ b/netbox/templates/ipam/vlangroup.html @@ -54,6 +54,7 @@ + {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/ipam/vrf.html b/netbox/templates/ipam/vrf.html index b320fe6b8..349fe20d3 100644 --- a/netbox/templates/ipam/vrf.html +++ b/netbox/templates/ipam/vrf.html @@ -60,7 +60,7 @@ {% plugin_left_page object %}
- {% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:vrf_list' %} + {% include 'inc/panels/tags.html' %} {% include 'inc/panels/custom_fields.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/tenancy/contact.html b/netbox/templates/tenancy/contact.html index 8bdf6c030..3c6ada5a0 100644 --- a/netbox/templates/tenancy/contact.html +++ b/netbox/templates/tenancy/contact.html @@ -60,7 +60,7 @@
{% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' with tags=object.tags.all url='tenancy:tenant_list' %} + {% include 'inc/panels/tags.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/tenancy/contactgroup.html b/netbox/templates/tenancy/contactgroup.html index 0eef750eb..efb86af91 100644 --- a/netbox/templates/tenancy/contactgroup.html +++ b/netbox/templates/tenancy/contactgroup.html @@ -45,6 +45,7 @@ + {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/tenancy/contactrole.html b/netbox/templates/tenancy/contactrole.html index 4ddde3624..3272728f2 100644 --- a/netbox/templates/tenancy/contactrole.html +++ b/netbox/templates/tenancy/contactrole.html @@ -30,6 +30,7 @@
+ {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/tenancy/tenant.html b/netbox/templates/tenancy/tenant.html index dc51b48c5..f54fd1425 100644 --- a/netbox/templates/tenancy/tenant.html +++ b/netbox/templates/tenancy/tenant.html @@ -36,7 +36,7 @@
{% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' with tags=object.tags.all url='tenancy:tenant_list' %} + {% include 'inc/panels/tags.html' %} {% include 'inc/panels/comments.html' %} {% include 'inc/panels/contacts.html' %} {% plugin_left_page object %} diff --git a/netbox/templates/tenancy/tenantgroup.html b/netbox/templates/tenancy/tenantgroup.html index 31a756d9e..75d2c5a27 100644 --- a/netbox/templates/tenancy/tenantgroup.html +++ b/netbox/templates/tenancy/tenantgroup.html @@ -45,6 +45,7 @@ + {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/virtualization/cluster.html b/netbox/templates/virtualization/cluster.html index 84b8235ad..b7af89bb2 100644 --- a/netbox/templates/virtualization/cluster.html +++ b/netbox/templates/virtualization/cluster.html @@ -61,7 +61,7 @@
{% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' with tags=object.tags.all url='virtualization:cluster_list' %} + {% include 'inc/panels/tags.html' %} {% include 'inc/panels/contacts.html' %} {% plugin_right_page object %}
diff --git a/netbox/templates/virtualization/clustergroup.html b/netbox/templates/virtualization/clustergroup.html index b367d97f7..3979fa0e6 100644 --- a/netbox/templates/virtualization/clustergroup.html +++ b/netbox/templates/virtualization/clustergroup.html @@ -28,6 +28,7 @@ + {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/virtualization/clustertype.html b/netbox/templates/virtualization/clustertype.html index e3c050a1b..de5f3c519 100644 --- a/netbox/templates/virtualization/clustertype.html +++ b/netbox/templates/virtualization/clustertype.html @@ -28,6 +28,7 @@
+ {% include 'inc/panels/tags.html' %} {% plugin_left_page object %}
diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index 0d9ea4a22..068d7f164 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -90,7 +90,7 @@
{% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' with tags=object.tags.all url='virtualization:virtualmachine_list' %} + {% include 'inc/panels/tags.html' %} {% include 'inc/panels/comments.html' %} {% plugin_left_page object %} diff --git a/netbox/templates/virtualization/vminterface.html b/netbox/templates/virtualization/vminterface.html index ef12b63a1..1678013f2 100644 --- a/netbox/templates/virtualization/vminterface.html +++ b/netbox/templates/virtualization/vminterface.html @@ -70,8 +70,8 @@
{% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' with tags=object.tags.all %} - {% plugin_right_page object %} + {% include 'inc/panels/tags.html' %} + {% plugin_right_page object %}
From 4932e4f8c64560a453542b8729dea49d0b590ee5 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 21 Oct 2021 11:28:25 -0400 Subject: [PATCH 35/35] Changelog for #6497 --- docs/release-notes/version-3.1.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index 291831500..c829ef2b9 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -20,6 +20,7 @@ When assigning a contact to an object, the user must select a predefined role (e * [#1337](https://github.com/netbox-community/netbox/issues/1337) - Add WWN field to interfaces * [#1943](https://github.com/netbox-community/netbox/issues/1943) - Relax uniqueness constraint on cluster names * [#3839](https://github.com/netbox-community/netbox/issues/3839) - Add `airflow` field for devices types and devices +* [#6497](https://github.com/netbox-community/netbox/issues/6497) - Extend tag support to organizational models * [#6711](https://github.com/netbox-community/netbox/issues/6711) - Add `longtext` custom field type with Markdown support * [#6715](https://github.com/netbox-community/netbox/issues/6715) - Add tenant assignment for cables * [#6874](https://github.com/netbox-community/netbox/issues/6874) - Add tenant assignment for locations @@ -37,6 +38,23 @@ When assigning a contact to an object, the user must select a predefined role (e * `/api/tenancy/contact-groups/` * `/api/tenancy/contact-roles/` * `/api/tenancy/contacts/` +* Added `tags` field to the following models: + * circuits.CircuitType + * dcim.DeviceRole + * dcim.Location + * dcim.Manufacturer + * dcim.Platform + * dcim.RackRole + * dcim.Region + * dcim.SiteGroup + * ipam.RIR + * ipam.Role + * ipam.VLANGroup + * tenancy.ContactGroup + * tenancy.ContactRole + * tenancy.TenantGroup + * virtualization.ClusterGroup + * virtualization.ClusterType * dcim.Cable * Added `tenant` field * dcim.Device