diff --git a/Dockerfile b/Dockerfile index c106edecfc..2d8f848eb2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,6 +19,8 @@ RUN apt-get update && \ python3-dev \ sudo \ supervisor \ + libmaxminddb0 \ + libmaxminddb-dev \ zlib1g-dev && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* && \ diff --git a/doc/admin/config.rst b/doc/admin/config.rst index b34dbe1dc5..b7a635adf9 100644 --- a/doc/admin/config.rst +++ b/doc/admin/config.rst @@ -481,3 +481,18 @@ You can configure the maximum file size for uploading various files:: ; Max upload size for other files in MiB, defaults to 10 MiB ; This includes all file upload type order questions max_size_other = 100 + + +GeoIP +----- + +pretix can optionally make use of a GeoIP database for some features. It needs a file in ``mmdb`` format, for example +`GeoLite2`_ or `GeoAcumen`_:: + + [geoip] + path=/var/geoipdata/ + filename_country=GeoLite2-Country.mmdb + + +.. _GeoAcumen: https://github.com/geoacumen/geoacumen-country +.. _GeoLite2: https://dev.maxmind.com/geoip/geolite2-free-geolocation-data \ No newline at end of file diff --git a/src/pretix/base/forms/questions.py b/src/pretix/base/forms/questions.py index 00ce292ea5..42a22ee5b9 100644 --- a/src/pretix/base/forms/questions.py +++ b/src/pretix/base/forms/questions.py @@ -45,6 +45,7 @@ import pytz from django import forms from django.conf import settings from django.contrib import messages +from django.contrib.gis.geoip2 import GeoIP2 from django.core.exceptions import ValidationError from django.core.files.uploadedfile import SimpleUploadedFile from django.core.validators import ( @@ -91,6 +92,7 @@ from pretix.helpers.countries import ( CachedCountries, get_phone_prefixes_sorted_and_localized, ) from pretix.helpers.escapejson import escapejson_attr +from pretix.helpers.http import get_client_ip from pretix.helpers.i18n import get_format_without_seconds from pretix.presale.signals import question_form_fields @@ -351,6 +353,15 @@ class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget): return "" +def guess_country_from_request(request, event): + if settings.HAS_GEOIP: + g = GeoIP2() + res = g.country(get_client_ip(request)) + if res['country_code'] and len(res['country_code']) == 2: + return Country(res['country_code']) + return guess_country(event) + + def guess_country(event): # Try to guess the initial country from either the country of the merchant # or the locale. This will hopefully save at least some users some scrolling :) @@ -382,6 +393,12 @@ def guess_phone_prefix(event): return get_phone_prefix(country) +def guess_phone_prefix_from_request(request, event): + with language(get_babel_locale()): + country = str(guess_country_from_request(request, event)) + return get_phone_prefix(country) + + def get_phone_prefix(country): for prefix, values in _COUNTRY_CODE_TO_REGION_CODE.items(): if country in values: @@ -564,6 +581,7 @@ class BaseQuestionsForm(forms.Form): :param cartpos: The cart position the form should be for :param event: The event this belongs to """ + request = kwargs.pop('request', None) cartpos = self.cartpos = kwargs.pop('cartpos', None) orderpos = self.orderpos = kwargs.pop('orderpos', None) pos = cartpos or orderpos @@ -661,7 +679,7 @@ class BaseQuestionsForm(forms.Form): 'autocomplete': 'address-level2', }), ) - country = (cartpos.country if cartpos else orderpos.country) or guess_country(event) + country = (cartpos.country if cartpos else orderpos.country) or guess_country_from_request(request, event) add_fields['country'] = CountryField( countries=CachedCountries ).formfield( @@ -768,7 +786,7 @@ class BaseQuestionsForm(forms.Form): help_text=help_text, widget=forms.Select, empty_label=' ', - initial=initial.answer if initial else (guess_country(event) if required else None), + initial=initial.answer if initial else (guess_country_from_request(request, event) if required else None), ) elif q.type == Question.TYPE_CHOICE: field = forms.ModelChoiceField( @@ -856,7 +874,7 @@ class BaseQuestionsForm(forms.Form): initial = None if not initial: - phone_prefix = guess_phone_prefix(event) + phone_prefix = guess_phone_prefix_from_request(request, event) if phone_prefix: initial = "+{}.".format(phone_prefix) @@ -992,7 +1010,7 @@ class BaseInvoiceAddressForm(forms.ModelForm): kwargs.setdefault('initial', {}) if not kwargs.get('instance') or not kwargs['instance'].country: - kwargs['initial']['country'] = guess_country(self.event) + kwargs['initial']['country'] = guess_country_from_request(self.request, self.event) super().__init__(*args, **kwargs) if not event.settings.invoice_address_vatid: diff --git a/src/pretix/base/views/mixins.py b/src/pretix/base/views/mixins.py index 3bebd53588..41c986a44c 100644 --- a/src/pretix/base/views/mixins.py +++ b/src/pretix/base/views/mixins.py @@ -71,6 +71,7 @@ class BaseQuestionsViewMixin: kwargs = self.question_form_kwargs(cr) form = self.form_class(event=self.request.event, prefix=cr.id, + request=self.request, cartpos=cartpos, orderpos=orderpos, all_optional=self.all_optional, diff --git a/src/pretix/presale/forms/checkout.py b/src/pretix/presale/forms/checkout.py index f57f79919f..99c60dc38d 100644 --- a/src/pretix/presale/forms/checkout.py +++ b/src/pretix/presale/forms/checkout.py @@ -45,7 +45,7 @@ from phonenumber_field.formfields import PhoneNumberField from pretix.base.forms.questions import ( BaseInvoiceAddressForm, BaseQuestionsForm, WrappedPhoneNumberPrefixWidget, - guess_phone_prefix, + guess_phone_prefix_from_request, ) from pretix.base.templatetags.rich_text import rich_text from pretix.base.validators import EmailBanlistValidator @@ -73,7 +73,7 @@ class ContactForm(forms.Form): if self.event.settings.order_phone_asked: if not self.initial.get('phone'): - phone_prefix = guess_phone_prefix(self.event) + phone_prefix = guess_phone_prefix_from_request(self.request, self.event) if phone_prefix: # We now exploit an implementation detail in PhoneNumberPrefixWidget to allow us to pass just # a country code but no number as an initial value. It's a bit hacky, but should be stable for diff --git a/src/pretix/presale/forms/waitinglist.py b/src/pretix/presale/forms/waitinglist.py index df099076ff..fa25da6317 100644 --- a/src/pretix/presale/forms/waitinglist.py +++ b/src/pretix/presale/forms/waitinglist.py @@ -25,7 +25,8 @@ from django.utils.translation import gettext_lazy as _ from phonenumber_field.formfields import PhoneNumberField from pretix.base.forms.questions import ( - NamePartsFormField, WrappedPhoneNumberPrefixWidget, guess_phone_prefix, + NamePartsFormField, WrappedPhoneNumberPrefixWidget, + guess_phone_prefix_from_request, ) from pretix.base.models import Quota, WaitingListEntry from pretix.base.templatetags.rich_text import rich_text @@ -40,6 +41,7 @@ class WaitingListForm(forms.ModelForm): fields = ('name_parts', 'email', 'phone') def __init__(self, *args, **kwargs): + request = kwargs.pop('request') self.event = kwargs.pop('event') self.channel = kwargs.pop('channel') customer = kwargs.pop('customer') @@ -93,7 +95,7 @@ class WaitingListForm(forms.ModelForm): if event.settings.waiting_list_phones_asked: if not self.initial.get('phone'): - phone_prefix = guess_phone_prefix(event) + phone_prefix = guess_phone_prefix_from_request(request, event) if phone_prefix: self.initial['phone'] = "+{}.".format(phone_prefix) diff --git a/src/pretix/presale/views/waiting.py b/src/pretix/presale/views/waiting.py index c39e56eede..378211f97e 100644 --- a/src/pretix/presale/views/waiting.py +++ b/src/pretix/presale/views/waiting.py @@ -48,6 +48,7 @@ class WaitingView(EventViewMixin, FormView): def get_form_kwargs(self): kwargs = super().get_form_kwargs() + kwargs['request'] = self.request kwargs['event'] = self.request.event kwargs['instance'] = WaitingListEntry( event=self.request.event, locale=get_language_without_region(), diff --git a/src/pretix/settings.py b/src/pretix/settings.py index c341eb2c9a..6b76ec5878 100644 --- a/src/pretix/settings.py +++ b/src/pretix/settings.py @@ -330,6 +330,12 @@ ENTROPY = { 'giftcard_secret': config.getint('entropy', 'giftcard_secret', fallback=12), } +HAS_GEOIP = False +if config.has_option('geoip', 'path'): + HAS_GEOIP = True + GEOIP_PATH = config.get('geoip', 'path') + GEOIP_COUNTRY = config.get('geoip', 'filename_country', fallback='GeoLite2-Country.mmdb') + # Internal settings PRETIX_EMAIL_NONE_VALUE = 'none@well-known.pretix.eu' diff --git a/src/setup.cfg b/src/setup.cfg index 3277a615ac..ce53d78b7d 100644 --- a/src/setup.cfg +++ b/src/setup.cfg @@ -34,6 +34,7 @@ filterwarnings = ignore:django.contrib.staticfiles.templatetags.static:DeprecationWarning ignore::django.utils.deprecation.RemovedInDjango41Warning: ignore::DeprecationWarning:compressor + ignore:pkg_resources is deprecated as an API: [coverage:run] diff --git a/src/setup.py b/src/setup.py index 21441e0d79..ee219e9f78 100644 --- a/src/setup.py +++ b/src/setup.py @@ -191,6 +191,7 @@ setup( 'djangorestframework==3.14.*', 'dnspython==2.2.*', 'drf_ujson2==1.7.*', + 'geoip2==4.*', 'isoweek', 'jsonschema', 'kombu==5.2.*',