Files
pretix_original/src/pretix/control/views/modelimport.py
Raphael Michel 990e9da21d Generalize import process from orders to more models (#4002)
* 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>
2024-04-03 10:15:30 +02:00

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,
})