Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/pretix/control/forms/filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
Invoice,
InvoiceAddress,
Item,
ItemCategory,
Order,
OrderPayment,
OrderPosition,
Expand Down Expand Up @@ -1177,15 +1178,31 @@ class CheckInFilterForm(FilterForm):
required=False,
empty_label=_('All products'),
)
category = forms.ModelChoiceField(
label=_('Category'),
queryset=ItemCategory.objects.none(),
required=False,
empty_label=_('All categories'),
)

def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event')
self.list = kwargs.pop('list')
super().__init__(*args, **kwargs)
if self.list.all_products:
self.fields['item'].queryset = self.event.items.all()
# Show categories that are used by any items in the event
self.fields['category'].queryset = ItemCategory.objects.filter(
event=self.event,
items__isnull=False
).distinct()
else:
self.fields['item'].queryset = self.list.limit_products.all()
# Show categories that are used by items in the limited product list
self.fields['category'].queryset = ItemCategory.objects.filter(
event=self.event,
items__in=self.list.limit_products.all()
).distinct()

def filter_qs(self, qs):
fdata = self.cleaned_data
Expand Down Expand Up @@ -1233,6 +1250,9 @@ def filter_qs(self, qs):
if fdata.get('item'):
qs = qs.filter(item=fdata.get('item'))

if fdata.get('category'):
qs = qs.filter(item__category=fdata.get('category'))

return qs


Expand Down
9 changes: 6 additions & 3 deletions src/pretix/control/templates/pretixcontrol/checkin/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,18 @@ <h1>
</div>
</nav>
<form class="row filter-form" action="" method="get">
<div class="col-md-4 col-sm-6 col-xs-12">
<div class="col-md-3 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.user layout='inline' %}
</div>
<div class="col-md-3 col-sm-6 col-xs-12">
<div class="col-md-2 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.status layout='inline' %}
</div>
<div class="col-md-3 col-sm-6 col-xs-12">
<div class="col-md-2 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.item layout='inline' %}
</div>
<div class="col-md-3 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.category layout='inline' %}
</div>
<div class="col-md-2 col-sm-6 col-xs-12">
<button class="btn btn-primary btn-block" type="submit">
<span class="fa fa-filter"></span>
Expand Down
1 change: 1 addition & 0 deletions src/pretix/plugins/badges/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class Meta:
fields = (
'id',
'name',
'category',
'default',
'layout',
'size',
Expand Down
22 changes: 21 additions & 1 deletion src/pretix/plugins/badges/exporters.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,25 @@ def render_pdf(event, positions, opt):

Renderer._register_fonts()

# Build renderer map for specific item assignments
renderermap = {
bi.item_id: _renderer(event, bi.layout)
for bi in BadgeItem.objects.select_related('layout').filter(item__event=event)
}

# Build category-based renderer map
category_renderermap = {}
for layout in event.badge_layouts.filter(category__isnull=False).select_related('category'):
if layout.category_id in category_renderermap:
# Validation: Multiple layouts assigned to the same category
raise ValueError(
f"Duplicate badge layout assignment for category ID {layout.category_id}. "
"Each category should have only one badge layout assigned."
)
category_renderermap[layout.category_id] = _renderer(event, layout)
# Note: If you want to allow duplicates and document precedence, comment out the raise and add a comment:
# "If multiple layouts are assigned to the same category, the last one will take precedence."

try:
default_renderer = _renderer(event, event.badge_layouts.get(default=True))
except BadgeLayout.DoesNotExist:
Expand Down Expand Up @@ -220,7 +235,12 @@ def render_page(positions):
pagebuffer = []
outbuffer = BytesIO()
for op in positions:
r = renderermap.get(op.item_id, default_renderer)
# Priority order: specific item assignment > category assignment > default
r = renderermap.get(op.item_id)
if not r and hasattr(op.item, 'category_id') and op.item.category_id:
r = category_renderermap.get(op.item.category_id)
if not r:
r = default_renderer
if not r:
continue
any = True
Expand Down
19 changes: 18 additions & 1 deletion src/pretix/plugins/badges/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,30 @@
from django.forms.models import ModelChoiceIterator
from django.utils.translation import gettext_lazy as _

from pretix.base.models import ItemCategory
from pretix.plugins.badges.models import BadgeItem, BadgeLayout


class BadgeLayoutForm(forms.ModelForm):
class Meta:
model = BadgeLayout
fields = ('name',)
fields = ('name', 'category')

def __init__(self, *args, **kwargs):
event = kwargs.pop('event', None)
super().__init__(*args, **kwargs)

if event:
# Only show categories that are actually used by items in this event
used_categories = ItemCategory.objects.filter(
items__event=event
).distinct().order_by('name')

self.fields['category'].queryset = used_categories
self.fields['category'].empty_label = _('(No specific category)')
self.fields['category'].help_text = _(
'If selected, this layout will be automatically assigned to items in this category.'
)


NoLayoutSingleton = BadgeLayout(pk='-')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 4.2.16 on 2024-01-01 00:00

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('pretixbase', '0001_initial'),
('badges', '0004_alter_badgelayout_layout'),
]

