Extensions list: sort_by parameter #159

Merged
Márton Lente merged 36 commits from filter-sort into main 2024-06-03 12:57:45 +02:00
33 changed files with 178 additions and 131 deletions
Showing only changes of commit 9182ed7543 - Show all commits

View File

@ -0,0 +1,12 @@
name: Bug Report
about: Report issues for the Extensions Platform website.
labels:
- "Type/Report"
- "Status/Needs Triage"
- "Priority/Normal"
body:
- type: textarea
id: body
attributes:
label: "Description"
hide_label: true

View File

@ -0,0 +1,34 @@
name: Team Management
about: Form to request creation of a team, or joining existing ones.
labels:
- "Type/Team"
body:
- type: markdown
attributes:
value: |
### Instructions
This form is to create, delete, or join a team.
You can leave teams by yourself from the [Teams page](https://extensions.blender.org/settings/teams/) on your profile.
- type: textarea
id: body
attributes:
label: "Description"
hide_label: true
value: |
**Team Management Request**
Mark the type of request you have:
- [ ] Create a team
- [ ] Join a team
- [ ] Add a member to my team
- [ ] Remove a member from my team
- [ ] Delete a team
**Team Information**
Name:
URL: e.g. https://extensions.blender.org/team/community/
**User Details**
Name of the user to add/remove:
Profile URL:

1
.gitignore vendored
View File

@ -68,6 +68,7 @@ wheels/
*.egg
*.manifest
*.spec
*.DS_Store
# Installer logs
pip-log.txt

View File

@ -1,4 +1,5 @@
{% extends 'users/settings/base.html' %}
{% block page_title %}Tokens{% endblock %}
{% block settings %}
<h1 class="mb-3">Tokens</h1>

View File

@ -31,9 +31,6 @@ a.badge-tag
border-color: var(--color-text)
color: var(--color-text)
.badge-tag
font-size: var(--fs-xs)
.badge-status
&-approved,
&-resolved

View File

@ -369,9 +369,11 @@
+padding(3, x)
+padding(0, y)
transition: background-color var(--transition-speed-fast)
vertical-align: baseline
a
a:not(.btn)
color: var(--color-text)
display: inline-block
+padding(1, y)
padding-inline: 0 !important
@ -390,6 +392,10 @@
.ext-review-list-activity
width: 1%
.ext-review-list-status a
vertical-align: text-top
width: 100%
.rating-form
select
color: var(--color-warning)

View File

@ -102,7 +102,7 @@
/* Lightbox component. */
.galleria
--galleria-btn-width: 100px
--galleria-btn-width: 12.8rem
--galleria-media-max-width: 100%
+media-lg
@ -148,14 +148,18 @@
&.btn-close
font-size: 3.2rem
height: 20vh
max-height: 80px
max-width: 80px
max-height: 8.0rem
max-width: var(--galleria-btn-width)
position: absolute
right: 0
top: 0
width: 80px
width: var(--galleria-btn-width)
z-index: 2
&.btn-close,
&.btn-next,
text-align: right
&.btn-next,
&.btn-prev
top: 50%
@ -166,6 +170,12 @@
&.btn-prev
left: 0
text-align: left
[class^="i-"]::before,
[class*=" i-"]::before
margin-left: 0
margin-right: 0
.underlay
background-color: rgba(0,0,0,0.9)

View File

@ -186,7 +186,7 @@
<li class="dropdown-divider"></li>
<li>
<a href="https://projects.blender.org/infrastructure/extensions-website/issues" class="dropdown-item">
<a href="https://projects.blender.org/infrastructure/extensions-website/issues/new?template=.gitea/issue_template/bug.yaml" class="dropdown-item" target="_blank">
<i class="i-alert-triangle"></i> {% trans 'Report Problem' %}
</a>
</li>

View File

@ -656,10 +656,9 @@ class Version(CreatedModifiedMixin, RatingMixin, TrackChangesMixin, models.Model
# TODO: actual signed URLs?
return self.file.source.url
@property
def download_url(self) -> str:
def download_url(self, append_repository=True) -> str:
filename = f'{self.extension.type_slug_singular}-{self.extension.slug}-v{self.version}.zip'
return reverse(
download_url = reverse(
'extensions:version-download',
kwargs={
'type_slug': self.extension.type_slug,
@ -668,6 +667,9 @@ class Version(CreatedModifiedMixin, RatingMixin, TrackChangesMixin, models.Model
'filename': filename,
},
)
if append_repository:
download_url += '?repository=/api/v1/extensions/'
return download_url
def get_delete_url(self) -> str:
return reverse(

View File

@ -69,9 +69,11 @@
{% trans "Reviews" %}
</a>
{% endif %}
{% if extension.latest_version != None %}
<a href="{{ extension.get_versions_url }}" class="{% if '/versions/' in request.get_full_path %}is-active{% endif %}">
{% trans "Version History" %}
</a>
{% endif %}
</div>
<div class="btn-row hero-tabs-admin mb-3 mb-md-0">
{% if is_maintainer %}

View File

@ -1,6 +1,6 @@
{% load extensions %}
<a href="{% url "extensions:by-tag" tag_slug=tag.slug %}" title="{{ tag.name }}"
class="badge badge-tag{% if not small %} badge-lg{% endif %} {{ class }}">
class="badge badge-tag {{ classes }}">
{{ tag.name }}
</a>

View File

@ -66,7 +66,7 @@
<ul class="flex-wrap">
{% for tag in latest.tags.all %}
<li class="mb-1">
{% include "extensions/components/badge_tag.html" with small=True version=latest %}
{% include "extensions/components/badge_tag.html" with version=latest classes="badge-sm" %}
</li>
{% endfor %}
</ul>

View File

@ -1,9 +1,9 @@
<div class="ext-detail-permissions">
<dt>Permissions</dt>
{% if version.permissions.all %}
{% if permissions %}
<dd>This version requests the following:
<ul>
{% for p in version.permissions.all %}
{% for p in permissions %}
<li>
<div>
<i class="i-permission-{{ p.slug }}"></i>

View File

@ -104,14 +104,14 @@
<div class="dl-row">
<div class="dl-col">
{% include "extensions/components/detail_card_version_permissions.html" with version=version %}
{% include "extensions/components/detail_card_version_permissions.html" with permissions=version.permissions.all %}
</div>
</div>
<div class="dl-row">
<dd class="ext-detail-info-tags">
{% if version.tags.count %}
{% include "extensions/components/tags.html" with small=True version=version %}
{% include "extensions/components/tags.html" with version=version %}
{% else %}
No tags.
{% endif %}

View File

@ -66,11 +66,12 @@
<hr class="my-4">
<section id="permissions" class="ext-detail-permissions">
<h2 class="mb-3">{% trans "Permissions" %}</h2>
{% if latest.permissions.all %}
{% with permissions=latest.permissions.all %}
{% if permissions %}
<div class="box">
<p>This extension requests the following permission{{ latest.permissions.all|pluralize }}:</p>
<p>This extension requests the following permission{{ permissions|pluralize }}:</p>
<ul>
{% for p in latest.permissions.all %}
{% for p in permissions %}
<li>
<div>
<i class="i-permission-{{ p.slug }}"></i>
@ -86,6 +87,7 @@
{% else %}
<p class="mb-0"><i class="i-check me-0"></i> This extension does not require special permissions.</p>
{% endif %}
{% endwith %}
</section>
{% endif %}
{% endblock extension_permissions %}
@ -249,7 +251,9 @@
</button>
</div>
<small class="d-block text-center w-100">
or manually <a class="text-muted text-underline" href="{{ request.scheme }}://{{ request.get_host }}{{ latest.download_url }}" download="{{ latest.download_name }}">download</a> and install it.
{# TODO @front-end: Replace URL of the manual /dev/ with /latest/. #}
...or <a class="text-underline text-primary" href="{{ request.scheme }}://{{ request.get_host }}{{ latest.download_url }}" download="{{ latest.download_name }}">download</a>
and <a class="text-underline text-primary" href="https://docs.blender.org/manual/en/dev/editors/preferences/extensions.html#install" target="_blank">Install from Disk</a>
</small>
</div>
{% else %}

View File

@ -1,5 +1,5 @@
{% extends "common/base.html" %}
{% load cache i18n %}
{% load i18n %}
{% block page_title %}Extensions{% endblock page_title %}
@ -32,7 +32,6 @@
{% endblock hero %}
{% block content %}
{% cache 60 home %}
<section class="mt-3">
<div class="d-flex">
<h2>
@ -78,5 +77,4 @@
</a>
</div>
</section>
{% endcache %}
{% endblock content %}

View File

@ -20,6 +20,18 @@
const input = document.getElementById('id_{{ image_form.prefix }}-source');
const previewEl = document.getElementsByClassName('{{ image_form.prefix }}-preview')[0];
const previewElIcon = previewEl.querySelector('.js-i-image');
const previewElComputedStyle = window.getComputedStyle(previewEl);
const previewElBgImg = previewElComputedStyle.getPropertyValue('background-image');
function hidePreviewElIcon() {
previewElIcon.classList.add('d-none');
}
// Check if previewElBgImg is set
if (previewElBgImg.length > 10) {
// Hide previewElIcon
hidePreviewElIcon();
}
input.addEventListener('change', function() {
const curFiles = input.files;
@ -27,7 +39,8 @@
const dataUrl = URL.createObjectURL(curFiles[0]);
previewEl.style['background-image'] = `url("${dataUrl}")`;
previewElIcon.classList.add('d-none');
hidePreviewElIcon();
}
});

View File

@ -74,7 +74,7 @@
</div>
<div class="dl-row">
<div class="dl-col">
{% include "extensions/components/detail_card_version_permissions.html" with version=version %}
{% include "extensions/components/detail_card_version_permissions.html" with permissions=version.permissions.all %}
</div>
</div>
<div class="dl-row">

View File

@ -66,7 +66,7 @@ class VersionUploadAPITest(APITestCase):
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(
response.data['message'],
f'Extension "{other_extension.extension_id}" not maintained by user "{self.user.full_name}"',
f'Extension "{other_extension.extension_id}" not maintained by user "{self.user.username}"',
)
def test_version_upload_extension_does_not_exist(self):

View File

@ -122,12 +122,20 @@ class ApiViewsTest(_BaseTestCase):
create_approved_version()
url = reverse('extensions:api')
# returns 1st and 3rd items
json = self.client.get(
url + '?platform=windows-amd64',
HTTP_ACCEPT='application/json',
).json()
self.assertEqual(len(json['data']), 2)
# only returns the 3rd item
json = self.client.get(
url + '?platform=platform-we-dont-know',
HTTP_ACCEPT='application/json',
).json()
self.assertEqual(len(json['data']), 1)
def test_blender_version_filter_latest_not_max_version(self):
version = create_approved_version(blender_version_min='4.0.1')
version.date_created

View File

@ -27,14 +27,14 @@ class ListedExtensionsSerializer(serializers.ModelSerializer):
error_messages = {
"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:
model = Extension
fields = ()
UNKNOWN_PLATFORM = 'unknown-platform-value'
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
self.blender_version = kwargs.pop('blender_version', None)
@ -53,7 +53,7 @@ class ListedExtensionsSerializer(serializers.ModelSerializer):
try:
Platform.objects.get(slug=self.platform)
except Platform.DoesNotExist:
self.fail('invalid_platform')
self.platform = self.UNKNOWN_PLATFORM
def to_representation(self, instance):
matching_version = None
@ -75,7 +75,8 @@ class ListedExtensionsSerializer(serializers.ModelSerializer):
continue
platform_slugs = set(p.slug for p in v.platforms.all())
# empty platforms field matches any platform filter
if self.platform and not (not platform_slugs or self.platform in platform_slugs):
# UNKNOWN_PLATFORM matches only empty platforms field
if self.platform and (platform_slugs and self.platform not in platform_slugs):
continue
matching_version = v
break
@ -91,7 +92,9 @@ class ListedExtensionsSerializer(serializers.ModelSerializer):
'tagline': matching_version.tagline,
'archive_hash': matching_version.file.original_hash,
'archive_size': matching_version.file.size_bytes,
'archive_url': self.request.build_absolute_uri(matching_version.download_url),
'archive_url': self.request.build_absolute_uri(
matching_version.download_url(append_repository=False)
),
'type': EXTENSION_TYPE_SLUGS_SINGULAR.get(instance.type),
'blender_version_min': matching_version.blender_version_min,
'blender_version_max': matching_version.blender_version_max,

View File

@ -5,8 +5,6 @@ from django.contrib import admin
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.safestring import mark_safe
import background_task.admin
import background_task.models
from .models import File, FileValidation
import files.signals
@ -175,48 +173,3 @@ class FileAdmin(admin.ModelAdmin):
return obj.validation.is_ok if hasattr(obj, 'validation') else None
is_ok.boolean = True
try:
admin.site.unregister(background_task.models.Task)
admin.site.unregister(background_task.models.CompletedTask)
except admin.site.NotRegistered:
pass
class TaskMixin:
"""Modify a few properties of background tasks displayed in admin."""
def no_errors(self, obj):
"""Replace background_task's "has_error".
Make Django's red/green boolean icons less confusing
in the context of "there's an error during task run".
"""
return not bool(obj.last_error)
no_errors.boolean = True
@admin.register(background_task.models.Task)
@admin.register(background_task.models.CompletedTask)
class TaskAdmin(background_task.admin.TaskAdmin, TaskMixin):
date_hierarchy = 'run_at'
list_display = [
'run_at',
'task_name',
'task_params',
'attempts',
'no_errors',
'locked_by',
'locked_by_pid_running',
]
list_filter = (
'task_name',
'run_at',
'failed_at',
'locked_at',
'attempts',
'creator_content_type',
)
search_fields = ['task_name', 'task_params', 'last_error', 'verbose_name']

View File

@ -228,7 +228,7 @@ class TestTasks(TestCase):
expected_abuse_report_text = """Add-on reported: "{extension.name}"
{some_user.full_name} reported Add-on "{extension.name}"
{some_user.username} reported Add-on "{extension.name}"
:
test message
@ -243,7 +243,7 @@ https://extensions.local:8111/
"""
expected_new_comment_text = """New comment on Add-on "{extension.name}"
{some_user.full_name} commented on Add-on "{extension.name}"
{some_user.username} commented on Add-on "{extension.name}"
:
this is bad
@ -258,7 +258,7 @@ https://extensions.local:8111/
"""
expected_rated_text = """Add-on rated: "{extension.name}"
{some_user.full_name} rated extension Add-on "{extension.name}"
{some_user.username} rated extension Add-on "{extension.name}"
:
rating text
@ -273,7 +273,7 @@ https://extensions.local:8111/
"""
expected_review_requested_text = """Add-on review requested: "Edit Breakdown"
{some_user.full_name} requested review of Add-on "Edit Breakdown"
{some_user.username} requested review of Add-on "Edit Breakdown"
:
Extension is ready for initial review

View File

@ -16,7 +16,12 @@ class NotificationsView(LoginRequiredMixin, ListView):
paginate_by = 20
def get_queryset(self):
return Notification.objects.filter(recipient=self.request.user).order_by('-id')
return (
Notification.objects.filter(recipient=self.request.user)
.select_related('action')
.prefetch_related('action__action_object', 'action__actor', 'action__target')
.order_by('-id')
)
class MarkReadAllView(LoginRequiredMixin, FormView):

View File

@ -15,7 +15,7 @@ Django==4.2.11
dj-database-url==1.0.0
django-activity-stream==2.0.0
django-admin-rangefilter==0.8.5
django-background-tasks-updated @ git+https://projects.blender.org/infrastructure/django-background-tasks.git@2e60c4ec2fd1e7155bc3f041e0ea4875495a476b
django-background-tasks-updated @ git+https://projects.blender.org/infrastructure/django-background-tasks.git@2cbe547fcf183354c80dfd848537f55fc05bf1d5
django-compat==1.0.15
django-extended-choices==1.3.3
django-loginas==0.3.10

View File

@ -12,11 +12,13 @@
</div>
</td>
<td>
<div class="d-flex flex-wrap">
{% include "extensions/components/authors.html" %}
{% if extension.team %}
<a class="text-secondary" href="{{ extension.team.get_absolute_url }}">({{ extension.team.name }})</a>
&nbsp;<a class="text-secondary" href="{{ extension.team.get_absolute_url }}">({{ extension.team.name }})</a>
{% endif %}
</div>
</td>
<td title="{{ extension.date_created }}">{{ extension.date_created|naturaltime_compact }}</td>
<td class="ext-review-list-activity" colspan="2">
@ -30,7 +32,7 @@
<span class="badge">{{ stats.count }}</span>
</a>
</td>
<td>
<td class="ext-review-list-status">
<a href="{{ extension.get_review_url }}" class="text-decoration-none">
{% with last_type=stats.last_type_display|default:"Awaiting Review" %}
{% include "common/components/status.html" with label=last_type slug=last_type|slugify object=extension classes="d-block" icon=True %}

View File

@ -7,7 +7,7 @@
<div class="hero-breadcrumbs">
<a href="{% url 'reviewers:approval-queue' %}">
<i class="i-chevron-left"></i>
<span>{% trans 'All in Queue' %}</span>
<span>{% trans 'All in Approval Queue' %}</span>
</a>
</div>
{% endblock hero_breadcrumbs %}
@ -21,9 +21,11 @@
<a href="#activity">
{% trans "Activity" %}
</a>
{% if extension.latest_version != None %}
<a href="{{ extension.get_versions_url }}" class="{% if '/versions/' in request.get_full_path %}is-active{% endif %}">
{% trans "Version History" %}
</a>
{% endif %}
</div>
<div class="btn-row hero-tabs-admin mb-3 mb-md-0">
{% if is_maintainer %}

View File

@ -23,20 +23,17 @@ STATUS_CHANGE_TYPES = [
class ApprovalQueueView(ListView):
model = Extension
paginate_by = 100
paginate_by = 50
def get_queryset(self):
qs = (
ApprovalActivity.objects.prefetch_related(
qs = ApprovalActivity.objects.prefetch_related(
'extension',
'extension__authors',
'extension__icon',
'extension__versions',
'extension__versions__file',
'extension__versions__file__validation',
)
.order_by('-date_created')
.all()
)
).order_by('-id')
by_extension = {}
by_date_created = []
for item in qs:
@ -88,7 +85,10 @@ class ExtensionsApprovalDetailView(DetailView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['review_activity'] = (
self.object.review_activity.select_related('user').order_by('date_created').all()
self.object.review_activity.select_related('user')
.prefetch_related('user__groups')
.order_by('date_created')
.all()
)
ctx['status_change_types'] = STATUS_CHANGE_TYPES

View File

@ -1,15 +1,21 @@
{% extends 'users/settings/base.html' %}
{% block page_title %}Teams{% endblock %}
{% block settings %}
<h1 class="mb-3">Teams</h1>
<div class="row">
<div class="col">
<p>
To create a new team or become part of an existing one, please
<a href="https://projects.blender.org/infrastructure/extensions-website/issues/new?template=.gitea/issue_template/team.yaml" target="_blank">get in touch</a>.
</p>
{% if team_memberships %}
<table class="table table-hover">
<thead>
<tr>
<th class="w-100">
Team name
Name
</th>
<th>
Role
@ -61,9 +67,6 @@
You are not assigned to any teams yet.
</p>
{% endif %}
<p class="pt-3">
We can help you with team management if you <a href="https://projects.blender.org/infrastructure/extensions-website/issues/new?title=Team%20Management%20Request&body=Please%20add%20user%20X%20to%20team%20Y">submit your request</a> to the issue tracker.
</p>
</div>
</div>
{% endblock settings %}

View File

@ -54,7 +54,7 @@ class User(TrackChangesMixin, AbstractUser):
is_subscribed_to_notification_emails = models.BooleanField(null=False, default=True)
def __str__(self) -> str:
return f'{self.full_name or self.username}'
return self.username
@property
def image_url(self) -> Optional[str]:
@ -142,4 +142,7 @@ class User(TrackChangesMixin, AbstractUser):
@property
def is_moderator(self):
# Used to review and approve extensions
return self.groups.filter(name='moderators').exists()
for g in self.groups.all():
if g.name == 'moderators':
return True
return False

View File

@ -1,7 +1,5 @@
{% load static %}
<img src="{% if user.image %}{{ user.image.url }}{% else %}{% static 'common/images/blank-profile-pic.png' %}{% endif %}" class="profile-avatar {{ classes }}">
{% if show_name %}
<span class="ms-2">
{% firstof user.full_name user.username %}
</span>
<span class="ms-2">{{ user.username }}</span>
{% endif %}

View File

@ -1,8 +1,9 @@
{% extends 'users/settings/base.html' %}
{% block page_title %}Delete Account{% endblock %}
{% block settings %}
{% with can_be_deleted=user.can_be_deleted %}
<h1 class="mb-3">Delete account</h1>
<h1 class="mb-3">Delete Account</h1>
<div class="row">
<div class="col">
<div>

View File

@ -1,6 +1,6 @@
{% extends 'users/settings/base.html' %}
{% load static %}
{% block page_title %}Profile{% endblock %}
{% block settings %}
<h1 class="mb-3">Profile</h1>
@ -15,9 +15,9 @@
<div class="row">
<div class="col-md-6 mb-4">
<label class="mb-2 h3" for="userFullName">Full Name</label>
<label class="h3 mb-2" for="username">Username</label>
<div class="align-items-center d-flex position-relative">
<input disabled class="form-control" type="text" value="{{ user.full_name }}" id="userFullName">
<input disabled class="form-control" type="text" value="{{ user.username }}" id="username">
<i class="i-lock"></i>
</div>
<p class="helptext mb-0">
@ -55,17 +55,6 @@
</div>
<div class="row">
<div class="col-md-6 mb-4">
<label class="h3 mb-2" for="username">Username</label>
<div class="align-items-center d-flex position-relative">
<input disabled class="form-control" type="text" value="{{ user.username }}" id="username">
<i class="i-lock"></i>
</div>
<p class="helptext mb-0">
Displayed in extension pages, rating comments and so on.
</p>
<div class="border-bottom-tertiary mt-3"></div>
</div>
<div class="col-md-6 mb-4">
<h3 class="label mb-2">Profile Picture</h3>
<img src="{{ user.image_url }}" width="44" height="44" class="rounded" alt="Profile Image">
@ -74,9 +63,7 @@
</p>
<div class="border-bottom-tertiary mt-3"></div>
</div>
</div>
<div class="row">
{# TODO: check badges display #}
{% if user.badges %}
<div class="col-md-6 mb-4">
@ -94,7 +81,9 @@
<div class="border-bottom-tertiary mt-3"></div>
</div>
{% endif %}
</div>
<div class="row">
<div class="col-md-6">
<h3 class="label mb-2">Preview</h3>
<div class="p-2 background-color rounded">