forked from CGM_Public/pretix_original
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>
This commit is contained in:
284
src/pretix/control/views/modelimport.py
Normal file
284
src/pretix/control/views/modelimport.py
Normal file
@@ -0,0 +1,284 @@
|
||||
#
|
||||
# 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,
|
||||
})
|
||||
Reference in New Issue
Block a user