operations = [
migrations.AddField(
model_name='badgelayout',
name='category',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='badge_layouts',
to='pretixbase.itemcategory',
verbose_name='Category'
),
),
]
9 changes: 9 additions & 0 deletions src/pretix/plugins/badges/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ class BadgeLayout(LoggedModel):
default=False,
)
name = models.CharField(max_length=190, verbose_name=_('Name'))
category = models.ForeignKey(
'pretixbase.ItemCategory',
on_delete=models.SET_NULL,
related_name='badge_layouts',
null=True,
blank=True,
verbose_name=_('Category'),
help_text=_('If set, this layout will be automatically used for items in this category.')
)
layout = models.TextField(
default='[{"type":"textarea","left":"0","bottom":"85","fontsize":"12.0","color":[0,0,0,1],"fontfamily":"Open Sans","bold":true,"italic":false,"width":"80","content":"attendee_name","text":"John Doe","align":"center"},{"type":"barcodearea","left":"24.87","bottom":"34","size":"30.00","content":"secret"},{"type":"textarea","left":"0","bottom":"83","fontsize":"10.0","color":[0,0,0,1],"fontfamily":"Open Sans","bold":false,"italic":false,"width":"80.00","downward":true,"content":"attendee_job_title","text":"Developer","align":"center"},{"type":"textarea","left":"0","bottom":"76","fontsize":"12.0","color":[0,0,0,1],"fontfamily":"Open Sans","bold":false,"italic":false,"width":"80","downward":true,"content":"attendee_company","text":"FOSSASIA","align":"center"}]'
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ <h1>{% trans "Badge layout" %}</h1>
{% csrf_token %}
{% bootstrap_form_errors form %}
{% bootstrap_field form.name layout="control" %}
{% bootstrap_field form.category layout="control" %}
<div class="form-group">
<label class="col-md-3 control-label">
{% trans "Badge design" %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ <h1>{% trans "Badges" %}</h1>
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Category" %}</th>
<th>{% trans "Default" %}</th>
<th class="action-col-2"></th>
<th class="action-col-3"></th>
</tr>
</thead>
<tbody>
Expand All @@ -55,6 +56,13 @@ <h1>{% trans "Badges" %}</h1>
<strong>{{ l.name }}</strong>
{% endif %}
</td>
<td>
{% if l.category %}
<span class="label label-info">{{ l.category.name }}</span>
{% else %}
<span class="text-muted">{% trans "All categories" %}</span>
{% endif %}
</td>
<td>
{% if l.default %}
<span class="text-success">
Expand All @@ -74,7 +82,14 @@ <h1>{% trans "Badges" %}</h1>
</td>
<td class="text-right flip">
{% if "can_change_event_settings" in request.eventpermset %}
<a href="{% url "plugins:badges:edit" organizer=request.event.organizer.slug event=request.event.slug layout=l.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "plugins:badges:update" organizer=request.event.organizer.slug event=request.event.slug layout=l.id %}"
class="btn btn-default btn-sm" title="{% trans "Edit name and category" %}" data-toggle="tooltip">
<i class="fa fa-pencil"></i>
</a>
<a href="{% url "plugins:badges:edit" organizer=request.event.organizer.slug event=request.event.slug layout=l.id %}"
class="btn btn-default btn-sm" title="{% trans "Visual editor" %}" data-toggle="tooltip">
<i class="fa fa-edit"></i>
</a>
<a href="{% url "plugins:badges:add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ l.id }}"
class="btn btn-default btn-sm" title="{% trans "Clone" %}" data-toggle="tooltip"><i class="fa fa-copy"></i></a>
<a href="{% url "plugins:badges:delete" organizer=request.event.organizer.slug event=request.event.slug layout=l.id %}" class="btn btn-delete btn-danger btn-sm"><i class="fa fa-trash"></i></a>
Expand Down
6 changes: 6 additions & 0 deletions src/pretix/plugins/badges/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
LayoutEditorView,
LayoutListView,
LayoutSetDefault,
LayoutUpdate,
OrderPrintDo,
)

