Compare commits

..

2 Commits

Author SHA1 Message Date
Mira Weller
7e57ab582e Bump version to 2024.6.1 2024-08-23 15:43:31 +02:00
Mira
d9d638a232 Escape HTML in placeholder samples in mail preview (#4413)
CVE-2024-8113
2024-08-23 15:20:22 +02:00
22 changed files with 1200 additions and 1687 deletions

View File

@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "2024.7.0.dev0"
__version__ = "2024.6.1"

View File

@@ -37,7 +37,6 @@ from decimal import Decimal
from zoneinfo import ZoneInfo
from django import forms
from django.conf import settings
from django.db.models import (
Case, CharField, Count, DateTimeField, F, IntegerField, Max, Min, OuterRef,
Q, Subquery, Sum, When,
@@ -55,7 +54,7 @@ from openpyxl.comments import Comment
from openpyxl.styles import Font, PatternFill
from pretix.base.models import (
Checkin, GiftCard, GiftCardTransaction, Invoice, InvoiceAddress, Order,
GiftCard, GiftCardTransaction, Invoice, InvoiceAddress, Order,
OrderPosition, Question,
)
from pretix.base.models.orders import (
@@ -542,22 +541,6 @@ class OrderListExporter(MultiSheetListExporter):
).order_by()
qs = base_qs.annotate(
payment_providers=Subquery(p_providers, output_field=CharField()),
checked_in_lists=Subquery(
Checkin.objects.filter(
successful=True,
type=Checkin.TYPE_ENTRY,
position=OuterRef("pk"),
).order_by().values("position").annotate(
c=GroupConcat(
"list__name",
# These appear not to work properly on SQLite. Well, we don't support SQLite outside testing
# anyways.
ordered='sqlite' not in settings.DATABASES['default']['ENGINE'],
distinct='sqlite' not in settings.DATABASES['default']['ENGINE'],
delimiter=", "
)
).values("c")
),
).select_related(
'order', 'order__invoice_address', 'order__customer', 'item', 'variation',
'voucher', 'tax_rule'
@@ -655,7 +638,6 @@ class OrderListExporter(MultiSheetListExporter):
_('Sales channel'), _('Order locale'),
_('E-mail address verified'),
_('External customer ID'),
_('Check-in lists'),
_('Payment providers'),
]
@@ -794,7 +776,6 @@ class OrderListExporter(MultiSheetListExporter):
_('Yes') if order.email_known_to_work else _('No'),
str(order.customer.external_identifier) if order.customer and order.customer.external_identifier else '',
]
row.append(op.checked_in_lists or "")
row.append(', '.join([
str(self.providers.get(p, p)) for p in sorted(set((op.payment_providers or '').split(',')))
if p and p != 'free'

View File

@@ -38,6 +38,7 @@ from datetime import datetime
from django import forms
from django.utils.formats import get_format
from django.utils.functional import lazy
from django.utils.html import escape
from django.utils.timezone import get_current_timezone, now
from django.utils.translation import gettext_lazy as _
@@ -64,7 +65,7 @@ def format_placeholders_help_text(placeholders, event=None):
placeholders = [(k, v.render_sample(event) if event else v) for k, v in placeholders.items()]
placeholders.sort(key=lambda x: x[0])
phs = [
'<button type="button" class="content-placeholder" title="%s">{%s}</button>' % (_("Sample: %s") % v if v else "", k)
'<button type="button" class="content-placeholder" title="%s">{%s}</button>' % (escape(_("Sample: %s") % v) if v else "", escape(k))
for k, v in placeholders
]
return _('Available placeholders: {list}').format(

View File

@@ -122,16 +122,6 @@ class ItemCategory(LoggedModel):
return _('{category} (Add-On products)').format(category=str(name))
return str(name)
def get_category_type_display(self):
if self.is_addon:
return _('Add-On products')
else:
return None
@property
def category_type(self):
return 'addon' if self.is_addon else 'normal'
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
if self.event:

View File

@@ -32,6 +32,7 @@
<tr>
<th>{% trans "Product categories" %}</th>
<th class="action-col-2"></th>
<th class="action-col-2"></th>
</tr>
</thead>
<tbody data-dnd-url="{% url "control:event.items.categories.reorder" organizer=request.event.organizer.slug event=request.event.slug %}">
@@ -40,16 +41,18 @@
<td>
<strong><a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}">{{ c.internal_name|default:c.name }}</a></strong>
</td>
<td>
<button formaction="{% url "control:event.items.categories.up" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 and not page_obj.has_previous %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button formaction="{% url "control:event.items.categories.down" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
<span class="dnd-container"></span>
</td>
<td class="text-right flip">
<button title="{% trans "Move up" %}" formaction="{% url "control:event.items.categories.up" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 and not page_obj.has_previous %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button title="{% trans "Move down" %}" formaction="{% url "control:event.items.categories.down" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
<span class="dnd-container" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
<a title="{% trans "Edit" %}" href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.items.categories.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ c.id }}"
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
<span class="fa fa-copy"></span>
</a>
<a title="{% trans "Delete" %}" href="{% url "control:event.items.categories.delete" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
<a href="{% url "control:event.items.categories.delete" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
</td>
</tr>
{% endfor %}

View File

@@ -56,7 +56,8 @@
<th>{% trans "Internal name" %}</th>
<th></th>
<th></th>
<th colspan="2">{% trans "Products" %}</th>
<th>{% trans "Products" %}</th>
<th class="action-col-2"></th>
<th class="action-col-2"></th>
</tr>
</thead>
@@ -101,10 +102,9 @@
{% endif %}
{% endif %}
</td>
<td {% if d.benefit_same_products %}colspan="2"{% endif %}>
{% if not d.benefit_same_products %}{% trans "Condition:" %}{% endif %}
<td>
{% if d.condition_all_products %}
<ul><li><em>{% trans "All" %}</em></li></ul>
<em>{% trans "All" %}</em>
{% else %}
<ul>
{% for item in d.condition_limit_products.all %}
@@ -115,28 +115,18 @@
</ul>
{% endif %}
</td>
{% if not d.benefit_same_products %}
<td>
{% trans "Applies to:" %}
<ul>
{% for item in d.benefit_limit_products.all %}
<li>
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item }}</a>
</li>
{% endfor %}
</ul>
</td>
{% endif %}
<td class="text-right flip">
<td>
<button formaction="{% url "control:event.items.discounts.up" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
class="btn btn-default btn-sm sortable-up" title="{% trans "Move up" %}"
class="btn btn-default btn-sm sortable-up"
{% if forloop.counter0 == 0 and not page_obj.has_previous %}
disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button formaction="{% url "control:event.items.discounts.down" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
class="btn btn-default btn-sm sortable-down" title="{% trans "Move down" %}"
class="btn btn-default btn-sm sortable-down"
{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}>
<i class="fa fa-arrow-down"></i></button>
<span class="dnd-container" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
<span class="dnd-container"></span>
</td>
<td class="text-right flip">
<a href="{% url "control:event.items.discounts.edit" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.items.discounts.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ d.id }}"

View File

@@ -1,15 +1,13 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load money %}
{% block title %}{% trans "Products" %}{% endblock %}
{% block inside %}
{% blocktrans asvar s_taxes %}taxes{% endblocktrans %}
<h1>{% trans "Products" %}</h1>
<p>
{% blocktrans trimmed %}
Below, you find a list of all available products. You can click on a product name to inspect and change
product details. You can also use the buttons on the right to change the order of products or move
products to a different category.
product details. You can also use the buttons on the right to change the order of products within a
give category.
{% endblocktrans %}
</p>
{% if items|length == 0 %}
@@ -31,7 +29,7 @@
<form method="post">
{% csrf_token %}
<div class="table-responsive">
<table class="table table-condensed table-hover table-items">
<table class="table table-condensed table-hover">
<thead>
<tr>
<th>{% trans "Product name" %}</th>
@@ -39,24 +37,16 @@
<th class="iconcol"></th>
<th class="iconcol"></th>
<th class="iconcol"></th>
<th class="text-right flip">{% trans "Default price" %}</th>
<th>{% trans "Category" %}</th>
<th class="action-col-2"><span class="sr-only">Move</span></th>
<th class="action-col-2"><span class="sr-only">Edit</span></th>
</tr>
</thead>
{% for c, items in cat_list %}
{% if c %}
<tbody>
<tr class="sortable-disabled"><th colspan="9" scope="colgroup" class="text-muted">
{{ c.name }}{% if c.category_type != "normal" %} <span class="font-normal">({{ c.get_category_type_display }})</span>{% endif %}
<a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" title="{% trans "Edit" %}"><span class="fa fa-edit fa-fw"></span></a>
</th></tr>
</tbody>
{% endif %}
<tbody data-dnd-url="{% url "control:event.items.reorder" organizer=request.event.organizer.slug event=request.event.slug category=c.id|default:0 %}"
data-dnd-group="items">
{% for i in items %}
{% if forloop.counter0 == 0 and i.category %}{% endif %}
{% regroup items by category as cat_list %}
{% for c in cat_list %}
<tbody data-dnd-url="{% url "control:event.items.reorder" organizer=request.event.organizer.slug event=request.event.slug %}">
{% for i in c.list %}
{% if forloop.counter0 == 0 and i.category %}<tr class="sortable-disabled"><th colspan="8" scope="colgroup" class="text-muted">{{ i.category }}</th></tr>{% endif %}
<tr data-dnd-id="{{ i.id }}" {% if not i.active %}class="row-muted"{% endif %}>
<td><strong>
{% if not i.active %}<strike>{% endif %}
@@ -102,15 +92,15 @@
</td>
<td>
{% if i.var_count %}
<span class="fa fa-bars fa-fw text-muted" data-toggle="tooltip" title="{% trans "Product with variations" %}"></span>
<span class="fa fa-th-large fa-fw text-muted" data-toggle="tooltip" title="{% trans "Product with variations" %}"></span>
{% endif %}
</td>
<td>
{% if i.category.is_addon %}
<span class="fa fa-plus-square fa-fw text-muted" data-toggle="tooltip"
<span class="fa fa-puzzle-piece fa-fw text-muted" data-toggle="tooltip"
title="{% trans "Only available as an add-on product" %}"></span>
{% elif i.require_bundling %}
<span class="fa fa-plus-square fa-fw text-muted" data-toggle="tooltip"
<span class="fa fa-puzzle-piece fa-fw text-muted" data-toggle="tooltip"
title="{% trans "Only available as part of a bundle" %}"></span>
{% elif i.hide_without_voucher %}
<span class="fa fa-tags fa-fw text-muted" data-toggle="tooltip"
@@ -120,29 +110,16 @@
title="{% trans "Can only be bought using a voucher" %}"></span>
{% endif %}
</td>
<td class="text-right flip">
{% if i.free_price %}
<span class="fa fa-edit fa-fw text-muted" data-toggle="tooltip" title="{% trans "Free price input" %}">
</span>
{% endif %}
{{ i.default_price|money:request.event.currency }}
{% if i.original_price %}<strike class="text-muted">{{ i.original_price|money:request.event.currency }}</strike>{% endif %}
{% if i.tax_rule and i.default_price %}
<br/>
<small class="text-muted">
{% blocktrans trimmed with rate=i.tax_rule.rate|floatformat:-2 taxname=i.tax_rule.name|default:s_taxes %}
incl. {{ rate }}% {{ taxname }}
{% endblocktrans %}
</small>
{% endif %}
<td>{% if i.category %}{{ i.category.name }}{% endif %}</td>
<td>
<button formaction="{% url "control:event.items.up" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button formaction="{% url "control:event.items.down" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
<span class="dnd-container"></span>
</td>
<td class="text-right flip col-actions">
<button title="{% trans "Move up" %}" formaction="{% url "control:event.items.up" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button title="{% trans "Move down" %}" formaction="{% url "control:event.items.down" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
<span class="dnd-container" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm" title="{% trans "Edit" %}"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.items.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ i.id }}" class="btn btn-default btn-sm" title="{% trans "Clone" %}" data-toggle="tooltip"><i class="fa fa-copy"></i></a>
<a href="{% url "control:event.items.delete" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-danger btn-sm" title="{% trans "Delete" %}"><i class="fa fa-trash"></i></a>
<a href="{% url "control:event.items.delete" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
</td>
</tr>
{% endfor %}

View File

@@ -51,9 +51,9 @@
{% endif %}
</td>
<td>
<button title="{% trans "Move up" %}" formaction="{% url "control:organizer.property.up" organizer=request.organizer.slug property=p.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 and not page_obj.has_previous %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button title="{% trans "Move down" %}" formaction="{% url "control:organizer.property.down" organizer=request.organizer.slug property=p.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
<span class="dnd-container" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
<button formaction="{% url "control:organizer.property.up" organizer=request.organizer.slug property=p.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 and not page_obj.has_previous %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button formaction="{% url "control:organizer.property.down" organizer=request.organizer.slug property=p.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
<span class="dnd-container"></span>
</td>
<td class="text-right flip">
<a href="{% url "control:organizer.property.edit" organizer=request.organizer.slug property=p.id %}"

View File

@@ -293,7 +293,7 @@ urlpatterns = [
re_path(r'^items/(?P<item>\d+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'),
re_path(r'^items/(?P<item>\d+)/up$', item.item_move_up, name='event.items.up'),
re_path(r'^items/(?P<item>\d+)/down$', item.item_move_down, name='event.items.down'),
re_path(r'^items/reorder/(?P<category>\d+)/$', item.reorder_items, name='event.items.reorder'),
re_path(r'^items/reorder$', item.reorder_items, name='event.items.reorder'),
re_path(r'^items/(?P<item>\d+)/delete$', item.ItemDelete.as_view(), name='event.items.delete'),
re_path(r'^items/typeahead/meta/$', typeahead.item_meta_values, name='event.items.meta.typeahead'),
re_path(r'^items/select2$', typeahead.items_select2, name='event.items.select2'),

View File

@@ -62,6 +62,7 @@ from django.http import (
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.html import escape
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.timezone import now
from django.utils.translation import gettext, gettext_lazy as _, gettext_noop
@@ -736,7 +737,7 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
else:
ctx[p.identifier] = '<span class="placeholder" title="{}">{}</span>'.format(
_('This value will be replaced based on dynamic parameters.'),
s
escape(s)
)
return ctx
@@ -786,7 +787,7 @@ class MailSettingsRendererPreview(MailSettingsPreview):
def placeholders(self, item):
ctx = {}
for p in get_available_placeholders(self.request.event, MailSettingsForm.base_context[item]).values():
ctx[p.identifier] = str(p.render_sample(self.request.event))
ctx[p.identifier] = escape(str(p.render_sample(self.request.event)))
return ctx
def get(self, request, *args, **kwargs):

View File

@@ -35,7 +35,6 @@
import json
from collections import OrderedDict, namedtuple
from itertools import groupby
from json.decoder import JSONDecodeError
from django.contrib import messages
@@ -114,8 +113,6 @@ class ItemList(ListView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['sales_channels'] = get_all_sales_channels()
items_by_category = {cat: list(items) for cat, items in groupby(ctx['items'], lambda item: item.category)}
ctx['cat_list'] = [(cat, items_by_category.get(cat, [])) for cat in [None, *self.request.event.categories.all()]]
return ctx
@@ -172,7 +169,7 @@ def item_move_down(request, organizer, event, item):
@transaction.atomic
@event_permission_required("can_change_items")
@require_http_methods(["POST"])
def reorder_items(request, organizer, event, category):
def reorder_items(request, organizer, event):
try:
ids = json.loads(request.body.decode('utf-8'))['ids']
except (JSONDecodeError, KeyError, ValueError):
@@ -183,21 +180,23 @@ def reorder_items(request, organizer, event, category):
if len(input_items) != len(ids):
raise Http404(_("Some of the provided object ids are invalid."))
if int(category):
target_category = request.event.categories.get(id=category)
else:
target_category = None
item_categories = {i.category_id for i in input_items}
if len(item_categories) > 1:
raise Http404(_("You cannot reorder items spanning different categories."))
# get first and only category
item_category = next(iter(item_categories))
if len(input_items) != request.event.items.filter(category=item_category).count():
raise Http404(_("Not all objects have been selected."))
for i in input_items:
pos = ids.index(str(i.pk))
if pos != i.position or target_category != i.category: # Save unneccessary UPDATE queries
if pos != i.position: # Save unneccessary UPDATE queries
i.position = pos
i.category = target_category
i.save(update_fields=['position', 'category_id'])
i.save(update_fields=['position'])
i.log_action(
'pretix.event.item.reordered', user=request.user, data={
'position': i,
'category': target_category and target_category.pk,
}
)

View File

@@ -50,7 +50,7 @@ from django.http import (
from django.shortcuts import redirect, render
from django.urls import resolve, reverse
from django.utils.functional import cached_property
from django.utils.html import format_html
from django.utils.html import escape, format_html
from django.utils.safestring import mark_safe
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
@@ -562,7 +562,7 @@ class VoucherBulkMailPreview(EventPermissionRequiredMixin, View):
else:
ctx[p.identifier] = '<span class="placeholder" title="{}">{}</span>'.format(
_('This value will be replaced based on dynamic parameters.'),
s
escape(s)
)
return self.SafeDict(ctx)

View File

@@ -64,8 +64,7 @@ def casual_reads():
class GroupConcat(Aggregate):
function = 'group_concat'
template = '%(function)s(%(distinct)s%(field)s, "%(separator)s")'
allow_distinct = True
template = '%(function)s(%(field)s, "%(separator)s")'
def __init__(self, *expressions, ordered=False, **extra):
self.ordered = ordered
@@ -74,17 +73,19 @@ class GroupConcat(Aggregate):
extra.update({'separator': ','})
super().__init__(*expressions, **extra)
def as_postgresql(self, compiler, connection, **extra_context):
def as_postgresql(self, compiler, connection):
if self.ordered:
template = "%(function)s(%(distinct)s%(field)s::text, '%(separator)s' ORDER BY %(field)s::text ASC)"
return super().as_sql(
compiler, connection,
function='string_agg',
template="%(function)s(%(field)s::text, '%(separator)s' ORDER BY %(field)s ASC)",
)
else:
template = "%(function)s(%(distinct)s%(field)s::text, '%(separator)s')"
return super().as_sql(
compiler, connection,
function='string_agg',
template=template,
**extra_context,
)
return super().as_sql(
compiler, connection,
function='string_agg',
template="%(function)s(%(field)s::text, '%(separator)s')",
)
class ReplicaRouter:

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-06-24 08:20+0000\n"
"PO-Revision-Date: 2024-06-24 15:12+0000\n"
"Last-Translator: Mira <weller@rami.io>\n"
"PO-Revision-Date: 2024-01-31 04:00+0000\n"
"Last-Translator: Eduardo Fernandez <sistemas@oyasama.es>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/"
"es/>\n"
"Language: es\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.5.5\n"
"X-Generator: Weblate 5.3.1\n"
#: pretix/_base_settings.py:78
msgid "English"
@@ -37,7 +37,7 @@ msgstr "Árabe"
#: pretix/_base_settings.py:82
msgid "Catalan"
msgstr "catalán"
msgstr ""
#: pretix/_base_settings.py:83
msgid "Chinese (simplified)"
@@ -117,7 +117,7 @@ msgstr "Ruso"
#: pretix/_base_settings.py:102
msgid "Slovak"
msgstr "eslovaco"
msgstr ""
#: pretix/_base_settings.py:103
msgid "Spanish"
@@ -2876,7 +2876,7 @@ msgstr "Código del cupón"
#: pretix/base/forms/__init__.py:118
#, python-brace-format
msgid "You can use {markup_name} in this field."
msgstr "Se puede usar {markup_name} en este campo."
msgstr ""
#: pretix/base/forms/__init__.py:178
#, python-format
@@ -3095,10 +3095,6 @@ msgid ""
"up. Please note: to use literal \"{\" or \"}\", you need to double them as "
"\"{{\" and \"}}\"."
msgstr ""
"Hay un error con el sintaxis de marcador de posición. Por favor, asegúrase "
"de que las llaves de apertura «{» y las llaves de cierre «}» de sus "
"marcadores de posición coinciden. Tenga en cuenta: para usar «{» o «}» "
"literalmente, se debe teclearlas dos veces como «{{» o «}}» respectivamente."
#: pretix/base/forms/validators.py:72 pretix/control/views/event.py:771
#, fuzzy, python-format
@@ -4605,7 +4601,7 @@ msgstr "Esperando, producto no disponible"
#: pretix/base/models/items.py:382
#: pretix/control/templates/pretixcontrol/subevents/fragment_unavail_mode_indicator.html:5
msgid "Show info text if unavailable"
msgstr "Muestra el texto informativo, si no es disponible"
msgstr ""
#: pretix/base/models/items.py:389 pretix/base/models/items.py:704
msgid "Don't use re-usable media, use regular one-off tickets"
@@ -5968,9 +5964,6 @@ msgid ""
"up two-factor authentication or leave the team. The setting may take a few "
"minutes to become effective for all users."
msgstr ""
"Si activado, todos los miembros del equipo requerirán configurar la "
"autenticación de dos factores o dejar el equipo. El ajuste podría tardar "
"pocos minutos para activar para todos los usuarios."
#: pretix/base/models/organizer.py:275
msgid "Can create events"
@@ -6247,9 +6240,6 @@ msgid ""
"of a specific product, you can also select a quota. In this case, all "
"products assigned to this quota can be selected."
msgstr ""
"Este producto añadirá al carrito del usuario si está usado el vale. En vez "
"de un producto específico, se puede seleccionar una cuota. En este caso, "
"todos los productos que están asignados al esta cuota se pueden seleccionar."
#: pretix/base/models/vouchers.py:265
msgid "This variation of the product select above is being used."
@@ -9661,11 +9651,6 @@ msgid ""
"still people on the waiting list. Vouchers that have already been sent "
"remain active."
msgstr ""
"La lista de espera estará deshabilitada después de esta fecha. Esto "
"significa que nadie puede añadirse a la lista; sin embargo, las entradas "
"estarán disponibles para la venta de nuevo si permite la cuota, incluso si "
"hay alguien en la lista de espera. Los vales que ya se han enviado "
"permanecerán activos."
#: pretix/base/settings.py:1420
msgid "Ask for a name"
@@ -9909,7 +9894,7 @@ msgstr "Ocultar todas las fechas pasadas del calendario"
#: pretix/base/settings.py:1664 pretix/base/settings.py:1673
msgid "No modifications after order was submitted"
msgstr "Ningunas modificaciones se entregaron después del pedido"
msgstr ""
#: pretix/base/settings.py:1665 pretix/base/settings.py:1674
#, fuzzy
@@ -9919,7 +9904,7 @@ msgstr "Sólo las órdenes pendientes o pagadas pueden ser cambiadas."
#: pretix/base/settings.py:1666 pretix/base/settings.py:1675
msgid "Both the attendee and the person who ordered can make changes"
msgstr "Ambos el/la asistente y el/la comprador(a) pueden hacer cambios"
msgstr ""
#: pretix/base/settings.py:1670
#, fuzzy
@@ -9938,8 +9923,6 @@ msgid ""
"By default, no more modifications are possible for an order as soon as one "
"of the tickets in the order has been checked in."
msgstr ""
"Por defecto, las modificaciones del pedido no se estarán permitidas una vez "
"que una de las entradas del pedido se hayan canjeado."
#: pretix/base/settings.py:1696
msgid "Last date of modifications"
@@ -16037,8 +16020,6 @@ msgid ""
"A first login using {agent_type} on {os_type} from {country} has been "
"detected."
msgstr ""
"Se ha detectado un inicio de sesión nuevo por {agent_type} usando {os_type} "
"de {country}."
#: pretix/control/logdisplay.py:460 pretix/control/views/user.py:489
#: pretix/control/views/user.py:549 pretix/control/views/user.py:608
@@ -18129,21 +18110,6 @@ msgid ""
"Best regards, \n"
"Your %(instance)s team\n"
msgstr ""
"Hola,\n"
"\n"
"Se ha detectado un inicio de sesión de tu cuenta %(instance)s por una "
"ubicación nueva o inusual. Se inició el sesión por %(agent)s de %(os)s de "
"%(country)s.\n"
"\n"
"Si tú hiciste esto, puedes ignorar este correo.\n"
"\n"
"Si no fuiste tú, recomendamos que cambies la contraseña en los ajustes de "
"cuenta:\n"
"\n"
"%(url)s\n"
"\n"
"Saludos cordiales,\n"
"Su equipo de %(instance)s\n"
#: pretix/control/templates/pretixcontrol/email/security_notice.txt:1
#, python-format
@@ -20004,51 +19970,52 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/global_sysreport.html:16
msgid "January"
msgstr "enero"
msgstr "Enero"
#: pretix/control/templates/pretixcontrol/global_sysreport.html:17
msgid "February"
msgstr "febrero"
msgstr "Febrero"
#: pretix/control/templates/pretixcontrol/global_sysreport.html:18
#, fuzzy
msgid "March"
msgstr "marzo"
msgstr "Marzo"
#: pretix/control/templates/pretixcontrol/global_sysreport.html:19
msgid "April"
msgstr "abril"
msgstr "Abril"
#: pretix/control/templates/pretixcontrol/global_sysreport.html:20
msgid "May"
msgstr "mayo"
msgstr "Mayo"
#: pretix/control/templates/pretixcontrol/global_sysreport.html:21
msgid "June"
msgstr "junio"
msgstr "Junio"
#: pretix/control/templates/pretixcontrol/global_sysreport.html:22
msgid "July"
msgstr "julio"
msgstr "Julio"
#: pretix/control/templates/pretixcontrol/global_sysreport.html:23
msgid "August"
msgstr "agosto"
msgstr "Agosto"
#: pretix/control/templates/pretixcontrol/global_sysreport.html:24
msgid "September"
msgstr "septiembre"
msgstr "Septiembre"
#: pretix/control/templates/pretixcontrol/global_sysreport.html:25
msgid "October"
msgstr "octubre"
msgstr "Octubre"
#: pretix/control/templates/pretixcontrol/global_sysreport.html:26
msgid "November"
msgstr "noviembre"
msgstr "Noviembre"
#: pretix/control/templates/pretixcontrol/global_sysreport.html:27
msgid "December"
msgstr "diciembre"
msgstr "Diciembre"
#: pretix/control/templates/pretixcontrol/global_sysreport.html:32
#, fuzzy

View File

@@ -8,16 +8,16 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-06-24 08:20+0000\n"
"PO-Revision-Date: 2024-06-24 15:12+0000\n"
"Last-Translator: Mira <weller@rami.io>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/"
"pretix-js/es/>\n"
"PO-Revision-Date: 2023-11-07 14:00+0000\n"
"Last-Translator: Zona Vip <contacto@zonavip.mx>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
"js/es/>\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.5.5\n"
"X-Generator: Weblate 5.1.1\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -1069,51 +1069,51 @@ msgstr "Dom"
#: pretix/static/pretixpresale/js/widget/widget.js:78
msgid "January"
msgstr "enero"
msgstr "Enero"
#: pretix/static/pretixpresale/js/widget/widget.js:79
msgid "February"
msgstr "febrero"
msgstr "Febrero"
#: pretix/static/pretixpresale/js/widget/widget.js:80
msgid "March"
msgstr "marzo"
msgstr "Marzo"
#: pretix/static/pretixpresale/js/widget/widget.js:81
msgid "April"
msgstr "abril"
msgstr "Abril"
#: pretix/static/pretixpresale/js/widget/widget.js:82
msgid "May"
msgstr "mayo"
msgstr "Mayo"
#: pretix/static/pretixpresale/js/widget/widget.js:83
msgid "June"
msgstr "junio"
msgstr "Junio"
#: pretix/static/pretixpresale/js/widget/widget.js:84
msgid "July"
msgstr "julio"
msgstr "Julio"
#: pretix/static/pretixpresale/js/widget/widget.js:85
msgid "August"
msgstr "agosto"
msgstr "Agosto"
#: pretix/static/pretixpresale/js/widget/widget.js:86
msgid "September"
msgstr "septiembre"
msgstr "Septiembre"
#: pretix/static/pretixpresale/js/widget/widget.js:87
msgid "October"
msgstr "octubre"
msgstr "Octubre"
#: pretix/static/pretixpresale/js/widget/widget.js:88
msgid "November"
msgstr "noviembre"
msgstr "Noviembre"
#: pretix/static/pretixpresale/js/widget/widget.js:89
msgid "December"
msgstr "diciembre"
msgstr "Diciembre"
#~ msgctxt "widget"
#~ msgid "See variations"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -46,6 +46,7 @@ from django.shortcuts import get_object_or_404, redirect
from django.template.loader import get_template
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.html import escape
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, ngettext
from django.views.generic import DeleteView, FormView, ListView, TemplateView
@@ -193,7 +194,7 @@ class BaseSenderView(EventPermissionRequiredMixin, FormView):
for k, v in get_available_placeholders(self.request.event, self.context_parameters).items():
context_dict[k] = '<span class="placeholder" title="{}">{}</span>'.format(
_('This value will be replaced based on dynamic parameters.'),
v.render_sample(self.request.event)
escape(v.render_sample(self.request.event))
)
subject = bleach.clean(form.cleaned_data['subject'].localize(l), tags=[])
@@ -608,7 +609,7 @@ class CreateRule(EventPermissionRequiredMixin, CreateView):
'position_or_address']).items():
context_dict[k] = '<span class="placeholder" title="{}">{}</span>'.format(
_('This value will be replaced based on dynamic parameters.'),
v.render_sample(self.request.event)
escape(v.render_sample(self.request.event))
)
subject = bleach.clean(form.cleaned_data['subject'].localize(l), tags=[])
@@ -684,7 +685,7 @@ class UpdateRule(EventPermissionRequiredMixin, UpdateView):
for k, v in get_available_placeholders(self.request.event, ['event', 'order', 'position_or_address']).items():
placeholders[k] = '<span class="placeholder" title="{}">{}</span>'.format(
_('This value will be replaced based on dynamic parameters.'),
v.render_sample(self.request.event)
escape(v.render_sample(self.request.event))
)
subject = bleach.clean(self.object.subject.localize(lang), tags=[])

View File

@@ -1,78 +1,48 @@
/*global $, Sortable*/
$(function () {
const allContainers = $("[data-dnd-url]");
function updateAllSortButtonStates() {
allContainers.each(function() { updateSortButtonState($(this)); });
}
function updateSortButtonState(container) {
var disabledUp = container.find(".sortable-up:disabled"),
firstUp = container.find(">tr[data-dnd-id] .sortable-up").first();
if (disabledUp.length && disabledUp.get(0) !== firstUp.get(0)) {
disabledUp.prop("disabled", false);
firstUp.prop("disabled", true);
}
var disabledDown = container.find(".sortable-down:disabled"),
lastDown = container.find(">tr[data-dnd-id] .sortable-down").last();
if (disabledDown.length && disabledDown.get(0) !== lastDown.get(0)) {
disabledDown.prop("disabled", false);
lastDown.prop("disabled", true);
}
}
let didSort = false, lastClick = 0;
allContainers.each(function(){
const container = $(this),
$("[data-dnd-url]").each(function(){
var container = $(this),
url = container.data("dnd-url"),
handle = $('<span class="btn btn-default btn-sm dnd-sort-handle"><i class="fa fa-arrows"></i></span>');
container.find(".dnd-container").append(handle);
if (!sessionStorage.dndShowMoveButtons) {
container.find(".sortable-up, .sortable-down").addClass("sr-only").on("click", function () {
sessionStorage.dndShowMoveButtons = 'true';
});
}
if (container.find("[data-dnd-id]").length < 2 && !container.data("dnd-group")) {
if (container.find("[data-dnd-id]").length < 2) {
handle.addClass("disabled");
return;
}
function maybeShowSortButtons() {
if (Date.now() - lastClick < 3000) {
$("[data-dnd-url] .sortable-up, [data-dnd-url] .sortable-down").removeClass("sr-only");
updateAllSortButtonStates();
}
lastClick = Date.now();
}
container.find(".dnd-sort-handle").on("mouseup", maybeShowSortButtons);
const group = container.data("dnd-group");
const containers = group ? container.parent().find('[data-dnd-group="' + group + '"]') : container;
Sortable.create(container.get(0), {
filter: ".sortable-disabled",
handle: ".dnd-sort-handle",
group: group,
onMove: function (evt) {
return evt.related.className.indexOf('sortable-disabled') === -1;
},
onStart: function (evt) {
containers.addClass("sortable-dragarea");
container.addClass("sortable-dragarea");
container.parent().addClass("sortable-sorting");
didSort = false;
},
onEnd: function (evt) {
containers.removeClass("sortable-dragarea");
container.removeClass("sortable-dragarea");
container.parent().removeClass("sortable-sorting");
if (!didSort) {
maybeShowSortButtons();
} else {
$("[data-dnd-url] .sortable-up, [data-dnd-url] .sortable-down").addClass("sr-only");
delete sessionStorage.dndShowMoveButtons;
var disabledUp = container.find(".sortable-up:disabled"),
firstUp = container.find(">tr[data-dnd-id] .sortable-up").first();
if (disabledUp.length && disabledUp.get(0) !== firstUp.get(0)) {
disabledUp.prop("disabled", false);
firstUp.prop("disabled", true);
}
var disabledDown = container.find(".sortable-down:disabled"),
lastDown = container.find(">tr[data-dnd-id] .sortable-down").last();
if (disabledDown.length && disabledDown.get(0) !== lastDown.get(0)) {
disabledDown.prop("disabled", false);
lastDown.prop("disabled", true);
}
},
onSort: function (evt){
if (evt.target !== evt.to) return;
didSort = true;
var container = $(evt.to),
ids = container.find("[data-dnd-id]").toArray().map(function (e) { return e.dataset.dndId; });
const ids = container.find("[data-dnd-id]").toArray().map(function (e) { return e.dataset.dndId; });
$.ajax(
{
'type': 'POST',

View File

@@ -841,21 +841,9 @@ h1 .label {
tbody[data-dnd-url] {
transition: opacity 1s;
}
.table-items tbody + tbody {
border-top-width: 1px;
}
.table-items tbody[data-dnd-url]:after {
content:'';
display: table-row;
height: 0.5em;
}
.sortable-sorting tbody:not(.sortable-dragarea) {
opacity: .4;
}
.font-normal {
font-style: normal;
font-weight: normal;
}
tbody th {
background: $table-bg-hover;
}

View File

@@ -402,24 +402,6 @@ class ItemsTest(ItemFormTest):
self.item2.refresh_from_db()
assert self.item1.position < self.item2.position
def test_reorder(self):
self.client.post('/control/event/%s/%s/items/reorder/0/' % (self.orga1.slug, self.event1.slug), {
'ids': [str(self.item2.id), str(self.item1.id)],
}, content_type='application/json')
self.item1.refresh_from_db()
self.item2.refresh_from_db()
assert self.item1.position > self.item2.position
assert self.item1.category is None
assert self.item2.category is None
self.client.post('/control/event/%s/%s/items/reorder/%s/' % (self.orga1.slug, self.event1.slug, self.addoncat.id), {
'ids': [str(self.item1.id), str(self.item2.id)],
}, content_type='application/json')
self.item1.refresh_from_db()
self.item2.refresh_from_db()
assert self.item1.position < self.item2.position
assert self.item1.category.id == self.addoncat.id
assert self.item2.category.id == self.addoncat.id
def test_create(self):
self.client.post('/control/event/%s/%s/items/add' % (self.orga1.slug, self.event1.slug), {
'name_0': 'T-Shirt',

View File

@@ -323,7 +323,7 @@ event_permission_urls = [
("can_change_items", "items/add", 200, HTTP_GET),
("can_change_items", "items/1/up", 404, HTTP_POST),
("can_change_items", "items/1/down", 404, HTTP_POST),
("can_change_items", "items/reorder/2/", 400, HTTP_POST),
("can_change_items", "items/reorder", 400, HTTP_POST),
("can_change_items", "items/1/delete", 404, HTTP_GET),
# ("can_change_items", "categories/", 200),
# We don't have to create categories and similar objects