mirror of
https://github.com/pretix/pretix.git
synced 2026-04-25 23:42:32 +00:00
* Generalize import process from orders to more models * Add voucher import * Model import: Guess assignments of based on column headers * Fix lock_seats being pointless * Update docs * Update doc/development/api/import.rst Co-authored-by: Richard Schreiber <schreiber@rami.io> * Update src/pretix/base/modelimport_vouchers.py Co-authored-by: Richard Schreiber <schreiber@rami.io> --------- Co-authored-by: Richard Schreiber <schreiber@rami.io>
285 lines
10 KiB
Python
285 lines
10 KiB
Python
#
|
|
# This file is part of pretix (Community Edition).
|
|
#
|
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
|
#
|
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
|
# this file, see <https://pretix.eu/about/en/license>.
|
|
#
|
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
# details.
|
|
#
|
|
# 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/>.
|
|
#
|
|
|
|
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
|
|
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
|
|
#
|
|
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
|
|
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
|
|
#
|
|
# This file contains Apache-licensed contributions copyrighted by: Alexander Schwartz
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
|
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations under the License.
|
|
|
|
import csv
|
|
import logging
|
|
from datetime import timedelta
|
|
|
|
from django.conf import settings
|
|
from django.contrib import messages
|
|
from django.shortcuts import get_object_or_404, redirect
|
|
from django.urls import reverse
|
|
from django.utils.functional import cached_property
|
|
from django.utils.timezone import now
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.views.generic import FormView, TemplateView
|
|
|
|
from pretix.base.models import CachedFile
|
|
from pretix.base.services.modelimport import (
|
|
import_orders, import_vouchers, parse_csv,
|
|
)
|
|
from pretix.base.views.tasks import AsyncAction
|
|
from pretix.control.forms.modelimport import (
|
|
OrdersProcessForm, VouchersProcessForm,
|
|
)
|
|
from pretix.control.permissions import EventPermissionRequiredMixin
|
|
from pretix.helpers.http import redirect_to_url
|
|
|
|
logger = logging.getLogger(__name__)
|
|
ENCODINGS = (
|
|
"utf8", "utf16", "utf32",
|
|
"iso-8859-1", "iso-8859-2", "iso-8859-3", "iso-8859-4", "iso-8859-5", "iso-8859-6", "iso-8859-7",
|
|
"iso-8859-8", "iso-8859-9", "iso-8859-10", "iso-8859-11", "iso-8859-12", "iso-8859-13", "iso-8859-14",
|
|
"iso-8859-15", "iso-8859-16",
|
|
"maccyrillic", "macgreek", "maciceland", "maclatin2", "macroman", "macturkish",
|
|
"windows-1250", "windows-1251", "windows-1252", "windows-1253", "windows-1254", "windows-1255",
|
|
"windows-1256", "windows-1257", "windows-1258"
|
|
)
|
|
|
|
|
|
class BaseImportView(TemplateView):
|
|
def post(self, request, *args, **kwargs):
|
|
if 'file' not in request.FILES:
|
|
return redirect_to_url(request.path)
|
|
if not request.FILES['file'].name.lower().endswith('.csv'):
|
|
messages.error(request, _('Please only upload CSV files.'))
|
|
return redirect_to_url(request.path)
|
|
if request.FILES['file'].size > settings.FILE_UPLOAD_MAX_SIZE_OTHER:
|
|
messages.error(request, _('Please do not upload files larger than 10 MB.'))
|
|
return redirect_to_url(request.path)
|
|
|
|
cf = CachedFile.objects.create(
|
|
expires=now() + timedelta(days=1),
|
|
date=now(),
|
|
filename='import.csv',
|
|
type='text/csv',
|
|
)
|
|
cf.file.save('import.csv', request.FILES['file'])
|
|
|
|
if self.request.POST.get("charset") in ENCODINGS:
|
|
charset = self.request.POST.get("charset")
|
|
else:
|
|
charset = "auto"
|
|
|
|
return redirect(self.get_process_url(request, cf, charset))
|
|
|
|
def get_context_data(self, **kwargs):
|
|
return super().get_context_data(encodings=ENCODINGS)
|
|
|
|
def get_process_url(self, request, cf, charset):
|
|
raise NotImplementedError() # noqa
|
|
|
|
|
|
class BaseProcessView(AsyncAction, FormView):
|
|
known_errortypes = ['DataImportError']
|
|
|
|
@property
|
|
def settings_key(self):
|
|
raise NotImplementedError() # noqa
|
|
|
|
@property
|
|
def settings_holder(self):
|
|
raise NotImplementedError() # noqa
|
|
|
|
def get_form_kwargs(self):
|
|
k = super().get_form_kwargs()
|
|
k.update({
|
|
'initial': self.settings_holder.settings.get(self.settings_key, as_type=dict),
|
|
'headers': self.parsed.fieldnames
|
|
})
|
|
return k
|
|
|
|
def form_valid(self, form):
|
|
self.settings_holder.settings.set(self.settings_key, form.cleaned_data)
|
|
if self.request.GET.get("charset") in ENCODINGS:
|
|
charset = self.request.GET.get("charset")
|
|
else:
|
|
charset = None
|
|
return self.do(
|
|
self.settings_holder.pk,
|
|
self.file.id,
|
|
form.cleaned_data,
|
|
self.request.LANGUAGE_CODE,
|
|
self.request.user.pk,
|
|
charset,
|
|
)
|
|
|
|
@cached_property
|
|
def file(self):
|
|
return get_object_or_404(CachedFile, pk=self.kwargs.get("file"), filename="import.csv")
|
|
|
|
@cached_property
|
|
def parsed(self):
|
|
if self.request.GET.get("charset") in ENCODINGS:
|
|
charset = self.request.GET.get("charset")
|
|
else:
|
|
charset = None
|
|
try:
|
|
return parse_csv(self.file.file, 1024 * 1024, charset=charset)
|
|
except UnicodeDecodeError:
|
|
messages.warning(
|
|
self.request,
|
|
_(
|
|
"We could not identify the character encoding of the CSV file. "
|
|
"Some characters were replaced with a placeholder."
|
|
)
|
|
)
|
|
return parse_csv(self.file.file, 1024 * 1024, "replace", charset=charset)
|
|
|
|
@cached_property
|
|
def parsed_list(self):
|
|
try:
|
|
return list(self.parsed)
|
|
except csv.Error:
|
|
logger.exception("Could not parse full CSV file")
|
|
return None
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
if 'async_id' in request.GET and settings.HAS_CELERY:
|
|
return self.get_result(request)
|
|
return FormView.get(self, request, *args, **kwargs)
|
|
|
|
def get_success_message(self, value):
|
|
return _('The import was successful.')
|
|
|
|
def get_success_url(self, value):
|
|
raise NotImplementedError() # noqa
|
|
|
|
def get_form_url(self):
|
|
raise NotImplementedError() # noqa
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
if 'async_id' in request.GET and settings.HAS_CELERY:
|
|
return self.get_result(request)
|
|
if not self.parsed or not self.parsed_list:
|
|
messages.error(request, _('We\'ve been unable to parse the uploaded file as a CSV file.'))
|
|
return redirect(self.get_form_url())
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_error_url(self):
|
|
return self.request.path
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
ctx['file'] = self.file
|
|
ctx['parsed'] = self.parsed
|
|
ctx['sample_rows'] = self.parsed_list[:3]
|
|
return ctx
|
|
|
|
|
|
class OrderImportView(EventPermissionRequiredMixin, BaseImportView):
|
|
template_name = 'pretixcontrol/orders/import_start.html'
|
|
permission = 'can_change_orders'
|
|
|
|
def get_process_url(self, request, cf, charset):
|
|
return reverse('control:event.orders.import.process', kwargs={
|
|
'event': request.event.slug,
|
|
'organizer': request.organizer.slug,
|
|
'file': cf.id
|
|
}) + "?charset=" + charset
|
|
|
|
|
|
class OrderProcessView(EventPermissionRequiredMixin, BaseProcessView):
|
|
permission = 'can_change_orders'
|
|
template_name = 'pretixcontrol/orders/import_process.html'
|
|
form_class = OrdersProcessForm
|
|
task = import_orders
|
|
settings_key = 'order_import_settings'
|
|
|
|
@property
|
|
def settings_holder(self):
|
|
return self.request.event
|
|
|
|
def get_form_kwargs(self):
|
|
k = super().get_form_kwargs()
|
|
k.update({
|
|
'event': self.request.event,
|
|
})
|
|
return k
|
|
|
|
def get_form_url(self):
|
|
return reverse('control:event.orders.import', kwargs={
|
|
'event': self.request.event.slug,
|
|
'organizer': self.request.organizer.slug,
|
|
})
|
|
|
|
def get_success_url(self, value):
|
|
return reverse('control:event.orders', kwargs={
|
|
'event': self.request.event.slug,
|
|
'organizer': self.request.organizer.slug,
|
|
})
|
|
|
|
|
|
class VoucherImportView(EventPermissionRequiredMixin, BaseImportView):
|
|
template_name = 'pretixcontrol/vouchers/import_start.html'
|
|
permission = 'can_change_vouchers'
|
|
|
|
def get_process_url(self, request, cf, charset):
|
|
return reverse('control:event.vouchers.import.process', kwargs={
|
|
'event': request.event.slug,
|
|
'organizer': request.organizer.slug,
|
|
'file': cf.id
|
|
}) + "?charset=" + charset
|
|
|
|
|
|
class VoucherProcessView(EventPermissionRequiredMixin, BaseProcessView):
|
|
permission = 'can_change_vouchers'
|
|
template_name = 'pretixcontrol/vouchers/import_process.html'
|
|
form_class = VouchersProcessForm
|
|
task = import_vouchers
|
|
settings_key = 'voucher_import_settings'
|
|
|
|
@property
|
|
def settings_holder(self):
|
|
return self.request.event
|
|
|
|
def get_form_kwargs(self):
|
|
k = super().get_form_kwargs()
|
|
k.update({
|
|
'event': self.request.event,
|
|
})
|
|
return k
|
|
|
|
def get_form_url(self):
|
|
return reverse('control:event.vouchers.import', kwargs={
|
|
'event': self.request.event.slug,
|
|
'organizer': self.request.organizer.slug,
|
|
})
|
|
|
|
def get_success_url(self, value):
|
|
return reverse('control:event.vouchers', kwargs={
|
|
'event': self.request.event.slug,
|
|
'organizer': self.request.organizer.slug,
|
|
})
|