Merge 'policies' and 'conditions of use' pages #117
18
abuse/migrations/0007_alter_abusereport_status.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.11 on 2024-05-06 13:30
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('abuse', '0006_remove_abusereport_date_deleted'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='abusereport',
|
||||
name='status',
|
||||
field=models.PositiveSmallIntegerField(choices=[(1, 'Untriaged'), (2, 'Confirmed'), (3, 'Resolved')], default=1),
|
||||
),
|
||||
]
|
@ -29,8 +29,8 @@ class AbuseReport(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
||||
|
||||
STATUSES = Choices(
|
||||
('UNTRIAGED', 1, 'Untriaged'),
|
||||
('VALID', 2, 'Valid'),
|
||||
('SUSPICIOUS', 3, 'Suspicious'),
|
||||
('CONFIRMED', 2, 'Confirmed'),
|
||||
('RESOLVED', 3, 'Resolved'),
|
||||
)
|
||||
|
||||
# NULL if the reporter is anonymous.
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit af61a962e1a30898279b4efdbb07a2dcb230a257
|
||||
Subproject commit 1126f102d8542ffb76af0269854048f276d9e50b
|
@ -10,9 +10,13 @@ from django.http.request import HttpRequest
|
||||
|
||||
def extra_context(request: HttpRequest) -> Dict[str, str]:
|
||||
"""Injects some configuration values into template context."""
|
||||
user_is_moderator = False
|
||||
if request.user.is_authenticated:
|
||||
user_is_moderator = request.user.is_moderator
|
||||
return {
|
||||
'BLENDER_ID': {
|
||||
'BASE_URL': settings.BLENDER_ID['BASE_URL'],
|
||||
},
|
||||
'canonical_url': request.build_absolute_uri(request.path),
|
||||
'user_is_moderator': user_is_moderator,
|
||||
}
|
||||
|
BIN
common/static/common/images/no-icon.png
Normal file
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
54
common/static/common/images/no-image.svg
Normal file
@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
width="1920"
|
||||
height="1080"
|
||||
viewBox="0 0 1920 1080"
|
||||
sodipodi:docname="no-image.svg"
|
||||
inkscape:export-filename="no-image_640x360.png"
|
||||
inkscape:export-xdpi="32"
|
||||
inkscape:export-ydpi="32"
|
||||
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#bbbbbb"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="false"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showguides="true"
|
||||
inkscape:export-bgcolor="#3f3f3fff"
|
||||
inkscape:zoom="0.28945313"
|
||||
inkscape:cx="540.67475"
|
||||
inkscape:cy="393.84614"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1056"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g1" />
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
inkscape:label="Image"
|
||||
id="g1">
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#ffffff"
|
||||
stroke-width="0.4"
|
||||
d="m 823.5878,596.10633 q 0,-24.97037 20.96278,-36.37659 -20.96278,-10.78966 -20.96278,-35.76002 v -28.05313 q 0,-23.1207 16.64692,-39.76761 16.64691,-16.64692 39.76761,-16.64692 h 11.71449 q 2.15795,-23.73725 21.57934,-40.07589 19.42139,-16.33863 46.85797,-16.03036 27.43657,0.30827 46.54967,16.03036 19.1132,15.72208 21.5794,40.07589 h 12.0227 q 23.1207,0 39.4594,16.64692 16.3386,16.64691 16.6469,39.76761 v 28.05313 q 0,24.97036 -20.6544,36.37658 20.6544,10.78966 20.6544,35.76003 v 44.39177 q 0,23.12071 -16.6469,39.76761 -16.647,16.64691 -39.4594,16.33864 h -44.08347 q -24.97036,0 -36.0683,-20.65451 -10.78967,20.65451 -36.37658,20.65451 h -43.77522 q -23.1207,0 -39.76761,-16.33864 Q 823.5878,663.92708 823.5878,640.4981 Z m 240.7637,44.39177 v -44.39177 q 0,-7.70691 -5.549,-7.70691 -4.0076,0 -9.2483,3.69932 -5.2407,3.69932 -13.2559,4.00759 -11.4061,0 -19.7296,-10.48139 -8.3235,-10.48138 -8.3235,-25.8952 0,-15.4138 8.3235,-25.27863 8.3235,-9.86484 19.7296,-10.48139 8.6317,0 15.7222,6.16553 2.7744,1.84966 6.1654,1.84966 6.1656,0 6.1656,-8.01519 v -28.05313 q 0,-10.17311 -7.0903,-17.26346 -7.0905,-7.09035 -16.9553,-7.09035 h -44.08347 q -4.93242,0 -7.09034,-2.77448 -2.15793,-2.77449 0.92482,-9.55656 6.16552,-7.39863 6.16552,-15.72209 0,-11.71449 -10.48139,-19.72967 -10.48139,-8.01517 -25.58691,-8.01517 -15.10553,0 -25.89519,8.01517 -10.78968,8.01518 -10.48139,19.72967 0,6.4738 4.31586,13.56416 5.2407,8.01517 3.39104,11.4062 -1.84966,3.39105 -7.7069,3.08277 h -43.77522 q -10.1731,0 -17.26346,7.09035 -7.09034,7.09035 -7.09034,17.26346 v 28.05313 q 0,8.01519 6.16551,8.01519 2.77449,0 8.94002,-4.00761 5.24069,-4.00758 12.94759,-4.00758 11.71449,0 19.72968,10.48139 8.01517,10.48139 8.32344,25.27863 0.30829,14.79727 -8.32344,25.8952 -8.63173,11.09794 -19.72968,10.48139 -8.32346,0 -15.72208,-6.16552 -3.08276,-1.84966 -6.16553,-1.84966 -6.16551,0 -6.16551,8.01518 v 44.39177 q 0,9.55655 7.09034,16.64691 7.09036,7.09034 17.26346,7.09034 h 43.77522 q 5.85724,0 7.39862,-3.08276 1.54138,-3.08276 -3.08276,-11.40622 -4.31586,-6.16551 -4.31586,-13.56414 0,-11.71449 10.48139,-19.72967 10.48139,-8.01519 25.89519,-8.01519 15.41381,0 25.58691,8.01519 10.17312,8.01518 10.48139,19.72967 0,8.32346 -6.16552,15.72207 -3.08275,6.78208 -0.92482,9.55656 2.15792,2.77449 7.09034,2.77449 h 44.08347 q 9.8648,0 16.9553,-7.09034 7.0903,-7.09036 7.0903,-16.64691 z M 855.64853,547.09042 v 25.58693 q 1.84965,-0.92483 6.16551,-0.92483 9.86485,0 16.95519,6.16552 2.15793,2.46621 4.93242,2.46621 4.62414,0 8.63173,-6.16552 4.00759,-6.16553 3.69932,-14.48899 -0.30827,-8.32344 -3.69932,-14.18069 -3.39103,-5.85724 -8.63173,-5.85724 -0.61656,0 -5.24069,2.4662 -8.94002,5.85724 -16.64692,5.85724 -2.4662,0 -6.16551,-0.92483 z m 203.15397,24.97037 q 1.8497,0 5.549,0.92483 v -25.58691 q -1.8498,0.92483 -6.1656,0.92483 -9.5566,0 -16.3386,-6.16553 -2.4661,-2.4662 -5.549,-2.4662 -4.6241,0 -8.3234,5.85724 -3.6993,5.85725 -3.6993,14.18069 0,8.32346 3.6993,14.48899 3.6993,6.16552 8.3234,6.16552 1.2332,0 5.8573,-2.46621 8.94,-5.85725 16.6469,-5.85725 z m -111.90424,92.17456 h 25.89519 q -4.00759,-12.63932 4.93242,-22.81242 2.46621,-2.15795 2.46621,-5.2407 0,-4.62415 -5.85726,-8.32346 -5.85724,-3.6993 -14.18069,-3.6993 -8.32346,0 -14.48897,3.6993 -6.16553,3.69931 -5.54898,8.32346 0,1.2331 2.15793,5.85724 7.70691,11.09795 4.62415,22.19588 z"
|
||||
horiz-adv-x="885"
|
||||
id="path1"
|
||||
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:7.27776;stroke-linecap:square;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
BIN
common/static/common/images/no-image_640x360.png
Normal file
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
@ -1,5 +1,27 @@
|
||||
// Create function btnBack
|
||||
function btnBack() {
|
||||
(function() {
|
||||
// Create function agreeWithTerms
|
||||
function agreeWithTerms() {
|
||||
const agreeWithTermsInput = document.querySelector('.js-agree-with-terms-input');
|
||||
|
||||
if (!agreeWithTermsInput) {
|
||||
// Stop function execution if agreeWithTermsInput is not present
|
||||
return;
|
||||
}
|
||||
|
||||
agreeWithTermsInput.addEventListener('change', function(e) {
|
||||
const agreeWithTermsBtnSubmit = document.querySelector('.js-agree-with-terms-btn-submit');
|
||||
|
||||
// Check if checkbox is checked
|
||||
if (e.target.checked == true) {
|
||||
agreeWithTermsBtnSubmit.removeAttribute('disabled');
|
||||
} else {
|
||||
agreeWithTermsBtnSubmit.setAttribute('disabled', true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Create function btnBack
|
||||
function btnBack() {
|
||||
const btnBack = document.querySelectorAll('.js-btn-back');
|
||||
|
||||
btnBack.forEach(function(item) {
|
||||
@ -8,10 +30,10 @@ function btnBack() {
|
||||
window.history.back();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create finction commentForm
|
||||
function commentForm() {
|
||||
// Create finction commentForm
|
||||
function commentForm() {
|
||||
const commentForm = document.querySelector('.js-comment-form');
|
||||
if (!commentForm) {
|
||||
return;
|
||||
@ -27,7 +49,7 @@ function commentForm() {
|
||||
let value = e.target.value;
|
||||
let verb = 'Comment';
|
||||
const activitySubmitButton = document.getElementById('activity-submit');
|
||||
activitySubmitButton.classList.remove('btn-success', 'btn-warning');
|
||||
activitySubmitButton.classList.remove('btn-primary', 'btn-success', 'btn-warning');
|
||||
|
||||
// Hide or show comment form msg on change
|
||||
if (value == 'AWC') {
|
||||
@ -38,14 +60,16 @@ function commentForm() {
|
||||
} else if (value == 'APR') {
|
||||
verb = 'Approve!';
|
||||
activitySubmitButton.classList.add('btn-success');
|
||||
} else {
|
||||
activitySubmitButton.classList.add('btn-primary');
|
||||
}
|
||||
|
||||
activitySubmitButton.querySelector('span').textContent = verb;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create function copyInstallUrl
|
||||
function copyInstallUrl() {
|
||||
// Create function copyInstallUrl
|
||||
function copyInstallUrl() {
|
||||
function init() {
|
||||
// Create variables
|
||||
const btnInstall = document.querySelector('.js-btn-install');
|
||||
@ -86,14 +110,16 @@ function copyInstallUrl() {
|
||||
}
|
||||
|
||||
init();
|
||||
}
|
||||
// Create function init
|
||||
function init() {
|
||||
}
|
||||
// Create function init
|
||||
function init() {
|
||||
agreeWithTerms();
|
||||
btnBack();
|
||||
commentForm();
|
||||
copyInstallUrl();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
init();
|
||||
});
|
||||
});
|
||||
}())
|
||||
|
@ -67,6 +67,10 @@
|
||||
.activity-icon
|
||||
top: 2.2rem
|
||||
|
||||
code,
|
||||
pre
|
||||
white-space: normal
|
||||
|
||||
.activity-status-change
|
||||
color: var(--color-text-tertiary)
|
||||
|
||||
|
4
common/static/common/styles/_button.sass
Normal file
@ -0,0 +1,4 @@
|
||||
button,
|
||||
.btn
|
||||
&[type=submit]
|
||||
transition: opacity var(--transition-speed)
|
@ -80,13 +80,6 @@
|
||||
.ext-detail-tagline
|
||||
+margin(2, bottom)
|
||||
|
||||
.ext-detail-description
|
||||
+padding(4)
|
||||
+style-rich-text
|
||||
|
||||
pre
|
||||
+margin(3, bottom)
|
||||
|
||||
.ext-detail-info
|
||||
dd
|
||||
color: var(--color-text)
|
||||
@ -270,8 +263,6 @@
|
||||
+margin(3, left)
|
||||
|
||||
details
|
||||
padding: 0
|
||||
|
||||
&[open]
|
||||
.show-on-collapse
|
||||
display: none
|
||||
@ -311,7 +302,7 @@
|
||||
.label
|
||||
line-height: calc(var(--spacer) * 2)
|
||||
|
||||
/* Nabdrawer. */
|
||||
/* Navdrawer. */
|
||||
.nav-link
|
||||
&[class*=" i-"]::before
|
||||
+margin(2, right)
|
||||
@ -328,8 +319,6 @@
|
||||
|
||||
a
|
||||
color: var(--color-text)
|
||||
// TODO: @web-assets check arbitrary style table link display specificity
|
||||
display: inline !important
|
||||
+padding(1, y)
|
||||
padding-inline: 0 !important
|
||||
|
||||
@ -378,3 +367,23 @@
|
||||
@extend .dropdown-divider
|
||||
|
||||
+margin(0, top)
|
||||
|
||||
.dropdown-item
|
||||
&a
|
||||
+padding(3, x)
|
||||
|
||||
.extension-icon
|
||||
width: var(--fs-h1)
|
||||
|
||||
.icon-preview, .featured-image-preview
|
||||
height: 9rem
|
||||
background-size: contain
|
||||
background-repeat: no-repeat
|
||||
background-color: var(--color-bg)
|
||||
border-radius: var(--border-radius)
|
||||
|
||||
.icon-preview
|
||||
width: 9rem
|
||||
|
||||
.featured-image-preview
|
||||
width: 16rem
|
||||
|
@ -1,4 +1,7 @@
|
||||
/* Aliases to use existing icons as permission slugs. */
|
||||
.i-permission-clipboard
|
||||
@extend .i-copy
|
||||
|
||||
.i-permission-files
|
||||
@extend .i-folder
|
||||
|
||||
|
@ -11,7 +11,12 @@
|
||||
|
||||
.form-control
|
||||
&[type="file"]
|
||||
height: calc(var(--spacer) * 2.5)
|
||||
// TODO: @web-assets improve component style
|
||||
height: calc(var(--spacer) * 3.5)
|
||||
|
||||
.invalid-feedback
|
||||
ul
|
||||
+padding(3, left)
|
||||
|
||||
/* Override Tagger's styling. */
|
||||
.was-validated .form-control:invalid,
|
||||
|
@ -9,6 +9,11 @@
|
||||
--nav-global-spacer-sm: var(--spacer-2)
|
||||
--nav-global-spacer-xs: var(--spacer-1)
|
||||
|
||||
.btn
|
||||
&:hover
|
||||
background-color: var(--nav-global-color-button-bg-hover)
|
||||
color: var(--nav-global-color-text-hover) !important
|
||||
|
||||
.btn-primary
|
||||
color: var(--color-accent) !important
|
||||
|
||||
|
13
common/static/common/styles/_notifications.sass
Normal file
@ -0,0 +1,13 @@
|
||||
// TODO: remove style if 'mark as unread' is implemented
|
||||
// .notifications-item
|
||||
// &.is-read
|
||||
// .dropdown-toggle
|
||||
// color: var(--color-text-secondary) !important
|
||||
//
|
||||
// &:hover
|
||||
// cursor: default
|
||||
|
||||
.notifications-item
|
||||
.dropdown-item
|
||||
padding-left: var(--spacer) !important
|
||||
padding-right: var(--spacer) !important
|
@ -1,4 +1,10 @@
|
||||
table,
|
||||
.table
|
||||
a
|
||||
text-decoration: underline
|
||||
|
||||
th
|
||||
color: var(--color-text-secondary)
|
||||
|
||||
thead
|
||||
white-space: normal
|
||||
|
@ -37,6 +37,9 @@
|
||||
.show
|
||||
opacity: 1
|
||||
|
||||
.style-rich-text
|
||||
+style-rich-text
|
||||
|
||||
.text-accent
|
||||
color: var(--color-accent)
|
||||
|
||||
|
@ -18,6 +18,7 @@ $container-width: map-get($container-max-widths, 'xl')
|
||||
@import '_alert.sass'
|
||||
@import '_badge.sass'
|
||||
@import '_box.sass'
|
||||
@import '_button.sass'
|
||||
@import '_cards.sass'
|
||||
@import '_code.sass'
|
||||
@import '_comments.sass'
|
||||
@ -28,6 +29,7 @@ $container-width: map-get($container-max-widths, 'xl')
|
||||
@import '_hero.sass'
|
||||
@import '_list.sass'
|
||||
@import '_navigation_global.sass'
|
||||
@import '_notifications.sass'
|
||||
@import '_table.sass'
|
||||
@import 'ratings/static/ratings/styles/_review.sass'
|
||||
@import 'ratings/static/ratings/styles/_stars.sass'
|
||||
|
@ -126,7 +126,7 @@
|
||||
</li>
|
||||
|
||||
{% block nav-upload %}
|
||||
<li class="me-2">
|
||||
<li>
|
||||
<a href="{% url 'extensions:submit' %}" class="btn btn-primary">
|
||||
<i class="i-upload"></i>
|
||||
<span>Upload Extension</span>
|
||||
@ -135,11 +135,13 @@
|
||||
{% endblock nav-upload %}
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
<a href="{% url 'notifications:notifications' %}">
|
||||
<i class="i-bell {% if user|unread_notification_count %}text-primary{% endif %}"></i>
|
||||
<li>
|
||||
<a class="btn btn-link px-2" href="{% url 'notifications:notifications' %}">
|
||||
<i class="i-bell {% if user|unread_notification_count %}text-accent{% endif %}"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item 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">
|
||||
<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-chevron-down"></i>
|
||||
</button>
|
||||
@ -151,7 +153,7 @@
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if user.is_moderator %}
|
||||
{% if user_is_moderator %}
|
||||
<li>
|
||||
<a href="{% url 'abuse:report-list' %}" class="dropdown-item">
|
||||
<i class="i-shield"></i> {% trans "Abuse Reports" %}
|
||||
@ -202,7 +204,10 @@
|
||||
</ul>
|
||||
</li>
|
||||
{% elif page_id != 'login' and page_id != 'register' %}
|
||||
{% include "common/components/nav_item.html" with name="oauth:login" title="Sign in" %}
|
||||
<a href="{% url 'oauth:login' %}" class="btn btn-link">
|
||||
<i class="i-log-in"></i>
|
||||
<span>{% trans "Sign in" %}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<li>
|
||||
|
@ -2,7 +2,10 @@
|
||||
{% spaceless %}
|
||||
{% with type=field.field.widget.input_type classes=classes|default:"" placeholder=placeholder|default:"" %}
|
||||
{% with field=field|remove_cols_rows|add_classes:classes|set_placeholder:placeholder %}
|
||||
{% autoescape off %}
|
||||
{% firstof label field.label as label %}
|
||||
{% firstof help_text field.help_text as help_text %}
|
||||
{% endautoescape %}
|
||||
|
||||
{% comment %} Checkboxes {% endcomment %}
|
||||
{% if type == 'checkbox' %}
|
||||
@ -37,8 +40,8 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if field.help_text %}
|
||||
<div class="form-text">{{ field.help_text|safe }}</div>
|
||||
{% if help_text and not field.is_hidden %}
|
||||
<div class="form-text">{{ help_text|safe }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if field.errors %}
|
||||
|
@ -29,17 +29,17 @@
|
||||
<span>{{ status }}</span>
|
||||
</div>
|
||||
|
||||
{% elif 'untriaged' in status.lower %}
|
||||
{% elif 'confirmed' in status.lower %}
|
||||
<div class="badge badge-danger {{ class }}">
|
||||
<span>{{ status }}</span>
|
||||
</div>
|
||||
|
||||
{% elif 'suspicious' in status.lower %}
|
||||
{% elif 'untriaged' in status.lower %}
|
||||
<div class="badge badge-warning {{ class }}">
|
||||
<span>{{ status }}</span>
|
||||
</div>
|
||||
|
||||
{% elif 'valid' in status.lower %}
|
||||
{% elif 'resolved' in status.lower %}
|
||||
<div class="badge badge-success {{ class }}">
|
||||
<span>{{ status }}</span>
|
||||
</div>
|
||||
|
@ -60,7 +60,9 @@ EXTENSION_TYPE_PLURAL = {
|
||||
EXTENSION_SLUGS_PATH = '|'.join(EXTENSION_TYPE_SLUGS.values())
|
||||
EXTENSION_SLUG_TYPES = {v: k for k, v in EXTENSION_TYPE_SLUGS_SINGULAR.items()}
|
||||
|
||||
ALLOWED_EXTENSION_MIMETYPES = ('application/zip', )
|
||||
ALLOWED_EXTENSION_MIMETYPES = ('application/zip',)
|
||||
ALLOWED_FEATURED_IMAGE_MIMETYPES = ('image/jpg', 'image/jpeg', 'image/png', 'image/webp')
|
||||
ALLOWED_ICON_MIMETYPES = ('image/png',)
|
||||
# FIXME: this controls the initial widget rendered server-side, and server-side validation
|
||||
# but not the additional JS-appended preview file inputs.
|
||||
# If this list changes, the "accept" attribute also has to be updated in appendImageUploadForm.
|
||||
|
@ -62,6 +62,7 @@ class ExtensionAdmin(admin.ModelAdmin):
|
||||
'website',
|
||||
)
|
||||
raw_id_fields = ('team',)
|
||||
autocomplete_fields = ('icon', 'featured_image')
|
||||
|
||||
fieldsets = (
|
||||
(
|
||||
@ -79,6 +80,7 @@ class ExtensionAdmin(admin.ModelAdmin):
|
||||
'name',
|
||||
'slug',
|
||||
'description',
|
||||
('icon', 'featured_image'),
|
||||
'status',
|
||||
),
|
||||
},
|
||||
@ -202,8 +204,13 @@ class TagAdmin(admin.ModelAdmin):
|
||||
return ()
|
||||
|
||||
|
||||
class VersionPermissionAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'slug')
|
||||
|
||||
|
||||
admin.site.register(models.Extension, ExtensionAdmin)
|
||||
admin.site.register(models.Version, VersionAdmin)
|
||||
admin.site.register(models.Maintainer, MaintainerAdmin)
|
||||
admin.site.register(models.License, LicenseAdmin)
|
||||
admin.site.register(models.Tag, TagAdmin)
|
||||
admin.site.register(models.VersionPermission, VersionPermissionAdmin)
|
||||
|
@ -2,12 +2,15 @@ import logging
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import django.core.exceptions
|
||||
|
||||
from files.validators import FileMIMETypeValidator
|
||||
from constants.base import ALLOWED_PREVIEW_MIMETYPES
|
||||
from constants.base import (
|
||||
ALLOWED_FEATURED_IMAGE_MIMETYPES,
|
||||
ALLOWED_ICON_MIMETYPES,
|
||||
ALLOWED_PREVIEW_MIMETYPES,
|
||||
)
|
||||
|
||||
import extensions.models
|
||||
import files.forms
|
||||
import files.models
|
||||
import reviewers.models
|
||||
|
||||
@ -38,61 +41,22 @@ EditPreviewFormSet = forms.inlineformset_factory(
|
||||
)
|
||||
|
||||
|
||||
class AddPreviewFileForm(forms.ModelForm):
|
||||
msg_unexpected_file_type = _('Choose a JPEG, PNG or WebP image, or an MP4 video')
|
||||
class AddPreviewFileForm(files.forms.BaseMediaFileForm):
|
||||
allowed_mimetypes = ALLOWED_PREVIEW_MIMETYPES
|
||||
error_messages = {'invalid_mimetype': _('Choose a JPEG, PNG or WebP image, or an MP4 video')}
|
||||
|
||||
class Meta:
|
||||
model = files.models.File
|
||||
fields = ('caption', 'source', 'original_hash', 'hash')
|
||||
widgets = {'original_hash': forms.HiddenInput(), 'hash': forms.HiddenInput()}
|
||||
class Meta(files.forms.BaseMediaFileForm.Meta):
|
||||
fields = ('caption',) + files.forms.BaseMediaFileForm.Meta.fields
|
||||
|
||||
source = forms.FileField(
|
||||
allow_empty_file=False,
|
||||
required=True,
|
||||
validators=[
|
||||
FileMIMETypeValidator(
|
||||
allowed_mimetypes=ALLOWED_PREVIEW_MIMETYPES,
|
||||
message=msg_unexpected_file_type,
|
||||
),
|
||||
],
|
||||
widget=forms.ClearableFileInput(
|
||||
attrs={'accept': ','.join(ALLOWED_PREVIEW_MIMETYPES)},
|
||||
),
|
||||
)
|
||||
caption = forms.CharField(max_length=255, required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.request = kwargs.pop('request')
|
||||
self.extension = kwargs.pop('extension')
|
||||
self.base_fields['source'].required = True
|
||||
self.base_fields['caption'].widget.attrs.update({'placeholder': 'Describe the preview'})
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean_original_hash(self, *args, **kwargs):
|
||||
"""Calculate original hash of the uploaded file."""
|
||||
if 'source' not in self.cleaned_data:
|
||||
return
|
||||
source = self.cleaned_data['source']
|
||||
return files.models.File.generate_hash(source)
|
||||
|
||||
def clean_hash(self, *args, **kwargs):
|
||||
return self.cleaned_data['original_hash']
|
||||
|
||||
def add_error(self, field, error):
|
||||
"""Add hidden `original_hash`/`hash` errors to the visible `source` field instead."""
|
||||
if isinstance(error, django.core.exceptions.ValidationError):
|
||||
if getattr(error, 'error_dict', None):
|
||||
hash_error = error.error_dict.pop('hash', None)
|
||||
if hash_error:
|
||||
error.error_dict['source'] = hash_error
|
||||
# `original_hash` is treated identically to `hash`, so its errors can be discarded
|
||||
error.error_dict.pop('original_hash', None)
|
||||
super().add_error(field, error)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Save Preview from the cleaned form data."""
|
||||
# Fill in missing fields from request and the source file
|
||||
self.instance.user = self.request.user
|
||||
|
||||
instance = super().save(*args, **kwargs)
|
||||
|
||||
# Create extension preview and save caption to it
|
||||
@ -169,24 +133,46 @@ class ExtensionUpdateForm(forms.ModelForm):
|
||||
extension=self.instance,
|
||||
request=self.request,
|
||||
)
|
||||
featured_image_form = FeaturedImageForm(
|
||||
self.request.POST,
|
||||
self.request.FILES,
|
||||
extension=self.instance,
|
||||
request=self.request,
|
||||
)
|
||||
icon_form = IconForm(
|
||||
self.request.POST,
|
||||
self.request.FILES,
|
||||
extension=self.instance,
|
||||
request=self.request,
|
||||
)
|
||||
else:
|
||||
edit_preview_formset = EditPreviewFormSet(instance=self.instance)
|
||||
add_preview_formset = AddPreviewFormSet(extension=self.instance, request=self.request)
|
||||
featured_image_form = FeaturedImageForm(extension=self.instance, request=self.request)
|
||||
icon_form = IconForm(extension=self.instance, request=self.request)
|
||||
self.edit_preview_formset = edit_preview_formset
|
||||
self.add_preview_formset = add_preview_formset
|
||||
self.featured_image_form = featured_image_form
|
||||
self.icon_form = icon_form
|
||||
|
||||
self.add_preview_formset.error_messages['too_few_forms'] = self.msg_need_previews
|
||||
|
||||
def is_valid(self, *args, **kwargs) -> bool:
|
||||
"""Validate all nested forms and form(set)s first."""
|
||||
# Require at least one preview image when requesting a review
|
||||
if 'submit_draft' in self.data:
|
||||
# Require at least one preview image when requesting a review
|
||||
if not self.instance.previews.exists():
|
||||
self.add_preview_formset.min_num = 1
|
||||
self.add_preview_formset.validate_min = True
|
||||
# Make feature image and icon required too
|
||||
self.featured_image_form.fields['source'].required = True
|
||||
self.icon_form.fields['source'].required = True
|
||||
|
||||
is_valid_flags = [
|
||||
self.edit_preview_formset.is_valid(),
|
||||
self.add_preview_formset.is_valid(),
|
||||
self.featured_image_form.is_valid(),
|
||||
self.icon_form.is_valid(),
|
||||
super().is_valid(*args, **kwargs),
|
||||
]
|
||||
return all(is_valid_flags)
|
||||
@ -212,6 +198,14 @@ class ExtensionUpdateForm(forms.ModelForm):
|
||||
"""Save the nested form(set)s, then the main form."""
|
||||
self.edit_preview_formset.save()
|
||||
self.add_preview_formset.save()
|
||||
|
||||
# Featured image and icon are only required when ready for review,
|
||||
# and can be empty or unchanged.
|
||||
if self.featured_image_form.has_changed():
|
||||
self.featured_image_form.save()
|
||||
if self.icon_form.has_changed():
|
||||
self.icon_form.save()
|
||||
|
||||
if getattr(self.instance, 'converted_to_draft', False):
|
||||
reviewers.models.ApprovalActivity(
|
||||
user=self.request.user,
|
||||
@ -259,3 +253,17 @@ class VersionDeleteForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = extensions.models.Version
|
||||
fields = []
|
||||
|
||||
|
||||
class FeaturedImageForm(files.forms.BaseMediaFileForm):
|
||||
prefix = 'featured-image'
|
||||
to_field = 'featured_image'
|
||||
allowed_mimetypes = ALLOWED_FEATURED_IMAGE_MIMETYPES
|
||||
error_messages = {'invalid_mimetype': _('Choose a JPEG, PNG or WebP image')}
|
||||
|
||||
|
||||
class IconForm(files.forms.BaseMediaFileForm):
|
||||
prefix = 'icon'
|
||||
to_field = 'icon'
|
||||
allowed_mimetypes = ALLOWED_ICON_MIMETYPES
|
||||
error_messages = {'invalid_mimetype': _('Choose a PNG image')}
|
||||
|
@ -0,0 +1,23 @@
|
||||
# Generated by Django 4.2.11 on 2024-05-06 12:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extensions', '0027_unique_preview_files'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='license',
|
||||
name='slug',
|
||||
field=models.SlugField(help_text='Should be taken from https://spdx.org/licenses/', unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='versionpermission',
|
||||
name='slug',
|
||||
field=models.SlugField(help_text='Permissions add-ons are expected to need.', unique=True),
|
||||
),
|
||||
]
|
@ -0,0 +1,25 @@
|
||||
# Generated by Django 4.2.11 on 2024-05-06 17:20
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('files', '0008_alter_file_thumbnail'),
|
||||
('extensions', '0028_alter_license_slug_alter_versionpermission_slug'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='extension',
|
||||
name='featured_image',
|
||||
field=models.OneToOneField(help_text='Shown by social networks when this extension is shared (used as `og:image` metadata field).Should have resolution of at least 1920 x 1080 and aspect ratio of 16:9.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='featured_image_of', to='files.file'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='extension',
|
||||
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'),
|
||||
),
|
||||
]
|
@ -19,8 +19,6 @@ from constants.base import (
|
||||
EXTENSION_TYPE_SLUGS,
|
||||
FILE_STATUS_CHOICES,
|
||||
)
|
||||
from constants.licenses import ALL_LICENSES
|
||||
from constants.version_permissions import ALL_VERSION_PERMISSIONS
|
||||
import common.help_texts
|
||||
import extensions.fields
|
||||
|
||||
@ -90,22 +88,13 @@ class License(CreatedModifiedMixin, models.Model):
|
||||
blank=False,
|
||||
null=False,
|
||||
help_text='Should be taken from https://spdx.org/licenses/',
|
||||
unique=True,
|
||||
)
|
||||
url = models.URLField(blank=False, null=False)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'{self.name}'
|
||||
|
||||
@classmethod
|
||||
def generate(cls):
|
||||
"""Generate License records from constants."""
|
||||
licenses = [cls(id=li.id, name=li.name, slug=li.slug, url=li.url) for li in ALL_LICENSES]
|
||||
cls.objects.bulk_create(licenses)
|
||||
|
||||
@classmethod
|
||||
def get_by_name(cls, name: str):
|
||||
return cls.objects.filter(name__startswith=name).first()
|
||||
|
||||
@classmethod
|
||||
def get_by_slug(cls, slug: str):
|
||||
return cls.objects.filter(slug__startswith=slug).first()
|
||||
@ -155,13 +144,29 @@ class Extension(CreatedModifiedMixin, RatingMixin, TrackChangesMixin, models.Mod
|
||||
help_text='Whether the extension should be listed. It is kept in sync via signals.',
|
||||
default=False,
|
||||
)
|
||||
previews = FilterableManyToManyField(
|
||||
|
||||
featured_image = models.OneToOneField(
|
||||
'files.File',
|
||||
through='Preview',
|
||||
related_name='extensions',
|
||||
# TODO: filter only images and videos.
|
||||
# q_filter=Q(type=FILE_TYPE_CHOICES.IMAGE),
|
||||
related_name='featured_image_of',
|
||||
null=True,
|
||||
blank=False,
|
||||
on_delete=models.SET_NULL,
|
||||
help_text=(
|
||||
"Shown by social networks when this extension is shared"
|
||||
" (used as `og:image` metadata field)."
|
||||
"Should have resolution of at least 1920 x 1080 and aspect ratio of 16:9."
|
||||
),
|
||||
)
|
||||
icon = models.OneToOneField(
|
||||
'files.File',
|
||||
related_name='icon_of',
|
||||
null=True,
|
||||
blank=False,
|
||||
on_delete=models.SET_NULL,
|
||||
help_text="A 256 x 256 icon representing this extension.",
|
||||
)
|
||||
previews = FilterableManyToManyField('files.File', through='Preview', related_name='extensions')
|
||||
|
||||
status = models.PositiveSmallIntegerField(choices=STATUSES, default=STATUSES.INCOMPLETE)
|
||||
support = models.URLField(
|
||||
help_text='URL for reporting issues or contact details for support.', null=True, blank=True
|
||||
@ -277,12 +282,16 @@ class Extension(CreatedModifiedMixin, RatingMixin, TrackChangesMixin, models.Mod
|
||||
def get_review_url(self):
|
||||
return reverse('reviewers:approval-detail', args=[self.slug])
|
||||
|
||||
def get_previews(self):
|
||||
"""Get preview files, sorted by Preview.position.
|
||||
def get_previews(self) -> List['Preview']:
|
||||
"""Get all preview files, sorted by Preview.position.
|
||||
|
||||
Avoid triggering additional querysets, rely on prefetch_related in the view.
|
||||
"""
|
||||
return [p.file for p in self.preview_set.all() if p.file.is_listed]
|
||||
return [p for p in self.preview_set.all()]
|
||||
|
||||
def get_previews_listed(self) -> List['Preview']:
|
||||
"""Get publicly listed preview files, sorted by Preview.position."""
|
||||
return [p for p in self.get_previews() if p.file.is_listed]
|
||||
|
||||
@property
|
||||
def valid_file_statuses(self) -> List[int]:
|
||||
@ -386,28 +395,16 @@ class VersionPermission(CreatedModifiedMixin, models.Model):
|
||||
blank=False,
|
||||
null=False,
|
||||
help_text='Permissions add-ons are expected to need.',
|
||||
unique=True,
|
||||
)
|
||||
help = models.CharField(max_length=128, null=False, blank=False, unique=True)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'{self.name}'
|
||||
|
||||
@classmethod
|
||||
def generate(cls):
|
||||
"""Generate Permission records from constants."""
|
||||
permissions = [
|
||||
cls(id=li.id, name=li.name, slug=li.slug, help=li.help)
|
||||
for li in ALL_VERSION_PERMISSIONS
|
||||
]
|
||||
cls.objects.bulk_create(permissions)
|
||||
|
||||
@classmethod
|
||||
def get_by_name(cls, name: str):
|
||||
return cls.objects.filter(name__startswith=name).first()
|
||||
|
||||
@classmethod
|
||||
def get_by_slug(cls, slug: str):
|
||||
return cls.objects.filter(slug__startswith=slug).first()
|
||||
return cls.objects.get(slug=slug)
|
||||
|
||||
|
||||
class Tag(CreatedModifiedMixin, models.Model):
|
||||
@ -537,12 +534,7 @@ class Version(CreatedModifiedMixin, RatingMixin, TrackChangesMixin, models.Model
|
||||
return
|
||||
|
||||
for permission_name in _permissions:
|
||||
permission = VersionPermission.get_by_name(permission_name)
|
||||
|
||||
# Just ignore versions that are incompatible.
|
||||
if not permission:
|
||||
continue
|
||||
|
||||
permission = VersionPermission.get_by_slug(permission_name)
|
||||
self.permissions.add(permission)
|
||||
|
||||
def set_initial_licenses(self, _licenses):
|
||||
|
@ -29,14 +29,27 @@ def _log_deletion(
|
||||
instance.record_deletion()
|
||||
|
||||
|
||||
def _delete_file(f, sender, instance, rel):
|
||||
source = f.source.name
|
||||
args = {'f_id': f.pk, 'h': f.hash, 'pk': instance.pk, 'sender': sender, 's': source, 'r': rel}
|
||||
logger.info('Deleting %(r)s file pk=%(f_id)s s=%(s)s hash=%(h)s of %(sender)s pk=%(pk)s', args)
|
||||
f.delete()
|
||||
|
||||
|
||||
@receiver(post_delete, sender=extensions.models.Preview)
|
||||
@receiver(post_delete, sender=extensions.models.Version)
|
||||
def _delete_file(sender: object, instance: object, **kwargs: object) -> None:
|
||||
def _delete_preview_or_version_file(sender: object, instance: object, **kwargs: object) -> None:
|
||||
f = instance.file
|
||||
args = {'f_id': f.pk, 'h': f.hash, 'pk': instance.pk, 'sender': sender, 's': f.source.name}
|
||||
logger.info('Deleting file pk=%(f_id)s s=%(s)s hash=%(h)s linked to %(sender)s pk=%(pk)s', args)
|
||||
f.delete()
|
||||
# TODO: this doesn't mean that the file was deleted from disk
|
||||
_delete_file(f, sender, instance, rel=sender)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=extensions.models.Extension)
|
||||
def _delete_featured_image_and_icon(sender: object, instance: object, **kwargs: object) -> None:
|
||||
for rel in ('featured_image', 'icon'):
|
||||
f = getattr(instance, rel)
|
||||
if not f:
|
||||
continue
|
||||
_delete_file(f, sender, instance, rel)
|
||||
|
||||
|
||||
@receiver(pre_save, sender=extensions.models.Extension)
|
||||
|
@ -18,7 +18,7 @@
|
||||
</div>
|
||||
{% endblock hero_breadcrumbs %}
|
||||
|
||||
<h1>{{ extension.name }}</h1>
|
||||
<h1>{% include "extensions/components/icon.html" %} {{ extension.name }}</h1>
|
||||
|
||||
<div class="hero-subtitle">
|
||||
{% if latest.tagline %}
|
||||
|
@ -4,10 +4,10 @@
|
||||
href="https://www.blender.org/download/releases/{{ version.blender_version_min|version_without_patch|replace:".,-" }}/"
|
||||
title="{{ version.blender_version_min }}">Blender {{ version.blender_version_min|version_without_patch }}</a>
|
||||
{% if is_editable %}
|
||||
—
|
||||
<input name="blender_version_max" class="form-control-sm"
|
||||
<span class="me-2">—</span>
|
||||
<input name="blender_version_max" class="form-control"
|
||||
value="{{version.blender_version_max|default_if_none:''}}"
|
||||
placeholder="{% trans 'maximum Blender version' %}"
|
||||
placeholder="{% trans 'max. Blender version' %}"
|
||||
pattern="^([0-9]+\.[0-9]+\.[0-9]+)?$"
|
||||
title="{% trans 'Blender version, e.g. 4.1.0' %}"
|
||||
/>
|
||||
|
@ -1,5 +1,7 @@
|
||||
{% load common filters %}
|
||||
{% with latest=extension.latest_version thumbnail_360p_url=extension.get_previews.0.thumbnail_360p_url %}
|
||||
{% load common filters static %}
|
||||
{% static "common/images/no-image_640x360.png" as featured_image_missing %}
|
||||
{% 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-content">
|
||||
<a href="{{ extension.get_absolute_url }}">
|
||||
|
@ -76,7 +76,9 @@
|
||||
<div class="dl-row">
|
||||
<div class="dl-col">
|
||||
<dt>{% trans 'Compatibility' %}</dt>
|
||||
<dd>{% include "extensions/components/blender_version.html" with version=version is_editable=is_editable form=form %}</dd>
|
||||
<dd class="align-items-center d-flex">
|
||||
{% include "extensions/components/blender_version.html" with version=version is_editable=is_editable form=form %}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -1,17 +1,17 @@
|
||||
{% with previews=extension.get_previews %}
|
||||
<section class="galleria-container" id="galleria-container">
|
||||
{% with preview_count=previews|length %}
|
||||
<section class="galleria-container" id="galleria-container">
|
||||
{% if previews %}
|
||||
<div class="galleria-items{% if previews.count > 5 %} is-many{% endif %}{% if previews.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 %}
|
||||
{% with thumbnail_1080p_url=preview.thumbnail_1080p_url %}
|
||||
{% with thumbnail_1080p_url=preview.file.thumbnail_1080p_url file=preview.file %}
|
||||
<a
|
||||
class="galleria-item js-galleria-item-preview galleria-item-type-{{ preview.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 }}"
|
||||
{% if 'video' in preview.content_type %}data-galleria-video-url="{{ preview.source.url }}"{% endif %}
|
||||
data-galleria-content-type="{{ preview.content_type }}"
|
||||
{% if 'video' in file.content_type %}data-galleria-video-url="{{ file.source.url }}"{% endif %}
|
||||
data-galleria-content-type="{{ file.content_type }}"
|
||||
data-galleria-index="{{ forloop.counter }}">
|
||||
|
||||
<img src="{{ thumbnail_1080p_url }}" alt="{{ preview.preview.caption }}">
|
||||
<img src="{{ thumbnail_1080p_url }}" alt="{{ preview.caption }}">
|
||||
</a>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
@ -26,5 +26,5 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
</section>
|
||||
{% endwith %}
|
||||
|
3
extensions/templates/extensions/components/icon.html
Normal file
@ -0,0 +1,3 @@
|
||||
{% 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 %}">
|
@ -14,14 +14,14 @@
|
||||
<div class="col-md-8 pt-2">
|
||||
{# Gallery #}
|
||||
{% block extension_galleria %}
|
||||
{% include "extensions/components/galleria.html" with extension=extension %}
|
||||
{% include "extensions/components/galleria.html" with extension=extension previews=extension.get_previews_listed %}
|
||||
{% endblock extension_galleria %}
|
||||
|
||||
{# Description #}
|
||||
{% block extension_description %}
|
||||
{% if extension.description %}
|
||||
<section id="about" class="mt-3">
|
||||
<div class="box ext-detail-description">
|
||||
<div class="box style-rich-text">
|
||||
{{ extension.description|markdown }}
|
||||
</div>
|
||||
</section>
|
||||
@ -42,7 +42,7 @@
|
||||
</span>
|
||||
</summary>
|
||||
|
||||
<div class="px-4">
|
||||
<div>
|
||||
{{ latest.release_notes|markdown }}
|
||||
</div>
|
||||
</details>
|
||||
@ -62,7 +62,7 @@
|
||||
|
||||
{# Permissions #}
|
||||
{% block extension_permissions %}
|
||||
{% if extension.type_slug == 'add-on' %}
|
||||
{% if extension.type_slug == 'add-ons' %}
|
||||
<hr class="my-4">
|
||||
<section id="permissions" class="ext-detail-permissions">
|
||||
<h2 class="mb-3">{% trans "Permissions" %}</h2>
|
||||
|
@ -45,7 +45,7 @@
|
||||
{# TODO: fix handling of tags #}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include "common/components/field.html" %}
|
||||
{% include "common/components/field.html" with placeholder="Enter the text here..." %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -55,7 +55,26 @@
|
||||
<section class="mt-4">
|
||||
<h2>{% trans 'Initial Version' %}</h2>
|
||||
<div class="card p-3">
|
||||
{% include "common/components/field.html" with field=form.release_notes %}
|
||||
{% include "common/components/field.html" with field=form.release_notes placeholder="Add the release notes..." %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-4">
|
||||
<h2>{% trans 'Featured image and icon' %}</h2>
|
||||
<div class="previews-upload">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% trans "Icon" as icon_label %}
|
||||
{% trans "A 256 x 256 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 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 %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
</div>
|
||||
<div class="details flex-grow-1">
|
||||
<div class="js-input-img-caption-helper mb-2">
|
||||
{% include "common/components/field.html" with field=inlineform.caption label='Caption' %}
|
||||
{% include "common/components/field.html" with field=inlineform.caption label='Caption' placeholder="Describe the preview" %}
|
||||
</div>
|
||||
<div class="align-items-center d-flex js-input-img-helper justify-content-between">
|
||||
{% include "common/components/field.html" with field=inlineform.source label='File' %}
|
||||
|
@ -0,0 +1,33 @@
|
||||
{% load common %}
|
||||
{# Handles displaying and editing the featured image #}
|
||||
{% with inlineform=image_form|add_form_classes %}
|
||||
{% with current_file=inlineform.instance.source %}
|
||||
<div class="{{ image_form.prefix }}-preview"
|
||||
style="background-image: url('{% if current_file %}{{ current_file.url }}{% endif %}');"
|
||||
title="{{ label }} of the extension">
|
||||
</div>
|
||||
{% for field in inlineform %}
|
||||
{% if field.name == "source" %}
|
||||
<small>
|
||||
{% include "common/components/field.html" with label=label help_text=help_text %}
|
||||
</small>
|
||||
{% else %}
|
||||
{% include "common/components/field.html" %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{{ inlineform.non_form_errors }}
|
||||
<script>
|
||||
(function() {
|
||||
const input = document.getElementById('id_{{ image_form.prefix }}-source');
|
||||
const previewEl = document.getElementsByClassName('{{ image_form.prefix }}-preview')[0];
|
||||
input.addEventListener('change', function() {
|
||||
const curFiles = input.files;
|
||||
if (curFiles.length > 0) {
|
||||
const dataUrl = URL.createObjectURL(curFiles[0]);
|
||||
previewEl.style['background-image'] = `url("${dataUrl}")`;
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endwith %}
|
||||
{% endwith %}
|
@ -30,11 +30,30 @@
|
||||
|
||||
<section class="card p-3">
|
||||
<div>
|
||||
{% include "common/components/field.html" with field=form.description label="Description" classes="one two three" placeholder="Describe this extension" %}
|
||||
{% include "common/components/field.html" with field=form.description label="Description" placeholder="Describe the extension..." %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{% include "common/components/field.html" with field=form.support %}
|
||||
{% include "common/components/field.html" with field=form.support placeholder="https://example.com" %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-4">
|
||||
<h2>{% trans 'Featured image and icon' %}</h2>
|
||||
<div class="previews-upload">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% trans "Icon" as icon_label %}
|
||||
{% trans "A 256 x 256 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 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 %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
@ -25,7 +25,7 @@
|
||||
<section class="card p-3">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include "common/components/field.html" with field=form.release_notes %}
|
||||
{% include "common/components/field.html" with field=form.release_notes placeholder="Add the release notes..." %}
|
||||
</div>
|
||||
</div>
|
||||
{% if form.non_field_errors or form.file.errors %}
|
||||
|
@ -65,11 +65,11 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col mx-4 mt-4">
|
||||
{% include "common/components/field.html" with field=form.agreed_with_terms %}
|
||||
{% include "common/components/field.html" with field=form.agreed_with_terms classes="js-agree-with-terms-input" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<button type="submit" class="btn btn-block btn-primary px-5 py-2">
|
||||
<button type="submit" class="btn btn-block btn-primary js-agree-with-terms-btn-submit px-5 py-2" disabled>
|
||||
<i class="i-upload"></i>
|
||||
<span>
|
||||
{% if extension %}
|
||||
@ -79,6 +79,27 @@
|
||||
{% endif %}
|
||||
</span>
|
||||
</button>
|
||||
<noscript>
|
||||
<div>
|
||||
<hr class="mt-5">
|
||||
<p>
|
||||
{% trans 'You see this, because of JavaScript is disabled in your browser.' %}
|
||||
</p>
|
||||
<button type="submit" class="btn btn-block btn-primary mb-2 px-5 py-2">
|
||||
<i class="i-upload"></i>
|
||||
<span>
|
||||
{% if extension %}
|
||||
{% trans 'Upload New Version' %}
|
||||
{% else %}
|
||||
{% trans 'Upload Extension' %}
|
||||
{% endif %}
|
||||
</span>
|
||||
</button>
|
||||
<p>
|
||||
{% trans 'By clicking the submit button, you agree to Blender Extensions conditions of use and policies.' %}
|
||||
</p>
|
||||
</div>
|
||||
</noscript>
|
||||
</div>
|
||||
|
||||
{% if form.non_field_errors %}
|
||||
|
@ -42,7 +42,7 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
<div class="px-4">
|
||||
<div>
|
||||
{% if version.release_notes %}
|
||||
<h3 class="mb-3">Changelog</h3>
|
||||
{{ version.release_notes|markdown }}
|
||||
|
BIN
extensions/tests/files/test_featured_image_0001.png
Normal file
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 152 KiB |
BIN
extensions/tests/files/test_icon_0001.png
Normal file
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
@ -108,6 +108,8 @@ class DeleteTest(TestCase):
|
||||
'description',
|
||||
'download_count',
|
||||
'extension_id',
|
||||
'featured_image',
|
||||
'icon',
|
||||
'is_listed',
|
||||
'name',
|
||||
'pk',
|
||||
|
@ -309,6 +309,9 @@ class SubmitFinaliseTest(TestCase):
|
||||
'extension_form': [{'description': ['This field is required.']}, None],
|
||||
'add_preview_formset': [[], ['Please add at least one preview.']],
|
||||
'edit_preview_formset': [[], []],
|
||||
'featured_image_form': [{'source': ['This field is required.']}, None],
|
||||
'icon_form': [{'source': ['This field is required.']}, None],
|
||||
'image_form': [{'source': ['This field is required.']}, None],
|
||||
},
|
||||
)
|
||||
|
||||
@ -349,12 +352,18 @@ class SubmitFinaliseTest(TestCase):
|
||||
}
|
||||
file_name1 = 'test_preview_image_0001.png'
|
||||
file_name2 = 'test_preview_image_0002.png'
|
||||
file_name3 = 'test_icon_0001.png'
|
||||
file_name4 = 'test_featured_image_0001.png'
|
||||
with open(TEST_FILES_DIR / file_name1, 'rb') as fp1, open(
|
||||
TEST_FILES_DIR / file_name2, 'rb'
|
||||
) as fp2:
|
||||
) as fp2, open(TEST_FILES_DIR / file_name3, 'rb') as fp3, open(
|
||||
TEST_FILES_DIR / file_name4, 'rb'
|
||||
) as fp4:
|
||||
files = {
|
||||
'form-0-source': fp1,
|
||||
'form-1-source': fp2,
|
||||
'icon-source': fp3,
|
||||
'featured-image-source': fp4,
|
||||
}
|
||||
response = self.client.post(self.file.get_submit_url(), {**data, **files})
|
||||
|
||||
@ -363,7 +372,7 @@ class SubmitFinaliseTest(TestCase):
|
||||
self.assertEqual(File.objects.filter(type=File.TYPES.BPY).count(), 1)
|
||||
self.assertEqual(Extension.objects.count(), 1)
|
||||
self.assertEqual(Version.objects.count(), 1)
|
||||
self.assertEqual(File.objects.filter(type=File.TYPES.IMAGE).count(), 2)
|
||||
self.assertEqual(File.objects.filter(type=File.TYPES.IMAGE).count(), 4)
|
||||
# Check an add-on was created with all given fields
|
||||
extension = Extension.objects.first()
|
||||
self.assertEqual(extension.get_type_display(), 'Add-on')
|
||||
|
@ -212,10 +212,7 @@ class UpdateTest(TestCase):
|
||||
[
|
||||
{},
|
||||
{'__all__': ['Please correct the duplicate values below.']},
|
||||
[
|
||||
'Please select another file instead of the duplicate',
|
||||
'Please select another file instead of the duplicate',
|
||||
],
|
||||
['Please select another file instead of the duplicate'],
|
||||
],
|
||||
)
|
||||
|
||||
@ -243,7 +240,7 @@ class UpdateTest(TestCase):
|
||||
self.maxDiff = None
|
||||
self.assertEqual(
|
||||
response.context['add_preview_formset'].forms[0].errors,
|
||||
{'source': ['File with this Hash already exists.']},
|
||||
{'source': ['File with this Original hash already exists.']},
|
||||
)
|
||||
|
||||
def test_post_upload_validation_error_unexpected_preview_format_gif(self):
|
||||
|
@ -16,8 +16,8 @@ from .mixins import (
|
||||
from extensions.forms import (
|
||||
ExtensionDeleteForm,
|
||||
ExtensionUpdateForm,
|
||||
VersionForm,
|
||||
VersionDeleteForm,
|
||||
VersionForm,
|
||||
)
|
||||
from extensions.models import Extension, Version
|
||||
from files.forms import FileForm
|
||||
@ -39,9 +39,12 @@ class ExtensionDetailView(ExtensionQuerysetMixin, DetailView):
|
||||
"""
|
||||
return self.get_extension_queryset().prefetch_related(
|
||||
'authors',
|
||||
'ratings',
|
||||
'ratings__user',
|
||||
'versions',
|
||||
'versions__file',
|
||||
'versions__file__validation',
|
||||
'versions__permissions',
|
||||
)
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
@ -126,6 +129,8 @@ class UpdateExtensionView(
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context['edit_preview_formset'] = context['form'].edit_preview_formset
|
||||
context['add_preview_formset'] = context['form'].add_preview_formset
|
||||
context['featured_image_form'] = context['form'].featured_image_form
|
||||
context['icon_form'] = context['form'].icon_form
|
||||
return context
|
||||
|
||||
@transaction.atomic
|
||||
@ -348,6 +353,8 @@ class DraftExtensionView(
|
||||
context['extension_form'] = extension_form
|
||||
context['edit_preview_formset'] = extension_form.edit_preview_formset
|
||||
context['add_preview_formset'] = extension_form.add_preview_formset
|
||||
context['featured_image_form'] = extension_form.featured_image_form
|
||||
context['icon_form'] = extension_form.icon_form
|
||||
return context
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
@ -6,6 +6,7 @@ import tempfile
|
||||
from django import forms
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import django.core.exceptions
|
||||
|
||||
from .validators import (
|
||||
ExtensionIDManifestValidator,
|
||||
@ -52,9 +53,7 @@ class FileForm(forms.ModelForm):
|
||||
message=error_messages['invalid_zip_archive'],
|
||||
),
|
||||
],
|
||||
widget=forms.ClearableFileInput(
|
||||
attrs={'accept': ','.join(ALLOWED_EXTENSION_MIMETYPES)}
|
||||
),
|
||||
widget=forms.ClearableFileInput(attrs={'accept': ','.join(ALLOWED_EXTENSION_MIMETYPES)}),
|
||||
help_text=msg_only_zip_files,
|
||||
)
|
||||
agreed_with_terms = forms.BooleanField(
|
||||
@ -154,3 +153,68 @@ class FileForm(forms.ModelForm):
|
||||
self.cleaned_data['type'] = EXTENSION_SLUG_TYPES[manifest['type']]
|
||||
|
||||
return self.cleaned_data
|
||||
|
||||
|
||||
class BaseMediaFileForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = files.models.File
|
||||
fields = ('source', 'original_hash')
|
||||
widgets = {'original_hash': forms.HiddenInput()}
|
||||
|
||||
source = forms.ImageField(widget=forms.FileInput)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.request = kwargs.pop('request')
|
||||
self.extension = kwargs.pop('extension')
|
||||
# Set current File so that the form actually displays it:
|
||||
if hasattr(self, 'to_field'):
|
||||
kwargs['instance'] = getattr(self.extension, getattr(self, 'to_field'))
|
||||
|
||||
source_field = self.base_fields['source']
|
||||
|
||||
# File might not be required depending on the context (saving draft vs sending to review)
|
||||
source_field.required = False
|
||||
|
||||
accept = ','.join(self.allowed_mimetypes)
|
||||
source_field.widget.attrs.update({'accept': accept})
|
||||
|
||||
# Replace ImageField's file extension validator with one that also check file's content
|
||||
source_field.validators = [
|
||||
FileMIMETypeValidator(
|
||||
allowed_mimetypes=self.allowed_mimetypes,
|
||||
message=self.error_messages['invalid_mimetype'],
|
||||
)
|
||||
]
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.instance.user = self.request.user
|
||||
|
||||
def clean_original_hash(self, *args, **kwargs):
|
||||
"""Calculate original hash of the uploaded file."""
|
||||
source = self.cleaned_data.get('source')
|
||||
if not source:
|
||||
return
|
||||
return files.models.File.generate_hash(source)
|
||||
|
||||
def add_error(self, field, error):
|
||||
"""Add hidden `original_hash` errors to the visible `source` field instead."""
|
||||
if isinstance(error, django.core.exceptions.ValidationError):
|
||||
if getattr(error, 'error_dict', None):
|
||||
hash_error = error.error_dict.pop('original_hash', None)
|
||||
if hash_error:
|
||||
error.error_dict['source'] = hash_error
|
||||
super(forms.ModelForm, self).add_error(field, error)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Save as `to_field` on the parent object (Extension)."""
|
||||
source = self.cleaned_data['source']
|
||||
self.instance.hash = self.instance.original_hash
|
||||
self.instance.original_name = source.name
|
||||
self.instance.size_bytes = source.size
|
||||
instance = super().save(*args, **kwargs)
|
||||
|
||||
if hasattr(self, 'to_field'):
|
||||
to_field = self.to_field
|
||||
setattr(self.extension, to_field, instance)
|
||||
return instance
|
||||
|
@ -144,8 +144,9 @@ class File(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
||||
self.full_clean()
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def is_listed(self):
|
||||
return self.status == self.model.STATUSES.APPROVED
|
||||
@property
|
||||
def is_listed(self) -> bool:
|
||||
return self.status == self.STATUSES.APPROVED
|
||||
|
||||
@property
|
||||
def is_image(self) -> bool:
|
||||
|
@ -1,6 +1,6 @@
|
||||
{% load common i18n %}
|
||||
{# FIXME: we might want to rephrase is_moderator in terms of Django's (group) permissions #}
|
||||
{% if perms.files.view_file or request.user.is_moderator %}
|
||||
{% if perms.files.view_file or user_is_moderator %}
|
||||
{% if suspicious_files %}
|
||||
<section>
|
||||
<div class="card pb-3 pt-4 px-4 mb-3 ext-detail-download-danger">
|
||||
|
@ -1,6 +1,6 @@
|
||||
{% load common i18n %}
|
||||
{# FIXME: we might want to rephrase is_moderator in terms of Django's (group) permissions #}
|
||||
{% if perms.files.view_file or request.user.is_moderator %}
|
||||
{% if perms.files.view_file or user_is_moderator %}
|
||||
{% if suspicious_files %}
|
||||
{% blocktrans asvar alert_text %}Scan of the {{ suspicious_files.0 }} indicates malicious content.{% endblocktrans %}
|
||||
<b class="text-danger pt-2" title="{{ alert_text }}">⚠</b>
|
||||
|
@ -54,8 +54,7 @@ class FileMIMETypeValidator:
|
||||
|
||||
|
||||
class ExtensionIDManifestValidator:
|
||||
"""
|
||||
Make sure the extension id is valid:
|
||||
"""Make sure the extension id is valid:
|
||||
* Extension id consists of Unicode letters, numbers or underscores.
|
||||
* Neither hyphens nor spaces are supported.
|
||||
* Each extension id most be unique across all extensions.
|
||||
@ -307,8 +306,9 @@ class PermissionsValidator:
|
||||
is_error = True
|
||||
else:
|
||||
for permission in value:
|
||||
if VersionPermission.get_by_slug(permission):
|
||||
continue
|
||||
try:
|
||||
VersionPermission.get_by_slug(permission)
|
||||
except VersionPermission.DoesNotExist:
|
||||
is_error = True
|
||||
logger.info(f'Permission unavailable: {permission}')
|
||||
|
||||
|
@ -4,45 +4,64 @@
|
||||
{% block page_title %}{% blocktranslate %}Notifications{% endblocktranslate %}{% endblock page_title %}
|
||||
|
||||
{% block content %}
|
||||
<h1>
|
||||
{% trans 'Notifications' %}
|
||||
<h1>{% trans 'Notifications' %}</h1>
|
||||
|
||||
|
||||
{% if notification_list %}
|
||||
<div class="notifications">
|
||||
{% if user|unread_notification_count %}
|
||||
<form class="d-inline" action="{% url 'notifications:notifications-mark-read-all' %}" method="post">
|
||||
<form action="{% url 'notifications:notifications-mark-read-all' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-sm" type="submit">{% trans 'Mark all as read' %}</button>
|
||||
<button class="btn mb-3" type="submit"><i class="i-eye"></i> {% trans 'Mark All as Read' %}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</h1>
|
||||
{% if notification_list %}
|
||||
<div class="box">
|
||||
<table class="notifications-list">
|
||||
<tbody>
|
||||
{% for notification in notification_list %}
|
||||
<div class="row mb-2 {% if notification.read_at%}text-muted{% endif %}">
|
||||
<div class="col">
|
||||
|
||||
<tr class="notifications-item {% if notification.read_at%}is-read{% endif %}">
|
||||
<td class="notifications-item-time">
|
||||
{{ notification.action.timestamp | naturaltime_compact }}
|
||||
|
||||
<a href="{% url 'extensions:by-author' user_id=notification.action.actor.pk %}">
|
||||
{{ notification.action.actor }}
|
||||
</a>
|
||||
|
||||
{{ notification.action.verb }}
|
||||
|
||||
<a href="{{ notification.action.target.get_absolute_url }}">{{ notification.action.target }}</a>
|
||||
|
||||
<a href="{{ notification.get_absolute_url }}"><button class="btn btn-sm">{% trans 'View' %}</button></a>
|
||||
|
||||
{% if not notification.read_at %}
|
||||
<form class="d-inline" action="{% url 'notifications:notifications-mark-read' pk=notification.pk %}" method="post">
|
||||
</td>
|
||||
<td class="notifications-item-content">
|
||||
{# TODO: @back-end add link to action target ID (so that link works as an anchor link) #}
|
||||
<a href="{{ notification.get_absolute_url }}"><span class="me-2">{{ notification.action.actor }} {{ notification.action.verb }} {{ notification.action.target }}</span><span class="notifications-item-dot"></span></a>
|
||||
</td>
|
||||
<td class="notifications-item-nav">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-link dropdown-toggle js-dropdown-toggle active" data-toggle-menu-id="js-notifications-item-nav-{{ notification.id }}">
|
||||
<i class="i-more-vertical"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right js-dropdown-menu" id="js-notifications-item-nav-{{ notification.id }}">
|
||||
<li class="nav-item-mark-as-read">
|
||||
<form action="{% url 'notifications:notifications-mark-read' pk=notification.pk %}" method="post">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-sm" type="submit">{% trans 'Mark as read' %}</button>
|
||||
<button class="dropdown-item" title="Mark as Read" type="submit"><i class="i-eye"></i> Mark as Read </button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</li>
|
||||
{# TODO: add feature 'Mark as Unread' (optional) #}
|
||||
{% comment %}
|
||||
<li class="nav-item-mark-as-unread">
|
||||
<form>
|
||||
<button class="dropdown-item" title="Mark as Unread" type="submit"><i class="i-eye-off"></i> Mark as Unread </button>
|
||||
</form>
|
||||
</li>
|
||||
{% endcomment %}
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'extensions:by-author' user_id=notification.action.actor.pk %}"><i class="i-user"></i> View User</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>
|
||||
{% trans 'You have no notifications' %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endblock content %}
|
||||
|
@ -14,7 +14,7 @@
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="box p-3">
|
||||
{% include "common/components/field.html" with field=form.text focus=True %}
|
||||
{% include "common/components/field.html" with field=form.text focus=True placeholder="Enter the text here..." %}
|
||||
|
||||
{% if form.non_field_errors %}
|
||||
<div class="invalid-feedback">
|
||||
|
@ -1,5 +1,5 @@
|
||||
{% extends "extensions/detail.html" %}
|
||||
{% load common extensions filters i18n humanize %}
|
||||
{% load common extensions filters i18n humanize static %}
|
||||
|
||||
{% block page_title %}Review: {{ extension.name }}{% endblock page_title %}
|
||||
|
||||
@ -69,7 +69,26 @@
|
||||
|
||||
|
||||
{% block extension_galleria %}
|
||||
{% include "extensions/components/galleria.html" with extension=extension %}
|
||||
|
||||
{% include "extensions/components/galleria.html" with extension=extension previews=extension.get_previews %}
|
||||
|
||||
{% static "common/images/no-image.png" as featured_image_missing %}
|
||||
{% trans "Featured image" as featured_image_title %}
|
||||
{% with featured_image=extension.featured_image %}{% with has_featured_image=featured_image.source.name %}
|
||||
<div class="card p-2 mt-2" style="width: 18rem;" title="{{ featured_image_title }}">
|
||||
<a
|
||||
{% if has_featured_image %}
|
||||
href="{{ featured_image.source.url }}"
|
||||
target="_blank"
|
||||
{% endif %}
|
||||
>
|
||||
<img class="card-img-top rounded" src="{% if has_featured_image %}{{ featured_image.source.url }}{% else %}{{ featured_image_missing }}{% endif %}" alt="{{ featured_image_title }}">
|
||||
</a>
|
||||
<div class="card-body">
|
||||
<small class="card-text">{{ featured_image_title}}</small>
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}{% endwith %}
|
||||
{% endblock extension_galleria %}
|
||||
|
||||
|
||||
@ -145,7 +164,7 @@
|
||||
{% csrf_token %}
|
||||
{% with form=comment_form|add_form_classes %}
|
||||
|
||||
{% include "common/components/field.html" with field=form.message %}
|
||||
{% include "common/components/field.html" with field=form.message placeholder="Enter the text here..." %}
|
||||
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="btn-row ms-3 w-100 justify-content-end">
|
||||
|
@ -80,7 +80,8 @@ class ExtensionsApprovalDetailView(DetailView):
|
||||
def get_queryset(self):
|
||||
return self.model.objects.prefetch_related(
|
||||
'authors',
|
||||
'previews',
|
||||
'preview_set',
|
||||
'preview_set__file',
|
||||
'versions',
|
||||
).all()
|
||||
|
||||
|
@ -4,29 +4,34 @@
|
||||
<h1 class="mb-3">Teams</h1>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="row border-bottom mb-2 pb-2">
|
||||
<div class="col">Team name</div>
|
||||
<div class="col">Role</div>
|
||||
<div class="col"></div>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-100">
|
||||
Team name
|
||||
</th>
|
||||
<th>
|
||||
Role
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for team_member in user.team_users.all %}
|
||||
{% with team=team_member.team %}
|
||||
<div class="row">
|
||||
<div class="col">{{ team.name }}</div>
|
||||
<div class="col">{{ team_member.get_role_display }}</div>
|
||||
<div class="col">
|
||||
{% comment %}
|
||||
{% if team_member.is_manager %}
|
||||
<a href="{{ team.get_manage_url }}">Manage</a>{# TODO: add team manage page #}
|
||||
{% else %}
|
||||
<a href="{{ team.get_absolute_url }}">View</a>
|
||||
{% endif %}
|
||||
{% endcomment %}
|
||||
<a href="{{ team.get_absolute_url }}">View</a>
|
||||
</div>
|
||||
<tr>
|
||||
<td>
|
||||
<a class="px-0" href="{{ team.get_absolute_url }}">{{ team.name }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<div class="badge">
|
||||
{{ team_member.get_role_display }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock settings %}
|
||||
|