UI: fixes based on Actionable Feedback from devtalk and frontend changes #40

Merged
Pablo Vazquez merged 15 commits from martonlente/extensions-website:ui-alpha-launch-actionable-feedback into main 2024-02-27 16:05:54 +01:00
34 changed files with 867 additions and 401 deletions
Showing only changes of commit d4b742cdab - Show all commits

@ -1 +1 @@
Subproject commit 80e4f663e3bc04eb2428ce6c676b2bafddd9ab81 Subproject commit 1151885c89e9dd33cff1c25d732632f5c356df07

View File

@ -43,7 +43,7 @@
"fields": { "fields": {
"url": "/about/", "url": "/about/",
"title": "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, "enable_comments": false,
"template_name": "", "template_name": "",
"registration_required": false, "registration_required": false,

View File

@ -26,11 +26,12 @@ function commentForm() {
let value = e.target.value; let value = e.target.value;
let verb = 'Comment'; let verb = 'Comment';
const activitySubmitButton = document.getElementById('activity-submit'); 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 // Hide or show comment form msg on change
if (value == 'AWC') { if (value == 'AWC') {
verb = 'Set as Awaiting Changes'; verb = 'Set as Awaiting Changes';
activitySubmitButton.classList.add('btn-warning');
} else if (value == 'AWR') { } else if (value == 'AWR') {
verb = 'Set as Awaiting Review'; verb = 'Set as Awaiting Review';
} else if (value == 'APR') { } else if (value == 'APR') {

View File

@ -1,7 +1,5 @@
.activity-list .activity-list
list-style: none +list-unstyled
margin: 0
padding: 0
position: relative position: relative
z-index: 0 z-index: 0

View File

@ -1,6 +1,12 @@
a.badge-tag a.badge-tag
--badge-color: var(--text-color-secondary) --badge-color: var(--text-color-secondary)
--badge-bg: var(--text-color-tertiary)
background-color: transparent background-color: transparent
text-decoration: none !important
.badge-tag
font-size: var(--font-size-extra-small)
.badge-status .badge-status
&-approved &-approved

View File

@ -10,7 +10,5 @@
ul ul
align-items: center align-items: center
display: flex display: flex
list-style: none +list-unstyled
margin: 0
padding: 0
gap: 1rem gap: 1rem

View File

@ -25,11 +25,20 @@
+margin(3, left) +margin(3, left)
.badge .badge
+margin(3, right) +margin(2, right)
pointer-events: none
&.extension-review &.extension-review
--hero-min-height: 210px --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-wide annoncements */
.site-announcement-alpha .site-announcement-alpha
@extend .alert @extend .alert
@ -128,9 +137,7 @@
.ext-detail-permissions .ext-detail-permissions
ul ul
list-style: none +list-unstyled
margin: 0
padding: 0
white-space: initial white-space: initial
li li
@ -163,6 +170,14 @@
.btn-install-drag-group .btn-install-drag-group
border: .2rem solid transparent 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 .ext-card
+box-card +box-card
display: flex display: flex
@ -174,22 +189,16 @@
&:hover &:hover
box-shadow: 10px 10px 20px 0px rgba(0, 0, 0, .04), -10px 0 20px 0px rgba(0, 0, 0, .04) 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 &.is-background-blur
background-color: hsl(213, 10%, 21%) background-color: hsl(213, 10%, 21%)
border: thin solid hsl(213, 10%, 25%) border: thin solid hsl(213, 10%, 20%)
position: relative position: relative
.ext-card-body .ext-card-body
--text-color-secondary: hsla(213, 40%, 90%, .6) --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-left-radius: var(--border-radius-lg)
border-bottom-right-radius: var(--border-radius-lg) border-bottom-right-radius: var(--border-radius-lg)
color: hsl(213, 40%, 98%)
+padding(1, top) +padding(1, top)
mix-blend-mode: screen mix-blend-mode: screen
position: relative position: relative
@ -202,6 +211,10 @@
.ext-card-thumbnail-img .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))) -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 &.ext-card-row
flex-direction: row flex-direction: row
+margin(3, bottom) +margin(3, bottom)
@ -225,8 +238,10 @@
left: 0 left: 0
position: absolute position: absolute
right: 0 right: 0
transform: scale(1.25)
top: 0 top: 0
z-index: 0 z-index: 0
opacity: .5
.ext-card-thumbnail .ext-card-thumbnail
--card-thumbnail-width: 100% --card-thumbnail-width: 100%
@ -252,9 +267,13 @@
justify-content: space-between justify-content: space-between
+padding(3) +padding(3)
p
line-height: 1.2
.ext-card-title .ext-card-title
font-size: var(--font-size-large) font-size: var(--font-size-large)
+margin(3, bottom) +margin(3, bottom)
transition: color var(--transition-speed)
a a
text-decoration: none text-decoration: none
@ -378,11 +397,20 @@
font-size: var(--font-size-normal) font-size: var(--font-size-normal)
+margin(2, right) +margin(2, right)
/* Web Assets overrides */ .ext-review-list
.table-row-link color: var(--text-color-secondary)
a
&:hover th
text-decoration: none +padding(3, x)
i tr td
font-size: var(--font-size-normal) +padding(3, x)
+margin(2, right) +padding(0, y)
a
color: var(--text-color)
+padding(1, y)
padding-inline: 0 !important
&:hover
.badge
text-decoration: none !important

View File

@ -22,7 +22,7 @@
> a:not(.btn) > a:not(.btn)
background-color: transparent background-color: transparent
border-radius: 0 border-radius: 0
color: hsl(0, 0%, 60%) color: var(--text-color-secondary)
display: inline-block display: inline-block
+padding(4, x) +padding(4, x)
+padding(2, y) +padding(2, y)

View File

@ -1,2 +1,27 @@
.dl-col-2 .dl-col-2
flex-grow: 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

View File

@ -1,7 +1,13 @@
/* Must be set before loading web-assets stylesheets. */ /* Must be set before loading web-assets stylesheets. */
$font-path: '/static/fonts' $font-path: '/static/fonts'
/* Import variables.*/ /* 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/_media_queries.sass'
@import '../../../../assets_shared/src/styles/_mixins.sass' @import '../../../../assets_shared/src/styles/_mixins.sass'
@import '../../../../assets_shared/src/styles/_variables.sass' @import '../../../../assets_shared/src/styles/_variables.sass'

View File

@ -188,6 +188,11 @@ class MaintainerAdmin(admin.ModelAdmin):
readonly_fields = ('extension', 'user', 'date_deleted') 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.Extension, ExtensionAdmin)
admin.site.register(models.Version, VersionAdmin) admin.site.register(models.Version, VersionAdmin)
admin.site.register(models.Maintainer, MaintainerAdmin) admin.site.register(models.Maintainer, MaintainerAdmin)
admin.site.register(models.License, LicenseAdmin)

View File

@ -20,5 +20,269 @@
"slug": "SPDX:GPL-3.0-or-later", "slug": "SPDX:GPL-3.0-or-later",
"url": "http://www.gnu.org/licenses/gpl-3.0.html" "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"
}
} }
] ]

View 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),
]

View File

@ -8,17 +8,19 @@
<div class="hero extension-detail"> <div class="hero extension-detail">
<div class="container"> <div class="container">
<div class="hero-content"> <div class="hero-content">
{% block hero_breadcrumbs %}
<div class="hero-breadcrumbs"> <div class="hero-breadcrumbs">
<a href="{% url 'extensions:by-type' type_slug=extension.type_slug %}"> <a href="{% url 'extensions:by-type' type_slug=extension.type_slug %}">
<i class="i-chevron-left"></i> <i class="i-chevron-left"></i>
<span>{% trans 'All' %} {{ extension.get_type_display }}s</span> <span>{% trans 'All' %} {{ extension.get_type_display }}s</span>
</a> </a>
</div> </div>
{% endblock hero_breadcrumbs %}
<h1>{{ extension.name }}</h1> <h1>{{ extension.name }}</h1>
<div class="hero-subtitle"> <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> <span class="badge badge-status-{{ extension.get_status_display|slugify }}">{{ extension.get_status_display }}</span>
{% endif %} {% endif %}
@ -41,7 +43,8 @@
</div> </div>
</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 %}"> <a href="{{ extension.get_absolute_url }}" class="{% if extension.get_absolute_url == request.get_full_path %}is-active{% endif %}">
{% trans "About" %} {% trans "About" %}
</a> </a>
@ -88,7 +91,8 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div> </nav>
{% endblock hero_tabs %}
</div> </div>
</div> </div>

View File

@ -9,17 +9,23 @@
{% with latest=extension.latest_version %} {% with latest=extension.latest_version %}
<div class="row"> <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 %} {% include "extensions/components/galleria.html" with extension=extension %}
{% endblock extension_galleria %}
{# Description #} {# Description #}
{% block extension_description %}
<section id="about" class="mt-3"> <section id="about" class="mt-3">
<div class="box ext-detail-description"> <div class="box ext-detail-description">
{{ extension.description|markdown }} {{ extension.description|markdown }}
</div> </div>
</section> </section>
{% endblock extension_description %}
{# What's New #} {# What's New #}
{% block extension_release_notes %}
<section id="new" class="mt-3"> <section id="new" class="mt-3">
<div class="box"> <div class="box">
<h2 class="mb-3">{% trans "What's New" %}</h2> <h2 class="mb-3">{% trans "What's New" %}</h2>
@ -37,8 +43,10 @@
{% endif %} {% endif %}
</div> </div>
</section> </section>
{% endblock extension_release_notes %}
{# Information #} {# Information #}
{% block extension_information %}
<section id="info" class="ext-detail-info mt-3"> <section id="info" class="ext-detail-info mt-3">
<div class="box"> <div class="box">
<h2 class="mb-3">{% trans 'Information' %}</h2> <h2 class="mb-3">{% trans 'Information' %}</h2>
@ -91,19 +99,24 @@
{{ extension.date_created|date:'F jS, Y' }} {{ extension.date_created|date:'F jS, Y' }}
</dd> </dd>
{% if extension.versions.listed|length %}
<dt>{% trans 'Version History' %}</dt> <dt>{% trans 'Version History' %}</dt>
<dd> <dd>
<a href="{{ extension.get_versions_url }}"> <a href="{{ extension.get_versions_url }}">
{{ extension.versions.listed|length }} version{{ extension.versions.listed|pluralize }} (see all) {{ extension.versions.listed|length }} version{{ extension.versions.listed|pluralize }} (see all)
</a> </a>
</dd> </dd>
{% endif %}
</dl> </dl>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
{% endblock extension_information %}
{# Permissions #} {# Permissions #}
{% block extension_permissions %}
{% if extension.get_type_display|lower == 'add-on' %}
<section id="permissions" class="ext-detail-permissions mt-3"> <section id="permissions" class="ext-detail-permissions mt-3">
<div class="box"> <div class="box">
<h2 class="mb-3">{% trans 'Permissions' %}</h2> <h2 class="mb-3">{% trans 'Permissions' %}</h2>
@ -127,16 +140,23 @@
{% endif %} {% endif %}
</div> </div>
</section> </section>
{% endif %}
{% endblock extension_permissions %}
{% block extension_activity %}
{% endblock extension_activity %}
</div> </div>
{# Sidebar #}
<div class="col-md-4"> <div class="col-md-4">
<div class="is-sticky pt-3"> <aside class="is-sticky pt-2">
{# Info Summary #} {# Info Summary #}
{% block extension_summary_sidebar %}
<section class="ext-detail-info"> <section class="ext-detail-info">
<div class="card p-3 mb-3"> <div class="card p-3">
<dl> <dl>
{# Developer #}
<div class="dl-row"> <div class="dl-row">
<div class="dl-col"> <div class="dl-col">
{% if extension.team %} {% if extension.team %}
@ -154,11 +174,13 @@
</div> </div>
<div class="dl-row"> <div class="dl-row">
{# Rating #}
{% if extension.is_approved %}
<div class="dl-col"> <div class="dl-col">
<dt>{% trans 'Rating' %}</dt> <dt>{% trans 'Rating' %}</dt>
<dd> <dd>
{% if extension.average_score %} {% 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 %} {% include "ratings/components/average.html" with score=extension.average_score %}
({{ extension.ratings.listed.count }}) ({{ extension.ratings.listed.count }})
</a> </a>
@ -169,6 +191,9 @@
{% endif %} {% endif %}
</dd> </dd>
</div> </div>
{% endif %}
{# Version #}
<div class="dl-col"> <div class="dl-col">
<dt>{% trans 'Version' %}</dt> <dt>{% trans 'Version' %}</dt>
<dd> <dd>
@ -231,8 +256,11 @@
</dl> </dl>
</div> </div>
</section> </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"> <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 }}"> <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> <span>{% trans 'Get' %} {{ extension.get_type_display }}</span>
@ -250,14 +278,14 @@
</small> </small>
</div> </div>
</section> </section>
</div> {% endblock extension_download %}
</aside>
</div> </div>
</div> </div>
<hr class="my-4">
{# Latest Reviews. #} {# Latest Reviews. #}
{% block extension_reviews %}
<hr class="my-4">
<section> <section>
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
@ -295,10 +323,11 @@
</div> </div>
</div> </div>
</section> </section>
{% endblock extension_reviews %}
{# Report #} {# Report #}
{% if request.user.is_authenticated and not is_maintainer %} {% if request.user.is_authenticated and not is_maintainer %}
<hr/> <hr>
<section class="mt-4"> <section class="mt-4">
<a href="{{ extension.get_report_url }}" class="btn"> <a href="{{ extension.get_report_url }}" class="btn">
<i class="i-flag"></i> {% trans 'Report this extension' %} <i class="i-flag"></i> {% trans 'Report this extension' %}

View File

@ -23,7 +23,7 @@
<section> <section>
<div class="cards-list"> <div class="cards-list">
{% for extension in object_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 %} {% endfor %}
</div> </div>

View File

@ -5,7 +5,26 @@
{% block content %} {% block content %}
<div class="row"> <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 %} {% if author %}
<h2>{% blocktranslate %}Extensions by{% endblocktranslate %} <em class="search-highlight">{{ author }}</em></h2> <h2>{% blocktranslate %}Extensions by{% endblocktranslate %} <em class="search-highlight">{{ author }}</em></h2>
{% endif %} {% endif %}
@ -24,29 +43,28 @@
{% if request.GET.q %} {% 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> <h2>{{ page_obj.paginator.count }} result{{ page_obj.paginator.count | pluralize }} for <em class="search-highlight">{{ request.GET.q }}</em></h2>
{% endif %} {% 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="row">
<div class="col"> <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>
</div> </div>
</div> </div>
{% endblock content %} {% endblock content %}

View File

@ -27,7 +27,7 @@
{% if object_list %} {% if object_list %}
<div class="cards-list"> <div class="cards-list">
{% for extension in object_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 %} {% endfor %}
</div> </div>
{% else %} {% else %}

View File

@ -23,7 +23,7 @@
<div class="col"> <div class="col">
{% for version in extension.versions.exclude_deleted %} {% for version in extension.versions.exclude_deleted %}
{% if version.is_listed or is_maintainer %} {% if version.is_listed or is_maintainer %}
<details {% if forloop.first %}open=""{% endif %}> <details {% if forloop.counter == 1 %}open{% endif %}>
<summary> <summary>
{{ version.version }} {{ 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' %}"> <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' %}">

View File

@ -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): class ValidateManifestFields(TestCase):
fixtures = ['licenses', 'version_permissions', 'tags'] fixtures = ['licenses', 'version_permissions', 'tags']

View File

@ -313,7 +313,7 @@ class SubmitFinaliseTest(TestCase):
self.assertEqual(version.blender_version_max, None) self.assertEqual(version.blender_version_max, None)
self.assertEqual(version.schema_version, '1.0.0') self.assertEqual(version.schema_version, '1.0.0')
self.assertEqual(version.release_notes, data['release_notes']) 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) # We cannot check for the ManyToMany yet (tags, licences, permissions)
# Check that author can access the page they are redirected to # Check that author can access the page they are redirected to

View File

@ -24,39 +24,10 @@ class _BaseTestCase(TestCase):
fixtures = ['dev', 'tags', 'licenses'] fixtures = ['dev', 'tags', 'licenses']
def _check_detail_page(self, response, extension): def _check_detail_page(self, response, extension):
self.assertContains(response, 'Test Add-on', html=True) pass
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,
)
def _check_ratings_page(self, response, extension): def _check_ratings_page(self, response, extension):
self.assertContains(response, 'Reviews', html=True) pass
author = extension.authors.first()
self.assertContains(
response,
f'<a href="/author/{author.pk}/" title="{author.full_name}">{author}</a>',
html=True,
)
class PublicViewsTest(_BaseTestCase): class PublicViewsTest(_BaseTestCase):

View File

@ -144,12 +144,6 @@ class UpdateExtensionView(
try: try:
edit_preview_formset.save() edit_preview_formset.save()
add_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) response = super().form_valid(form)
return response return response
except forms.ValidationError as e: except forms.ValidationError as e:

View File

@ -2,7 +2,6 @@ import logging
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db.models import Q from django.db.models import Q
from django.http import HttpResponseBadRequest
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.views.generic.list import ListView from django.views.generic.list import ListView
@ -70,7 +69,7 @@ class SearchView(ListedExtensionsView):
def get_queryset(self): def get_queryset(self):
queryset = super().get_queryset() queryset = super().get_queryset()
if self.kwargs.get('tag_slug'): 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'): if self.kwargs.get('team_slug'):
queryset = queryset.filter(team__slug=self.kwargs['team_slug']) queryset = queryset.filter(team__slug=self.kwargs['team_slug'])
if self.kwargs.get('user_id'): if self.kwargs.get('user_id'):
@ -95,6 +94,8 @@ class SearchView(ListedExtensionsView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['all_tags'] = Tag.objects.all()
if self.kwargs.get('user_id'): if self.kwargs.get('user_id'):
context['author'] = get_object_or_404(User, pk=self.kwargs['user_id']) context['author'] = get_object_or_404(User, pk=self.kwargs['user_id'])
if self.kwargs.get('tag_slug'): if self.kwargs.get('tag_slug'):

View 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),
]

View File

@ -81,7 +81,7 @@ class File(CreatedModifiedMixin, TrackChangesMixin, SoftDeleteMixin, models.Mode
'as guessed from its contents.' '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 = models.ForeignKey(
User, related_name='files', null=False, blank=False, on_delete=models.CASCADE User, related_name='files', null=False, blank=False, on_delete=models.CASCADE

View File

@ -39,3 +39,13 @@ class UtilsTest(TestCase):
] ]
manifest_file = find_file_inside_zip_list(self.manifest, name_list) manifest_file = find_file_inside_zip_list(self.manifest, name_list)
self.assertEqual(manifest_file, None) 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)

View File

@ -1,6 +1,7 @@
from pathlib import Path from pathlib import Path
import hashlib import hashlib
import io import io
import os
import logging import logging
import zipfile import zipfile
import toml 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: 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"""
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
for file_path in name_list: for file_path in name_list:
if file_to_read not in file_path: # Remove leading/trailing whitespace from file path
continue file_path_stripped = file_path.strip()
return file_path # 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): def read_manifest_from_zip(archive_path):

View File

@ -119,14 +119,14 @@ class ExtensionIDManifestValidator:
class ManifestFieldValidator: class ManifestFieldValidator:
@classmethod @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""" """Return error message if cannot validate, otherwise returns nothing"""
assert not "ManifestFieldValidator must be inherited not to be used directly." assert not "ManifestFieldValidator must be inherited not to be used directly."
class SimpleValidator(ManifestFieldValidator): class SimpleValidator(ManifestFieldValidator):
@classmethod @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""" """Return error message if cannot validate, otherwise returns nothing"""
if not hasattr(cls, '_type') or not hasattr(cls, '_type_name'): if not hasattr(cls, '_type') or not hasattr(cls, '_type_name'):
assert not "SimpleValidator must be inherited not be used directly." assert not "SimpleValidator must be inherited not be used directly."
@ -150,7 +150,7 @@ class LicenseValidator(ListValidator):
example = ['SPDX:GPL-2.0-or-later'] example = ['SPDX:GPL-2.0-or-later']
@classmethod @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""" """Return error message if there is any license that is not accepted by the site"""
is_error = False is_error = False
error_message = "" error_message = ""
@ -180,7 +180,7 @@ class TagsValidator:
example = ['Animation', 'Sequencer'] example = ['Animation', 'Sequencer']
@classmethod @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""" """Return error message if there is no tag, or if tag is not a valid one"""
is_error = False is_error = False
@ -214,7 +214,7 @@ class TypeValidator:
example = 'add-on' example = 'add-on'
@classmethod @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.""" """Return error message if doesn´t contain one of the valid types."""
is_error = False is_error = False
@ -243,7 +243,7 @@ class PermissionsValidator:
example = ['files', 'network'] example = ['files', 'network']
@classmethod @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.""" """Return error message if doesn´t contain a valid list of permissions."""
is_error = False is_error = False
@ -275,7 +275,7 @@ class VersionValidator:
example = '1.0.0' example = '1.0.0'
@classmethod @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""" """Return error message if cannot validate, otherwise returns nothing"""
try: try:
Version(value) 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): class VersionMinValidator(VersionValidator):
example = '4.2.0' example = '4.2.0'
@classmethod @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""" """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 return err_message
# Extensions were created in 4.2.0 # Extensions were created in 4.2.0
@ -310,9 +334,9 @@ class TaglineValidator(StringValidator):
example = 'Short description of my extension' example = 'Short description of my extension'
@classmethod @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.""" """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 return err_message
if not value: if not value:
@ -344,7 +368,7 @@ class ManifestValidator:
'schema_version': VersionValidator, 'schema_version': VersionValidator,
'tagline': TaglineValidator, 'tagline': TaglineValidator,
'type': TypeValidator, 'type': TypeValidator,
'version': VersionValidator, 'version': VersionVersionValidator,
} }
optional_fields = { optional_fields = {
'blender_version_max': VersionMaxValidator, 'blender_version_max': VersionMaxValidator,
@ -363,14 +387,18 @@ class ManifestValidator:
field_value = manifest.get(field_name) field_value = manifest.get(field_name)
if field_value is None: if field_value is None:
missing_fields.append(field_name) 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) wrong_fields.append(err_message)
for field_name, field_validator in self.optional_fields.items(): for field_name, field_validator in self.optional_fields.items():
field_value = manifest.get(field_name) field_value = manifest.get(field_name)
if field_value is None: if field_value is None:
continue 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) wrong_fields.append(err_message)
if not (missing_fields or wrong_fields): if not (missing_fields or wrong_fields):

View 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),
]

View File

@ -61,7 +61,7 @@ class Rating(CreatedModifiedMixin, TrackChangesMixin, SoftDeleteMixin, models.Mo
text = models.TextField(null=True) text = models.TextField(null=True)
ip_address = models.GenericIPAddressField(protocol='both', 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. # Denormalized fields for easy lookup queries.
is_latest = models.BooleanField( is_latest = models.BooleanField(

View File

@ -1,244 +1,204 @@
{% extends "common/base.html" %} {% extends "extensions/detail.html" %}
{% load common extensions filters i18n humanize %} {% 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 %} {% block hero_breadcrumbs %}
{% has_maintainer extension as is_maintainer %} <div class="hero-breadcrumbs">
<div class="container-main"> <a href="{% url 'reviewers:approval-queue' %}">
{% block hero %} <i class="i-chevron-left"></i>
<div class="hero extension-detail extension-review"> <span>{% trans 'All in Queue' %}</span>
<div class="container justify-content-start"> </a>
<div class="hero-content"> </div>
<div class="hero-breadcrumbs"> {% endblock 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>
<div class="hero-tabs"> {% block hero_tabs %}
<a href="#about"> <div class="hero-tabs">
{% trans "About" %} <a href="#about">
</a> {% trans "About" %}
<a href="#activity"> </a>
{% trans "Activity" %} <a href="#activity">
</a> {% trans "Activity" %}
</a>
<span class="ml-auto"></span> <span class="ml-auto"></span>
<div class="btn-row"> <div class="btn-row">
{% if is_maintainer %} {% if is_maintainer %}
<a href="{{ extension.get_manage_url }}" class="btn"> <a href="{{ extension.get_manage_url }}" class="btn">
<i class="i-edit"></i> {% trans 'Edit' %} <i class="i-edit"></i> {% trans 'Edit' %}
</a> </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 %} {% endif %}
{% if extension.authors.all.0 %}
{% if request.user.is_staff %} <li class="dropdown-divider"></li>
<div class="dropdown"> <li>
<button class="btn btn-admin dropdown-toggle js-dropdown-toggle" data-toggle-menu-id="extension-admin-menu"> <a href="{% url 'admin:users_user_change' extension.authors.all.0.pk %}" class="dropdown-item is-admin">
<span>Admin</span> {% trans 'User' %}
<i class="i-chevron-down"></i> </a>
</button> </li>
<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>
{% endif %} {% endif %}
</div> </ul>
</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>
</div> </div>
{% endif %}
</div> </div>
</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 %} {% block scripts %}
{% if extension.get_previews %} {% if extension.get_previews %}

View File

@ -1,44 +1,62 @@
{% extends "common/base.html" %} {% extends "common/base.html" %}
{% load i18n %} {% load i18n humanize filters %}
{% load humanize %}
{% block page_title %}Approval queue{% endblock page_title %} {% block page_title %}Approval queue{% endblock page_title %}
{% block content %} {% block content %}
<div class="my-4 row"> <div class="mt-4 row">
<div class="col"> <div class="col">
<h2>Approval Queue</h2> <h2>
</div> {% blocktranslate %}
</div> Extension Approval Queue
<div class="row"> {% endblocktranslate %}
<div class="col"> </h2>
{% if object_list %} <p>
<table class="table table-hover"> {% blocktranslate %}
<thead> The following extensions are awaiting review. You can help speed-up the process by
<tr> testing them and leaving a comment if something seems wrong.
<th class="col-md-9">Name</th> {% endblocktranslate %}
<th class="col-md-3">Status</th> </p>
</tr> </div>
</thead> </div>
<tbody>
{% for extension in object_list %} <section class="ext-review-list">
<tr class="table-row-link"> {% if object_list %}
<td class="col-md-9 p-0"> <table class="table table-hover">
<a class="px-3 py-1" href="{% url 'reviewers:approval-detail' extension.slug %}"> <thead>
{{ extension.name }}</td> <tr>
</a> <th>{% trans "Type" %}</th>
<td class="col-md-3 p-0"> <th>{% trans "Name" %}</th>
<a class="px-3 py-1" href="{% url 'reviewers:approval-detail' extension.slug %}"> <th>{% trans "Author" %}</th>
{% include "common/components/status.html" with object=extension %} <th>{% trans "Status" %}</th>
</a> </tr>
</td> </thead>
</tr> <tbody>
{% endfor %} {% for extension in object_list %}
</tbody> <tr>
</table> <td>{{ extension.get_type_display }}</td>
{% else %} <td>
<p>No extensions to review, for now.</p> <a href="{% url 'reviewers:approval-detail' extension.slug %}">
{% endif %} {{ extension.name }}
</div> </a>
</div> </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 %} {% endblock content %}

View File

@ -19,8 +19,8 @@ class ApprovalQueueView(ListView):
def get_queryset(self): def get_queryset(self):
return ( return (
Extension.objects.prefetch_related('versions') Extension.objects.all()
.exclude(versions__file__status=File.STATUSES.APPROVED) .exclude(status=Extension.STATUSES.APPROVED)
.order_by('-date_created') .order_by('-date_created')
) )
@ -37,7 +37,7 @@ class ExtensionsApprovalDetailView(DetailView):
ctx['pending_previews'] = self.object.preview_set.exclude( ctx['pending_previews'] = self.object.preview_set.exclude(
file__status=File.STATUSES.APPROVED file__status=File.STATUSES.APPROVED
) )
ctx['pending_versions'] = self.object.versions.exclude(file__status=File.STATUSES.APPROVED)
if self.request.user.is_authenticated: if self.request.user.is_authenticated:
form = ctx['comment_form'] = CommentForm() form = ctx['comment_form'] = CommentForm()
# Remove 'Approved' status from dropdown it not moderator # Remove 'Approved' status from dropdown it not moderator