UI: fixes based on Actionable Feedback from devtalk and frontend changes #40
@ -1 +1 @@
|
||||
Subproject commit 80e4f663e3bc04eb2428ce6c676b2bafddd9ab81
|
||||
Subproject commit 1151885c89e9dd33cff1c25d732632f5c356df07
|
@ -43,7 +43,7 @@
|
||||
"fields": {
|
||||
"url": "/about/",
|
||||
"title": "About",
|
||||
"content": "# Blender Extensions Platform\n\nThe Blender Extensions platform is the online directory of free and Open Source extensions for Blender.\n\nThe goal of this platform is to make it easy for Blender users to find and share their add-ons and themes, entirely within the Free and Open Source spirit.\n\nThe platform only offers [**GNU GPL compliant software**](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html).\n\n## How to get started\n\n 1. Download a recent daily build of [Blender 4.2 Apha](https://builder.blender.org).\n 2. Enable the [experimental feature](https://docs.blender.org/manual/en/latest/editors/preferences/experimental.html): Prototypes → Extensions.\n\nNow you can should be able to browse and install extensions in Blender:\n\n![User Preferences → Extensions](https://code.blender.org/wp-content/uploads/2024/02/image-1.png)\n\nYou can also explore the extensions in this web-site and install them via drag & dropping into Blender.\n\n### Follow these 5 simple steps\n\n1. Find an extension that suits you click on the **Get** button.\n\n!['Get Add-on' button](https://code.blender.org/wp-content/uploads/2024/02/image-2.png)\n\n2. Drag the button out of the website ...\n\n!['Drag and Drop into Blender' button](https://code.blender.org/wp-content/uploads/2024/02/image-3.png)\n\n3. ... into Blender.\n\n![Link dragged into Blender](https://code.blender.org/wp-content/uploads/2024/02/image-4.png)\n\n4. You will be prompted about installing and enabling the extension. Confirm to download it.\n\n![Install & Enable](https://code.blender.org/wp-content/uploads/2024/02/image-5.png)\n\n5. Enjoy your newly installed extension\n\n\n## How to publish an extension\n\n* Read the [Conditions of Use](/conditions-of-use/) and [Policies](/policies/).\n* Follow the [guidelines in the User Manual](https://docs.blender.org/manual/en/dev/extensions/index.html#how-to-create-extensions).\n* [Upload your Extension](/submit/).\n* [Wait for Review](/approval-queue/).\n\n\n## See also\n\n* [Conditions of Use](/conditions-of-use/)\n* [Extensions Policies](/policies/)\n* [Report a Bug](https://projects.blender.org/infrastructure/extensions-website/issues)",
|
||||
"content": "# Blender Extensions Platform\n\nThe Blender Extensions platform is the online directory of free and Open Source extensions for Blender.\n\nThe goal of this platform is to make it easy for Blender users to find and share their add-ons and themes, entirely within the Free and Open Source spirit.\n\nThe platform only offers [**GNU GPL compliant software**](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html).\n\n## How to get started\n\n 1. Download a recent daily build of [Blender 4.2 Apha](https://builder.blender.org).\n 2. Enable the [experimental feature](https://docs.blender.org/manual/en/latest/editors/preferences/experimental.html): Prototypes → Extensions.\n\nNow you can should be able to browse and install extensions in Blender:\n\n![User Preferences → Extensions](https://code.blender.org/wp-content/uploads/2024/02/about-1.jpg)\n\nYou can also explore the extensions in this web-site and install them via drag & dropping into Blender.\n\n### Follow these 5 simple steps\n\n1. Find an extension that suits you click on the **Get** button.\n\n!['Get Add-on' button](https://code.blender.org/wp-content/uploads/2024/02/about-2.jpg)\n\n2. Drag the button out of the website ...\n\n!['Drag and Drop into Blender' button](https://code.blender.org/wp-content/uploads/2024/02/about-3.jpg)\n\n3. ... into Blender.\n\n![Link dragged into Blender](https://code.blender.org/wp-content/uploads/2024/02/about-4.jpg)\n\n4. You will be prompted about installing and enabling the extension. Confirm to download it.\n\n![Install & Enable](https://code.blender.org/wp-content/uploads/2024/02/about-5.jpg)\n\n5. Enjoy your newly installed extension\n\n## How to publish an extension\n\n* Read the [Conditions of Use](/conditions-of-use/) and [Policies](/policies/).\n* Follow the [guidelines in the User Manual](https://docs.blender.org/manual/en/dev/extensions/index.html#how-to-create-extensions).\n* [Upload your Extension](/submit/).\n* [Wait for Review](/approval-queue/).\n\n## See also\n\n* [Conditions of Use](/conditions-of-use/)\n* [Extensions Policies](/policies/)\n* [Report a Bug](https://projects.blender.org/infrastructure/extensions-website/issues)",
|
||||
"enable_comments": false,
|
||||
"template_name": "",
|
||||
"registration_required": false,
|
||||
|
@ -26,11 +26,12 @@ function commentForm() {
|
||||
let value = e.target.value;
|
||||
let verb = 'Comment';
|
||||
const activitySubmitButton = document.getElementById('activity-submit');
|
||||
activitySubmitButton.classList.remove('btn-success');
|
||||
activitySubmitButton.classList.remove('btn-success', 'btn-warning');
|
||||
|
||||
// Hide or show comment form msg on change
|
||||
if (value == 'AWC') {
|
||||
verb = 'Set as Awaiting Changes';
|
||||
activitySubmitButton.classList.add('btn-warning');
|
||||
} else if (value == 'AWR') {
|
||||
verb = 'Set as Awaiting Review';
|
||||
} else if (value == 'APR') {
|
||||
|
@ -1,7 +1,5 @@
|
||||
.activity-list
|
||||
list-style: none
|
||||
margin: 0
|
||||
padding: 0
|
||||
+list-unstyled
|
||||
position: relative
|
||||
z-index: 0
|
||||
|
||||
|
@ -1,6 +1,12 @@
|
||||
a.badge-tag
|
||||
--badge-color: var(--text-color-secondary)
|
||||
--badge-bg: var(--text-color-tertiary)
|
||||
|
||||
background-color: transparent
|
||||
text-decoration: none !important
|
||||
|
||||
.badge-tag
|
||||
font-size: var(--font-size-extra-small)
|
||||
|
||||
.badge-status
|
||||
&-approved
|
||||
|
@ -10,7 +10,5 @@
|
||||
ul
|
||||
align-items: center
|
||||
display: flex
|
||||
list-style: none
|
||||
margin: 0
|
||||
padding: 0
|
||||
+list-unstyled
|
||||
gap: 1rem
|
||||
|
@ -25,11 +25,20 @@
|
||||
+margin(3, left)
|
||||
|
||||
.badge
|
||||
+margin(3, right)
|
||||
+margin(2, right)
|
||||
pointer-events: none
|
||||
|
||||
&.extension-review
|
||||
--hero-min-height: 210px
|
||||
|
||||
.ext-detail-download-danger
|
||||
background-color: var(--color-danger-bg)
|
||||
color: var(--color-danger-text)
|
||||
|
||||
.btn
|
||||
--color-danger-bg: hsl(0deg, 25%, 28%)
|
||||
--color-danger-bg-hover: hsl(0deg, 25%, 32%)
|
||||
|
||||
/* Site-wide annoncements */
|
||||
.site-announcement-alpha
|
||||
@extend .alert
|
||||
@ -128,9 +137,7 @@
|
||||
|
||||
.ext-detail-permissions
|
||||
ul
|
||||
list-style: none
|
||||
margin: 0
|
||||
padding: 0
|
||||
+list-unstyled
|
||||
white-space: initial
|
||||
|
||||
li
|
||||
@ -163,6 +170,14 @@
|
||||
.btn-install-drag-group
|
||||
border: .2rem solid transparent
|
||||
|
||||
.cards-list
|
||||
+media-sm
|
||||
--cards-list-items-per-row: 2
|
||||
+media-md
|
||||
--cards-list-items-per-row: 3
|
||||
+media-lg
|
||||
--cards-list-items-per-row: 4
|
||||
|
||||
.ext-card
|
||||
+box-card
|
||||
display: flex
|
||||
@ -174,22 +189,16 @@
|
||||
&:hover
|
||||
box-shadow: 10px 10px 20px 0px rgba(0, 0, 0, .04), -10px 0 20px 0px rgba(0, 0, 0, .04)
|
||||
|
||||
.ext-card-thumbnail
|
||||
.ext-card-thumbnail-img
|
||||
transform: scale(1.025)
|
||||
|
||||
&.is-background-blur
|
||||
background-color: hsl(213, 10%, 21%)
|
||||
border: thin solid hsl(213, 10%, 25%)
|
||||
border: thin solid hsl(213, 10%, 20%)
|
||||
position: relative
|
||||
|
||||
.ext-card-body
|
||||
--text-color-secondary: hsla(213, 40%, 90%, .6)
|
||||
|
||||
background-color: hsla(213, 80%, 1%, .4)
|
||||
border-bottom-left-radius: var(--border-radius-lg)
|
||||
border-bottom-right-radius: var(--border-radius-lg)
|
||||
color: hsl(213, 40%, 98%)
|
||||
+padding(1, top)
|
||||
mix-blend-mode: screen
|
||||
position: relative
|
||||
@ -202,6 +211,10 @@
|
||||
.ext-card-thumbnail-img
|
||||
-webkit-mask-image: -webkit-gradient(linear, left 60%, left bottom, from(rgba(0,0,0,1)), to(rgba(0,0,0,0)))
|
||||
|
||||
.ext-card-thumbnail:hover
|
||||
&+.ext-card-body .ext-card-title
|
||||
color: var(--text-color-primary)
|
||||
|
||||
&.ext-card-row
|
||||
flex-direction: row
|
||||
+margin(3, bottom)
|
||||
@ -225,8 +238,10 @@
|
||||
left: 0
|
||||
position: absolute
|
||||
right: 0
|
||||
transform: scale(1.25)
|
||||
top: 0
|
||||
z-index: 0
|
||||
opacity: .5
|
||||
|
||||
.ext-card-thumbnail
|
||||
--card-thumbnail-width: 100%
|
||||
@ -252,9 +267,13 @@
|
||||
justify-content: space-between
|
||||
+padding(3)
|
||||
|
||||
p
|
||||
line-height: 1.2
|
||||
|
||||
.ext-card-title
|
||||
font-size: var(--font-size-large)
|
||||
+margin(3, bottom)
|
||||
transition: color var(--transition-speed)
|
||||
|
||||
a
|
||||
text-decoration: none
|
||||
@ -378,11 +397,20 @@
|
||||
font-size: var(--font-size-normal)
|
||||
+margin(2, right)
|
||||
|
||||
/* Web Assets overrides */
|
||||
.table-row-link
|
||||
a
|
||||
&:hover
|
||||
text-decoration: none
|
||||
i
|
||||
font-size: var(--font-size-normal)
|
||||
+margin(2, right)
|
||||
.ext-review-list
|
||||
color: var(--text-color-secondary)
|
||||
|
||||
th
|
||||
+padding(3, x)
|
||||
tr td
|
||||
+padding(3, x)
|
||||
+padding(0, y)
|
||||
|
||||
a
|
||||
color: var(--text-color)
|
||||
+padding(1, y)
|
||||
padding-inline: 0 !important
|
||||
|
||||
&:hover
|
||||
.badge
|
||||
text-decoration: none !important
|
||||
|
@ -22,7 +22,7 @@
|
||||
> a:not(.btn)
|
||||
background-color: transparent
|
||||
border-radius: 0
|
||||
color: hsl(0, 0%, 60%)
|
||||
color: var(--text-color-secondary)
|
||||
display: inline-block
|
||||
+padding(4, x)
|
||||
+padding(2, y)
|
||||
|
@ -1,2 +1,27 @@
|
||||
.dl-col-2
|
||||
flex-grow: 2
|
||||
|
||||
.list-filters
|
||||
+box-card
|
||||
background-color: var(--background-color-tertiary)
|
||||
+padding(3)
|
||||
|
||||
h3
|
||||
border-bottom: var(--border-width) solid var(--border-color)
|
||||
color: var(--text-color-secondary)
|
||||
+padding(2, bottom)
|
||||
|
||||
ul
|
||||
+list-unstyled
|
||||
|
||||
li
|
||||
&.is-active
|
||||
color: var(--text-color-primary)
|
||||
+font-weight-bold
|
||||
|
||||
a
|
||||
display: block
|
||||
|
||||
&:hover
|
||||
color: var(--text-color-primary)
|
||||
text-decoration: none
|
||||
|
@ -1,7 +1,13 @@
|
||||
/* Must be set before loading web-assets stylesheets. */
|
||||
$font-path: '/static/fonts'
|
||||
|
||||
|
||||
/* Import variables.*/
|
||||
$grid-breakpoints: (xs: 0,sm: 768px,md: 1020px,lg: 1220px,xl: 1380px,xxl: 1680px) !default
|
||||
|
||||
$container-max-widths: (sm: 760px, md: 1020px, lg: 1070px, xl: 1320px, xxl: 1600px)
|
||||
$container-width: map-get($container-max-widths, 'xl')
|
||||
|
||||
@import '../../../../assets_shared/src/styles/_media_queries.sass'
|
||||
@import '../../../../assets_shared/src/styles/_mixins.sass'
|
||||
@import '../../../../assets_shared/src/styles/_variables.sass'
|
||||
|
@ -188,6 +188,11 @@ class MaintainerAdmin(admin.ModelAdmin):
|
||||
readonly_fields = ('extension', 'user', 'date_deleted')
|
||||
|
||||
|
||||
class LicenseAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'slug', 'url')
|
||||
|
||||
|
||||
admin.site.register(models.Extension, ExtensionAdmin)
|
||||
admin.site.register(models.Version, VersionAdmin)
|
||||
admin.site.register(models.Maintainer, MaintainerAdmin)
|
||||
admin.site.register(models.License, LicenseAdmin)
|
||||
|
@ -20,5 +20,269 @@
|
||||
"slug": "SPDX:GPL-3.0-or-later",
|
||||
"url": "http://www.gnu.org/licenses/gpl-3.0.html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "extensions.license",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"date_created": "2022-08-02T12:44:46.422Z",
|
||||
"date_modified": "2022-08-02T12:44:46.422Z",
|
||||
"name": "BSD 1-Clause \"Simplified\" License",
|
||||
"slug": "SPDX:BSD-1-Clause",
|
||||
"url": "https://spdx.org/licenses/BSD-1-Clause.html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "extensions.license",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"date_created": "2022-08-02T12:44:46.422Z",
|
||||
"date_modified": "2022-08-02T12:44:46.422Z",
|
||||
"name": "BSD 2-Clause \"Simplified\" License",
|
||||
"slug": "SPDX:BSD-2-Clause",
|
||||
"url": "https://spdx.org/licenses/BSD-2-Clause.html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "extensions.license",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"date_created": "2022-08-02T12:44:46.422Z",
|
||||
"date_modified": "2022-08-02T12:44:46.422Z",
|
||||
"name": "BSD 3-Clause \"New\" or \"Revised\" License",
|
||||
"slug": "SPDX:BSD-3-Clause",
|
||||
"url": "https://spdx.org/licenses/BSD-3-Clause.html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "extensions.license",
|
||||
"pk": 7,
|
||||
"fields": {
|
||||
"date_created": "2022-08-02T12:44:46.422Z",
|
||||
"date_modified": "2022-08-02T12:44:46.422Z",
|
||||
"name": "Boost Software License 1.0",
|
||||
"slug": "SPDX:BSL-1.0",
|
||||
"url": "https://spdx.org/licenses/BSL-1.0.html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "extensions.license",
|
||||
"pk": 8,
|
||||
"fields": {
|
||||
"date_created": "2022-08-02T12:44:46.422Z",
|
||||
"date_modified": "2022-08-02T12:44:46.422Z",
|
||||
"name": "CC0-1.0 Universal (CC0 1.0) Public Domain Dedication",
|
||||
"slug": "SPDX:CC0-1.0",
|
||||
"url": "https://spdx.org/licenses/CC0-1.0.html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "extensions.license",
|
||||
"pk": 9,
|
||||
"fields": {
|
||||
"date_created": "2022-08-02T12:44:46.422Z",
|
||||
"date_modified": "2022-08-02T12:44:46.422Z",
|
||||
"name": "Creative Commons Attribution 1.0 Generic",
|
||||
"slug": "SPDX:CC-BY-1.0",
|
||||
"url": "https://spdx.org/licenses/CC-BY-1.0.html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "extensions.license",
|
||||
"pk": 10,
|
||||
"fields": {
|
||||
"date_created": "2022-08-02T12:44:46.422Z",
|
||||
"date_modified": "2022-08-02T12:44:46.422Z",
|
||||
"name": "Creative Commons Attribution 2.0 Generic",
|
||||
"slug": "SPDX:CC-BY-2.0",
|
||||
"url": "https://spdx.org/licenses/CC-BY-2.0.html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "extensions.license",
|
||||
"pk": 11,
|
||||
"fields": {
|
||||
"date_created": "2022-08-02T12:44:46.422Z",
|
||||
"date_modified": "2022-08-02T12:44:46.422Z",
|
||||
"name": "Creative Commons Attribution 2.5 Generic",
|
||||
"slug": "SPDX:CC-BY-2.5",
|
||||
"url": "https://spdx.org/licenses/CC-BY-2.5.html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "extensions.license",
|
||||
"pk": 12,
|
||||
"fields": {
|
||||
"date_created": "2022-08-02T12:44:46.422Z",
|
||||
"date_modified": "2022-08-02T12:44:46.422Z",
|
||||
"name": "Creative Commons Attribution 3.0 Unported",
|
||||
"slug": "SPDX:CC-BY-3.0",
|
||||
"url": "https://spdx.org/licenses/CC-BY-3.0.html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "extensions.license",
|
||||
"pk": 13,
|
||||
"fields": {
|
||||
"date_created": "2022-08-02T12:44:46.422Z",
|
||||
"date_modified": "2022-08-02T12:44:46.422Z",
|
||||
"name": "Creative Commons Attribution 4.0 International",
|
||||
"slug": "SPDX:CC-BY-4.0",
|
||||
"url": "https://spdx.org/licenses/CC-BY-4.0.html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "extensions.license",
|
||||
"pk": 14,
|
||||
"fields": {
|
||||
"date_created": "2022-08-02T12:44:46.422Z",
|
||||
"date_modified": "2022-08-02T12:44:46.422Z",
|
||||
"name": "Creative Commons Attribution Share Alike 1.0 Generic",
|
||||
"slug": "SPDX:CC-BY-SA-1.0",
|
||||
"url": "https://spdx.org/licenses/CC-BY-SA-1.0.html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "extensions.license",
|
||||
"pk": 15,
|
||||
"fields": {
|
||||
"date_created": "2022-08-02T12:44:46.422Z",
|
||||
"date_modified": "2022-08-02T12:44:46.422Z",
|
||||
"name": "Creative Commons Attribution Share Alike 2.0 Generic",
|
||||
"slug": "SPDX:CC-BY-SA-2.0",
|
||||
"url": "https://spdx.org/licenses/CC-BY-SA-2.0.html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "extensions.license",
|
||||
"pk": 16,
|
||||
"fields": {
|
||||
"date_created": "2022-08-02T12:44:46.422Z",
|
||||
"date_modified": "2022-08-02T12:44:46.422Z",
|
||||
"name": "Creative Commons Attribution Share Alike 2.5 Generic",
|
||||
"slug": "SPDX:CC-BY-SA-2.5",
|
||||
"url": "https://spdx.org/licenses/CC-BY-SA-2.5.html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "extensions.license",
|
||||
"pk": 17,
|
||||
"fields": {
|
||||
"date_created": "2022-08-02T12:44:46.422Z",
|
||||
"date_modified": "2022-08-02T12:44:46.422Z",
|
||||
"name": "Creative Commons Attribution Share Alike 3.0 Unported",
|
||||
"slug": "SPDX:CC-BY-SA-3.0",
|
||||
"url": "https://spdx.org/licenses/CC-BY-SA-3.0.html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "extensions.license",
|
||||
"pk": 18,
|
||||
"fields": {
|
||||
"date_created": "2022-08-02T12:44:46.422Z",
|
||||
"date_modified": "2022-08-02T12:44:46.422Z",
|
||||
"name": "Creative Commons Attribution Share Alike 4.0 International",
|
||||
"slug": "SPDX:CC-BY-SA-4.0",
|
||||
"url": "https://spdx.org/licenses/CC-BY-SA-4.0.html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "extensions.license",
|
||||
"pk": 19,
|
||||
"fields": {
|
||||
"date_created": "2022-08-02T12:44:46.422Z",
|
||||
"date_modified": "2022-08-02T12:44:46.422Z",
|
||||
"name": "GNU General Public License v2.0 or later",
|
||||
"slug": "SPDX:GPL-2.0-or-later",
|
||||
"url": "https://spdx.org/licenses/GPL-2.0-or-later.html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "extensions.license",
|
||||
"pk": 20,
|
||||
"fields": {
|
||||
"date_created": "2022-08-02T12:44:46.422Z",
|
||||
"date_modified": "2022-08-02T12:44:46.422Z",
|
||||
"name": "GNU General Public License v3.0 or later",
|
||||
"slug": "SPDX:GPL-3.0-or-later",
|
||||
"url": "https://spdx.org/licenses/GPL-3.0-or-later.html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "extensions.license",
|
||||
"pk": 21,
|
||||
"fields": {
|
||||
"date_created": "2022-08-02T12:44:46.422Z",
|
||||
"date_modified": "2022-08-02T12:44:46.422Z",
|
||||
"name": "GNU Lesser General Public License v2.1 or later",
|
||||
"slug": "SPDX:LGPL-2.1-or-later",
|
||||
"url": "https://spdx.org/licenses/LGPL-2.1-or-later.html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "extensions.license",
|
||||
"pk": 22,
|
||||
"fields": {
|
||||
"date_created": "2022-08-02T12:44:46.422Z",
|
||||
"date_modified": "2022-08-02T12:44:46.422Z",
|
||||
"name": "GNU Lesser General Public License v3.0 or later",
|
||||
"slug": "SPDX:LGPL-3.0-or-later",
|
||||
"url": "https://spdx.org/licenses/LGPL-3.0-or-later.html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "extensions.license",
|
||||
"pk": 23,
|
||||
"fields": {
|
||||
"date_created": "2022-08-02T12:44:46.422Z",
|
||||
"date_modified": "2022-08-02T12:44:46.422Z",
|
||||
"name": "MIT License",
|
||||
"slug": "SPDX:MIT",
|
||||
"url": "https://spdx.org/licenses/MIT.html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "extensions.license",
|
||||
"pk": 24,
|
||||
"fields": {
|
||||
"date_created": "2022-08-02T12:44:46.422Z",
|
||||
"date_modified": "2022-08-02T12:44:46.422Z",
|
||||
"name": "MIT No Attribution",
|
||||
"slug": "SPDX:MIT-0",
|
||||
"url": "https://spdx.org/licenses/MIT-0.html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "extensions.license",
|
||||
"pk": 25,
|
||||
"fields": {
|
||||
"date_created": "2022-08-02T12:44:46.422Z",
|
||||
"date_modified": "2022-08-02T12:44:46.422Z",
|
||||
"name": "Mozilla Public License 2.0",
|
||||
"slug": "SPDX:MPL-2.0",
|
||||
"url": "https://spdx.org/licenses/MPL-2.0.html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "extensions.license",
|
||||
"pk": 26,
|
||||
"fields": {
|
||||
"date_created": "2022-08-02T12:44:46.422Z",
|
||||
"date_modified": "2022-08-02T12:44:46.422Z",
|
||||
"name": "Pixar Open RenderMan License",
|
||||
"slug": "SPDX:Pixar",
|
||||
"url": "https://spdx.org/licenses/Pixar.html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "extensions.license",
|
||||
"pk": 27,
|
||||
"fields": {
|
||||
"date_created": "2022-08-02T12:44:46.422Z",
|
||||
"date_modified": "2022-08-02T12:44:46.422Z",
|
||||
"name": "Zlib License",
|
||||
"slug": "SPDX:Zlib",
|
||||
"url": "https://spdx.org/licenses/Zlib.html"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
17
extensions/migrations/0023_apply_new_licenses.py
Normal file
17
extensions/migrations/0023_apply_new_licenses.py
Normal file
@ -0,0 +1,17 @@
|
||||
from django.db import migrations
|
||||
from django.core.management import call_command
|
||||
|
||||
|
||||
def load_fixtures(apps, schema_editor):
|
||||
call_command('loaddata', 'extensions/fixtures/licenses.json')
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extensions', '0022_alter_extension_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(load_fixtures),
|
||||
]
|
@ -8,17 +8,19 @@
|
||||
<div class="hero extension-detail">
|
||||
<div class="container">
|
||||
<div class="hero-content">
|
||||
{% block hero_breadcrumbs %}
|
||||
<div class="hero-breadcrumbs">
|
||||
<a href="{% url 'extensions:by-type' type_slug=extension.type_slug %}">
|
||||
<i class="i-chevron-left"></i>
|
||||
<span>{% trans 'All' %} {{ extension.get_type_display }}s</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endblock hero_breadcrumbs %}
|
||||
|
||||
<h1>{{ extension.name }}</h1>
|
||||
|
||||
<div class="hero-subtitle">
|
||||
{% if extension.status != extension.STATUSES.APPROVED %}
|
||||
{% if not extension.is_approved %}
|
||||
<span class="badge badge-status-{{ extension.get_status_display|slugify }}">{{ extension.get_status_display }}</span>
|
||||
{% endif %}
|
||||
|
||||
@ -41,7 +43,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-tabs">
|
||||
{% block hero_tabs %}
|
||||
<nav class="hero-tabs">
|
||||
<a href="{{ extension.get_absolute_url }}" class="{% if extension.get_absolute_url == request.get_full_path %}is-active{% endif %}">
|
||||
{% trans "About" %}
|
||||
</a>
|
||||
@ -88,7 +91,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{% endblock hero_tabs %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,17 +9,23 @@
|
||||
{% with latest=extension.latest_version %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8 pt-3">
|
||||
<div class="col-md-8 pt-2">
|
||||
{# Gallery #}
|
||||
{% block extension_galleria %}
|
||||
{% include "extensions/components/galleria.html" with extension=extension %}
|
||||
{% endblock extension_galleria %}
|
||||
|
||||
{# Description #}
|
||||
{% block extension_description %}
|
||||
<section id="about" class="mt-3">
|
||||
<div class="box ext-detail-description">
|
||||
{{ extension.description|markdown }}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock extension_description %}
|
||||
|
||||
{# What's New #}
|
||||
{% block extension_release_notes %}
|
||||
<section id="new" class="mt-3">
|
||||
<div class="box">
|
||||
<h2 class="mb-3">{% trans "What's New" %}</h2>
|
||||
@ -37,8 +43,10 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock extension_release_notes %}
|
||||
|
||||
{# Information #}
|
||||
{% block extension_information %}
|
||||
<section id="info" class="ext-detail-info mt-3">
|
||||
<div class="box">
|
||||
<h2 class="mb-3">{% trans 'Information' %}</h2>
|
||||
@ -91,19 +99,24 @@
|
||||
{{ extension.date_created|date:'F jS, Y' }}
|
||||
</dd>
|
||||
|
||||
{% if extension.versions.listed|length %}
|
||||
<dt>{% trans 'Version History' %}</dt>
|
||||
<dd>
|
||||
<a href="{{ extension.get_versions_url }}">
|
||||
{{ extension.versions.listed|length }} version{{ extension.versions.listed|pluralize }} (see all)
|
||||
</a>
|
||||
</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock extension_information %}
|
||||
|
||||
{# Permissions #}
|
||||
{% block extension_permissions %}
|
||||
{% if extension.get_type_display|lower == 'add-on' %}
|
||||
<section id="permissions" class="ext-detail-permissions mt-3">
|
||||
<div class="box">
|
||||
<h2 class="mb-3">{% trans 'Permissions' %}</h2>
|
||||
@ -127,16 +140,23 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock extension_permissions %}
|
||||
|
||||
{% block extension_activity %}
|
||||
{% endblock extension_activity %}
|
||||
</div>
|
||||
|
||||
{# Sidebar #}
|
||||
<div class="col-md-4">
|
||||
<div class="is-sticky pt-3">
|
||||
<aside class="is-sticky pt-2">
|
||||
|
||||
{# Info Summary #}
|
||||
{% block extension_summary_sidebar %}
|
||||
<section class="ext-detail-info">
|
||||
<div class="card p-3 mb-3">
|
||||
<div class="card p-3">
|
||||
<dl>
|
||||
{# Developer #}
|
||||
<div class="dl-row">
|
||||
<div class="dl-col">
|
||||
{% if extension.team %}
|
||||
@ -154,11 +174,13 @@
|
||||
</div>
|
||||
|
||||
<div class="dl-row">
|
||||
{# Rating #}
|
||||
{% if extension.is_approved %}
|
||||
<div class="dl-col">
|
||||
<dt>{% trans 'Rating' %}</dt>
|
||||
<dd>
|
||||
{% if extension.average_score %}
|
||||
<a href="{{ extension.get_ratings_url }}">
|
||||
<a href="{{ extension.get_ratings_url }}" class="text-decoration-none">
|
||||
{% include "ratings/components/average.html" with score=extension.average_score %}
|
||||
({{ extension.ratings.listed.count }})
|
||||
</a>
|
||||
@ -169,6 +191,9 @@
|
||||
{% endif %}
|
||||
</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Version #}
|
||||
<div class="dl-col">
|
||||
<dt>{% trans 'Version' %}</dt>
|
||||
<dd>
|
||||
@ -231,8 +256,11 @@
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock extension_summary_sidebar %}
|
||||
|
||||
<section class="ext-detail-download">
|
||||
{# Download #}
|
||||
{% block extension_download %}
|
||||
<section class="ext-detail-download mt-3">
|
||||
<div class="btn-group js-btn-install-group">
|
||||
<button class="btn btn-flex btn-accent js-btn-install" data-install-url="{{ request.scheme }}://{{ request.get_host }}{{ latest.download_url }}">
|
||||
<span>{% trans 'Get' %} {{ extension.get_type_display }}</span>
|
||||
@ -250,14 +278,14 @@
|
||||
</small>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock extension_download %}
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
{# Latest Reviews. #}
|
||||
{% block extension_reviews %}
|
||||
<hr class="my-4">
|
||||
<section>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
@ -295,10 +323,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock extension_reviews %}
|
||||
|
||||
{# Report #}
|
||||
{% if request.user.is_authenticated and not is_maintainer %}
|
||||
<hr/>
|
||||
<hr>
|
||||
<section class="mt-4">
|
||||
<a href="{{ extension.get_report_url }}" class="btn">
|
||||
<i class="i-flag"></i> {% trans 'Report this extension' %}
|
||||
|
@ -23,7 +23,7 @@
|
||||
<section>
|
||||
<div class="cards-list">
|
||||
{% for extension in object_list %}
|
||||
{% include "extensions/components/card.html" with blur=True %}
|
||||
{% include "extensions/components/card.html" with blur=True show_type=True %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
|
@ -5,7 +5,26 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col my-4">
|
||||
<div class="col-md-2">
|
||||
<aside class="is-sticky pt-3">
|
||||
<div class="list-filters">
|
||||
<h3>Tags</h3>
|
||||
<ul>
|
||||
{% for list_tag in all_tags %}
|
||||
{% if list_tag.taggit_taggeditem_items.all|length %}
|
||||
<li class="{% if tag == list_tag %}is-active{% endif %}">
|
||||
<a href="{% url "extensions:by-tag" tag_slug=list_tag.slug %}" title="{{ list_tag.name }}">
|
||||
{{ list_tag.name }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div class="col-md-10 my-4">
|
||||
{% if author %}
|
||||
<h2>{% blocktranslate %}Extensions by{% endblocktranslate %} <em class="search-highlight">{{ author }}</em></h2>
|
||||
{% endif %}
|
||||
@ -24,29 +43,28 @@
|
||||
{% if request.GET.q %}
|
||||
<h2>{{ page_obj.paginator.count }} result{{ page_obj.paginator.count | pluralize }} for <em class="search-highlight">{{ request.GET.q }}</em></h2>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% if object_list %}
|
||||
<div class="cards-list card-layout-horizontal">
|
||||
{% for extension in object_list %}
|
||||
{% include "extensions/components/card.html" with show_type=True blur=True %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>
|
||||
{% blocktranslate %}No extensions found.{% endblocktranslate %}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include "common/components/pagination.html" with label="result" %}
|
||||
{% if object_list %}
|
||||
<div class="cards-list card-layout-horizontal cards-3">
|
||||
{% for extension in object_list %}
|
||||
{% include "extensions/components/card.html" with show_type=False blur=True %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>
|
||||
{% blocktranslate %}No extensions found.{% endblocktranslate %}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include "common/components/pagination.html" with label="result" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
|
@ -27,7 +27,7 @@
|
||||
{% if object_list %}
|
||||
<div class="cards-list">
|
||||
{% for extension in object_list %}
|
||||
{% include "extensions/manage/components/card.html" with blur=True %}
|
||||
{% include "extensions/manage/components/card.html" with blur=True show_type=True %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
|
@ -23,7 +23,7 @@
|
||||
<div class="col">
|
||||
{% for version in extension.versions.exclude_deleted %}
|
||||
{% if version.is_listed or is_maintainer %}
|
||||
<details {% if forloop.first %}open=""{% endif %}>
|
||||
<details {% if forloop.counter == 1 %}open{% endif %}>
|
||||
<summary>
|
||||
{{ version.version }}
|
||||
<span class="date" title="{% firstof version.date_approved|date:'l jS, F Y - H:i' version.date_created|date:'l jS, F Y - H:i' %}">
|
||||
|
@ -201,6 +201,39 @@ class ValidateManifestTest(CreateFileTest):
|
||||
},
|
||||
)
|
||||
|
||||
def test_validation_manifest_extension_id_repeated_version(self):
|
||||
"""Test if we try to add a version to an extension without changing the version number"""
|
||||
self.assertEqual(Extension.objects.count(), 0)
|
||||
version = self._create_valid_extension('blender_kitsu')
|
||||
self.assertEqual(Extension.objects.count(), 1)
|
||||
|
||||
# The same author is to send a new version to thte same extension
|
||||
self.client.force_login(version.file.user)
|
||||
|
||||
kitsu_version_clash = {
|
||||
"name": "Change the name for lols",
|
||||
"id": version.extension.extension_id,
|
||||
"version": version.version,
|
||||
}
|
||||
|
||||
extension_file = self._create_file_from_data(
|
||||
"kitsu_clash.zip", kitsu_version_clash, self.user
|
||||
)
|
||||
with open(extension_file, 'rb') as fp:
|
||||
response = self.client.post(
|
||||
version.extension.get_new_version_url(), {'source': fp, 'agreed_with_terms': True}
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertDictEqual(
|
||||
response.context['form'].errors,
|
||||
{
|
||||
'source': [
|
||||
f'The version {version.version} was already uploaded for this extension ({ version.extension.name})'
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class ValidateManifestFields(TestCase):
|
||||
fixtures = ['licenses', 'version_permissions', 'tags']
|
||||
|
@ -313,7 +313,7 @@ class SubmitFinaliseTest(TestCase):
|
||||
self.assertEqual(version.blender_version_max, None)
|
||||
self.assertEqual(version.schema_version, '1.0.0')
|
||||
self.assertEqual(version.release_notes, data['release_notes'])
|
||||
self.assertEqual(version.file.get_status_display(), 'Awaiting Review')
|
||||
self.assertEqual(version.file.get_status_display(), 'Approved')
|
||||
# We cannot check for the ManyToMany yet (tags, licences, permissions)
|
||||
|
||||
# Check that author can access the page they are redirected to
|
||||
|
@ -24,39 +24,10 @@ class _BaseTestCase(TestCase):
|
||||
fixtures = ['dev', 'tags', 'licenses']
|
||||
|
||||
def _check_detail_page(self, response, extension):
|
||||
self.assertContains(response, 'Test Add-on', html=True)
|
||||
self.assertContains(response, '<strong>Description in bold</strong>', html=True)
|
||||
self.assertContains(response, '1.3.4', html=True)
|
||||
self.assertContains(
|
||||
response,
|
||||
'<a rel="nofollow" target="_blank" href="https://example.com/issues/">example.com</a>',
|
||||
html=True,
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
'<a rel="nofollow" target="_blank" href="https://example.com/">example.com</a>',
|
||||
html=True,
|
||||
)
|
||||
self.assertContains(response, 'Community', html=True)
|
||||
self.assertContains(
|
||||
response, f'<dt>Downloads</dt><dd>{extension.download_count}</dd>', html=True
|
||||
)
|
||||
self.assertContains(response, 'Blender 2.93.1 and newer', html=True)
|
||||
author = extension.authors.first()
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<a href="/author/{author.pk}/" title="{author.full_name}">{author}</a>',
|
||||
html=True,
|
||||
)
|
||||
pass
|
||||
|
||||
def _check_ratings_page(self, response, extension):
|
||||
self.assertContains(response, 'Reviews', html=True)
|
||||
author = extension.authors.first()
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<a href="/author/{author.pk}/" title="{author.full_name}">{author}</a>',
|
||||
html=True,
|
||||
)
|
||||
pass
|
||||
|
||||
|
||||
class PublicViewsTest(_BaseTestCase):
|
||||
|
@ -144,12 +144,6 @@ class UpdateExtensionView(
|
||||
try:
|
||||
edit_preview_formset.save()
|
||||
add_preview_formset.save()
|
||||
# Set all pending previews as approved, for now.
|
||||
for p in self.extension.preview_set.filter(
|
||||
file__status=File.STATUSES.AWAITING_REVIEW
|
||||
):
|
||||
p.file.status = File.STATUSES.APPROVED
|
||||
p.file.save()
|
||||
response = super().form_valid(form)
|
||||
return response
|
||||
except forms.ValidationError as e:
|
||||
|
@ -2,7 +2,6 @@ import logging
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponseBadRequest
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.views.generic.list import ListView
|
||||
|
||||
@ -70,7 +69,7 @@ class SearchView(ListedExtensionsView):
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
if self.kwargs.get('tag_slug'):
|
||||
queryset = queryset.filter(versions__tags__slug=self.kwargs['tag_slug'])
|
||||
queryset = queryset.filter(versions__tags__slug=self.kwargs['tag_slug']).distinct()
|
||||
if self.kwargs.get('team_slug'):
|
||||
queryset = queryset.filter(team__slug=self.kwargs['team_slug'])
|
||||
if self.kwargs.get('user_id'):
|
||||
@ -95,6 +94,8 @@ class SearchView(ListedExtensionsView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['all_tags'] = Tag.objects.all()
|
||||
|
||||
if self.kwargs.get('user_id'):
|
||||
context['author'] = get_object_or_404(User, pk=self.kwargs['user_id'])
|
||||
if self.kwargs.get('tag_slug'):
|
||||
|
27
files/migrations/0004_alter_file_status.py
Normal file
27
files/migrations/0004_alter_file_status.py
Normal file
@ -0,0 +1,27 @@
|
||||
# Generated by Django 4.0.6 on 2024-02-26 13:25
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def approve_all_files(apps, schema_editor):
|
||||
model = apps.get_model('files', 'file')
|
||||
for ob in model.objects.all():
|
||||
# APPROVE
|
||||
ob.status = 3
|
||||
ob.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('files', '0003_alter_file_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='file',
|
||||
name='status',
|
||||
field=models.PositiveSmallIntegerField(choices=[(2, 'Awaiting Review'), (3, 'Approved'), (4, 'Disabled by staff'), (5, 'Disabled by author')], default=3),
|
||||
),
|
||||
migrations.RunPython(approve_all_files),
|
||||
]
|
@ -81,7 +81,7 @@ class File(CreatedModifiedMixin, TrackChangesMixin, SoftDeleteMixin, models.Mode
|
||||
'as guessed from its contents.'
|
||||
),
|
||||
)
|
||||
status = models.PositiveSmallIntegerField(choices=STATUSES, default=STATUSES.AWAITING_REVIEW)
|
||||
status = models.PositiveSmallIntegerField(choices=STATUSES, default=STATUSES.APPROVED)
|
||||
|
||||
user = models.ForeignKey(
|
||||
User, related_name='files', null=False, blank=False, on_delete=models.CASCADE
|
||||
|
@ -39,3 +39,13 @@ class UtilsTest(TestCase):
|
||||
]
|
||||
manifest_file = find_file_inside_zip_list(self.manifest, name_list)
|
||||
self.assertEqual(manifest_file, None)
|
||||
|
||||
def test_find_manifest_with_space(self):
|
||||
name_list = [
|
||||
"foobar-1.0.3/ blender_manifest.toml",
|
||||
"foobar-1.0.3/_blender_manifest.toml",
|
||||
"foobar-1.0.3/blender_manifest.toml.txt",
|
||||
"blender_manifest.toml/my_files.py",
|
||||
]
|
||||
manifest_file = find_file_inside_zip_list(self.manifest, name_list)
|
||||
self.assertEqual(manifest_file, None)
|
||||
|
@ -1,6 +1,7 @@
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import io
|
||||
import os
|
||||
import logging
|
||||
import zipfile
|
||||
import toml
|
||||
@ -46,17 +47,14 @@ def get_sha256_from_value(value: str):
|
||||
|
||||
|
||||
def find_file_inside_zip_list(file_to_read: str, name_list: list) -> str:
|
||||
"""
|
||||
Return the first occurance of file_to_read insize a zip name_list
|
||||
It shouldn't matter how deep inside the list the file is.
|
||||
"""
|
||||
if file_to_read in name_list:
|
||||
return file_to_read
|
||||
|
||||
"""Return the first occurance of file_to_read insize a zip name_list"""
|
||||
for file_path in name_list:
|
||||
if file_to_read not in file_path:
|
||||
continue
|
||||
return file_path
|
||||
# Remove leading/trailing whitespace from file path
|
||||
file_path_stripped = file_path.strip()
|
||||
# Check if the basename of the stripped path is equal to the target file name
|
||||
if os.path.basename(file_path_stripped) == file_to_read:
|
||||
return file_path_stripped
|
||||
return None
|
||||
|
||||
|
||||
def read_manifest_from_zip(archive_path):
|
||||
|
@ -119,14 +119,14 @@ class ExtensionIDManifestValidator:
|
||||
|
||||
class ManifestFieldValidator:
|
||||
@classmethod
|
||||
def validate(cls, *, name: str, value: object) -> str:
|
||||
def validate(cls, *, name: str, value: object, manifest: dict) -> str:
|
||||
"""Return error message if cannot validate, otherwise returns nothing"""
|
||||
assert not "ManifestFieldValidator must be inherited not to be used directly."
|
||||
|
||||
|
||||
class SimpleValidator(ManifestFieldValidator):
|
||||
@classmethod
|
||||
def validate(cls, *, name: str, value: object) -> str:
|
||||
def validate(cls, *, name: str, value: object, manifest: dict) -> str:
|
||||
"""Return error message if cannot validate, otherwise returns nothing"""
|
||||
if not hasattr(cls, '_type') or not hasattr(cls, '_type_name'):
|
||||
assert not "SimpleValidator must be inherited not be used directly."
|
||||
@ -150,7 +150,7 @@ class LicenseValidator(ListValidator):
|
||||
example = ['SPDX:GPL-2.0-or-later']
|
||||
|
||||
@classmethod
|
||||
def validate(cls, *, name: str, value: list[str]) -> str:
|
||||
def validate(cls, *, name: str, value: list[str], manifest: dict) -> str:
|
||||
"""Return error message if there is any license that is not accepted by the site"""
|
||||
is_error = False
|
||||
error_message = ""
|
||||
@ -180,7 +180,7 @@ class TagsValidator:
|
||||
example = ['Animation', 'Sequencer']
|
||||
|
||||
@classmethod
|
||||
def validate(cls, *, name: str, value: list[str]) -> str:
|
||||
def validate(cls, *, name: str, value: list[str], manifest: dict) -> str:
|
||||
"""Return error message if there is no tag, or if tag is not a valid one"""
|
||||
is_error = False
|
||||
|
||||
@ -214,7 +214,7 @@ class TypeValidator:
|
||||
example = 'add-on'
|
||||
|
||||
@classmethod
|
||||
def validate(cls, *, name: str, value: str) -> str:
|
||||
def validate(cls, *, name: str, value: str, manifest: dict) -> str:
|
||||
"""Return error message if doesn´t contain one of the valid types."""
|
||||
is_error = False
|
||||
|
||||
@ -243,7 +243,7 @@ class PermissionsValidator:
|
||||
example = ['files', 'network']
|
||||
|
||||
@classmethod
|
||||
def validate(cls, *, name: str, value: str) -> str:
|
||||
def validate(cls, *, name: str, value: str, manifest: dict) -> str:
|
||||
"""Return error message if doesn´t contain a valid list of permissions."""
|
||||
is_error = False
|
||||
|
||||
@ -275,7 +275,7 @@ class VersionValidator:
|
||||
example = '1.0.0'
|
||||
|
||||
@classmethod
|
||||
def validate(cls, *, name: str, value: str) -> str:
|
||||
def validate(cls, *, name: str, value: str, manifest: dict) -> str:
|
||||
"""Return error message if cannot validate, otherwise returns nothing"""
|
||||
try:
|
||||
Version(value)
|
||||
@ -288,13 +288,37 @@ class VersionValidator:
|
||||
)
|
||||
|
||||
|
||||
class VersionVersionValidator(VersionValidator):
|
||||
example = '1.0.0'
|
||||
|
||||
@classmethod
|
||||
def validate(cls, *, name: str, value: str, manifest: dict) -> str:
|
||||
"""Return error message if cannot validate, otherwise returns nothing"""
|
||||
if err_message := super().validate(name=name, value=value, manifest=manifest):
|
||||
return err_message
|
||||
|
||||
extension = Extension.objects.filter(extension_id=manifest.get("id")).first()
|
||||
|
||||
# If the extension wasn't created yet, any version is valid
|
||||
if not extension:
|
||||
return
|
||||
|
||||
version = Extension.objects.filter(versions__version=value).first()
|
||||
|
||||
if version:
|
||||
return (
|
||||
f'The version {value} was already uploaded for this extension '
|
||||
f'({extension.name})'
|
||||
)
|
||||
|
||||
|
||||
class VersionMinValidator(VersionValidator):
|
||||
example = '4.2.0'
|
||||
|
||||
@classmethod
|
||||
def validate(cls, *, name: str, value: str) -> str:
|
||||
def validate(cls, *, name: str, value: str, manifest: dict) -> str:
|
||||
"""Return error message if cannot validate, otherwise returns nothing"""
|
||||
if err_message := super().validate(name=name, value=value):
|
||||
if err_message := super().validate(name=name, value=value, manifest=manifest):
|
||||
return err_message
|
||||
|
||||
# Extensions were created in 4.2.0
|
||||
@ -310,9 +334,9 @@ class TaglineValidator(StringValidator):
|
||||
example = 'Short description of my extension'
|
||||
|
||||
@classmethod
|
||||
def validate(cls, *, name: str, value: str) -> str:
|
||||
def validate(cls, *, name: str, value: str, manifest: dict) -> str:
|
||||
"""Return error message if has period at the end of the line or is too long."""
|
||||
if err_message := super().validate(name=name, value=value):
|
||||
if err_message := super().validate(name=name, value=value, manifest=manifest):
|
||||
return err_message
|
||||
|
||||
if not value:
|
||||
@ -344,7 +368,7 @@ class ManifestValidator:
|
||||
'schema_version': VersionValidator,
|
||||
'tagline': TaglineValidator,
|
||||
'type': TypeValidator,
|
||||
'version': VersionValidator,
|
||||
'version': VersionVersionValidator,
|
||||
}
|
||||
optional_fields = {
|
||||
'blender_version_max': VersionMaxValidator,
|
||||
@ -363,14 +387,18 @@ class ManifestValidator:
|
||||
field_value = manifest.get(field_name)
|
||||
if field_value is None:
|
||||
missing_fields.append(field_name)
|
||||
elif err_message := field_validator.validate(name=field_name, value=field_value):
|
||||
elif err_message := field_validator.validate(
|
||||
name=field_name, value=field_value, manifest=manifest
|
||||
):
|
||||
wrong_fields.append(err_message)
|
||||
|
||||
for field_name, field_validator in self.optional_fields.items():
|
||||
field_value = manifest.get(field_name)
|
||||
if field_value is None:
|
||||
continue
|
||||
elif err_message := field_validator.validate(name=field_name, value=field_value):
|
||||
elif err_message := field_validator.validate(
|
||||
name=field_name, value=field_value, manifest=manifest
|
||||
):
|
||||
wrong_fields.append(err_message)
|
||||
|
||||
if not (missing_fields or wrong_fields):
|
||||
|
27
ratings/migrations/0004_alter_rating_status.py
Normal file
27
ratings/migrations/0004_alter_rating_status.py
Normal file
@ -0,0 +1,27 @@
|
||||
# Generated by Django 4.0.6 on 2024-02-26 16:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def approve_all_reviews(apps, schema_editor):
|
||||
model = apps.get_model('ratings', 'rating')
|
||||
for ob in model.objects.all():
|
||||
# APPROVE
|
||||
ob.status = 3
|
||||
ob.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ratings', '0003_alter_rating_score'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='rating',
|
||||
name='status',
|
||||
field=models.PositiveSmallIntegerField(choices=[(2, 'Awaiting Review'), (3, 'Approved'), (4, 'Rejected by staff')], default=3),
|
||||
),
|
||||
migrations.RunPython(approve_all_reviews),
|
||||
]
|
@ -61,7 +61,7 @@ class Rating(CreatedModifiedMixin, TrackChangesMixin, SoftDeleteMixin, models.Mo
|
||||
text = models.TextField(null=True)
|
||||
ip_address = models.GenericIPAddressField(protocol='both', null=True)
|
||||
|
||||
status = models.PositiveSmallIntegerField(choices=STATUSES, default=STATUSES.AWAITING_REVIEW)
|
||||
status = models.PositiveSmallIntegerField(choices=STATUSES, default=STATUSES.APPROVED)
|
||||
|
||||
# Denormalized fields for easy lookup queries.
|
||||
is_latest = models.BooleanField(
|
||||
|
@ -1,244 +1,204 @@
|
||||
{% extends "common/base.html" %}
|
||||
{% extends "extensions/detail.html" %}
|
||||
{% load common extensions filters i18n humanize %}
|
||||
|
||||
{% block page_title %}Review{% endblock page_title %}
|
||||
{% block page_title %}Review: {{ extension.name }}{% endblock page_title %}
|
||||
|
||||
{% block container_main %}
|
||||
{% has_maintainer extension as is_maintainer %}
|
||||
<div class="container-main">
|
||||
{% block hero %}
|
||||
<div class="hero extension-detail extension-review">
|
||||
<div class="container justify-content-start">
|
||||
<div class="hero-content">
|
||||
<div class="hero-breadcrumbs">
|
||||
<a href="{% url 'reviewers:approval-queue' %}">
|
||||
<i class="i-chevron-left"></i>
|
||||
<span>Back to Queue</span>
|
||||
</a>
|
||||
</div>
|
||||
<h1>{{ extension.name }}</h1>
|
||||
<div class="px-4">
|
||||
{% include "common/components/status.html" with object=extension %}
|
||||
</div>
|
||||
</div>
|
||||
{% block hero_breadcrumbs %}
|
||||
<div class="hero-breadcrumbs">
|
||||
<a href="{% url 'reviewers:approval-queue' %}">
|
||||
<i class="i-chevron-left"></i>
|
||||
<span>{% trans 'All in Queue' %}</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endblock hero_breadcrumbs %}
|
||||
|
||||
<div class="hero-tabs">
|
||||
<a href="#about">
|
||||
{% trans "About" %}
|
||||
</a>
|
||||
<a href="#activity">
|
||||
{% trans "Activity" %}
|
||||
</a>
|
||||
{% block hero_tabs %}
|
||||
<div class="hero-tabs">
|
||||
<a href="#about">
|
||||
{% trans "About" %}
|
||||
</a>
|
||||
<a href="#activity">
|
||||
{% trans "Activity" %}
|
||||
</a>
|
||||
|
||||
<span class="ml-auto"></span>
|
||||
<span class="ml-auto"></span>
|
||||
|
||||
<div class="btn-row">
|
||||
{% if is_maintainer %}
|
||||
<a href="{{ extension.get_manage_url }}" class="btn">
|
||||
<i class="i-edit"></i> {% trans 'Edit' %}
|
||||
</a>
|
||||
<div class="btn-row">
|
||||
{% if is_maintainer %}
|
||||
<a href="{{ extension.get_manage_url }}" class="btn">
|
||||
<i class="i-edit"></i> {% trans 'Edit' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if request.user.is_staff %}
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-admin dropdown-toggle js-dropdown-toggle" data-toggle-menu-id="extension-admin-menu">
|
||||
<span>Admin</span>
|
||||
<i class="i-chevron-down"></i>
|
||||
</button>
|
||||
<ul id="extension-admin-menu" class="dropdown-menu dropdown-menu-right js-dropdown-menu">
|
||||
<li>
|
||||
<a href="{% url 'admin:extensions_extension_change' extension.pk %}" class="dropdown-item is-admin">
|
||||
{% trans 'Extension' %}
|
||||
</a>
|
||||
</li>
|
||||
{% if extension.latest_version %}
|
||||
<li>
|
||||
<a href="{% url 'admin:extensions_version_change' extension.latest_version.pk %}" class="dropdown-item is-admin">
|
||||
{% trans 'Version' %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if request.user.is_staff %}
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-admin dropdown-toggle js-dropdown-toggle" data-toggle-menu-id="extension-admin-menu">
|
||||
<span>Admin</span>
|
||||
<i class="i-chevron-down"></i>
|
||||
</button>
|
||||
<ul id="extension-admin-menu" class="dropdown-menu dropdown-menu-right js-dropdown-menu">
|
||||
<li>
|
||||
<a href="{% url 'admin:extensions_extension_change' extension.pk %}" class="dropdown-item is-admin">
|
||||
{% trans 'Extension' %}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'admin:extensions_version_change' pending_versions.0.pk %}" class="dropdown-item is-admin">
|
||||
{% trans 'Version' %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="dropdown-divider"></li>
|
||||
<li>
|
||||
<a href="{% url 'admin:users_user_change' extension.authors.all.0.pk %}" class="dropdown-item is-admin">
|
||||
{% trans 'User' %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% if extension.authors.all.0 %}
|
||||
<li class="dropdown-divider"></li>
|
||||
<li>
|
||||
<a href="{% url 'admin:users_user_change' extension.authors.all.0.pk %}" class="dropdown-item is-admin">
|
||||
{% trans 'User' %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock hero %}
|
||||
|
||||
<div class="container pb-5">
|
||||
<div class="row">
|
||||
<div class="col-md-8 pt-3">
|
||||
{% include "extensions/components/galleria.html" with extension=extension %}
|
||||
|
||||
<section class="mt-2 mb-4 box p-3">
|
||||
{% if pending_previews %}
|
||||
<h3>Previews Pending Approval</h3>
|
||||
<div class="row">
|
||||
{% for preview in pending_previews %}
|
||||
<div class="col-md-3">
|
||||
<a href="{{ preview.file.source.url }}" class="d-block mb-2" title="{{ preview.caption }}">
|
||||
<img class="img-fluid rounded" src="{{ preview.file.source.url }}" alt="{{ preview.caption }}">
|
||||
</a>
|
||||
{% include "common/components/status.html" with object=preview.file class="d-block" %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>{% trans "No previews pending approval." %}</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section id="about" class="mt-3">
|
||||
<div class="box ext-detail-description">
|
||||
{{ extension.description|markdown }}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<section id="activity">
|
||||
<h2>Activity</h2>
|
||||
|
||||
{% if extension.review_activity.all %}
|
||||
<ul class="activity-list">
|
||||
{% for activity in extension.review_activity.all %}
|
||||
|
||||
{% if activity.type != 'COM' %}
|
||||
<li class="activity-status-change activity-status-{{ activity.get_type_display|slugify }}">
|
||||
<a href="{% url "extensions:by-author" user_id=activity.user.pk %}"><strong>{{ activity.user }}</strong></a>
|
||||
changed review status to
|
||||
<span class="badge badge-status-{{ activity.get_type_display|slugify }}">
|
||||
{{ activity.get_type_display }}
|
||||
</span>
|
||||
<a href="#activity-{{ activity.id }}" title="{{ activity.date_created }}">
|
||||
{{ activity.date_created|naturaltime_compact }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if activity.message %}
|
||||
<li class="comment-card">
|
||||
<header>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="{% url "extensions:by-author" user_id=activity.user.pk %}">
|
||||
{{ activity.user }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="ml-auto">
|
||||
<a href="#activity-{{ activity.id }}" title="{{ activity.date_created }}">
|
||||
{{ activity.date_created|naturaltime_compact }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
<div>
|
||||
{{ activity.message }}
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>{% trans "No recent activity." %}</p>
|
||||
{% endif %}
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="card p-3">
|
||||
<h3 class="mb-3">Add comment</h3>
|
||||
{% if request.user.is_authenticated %}
|
||||
<form class="comment-form js-comment-form" method="post" action="{% url 'reviewers:approval-comment' extension.slug %}">
|
||||
{% csrf_token %}
|
||||
{% with form=comment_form|add_form_classes %}
|
||||
|
||||
{% include "common/components/field.html" with field=form.message %}
|
||||
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="btn-row ml-3 w-100 justify-content-end">
|
||||
{% if is_maintainer or request.user.is_moderator %}
|
||||
{% include "common/components/field.html" with field=form.type %}
|
||||
{% endif %}
|
||||
<button type="submit" id="activity-submit" class="btn btn-primary">
|
||||
<span>{% trans "Comment" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
</form>
|
||||
{% else %}
|
||||
<p>Login to comment.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
|
||||
<div class="is-sticky pt-3 ext-detail-info">
|
||||
{% include "extensions/components/extension_edit_detail_card.html" with extension=extension latest=pending_versions.0 %}
|
||||
|
||||
<div class="card p-3 mt-3">
|
||||
<p class="text-danger">
|
||||
Warning: this extension has not been reviewed yet.
|
||||
Try at your own risk.
|
||||
</p>
|
||||
<div class="btn-col">
|
||||
<a href="{{ pending_versions.0.download_url }}" download="{{ pending_versions.0.download_name }}" class="btn btn-danger btn-block">
|
||||
<i class="i-download"></i>
|
||||
<span>
|
||||
{% trans 'Download' %} v{{ pending_versions.0.version }}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if is_maintainer %}
|
||||
<div class="btn-col mt-3">
|
||||
<a href="{{ pending_versions.0.get_delete_url }}" class="btn btn-link btn-danger">
|
||||
<i class="i-trash"></i>
|
||||
<span>{% trans "Delete Version" %}</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card p-3 mt-3">
|
||||
<dl>
|
||||
<div class="dl-row">
|
||||
<div class="dl-col">
|
||||
<dt>Versions</dt>
|
||||
<dd>
|
||||
{% if pending_versions.length > 1 %}
|
||||
<ul class="list-unstyled mb-0">
|
||||
{% for version in pending_versions %}
|
||||
<li>
|
||||
<a href="{{ version.file.source.url }}">{{ version.file }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<span>No more versions pending.</span>
|
||||
{% endif %}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock container_main %}
|
||||
{% endblock hero_tabs %}
|
||||
|
||||
|
||||
{% block extension_galleria %}
|
||||
{% include "extensions/components/galleria.html" with extension=extension %}
|
||||
|
||||
{% if pending_previews %}
|
||||
<section class="my-3 box p-3">
|
||||
<h3>Previews Pending Approval</h3>
|
||||
<div class="row">
|
||||
{% for preview in pending_previews %}
|
||||
<div class="col-md-3">
|
||||
<a href="{{ preview.file.source.url }}" class="d-block mb-2" title="{{ preview.caption }}" target="_blank">
|
||||
<img class="img-fluid rounded" src="{{ preview.file.source.url }}" alt="{{ preview.caption }}">
|
||||
</a>
|
||||
{% include "common/components/status.html" with object=preview.file class="d-block" %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock extension_galleria %}
|
||||
|
||||
|
||||
{% block extension_activity %}
|
||||
<section id="activity" class="mt-4">
|
||||
<h2>Activity</h2>
|
||||
|
||||
{% if extension.review_activity.all %}
|
||||
<ul class="activity-list">
|
||||
{% for activity in extension.review_activity.all %}
|
||||
|
||||
{# All activities except comments. #}
|
||||
{% if activity.type != 'COM' %}
|
||||
<li class="activity-status-change activity-status-{{ activity.get_type_display|slugify }}">
|
||||
<a href="{% url "extensions:by-author" user_id=activity.user.pk %}"><strong>{{ activity.user }}</strong></a>
|
||||
changed review status to
|
||||
<span class="badge badge-status-{{ activity.get_type_display|slugify }}">
|
||||
{{ activity.get_type_display }}
|
||||
</span>
|
||||
<a href="#activity-{{ activity.id }}" title="{{ activity.date_created }}">
|
||||
{{ activity.date_created|naturaltime_compact }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{# Comments. #}
|
||||
{% if activity.message %}
|
||||
<li class="comment-card">
|
||||
<header>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="{% url "extensions:by-author" user_id=activity.user.pk %}">
|
||||
{{ activity.user }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="ml-auto">
|
||||
<a href="#activity-{{ activity.id }}" title="{{ activity.date_created }}">
|
||||
{{ activity.date_created|naturaltime_compact }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
<div>
|
||||
{{ activity.message }}
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>{% trans "No recent activity." %}</p>
|
||||
{% endif %}
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="card p-3">
|
||||
<h3 class="mb-3">Add comment</h3>
|
||||
{% if request.user.is_authenticated %}
|
||||
<form class="comment-form js-comment-form" method="post" action="{% url 'reviewers:approval-comment' extension.slug %}">
|
||||
{% csrf_token %}
|
||||
{% with form=comment_form|add_form_classes %}
|
||||
|
||||
{% include "common/components/field.html" with field=form.message %}
|
||||
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="btn-row ml-3 w-100 justify-content-end">
|
||||
{% if is_maintainer or request.user.is_moderator %}
|
||||
{% include "common/components/field.html" with field=form.type %}
|
||||
{% endif %}
|
||||
<button type="submit" id="activity-submit" class="btn btn-primary">
|
||||
<span>{% trans "Comment" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
</form>
|
||||
{% else %}
|
||||
<p>Login to comment.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock extension_activity %}
|
||||
|
||||
{# Download all versions. #}
|
||||
{% block extension_download %}
|
||||
<section>
|
||||
{% if extension.is_approved %}
|
||||
<div class="card p-3 mt-3">
|
||||
<h3>{% trans 'Review' %}</h3>
|
||||
<p><i class="i-check text-success"></i> {% trans 'This extension has been approved!' %}</p>
|
||||
<a href="{{ extension.get_absolute_url }}" class="btn w-100">
|
||||
<span>{% trans 'View Extension' %}</span>
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card p-3 mt-3 ext-detail-download-danger">
|
||||
<h3>Caution</h3>
|
||||
<p>
|
||||
This extension has not been reviewed yet.
|
||||
<strong>Try at your own risk.</strong>
|
||||
</p>
|
||||
<div class="btn-col">
|
||||
<a href="{{ extension.latest_version.download_url }}" download="{{ extension.latest_version.download_name }}" class="btn btn-danger btn-block">
|
||||
<i class="i-download"></i>
|
||||
<span>
|
||||
{% trans 'Download' %} v{{ extension.latest_version.version }}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock extension_download %}
|
||||
|
||||
{# No reviews. #}
|
||||
{% block extension_reviews %}{% endblock extension_reviews %}
|
||||
|
||||
{% block scripts %}
|
||||
{% if extension.get_previews %}
|
||||
|
@ -1,44 +1,62 @@
|
||||
{% extends "common/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
{% load i18n humanize filters %}
|
||||
|
||||
{% block page_title %}Approval queue{% endblock page_title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="my-4 row">
|
||||
<div class="col">
|
||||
<h2>Approval Queue</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% if object_list %}
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-md-9">Name</th>
|
||||
<th class="col-md-3">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for extension in object_list %}
|
||||
<tr class="table-row-link">
|
||||
<td class="col-md-9 p-0">
|
||||
<a class="px-3 py-1" href="{% url 'reviewers:approval-detail' extension.slug %}">
|
||||
{{ extension.name }}</td>
|
||||
</a>
|
||||
<td class="col-md-3 p-0">
|
||||
<a class="px-3 py-1" href="{% url 'reviewers:approval-detail' extension.slug %}">
|
||||
{% include "common/components/status.html" with object=extension %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No extensions to review, for now.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 row">
|
||||
<div class="col">
|
||||
<h2>
|
||||
{% blocktranslate %}
|
||||
Extension Approval Queue
|
||||
{% endblocktranslate %}
|
||||
</h2>
|
||||
<p>
|
||||
{% blocktranslate %}
|
||||
The following extensions are awaiting review. You can help speed-up the process by
|
||||
testing them and leaving a comment if something seems wrong.
|
||||
{% endblocktranslate %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="ext-review-list">
|
||||
{% if object_list %}
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Type" %}</th>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Author" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for extension in object_list %}
|
||||
<tr>
|
||||
<td>{{ extension.get_type_display }}</td>
|
||||
<td>
|
||||
<a href="{% url 'reviewers:approval-detail' extension.slug %}">
|
||||
{{ extension.name }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if extension.authors.count %}
|
||||
{% include "extensions/components/authors.html" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="{% url 'reviewers:approval-detail' extension.slug %}" class="text-decoration-none">
|
||||
{% include "common/components/status.html" with object=extension class="d-block" %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>{% trans "No extensions to review." %}</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock content %}
|
||||
|
@ -19,8 +19,8 @@ class ApprovalQueueView(ListView):
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
Extension.objects.prefetch_related('versions')
|
||||
.exclude(versions__file__status=File.STATUSES.APPROVED)
|
||||
Extension.objects.all()
|
||||
.exclude(status=Extension.STATUSES.APPROVED)
|
||||
.order_by('-date_created')
|
||||
)
|
||||
|
||||
@ -37,7 +37,7 @@ class ExtensionsApprovalDetailView(DetailView):
|
||||
ctx['pending_previews'] = self.object.preview_set.exclude(
|
||||
file__status=File.STATUSES.APPROVED
|
||||
)
|
||||
ctx['pending_versions'] = self.object.versions.exclude(file__status=File.STATUSES.APPROVED)
|
||||
|
||||
if self.request.user.is_authenticated:
|
||||
form = ctx['comment_form'] = CommentForm()
|
||||
# Remove 'Approved' status from dropdown it not moderator
|
||||
|
Loading…
Reference in New Issue
Block a user