Expand Down Expand Up @@ -43,6 +44,11 @@
LayoutDelete.as_view(),
name='delete',
),
url(
r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/badges/(?P<layout>\d+)/edit$',
LayoutUpdate.as_view(),
name='update',
),
url(
r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/badges/(?P<layout>\d+)/editor',
LayoutEditorView.as_view(),
Expand Down
45 changes: 44 additions & 1 deletion src/pretix/plugins/badges/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django.views import View
from django.views.generic import CreateView, DeleteView, DetailView, ListView
from django.views.generic import CreateView, DeleteView, DetailView, ListView, UpdateView
from reportlab.lib import pagesizes
from reportlab.pdfgen import canvas

Expand Down Expand Up @@ -99,6 +99,7 @@ def copy_from(self):

def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['event'] = self.request.event

if self.copy_from:
i = modelcopy(self.copy_from)
Expand All @@ -109,6 +110,48 @@ def get_form_kwargs(self):
return kwargs


class LayoutUpdate(BadgePluginEnabledMixin, EventPermissionRequiredMixin, UpdateView):
model = BadgeLayout
form_class = BadgeLayoutForm
template_name = 'pretixplugins/badges/edit.html'
permission = 'can_change_event_settings'
context_object_name = 'layout'

def get_object(self, queryset=None) -> BadgeLayout:
try:
return self.request.event.badge_layouts.get(id=self.kwargs['layout'])
except BadgeLayout.DoesNotExist as e:
raise Http404(_('The requested badge layout does not exist.')) from e

def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['event'] = self.request.event
return kwargs

@transaction.atomic
def form_valid(self, form):
messages.success(self.request, _('Your changes have been saved.'))
form.instance.log_action(
'pretix.plugins.badges.layout.changed',
user=self.request.user,
data=dict(form.cleaned_data),
)
return super().form_valid(form)

def form_invalid(self, form):
messages.error(self.request, _('We could not save your changes. See below for details.'))
return super().form_invalid(form)

def get_success_url(self):
return reverse(
'plugins:badges:index',
kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
},
)


class LayoutSetDefault(BadgePluginEnabledMixin, EventPermissionRequiredMixin, DetailView):
model = BadgeLayout
permission = 'can_change_event_settings'
Expand Down
20 changes: 20 additions & 0 deletions src/pretix/plugins/checkinlists/exporters.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from pretix.base.models import (
Checkin,
InvoiceAddress,
ItemCategory,
Order,
OrderPosition,
Question,
Expand Down Expand Up @@ -119,6 +120,16 @@ def _fields(self):
required=False,
),
),
(
'category',
forms.ModelChoiceField(
queryset=ItemCategory.objects.none(),
label=_('Category'),
required=False,
empty_label=_('All categories'),
help_text=_('Only include tickets for products in this category.'),
),
),
(
'questions',
forms.ModelMultipleChoiceField(
Expand Down Expand Up @@ -151,6 +162,12 @@ def _fields(self):
)
d['list'].widget.choices = d['list'].choices
d['list'].required = True

# Set up category queryset to show only categories used by products
d['category'].queryset = ItemCategory.objects.filter(
event=self.event,
items__isnull=False
).distinct()

return d

Expand Down Expand Up @@ -258,6 +275,9 @@ def _get_queryset(self, cl, form_data):
if form_data.get('attention_only'):
qs = qs.filter(Q(item__checkin_attention=True) | Q(order__checkin_attention=True))

if form_data.get('category'):
qs = qs.filter(item__category=form_data.get('category'))

if not cl.include_pending:
qs = qs.filter(order__status=Order.STATUS_PAID)
else:
Expand Down
Loading