Extensions list: sort_by parameter #159
@ -45,6 +45,7 @@ class AbuseReportTypeFilter(admin.SimpleListFilter):
|
|||||||
|
|
||||||
|
|
||||||
class AbuseReportAdmin(CommaSearchInAdminMixin, admin.ModelAdmin):
|
class AbuseReportAdmin(CommaSearchInAdminMixin, admin.ModelAdmin):
|
||||||
|
save_on_top = True
|
||||||
view_on_site = True
|
view_on_site = True
|
||||||
actions = ('delete_selected', 'mark_as_valid', 'mark_as_suspicious')
|
actions = ('delete_selected', 'mark_as_valid', 'mark_as_suspicious')
|
||||||
date_hierarchy = 'date_modified'
|
date_hierarchy = 'date_modified'
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit ffcc72b5cb153fc2a409c795adca82e350655aa2
|
Subproject commit 6a52e5abcc118133b8cb51137b34bf856da716c4
|
@ -330,3 +330,6 @@ ACTSTREAM_SETTINGS = {
|
|||||||
# Require file validation for other file processing (e.g. thumbnails).
|
# Require file validation for other file processing (e.g. thumbnails).
|
||||||
# Should be set for staging/production.
|
# Should be set for staging/production.
|
||||||
REQUIRE_FILE_VALIDATION = os.getenv('REQUIRE_FILE_VALIDATION', False)
|
REQUIRE_FILE_VALIDATION = os.getenv('REQUIRE_FILE_VALIDATION', False)
|
||||||
|
|
||||||
|
# Maximum number of attempts for failing background tasks
|
||||||
|
MAX_ATTEMPTS = 5
|
||||||
|
@ -1,25 +1,44 @@
|
|||||||
(function() {
|
(function() {
|
||||||
// Create function agreeWithTerms
|
// Create function agreeWithTerms
|
||||||
function agreeWithTerms() {
|
function agreeWithTerms() {
|
||||||
const agreeWithTermsInput = document.querySelector('.js-agree-with-terms-input');
|
const agreeWithTermsTrigger = document.querySelectorAll('.js-agree-with-terms-trigger');
|
||||||
|
const agreeWithTermsCheckbox = document.querySelector('.js-agree-with-terms-checkbox');
|
||||||
|
const agreeWithTermsFileInput = document.querySelector('.js-submit-form-file-input');
|
||||||
|
let agreeWithTermsBtnSubmit = document.querySelector('.js-agree-with-terms-btn-submit');
|
||||||
|
|
||||||
if (!agreeWithTermsInput) {
|
if (!agreeWithTermsCheckbox) {
|
||||||
// Stop function execution if agreeWithTermsInput is not present
|
// Stop function execution if agreeWithTermsCheckbox is not present
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
agreeWithTermsInput.addEventListener('change', function(e) {
|
// Both the file input and checkbox can trigger the submit button.
|
||||||
const agreeWithTermsBtnSubmit = document.querySelector('.js-agree-with-terms-btn-submit');
|
agreeWithTermsTrigger.forEach((el) => {
|
||||||
|
el.addEventListener('change', function() {
|
||||||
|
|
||||||
// Check if checkbox is checked
|
// Check if checkbox is checked, and file input has a file selected.
|
||||||
if (e.target.checked == true) {
|
let is_allowed = (agreeWithTermsCheckbox.checked == true) && (agreeWithTermsFileInput.value != "");
|
||||||
agreeWithTermsBtnSubmit.removeAttribute('disabled');
|
|
||||||
} else {
|
if (is_allowed) {
|
||||||
agreeWithTermsBtnSubmit.setAttribute('disabled', true);
|
agreeWithTermsBtnSubmit.removeAttribute('disabled');
|
||||||
}
|
} else {
|
||||||
|
agreeWithTermsBtnSubmit.setAttribute('disabled', true);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Create function submitFormFileInputClear
|
||||||
|
// When the user chooses a new file to upload, clear the error classes.
|
||||||
|
function submitFormFileInputClear() {
|
||||||
|
const submitFormFileInput = document.querySelector('.js-submit-form-file-input');
|
||||||
|
|
||||||
|
submitFormFileInput.addEventListener('change', function(e) {
|
||||||
|
e.target.classList.remove('is-invalid');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Create function btnBack
|
// Create function btnBack
|
||||||
function btnBack() {
|
function btnBack() {
|
||||||
const btnBack = document.querySelectorAll('.js-btn-back');
|
const btnBack = document.querySelectorAll('.js-btn-back');
|
||||||
@ -133,6 +152,7 @@
|
|||||||
// Create function init
|
// Create function init
|
||||||
function init() {
|
function init() {
|
||||||
agreeWithTerms();
|
agreeWithTerms();
|
||||||
|
submitFormFileInputClear();
|
||||||
btnBack();
|
btnBack();
|
||||||
commentForm();
|
commentForm();
|
||||||
copyInstallUrl();
|
copyInstallUrl();
|
||||||
|
@ -1,16 +1,4 @@
|
|||||||
function galleriaCloneFirstItem() {
|
/* Set the image and attributes of the large preview element. */
|
||||||
const galleriaCarrousel = document.getElementById('galleria-items');
|
|
||||||
if (!galleriaCarrousel) { return;}
|
|
||||||
|
|
||||||
let firstGalleriaItem = galleriaCarrousel.firstElementChild.cloneNode(true);
|
|
||||||
firstGalleriaItem.classList.remove('js-galleria-item-preview', 'is-active');
|
|
||||||
firstGalleriaItem.classList.add('js-expand-on-click');
|
|
||||||
firstGalleriaItem.id = 'galleria-item-lg';
|
|
||||||
|
|
||||||
document.getElementById("galleria-container").prepend(firstGalleriaItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function galleriaSetLargePreview(item) {
|
function galleriaSetLargePreview(item) {
|
||||||
let previewsContainer = document.getElementById('galleria-items');
|
let previewsContainer = document.getElementById('galleria-items');
|
||||||
let previewLarge = document.getElementById('galleria-item-lg');
|
let previewLarge = document.getElementById('galleria-item-lg');
|
||||||
@ -25,7 +13,7 @@ function galleriaSetLargePreview(item) {
|
|||||||
const galleriaVideoUrl = item.dataset.galleriaVideoUrl;
|
const galleriaVideoUrl = item.dataset.galleriaVideoUrl;
|
||||||
|
|
||||||
previewLarge.classList = item.classList;
|
previewLarge.classList = item.classList;
|
||||||
previewLarge.firstElementChild.src = galleryItem.src;
|
previewLarge.firstElementChild.src = item.href;
|
||||||
previewLarge.firstElementChild.alt = galleryItem.alt;
|
previewLarge.firstElementChild.alt = galleryItem.alt;
|
||||||
previewLarge.dataset.galleriaIndex = galleriaIndex;
|
previewLarge.dataset.galleriaIndex = galleriaIndex;
|
||||||
previewLarge.dataset.galleriaContentType = galleriaContentType;
|
previewLarge.dataset.galleriaContentType = galleriaContentType;
|
||||||
@ -36,6 +24,22 @@ function galleriaSetLargePreview(item) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Create the large preview by cloning the first thumbnail. */
|
||||||
|
function galleriaCloneFirstItem() {
|
||||||
|
const galleriaCarrousel = document.getElementById('galleria-items');
|
||||||
|
if (!galleriaCarrousel) { return;}
|
||||||
|
|
||||||
|
let firstGalleriaItem = galleriaCarrousel.firstElementChild.cloneNode(true);
|
||||||
|
document.getElementById("galleria-container").prepend(firstGalleriaItem);
|
||||||
|
|
||||||
|
firstGalleriaItem.classList.remove('js-galleria-item-preview', 'is-active');
|
||||||
|
firstGalleriaItem.classList.add('js-expand-on-click');
|
||||||
|
firstGalleriaItem.id = 'galleria-item-lg';
|
||||||
|
|
||||||
|
galleriaSetLargePreview(firstGalleriaItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function galleriaCreateCaption(captionText, overlay) {
|
function galleriaCreateCaption(captionText, overlay) {
|
||||||
let captionContainer = document.getElementById('galleria-caption');
|
let captionContainer = document.getElementById('galleria-caption');
|
||||||
|
|
||||||
@ -70,7 +74,7 @@ function galleriaScrollNavigation() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Create the overlay that will host the image and navigation controls. */
|
||||||
function galleriaCreateOverlay() {
|
function galleriaCreateOverlay() {
|
||||||
let overlay = document.createElement("div");
|
let overlay = document.createElement("div");
|
||||||
overlay.classList.add("galleria");
|
overlay.classList.add("galleria");
|
||||||
@ -79,7 +83,7 @@ function galleriaCreateOverlay() {
|
|||||||
return overlay;
|
return overlay;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Close and delete the overlay. */
|
||||||
function galleriaCloseOverlay(overlay) {
|
function galleriaCloseOverlay(overlay) {
|
||||||
if (overlay.parentNode === document.body) {
|
if (overlay.parentNode === document.body) {
|
||||||
document.body.removeChild(overlay);
|
document.body.removeChild(overlay);
|
||||||
@ -87,7 +91,7 @@ function galleriaCloseOverlay(overlay) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Create the backdrop behind the overlay. */
|
||||||
function galleriaCreateUnderlay() {
|
function galleriaCreateUnderlay() {
|
||||||
let underlay = document.createElement("div");
|
let underlay = document.createElement("div");
|
||||||
underlay.classList.add("underlay");
|
underlay.classList.add("underlay");
|
||||||
@ -103,7 +107,7 @@ function galleriaCreateLoadingPlaceholder() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Create Image element. */
|
/* Create Large Image element. */
|
||||||
function galleriaCreateMediaImage(galleriaItem, overlay, loadingPlaceholder) {
|
function galleriaCreateMediaImage(galleriaItem, overlay, loadingPlaceholder) {
|
||||||
let galleriaNewItem = new Image();
|
let galleriaNewItem = new Image();
|
||||||
galleriaNewItem.id = 'galleria-active-item';
|
galleriaNewItem.id = 'galleria-active-item';
|
||||||
@ -114,7 +118,7 @@ function galleriaCreateMediaImage(galleriaItem, overlay, loadingPlaceholder) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
galleriaNewItem.src = galleriaItem.firstElementChild.src;
|
galleriaNewItem.src = galleriaItem.href;
|
||||||
galleriaNewItem.alt = galleriaItem.firstElementChild.alt;
|
galleriaNewItem.alt = galleriaItem.firstElementChild.alt;
|
||||||
|
|
||||||
galleriaCreateCaption(galleriaNewItem.alt, overlay);
|
galleriaCreateCaption(galleriaNewItem.alt, overlay);
|
||||||
@ -157,7 +161,6 @@ function galleriaCreateMedia(galleriaItem, galleriaContentType, overlay) {
|
|||||||
galleriaCreateMediaImage(galleriaItem, overlay, loadingPlaceholder);
|
galleriaCreateMediaImage(galleriaItem, overlay, loadingPlaceholder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
overlay.appendChild(loadingPlaceholder);
|
overlay.appendChild(loadingPlaceholder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,14 +4,17 @@
|
|||||||
z-index: 0
|
z-index: 0
|
||||||
|
|
||||||
> li
|
> li
|
||||||
--border-color: var(--box-bg-color)
|
|
||||||
position: relative
|
position: relative
|
||||||
|
|
||||||
|
&:only-child
|
||||||
|
/* Remove vertical line if only item. */
|
||||||
|
.activity-item:before
|
||||||
|
display: none
|
||||||
|
|
||||||
&:first-child
|
&:first-child
|
||||||
/* Remove top half of the vertical line for first item. */
|
/* Remove top half of the vertical line for first item. */
|
||||||
.activity-status-change:before
|
.activity-item:before
|
||||||
height: 50%
|
top: 3rem
|
||||||
top: 50%
|
|
||||||
|
|
||||||
&:last-child
|
&:last-child
|
||||||
.activity-status-change:before
|
.activity-status-change:before
|
||||||
@ -109,7 +112,7 @@
|
|||||||
font-size: var(--fs-sm)
|
font-size: var(--fs-sm)
|
||||||
height: var(--spacer-4)
|
height: var(--spacer-4)
|
||||||
justify-content: center
|
justify-content: center
|
||||||
left: -.66rem
|
left: -1rem
|
||||||
position: absolute
|
position: absolute
|
||||||
width: var(--spacer-4)
|
width: var(--spacer-4)
|
||||||
|
|
||||||
|
@ -7,9 +7,9 @@
|
|||||||
|
|
||||||
.badge-notifications-count
|
.badge-notifications-count
|
||||||
background-color: var(--color-accent)
|
background-color: var(--color-accent)
|
||||||
border-color: var(--nav-global-color-bg)
|
border-color: var(--bwa-color-bg-primary)
|
||||||
border-radius: var(--spacer-2)
|
border-radius: var(--spacer-2)
|
||||||
color: var(--nav-global-color-text-active)
|
color: var(--bwa-color-text-primary)
|
||||||
display: flex
|
display: flex
|
||||||
font-size: .8rem
|
font-size: .8rem
|
||||||
+fw-bold
|
+fw-bold
|
||||||
@ -17,7 +17,7 @@
|
|||||||
left: 1.8rem
|
left: 1.8rem
|
||||||
min-width: var(--spacer)
|
min-width: var(--spacer)
|
||||||
position: absolute
|
position: absolute
|
||||||
top: var(--spacer-1)
|
top: .2rem
|
||||||
|
|
||||||
a.badge-tag
|
a.badge-tag
|
||||||
--badge-color: var(--color-text-secondary)
|
--badge-color: var(--color-text-secondary)
|
||||||
@ -45,3 +45,18 @@ a.badge-tag
|
|||||||
&-disabled-by-staff,
|
&-disabled-by-staff,
|
||||||
&-disabled-by-author
|
&-disabled-by-author
|
||||||
@extend .badge-secondary
|
@extend .badge-secondary
|
||||||
|
|
||||||
|
.badge-outline
|
||||||
|
background-color: transparent
|
||||||
|
|
||||||
|
&.badge-status
|
||||||
|
&-approved
|
||||||
|
color: var(--color-success)
|
||||||
|
&-awaiting-review
|
||||||
|
color: var(--color-info)
|
||||||
|
&-incomplete,
|
||||||
|
&-awaiting-changes,
|
||||||
|
color: var(--color-warning)
|
||||||
|
&-disabled-by-staff,
|
||||||
|
&-disabled-by-author
|
||||||
|
color: var(--color-secondary)
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
.cards-item-content
|
.cards-item-content
|
||||||
overflow: hidden
|
overflow: hidden
|
||||||
|
|
||||||
.crads-item-excerpt
|
.cards-item-excerpt
|
||||||
line-height: calc(24 / 18)
|
line-height: calc(24 / 18)
|
||||||
|
|
||||||
.cards-item-extra
|
.cards-item-extra
|
||||||
@ -25,5 +25,24 @@
|
|||||||
.stars
|
.stars
|
||||||
font-size: 1.4rem
|
font-size: 1.4rem
|
||||||
|
|
||||||
|
.cards-item-headline
|
||||||
|
color: var(--color-text-secondary)
|
||||||
|
font-size: var(--fs-xs)
|
||||||
|
+fw-normal
|
||||||
|
letter-spacing: .1rem
|
||||||
|
line-height: var(--spacer)
|
||||||
|
+margin(1, bottom)
|
||||||
|
text-transform: uppercase
|
||||||
|
|
||||||
|
.cards-item-thumbnail
|
||||||
|
background-color: var(--color-bg-secondary)
|
||||||
|
border-bottom-left-radius: 0
|
||||||
|
border-bottom-right-radius: 0
|
||||||
|
|
||||||
.cards-item-title
|
.cards-item-title
|
||||||
+padding(0, y)
|
+padding(0, y)
|
||||||
|
|
||||||
|
.is-row-add-ons,
|
||||||
|
.is-row-themes
|
||||||
|
.cards-item-headline
|
||||||
|
display: none
|
||||||
|
@ -16,14 +16,19 @@
|
|||||||
overflow: initial
|
overflow: initial
|
||||||
text-shadow: none
|
text-shadow: none
|
||||||
|
|
||||||
|
h1
|
||||||
|
margin-left: calc(var(--spacer-3))
|
||||||
|
|
||||||
.hero-content
|
.hero-content
|
||||||
margin: auto 0
|
margin: auto 0
|
||||||
|
|
||||||
.hero-subtitle
|
.hero-subtitle
|
||||||
|
margin-left: calc(var(--spacer-4) + var(--spacer-1) + var(--fs-hero-title))
|
||||||
max-width: none
|
max-width: none
|
||||||
|
|
||||||
.badge
|
.badge
|
||||||
+margin(2, right)
|
+margin(2, left)
|
||||||
|
pointer-events: none
|
||||||
|
|
||||||
.hero-overlay
|
.hero-overlay
|
||||||
background-color: transparent
|
background-color: transparent
|
||||||
@ -152,10 +157,10 @@
|
|||||||
padding: 0
|
padding: 0
|
||||||
|
|
||||||
strong
|
strong
|
||||||
font-size: var(--fs-lg)
|
font-size: var(--fs-h4)
|
||||||
|
|
||||||
i
|
i
|
||||||
font-size: var(--fs-lg)
|
font-size: var(--fs-h4)
|
||||||
+margin(3, right)
|
+margin(3, right)
|
||||||
|
|
||||||
.ext-detail-download
|
.ext-detail-download
|
||||||
@ -244,11 +249,17 @@
|
|||||||
font-size: var(--fs-sm)
|
font-size: var(--fs-sm)
|
||||||
line-height: var(--lh-sm)
|
line-height: var(--lh-sm)
|
||||||
|
|
||||||
ul
|
.details-buttons
|
||||||
+list-unstyled
|
align-items: baseline
|
||||||
margin: 0
|
display: flex
|
||||||
+padding(2, top)
|
gap: var(--spacer-2)
|
||||||
@extend .list-inline
|
justify-content: space-between
|
||||||
|
|
||||||
|
.btn-row
|
||||||
|
flex-wrap: nowrap
|
||||||
|
|
||||||
|
.btn
|
||||||
|
+padding(2, x)
|
||||||
|
|
||||||
.drag-widget
|
.drag-widget
|
||||||
align-items: center
|
align-items: center
|
||||||
@ -261,9 +272,18 @@
|
|||||||
pointer-events: none
|
pointer-events: none
|
||||||
user-select: none
|
user-select: none
|
||||||
|
|
||||||
.form-control
|
.ext-preview-thumbnail-icon
|
||||||
&[type="file"]
|
align-items: center
|
||||||
max-width: 50%
|
display: flex
|
||||||
|
font-size: var(--fs-h2)
|
||||||
|
justify-content: center
|
||||||
|
color: var(--color-text-tertiary)
|
||||||
|
|
||||||
|
&:hover i
|
||||||
|
color: var(--color-text-secondary)
|
||||||
|
|
||||||
|
i
|
||||||
|
transition: color var(--transition-speed)
|
||||||
|
|
||||||
.ext-version-history
|
.ext-version-history
|
||||||
summary
|
summary
|
||||||
@ -346,6 +366,12 @@
|
|||||||
.badge
|
.badge
|
||||||
text-decoration: none !important
|
text-decoration: none !important
|
||||||
|
|
||||||
|
.ext-review-list-name
|
||||||
|
display: flex
|
||||||
|
|
||||||
|
.extension-icon
|
||||||
|
+margin(2, right)
|
||||||
|
|
||||||
.ext-review-list-type
|
.ext-review-list-type
|
||||||
max-width: 9ch
|
max-width: 9ch
|
||||||
min-width: 9ch
|
min-width: 9ch
|
||||||
@ -392,7 +418,16 @@
|
|||||||
+padding(3, x)
|
+padding(3, x)
|
||||||
|
|
||||||
.extension-icon
|
.extension-icon
|
||||||
width: var(--fs-h1)
|
display: inline-block
|
||||||
|
vertical-align: bottom
|
||||||
|
width: var(--fs-lg)
|
||||||
|
|
||||||
|
img
|
||||||
|
border-radius: calc(var(--border-radius) / 2)
|
||||||
|
max-width: 100%
|
||||||
|
|
||||||
|
&.icon-lg
|
||||||
|
width: var(--fs-hero-title)
|
||||||
|
|
||||||
.icon-preview, .featured-image-preview
|
.icon-preview, .featured-image-preview
|
||||||
height: 9rem
|
height: 9rem
|
||||||
|
3
common/static/common/styles/_navigation.sass
Normal file
3
common/static/common/styles/_navigation.sass
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.navbar-search
|
||||||
|
input
|
||||||
|
min-width: calc(var(--spacer) * 4)
|
@ -1,22 +1,12 @@
|
|||||||
.nav-global
|
.nav-global .nav-global-nav-links
|
||||||
--nav-global-border-radius: var(--border-radius)
|
+padding(2, right)
|
||||||
--nav-global-border-radius-lg: var(--border-radius-lg)
|
|
||||||
--nav-global-button-height: calc(var(--spacer) * 2.5)
|
|
||||||
--nav-global-font-size: var(--fs-sm)
|
|
||||||
--nav-global-link-padding-y: var(--nav-global-spacer-xs);
|
|
||||||
--nav-global-navbar-height: var(--navbar-primary-height, var(--spacer-6));
|
|
||||||
--nav-global-spacer: var(--spacer)
|
|
||||||
--nav-global-spacer-sm: var(--spacer-2)
|
|
||||||
--nav-global-spacer-xs: var(--spacer-1)
|
|
||||||
|
|
||||||
.btn
|
@media (max-width: 767px)
|
||||||
&:hover
|
.nav-global .nav-global-nav-links li a:hover,
|
||||||
background-color: var(--nav-global-color-button-bg-hover)
|
.nav-global .nav-global-nav-links li a.nav-global-link-active
|
||||||
color: var(--nav-global-color-text-hover) !important
|
background-color: var(--bwa-color-accent-bg) !important
|
||||||
|
color: var(--bwa-color-accent) !important
|
||||||
|
|
||||||
.btn-primary
|
+media-xl
|
||||||
color: var(--color-accent) !important
|
.nav-global .nav-global-container
|
||||||
|
max-width: 1320px
|
||||||
input,
|
|
||||||
.form-control
|
|
||||||
height: var(--nav-global-button-height)
|
|
||||||
|
@ -48,6 +48,10 @@
|
|||||||
.style-rich-text
|
.style-rich-text
|
||||||
+style-rich-text
|
+style-rich-text
|
||||||
|
|
||||||
|
// TODO: @web-assets move style pre to web-assets
|
||||||
|
pre
|
||||||
|
+margin(3, bottom)
|
||||||
|
|
||||||
.text-accent
|
.text-accent
|
||||||
color: var(--color-accent)
|
color: var(--color-accent)
|
||||||
|
|
||||||
|
@ -29,6 +29,7 @@ $container-width: map-get($container-max-widths, 'xl')
|
|||||||
@import '_galleria.sass'
|
@import '_galleria.sass'
|
||||||
@import '_hero.sass'
|
@import '_hero.sass'
|
||||||
@import '_list.sass'
|
@import '_list.sass'
|
||||||
|
@import '_navigation.sass'
|
||||||
@import '_navigation_global.sass'
|
@import '_navigation_global.sass'
|
||||||
@import '_notifications.sass'
|
@import '_notifications.sass'
|
||||||
@import '_table.sass'
|
@import '_table.sass'
|
||||||
@ -39,19 +40,6 @@ $container-width: map-get($container-max-widths, 'xl')
|
|||||||
\:root
|
\:root
|
||||||
--z-index-galleria: 1050
|
--z-index-galleria: 1050
|
||||||
|
|
||||||
.nav-global button.nav-global-logo
|
|
||||||
+media-xs
|
|
||||||
width: 60px
|
|
||||||
|
|
||||||
/* TODO: temporarily here until it can be moved to web-assets v2. */
|
|
||||||
.nav-global-links-right
|
|
||||||
gap: 0 var(--spacer-2)
|
|
||||||
.navbar-search
|
|
||||||
margin: 0
|
|
||||||
|
|
||||||
.navbar-search
|
|
||||||
width: 160px
|
|
||||||
|
|
||||||
.profile-avatar
|
.profile-avatar
|
||||||
border-radius: 50%
|
border-radius: 50%
|
||||||
height: var(--spacer-4)
|
height: var(--spacer-4)
|
||||||
|
@ -31,19 +31,15 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="has-global-bar">
|
<body class="has-global-bar">
|
||||||
{% switch "is_alpha" %}
|
{% switch "is_beta" %}
|
||||||
<div class="site-announcement-alpha">
|
|
||||||
This platform is currently in alpha.
|
|
||||||
<a class="text-underline" href="https://projects.blender.org/infrastructure/extensions-website/issues" target="_blank">Please report any issues you may find</a>, thanks! <a class="text-underline" href="https://devtalk.blender.org/tag/extensions" target="_blank">Learn more</a>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
{% switch "is_beta" %}
|
|
||||||
<div class="site-announcement-beta">
|
<div class="site-announcement-beta">
|
||||||
The website will be officially released together with Blender 4.2.
|
This platform is currently in beta.
|
||||||
Meanwhile you can use the extensions with a <a class="text-underline" href="https://builder.blender.org/" target="_blank"> daily build</a> of Blender. <a class="text-underline" href="https://devtalk.blender.org/tag/extensions" target="_blank">Learn more</a>
|
<a class="text-underline" href="https://projects.blender.org/infrastructure/extensions-website/issues" target="_blank">Please report any issues you may find</a>, thanks!
|
||||||
|
|
||||||
|
Access extensions with a <a class="text-underline" href="https://builder.blender.org/" target="_blank"> daily build</a> of Blender. <a class="text-underline" href="https://code.blender.org/2024/05/extensions-platform-beta-release/" target="_blank">Learn more</a>
|
||||||
</div>
|
</div>
|
||||||
{% endswitch %}
|
|
||||||
{% endswitch %}
|
{% endswitch %}
|
||||||
|
|
||||||
{% if request.user.is_staff %}
|
{% if request.user.is_staff %}
|
||||||
<div class="whoosh-container">
|
<div class="whoosh-container">
|
||||||
<a href="{% url 'admin:index' %}" title='Admin' class="whoosh">
|
<a href="{% url 'admin:index' %}" title='Admin' class="whoosh">
|
||||||
@ -52,10 +48,11 @@
|
|||||||
{% block admin_button_page %}{% endblock %}
|
{% block admin_button_page %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{# TODO: improve nav-global layout for small screens #}
|
||||||
<div class="nav-global">
|
<div class="nav-global">
|
||||||
<div class="nav-global-container">
|
<div class="nav-global-container">
|
||||||
<nav>
|
<nav>
|
||||||
<div class="site-beta-logo-container">
|
<div class="d-md-block d-none site-beta-logo-container text-nowrap">
|
||||||
<a href="/" class="nav-global-logo{% if request.get_full_path == '/' %} is-active{% endif %}">
|
<a href="/" class="nav-global-logo{% if request.get_full_path == '/' %} is-active{% endif %}">
|
||||||
<svg fill-rule="nonzero" viewBox="0 0 200 162.05">
|
<svg fill-rule="nonzero" viewBox="0 0 200 162.05">
|
||||||
<path
|
<path
|
||||||
@ -73,55 +70,48 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="nav-global-logo js-dropdown-toggle" data-toggle-menu-id="nav-global-nav-links">
|
<button class="nav-global-logo js-dropdown-toggle" data-toggle-menu-id="nav-global-nav-links">
|
||||||
<svg fill-rule="nonzero" viewBox="0 0 850.2 162.05">
|
<svg fill-rule="nonzero" viewBox="0 0 200 162.05">
|
||||||
<path
|
<path
|
||||||
d="M61.1 104.56c.05 2.6.88 7.66 2.12 11.61a61.27 61.27 0 0 0 13.24 22.92 68.39 68.39 0 0 0 23.17 16.64 74.46 74.46 0 0 0 30.42 6.32 74.52 74.52 0 0 0 30.4-6.42 68.87 68.87 0 0 0 23.15-16.7 61.79 61.79 0 0 0 13.23-22.97 58.06 58.06 0 0 0 2.07-25.55 59.18 59.18 0 0 0-8.44-23.1 64.45 64.45 0 0 0-15.4-16.98h.02L112.76 2.46l-.16-.12c-4.09-3.14-10.96-3.13-15.46.02-4.55 3.18-5.07 8.44-1.02 11.75l-.02.02 26 21.14-79.23.08h-.1c-6.55.01-12.85 4.3-14.1 9.74-1.27 5.53 3.17 10.11 9.98 10.14v.02l40.15-.07-71.66 55-.27.2c-6.76 5.18-8.94 13.78-4.69 19.23 4.32 5.54 13.51 5.55 20.34.03l39.1-32s-.56 4.32-.52 6.91zm100.49 14.47c-8.06 8.2-19.34 12.86-31.54 12.89-12.23.02-23.5-4.6-31.57-12.79-3.93-4-6.83-8.59-8.61-13.48a35.57 35.57 0 0 1 2.34-29.25 39.1 39.1 0 0 1 9.58-11.4 44.68 44.68 0 0 1 28.24-9.85 44.59 44.59 0 0 1 28.24 9.77 38.94 38.94 0 0 1 9.58 11.36 35.58 35.58 0 0 1 4.33 14.18 35.1 35.1 0 0 1-1.98 15.05 37.7 37.7 0 0 1-8.61 13.52zm-57.6-27.91a23.55 23.55 0 0 1 8.55-16.68 28.45 28.45 0 0 1 18.39-6.57 28.5 28.5 0 0 1 18.38 6.57 23.57 23.57 0 0 1 8.55 16.67c.37 6.83-2.37 13.19-7.2 17.9a28.18 28.18 0 0 1-19.73 7.79c-7.83 0-14.84-3-19.75-7.8a23.13 23.13 0 0 1-7.19-17.88z" />
|
d="M61.1 104.56c.05 2.6.88 7.66 2.12 11.61a61.27 61.27 0 0 0 13.24 22.92 68.39 68.39 0 0 0 23.17 16.64 74.46 74.46 0 0 0 30.42 6.32 74.52 74.52 0 0 0 30.4-6.42 68.87 68.87 0 0 0 23.15-16.7 61.79 61.79 0 0 0 13.23-22.97 58.06 58.06 0 0 0 2.07-25.55 59.18 59.18 0 0 0-8.44-23.1 64.45 64.45 0 0 0-15.4-16.98h.02L112.76 2.46l-.16-.12c-4.09-3.14-10.96-3.13-15.46.02-4.55 3.18-5.07 8.44-1.02 11.75l-.02.02 26 21.14-79.23.08h-.1c-6.55.01-12.85 4.3-14.1 9.74-1.27 5.53 3.17 10.11 9.98 10.14v.02l40.15-.07-71.66 55-.27.2c-6.76 5.18-8.94 13.78-4.69 19.23 4.32 5.54 13.51 5.55 20.34.03l39.1-32s-.56 4.32-.52 6.91zm100.49 14.47c-8.06 8.2-19.34 12.86-31.54 12.89-12.23.02-23.5-4.6-31.57-12.79-3.93-4-6.83-8.59-8.61-13.48a35.57 35.57 0 0 1 2.34-29.25 39.1 39.1 0 0 1 9.58-11.4 44.68 44.68 0 0 1 28.24-9.85 44.59 44.59 0 0 1 28.24 9.77 38.94 38.94 0 0 1 9.58 11.36 35.58 35.58 0 0 1 4.33 14.18 35.1 35.1 0 0 1-1.98 15.05 37.7 37.7 0 0 1-8.61 13.52zm-57.6-27.91a23.55 23.55 0 0 1 8.55-16.68 28.45 28.45 0 0 1 18.39-6.57 28.5 28.5 0 0 1 18.38 6.57 23.57 23.57 0 0 1 8.55 16.67c.37 6.83-2.37 13.19-7.2 17.9a28.18 28.18 0 0 1-19.73 7.79c-7.83 0-14.84-3-19.75-7.8a23.13 23.13 0 0 1-7.19-17.88z" />
|
||||||
</svg>
|
</svg>
|
||||||
<svg class="nav-global-icon nav-global-icon-dropdown-toggle" height="100px" width="100px" viewBox="0 0 1000 1000">
|
<strong>Extensions</strong>
|
||||||
<path
|
<i class="i-chevron-down"></i>
|
||||||
d="m 206.53824,376.41174 a 42,42 0 0 1 71,-29 l 221,220 220,-220 a 42,42 0 1 1 59,59 l -250,250 a 42,42 0 0 1 -59,0 l -250,-250 a 42,42 0 0 1 -12,-30 z" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<ul class="nav-global-nav-links nav-global-dropdown js-dropdown-menu" id="nav-global-nav-links">
|
<ul class="flex-nowrap nav-global-nav-links nav-global-dropdown js-dropdown-menu" id="nav-global-nav-links">
|
||||||
|
<li class="d-md-none">
|
||||||
|
<a href="/" class="{% if request.get_full_path == '/' %}nav-global-link-active{% endif %}">
|
||||||
|
Home
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'extensions:by-type' type_slug='add-ons' %}" class="{% if '/add-ons/' in request.get_full_path %}is-active{% endif %}">
|
<a href="{% url 'extensions:by-type' type_slug='add-ons' %}" class="{% if '/add-ons/' in request.get_full_path %}nav-global-link-active{% endif %}">
|
||||||
Add-ons
|
Add-ons
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'extensions:by-type' type_slug='themes' %}" class="{% if '/themes/' in request.get_full_path %}is-active{% endif %}">
|
<a href="{% url 'extensions:by-type' type_slug='themes' %}" class="{% if '/themes/' in request.get_full_path %}nav-global-link-active{% endif %}">
|
||||||
Themes
|
Themes
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'reviewers:approval-queue' %}" class="{% if '/approval-queue/' in request.get_full_path %}is-active{% endif %}">
|
<a href="{% url 'reviewers:approval-queue' %}" class="{% if '/approval-queue/' in request.get_full_path %}nav-global-link-active{% endif %}">
|
||||||
Approval Queue
|
Approval Queue
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'flatpage-about' %}" class="{% if '/about/' in request.get_full_path %}is-active{% endif %}">
|
<a href="{% url 'flatpage-about' %}" class="{% if '/about/' in request.get_full_path %}nav-global-link-active{% endif %}">
|
||||||
About
|
About
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<ul class="nav-global-links-right">
|
<ul class="nav-global-links-right">
|
||||||
<li>
|
<li class="d-lg-inline-flex d-none">
|
||||||
<button class="js-toggle-theme-btn px-2"><i class="js-toggle-theme-btn-icon i-adjust"></i></button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<search>
|
<search>
|
||||||
<form action="{% url "extensions:search" %}" method="GET" class="navbar-search">
|
<form action="{% url "extensions:search" %}" class="navbar-search" method="GET">
|
||||||
<input type="text" name="q" class="form-control"
|
<input aria-label="Search" aria-describedby="nav-search-button" class="form-control" type="text" placeholder="Search..." {% if request.GET.q %} value="{{ request.GET.q }}" {% else %} {% endif %}>
|
||||||
{% if request.GET.q %}
|
<button id="nav-search-button" type="submit">
|
||||||
value="{{ request.GET.q }}"
|
|
||||||
{% else %}
|
|
||||||
placeholder="Search..."
|
|
||||||
{% endif %}
|
|
||||||
aria-label="Search"
|
|
||||||
aria-describedby="nav-search-button">
|
|
||||||
<button type="submit" id="nav-search-button">
|
|
||||||
<i class="i-search"></i>
|
<i class="i-search"></i>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@ -129,17 +119,18 @@
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
{% block nav-upload %}
|
{% block nav-upload %}
|
||||||
<li>
|
<li class="d-lg-inline-flex d-none">
|
||||||
<a href="{% url 'extensions:submit' %}" class="btn btn-primary">
|
<a class="nav-global-btn nav-global-btn-primary" href="{% url 'extensions:submit' %}"><i class="i-upload"></i><span>Upload Extension</span></a>
|
||||||
<i class="i-upload"></i>
|
</li>
|
||||||
<span>Upload Extension</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endblock nav-upload %}
|
{% endblock nav-upload %}
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<button class="js-toggle-theme-btn px-2"><i class="js-toggle-theme-btn-icon i-adjust"></i></button>
|
||||||
|
</li>
|
||||||
|
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<li>
|
<li>
|
||||||
<a class="btn btn-link position-relative px-2" href="{% url 'notifications:notifications' %}">
|
<a class="nav-global-btn position-relative" href="{% url 'notifications:notifications' %}">
|
||||||
{% with unread_notification_count=user|unread_notification_count %}
|
{% with unread_notification_count=user|unread_notification_count %}
|
||||||
{% if unread_notification_count %}
|
{% if unread_notification_count %}
|
||||||
<div class="badge badge-notifications-count">{{ unread_notification_count }}</div>
|
<div class="badge badge-notifications-count">{{ unread_notification_count }}</div>
|
||||||
@ -148,7 +139,7 @@
|
|||||||
<i class="i-bell"></i>
|
<i class="i-bell"></i>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item dropdown">
|
<li class="dropdown">
|
||||||
<button id="navbarDropdown" aria-expanded="false" aria-haspopup="true" data-toggle-menu-id="nav-account-dropdown" role="button" class="nav-link dropdown-toggle js-dropdown-toggle pe-3 px-2">
|
<button id="navbarDropdown" aria-expanded="false" aria-haspopup="true" data-toggle-menu-id="nav-account-dropdown" role="button" class="nav-link dropdown-toggle js-dropdown-toggle pe-3 px-2">
|
||||||
<i class="i-user"></i>
|
<i class="i-user"></i>
|
||||||
<i class="i-chevron-down"></i>
|
<i class="i-chevron-down"></i>
|
||||||
@ -170,6 +161,12 @@
|
|||||||
<li class="dropdown-divider"></li>
|
<li class="dropdown-divider"></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<li class="d-xl-none">
|
||||||
|
<a href="{% url 'extensions:submit' %}" class="dropdown-item">
|
||||||
|
<i class="i-upload"></i> {% trans 'Upload Extension' %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'extensions:manage-list' %}" class="dropdown-item">
|
<a href="{% url 'extensions:manage-list' %}" class="dropdown-item">
|
||||||
<i class="i-puzzle"></i> {% trans 'My Extensions' %}
|
<i class="i-puzzle"></i> {% trans 'My Extensions' %}
|
||||||
|
@ -1,38 +1,40 @@
|
|||||||
{% load i18n common %}
|
{% load i18n common %}
|
||||||
{% if num_pages > 1 %}
|
{% if num_pages > 1 %}
|
||||||
<ol class="pagination">
|
<ul class="pagination">
|
||||||
{% if pager.has_previous %}
|
{% if pager.has_previous %}
|
||||||
<li>
|
<li class="page-item page-first">
|
||||||
<a rel="prev" href="{{ pager.url|urlparams:pager.previous_page_number }}">
|
<a href="?page=1">{% trans "First" %}</a>
|
||||||
{{ _('Prev') }}
|
</li>
|
||||||
</a>
|
|
||||||
|
<li class="page-item page-prev">
|
||||||
|
<a href="?page={{ pager.previous_page_number }}" rel="prev"><i class="i-chevron-left"></i> {% trans "Prev" %}</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if pager.dotted_lower %}
|
|
||||||
<li><a href="{{ pager.url|urlparams:1 }}">{{ 1 }}</a></li>
|
|
||||||
<li class="skip">…</li>
|
|
||||||
{% endif %}
|
|
||||||
{% for x in pager.page_range %}
|
{% for x in pager.page_range %}
|
||||||
<li {{ x|class_selected:pager.number }}>
|
<li class="page-item {{ x|class_selected:pager.number }}">
|
||||||
<a href="{{ pager.url|urlparams:x }}">{{ x }}</a>
|
<a href="?page={{ x }}">{{ x }}</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if pager.dotted_upper %}
|
|
||||||
<li class="skip">…</li>
|
|
||||||
<li><a href="{{ pager.url|urlparams:num_pages }}">{{ num_pages }}</a></li>
|
|
||||||
{% endif %}
|
|
||||||
{% if pager.has_next %}
|
{% if pager.has_next %}
|
||||||
<li>
|
<li class="page-item page-next">
|
||||||
<a rel="next" href="{{ pager.url|urlparams:pager.next_page_number }}">
|
<a href="?page={{ pager.next_page_number }}" rel="next">{% trans "Next" %} <i class="i-chevron-right"></i></a>
|
||||||
{{ _('Next') }}
|
</li>
|
||||||
</a>
|
|
||||||
|
<li class="page-item page-last">
|
||||||
|
<a href="?page={{ num_pages }}">{% trans "Last" %}</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ol>
|
</ul>
|
||||||
<div class="num-results">
|
|
||||||
{% blocktranslate with begin=pager.start_index end=pager.end_index count=count %}
|
{# TODO: add paginator page count if needed #}
|
||||||
Results <strong>{{ begin }}</strong>–<strong>{{ end }}</strong>
|
{% comment %}
|
||||||
of <strong>{{ count }}</strong>
|
<div class="num-results">
|
||||||
{% endblocktranslate %}
|
{% blocktranslate with begin=pager.start_index end=pager.end_index count=count %}
|
||||||
</div>
|
Results <strong>{{ begin }}</strong>–<strong>{{ end }}</strong>
|
||||||
|
of <strong>{{ count }}</strong>
|
||||||
|
{% endblocktranslate %}
|
||||||
|
</div>
|
||||||
|
{% endcomment %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -76,7 +76,7 @@ def urlparams(url, page, *args, **kwargs):
|
|||||||
@register.filter
|
@register.filter
|
||||||
def class_selected(a, b):
|
def class_selected(a, b):
|
||||||
"""Return ``'class="selected"'`` if ``a == b``."""
|
"""Return ``'class="selected"'`` if ``a == b``."""
|
||||||
return mark_safe('class="selected"' if a == b else '')
|
return mark_safe('active' if a == b else '')
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
|
@ -6,7 +6,7 @@ from mdgen import MarkdownPostProvider
|
|||||||
import factory
|
import factory
|
||||||
import factory.fuzzy
|
import factory.fuzzy
|
||||||
|
|
||||||
from extensions.models import Extension, Version, Tag, Preview
|
from extensions.models import Extension, Version, Tag, Preview, Platform
|
||||||
from ratings.models import Rating
|
from ratings.models import Rating
|
||||||
|
|
||||||
fake_markdown = Faker()
|
fake_markdown = Faker()
|
||||||
@ -83,6 +83,17 @@ class VersionFactory(DjangoModelFactory):
|
|||||||
RatingFactory, size=lambda: random.randint(1, 50), factory_related_name='version'
|
RatingFactory, size=lambda: random.randint(1, 50), factory_related_name='version'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@factory.post_generation
|
||||||
|
def platforms(self, create, extracted, **kwargs):
|
||||||
|
if not create:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not extracted:
|
||||||
|
return
|
||||||
|
|
||||||
|
tags = Platform.objects.filter(slug__in=extracted)
|
||||||
|
self.platforms.add(*tags)
|
||||||
|
|
||||||
@factory.post_generation
|
@factory.post_generation
|
||||||
def tags(self, create, extracted, **kwargs):
|
def tags(self, create, extracted, **kwargs):
|
||||||
if not create:
|
if not create:
|
||||||
|
@ -13,13 +13,13 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class MaintainerInline(admin.TabularInline):
|
class MaintainerInline(admin.TabularInline):
|
||||||
model = Maintainer
|
model = Maintainer
|
||||||
raw_id_fields = ('user',)
|
autocomplete_fields = ('user',)
|
||||||
extra = 0
|
extra = 0
|
||||||
|
|
||||||
|
|
||||||
class PreviewInline(NoAddDeleteMixin, admin.TabularInline):
|
class PreviewInline(NoAddDeleteMixin, admin.TabularInline):
|
||||||
model = Extension.previews.through
|
model = Extension.previews.through
|
||||||
raw_id_fields = ('file',)
|
autocomplete_fields = ('file',)
|
||||||
show_change_link = True
|
show_change_link = True
|
||||||
can_add = False
|
can_add = False
|
||||||
extra = 0
|
extra = 0
|
||||||
@ -34,6 +34,8 @@ class VersionInline(NoAddDeleteMixin, admin.TabularInline):
|
|||||||
|
|
||||||
|
|
||||||
class ExtensionAdmin(admin.ModelAdmin):
|
class ExtensionAdmin(admin.ModelAdmin):
|
||||||
|
save_on_top = True
|
||||||
|
date_hierarchy = 'date_created'
|
||||||
list_display = (
|
list_display = (
|
||||||
'__str__',
|
'__str__',
|
||||||
'type',
|
'type',
|
||||||
@ -43,13 +45,31 @@ class ExtensionAdmin(admin.ModelAdmin):
|
|||||||
'view_count',
|
'view_count',
|
||||||
'average_score',
|
'average_score',
|
||||||
)
|
)
|
||||||
list_filter = ('type', 'status')
|
list_filter = (
|
||||||
search_fields = ('id', '^slug', 'name')
|
'type',
|
||||||
|
'status',
|
||||||
|
'is_listed',
|
||||||
|
'date_approved',
|
||||||
|
'date_created',
|
||||||
|
'date_modified',
|
||||||
|
'date_status_changed',
|
||||||
|
)
|
||||||
|
search_fields = (
|
||||||
|
'id',
|
||||||
|
'^slug',
|
||||||
|
'name',
|
||||||
|
'authors__email',
|
||||||
|
'authors__full_name',
|
||||||
|
'authors__username',
|
||||||
|
'team__name',
|
||||||
|
'versions__file__user__email',
|
||||||
|
'versions__file__user__full_name',
|
||||||
|
'versions__file__user__username',
|
||||||
|
)
|
||||||
inlines = (MaintainerInline, PreviewInline, VersionInline)
|
inlines = (MaintainerInline, PreviewInline, VersionInline)
|
||||||
readonly_fields = (
|
readonly_fields = (
|
||||||
'id',
|
'id',
|
||||||
'type',
|
'type',
|
||||||
'name',
|
|
||||||
'slug',
|
'slug',
|
||||||
'date_created',
|
'date_created',
|
||||||
'date_status_changed',
|
'date_status_changed',
|
||||||
@ -61,9 +81,10 @@ class ExtensionAdmin(admin.ModelAdmin):
|
|||||||
'download_count',
|
'download_count',
|
||||||
'view_count',
|
'view_count',
|
||||||
'website',
|
'website',
|
||||||
|
'icon',
|
||||||
|
'featured_image',
|
||||||
)
|
)
|
||||||
raw_id_fields = ('team',)
|
autocomplete_fields = ('team',)
|
||||||
autocomplete_fields = ('icon', 'featured_image')
|
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(
|
(
|
||||||
@ -71,7 +92,7 @@ class ExtensionAdmin(admin.ModelAdmin):
|
|||||||
{
|
{
|
||||||
'fields': (
|
'fields': (
|
||||||
('team',),
|
('team',),
|
||||||
('id', 'type'),
|
('id', 'type', 'extension_id'),
|
||||||
(
|
(
|
||||||
'date_created',
|
'date_created',
|
||||||
'date_status_changed',
|
'date_status_changed',
|
||||||
@ -115,6 +136,8 @@ class ExtensionAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
|
|
||||||
class VersionAdmin(admin.ModelAdmin):
|
class VersionAdmin(admin.ModelAdmin):
|
||||||
|
save_on_top = True
|
||||||
|
date_hierarchy = 'date_created'
|
||||||
list_display = (
|
list_display = (
|
||||||
'__str__',
|
'__str__',
|
||||||
'extension',
|
'extension',
|
||||||
@ -125,12 +148,23 @@ class VersionAdmin(admin.ModelAdmin):
|
|||||||
'file__status',
|
'file__status',
|
||||||
'blender_version_min',
|
'blender_version_min',
|
||||||
'blender_version_max',
|
'blender_version_max',
|
||||||
|
'permissions',
|
||||||
|
'date_created',
|
||||||
|
'date_modified',
|
||||||
'licenses',
|
'licenses',
|
||||||
'tags',
|
'tags',
|
||||||
'permissions',
|
'platforms',
|
||||||
)
|
)
|
||||||
search_fields = ('id', 'extension__slug', 'extension__name')
|
search_fields = (
|
||||||
raw_id_fields = ('extension', 'file')
|
'id',
|
||||||
|
'extension__slug',
|
||||||
|
'extension__name',
|
||||||
|
'extension__extension_id',
|
||||||
|
'file__user__email',
|
||||||
|
'file__user__full_name',
|
||||||
|
'file__user__username',
|
||||||
|
)
|
||||||
|
autocomplete_fields = ('extension', 'file')
|
||||||
readonly_fields = (
|
readonly_fields = (
|
||||||
'id',
|
'id',
|
||||||
'tagline',
|
'tagline',
|
||||||
@ -157,6 +191,7 @@ class VersionAdmin(admin.ModelAdmin):
|
|||||||
'tags',
|
'tags',
|
||||||
'file',
|
'file',
|
||||||
'permissions',
|
'permissions',
|
||||||
|
'platforms',
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -193,6 +228,10 @@ class LicenseAdmin(admin.ModelAdmin):
|
|||||||
list_display = ('name', 'slug', 'url')
|
list_display = ('name', 'slug', 'url')
|
||||||
|
|
||||||
|
|
||||||
|
class PlatformAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'slug')
|
||||||
|
|
||||||
|
|
||||||
class TagAdmin(admin.ModelAdmin):
|
class TagAdmin(admin.ModelAdmin):
|
||||||
model = Tag
|
model = Tag
|
||||||
list_display = ('name', 'slug', 'type')
|
list_display = ('name', 'slug', 'type')
|
||||||
@ -213,5 +252,6 @@ 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)
|
admin.site.register(models.License, LicenseAdmin)
|
||||||
|
admin.site.register(models.Platform, PlatformAdmin)
|
||||||
admin.site.register(models.Tag, TagAdmin)
|
admin.site.register(models.Tag, TagAdmin)
|
||||||
admin.site.register(models.VersionPermission, VersionPermissionAdmin)
|
admin.site.register(models.VersionPermission, VersionPermissionAdmin)
|
||||||
|
@ -20,6 +20,6 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='extension',
|
model_name='extension',
|
||||||
name='icon',
|
name='icon',
|
||||||
field=models.OneToOneField(help_text='A 256 x 256 icon representing this extension.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='icon_of', to='files.file'),
|
field=models.OneToOneField(help_text='A 256 x 256 PNG icon representing this extension.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='icon_of', to='files.file'),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
38
extensions/migrations/0030_platform_version_platforms.py
Normal file
38
extensions/migrations/0030_platform_version_platforms.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Generated by Django 4.2.11 on 2024-05-14 11:06
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def populate_platforms(apps, schema_editor):
|
||||||
|
Platform = apps.get_model('extensions', 'Platform')
|
||||||
|
for p in ["windows-amd64", "windows-arm64", "macos-x86_64", "macos-arm64", "linux-x86_64"]:
|
||||||
|
Platform(name=p, slug=p).save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extensions', '0029_remove_extensionreviewerflags_extension_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Platform',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('date_created', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('date_modified', models.DateTimeField(auto_now=True)),
|
||||||
|
('name', models.CharField(max_length=128, unique=True)),
|
||||||
|
('slug', models.SlugField(help_text='A platform tag, see https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/', unique=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='version',
|
||||||
|
name='platforms',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='versions', to='extensions.platform'),
|
||||||
|
),
|
||||||
|
migrations.RunPython(populate_platforms),
|
||||||
|
]
|
@ -99,6 +99,23 @@ class License(CreatedModifiedMixin, models.Model):
|
|||||||
return cls.objects.filter(slug__startswith=slug).first()
|
return cls.objects.filter(slug__startswith=slug).first()
|
||||||
|
|
||||||
|
|
||||||
|
class Platform(CreatedModifiedMixin, models.Model):
|
||||||
|
name = models.CharField(max_length=128, null=False, blank=False, unique=True)
|
||||||
|
slug = models.SlugField(
|
||||||
|
blank=False,
|
||||||
|
null=False,
|
||||||
|
help_text='A platform tag, see https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/', # noqa
|
||||||
|
unique=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f'{self.name}'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_slug(cls, slug: str):
|
||||||
|
return cls.objects.filter(slug__startswith=slug).first()
|
||||||
|
|
||||||
|
|
||||||
class ExtensionManager(models.Manager):
|
class ExtensionManager(models.Manager):
|
||||||
@property
|
@property
|
||||||
def listed(self):
|
def listed(self):
|
||||||
@ -162,7 +179,7 @@ class Extension(CreatedModifiedMixin, RatingMixin, TrackChangesMixin, models.Mod
|
|||||||
null=True,
|
null=True,
|
||||||
blank=False,
|
blank=False,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
help_text="A 256 x 256 icon representing this extension.",
|
help_text="A 256 x 256 PNG icon representing this extension.",
|
||||||
)
|
)
|
||||||
previews = FilterableManyToManyField('files.File', through='Preview', related_name='extensions')
|
previews = FilterableManyToManyField('files.File', through='Preview', related_name='extensions')
|
||||||
|
|
||||||
@ -441,8 +458,9 @@ class VersionManager(models.Manager):
|
|||||||
|
|
||||||
def update_or_create(self, *args, **kwargs):
|
def update_or_create(self, *args, **kwargs):
|
||||||
# Stash the ManyToMany to be created after the Version has a valid ID already
|
# Stash the ManyToMany to be created after the Version has a valid ID already
|
||||||
permissions = kwargs.pop('permissions', [])
|
|
||||||
licenses = kwargs.pop('licenses', [])
|
licenses = kwargs.pop('licenses', [])
|
||||||
|
permissions = kwargs.pop('permissions', [])
|
||||||
|
platforms = kwargs.pop('platforms', [])
|
||||||
tags = kwargs.pop('tags', [])
|
tags = kwargs.pop('tags', [])
|
||||||
|
|
||||||
version, result = super().update_or_create(*args, **kwargs)
|
version, result = super().update_or_create(*args, **kwargs)
|
||||||
@ -450,6 +468,7 @@ class VersionManager(models.Manager):
|
|||||||
# Add the ManyToMany to the already initialized Version
|
# Add the ManyToMany to the already initialized Version
|
||||||
version.set_initial_licenses(licenses)
|
version.set_initial_licenses(licenses)
|
||||||
version.set_initial_permissions(permissions)
|
version.set_initial_permissions(permissions)
|
||||||
|
version.set_initial_platforms(platforms)
|
||||||
version.set_initial_tags(tags)
|
version.set_initial_tags(tags)
|
||||||
return version, result
|
return version, result
|
||||||
|
|
||||||
@ -518,6 +537,7 @@ class Version(CreatedModifiedMixin, RatingMixin, TrackChangesMixin, models.Model
|
|||||||
)
|
)
|
||||||
|
|
||||||
permissions = models.ManyToManyField(VersionPermission, related_name='versions', blank=True)
|
permissions = models.ManyToManyField(VersionPermission, related_name='versions', blank=True)
|
||||||
|
platforms = models.ManyToManyField(Platform, related_name='versions', blank=True)
|
||||||
|
|
||||||
release_notes = models.TextField(help_text=common.help_texts.markdown, blank=True)
|
release_notes = models.TextField(help_text=common.help_texts.markdown, blank=True)
|
||||||
|
|
||||||
@ -546,6 +566,14 @@ class Version(CreatedModifiedMixin, RatingMixin, TrackChangesMixin, models.Model
|
|||||||
permission = VersionPermission.get_by_slug(permission_name)
|
permission = VersionPermission.get_by_slug(permission_name)
|
||||||
self.permissions.add(permission)
|
self.permissions.add(permission)
|
||||||
|
|
||||||
|
def set_initial_platforms(self, _platforms):
|
||||||
|
if not _platforms:
|
||||||
|
return
|
||||||
|
|
||||||
|
for slug in _platforms:
|
||||||
|
platform = Platform.get_by_slug(slug)
|
||||||
|
self.platforms.add(platform)
|
||||||
|
|
||||||
def set_initial_licenses(self, _licenses):
|
def set_initial_licenses(self, _licenses):
|
||||||
if not _licenses:
|
if not _licenses:
|
||||||
return
|
return
|
||||||
|
@ -30,27 +30,24 @@ function appendImageUploadForm() {
|
|||||||
<div class="previews-list-item">
|
<div class="previews-list-item">
|
||||||
<div class="align-items-center d-flex previews-list-item-thumbnail ps-3">
|
<div class="align-items-center d-flex previews-list-item-thumbnail ps-3">
|
||||||
<div class="js-input-img-thumbnail previews-list-item-thumbnail-img" title="Preview">
|
<div class="js-input-img-thumbnail previews-list-item-thumbnail-img" title="Preview">
|
||||||
<div class="align-items-center d-flex js-input-img-thumbnail-icon justify-content-center">
|
<div class="ext-preview-thumbnail-icon js-input-img-thumbnail-icon">
|
||||||
<i class="i-image"></i>
|
<i class="i-image"></i>/<i class="i-reel"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="details flex-grow-1">
|
<div class="details">
|
||||||
<div class="mb-2">
|
<div>
|
||||||
<label for="${formsetPrefix}-${i}-caption">Caption</label>
|
<label for="${formsetPrefix}-${i}-caption">Image or Video</label>
|
||||||
<input class="js-input-img-caption form-control" id="${formsetPrefix}-${i}-caption" type="text" maxlength="255" name="${formsetPrefix}-${i}-caption" placeholder="Describe the preview">
|
<input class="js-input-img-caption form-control" id="${formsetPrefix}-${i}-caption" type="text" maxlength="255" name="${formsetPrefix}-${i}-caption" placeholder="Description">
|
||||||
</div>
|
</div>
|
||||||
<div class="align-items-center d-flex justify-content-between">
|
<div class="details-buttons">
|
||||||
<input accept="image/jpg,image/jpeg,image/png,image/webp,video/mp4" class="form-control form-control-sm js-input-img" id="id_${formsetPrefix}-${i}-source" type="file" name="${formsetPrefix}-${i}-source">
|
<input accept="image/jpg,image/jpeg,image/png,image/webp,video/mp4" class="form-control form-control-sm js-input-img" id="id_${formsetPrefix}-${i}-source" type="file" name="${formsetPrefix}-${i}-source">
|
||||||
<ul class="pt-0">
|
<div class="btn-row">
|
||||||
<li>
|
<button class="btn btn-link btn-sm js-btn-remove-img-upload-form"><i class="i-trash"></i> Delete</button>
|
||||||
<button class="btn btn-link btn-sm js-btn-reset-img-upload-form ps-2 pe-0"><i class="i-refresh"></i> Reset</button>
|
<button class="btn btn-link btn-sm js-btn-reset-img-upload-form"><i class="i-refresh"></i> Reset</button>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
|
||||||
<button class="btn btn-link btn-sm js-btn-remove-img-upload-form ps-2 pe-0"><i class="i-trash"></i> Delete</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-text">A JPEG, PNG or WebP image, or an MP4 video.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endblock hero_breadcrumbs %}
|
{% endblock hero_breadcrumbs %}
|
||||||
|
|
||||||
<h1>{% include "extensions/components/icon.html" %} {{ extension.name }}</h1>
|
<h1>{% include "extensions/components/icon.html" with classes="icon-lg" %} {{ extension.name }}</h1>
|
||||||
|
|
||||||
<div class="hero-subtitle">
|
<div class="hero-subtitle">
|
||||||
{% if latest.tagline %}
|
{% if latest.tagline %}
|
||||||
@ -26,12 +26,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="ext-detail-authors">
|
<div class="ext-detail-authors">
|
||||||
{% if not extension.is_approved %}
|
|
||||||
<a href="{{ extension.get_review_url }}" class="badge badge-status-{{ extension.get_status_display|slugify }}">
|
|
||||||
{{ extension.get_status_display }}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<a href="{% url 'extensions:by-type' type_slug=extension.type_slug %}">{{ extension.get_type_display }}</a>
|
<a href="{% url 'extensions:by-type' type_slug=extension.type_slug %}">{{ extension.get_type_display }}</a>
|
||||||
|
|
||||||
{% if extension.team %}
|
{% if extension.team %}
|
||||||
@ -43,6 +37,12 @@
|
|||||||
{% elif extension.authors.count %}
|
{% elif extension.authors.count %}
|
||||||
{% trans 'by' %} {% include "extensions/components/authors.html" %}
|
{% trans 'by' %} {% include "extensions/components/authors.html" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if not extension.is_approved %}
|
||||||
|
<span class="badge badge-outline badge-status-{{ extension.get_status_display|slugify }}">
|
||||||
|
{{ extension.get_status_display }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,14 +1,23 @@
|
|||||||
{% load common filters static %}
|
{% load common filters static %}
|
||||||
{% static "common/images/no-image_640x360.png" as featured_image_missing %}
|
{% with latest=extension.latest_version type_display=extension.get_type_display %}
|
||||||
{% with latest=extension.latest_version %}
|
|
||||||
{% firstof extension.featured_image.thumbnail_360p_url featured_image_missing as thumbnail_360p_url %}
|
|
||||||
<div class="cards-item">
|
<div class="cards-item">
|
||||||
<div class="cards-item-content">
|
<div class="cards-item-content">
|
||||||
<a href="{{ extension.get_absolute_url }}">
|
<a href="{{ extension.get_absolute_url }}">
|
||||||
<div class="cards-item-thumbnail">
|
{% with featured_image=extension.featured_image.thumbnail_360p_url %}
|
||||||
<img alt="{{ extension.name }}" src="{{ thumbnail_360p_url }}" title="{{ extension.name }}">
|
<div class="cards-item-thumbnail">
|
||||||
</div>
|
{% if featured_image %}
|
||||||
|
<img alt="{{ extension.name }}" src="{{ featured_image }}" title="{{ extension.name }}">
|
||||||
|
{% else %}
|
||||||
|
<div class="align-items-center d-flex justify-content-center position-absolute">
|
||||||
|
<i class="fs-3 {% if type_display == "Theme" %}i-brush{% else %}i-puzzle{% endif %}"></i>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
</a>
|
</a>
|
||||||
|
<div class="cards-item-headline">
|
||||||
|
{{ type_display }}
|
||||||
|
</div>
|
||||||
<h3 class="cards-item-title">
|
<h3 class="cards-item-title">
|
||||||
<a href="{{ extension.get_absolute_url }}">{{ extension.name }}</a>
|
<a href="{{ extension.get_absolute_url }}">{{ extension.name }}</a>
|
||||||
</h3>
|
</h3>
|
||||||
@ -48,7 +57,7 @@
|
|||||||
|
|
||||||
{% if show_type %}
|
{% if show_type %}
|
||||||
<li class="ms-auto">
|
<li class="ms-auto">
|
||||||
{{ extension.get_type_display }}
|
{{ type_display }}
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
<div class="galleria-items{% if preview_count > 5 %} is-many{% endif %}{% if preview_count == 1 %} is-single{% endif %}" id="galleria-items">
|
<div class="galleria-items{% if preview_count > 5 %} is-many{% endif %}{% if preview_count == 1 %} is-single{% endif %}" id="galleria-items">
|
||||||
{% for preview in previews %}
|
{% for preview in previews %}
|
||||||
{% with thumbnail_1080p_url=preview.file.thumbnail_1080p_url file=preview.file %}
|
{% with thumbnail_1080p_url=preview.file.thumbnail_1080p_url file=preview.file %}
|
||||||
|
{% with thumbnail_360p_url=preview.file.thumbnail_360p_url file=preview.file %}
|
||||||
<a
|
<a
|
||||||
class="galleria-item js-galleria-item-preview galleria-item-type-{{ file.content_type|slugify|slice:5 }}{% if forloop.first %} is-active{% endif %}"
|
class="galleria-item js-galleria-item-preview galleria-item-type-{{ file.content_type|slugify|slice:5 }}{% if forloop.first %} is-active{% endif %}"
|
||||||
href="{{ thumbnail_1080p_url }}"
|
href="{{ thumbnail_1080p_url }}"
|
||||||
@ -11,9 +12,10 @@
|
|||||||
data-galleria-content-type="{{ file.content_type }}"
|
data-galleria-content-type="{{ file.content_type }}"
|
||||||
data-galleria-index="{{ forloop.counter }}">
|
data-galleria-index="{{ forloop.counter }}">
|
||||||
|
|
||||||
<img src="{{ thumbnail_1080p_url }}" alt="{{ preview.caption }}">
|
<img src="{{ thumbnail_360p_url }}" alt="{{ preview.caption }}">
|
||||||
</a>
|
</a>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
<img class="extension-icon mb-2 rounded" src="{% if extension.icon.source %}{{ extension.icon.source.url }}{% else %}{% static 'common/images/no-icon.png' %}{% endif %}">
|
<div class="extension-icon {{ classes}}">
|
||||||
|
<img alt="{{ extension.get_type_display }} {{ extension.name }}" src="{% if extension.icon.source %}{{ extension.icon.source.url }}{% else %}{% static 'common/images/no-icon.png' %}{% endif %}">
|
||||||
|
</div>
|
||||||
|
10
extensions/templates/extensions/components/platforms.html
Normal file
10
extensions/templates/extensions/components/platforms.html
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{% if version.platforms.all %}
|
||||||
|
<div>
|
||||||
|
Supported platforms:
|
||||||
|
<ul>
|
||||||
|
{% for p in version.platforms.all %}
|
||||||
|
<li>{{p.name}}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
@ -180,7 +180,10 @@
|
|||||||
<div class="dl-row">
|
<div class="dl-row">
|
||||||
<div class="dl-col">
|
<div class="dl-col">
|
||||||
<dt>{% trans 'Compatibility' %}</dt>
|
<dt>{% trans 'Compatibility' %}</dt>
|
||||||
<dd>{% include "extensions/components/blender_version.html" with version=latest %}</dd>
|
<dd>
|
||||||
|
{% include "extensions/components/blender_version.html" with version=latest %}
|
||||||
|
{% include "extensions/components/platforms.html" with version=latest %}
|
||||||
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -14,27 +14,25 @@
|
|||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<h2>
|
<h2>
|
||||||
{% blocktranslate with type=type|lower %}
|
{% blocktranslate with type=type|lower %}
|
||||||
Submit your {{ type }} for review
|
Submit {{ type }} for approval
|
||||||
{% endblocktranslate %}
|
{% endblocktranslate %}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-8 mt-3">
|
<div class="col-md-8">
|
||||||
<form id="update-extension-form" method="post" enctype="multipart/form-data">
|
<form id="update-extension-form" method="post" enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% with form=form|add_form_classes extension_form=extension_form|add_form_classes %}
|
{% with form=form|add_form_classes extension_form=extension_form|add_form_classes %}
|
||||||
|
|
||||||
<section class="card p-3 mb-3">
|
<section class="pb-3">
|
||||||
<p>
|
<p>
|
||||||
{% blocktranslate with type=type|lower %}
|
{% blocktranslate with type=type|lower %}
|
||||||
Please check and edit your {{ type }}'s details before submitting it for review.
|
Please check and edit your {{ type }}'s details before submitting it.
|
||||||
{% endblocktranslate %}
|
{% endblocktranslate %}
|
||||||
</p>
|
<br>
|
||||||
<p>
|
|
||||||
{% blocktranslate with type=type|lower %}
|
{% blocktranslate with type=type|lower %}
|
||||||
After the {{ type }} is approved by the Blender community or Blender Institute's staff,
|
The {{ type }} will be publicly visible once it is approved by the moderation team.
|
||||||
it will become publicly visible on the platform.
|
|
||||||
{% endblocktranslate %}
|
{% endblocktranslate %}
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
@ -60,21 +58,22 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="mt-4">
|
<section class="mt-4">
|
||||||
<h2>{% trans 'Featured image and icon' %}</h2>
|
<h2>{% trans 'Media' %}</h2>
|
||||||
<div class="previews-upload">
|
<div class="row flex">
|
||||||
<div class="row">
|
<div class="col-md-6">
|
||||||
<div class="col">
|
<div class="box p-3">
|
||||||
{% trans "Icon" as icon_label %}
|
{% trans "Featured Image" as featured_image_label %}
|
||||||
{% trans "A 256 x 256 icon representing this extension." as icon_help_text %}
|
{% trans "A JPEG, PNG or WebP image, at least 1920 x 1080 and with aspect ratio of 16:9." as featured_image_help_text %}
|
||||||
{% include "extensions/manage/components/set_image.html" with image_form=icon_form label=icon_label help_text=icon_help_text %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col">
|
|
||||||
{% trans "Featured image" as featured_image_label %}
|
|
||||||
{% trans "Should have resolution of at least 1920 x 1080 and aspect ratio of 16:9." as featured_image_help_text %}
|
|
||||||
{% include "extensions/manage/components/set_image.html" with image_form=featured_image_form label=featured_image_label help_text=featured_image_help_text %}
|
{% include "extensions/manage/components/set_image.html" with image_form=featured_image_form label=featured_image_label help_text=featured_image_help_text %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="box p-3">
|
||||||
|
{% trans "Icon" as icon_label %}
|
||||||
|
{% trans "A 256 x 256 PNG icon representing this extension." as icon_help_text %}
|
||||||
|
{% include "extensions/manage/components/set_image.html" with image_form=icon_form label=icon_label help_text=icon_help_text %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
{% extends "common/base.html" %}
|
{% extends "common/base.html" %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block page_title %}{% include "extensions/components/listing_title" %}{% endblock page_title %}
|
{% block page_title %}{% include "extensions/components/listing_title.html" %}{% endblock page_title %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row {% if type == 'Add-ons' %}is-row-add-ons{% elif type == 'Themes' %}is-row-themes{% endif %}">
|
||||||
<div class="col-md-12 my-4">
|
<div class="col-md-12 my-4">
|
||||||
{% if type %}
|
{% if type %}
|
||||||
<h2>{{ type }}</h2>
|
<h2>{{ type }}</h2>
|
||||||
@ -33,8 +33,10 @@
|
|||||||
<div class="box box-filter-sort mb-2 mb-md-0 me-md-3 overflow-hidden p-2">
|
<div class="box box-filter-sort mb-2 mb-md-0 me-md-3 overflow-hidden p-2">
|
||||||
<div class="box-filter-sort-start d-none h-100 js-box-filter-sort-start position-absolute start-0 top-0 w-5 z-1"></div>
|
<div class="box-filter-sort-start d-none h-100 js-box-filter-sort-start position-absolute start-0 top-0 w-5 z-1"></div>
|
||||||
<div class="btn-row flex-nowrap js-box-filter-sort-btns overflow-x-auto">
|
<div class="btn-row flex-nowrap js-box-filter-sort-btns overflow-x-auto">
|
||||||
{# TODO: @back-end add conditional link to either Add-ons or Themes pages #}
|
<a class="align-items-center btn btn-sm d-flex {% if not tag %}btn-primary{% endif %}" href="{% url 'extensions:by-type' type_slug=type|slugify %}" title="All">
|
||||||
<a class="align-items-center btn btn-sm d-flex {% if not tag %}btn-primary{% endif %}" href="/add-ons" title="All">
|
{% if tag %}
|
||||||
|
{# TODO @back-end: Find a proper way to get the plural tag type to build the URL. #}
|
||||||
|
<a class="btn btn-sm" href="/{{ tag.get_type_display|slugify }}s/" title="All">
|
||||||
<div>
|
<div>
|
||||||
All
|
All
|
||||||
</div>
|
</div>
|
||||||
@ -42,6 +44,18 @@
|
|||||||
1
|
1
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="btn btn-sm btn-primary" href="{% url 'extensions:by-type' type_slug=type|slugify %}" title="All">
|
||||||
|
<div>
|
||||||
|
All
|
||||||
|
</div>
|
||||||
|
<div class="align-items-center bg-primary d-flex h-3 fs-xs justify-content-center ms-2 rounded-circle w-3">
|
||||||
|
1
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for list_tag in tags %}
|
||||||
{% for list_tag in tags %}
|
{% for list_tag in tags %}
|
||||||
<a class="align-items-center btn btn-sm d-flex {% if tag == list_tag %}btn-primary{% endif %}" href="{% url "extensions:by-tag" tag_slug=list_tag.slug %}" title="{{ list_tag.name }}">
|
<a class="align-items-center btn btn-sm d-flex {% if tag == list_tag %}btn-primary{% endif %}" href="{% url "extensions:by-tag" tag_slug=list_tag.slug %}" title="{{ list_tag.name }}">
|
||||||
<div>
|
<div>
|
||||||
@ -86,9 +100,11 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>
|
<div class="mt-3">
|
||||||
{% blocktranslate %}No extensions found.{% endblocktranslate %}
|
<p class="pt-3 text-center">
|
||||||
</p>
|
{% blocktranslate %}No extensions found.{% endblocktranslate %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -2,6 +2,10 @@
|
|||||||
{# Upload new preview images #}
|
{# Upload new preview images #}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<div id="add-img-container" class="previews-list">
|
<div id="add-img-container" class="previews-list">
|
||||||
|
<div class="form-text">
|
||||||
|
Preview images are displayed in 16:9 ratio.
|
||||||
|
</div>
|
||||||
|
|
||||||
{{ add_preview_formset.management_form }}
|
{{ add_preview_formset.management_form }}
|
||||||
{{ add_preview_formset.non_form_errors }}
|
{{ add_preview_formset.non_form_errors }}
|
||||||
{% for newform in add_preview_formset %}
|
{% for newform in add_preview_formset %}
|
||||||
@ -10,22 +14,23 @@
|
|||||||
<div class="previews-list-item">
|
<div class="previews-list-item">
|
||||||
<div class="d-flex previews-list-item-thumbnail ps-3">
|
<div class="d-flex previews-list-item-thumbnail ps-3">
|
||||||
<div class="js-input-img-thumbnail previews-list-item-thumbnail-img" title="Preview">
|
<div class="js-input-img-thumbnail previews-list-item-thumbnail-img" title="Preview">
|
||||||
<div class="align-items-center d-flex js-input-img-thumbnail-icon justify-content-center">
|
<div class="ext-preview-thumbnail-icon js-input-img-thumbnail-icon">
|
||||||
<i class="i-image"></i>
|
<i class="i-image"></i>/<i class="i-reel"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="details flex-grow-1">
|
<div class="details">
|
||||||
<div class="js-input-img-caption-helper mb-2">
|
<div class="js-input-img-caption-helper">
|
||||||
{% include "common/components/field.html" with field=inlineform.caption label='Caption' placeholder="Describe the preview" %}
|
{% include "common/components/field.html" with field=inlineform.caption label='Image or Video' placeholder="Description" %}
|
||||||
</div>
|
</div>
|
||||||
<div class="align-items-center d-flex js-input-img-helper justify-content-between">
|
<div class="details-buttons js-input-img-helper">
|
||||||
{% include "common/components/field.html" with classes="form-control-sm" field=inlineform.source label='File' %}
|
<div>
|
||||||
<ul class="pt-0">
|
{% trans "A JPEG, PNG or WebP image, or an MP4 video." as preview_help_text %}
|
||||||
<li>
|
{% include "common/components/field.html" with classes="form-control-sm" field=inlineform.source label='File' help_text=preview_help_text %}
|
||||||
<button class="btn btn-link btn-sm js-btn-reset-img-upload-form ps-2 pe-0"><i class="i-refresh"></i> Reset</button>
|
</div>
|
||||||
</li>
|
<div class="btn-row">
|
||||||
</ul>
|
<button class="btn btn-link btn-sm js-btn-reset-img-upload-form"><i class="i-refresh"></i> Reset</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -38,15 +43,7 @@
|
|||||||
<div class="col text-right mt-3">
|
<div class="col text-right mt-3">
|
||||||
<a id="btn-add-img" class="btn">
|
<a id="btn-add-img" class="btn">
|
||||||
<i class="i-plus"></i>
|
<i class="i-plus"></i>
|
||||||
<span>{% trans 'Add Preview' %}</span>
|
<span>{% trans 'Add Preview Slot' %}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<div class="form-text">
|
|
||||||
Preview images are displayed in 16:9 ratio.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
@ -15,18 +15,23 @@
|
|||||||
<div class="js-preview-drag drag-widget is-draggable">
|
<div class="js-preview-drag drag-widget is-draggable">
|
||||||
<i class="i-menu"></i>
|
<i class="i-menu"></i>
|
||||||
<div class="previews-list-item-thumbnail">
|
<div class="previews-list-item-thumbnail">
|
||||||
<div class="previews-list-item-thumbnail-img" style="background-image: url('{% if file.is_image %}{{ file.source.url }}{% elif file.is_video and file.thumbnail %}{{ file.thumbnail.url }}{% endif %}');" title="Preview"></div>
|
{% with thumbnail_360p_url=file.thumbnail_360p_url file=file %}
|
||||||
|
<div
|
||||||
|
class="previews-list-item-thumbnail-img"
|
||||||
|
style="background-image: url('{% if file.is_image %}{{ thumbnail_360p_url }}{% elif file.is_video and file.thumbnail %}{{ file.thumbnail.url }}{% endif %}');" title="Preview">
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="details">
|
<div class="details">
|
||||||
<div>
|
<div>
|
||||||
{% include "common/components/field.html" with field=inlineform.id %}
|
{% include "common/components/field.html" with field=inlineform.id %}
|
||||||
{% include "common/components/field.html" with field=inlineform.caption %}
|
{% include "common/components/field.html" with field=inlineform.caption label=file.get_type_display placeholder="Description" %}
|
||||||
{% include "common/components/field.html" with field=inlineform.position %}
|
{% include "common/components/field.html" with field=inlineform.position %}
|
||||||
</div>
|
</div>
|
||||||
<ul>
|
<ul class="list-inline">
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ inlineform.instance.file.source.url }}" target="_blank">
|
<a href="{{ inlineform.instance.file.source.url }}" target="_blank" class="text-underline">
|
||||||
<small>Source File</small>
|
<small>Source File</small>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
{# Handles displaying and editing the featured image #}
|
{# Handles displaying and editing the featured image #}
|
||||||
{% with inlineform=image_form|add_form_classes %}
|
{% with inlineform=image_form|add_form_classes %}
|
||||||
{% with current_file=inlineform.instance.source %}
|
{% with current_file=inlineform.instance.source %}
|
||||||
<div class="align-items-center d-flex justify-content-center mb-2 {{ image_form.prefix }}-preview"
|
<div class="ext-preview-thumbnail-icon mb-2 {{ image_form.prefix }}-preview"
|
||||||
style="background-image: url('{% if current_file %}{{ current_file.url }}{% endif %}');"
|
style="background-image: url('{% if current_file %}{{ current_file.url }}{% endif %}');"
|
||||||
title="{{ label }} of the extension">
|
title="{{ label }} of the extension">
|
||||||
<i class="i-image js-i-image"></i>
|
<i class="i-image js-i-image"></i>
|
||||||
|
@ -44,13 +44,13 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
{% trans "Icon" as icon_label %}
|
{% trans "Icon" as icon_label %}
|
||||||
{% trans "A 256 x 256 icon representing this extension." as icon_help_text %}
|
{% trans "A 256 x 256 PNG icon representing this extension." as icon_help_text %}
|
||||||
{% include "extensions/manage/components/set_image.html" with image_form=icon_form label=icon_label help_text=icon_help_text %}
|
{% include "extensions/manage/components/set_image.html" with image_form=icon_form label=icon_label help_text=icon_help_text %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col">
|
<div class="col">
|
||||||
{% trans "Featured image" as featured_image_label %}
|
{% trans "Featured image" as featured_image_label %}
|
||||||
{% trans "Should have resolution of at least 1920 x 1080 and aspect ratio of 16:9." as featured_image_help_text %}
|
{% trans "A JPEG, PNG or WebP image, at least 1920 x 1080 and with aspect ratio of 16:9." as featured_image_help_text %}
|
||||||
{% include "extensions/manage/components/set_image.html" with image_form=featured_image_form label=featured_image_label help_text=featured_image_help_text %}
|
{% include "extensions/manage/components/set_image.html" with image_form=featured_image_form label=featured_image_label help_text=featured_image_help_text %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -46,7 +46,7 @@
|
|||||||
{% switch is_alpha %}
|
{% switch is_alpha %}
|
||||||
<a class="text-underline" href="https://docs.blender.org/manual/en/dev/extensions/#how-to-create-extensions">guidelines</a>.
|
<a class="text-underline" href="https://docs.blender.org/manual/en/dev/extensions/#how-to-create-extensions">guidelines</a>.
|
||||||
{% else %}
|
{% else %}
|
||||||
<a class="text-underline" href="https://docs.blender.org/manual/en/latest/extensions/#how-to-create-extensions">guidelines</a>.
|
<a class="text-underline" href="https://docs.blender.org/manual/en/dev/extensions/#how-to-create-extensions">guidelines</a>.
|
||||||
{% endswitch %}
|
{% endswitch %}
|
||||||
</strong>
|
</strong>
|
||||||
</li>
|
</li>
|
||||||
@ -60,12 +60,12 @@
|
|||||||
{% with form=form|add_form_classes %}
|
{% with form=form|add_form_classes %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
{% include "common/components/field.html" with field=form.source label='File' %}
|
{% include "common/components/field.html" with field=form.source label='File' classes="js-agree-with-terms-trigger js-submit-form-file-input" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col mx-4 mt-4">
|
<div class="col mx-4 mt-4">
|
||||||
{% include "common/components/field.html" with field=form.agreed_with_terms classes="js-agree-with-terms-input" %}
|
{% include "common/components/field.html" with field=form.agreed_with_terms classes="js-agree-with-terms-trigger js-agree-with-terms-checkbox" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
|
@ -56,7 +56,10 @@
|
|||||||
<div class="dl-row">
|
<div class="dl-row">
|
||||||
<div class="dl-col">
|
<div class="dl-col">
|
||||||
<dt>{% trans 'Compatibility' %}</dt>
|
<dt>{% trans 'Compatibility' %}</dt>
|
||||||
<dd>{% include "extensions/components/blender_version.html" with version=version %}</dd>
|
<dd>
|
||||||
|
{% include "extensions/components/blender_version.html" with version=version %}
|
||||||
|
{% include "extensions/components/platforms.html" with version=version %}
|
||||||
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dl-row">
|
<div class="dl-row">
|
||||||
@ -94,7 +97,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="btn-col mb-3">
|
<div class="btn-col">
|
||||||
<a href="{{ version.download_url }}" download="{{ version.download_name }}" class="btn btn-primary btn-block">
|
<a href="{{ version.download_url }}" download="{{ version.download_name }}" class="btn btn-primary btn-block">
|
||||||
<i class="i-download"></i>
|
<i class="i-download"></i>
|
||||||
<span>{% trans 'Download' %} v{{ version.version }}</span>
|
<span>{% trans 'Download' %} v{{ version.version }}</span>
|
||||||
|
@ -15,23 +15,23 @@ def naturaltime_compact(time):
|
|||||||
# Take only the first part, e.g. "3 days, 2h ago", becomes " 3d ago"
|
# Take only the first part, e.g. "3 days, 2h ago", becomes " 3d ago"
|
||||||
compact_time = compact_time.split(',')[0]
|
compact_time = compact_time.split(',')[0]
|
||||||
|
|
||||||
compact_time = compact_time.replace(' seconds ago', ' s')
|
compact_time = compact_time.replace(' ago', '')
|
||||||
compact_time = compact_time.replace('a minute ago', '1 m')
|
compact_time = compact_time.replace(' seconds', ' s')
|
||||||
compact_time = compact_time.replace(' minutes ago', ' m')
|
compact_time = compact_time.replace(' second', ' s')
|
||||||
compact_time = compact_time.replace('an hour ago', '1 h') # Exactly 1 hour.
|
compact_time = compact_time.replace(' minutes', ' m')
|
||||||
compact_time = compact_time.replace(' hours ago', ' h')
|
compact_time = compact_time.replace('a minute', '1 m')
|
||||||
compact_time = compact_time.replace('1 day ago', '1 d')
|
compact_time = compact_time.replace(' minute', ' m')
|
||||||
compact_time = compact_time.replace('1 day', '1 d')
|
compact_time = compact_time.replace(' hours', ' h')
|
||||||
compact_time = compact_time.replace(' days ago', ' d')
|
compact_time = compact_time.replace('an hour', '1 h')
|
||||||
compact_time = compact_time.replace('1 week ago', '1 w')
|
compact_time = compact_time.replace(' hour', ' h')
|
||||||
compact_time = compact_time.replace('1 week', '1 w')
|
compact_time = compact_time.replace(' days', ' d')
|
||||||
compact_time = compact_time.replace(' weeks ago', ' w')
|
compact_time = compact_time.replace(' day', ' d')
|
||||||
compact_time = compact_time.replace('1 month ago', '1 mo')
|
compact_time = compact_time.replace(' weeks', ' w')
|
||||||
compact_time = compact_time.replace('1 month', '1 mo')
|
compact_time = compact_time.replace(' week', ' w')
|
||||||
compact_time = compact_time.replace(' months ago', ' mo')
|
compact_time = compact_time.replace(' months', ' mo')
|
||||||
compact_time = compact_time.replace('1 year ago', '1 y')
|
compact_time = compact_time.replace(' month', ' mo')
|
||||||
compact_time = compact_time.replace('1 year', '1 y')
|
compact_time = compact_time.replace(' years', ' y')
|
||||||
compact_time = compact_time.replace(' years ago', ' y')
|
compact_time = compact_time.replace(' year', ' y')
|
||||||
|
|
||||||
return compact_time
|
return compact_time
|
||||||
|
|
||||||
|
BIN
extensions/tests/files/invalid-missing-wheels.zip
Normal file
BIN
extensions/tests/files/invalid-missing-wheels.zip
Normal file
Binary file not shown.
@ -84,6 +84,11 @@ EXPECTED_VALIDATION_ERRORS = {
|
|||||||
},
|
},
|
||||||
'invalid-manifest-toml.zip': {'source': ['Could not parse the manifest file.']},
|
'invalid-manifest-toml.zip': {'source': ['Could not parse the manifest file.']},
|
||||||
'invalid-theme-multiple-xmls.zip': {'source': ['A theme should have exactly one XML file.']},
|
'invalid-theme-multiple-xmls.zip': {'source': ['A theme should have exactly one XML file.']},
|
||||||
|
'invalid-missing-wheels.zip': {
|
||||||
|
'source': [
|
||||||
|
'A declared wheel is missing in the zip file, expected path: addon/./wheels/test-wheel-whatever.whl'
|
||||||
|
]
|
||||||
|
},
|
||||||
}
|
}
|
||||||
POST_DATA = {
|
POST_DATA = {
|
||||||
'preview_set-TOTAL_FORMS': ['0'],
|
'preview_set-TOTAL_FORMS': ['0'],
|
||||||
@ -169,13 +174,13 @@ class SubmitFileTest(TestCase):
|
|||||||
user = UserFactory()
|
user = UserFactory()
|
||||||
self.client.force_login(user)
|
self.client.force_login(user)
|
||||||
|
|
||||||
for test_archive, extected_errors in EXPECTED_VALIDATION_ERRORS.items():
|
for test_archive, expected_errors in EXPECTED_VALIDATION_ERRORS.items():
|
||||||
with self.subTest(test_archive=test_archive):
|
with self.subTest(test_archive=test_archive):
|
||||||
with open(TEST_FILES_DIR / test_archive, 'rb') as fp:
|
with open(TEST_FILES_DIR / test_archive, 'rb') as fp:
|
||||||
response = self.client.post(self.url, {'source': fp, 'agreed_with_terms': True})
|
response = self.client.post(self.url, {'source': fp, 'agreed_with_terms': True})
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertDictEqual(response.context['form'].errors, extected_errors)
|
self.assertDictEqual(response.context['form'].errors, expected_errors)
|
||||||
|
|
||||||
def test_addon_without_top_level_directory(self):
|
def test_addon_without_top_level_directory(self):
|
||||||
self.assertEqual(Extension.objects.count(), 0)
|
self.assertEqual(Extension.objects.count(), 0)
|
||||||
|
@ -7,6 +7,7 @@ from common.tests.factories.extensions import create_version, create_approved_ve
|
|||||||
from common.tests.factories.users import UserFactory
|
from common.tests.factories.users import UserFactory
|
||||||
from extensions.models import Extension, Version
|
from extensions.models import Extension, Version
|
||||||
from files.models import File
|
from files.models import File
|
||||||
|
from teams.models import Team
|
||||||
|
|
||||||
|
|
||||||
def _create_extension():
|
def _create_extension():
|
||||||
@ -112,6 +113,18 @@ class ApiViewsTest(_BaseTestCase):
|
|||||||
).json()
|
).json()
|
||||||
self.assertEqual(len(json3['data']), 3)
|
self.assertEqual(len(json3['data']), 3)
|
||||||
|
|
||||||
|
def test_platform_filter(self):
|
||||||
|
create_approved_version(platforms=['windows-amd64'])
|
||||||
|
create_approved_version(platforms=['windows-arm64'])
|
||||||
|
create_approved_version()
|
||||||
|
url = reverse('extensions:api')
|
||||||
|
|
||||||
|
json = self.client.get(
|
||||||
|
url + '?platform=windows-amd64',
|
||||||
|
HTTP_ACCEPT='application/json',
|
||||||
|
).json()
|
||||||
|
self.assertEqual(len(json['data']), 2)
|
||||||
|
|
||||||
def test_blender_version_filter_latest_not_max_version(self):
|
def test_blender_version_filter_latest_not_max_version(self):
|
||||||
version = create_approved_version(blender_version_min='4.0.1')
|
version = create_approved_version(blender_version_min='4.0.1')
|
||||||
version.date_created
|
version.date_created
|
||||||
@ -144,6 +157,20 @@ class ApiViewsTest(_BaseTestCase):
|
|||||||
# we are expecting the latest matching, not the maximum version
|
# we are expecting the latest matching, not the maximum version
|
||||||
self.assertEqual(json['data'][0]['version'], '1.0.1')
|
self.assertEqual(json['data'][0]['version'], '1.0.1')
|
||||||
|
|
||||||
|
def test_maintaner_is_team(self):
|
||||||
|
version = create_approved_version(blender_version_min='4.0.1')
|
||||||
|
team = Team(name='test team', slug='test-team')
|
||||||
|
team.save()
|
||||||
|
version.extension.team = team
|
||||||
|
version.extension.save()
|
||||||
|
url = reverse('extensions:api')
|
||||||
|
|
||||||
|
json = self.client.get(
|
||||||
|
url,
|
||||||
|
HTTP_ACCEPT='application/json',
|
||||||
|
).json()
|
||||||
|
self.assertEqual(json['data'][0]['maintainer'], 'test team')
|
||||||
|
|
||||||
|
|
||||||
class ExtensionDetailViewTest(_BaseTestCase):
|
class ExtensionDetailViewTest(_BaseTestCase):
|
||||||
def test_cannot_view_unlisted_extension_anonymously(self):
|
def test_cannot_view_unlisted_extension_anonymously(self):
|
||||||
|
@ -7,7 +7,7 @@ from drf_spectacular.utils import OpenApiParameter, extend_schema
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
from common.compare import is_in_version_range, version
|
from common.compare import is_in_version_range, version
|
||||||
from extensions.models import Extension
|
from extensions.models import Extension, Platform
|
||||||
from extensions.utils import clean_json_dictionary_from_optional_fields
|
from extensions.utils import clean_json_dictionary_from_optional_fields
|
||||||
|
|
||||||
|
|
||||||
@ -20,7 +20,10 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class ListedExtensionsSerializer(serializers.ModelSerializer):
|
class ListedExtensionsSerializer(serializers.ModelSerializer):
|
||||||
error_messages = {
|
error_messages = {
|
||||||
"invalid_version": "Invalid version: use full semantic versioning like 4.2.0."
|
"invalid_blender_version": "Invalid blender_version: use full semantic versioning like "
|
||||||
|
"4.2.0.",
|
||||||
|
"invalid_platform": "Invalid platform: use notation specified in "
|
||||||
|
"https://developer.blender.org/docs/features/extensions/schema/1.0.0/",
|
||||||
}
|
}
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -30,16 +33,22 @@ class ListedExtensionsSerializer(serializers.ModelSerializer):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.request = kwargs.pop('request', None)
|
self.request = kwargs.pop('request', None)
|
||||||
self.blender_version = kwargs.pop('blender_version', None)
|
self.blender_version = kwargs.pop('blender_version', None)
|
||||||
|
self.platform = kwargs.pop('platform', None)
|
||||||
self._validate()
|
self._validate()
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def _validate(self):
|
def _validate(self):
|
||||||
if self.blender_version is None:
|
if self.blender_version:
|
||||||
return
|
try:
|
||||||
try:
|
version(self.blender_version)
|
||||||
version(self.blender_version)
|
except ValidationError:
|
||||||
except ValidationError:
|
self.fail('invalid_blender_version')
|
||||||
self.fail('invalid_version')
|
if self.platform:
|
||||||
|
# FIXME change to an in-memory lookup?
|
||||||
|
try:
|
||||||
|
Platform.objects.get(slug=self.platform)
|
||||||
|
except Platform.DoesNotExist:
|
||||||
|
self.fail('invalid_platform')
|
||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
matching_version = None
|
matching_version = None
|
||||||
@ -52,18 +61,19 @@ class ListedExtensionsSerializer(serializers.ModelSerializer):
|
|||||||
if not versions:
|
if not versions:
|
||||||
return None
|
return None
|
||||||
versions = sorted(versions, key=lambda v: v.date_created, reverse=True)
|
versions = sorted(versions, key=lambda v: v.date_created, reverse=True)
|
||||||
if self.blender_version:
|
for v in versions:
|
||||||
for v in versions:
|
if self.blender_version and not is_in_version_range(
|
||||||
if is_in_version_range(
|
self.blender_version,
|
||||||
self.blender_version,
|
v.blender_version_min,
|
||||||
v.blender_version_min,
|
v.blender_version_max,
|
||||||
v.blender_version_max,
|
):
|
||||||
):
|
continue
|
||||||
matching_version = v
|
platform_slugs = set(p.slug for p in v.platforms.all())
|
||||||
break
|
# empty platforms field matches any platform filter
|
||||||
else:
|
if self.platform and not (not platform_slugs or self.platform in platform_slugs):
|
||||||
# same as latest_version, but without triggering a new queryset
|
continue
|
||||||
matching_version = versions[0]
|
matching_version = v
|
||||||
|
break
|
||||||
|
|
||||||
if not matching_version:
|
if not matching_version:
|
||||||
return None
|
return None
|
||||||
@ -82,9 +92,10 @@ class ListedExtensionsSerializer(serializers.ModelSerializer):
|
|||||||
'blender_version_max': matching_version.blender_version_max,
|
'blender_version_max': matching_version.blender_version_max,
|
||||||
'website': self.request.build_absolute_uri(instance.get_absolute_url()),
|
'website': self.request.build_absolute_uri(instance.get_absolute_url()),
|
||||||
# avoid triggering additional db queries, reuse the prefetched queryset
|
# avoid triggering additional db queries, reuse the prefetched queryset
|
||||||
'maintainer': str(instance.authors.all()[0]),
|
'maintainer': instance.team and instance.team.name or str(instance.authors.all()[0]),
|
||||||
'license': [license_iter.slug for license_iter in matching_version.licenses.all()],
|
'license': [license_iter.slug for license_iter in matching_version.licenses.all()],
|
||||||
'permissions': [permission.slug for permission in matching_version.permissions.all()],
|
'permissions': [permission.slug for permission in matching_version.permissions.all()],
|
||||||
|
'platforms': [platform.slug for platform in matching_version.platforms.all()],
|
||||||
# TODO: handle copyright
|
# TODO: handle copyright
|
||||||
'tags': [str(tag) for tag in matching_version.tags.all()],
|
'tags': [str(tag) for tag in matching_version.tags.all()],
|
||||||
}
|
}
|
||||||
@ -101,21 +112,33 @@ class ExtensionsAPIView(APIView):
|
|||||||
name="blender_version",
|
name="blender_version",
|
||||||
description=("Blender version to check for compatibility"),
|
description=("Blender version to check for compatibility"),
|
||||||
type=str,
|
type=str,
|
||||||
)
|
),
|
||||||
|
OpenApiParameter(
|
||||||
|
name="platform",
|
||||||
|
description=("Platform to check for compatibility"),
|
||||||
|
type=str,
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
blender_version = request.GET.get('blender_version')
|
blender_version = request.GET.get('blender_version')
|
||||||
|
platform = request.GET.get('platform')
|
||||||
qs = Extension.objects.listed.prefetch_related(
|
qs = Extension.objects.listed.prefetch_related(
|
||||||
'authors',
|
'authors',
|
||||||
|
'team',
|
||||||
'versions',
|
'versions',
|
||||||
'versions__file',
|
'versions__file',
|
||||||
'versions__licenses',
|
'versions__licenses',
|
||||||
'versions__permissions',
|
'versions__permissions',
|
||||||
|
'versions__platforms',
|
||||||
'versions__tags',
|
'versions__tags',
|
||||||
).all()
|
).all()
|
||||||
serializer = self.serializer_class(
|
serializer = self.serializer_class(
|
||||||
qs, blender_version=blender_version, request=request, many=True
|
qs,
|
||||||
|
blender_version=blender_version,
|
||||||
|
platform=platform,
|
||||||
|
request=request,
|
||||||
|
many=True,
|
||||||
)
|
)
|
||||||
data = [e for e in serializer.data if e is not None]
|
data = [e for e in serializer.data if e is not None]
|
||||||
return Response(
|
return Response(
|
||||||
|
@ -45,6 +45,7 @@ class ExtensionDetailView(ExtensionQuerysetMixin, DetailView):
|
|||||||
'versions__file',
|
'versions__file',
|
||||||
'versions__file__validation',
|
'versions__file__validation',
|
||||||
'versions__permissions',
|
'versions__permissions',
|
||||||
|
'versions__platforms',
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_object(self, queryset=None):
|
def get_object(self, queryset=None):
|
||||||
|
@ -29,7 +29,7 @@ class ListedExtensionsView(ListView):
|
|||||||
|
|
||||||
|
|
||||||
class HomeView(ListedExtensionsView):
|
class HomeView(ListedExtensionsView):
|
||||||
paginate_by = 15
|
paginate_by = 16
|
||||||
template_name = 'extensions/home.html'
|
template_name = 'extensions/home.html'
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
@ -69,7 +69,7 @@ def extension_version_download(request, type_slug, slug, version):
|
|||||||
|
|
||||||
|
|
||||||
class SearchView(ListedExtensionsView):
|
class SearchView(ListedExtensionsView):
|
||||||
paginate_by = 15
|
paginate_by = 16
|
||||||
template_name = 'extensions/list.html'
|
template_name = 'extensions/list.html'
|
||||||
|
|
||||||
def _get_type_id_by_slug(self):
|
def _get_type_id_by_slug(self):
|
||||||
@ -95,7 +95,7 @@ class SearchView(ListedExtensionsView):
|
|||||||
qs = self.request.GET['q'].split()
|
qs = self.request.GET['q'].split()
|
||||||
search_query = Q()
|
search_query = Q()
|
||||||
for token in qs:
|
for token in qs:
|
||||||
search_query |= (
|
search_query &= (
|
||||||
Q(slug__icontains=token)
|
Q(slug__icontains=token)
|
||||||
| Q(name__icontains=token)
|
| Q(name__icontains=token)
|
||||||
| Q(description__icontains=token)
|
| Q(description__icontains=token)
|
||||||
|
@ -63,6 +63,7 @@ class FileAdmin(admin.ModelAdmin):
|
|||||||
kwargs.update({'help_texts': {'metadata': help_text}})
|
kwargs.update({'help_texts': {'metadata': help_text}})
|
||||||
return super().get_form(request, obj, **kwargs)
|
return super().get_form(request, obj, **kwargs)
|
||||||
|
|
||||||
|
date_hierarchy = 'date_created'
|
||||||
view_on_site = False
|
view_on_site = False
|
||||||
save_on_top = True
|
save_on_top = True
|
||||||
|
|
||||||
@ -70,8 +71,10 @@ class FileAdmin(admin.ModelAdmin):
|
|||||||
'validation__is_ok',
|
'validation__is_ok',
|
||||||
'type',
|
'type',
|
||||||
'status',
|
'status',
|
||||||
'date_status_changed',
|
|
||||||
'date_approved',
|
'date_approved',
|
||||||
|
'date_created',
|
||||||
|
'date_modified',
|
||||||
|
'date_status_changed',
|
||||||
('extension', admin.EmptyFieldListFilter),
|
('extension', admin.EmptyFieldListFilter),
|
||||||
)
|
)
|
||||||
list_display = (
|
list_display = (
|
||||||
@ -84,8 +87,9 @@ class FileAdmin(admin.ModelAdmin):
|
|||||||
'is_ok',
|
'is_ok',
|
||||||
)
|
)
|
||||||
|
|
||||||
list_select_related = ('version__extension', 'user')
|
list_select_related = ('version__extension', 'user', 'extension', 'version', 'validation')
|
||||||
|
|
||||||
|
autocomplete_fields = ['user']
|
||||||
readonly_fields = (
|
readonly_fields = (
|
||||||
'id',
|
'id',
|
||||||
'date_created',
|
'date_created',
|
||||||
@ -96,7 +100,6 @@ class FileAdmin(admin.ModelAdmin):
|
|||||||
'thumbnails',
|
'thumbnails',
|
||||||
'thumbnail',
|
'thumbnail',
|
||||||
'type',
|
'type',
|
||||||
'user',
|
|
||||||
'original_hash',
|
'original_hash',
|
||||||
'original_name',
|
'original_name',
|
||||||
'hash',
|
'hash',
|
||||||
@ -110,6 +113,9 @@ class FileAdmin(admin.ModelAdmin):
|
|||||||
'original_name',
|
'original_name',
|
||||||
'hash',
|
'hash',
|
||||||
'source',
|
'source',
|
||||||
|
'user__email',
|
||||||
|
'user__full_name',
|
||||||
|
'user__username',
|
||||||
)
|
)
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
|
@ -39,6 +39,7 @@ class FileForm(forms.ModelForm):
|
|||||||
'missing_or_multiple_theme_xml': _('A theme should have exactly one XML file.'),
|
'missing_or_multiple_theme_xml': _('A theme should have exactly one XML file.'),
|
||||||
'invalid_zip_archive': msg_only_zip_files,
|
'invalid_zip_archive': msg_only_zip_files,
|
||||||
'missing_manifest_toml': _('The manifest file is missing.'),
|
'missing_manifest_toml': _('The manifest file is missing.'),
|
||||||
|
'missing_wheel': _('A declared wheel is missing in the zip file, expected path: %(path)s'),
|
||||||
}
|
}
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -143,7 +144,15 @@ class FileForm(forms.ModelForm):
|
|||||||
|
|
||||||
manifest, error_codes = utils.read_manifest_from_zip(file_path)
|
manifest, error_codes = utils.read_manifest_from_zip(file_path)
|
||||||
for code in error_codes:
|
for code in error_codes:
|
||||||
errors.append(forms.ValidationError(self.error_messages[code]))
|
if isinstance(code, dict):
|
||||||
|
errors.append(
|
||||||
|
forms.ValidationError(
|
||||||
|
self.error_messages[code['code']],
|
||||||
|
params=code['params'],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
errors.append(forms.ValidationError(self.error_messages[code]))
|
||||||
if errors:
|
if errors:
|
||||||
self.add_error('source', errors)
|
self.add_error('source', errors)
|
||||||
|
|
||||||
|
@ -108,6 +108,9 @@ def read_manifest_from_zip(archive_path):
|
|||||||
"""
|
"""
|
||||||
manifest_name = 'blender_manifest.toml'
|
manifest_name = 'blender_manifest.toml'
|
||||||
error_codes = []
|
error_codes = []
|
||||||
|
file_list = []
|
||||||
|
manifest_content = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with zipfile.ZipFile(archive_path) as myzip:
|
with zipfile.ZipFile(archive_path) as myzip:
|
||||||
bad_file = myzip.testzip()
|
bad_file = myzip.testzip()
|
||||||
@ -129,34 +132,45 @@ def read_manifest_from_zip(archive_path):
|
|||||||
error_codes.append('invalid_manifest_path')
|
error_codes.append('invalid_manifest_path')
|
||||||
return None, error_codes
|
return None, error_codes
|
||||||
|
|
||||||
# Extract the file content
|
|
||||||
with myzip.open(manifest_filepath) as file_content:
|
with myzip.open(manifest_filepath) as file_content:
|
||||||
toml_content = toml.loads(file_content.read().decode())
|
manifest_content = file_content.read().decode()
|
||||||
|
|
||||||
# If manifest was parsed successfully, do additional type-specific validation
|
|
||||||
type_slug = toml_content['type']
|
|
||||||
if type_slug == 'theme':
|
|
||||||
theme_xmls = filter_paths_by_ext(file_list, '.xml')
|
|
||||||
if len(list(theme_xmls)) != 1:
|
|
||||||
error_codes.append('missing_or_multiple_theme_xml')
|
|
||||||
elif type_slug == 'add-on':
|
|
||||||
# __init__.py is expected to be next to the manifest
|
|
||||||
expected_init_path = os.path.join(os.path.dirname(manifest_filepath), '__init__.py')
|
|
||||||
init_filepath = find_exact_path(file_list, expected_init_path)
|
|
||||||
if not init_filepath:
|
|
||||||
error_codes.append('invalid_missing_init')
|
|
||||||
|
|
||||||
return toml_content, error_codes
|
|
||||||
|
|
||||||
except toml.decoder.TomlDecodeError as e:
|
|
||||||
logger.error(f"Manifest Error: {e.msg}")
|
|
||||||
error_codes.append('invalid_manifest_toml')
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error extracting from archive: {e}")
|
logger.error(f"Error extracting from archive: {e}")
|
||||||
error_codes.append('invalid_zip_archive')
|
error_codes.append('invalid_zip_archive')
|
||||||
|
return None, error_codes
|
||||||
|
|
||||||
return None, error_codes
|
try:
|
||||||
|
toml_content = toml.loads(manifest_content)
|
||||||
|
except toml.decoder.TomlDecodeError as e:
|
||||||
|
logger.error(f"Manifest Error: {e.msg}")
|
||||||
|
error_codes.append('invalid_manifest_toml')
|
||||||
|
return None, error_codes
|
||||||
|
|
||||||
|
# If manifest was parsed successfully, do additional type-specific validation
|
||||||
|
type_slug = toml_content['type']
|
||||||
|
if type_slug == 'theme':
|
||||||
|
theme_xmls = filter_paths_by_ext(file_list, '.xml')
|
||||||
|
if len(list(theme_xmls)) != 1:
|
||||||
|
error_codes.append('missing_or_multiple_theme_xml')
|
||||||
|
elif type_slug == 'add-on':
|
||||||
|
# __init__.py is expected to be next to the manifest
|
||||||
|
expected_init_path = os.path.join(os.path.dirname(manifest_filepath), '__init__.py')
|
||||||
|
init_filepath = find_exact_path(file_list, expected_init_path)
|
||||||
|
if not init_filepath:
|
||||||
|
error_codes.append('invalid_missing_init')
|
||||||
|
|
||||||
|
wheels = toml_content.get('wheels')
|
||||||
|
if wheels:
|
||||||
|
for wheel in wheels:
|
||||||
|
expected_wheel_path = os.path.join(os.path.dirname(manifest_filepath), wheel)
|
||||||
|
wheel_filepath = find_exact_path(file_list, expected_wheel_path)
|
||||||
|
if not wheel_filepath:
|
||||||
|
error_codes.append(
|
||||||
|
{'code': 'missing_wheel', 'params': {'path': expected_wheel_path}}
|
||||||
|
)
|
||||||
|
|
||||||
|
return toml_content, error_codes
|
||||||
|
|
||||||
|
|
||||||
def guess_mimetype_from_ext(file_name: str) -> str:
|
def guess_mimetype_from_ext(file_name: str) -> str:
|
||||||
|
@ -7,7 +7,13 @@ from django.utils.deconstruct import deconstructible
|
|||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from extensions.models import Extension, License, VersionPermission, Tag
|
from extensions.models import (
|
||||||
|
Extension,
|
||||||
|
License,
|
||||||
|
Platform,
|
||||||
|
Tag,
|
||||||
|
VersionPermission,
|
||||||
|
)
|
||||||
from constants.base import EXTENSION_TYPE_SLUGS_SINGULAR, EXTENSION_TYPE_CHOICES
|
from constants.base import EXTENSION_TYPE_SLUGS_SINGULAR, EXTENSION_TYPE_CHOICES
|
||||||
from files.utils import guess_mimetype_from_ext, guess_mimetype_from_content
|
from files.utils import guess_mimetype_from_ext, guess_mimetype_from_content
|
||||||
|
|
||||||
@ -214,7 +220,7 @@ class LicenseValidator(ListValidator):
|
|||||||
|
|
||||||
error_message = mark_safe(
|
error_message = mark_safe(
|
||||||
f'Manifest value error: <code>license</code> expects a list of '
|
f'Manifest value error: <code>license</code> expects a list of '
|
||||||
f'<a href="https://docs.blender.org/manual/en/dev/extensions/licenses.html">'
|
f'<a href="https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html">'
|
||||||
f'supported licenses</a>. e.g., {cls.example}.'
|
f'supported licenses</a>. e.g., {cls.example}.'
|
||||||
)
|
)
|
||||||
if unknown_value:
|
if unknown_value:
|
||||||
@ -247,7 +253,7 @@ class TagsValidatorBase:
|
|||||||
|
|
||||||
error_message = mark_safe(
|
error_message = mark_safe(
|
||||||
f'Manifest value error: <code>tags</code> expects a list of '
|
f'Manifest value error: <code>tags</code> expects a list of '
|
||||||
f'<a href="https://docs.blender.org/manual/en/dev/extensions/tags.html" '
|
f'<a href="https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html" '
|
||||||
f'target="_blank"> supported {type_name} tags</a>. e.g., {cls.example}. '
|
f'target="_blank"> supported {type_name} tags</a>. e.g., {cls.example}. '
|
||||||
)
|
)
|
||||||
if unknown_value:
|
if unknown_value:
|
||||||
@ -361,6 +367,53 @@ class PermissionsValidator:
|
|||||||
return mark_safe(error_message)
|
return mark_safe(error_message)
|
||||||
|
|
||||||
|
|
||||||
|
class PlatformsValidator:
|
||||||
|
"""See https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/"""
|
||||||
|
|
||||||
|
example = ["windows-amd64", "linux-x86_64"]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
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 = ""
|
||||||
|
|
||||||
|
unknown_value = None
|
||||||
|
if type(value) != list:
|
||||||
|
is_error = True
|
||||||
|
else:
|
||||||
|
for platform in value:
|
||||||
|
if Platform.get_by_slug(platform):
|
||||||
|
continue
|
||||||
|
is_error = True
|
||||||
|
unknown_value = platform
|
||||||
|
break
|
||||||
|
|
||||||
|
if not is_error:
|
||||||
|
return
|
||||||
|
|
||||||
|
error_message = mark_safe(
|
||||||
|
f'Manifest value error: <code>platforms</code> expects a list of '
|
||||||
|
f'supported platforms. e.g., {cls.example}.'
|
||||||
|
)
|
||||||
|
if unknown_value:
|
||||||
|
error_message += mark_safe(f' Unknown value: {escape(unknown_value)}.')
|
||||||
|
|
||||||
|
return error_message
|
||||||
|
|
||||||
|
|
||||||
|
class WheelsValidator:
|
||||||
|
example = ["./wheels/mywheel-v1.0.0-py3-none-any.whl"]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate(cls, *, name: str, value: list[str], manifest: dict) -> str:
|
||||||
|
if type(value) != list:
|
||||||
|
return mark_safe(
|
||||||
|
f'Manifest value error: <code>wheels</code> expects a list of '
|
||||||
|
f'wheel files . e.g., {cls.example}.'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class VersionValidator:
|
class VersionValidator:
|
||||||
example = '1.0.0'
|
example = '1.0.0'
|
||||||
|
|
||||||
@ -396,7 +449,7 @@ class SchemaVersionValidator(VersionValidator):
|
|||||||
# TODO make a user manual page with the list of the different schemas.
|
# TODO make a user manual page with the list of the different schemas.
|
||||||
return mark_safe(
|
return mark_safe(
|
||||||
f'Manifest value error: <code>schema</code> version ({escape(value)}) '
|
f'Manifest value error: <code>schema</code> version ({escape(value)}) '
|
||||||
f'<a href="https://docs.blender.org/manual/en/dev/extensions/'
|
f'<a href="https://docs.blender.org/manual/en/dev/advanced/extensions/'
|
||||||
f'getting_started.html#manifest" target="_blank">not supported</a>.'
|
f'getting_started.html#manifest" target="_blank">not supported</a>.'
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -489,10 +542,12 @@ class ManifestValidator:
|
|||||||
}
|
}
|
||||||
optional_fields = {
|
optional_fields = {
|
||||||
'blender_version_max': VersionMaxValidator,
|
'blender_version_max': VersionMaxValidator,
|
||||||
'website': StringValidator,
|
|
||||||
'copyright': ListValidator,
|
'copyright': ListValidator,
|
||||||
'permissions': PermissionsValidator,
|
'permissions': PermissionsValidator,
|
||||||
|
'platforms': PlatformsValidator,
|
||||||
'tags': TagsValidator,
|
'tags': TagsValidator,
|
||||||
|
'website': StringValidator,
|
||||||
|
'wheels': WheelsValidator,
|
||||||
}
|
}
|
||||||
all_fields = {**mandatory_fields, **optional_fields}
|
all_fields = {**mandatory_fields, **optional_fields}
|
||||||
|
|
||||||
|
@ -64,4 +64,6 @@
|
|||||||
{% trans 'You have no notifications' %}
|
{% trans 'You have no notifications' %}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{{ page_obj|paginator }}
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
@ -13,7 +13,7 @@ from notifications.models import Notification
|
|||||||
|
|
||||||
class NotificationsView(LoginRequiredMixin, ListView):
|
class NotificationsView(LoginRequiredMixin, ListView):
|
||||||
model = Notification
|
model = Notification
|
||||||
paginate_by = 10
|
paginate_by = 20
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return Notification.objects.filter(recipient=self.request.user).order_by('-id')
|
return Notification.objects.filter(recipient=self.request.user).order_by('-id')
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
- name: Coming up next
|
- name: Coming up next
|
||||||
ansible.builtin.debug:
|
ansible.builtin.debug:
|
||||||
msg: Running {{ playbook_type }} of {{ source_url }}@{{ branch }} to {{ env }} ({{ domain }})
|
msg: Running {{ playbook_type }} of {{ source_url }}@{{ branch }} to {{ env }} ({{ domain }})
|
||||||
when: 'playbook_type is defined'
|
when: playbook_type is defined
|
||||||
tags:
|
tags:
|
||||||
- always
|
- always
|
||||||
|
|
||||||
@ -16,7 +16,7 @@
|
|||||||
{% if not ansible_check_mode and not ansible_diff_mode %} and {% endif %}{% if not ansible_check_mode %}--check for dry run{% endif %}.
|
{% if not ansible_check_mode and not ansible_diff_mode %} and {% endif %}{% if not ansible_check_mode %}--check for dry run{% endif %}.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
Press return to continue. Press Ctrl+c and then "a" to abort.
|
Press return to continue. Press Ctrl+c and then "a" to abort.
|
||||||
when: 'playbook_type is defined'
|
when: playbook_type is defined
|
||||||
tags:
|
tags:
|
||||||
- always
|
- always
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
---
|
---
|
||||||
- name: "Writing {{ conf_d }}{{ conf_f }}"
|
- name: Writing {{ conf_d }}{{ conf_f }}
|
||||||
vars:
|
vars:
|
||||||
conf_d: /etc/nginx/conf.d/
|
conf_d: /etc/nginx/conf.d/
|
||||||
conf_f: log-format-upstreaminfo.conf
|
conf_f: log-format-upstreaminfo.conf
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
src: "templates/nginx/conf.d/{{ conf_f }}"
|
src: templates/nginx/conf.d/{{ conf_f }}
|
||||||
dest: "{{ conf_d }}{{ conf_f }}"
|
dest: "{{ conf_d }}{{ conf_f }}"
|
||||||
backup: true
|
backup: true
|
||||||
mode: 0664
|
mode: 0664
|
||||||
|
@ -24,6 +24,28 @@
|
|||||||
tags:
|
tags:
|
||||||
- deps
|
- deps
|
||||||
|
|
||||||
|
- name: Configuring ClamAV
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /etc/clamav/clamd.conf
|
||||||
|
regexp: "{{ item.regexp }}"
|
||||||
|
line: "{{ item.line }}"
|
||||||
|
state: present
|
||||||
|
backup: true
|
||||||
|
with_items:
|
||||||
|
- regexp: ^#*\s*MaxScanSize\s
|
||||||
|
line: MaxScanSize 200M
|
||||||
|
- regexp: ^#*\s*MaxFileSize\s
|
||||||
|
line: MaxFileSize 200M
|
||||||
|
- regexp: ^#*\s*PCREMaxFileSize\s
|
||||||
|
line: PCREMaxFileSize 200M
|
||||||
|
- regexp: ^#*\s*StreamMaxLength\s
|
||||||
|
line: StreamMaxLength 200M
|
||||||
|
notify:
|
||||||
|
- Restart ClamAV daemon
|
||||||
|
tags:
|
||||||
|
- deps
|
||||||
|
- clamav
|
||||||
|
|
||||||
- name: Creating user "{{ user }}:{{ group }}"
|
- name: Creating user "{{ user }}:{{ group }}"
|
||||||
ansible.builtin.user:
|
ansible.builtin.user:
|
||||||
name: "{{ user }}"
|
name: "{{ user }}"
|
||||||
@ -60,3 +82,10 @@
|
|||||||
- import_tasks: tasks/setup_other_services.yaml
|
- import_tasks: tasks/setup_other_services.yaml
|
||||||
tags:
|
tags:
|
||||||
- services
|
- services
|
||||||
|
|
||||||
|
handlers:
|
||||||
|
- name: Restart ClamAV daemon
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: clamav-daemon.service
|
||||||
|
state: restarted
|
||||||
|
enabled: true
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
owner: root
|
owner: root
|
||||||
group: root
|
group: root
|
||||||
state: directory
|
state: directory
|
||||||
mode: '0755'
|
mode: "0755"
|
||||||
tags:
|
tags:
|
||||||
- uwsgi
|
- uwsgi
|
||||||
- name: Copying uWSGI config files
|
- name: Copying uWSGI config files
|
||||||
|
@ -9,7 +9,7 @@ max_requests: 1000
|
|||||||
max_requests_jitter: 50
|
max_requests_jitter: 50
|
||||||
port: 8200
|
port: 8200
|
||||||
workers: 2
|
workers: 2
|
||||||
client_max_body_size: "50m"
|
client_max_body_size: "200m"
|
||||||
python_version: "3.10"
|
python_version: "3.10"
|
||||||
delete_venv: false # set to true if venv has to be re-created from scratch
|
delete_venv: false # set to true if venv has to be re-created from scratch
|
||||||
|
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
{% load filters %}
|
{% load filters %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="ext-review-list-type">{{ extension.get_type_display }}</td>
|
<td class="ext-review-list-type">{{ extension.get_type_display }}</td>
|
||||||
<td>
|
<td class="ext-review-list-name">
|
||||||
|
<a href="{{ extension.get_review_url }}">
|
||||||
|
{% include "extensions/components/icon.html" %}
|
||||||
|
</a>
|
||||||
<a href="{{ extension.get_review_url }}">
|
<a href="{{ extension.get_review_url }}">
|
||||||
{{ extension.name }}
|
{{ extension.name }}
|
||||||
</a>
|
</a>
|
||||||
|
@ -101,31 +101,8 @@
|
|||||||
<ul class="activity-list">
|
<ul class="activity-list">
|
||||||
{% for activity in review_activity %}
|
{% for activity in review_activity %}
|
||||||
<li id="activity-{{ activity.id }}">
|
<li id="activity-{{ activity.id }}">
|
||||||
|
|
||||||
{% if activity.type in status_change_types %}
|
|
||||||
<div class="activity-item activity-status-change activity-status-{{ activity.get_type_display|slugify }}">
|
|
||||||
<i class="activity-icon i-activity-{{ activity.get_type_display|slugify }}"></i>
|
|
||||||
|
|
||||||
<a href="{% url "extensions:by-author" user_id=activity.user.pk %}">
|
|
||||||
{% include "users/components/profile_display.html" with user=activity.user classes="" %}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{# Comments. #}
|
|
||||||
{% if activity.message %}
|
|
||||||
<article class="activity-item comment-card">
|
<article class="activity-item comment-card">
|
||||||
<i class="activity-icon i-comment"></i>
|
<i class="activity-icon {% if activity.type in status_change_types %}i-activity-{{ activity.get_type_display|slugify }}{% else %}i-comment{% endif %}"></i>
|
||||||
|
|
||||||
<aside>
|
<aside>
|
||||||
<a href="{% url "extensions:by-author" user_id=activity.user.pk %}">
|
<a href="{% url "extensions:by-author" user_id=activity.user.pk %}">
|
||||||
{% include "users/components/profile_display.html" with user=activity.user classes="" %}
|
{% include "users/components/profile_display.html" with user=activity.user classes="" %}
|
||||||
@ -138,6 +115,12 @@
|
|||||||
<a href="{% url "extensions:by-author" user_id=activity.user.pk %}">
|
<a href="{% url "extensions:by-author" user_id=activity.user.pk %}">
|
||||||
{{ activity.user }}
|
{{ activity.user }}
|
||||||
</a>
|
</a>
|
||||||
|
{% if activity.type in status_change_types %}
|
||||||
|
changed review status to
|
||||||
|
<span class="badge badge-status-{{ activity.get_type_display|slugify }}">
|
||||||
|
{{ activity.get_type_display }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
<li class="ms-auto">
|
<li class="ms-auto">
|
||||||
<a href="#activity-{{ activity.id }}" title="{{ activity.date_created }}">
|
<a href="#activity-{{ activity.id }}" title="{{ activity.date_created }}">
|
||||||
@ -146,10 +129,10 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</header>
|
</header>
|
||||||
<div>{{ activity.message|markdown }}</div>
|
<div>
|
||||||
|
{{ activity.message|markdown }}</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
{% endif %}
|
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{% extends "common/base.html" %}
|
{% extends "common/base.html" %}
|
||||||
{% load i18n humanize filters %}
|
{% load i18n humanize filters common %}
|
||||||
|
|
||||||
{% block page_title %}Approval queue{% endblock page_title %}
|
{% block page_title %}Approval queue{% endblock page_title %}
|
||||||
|
|
||||||
@ -45,5 +45,6 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<p>{% trans "No extensions to review." %}</p>
|
<p>{% trans "No extensions to review." %}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{{ page_obj|paginator }}
|
||||||
</section>
|
</section>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
@ -11,6 +11,7 @@ class TeamsUsersInline(admin.TabularInline):
|
|||||||
|
|
||||||
@admin.register(teams.models.Team)
|
@admin.register(teams.models.Team)
|
||||||
class TeamAdmin(admin.ModelAdmin):
|
class TeamAdmin(admin.ModelAdmin):
|
||||||
|
save_on_top = True
|
||||||
list_display = ('name', 'slug', 'user_count', 'date_created')
|
list_display = ('name', 'slug', 'user_count', 'date_created')
|
||||||
list_display_links = ['name']
|
list_display_links = ['name']
|
||||||
list_filter = ['date_created']
|
list_filter = ['date_created']
|
||||||
|
@ -13,6 +13,7 @@ class UserAdmin(auth_admin.UserAdmin):
|
|||||||
|
|
||||||
list_display_links = ['username']
|
list_display_links = ['username']
|
||||||
list_filter = auth_admin.UserAdmin.list_filter + (
|
list_filter = auth_admin.UserAdmin.list_filter + (
|
||||||
|
'groups__name',
|
||||||
'date_joined',
|
'date_joined',
|
||||||
'confirmed_email_at',
|
'confirmed_email_at',
|
||||||
'date_deletion_requested',
|
'date_deletion_requested',
|
||||||
|
Loading…
Reference in New Issue
Block a user