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": {
"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,

View File

@ -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') {

View File

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

View File

@ -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

View File

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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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'

View File

@ -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)

View File

@ -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"
}
}
]

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="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>

View File

@ -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' %}

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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' %}">

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):
fixtures = ['licenses', 'version_permissions', 'tags']

View File

@ -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

View File

@ -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):

View File

@ -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:

View File

@ -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'):

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.'
),
)
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

View File

@ -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)

View File

@ -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):

View File

@ -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):

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)
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(

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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