trivial: apply black

This commit is contained in:
Frédéric Péters 2021-02-15 18:01:46 +01:00
parent bc9f6b9fba
commit 47d67c395e
238 changed files with 5927 additions and 3280 deletions

View File

@ -25,10 +25,11 @@ class AppConfig(django.apps.AppConfig):
def get_before_urls(self):
from . import urls
return urls.urlpatterns
def get_extra_manager_actions(self):
return [{'href': reverse('combo-manager-assets'),
'text': _('Assets')}]
return [{'href': reverse('combo-manager-assets'), 'text': _('Assets')}]
default_app_config = 'combo.apps.assets.AppConfig'

View File

@ -54,11 +54,10 @@ class Set(GenericAPIView):
data = serializer.validated_data
asset, created = Asset.objects.get_or_create(key=key)
asset.asset = File(
BytesIO(data['asset']['content']),
name=data['asset'].get('filename'))
asset.asset = File(BytesIO(data['asset']['content']), name=data['asset'].get('filename'))
asset.save()
response = {'err': 0}
return Response(response)
view_set = Set.as_view()

View File

@ -9,14 +9,16 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
]
dependencies = []
operations = [
migrations.CreateModel(
name='Asset',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
(
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('key', models.CharField(max_length=128, unique=True)),
('asset', models.FileField(upload_to='assets')),
],

View File

@ -19,6 +19,7 @@ import json
from django.core import serializers
from django.db import models
class AssetManager(models.Manager):
def get_by_natural_key(self, key):
return self.get(key=key)
@ -35,8 +36,11 @@ class Asset(models.Model):
return [x.get_as_serialized_object() for x in Asset.objects.all()]
def get_as_serialized_object(self):
serialized_asset = json.loads(serializers.serialize('json', [self],
use_natural_foreign_keys=True, use_natural_primary_keys=True))[0]
serialized_asset = json.loads(
serializers.serialize(
'json', [self], use_natural_foreign_keys=True, use_natural_primary_keys=True
)
)[0]
del serialized_asset['model']
del serialized_asset['pk']
return serialized_asset

View File

@ -35,11 +35,7 @@ assets_manager_urls = [
urlpatterns = [
url(r'^assets/(?P<key>[\w_:-]+)$', views.serve_asset),
url(r'^manage/assets/', decorated_includes(manager_required,
include(assets_manager_urls))),
url(r'^api/assets/set/(?P<key>[\w_:-]+)/$', api_views.view_set,
name='api-assets-set'),
url(r'^ajax/assets-export-size/$',
views.assets_export_size, name='combo-manager-assets-export-size'),
url(r'^manage/assets/', decorated_includes(manager_required, include(assets_manager_urls))),
url(r'^api/assets/set/(?P<key>[\w_:-]+)/$', api_views.view_set, name='api-assets-set'),
url(r'^ajax/assets-export-size/$', views.assets_export_size, name='combo-manager-assets-export-size'),
]

View File

@ -60,8 +60,7 @@ def tar_assets_files(tar):
media_prefix = default_storage.path('')
for basedir, dirnames, filenames in os.walk(media_prefix):
for filename in filenames:
tar.add(os.path.join(basedir, filename),
os.path.join(basedir, filename)[len(media_prefix):])
tar.add(os.path.join(basedir, filename), os.path.join(basedir, filename)[len(media_prefix) :])
export = {'assets': Asset.export_all_for_json()}
add_tar_content(tar, '_assets.json', json.dumps(export, indent=2))

View File

@ -62,8 +62,7 @@ class CkEditorAsset(object):
def thumb(self):
if getattr(settings, 'CKEDITOR_IMAGE_BACKEND', None):
thumb = ckeditor.utils.get_media_url(
ckeditor.utils.get_thumb_filename(self.filepath))
thumb = ckeditor.utils.get_media_url(ckeditor.utils.get_thumb_filename(self.filepath))
else:
thumb = self.src
return thumb
@ -104,18 +103,18 @@ class SlotAsset(object):
assets = dict([(x.key, x) for x in Asset.objects.all()])
uniq_slots = {}
uniq_slots.update(settings.COMBO_ASSET_SLOTS)
cells = CellBase.get_cells(select_related={
'__all__': ['page'],
'data_linkcell': ['link_page']})
cells = CellBase.get_cells(select_related={'__all__': ['page'], 'data_linkcell': ['link_page']})
for cell in cells:
uniq_slots.update(cell.get_asset_slots())
for map_layer in MapLayer.objects.filter(kind='geojson'):
uniq_slots.update(map_layer.get_asset_slots())
for key, value in uniq_slots.items():
yield cls(key,
name=value.get('label'),
asset_type=value.get('asset-type', 'image'),
asset=assets.get(key))
yield cls(
key,
name=value.get('label'),
asset_type=value.get('asset-type', 'image'),
asset=assets.get(key),
)
class Assets(ListView):
@ -171,13 +170,14 @@ class AssetUpload(FormView):
# use native ckeditor view so it's available from ckeditor file/image
# dialogs.
ckeditor_upload_view = ckeditor.views.ImageUploadView()
self.request.GET = {'CKEditorFuncNum': '-'} # hack
self.request.GET = {'CKEditorFuncNum': '-'} # hack
ckeditor_upload_view.post(self.request)
return super(AssetUpload, self).form_valid(form)
def get_success_url(self):
return Assets(request=self.request).get_anchored_url(name=self.request.FILES['upload'].name)
asset_upload = AssetUpload.as_view()
@ -189,7 +189,7 @@ class AssetOverwrite(FormView):
def form_valid(self, form):
img_orig = self.request.GET['img']
if '..' in img_orig:
raise PermissionDenied() # better safe than sorry
raise PermissionDenied() # better safe than sorry
base_path = settings.CKEDITOR_UPLOAD_PATH
if getattr(settings, 'CKEDITOR_RESTRICT_BY_USER', False):
base_path = os.path.join(base_path, self.request.user.username)
@ -204,8 +204,8 @@ class AssetOverwrite(FormView):
if ext_orig != ext_upload:
messages.error(
self.request,
_('You have to upload a file with the same extension (%(ext)s).')
% {'ext': ext_orig})
_('You have to upload a file with the same extension (%(ext)s).') % {'ext': ext_orig},
)
return super(AssetOverwrite, self).form_valid(form)
default_storage.delete(img_orig)
@ -214,7 +214,7 @@ class AssetOverwrite(FormView):
default_storage.delete(thumb)
saved_path = default_storage.save(img_orig, upload)
backend = ckeditor.image_processing.get_backend()
upload.seek(0) # rewind file to be sure
upload.seek(0) # rewind file to be sure
try:
backend.image_verify(upload)
except ckeditor.utils.NotAnImageException:
@ -228,6 +228,7 @@ class AssetOverwrite(FormView):
img_orig = self.request.GET['img']
return Assets(request=self.request).get_anchored_url(name=os.path.basename(img_orig))
asset_overwrite = AssetOverwrite.as_view()
@ -237,16 +238,14 @@ class AssetDelete(TemplateView):
def post(self, request):
img_orig = request.GET['img']
if '..' in img_orig:
raise PermissionDenied() # better safe than sorry
raise PermissionDenied() # better safe than sorry
base_path = settings.CKEDITOR_UPLOAD_PATH
if getattr(settings, 'CKEDITOR_RESTRICT_BY_USER', False):
base_path = os.path.join(base_path, request.user.username)
if not img_orig.startswith(base_path):
raise PermissionDenied()
default_storage.delete(img_orig)
return redirect(
Assets(request=self.request).get_anchored_url(
name=os.path.basename(img_orig)))
return redirect(Assets(request=self.request).get_anchored_url(name=os.path.basename(img_orig)))
asset_delete = AssetDelete.as_view()
@ -263,7 +262,8 @@ class SlotAssets(ListView):
key,
name=value.get('short_label'),
asset_type=value.get('asset-type', 'image'),
asset=assets.get(key))
asset=assets.get(key),
)
def get_queryset(self):
cell_reference = self.kwargs['cell_reference']
@ -305,7 +305,11 @@ class SlotAssetUpload(FormView):
except ObjectDoesNotExist:
pass
else:
return reverse('combo-manager-page-view', kwargs={'pk': cell.page_id}) + '#cell-' + cell_reference
return (
reverse('combo-manager-page-view', kwargs={'pk': cell.page_id})
+ '#cell-'
+ cell_reference
)
return Assets(request=self.request).get_anchored_url(key=self.kwargs['key'])
@ -326,7 +330,10 @@ class SlotAssetDelete(TemplateView):
pass
else:
return redirect(
reverse('combo-manager-page-view', kwargs={'pk': cell.page_id}) + '#cell-' + cell_reference)
reverse('combo-manager-page-view', kwargs={'pk': cell.page_id})
+ '#cell-'
+ cell_reference
)
return redirect(Assets(request=self.request).get_anchored_url(key=kwargs['key']))
@ -348,6 +355,7 @@ class AssetsImport(FormView):
messages.success(self.request, _('The assets file has been imported.'))
return super(AssetsImport, self).form_valid(form)
assets_import = AssetsImport.as_view()
@ -395,4 +403,5 @@ class AssetsExportSize(TemplateView):
context['size'] = computed_size
return context
assets_export_size = AssetsExportSize.as_view()

View File

@ -24,6 +24,7 @@ class AppConfig(django.apps.AppConfig):
def get_before_urls(self):
from . import urls
return urls.urlpatterns

View File

@ -24,12 +24,16 @@ from combo.apps.wcs.utils import get_wcs_options
class BookingCalendarForm(forms.ModelForm):
class Meta:
model = BookingCalendar
fields = (
'title', 'agenda_reference', 'formdef_reference',
'slot_duration', 'minimal_booking_duration', 'days_displayed')
'title',
'agenda_reference',
'formdef_reference',
'slot_duration',
'minimal_booking_duration',
'days_displayed',
)
def __init__(self, *args, **kwargs):
super(BookingCalendarForm, self).__init__(*args, **kwargs)
@ -40,7 +44,6 @@ class BookingCalendarForm(forms.ModelForm):
class BookingForm(forms.Form):
def __init__(self, *args, **kwargs):
self.cell = kwargs.pop('cell')
super(BookingForm, self).__init__(*args, **kwargs)

View File

@ -16,19 +16,44 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='BookingCalendar',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('placeholder', models.CharField(max_length=20)),
('order', models.PositiveIntegerField()),
('slug', models.SlugField(verbose_name='Slug', blank=True)),
('extra_css_class', models.CharField(max_length=100, verbose_name='Extra classes for CSS styling', blank=True)),
(
'extra_css_class',
models.CharField(
max_length=100, verbose_name='Extra classes for CSS styling', blank=True
),
),
('public', models.BooleanField(default=True, verbose_name='Public')),
('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
(
'restricted_to_unlogged',
models.BooleanField(default=False, verbose_name='Restrict to unlogged users'),
),
('last_update_timestamp', models.DateTimeField(auto_now=True)),
('title', models.CharField(max_length=128, null=True, verbose_name='Title', blank=True)),
('agenda_reference', models.CharField(max_length=128, verbose_name='Agenda')),
('formdef_reference', models.CharField(max_length=128, verbose_name='Form')),
('slot_duration', models.DurationField(default=datetime.timedelta(0, 1800), help_text='Format is hours:minutes:seconds', verbose_name='Slot duration')),
('minimal_booking_duration', models.DurationField(default=datetime.timedelta(0, 3600), help_text='Format is hours:minutes:seconds', verbose_name='Minimal booking duration')),
(
'slot_duration',
models.DurationField(
default=datetime.timedelta(0, 1800),
help_text='Format is hours:minutes:seconds',
verbose_name='Slot duration',
),
),
(
'minimal_booking_duration',
models.DurationField(
default=datetime.timedelta(0, 3600),
help_text='Format is hours:minutes:seconds',
verbose_name='Minimal booking duration',
),
),
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
('page', models.ForeignKey(to='data.Page', on_delete=models.CASCADE)),
],

View File

@ -23,8 +23,7 @@ from django.utils.translation import ugettext_lazy as _
from combo.data.models import CellBase
from combo.data.library import register_cell_class
from .utils import (is_chrono_enabled, is_wcs_enabled,
get_chrono_events, get_calendar_context_vars)
from .utils import is_chrono_enabled, is_wcs_enabled, get_chrono_events, get_calendar_context_vars
@register_cell_class
@ -34,11 +33,15 @@ class BookingCalendar(CellBase):
agenda_reference = models.CharField(_('Agenda'), max_length=128)
formdef_reference = models.CharField(_('Form'), max_length=128)
slot_duration = models.DurationField(
_('Slot duration'), default=datetime.timedelta(minutes=30),
help_text=_('Format is hours:minutes:seconds'))
_('Slot duration'),
default=datetime.timedelta(minutes=30),
help_text=_('Format is hours:minutes:seconds'),
)
minimal_booking_duration = models.DurationField(
_('Minimal booking duration'), default=datetime.timedelta(hours=1),
help_text=_('Format is hours:minutes:seconds'))
_('Minimal booking duration'),
default=datetime.timedelta(hours=1),
help_text=_('Format is hours:minutes:seconds'),
)
days_displayed = models.PositiveSmallIntegerField(_('Number of days to display'), default=7)
template_name = 'calendar/booking_calendar_cell.html'
@ -48,6 +51,7 @@ class BookingCalendar(CellBase):
def get_default_form_class(self):
from .forms import BookingCalendarForm
return BookingCalendarForm
@classmethod
@ -55,14 +59,18 @@ class BookingCalendar(CellBase):
return settings.BOOKING_CALENDAR_CELL_ENABLED and is_chrono_enabled() and is_wcs_enabled()
def is_visible(self, **kwargs):
return self.agenda_reference and self.formdef_reference \
return (
self.agenda_reference
and self.formdef_reference
and super(BookingCalendar, self).is_visible(**kwargs)
)
def get_cell_extra_context(self, context):
if context.get('placeholder_search_mode'):
return {}
extra_context = super(BookingCalendar, self).get_cell_extra_context(context)
events_data = get_chrono_events(self.agenda_reference, not(context.get('synchronous')))
extra_context.update(get_calendar_context_vars(
context['request'], extra_context['cell'], events_data))
events_data = get_chrono_events(self.agenda_reference, not (context.get('synchronous')))
extra_context.update(
get_calendar_context_vars(context['request'], extra_context['cell'], events_data)
)
return extra_context

View File

@ -20,5 +20,9 @@ from .views import BookingView, CalendarContentAjaxView
urlpatterns = [
url(r'^calendar/book/(?P<pk>[\w,-]+)/$', BookingView.as_view(), name='calendar-booking'),
url(r'^ajax/calendar/content/(?P<pk>\w+)/$', CalendarContentAjaxView.as_view(), name='ajax-calendar-content'),
url(
r'^ajax/calendar/content/(?P<pk>\w+)/$',
CalendarContentAjaxView.as_view(),
name='ajax-calendar-content',
),
]

View File

@ -62,16 +62,19 @@ def get_agendas():
except ValueError:
return references
for agenda in result.get('data'):
references.append((
'%s:%s' % (chrono['slug'], agenda['id']), agenda['text']))
references.append(('%s:%s' % (chrono['slug'], agenda['id']), agenda['text']))
return references
def get_chrono_events(agenda_reference, synchronous):
chrono_key, chrono_slug = agenda_reference.split(':')
chrono = get_chrono_service()
response = requests.get('api/agenda/%s/datetimes/' % chrono_slug, remote_service=chrono,
without_user=True, raise_if_not_cached=synchronous)
response = requests.get(
'api/agenda/%s/datetimes/' % chrono_slug,
remote_service=chrono,
without_user=True,
raise_if_not_cached=synchronous,
)
try:
if response.status_code != 200:
raise ValueError
@ -86,8 +89,7 @@ def get_calendar_context_vars(request, cell, events_data):
if 'error' in events_data:
return events_data
events = events_data['data']
calendar = get_calendar(events, cell.slot_duration, cell.days_displayed,
cell.minimal_booking_duration)
calendar = get_calendar(events, cell.slot_duration, cell.days_displayed, cell.minimal_booking_duration)
paginator = Paginator(calendar.get_computed_days(), cell.days_displayed)
try:
cal_page = paginator.page(page)
@ -95,11 +97,7 @@ def get_calendar_context_vars(request, cell, events_data):
cal_page = paginator.page(1)
except (EmptyPage,):
cal_page = paginator.page(paginator.num_pages)
return {
'calendar': calendar,
'calendar_days': cal_page,
'calendar_slots': calendar.get_slots()
}
return {'calendar': calendar, 'calendar_days': cal_page, 'calendar_slots': calendar.get_slots()}
def get_calendar(events, offset, days_displayed, min_duration):
@ -113,8 +111,7 @@ def get_calendar(events, offset, days_displayed, min_duration):
else:
day = calendar.get_day(event_datetime.date())
# add slots to day
day.add_slots(DaySlot(
event_datetime, True if not event.get('disabled', True) else False))
day.add_slots(DaySlot(event_datetime, True if not event.get('disabled', True) else False))
return calendar
@ -123,7 +120,7 @@ def get_form_url_with_params(cell, data):
session_vars = {
"session_var_booking_agenda_slug": cell.agenda_reference.split(':')[1],
"session_var_booking_start": data['start'].isoformat(),
"session_var_booking_end": data['end'].isoformat()
"session_var_booking_end": data['end'].isoformat(),
}
wcs_key, wcs_slug = cell.formdef_reference.split(':')
wcs = get_wcs_services().get(wcs_key)
@ -132,7 +129,6 @@ def get_form_url_with_params(cell, data):
class DaySlot(object):
def __init__(self, date_time, available, exist=True):
self.date_time = localtime(make_aware(date_time))
self.available = available
@ -147,7 +143,6 @@ class DaySlot(object):
class WeekDay(object):
def __init__(self, date):
self.date = date
self.slots = []
@ -174,7 +169,6 @@ class WeekDay(object):
class Calendar(object):
def __init__(self, offset, days_displayed, min_duration):
self.offset = offset
self.days_displayed = days_displayed
@ -192,7 +186,7 @@ class Calendar(object):
for day in self.days:
slots = day.slots
for idx in range(len(slots) - required_contiguous_slots):
if all([x.available for x in slots[idx:idx+required_contiguous_slots]]):
if all([x.available for x in slots[idx : idx + required_contiguous_slots]]):
return slots[idx]
return None
@ -201,8 +195,7 @@ class Calendar(object):
end = self.get_maximum_slot()
while start <= end:
yield start
start = datetime.datetime.combine(
datetime.date.today(), start) + self.offset
start = datetime.datetime.combine(datetime.date.today(), start) + self.offset
start = start.time()
def get_computed_days(self):

View File

@ -22,8 +22,7 @@ from django.views.generic.detail import SingleObjectMixin
from .forms import BookingForm
from .models import BookingCalendar
from .utils import (get_form_url_with_params, get_chrono_events,
get_calendar_context_vars)
from .utils import get_form_url_with_params, get_chrono_events, get_calendar_context_vars
class BookingView(SingleObjectMixin, View):
@ -38,8 +37,7 @@ class BookingView(SingleObjectMixin, View):
form.is_valid()
except ValueError as exc:
messages.error(request, force_text(exc))
redirect_url = '%s?%s' % (
cell.page.get_online_url(), request.GET.urlencode())
redirect_url = '%s?%s' % (cell.page.get_online_url(), request.GET.urlencode())
return HttpResponseRedirect(redirect_url)
data = form.cleaned_data
url = get_form_url_with_params(cell, data)

View File

@ -15,12 +15,14 @@ import django.apps
from django.utils.timezone import now, timedelta
from django.utils.translation import ugettext_lazy as _
class AppConfig(django.apps.AppConfig):
name = 'combo.apps.dashboard'
verbose_name = _('Dashboard')
def get_before_urls(self):
from . import urls
return urls.urlpatterns
def hourly(self):
@ -28,8 +30,10 @@ class AppConfig(django.apps.AppConfig):
def clean_autotiles(self):
from combo.data.models import ConfigJsonCell
ConfigJsonCell.objects.filter(placeholder='_auto_tile',
last_update_timestamp__lte=now() - timedelta(days=2)).delete()
ConfigJsonCell.objects.filter(
placeholder='_auto_tile', last_update_timestamp__lte=now() - timedelta(days=2)
).delete()
default_app_config = 'combo.apps.dashboard.AppConfig'

View File

@ -18,13 +18,24 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='DashboardCell',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('placeholder', models.CharField(max_length=20)),
('order', models.PositiveIntegerField()),
('slug', models.SlugField(verbose_name='Slug', blank=True)),
('extra_css_class', models.CharField(max_length=100, verbose_name='Extra classes for CSS styling', blank=True)),
(
'extra_css_class',
models.CharField(
max_length=100, verbose_name='Extra classes for CSS styling', blank=True
),
),
('public', models.BooleanField(default=True, verbose_name='Public')),
('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
(
'restricted_to_unlogged',
models.BooleanField(default=False, verbose_name='Restrict to unlogged users'),
),
('last_update_timestamp', models.DateTimeField(auto_now=True)),
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
('page', models.ForeignKey(to='data.Page', on_delete=models.CASCADE)),
@ -36,7 +47,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Tile',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('cell_pk', models.PositiveIntegerField()),
('order', models.PositiveIntegerField()),
('cell_type', models.ForeignKey(to='contenttypes.ContentType', on_delete=models.CASCADE)),

View File

@ -49,8 +49,10 @@ class DashboardCell(CellBase):
def render(self, context):
tiles = Tile.objects.filter(dashboard=self, user=context['user'])
validity_info_dict = {(x.content_type_id, x.object_id): True for x in
ValidityInfo.objects.filter(invalid_since__lt=now() - datetime.timedelta(days=2))}
validity_info_dict = {
(x.content_type_id, x.object_id): True
for x in ValidityInfo.objects.filter(invalid_since__lt=now() - datetime.timedelta(days=2))
}
context['tiles'] = [x for x in tiles if (x.cell_type_id, x.cell_pk) not in validity_info_dict]
return super(DashboardCell, self).render(context)

View File

@ -21,17 +21,26 @@ from ..models import Tile
register = template.Library()
def get_cell_data(cell):
# return a dictionary with cell parameters relevant for tile comparison
if cell is None:
return {}
cell_data = serializers.serialize('python', [cell])[0]
del cell_data['pk']
for key in ('restricted_to_unlogged', 'groups', 'last_update_timestamp',
'order', 'placeholder', 'public', 'page'):
for key in (
'restricted_to_unlogged',
'groups',
'last_update_timestamp',
'order',
'placeholder',
'public',
'page',
):
del cell_data['fields'][key]
return cell_data
@register.filter
def as_dashboard_cell(cell, user):
cell_data = get_cell_data(cell)

View File

@ -19,16 +19,24 @@ from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^api/dashboard/add/(?P<cell_reference>[\w_-]+)/$',
url(
r'^api/dashboard/add/(?P<cell_reference>[\w_-]+)/$',
views.dashboard_add_tile,
name='combo-dashboard-add-tile'),
url(r'^api/dashboard/remove/(?P<cell_reference>[\w_-]+)/$',
name='combo-dashboard-add-tile',
),
url(
r'^api/dashboard/remove/(?P<cell_reference>[\w_-]+)/$',
views.dashboard_remove_tile,
name='combo-dashboard-remove-tile'),
url(r'^api/dashboard/auto-tile/(?P<key>[\w_-]+)/$',
name='combo-dashboard-remove-tile',
),
url(
r'^api/dashboard/auto-tile/(?P<key>[\w_-]+)/$',
views.dashboard_auto_tile,
name='combo-dashboard-auto-tile'),
url(r'^api/dashboard/reorder/(?P<dashboard_id>[\w]+)/$',
name='combo-dashboard-auto-tile',
),
url(
r'^api/dashboard/reorder/(?P<dashboard_id>[\w]+)/$',
views.dashboard_reorder_tiles,
name='combo-dashboard-reorder-tiles'),
name='combo-dashboard-reorder-tiles',
),
]

View File

@ -21,7 +21,13 @@ from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
from django.urls import reverse
from django.db.models import Max, Min
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect, HttpResponseNotAllowed
from django.http import (
Http404,
HttpResponse,
HttpResponseBadRequest,
HttpResponseRedirect,
HttpResponseNotAllowed,
)
from django.utils.encoding import force_text
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import View
@ -39,11 +45,9 @@ def dashboard_success(request, dashboard, cell_data):
if request.is_ajax():
return HttpResponse(
json.dumps({
'err': 0,
'url': request.build_absolute_uri(dashboard_url),
'cell_data': cell_data}),
content_type='application/json')
json.dumps({'err': 0, 'url': request.build_absolute_uri(dashboard_url), 'cell_data': cell_data}),
content_type='application/json',
)
return HttpResponseRedirect(dashboard_url)
@ -64,25 +68,31 @@ class DashboardAddTileView(View):
cell.placeholder = '_dashboard'
cell.save()
tile = Tile(dashboard=dashboard,
cell=cell,
user=request.user,
order=0)
tile = Tile(dashboard=dashboard, cell=cell, user=request.user, order=0)
if settings.COMBO_DASHBOARD_NEW_TILE_POSITION == 'first':
order = Tile.objects.filter(dashboard=dashboard, user=request.user).aggregate(Min('order')).get('order__min')
order = (
Tile.objects.filter(dashboard=dashboard, user=request.user)
.aggregate(Min('order'))
.get('order__min')
)
tile.order = order - 1 if order is not None else 0
elif settings.COMBO_DASHBOARD_NEW_TILE_POSITION == 'last':
order = Tile.objects.filter(dashboard=dashboard, user=request.user).aggregate(Max('order')).get('order_max')
order = (
Tile.objects.filter(dashboard=dashboard, user=request.user)
.aggregate(Max('order'))
.get('order_max')
)
tile.order = order + 1 if order is not None else 0
tile.save()
cell_data = get_cell_data(cell)
cell_data['remove_url'] = reverse(
'combo-dashboard-remove-tile',
kwargs={'cell_reference': cell.get_reference()})
'combo-dashboard-remove-tile', kwargs={'cell_reference': cell.get_reference()}
)
return dashboard_success(request, dashboard, cell_data)
dashboard_add_tile = DashboardAddTileView.as_view()
@ -101,11 +111,12 @@ class DashboardRemoveTileView(View):
# do not remove cell so it can directly be added back
cell_data['add_url'] = reverse(
'combo-dashboard-add-tile',
kwargs={'cell_reference': cell.get_reference()})
'combo-dashboard-add-tile', kwargs={'cell_reference': cell.get_reference()}
)
return dashboard_success(request, dashboard, cell_data)
dashboard_remove_tile = DashboardRemoveTileView.as_view()
@ -120,8 +131,7 @@ def dashboard_auto_tile(request, *args, **kwargs):
return HttpResponseBadRequest('bad json request: "%s"' % request.body)
dashboard = DashboardCell.objects.filter(page__snapshot__isnull=True)[0]
cell = ConfigJsonCell(key=kwargs.get('key'), order=1,
page_id=dashboard.page_id, placeholder='_auto_tile')
cell = ConfigJsonCell(key=kwargs.get('key'), order=1, page_id=dashboard.page_id, placeholder='_auto_tile')
# only keep parameters that are actually defined for this cell type.
cell.parameters = {}
@ -137,8 +147,8 @@ def dashboard_auto_tile(request, *args, **kwargs):
response = render_cell(request, cell=cell)
response['x-add-to-dashboard-url'] = reverse(
'combo-dashboard-add-tile',
kwargs={'cell_reference': cell.get_reference()})
'combo-dashboard-add-tile', kwargs={'cell_reference': cell.get_reference()}
)
return response

View File

@ -29,6 +29,7 @@ class AppConfig(django.apps.AppConfig):
def get_before_urls(self):
from . import urls
return urls.urlpatterns
def hourly(self):
@ -36,6 +37,7 @@ class AppConfig(django.apps.AppConfig):
def update_available_statistics(self):
from .models import Statistic, ChartNgCell
if not settings.KNOWN_SERVICES:
return
@ -72,7 +74,7 @@ class AppConfig(django.apps.AppConfig):
'site_title': site_title,
'filters': stat.get('filters', []),
'available': True,
}
},
)
Statistic.objects.filter(last_update__lt=start_update).update(available=False)

View File

@ -34,9 +34,12 @@ class ChartForm(forms.ModelForm):
super(ChartForm, self).__init__(*args, **kwargs)
available_charts = []
for site_key, site_dict in settings.KNOWN_SERVICES.get('bijoe').items():
result = requests.get('/visualization/json/',
remote_service=site_dict, without_user=True,
headers={'accept': 'application/json'}).json()
result = requests.get(
'/visualization/json/',
remote_service=site_dict,
without_user=True,
headers={'accept': 'application/json'},
).json()
available_charts.extend([(x['path'], x['name']) for x in result])
available_charts.sort(key=lambda x: x[1])
self.fields['url'].widget = forms.Select(choices=available_charts)
@ -47,8 +50,17 @@ class ChartNgForm(forms.ModelForm):
class Meta:
model = ChartNgCell
fields = ('title', 'statistic', 'time_range','time_range_start', 'time_range_end', 'chart_type',
'height', 'sort_order', 'hide_null_values')
fields = (
'title',
'statistic',
'time_range',
'time_range_start',
'time_range_end',
'chart_type',
'height',
'sort_order',
'hide_null_values',
)
widgets = {
'time_range_start': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
'time_range_end': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),

View File

@ -15,15 +15,24 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Gauge',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('placeholder', models.CharField(max_length=20)),
('order', models.PositiveIntegerField()),
('slug', models.SlugField(verbose_name='Slug', blank=True)),
('public', models.BooleanField(default=True, verbose_name='Public')),
('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
(
'restricted_to_unlogged',
models.BooleanField(default=False, verbose_name='Restrict to unlogged users'),
),
('title', models.CharField(max_length=150, null=True, verbose_name='Title', blank=True)),
('url', models.CharField(max_length=150, null=True, verbose_name='URL', blank=True)),
('data_source', models.CharField(max_length=150, null=True, verbose_name='Data Source', blank=True)),
(
'data_source',
models.CharField(max_length=150, null=True, verbose_name='Data Source', blank=True),
),
('max_value', models.PositiveIntegerField(null=True, verbose_name='Max Value', blank=True)),
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
('page', models.ForeignKey(to='data.Page', on_delete=models.CASCADE)),

View File

@ -16,19 +16,37 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='CubesBarChart',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('placeholder', models.CharField(max_length=20)),
('order', models.PositiveIntegerField()),
('slug', models.SlugField(verbose_name='Slug', blank=True)),
('public', models.BooleanField(default=True, verbose_name='Public')),
('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
(
'restricted_to_unlogged',
models.BooleanField(default=False, verbose_name='Restrict to unlogged users'),
),
('title', models.CharField(max_length=150, null=True, verbose_name='Title', blank=True)),
('url', models.URLField(max_length=150, null=True, verbose_name='URL', blank=True)),
('cube', models.CharField(max_length=256, null=True, verbose_name='Cube', blank=True)),
('aggregate1', models.CharField(max_length=64, null=True, verbose_name='Aggregate', blank=True)),
('drilldown1', models.CharField(max_length=64, null=True, verbose_name='Drilldown 1', blank=True)),
('drilldown2', models.CharField(max_length=64, null=True, verbose_name='Drilldown 2', blank=True)),
('other_parameters', models.TextField(null=True, verbose_name='Other parameters', blank=True)),
(
'aggregate1',
models.CharField(max_length=64, null=True, verbose_name='Aggregate', blank=True),
),
(
'drilldown1',
models.CharField(max_length=64, null=True, verbose_name='Drilldown 1', blank=True),
),
(
'drilldown2',
models.CharField(max_length=64, null=True, verbose_name='Drilldown 2', blank=True),
),
(
'other_parameters',
models.TextField(null=True, verbose_name='Other parameters', blank=True),
),
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
('page', models.ForeignKey(to='data.Page', on_delete=models.CASCADE)),
],
@ -40,19 +58,37 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='CubesTable',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('placeholder', models.CharField(max_length=20)),
('order', models.PositiveIntegerField()),
('slug', models.SlugField(verbose_name='Slug', blank=True)),
('public', models.BooleanField(default=True, verbose_name='Public')),
('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
(
'restricted_to_unlogged',
models.BooleanField(default=False, verbose_name='Restrict to unlogged users'),
),
('title', models.CharField(max_length=150, null=True, verbose_name='Title', blank=True)),
('url', models.URLField(max_length=150, null=True, verbose_name='URL', blank=True)),
('cube', models.CharField(max_length=256, null=True, verbose_name='Cube', blank=True)),
('aggregate1', models.CharField(max_length=64, null=True, verbose_name='Aggregate', blank=True)),
('drilldown1', models.CharField(max_length=64, null=True, verbose_name='Drilldown 1', blank=True)),
('drilldown2', models.CharField(max_length=64, null=True, verbose_name='Drilldown 2', blank=True)),
('other_parameters', models.TextField(null=True, verbose_name='Other parameters', blank=True)),
(
'aggregate1',
models.CharField(max_length=64, null=True, verbose_name='Aggregate', blank=True),
),
(
'drilldown1',
models.CharField(max_length=64, null=True, verbose_name='Drilldown 1', blank=True),
),
(
'drilldown2',
models.CharField(max_length=64, null=True, verbose_name='Drilldown 2', blank=True),
),
(
'other_parameters',
models.TextField(null=True, verbose_name='Other parameters', blank=True),
),
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
('page', models.ForeignKey(to='data.Page', on_delete=models.CASCADE)),
],

View File

@ -15,13 +15,24 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='ChartCell',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('placeholder', models.CharField(max_length=20)),
('order', models.PositiveIntegerField()),
('slug', models.SlugField(verbose_name='Slug', blank=True)),
('extra_css_class', models.CharField(max_length=100, verbose_name='Extra classes for CSS styling', blank=True)),
(
'extra_css_class',
models.CharField(
max_length=100, verbose_name='Extra classes for CSS styling', blank=True
),
),
('public', models.BooleanField(default=True, verbose_name='Public')),
('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
(
'restricted_to_unlogged',
models.BooleanField(default=False, verbose_name='Restrict to unlogged users'),
),
('title', models.CharField(max_length=150, null=True, verbose_name='Title', blank=True)),
('url', models.URLField(max_length=150, null=True, verbose_name='URL', blank=True)),
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),

View File

@ -18,19 +18,58 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='ChartNgCell',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
(
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('placeholder', models.CharField(max_length=20)),
('order', models.PositiveIntegerField()),
('slug', models.SlugField(blank=True, verbose_name='Slug')),
('extra_css_class', models.CharField(blank=True, max_length=100, verbose_name='Extra classes for CSS styling')),
(
'extra_css_class',
models.CharField(
blank=True, max_length=100, verbose_name='Extra classes for CSS styling'
),
),
('public', models.BooleanField(default=True, verbose_name='Public')),
('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
(
'restricted_to_unlogged',
models.BooleanField(default=False, verbose_name='Restrict to unlogged users'),
),
('last_update_timestamp', models.DateTimeField(auto_now=True)),
('data_reference', models.CharField(max_length=150, verbose_name='Data')),
('title', models.CharField(blank=True, max_length=150, verbose_name='Title')),
('cached_json', jsonfield.fields.JSONField(blank=True, default=dict)),
('chart_type', models.CharField(choices=[(b'bar', 'Bar'), (b'horizontal-bar', 'Horizontal Bar'), (b'stacked-bar', 'Stacked Bar'), (b'line', 'Line'), (b'pie', 'Pie'), (b'dot', 'Dot'), (b'table', 'Table')], default=b'bar', max_length=20, verbose_name='Chart Type')),
('height', models.CharField(choices=[(b'150', 'Short (150px)'), (b'250', 'Average (250px)'), (b'350', 'Tall (350px)')], default=b'250', max_length=20, verbose_name='Height')),
(
'chart_type',
models.CharField(
choices=[
(b'bar', 'Bar'),
(b'horizontal-bar', 'Horizontal Bar'),
(b'stacked-bar', 'Stacked Bar'),
(b'line', 'Line'),
(b'pie', 'Pie'),
(b'dot', 'Dot'),
(b'table', 'Table'),
],
default=b'bar',
max_length=20,
verbose_name='Chart Type',
),
),
(
'height',
models.CharField(
choices=[
(b'150', 'Short (150px)'),
(b'250', 'Average (250px)'),
(b'350', 'Tall (350px)'),
],
default=b'250',
max_length=20,
verbose_name='Height',
),
),
('groups', models.ManyToManyField(blank=True, to='auth.Group', verbose_name='Groups')),
('page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='data.Page')),
],

View File

@ -15,21 +15,54 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='chartngcell',
name='hide_null_values',
field=models.BooleanField(default=False, help_text='This setting only applies for one-dimensional charts.', verbose_name='Hide null values'),
field=models.BooleanField(
default=False,
help_text='This setting only applies for one-dimensional charts.',
verbose_name='Hide null values',
),
),
migrations.AddField(
model_name='chartngcell',
name='sort_order',
field=models.CharField(choices=[('none', 'None'), ('alpha', 'Alphabetically'), ('asc', 'Increasing values'), ('desc', 'Decreasing values')], default='none', help_text='This setting only applies for one-dimensional charts.', max_length=5, verbose_name='Sort data'),
field=models.CharField(
choices=[
('none', 'None'),
('alpha', 'Alphabetically'),
('asc', 'Increasing values'),
('desc', 'Decreasing values'),
],
default='none',
help_text='This setting only applies for one-dimensional charts.',
max_length=5,
verbose_name='Sort data',
),
),
migrations.AlterField(
model_name='chartngcell',
name='chart_type',
field=models.CharField(choices=[('bar', 'Bar'), ('horizontal-bar', 'Horizontal Bar'), ('stacked-bar', 'Stacked Bar'), ('line', 'Line'), ('pie', 'Pie'), ('dot', 'Dot'), ('table', 'Table')], default='bar', max_length=20, verbose_name='Chart Type'),
field=models.CharField(
choices=[
('bar', 'Bar'),
('horizontal-bar', 'Horizontal Bar'),
('stacked-bar', 'Stacked Bar'),
('line', 'Line'),
('pie', 'Pie'),
('dot', 'Dot'),
('table', 'Table'),
],
default='bar',
max_length=20,
verbose_name='Chart Type',
),
),
migrations.AlterField(
model_name='chartngcell',
name='height',
field=models.CharField(choices=[('150', 'Short (150px)'), ('250', 'Average (250px)'), ('350', 'Tall (350px)')], default='250', max_length=20, verbose_name='Height'),
field=models.CharField(
choices=[('150', 'Short (150px)'), ('250', 'Average (250px)'), ('350', 'Tall (350px)')],
default='250',
max_length=20,
verbose_name='Height',
),
),
]

View File

@ -16,7 +16,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Statistic',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
(
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('slug', models.SlugField(max_length=256, verbose_name='Slug')),
('label', models.CharField(max_length=256, verbose_name='Label')),
('site_slug', models.SlugField(max_length=256, verbose_name='Site slug')),
@ -37,6 +40,12 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='chartngcell',
name='statistic',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cells', to='dataviz.Statistic', verbose_name='Data'),
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='cells',
to='dataviz.Statistic',
verbose_name='Data',
),
),
]

View File

@ -23,7 +23,7 @@ def update_cells(apps, schema_editor):
'label': cell.cached_json['name'],
'url': cell.cached_json['data-url'],
'site_title': bijoe_sites.get(site_slug, {}).get('title'),
}
},
)
cell.statistic = statistic
cell.save()

View File

@ -15,7 +15,18 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='chartngcell',
name='time_range',
field=models.CharField(blank=True, choices=[('current-year', 'Current year'), ('previous-year', 'Previous year'), ('current-month', 'Current month'), ('previous-month', 'Previous month'), ('range', 'Free range')], max_length=20, verbose_name='Filtering (time)'),
field=models.CharField(
blank=True,
choices=[
('current-year', 'Current year'),
('previous-year', 'Previous year'),
('current-month', 'Current month'),
('previous-month', 'Previous month'),
('range', 'Free range'),
],
max_length=20,
verbose_name='Filtering (time)',
),
),
migrations.AddField(
model_name='chartngcell',

View File

@ -67,13 +67,14 @@ class Gauge(CellBase):
data_source_url = get_templated_url(self.data_source)
else:
data_source_url = reverse('combo-ajax-gauge-count', kwargs={'cell': self.id})
return {'cell': self,
'title': self.title,
'url': get_templated_url(self.url) if self.url else None,
'max_value': self.max_value,
'data_source_url': data_source_url,
'jsonp': self.jsonp_data_source,
}
return {
'cell': self,
'title': self.title,
'url': get_templated_url(self.url) if self.url else None,
'max_value': self.max_value,
'data_source_url': data_source_url,
'jsonp': self.jsonp_data_source,
}
@register_cell_class
@ -88,10 +89,15 @@ class ChartCell(CellBase):
@classmethod
def is_enabled(self):
return settings.LEGACY_CHART_CELL_ENABLED and hasattr(settings, 'KNOWN_SERVICES') and settings.KNOWN_SERVICES.get('bijoe')
return (
settings.LEGACY_CHART_CELL_ENABLED
and hasattr(settings, 'KNOWN_SERVICES')
and settings.KNOWN_SERVICES.get('bijoe')
)
def get_default_form_class(self):
from .forms import ChartForm
return ChartForm
def get_additional_label(self):
@ -149,7 +155,12 @@ class ChartNgCell(CellBase):
)
statistic = models.ForeignKey(
verbose_name=_('Data'), to=Statistic, blank=False, null=True, on_delete=models.SET_NULL, related_name='cells'
verbose_name=_('Data'),
to=Statistic,
blank=False,
null=True,
on_delete=models.SET_NULL,
related_name='cells',
)
filter_params = JSONField(default=dict)
title = models.CharField(_('Title'), max_length=150, blank=True)
@ -163,39 +174,54 @@ class ChartNgCell(CellBase):
('current-month', _('Current month')),
('previous-month', _('Previous month')),
('range', _('Free range')),
)
),
)
time_range_start = models.DateField(_('From'), null=True, blank=True)
time_range_end = models.DateField(_('To'), null=True, blank=True)
chart_type = models.CharField(_('Chart Type'), max_length=20, default='bar',
choices=(
('bar', _('Bar')),
('horizontal-bar', _('Horizontal Bar')),
('stacked-bar', _('Stacked Bar')),
('line', _('Line')),
('pie', _('Pie')),
('dot', _('Dot')),
('table', _('Table')),
))
chart_type = models.CharField(
_('Chart Type'),
max_length=20,
default='bar',
choices=(
('bar', _('Bar')),
('horizontal-bar', _('Horizontal Bar')),
('stacked-bar', _('Stacked Bar')),
('line', _('Line')),
('pie', _('Pie')),
('dot', _('Dot')),
('table', _('Table')),
),
)
height = models.CharField(_('Height'), max_length=20, default='250',
choices=(
('150', _('Short (150px)')),
('250', _('Average (250px)')),
('350', _('Tall (350px)')),
))
height = models.CharField(
_('Height'),
max_length=20,
default='250',
choices=(
('150', _('Short (150px)')),
('250', _('Average (250px)')),
('350', _('Tall (350px)')),
),
)
sort_order = models.CharField(_('Sort data'), max_length=5, default='none',
help_text=_('This setting only applies for one-dimensional charts.'),
choices=(
('none', _('None')),
('alpha', _('Alphabetically')),
('asc', _('Increasing values')),
('desc', _('Decreasing values')),
))
sort_order = models.CharField(
_('Sort data'),
max_length=5,
default='none',
help_text=_('This setting only applies for one-dimensional charts.'),
choices=(
('none', _('None')),
('alpha', _('Alphabetically')),
('asc', _('Increasing values')),
('desc', _('Decreasing values')),
),
)
hide_null_values = models.BooleanField(default=False, verbose_name=_('Hide null values'),
help_text=_('This setting only applies for one-dimensional charts.'))
hide_null_values = models.BooleanField(
default=False,
verbose_name=_('Hide null values'),
help_text=_('This setting only applies for one-dimensional charts.'),
)
manager_form_template = 'combo/chartngcell_form.html'
@ -208,6 +234,7 @@ class ChartNgCell(CellBase):
def get_default_form_class(self):
from .forms import ChartNgForm
return ChartNgForm
def get_additional_label(self):
@ -234,7 +261,7 @@ class ChartNgCell(CellBase):
ctx = super(ChartNgCell, self).get_cell_extra_context(context)
if self.chart_type == 'table' and self.statistic:
try:
chart = self.get_chart(raise_if_not_cached=not(context.get('synchronous')))
chart = self.get_chart(raise_if_not_cached=not (context.get('synchronous')))
except UnsupportedDataSet:
ctx['table'] = '<p>%s</p>' % _('Unsupported dataset.')
except HTTPError as e:
@ -267,9 +294,7 @@ class ChartNgCell(CellBase):
response.raise_for_status()
response = response.json()
style = pygal.style.DefaultStyle(
font_family='OpenSans, sans-serif',
background='transparent')
style = pygal.style.DefaultStyle(font_family='OpenSans, sans-serif', background='transparent')
chart = {
'bar': pygal.Bar,
@ -279,7 +304,7 @@ class ChartNgCell(CellBase):
'pie': pygal.Pie,
'dot': pygal.Dot,
'table': pygal.Bar,
}[self.chart_type](config=pygal.Config(style=copy.copy(style)))
}[self.chart_type](config=pygal.Config(style=copy.copy(style)))
if self.statistic.service_slug == 'bijoe':
x_labels, y_labels, data = self.parse_response(response, chart)
@ -386,9 +411,17 @@ class ChartNgCell(CellBase):
chart.truncate_legend = 30
# matplotlib tab10 palette
chart.config.style.colors = (
'#1f77b4', '#ff7f0e', '#2ca02c', '#d62728',
'#9467bd', '#8c564b', '#e377c2', '#7f7f7f',
'#bcbd22', '#17becf')
'#1f77b4',
'#ff7f0e',
'#2ca02c',
'#d62728',
'#9467bd',
'#8c564b',
'#e377c2',
'#7f7f7f',
'#bcbd22',
'#17becf',
)
if self.chart_type == 'dot':
chart.show_legend = False
@ -463,6 +496,7 @@ class ChartNgCell(CellBase):
@staticmethod
def get_value_formatter(unit, measure):
if unit == 'seconds' or measure == 'duration':
def format_duration(value):
if value is None:
return '-'
@ -474,8 +508,8 @@ class ChartNgCell(CellBase):
hours_string = ungettext('%d hour', '%d hours', hours) % hours
if days and hours:
value = _('%(days_string)s and %(hours_string)s') % {
'days_string': days_string,
'hours_string': hours_string,
'days_string': days_string,
'hours_string': hours_string,
}
elif days:
value = days_string
@ -484,6 +518,7 @@ class ChartNgCell(CellBase):
else:
value = _('Less than an hour')
return force_text(value)
return format_duration
elif measure == 'percent':
percent_formatter = lambda x: '{:.1f}%'.format(x)

View File

@ -19,8 +19,6 @@ from django.conf.urls import url
from .views import ajax_gauge_count, dataviz_graph
urlpatterns = [
url(r'^ajax/gauge-count/(?P<cell>[\w_-]+)/$',
ajax_gauge_count, name='combo-ajax-gauge-count'),
url(r'^api/dataviz/graph/(?P<cell>[\w_-]+)/$',
dataviz_graph, name='combo-dataviz-graph'),
url(r'^ajax/gauge-count/(?P<cell>[\w_-]+)/$', ajax_gauge_count, name='combo-ajax-gauge-count'),
url(r'^api/dataviz/graph/(?P<cell>[\w_-]+)/$', dataviz_graph, name='combo-dataviz-graph'),
]

View File

@ -41,8 +41,8 @@ def dataviz_graph(request, *args, **kwargs):
error_text = None
try:
chart = cell.get_chart(
width=int(request.GET['width']) if request.GET.get('width') else None,
height=int(request.GET['height']) if request.GET.get('height') else int(cell.height)
width=int(request.GET['width']) if request.GET.get('width') else None,
height=int(request.GET['height']) if request.GET.get('height') else int(cell.height),
)
except UnsupportedDataSet as e:
error_text = _('Unsupported dataset.')
@ -60,8 +60,10 @@ def dataviz_graph(request, *args, **kwargs):
y="20"
x="10"
style="font-family: sans-serif; font-size: 16px; fill:#000000;">%(text)s</text>
</svg>""" % {'width': request.GET.get('width', 200),
'text': error_text}
</svg>""" % {
'width': request.GET.get('width', 200),
'text': error_text,
}
else:
svg = chart.render()
return HttpResponse(svg, content_type='image/svg+xml')

View File

@ -17,12 +17,15 @@
import django.apps
from django.utils.translation import ugettext_lazy as _
class AppConfig(django.apps.AppConfig):
name = 'combo.apps.family'
verbose_name = _('Family')
def get_before_urls(self):
from . import urls
return urls.urlpatterns
default_app_config = 'combo.apps.family.AppConfig'

View File

@ -17,8 +17,11 @@
from django import forms
from django.utils.translation import ugettext_lazy as _
class FamilyLinkForm(forms.Form):
family_id = forms.CharField(label=_('Family identifier'),
widget=forms.TextInput(attrs={'required': 'required'}))
family_code = forms.CharField(label=_('Secret code'),
widget=forms.PasswordInput(attrs={'required': 'required'}))
family_id = forms.CharField(
label=_('Family identifier'), widget=forms.TextInput(attrs={'required': 'required'})
)
family_code = forms.CharField(
label=_('Secret code'), widget=forms.PasswordInput(attrs={'required': 'required'})
)

View File

@ -16,7 +16,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='FamilyInfosCell',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('placeholder', models.CharField(max_length=20)),
('order', models.PositiveIntegerField()),
('slug', models.SlugField(verbose_name='Slug', blank=True)),

View File

@ -31,7 +31,10 @@ class FamilyInfosCell(CellBase):
verbose_name = _('Family Information Cell')
class Media:
js = ('xstatic/jquery-ui.min.js', 'js/gadjo.js',)
js = (
'xstatic/jquery-ui.min.js',
'js/gadjo.js',
)
@classmethod
def is_enabled(cls):
@ -43,8 +46,7 @@ class FamilyInfosCell(CellBase):
user = self.get_concerned_user(context)
if not user or user.is_anonymous:
return {}
response = get_family(user=user,
raise_if_not_cached=not(context.get('synchronous')))
response = get_family(user=user, raise_if_not_cached=not (context.get('synchronous')))
if response.status_code == 200:
return {'family': response.json()}
return {'error': _('An error occured while retrieving family details.')}

View File

@ -19,6 +19,6 @@ from django.conf.urls import url
from .views import FamilyLinkView, FamilyUnlinkView
urlpatterns = [
url(r'^family/link/?$', FamilyLinkView.as_view(), name='family-link'),
url(r'^family/unlink/?$', FamilyUnlinkView.as_view(), name='family-unlink')
url(r'^family/link/?$', FamilyLinkView.as_view(), name='family-link'),
url(r'^family/unlink/?$', FamilyUnlinkView.as_view(), name='family-unlink'),
]

View File

@ -22,8 +22,7 @@ from combo.utils import requests
def get_passerelle_service():
try:
return [x for x in settings.KNOWN_SERVICES['passerelle'].values()
if not x.get('secondary')][0]
return [x for x in settings.KNOWN_SERVICES['passerelle'].values() if not x.get('secondary')][0]
except (AttributeError, IndexError, KeyError):
return None
@ -31,12 +30,12 @@ def get_passerelle_service():
def is_family_enabled():
return get_passerelle_service() and hasattr(settings, 'FAMILY_SERVICE')
def remote_service(endpoint, **kwargs):
path = settings.FAMILY_SERVICE.get('root') + endpoint
return requests.get(path,
remote_service=get_passerelle_service(),
headers={'accept': 'application/json'},
**kwargs)
return requests.get(
path, remote_service=get_passerelle_service(), headers={'accept': 'application/json'}, **kwargs
)
def get_family(**kwargs):
@ -52,15 +51,12 @@ def link_family(user, family_id, family_code):
'params': {
'login': family_id,
'password': family_code,
}
},
}
return remote_service(endpoint, **kwargs)
def unlink_family(user):
endpoint = 'family/unlink/'
kwargs = {
'user': user,
'invalidate_cache': True
}
kwargs = {'user': user, 'invalidate_cache': True}
return remote_service(endpoint, **kwargs)

View File

@ -25,7 +25,7 @@ from .utils import link_family, unlink_family
ERROR_MESSAGES = {
100: _('Wrong credentials: make sure you typed them correctly.'),
101: _('This family account is blocked.')
101: _('This family account is blocked.'),
}
@ -40,13 +40,15 @@ class FamilyLinkView(FormView):
return '.'
def form_valid(self, form):
response = link_family(self.request.user, form.cleaned_data['family_id'],
form.cleaned_data['family_code'])
response = link_family(
self.request.user, form.cleaned_data['family_id'], form.cleaned_data['family_code']
)
if not response.ok or response.json().get('err'):
error_code = response.json().get('err')
error_message = ERROR_MESSAGES.get(error_code,
_('Failed to link to family. Please check your credentials and retry later.'))
error_message = ERROR_MESSAGES.get(
error_code, _('Failed to link to family. Please check your credentials and retry later.')
)
messages.error(self.request, error_message)
else:
messages.info(self.request, _('Your account was successfully linked.'))

View File

@ -17,8 +17,10 @@
import django.apps
from django.utils.translation import ugettext_lazy as _
class AppConfig(django.apps.AppConfig):
name = 'combo.apps.fargo'
verbose_name = _('Portfolio')
default_app_config = 'combo.apps.fargo.AppConfig'

View File

@ -15,12 +15,18 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='RecentDocumentsCell',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('placeholder', models.CharField(max_length=20)),
('order', models.PositiveIntegerField()),
('slug', models.SlugField(verbose_name='Slug', blank=True)),
('public', models.BooleanField(default=True, verbose_name='Public')),
('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
(
'restricted_to_unlogged',
models.BooleanField(default=False, verbose_name='Restrict to unlogged users'),
),
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
('page', models.ForeignKey(to='data.Page', on_delete=models.CASCADE)),
],

View File

@ -55,9 +55,9 @@ class RecentDocumentsCell(CellBase):
return {'fargo_site': Select(choices=combo_fargo_sites)}
def get_default_form_class(self):
return model_forms.modelform_factory(self.__class__,
fields=self.get_form_fields(),
widgets=self.get_form_widgets())
return model_forms.modelform_factory(
self.__class__, fields=self.get_form_fields(), widgets=self.get_form_widgets()
)
def is_visible(self, **kwargs):
user = kwargs.get('user')
@ -72,12 +72,14 @@ class RecentDocumentsCell(CellBase):
def get_json(self, path, context):
user = self.get_concerned_user(context)
try:
response = requests.get(path,
remote_service=get_fargo_site(self.fargo_site),
user=user,
raise_if_not_cached=not(context.get('synchronous')),
headers={'accept': 'application/json'},
log_errors=False)
response = requests.get(
path,
remote_service=get_fargo_site(self.fargo_site),
user=user,
raise_if_not_cached=not (context.get('synchronous')),
headers={'accept': 'application/json'},
log_errors=False,
)
response.raise_for_status()
return response.json()
except HTTPError as e:
@ -92,7 +94,12 @@ class RecentDocumentsCell(CellBase):
log = logger.info
else:
log = logger.error
log('could not retrieve recent documents for user %s: status-code=%s err=%s', user, status_code, err)
log(
'could not retrieve recent documents for user %s: status-code=%s err=%s',
user,
status_code,
err,
)
except RequestException as e:
logger.error('could not retrieve recent documents for user %s: %s', user, e)
return {}

View File

@ -24,6 +24,8 @@ class AppConfig(django.apps.AppConfig):
def get_after_manager_urls(self):
from . import urls
return urls.gallery_manager_urls
default_app_config = 'combo.apps.gallery.AppConfig'

View File

@ -19,10 +19,14 @@ from django.utils.translation import ugettext_lazy as _
from .models import Image
class ImageAddForm(forms.ModelForm):
class Meta:
model = Image
fields = ('image', 'title',)
fields = (
'image',
'title',
)
class ImageEditForm(forms.ModelForm):

View File

@ -15,13 +15,24 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='GalleryCell',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('placeholder', models.CharField(max_length=20)),
('order', models.PositiveIntegerField()),
('extra_css_class', models.CharField(max_length=100, verbose_name='Extra classes for CSS styling', blank=True)),
(
'extra_css_class',
models.CharField(
max_length=100, verbose_name='Extra classes for CSS styling', blank=True
),
),
('slug', models.SlugField(verbose_name='Slug', blank=True)),
('public', models.BooleanField(default=True, verbose_name='Public')),
('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
(
'restricted_to_unlogged',
models.BooleanField(default=False, verbose_name='Restrict to unlogged users'),
),
('last_update_timestamp', models.DateTimeField(auto_now=True)),
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
('page', models.ForeignKey(to='data.Page', on_delete=models.CASCADE)),
@ -34,10 +45,18 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Image',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('image', models.ImageField(upload_to=b'uploads/gallery/%Y/%m/', verbose_name='Image')),
('order', models.PositiveIntegerField()),
('gallery', models.ForeignKey(verbose_name='Gallery', to='gallery.GalleryCell', on_delete=models.CASCADE)),
(
'gallery',
models.ForeignKey(
verbose_name='Gallery', to='gallery.GalleryCell', on_delete=models.CASCADE
),
),
],
options={
'ordering': ['order'],

View File

@ -25,6 +25,7 @@ from django.utils.translation import ugettext_lazy as _
from combo.data.models import CellBase
from combo.data.library import register_cell_class
@register_cell_class
class GalleryCell(CellBase):
title = models.CharField(_('Title'), max_length=50, blank=True, null=True)
@ -43,8 +44,7 @@ class GalleryCell(CellBase):
return {'images': [x.get_as_serialized_object() for x in self.image_set.all()]}
def import_subobjects(self, cell_json):
images = serializers.deserialize('json', json.dumps(cell_json['images']),
ignorenonexistent=True)
images = serializers.deserialize('json', json.dumps(cell_json['images']), ignorenonexistent=True)
for image in images:
image.object.gallery_id = self.id
image.save()
@ -52,8 +52,7 @@ class GalleryCell(CellBase):
class Image(models.Model):
gallery = models.ForeignKey(GalleryCell, on_delete=models.CASCADE, verbose_name=_('Gallery'))
image = models.ImageField(_('Image'),
upload_to='uploads/gallery/%Y/%m/')
image = models.ImageField(_('Image'), upload_to='uploads/gallery/%Y/%m/')
order = models.PositiveIntegerField()
title = models.CharField(_('Title'), max_length=50, blank=True)
@ -61,7 +60,11 @@ class Image(models.Model):
ordering = ['order']
def get_as_serialized_object(self):
serialized_image = json.loads(serializers.serialize('json', [self], use_natural_foreign_keys=True, use_natural_primary_keys=True))[0]
serialized_image = json.loads(
serializers.serialize(
'json', [self], use_natural_foreign_keys=True, use_natural_primary_keys=True
)
)[0]
del serialized_image['fields']['gallery']
del serialized_image['pk']
return serialized_image

View File

@ -19,12 +19,16 @@ from django.conf.urls import url
from . import views
gallery_manager_urls = [
url('^gallery/(?P<gallery_pk>\w+)/images/add/$', views.image_add,
name='combo-gallery-image-add'),
url('^gallery/(?P<gallery_pk>\w+)/order$', views.image_order,
name='combo-gallery-image-order'),
url('^gallery/(?P<gallery_pk>\w+)/images/(?P<pk>\w+)/edit$', views.image_edit,
name='combo-gallery-image-edit'),
url('^gallery/(?P<gallery_pk>\w+)/images/(?P<pk>\w+)/delete$', views.image_delete,
name='combo-gallery-image-delete'),
url('^gallery/(?P<gallery_pk>\w+)/images/add/$', views.image_add, name='combo-gallery-image-add'),
url('^gallery/(?P<gallery_pk>\w+)/order$', views.image_order, name='combo-gallery-image-order'),
url(
'^gallery/(?P<gallery_pk>\w+)/images/(?P<pk>\w+)/edit$',
views.image_edit,
name='combo-gallery-image-edit',
),
url(
'^gallery/(?P<gallery_pk>\w+)/images/(?P<pk>\w+)/delete$',
views.image_delete,
name='combo-gallery-image-delete',
),
]

View File

@ -16,12 +16,21 @@
from django.urls import reverse, reverse_lazy
from django.shortcuts import redirect
from django.views.generic import (TemplateView, RedirectView, DetailView,
CreateView, UpdateView, ListView, DeleteView, FormView)
from django.views.generic import (
TemplateView,
RedirectView,
DetailView,
CreateView,
UpdateView,
ListView,
DeleteView,
FormView,
)
from .models import Image, GalleryCell
from .forms import ImageAddForm, ImageEditForm
class ImageAddView(CreateView):
model = Image
template_name = 'combo/gallery_image_form.html'
@ -37,8 +46,12 @@ class ImageAddView(CreateView):
return super(ImageAddView, self).form_valid(form)
def get_success_url(self):
return reverse('combo-manager-page-view',
kwargs={'pk': self.object.gallery.page.id}) + '#cell-' + self.object.gallery.get_reference()
return (
reverse('combo-manager-page-view', kwargs={'pk': self.object.gallery.page.id})
+ '#cell-'
+ self.object.gallery.get_reference()
)
image_add = ImageAddView.as_view()
@ -49,8 +62,12 @@ class ImageEditView(UpdateView):
form_class = ImageEditForm
def get_success_url(self):
return reverse('combo-manager-page-view',
kwargs={'pk': self.object.gallery.page.id}) + '#cell-' + self.object.gallery.get_reference()
return (
reverse('combo-manager-page-view', kwargs={'pk': self.object.gallery.page.id})
+ '#cell-'
+ self.object.gallery.get_reference()
)
image_edit = ImageEditView.as_view()
@ -65,6 +82,6 @@ def image_order(request, gallery_pk):
gallery = GalleryCell.objects.get(id=gallery_pk)
new_order = [int(x) for x in request.GET['new-order'].split(',')]
for image in gallery.image_set.all():
image.order = new_order.index(image.id)+1
image.order = new_order.index(image.id) + 1
image.save()
return redirect(reverse('combo-manager-page-view', kwargs={'pk': gallery.page.id}))

View File

@ -19,18 +19,44 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='LatestPageUpdatesCell',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
(
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('placeholder', models.CharField(max_length=20)),
('order', models.PositiveIntegerField()),
('slug', models.SlugField(blank=True, verbose_name='Slug')),
('extra_css_class', models.CharField(blank=True, max_length=100, verbose_name='Extra classes for CSS styling')),
(
'extra_css_class',
models.CharField(
blank=True, max_length=100, verbose_name='Extra classes for CSS styling'
),
),
('public', models.BooleanField(default=True, verbose_name='Public')),
('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
(
'restricted_to_unlogged',
models.BooleanField(default=False, verbose_name='Restrict to unlogged users'),
),
('last_update_timestamp', models.DateTimeField(auto_now=True)),
('limit', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='Maximum number of entries')),
(
'limit',
models.PositiveSmallIntegerField(
blank=True, null=True, verbose_name='Maximum number of entries'
),
),
('groups', models.ManyToManyField(blank=True, to='auth.Group', verbose_name='Groups')),
('page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='data.Page')),
('root_page', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='kb_latest_page_updates_cell_root_page', to='data.Page', verbose_name='Root Page')),
(
'root_page',
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='kb_latest_page_updates_cell_root_page',
to='data.Page',
verbose_name='Root Page',
),
),
],
options={
'verbose_name': 'Latest Page Updates',

View File

@ -24,13 +24,18 @@ from django.utils.translation import ugettext_lazy as _
from combo.data.models import CellBase, Page
from combo.data.library import register_cell_class
@register_cell_class
class LatestPageUpdatesCell(CellBase):
root_page = models.ForeignKey(
Page, on_delete=models.SET_NULL, null=True, blank=True,
verbose_name=_('Root Page'), related_name='kb_latest_page_updates_cell_root_page')
limit = models.PositiveSmallIntegerField(
_('Maximum number of entries'), default=10)
Page,
on_delete=models.SET_NULL,
null=True,
blank=True,
verbose_name=_('Root Page'),
related_name='kb_latest_page_updates_cell_root_page',
)
limit = models.PositiveSmallIntegerField(_('Maximum number of entries'), default=10)
template_name = 'combo/latest-page-updates-cell.html'
exclude_from_search = True
@ -46,6 +51,6 @@ class LatestPageUpdatesCell(CellBase):
pages = Page.objects.all()
user = self.get_concerned_user(context)
extra_context['pages'] = itertools.islice(
(x for x in pages.order_by('-last_update_timestamp') if x.is_visible(user=user)),
self.limit)
(x for x in pages.order_by('-last_update_timestamp') if x.is_visible(user=user)), self.limit
)
return extra_context

View File

@ -31,11 +31,11 @@ class AppConfig(django.apps.AppConfig):
def get_before_urls(self):
from . import urls
return urls.urlpatterns
def get_extra_manager_actions(self):
return [{'href': reverse('lingo-manager-homepage'),
'text': _('Online Payment')}]
return [{'href': reverse('lingo-manager-homepage'), 'text': _('Online Payment')}]
def hourly(self):
self.update_transactions()
@ -43,11 +43,11 @@ class AppConfig(django.apps.AppConfig):
def update_transactions(self):
from .models import Transaction, EXPIRED
logger = logging.getLogger(__name__)
now = timezone.now()
to_expire = Transaction.objects.filter(
start_date__lt=now-datetime.timedelta(hours=1),
end_date__isnull=True
start_date__lt=now - datetime.timedelta(hours=1), end_date__isnull=True
)
for transaction in to_expire:
logger.info('transaction %r is expired', transaction.order_id)
@ -57,23 +57,26 @@ class AppConfig(django.apps.AppConfig):
to_retry = Transaction.objects.filter(
status__in=(eopayment.PAID, eopayment.ACCEPTED),
to_be_paid_remote_items__isnull=False,
start_date__gt=now-datetime.timedelta(days=4)
start_date__gt=now - datetime.timedelta(days=4),
)
for transaction in to_retry:
transaction.retry_notify_remote_items_of_payments()
def notify_payments(self):
from combo.apps.lingo.models import BasketItem
logger = logging.getLogger(__name__)
now = timezone.now()
for item in BasketItem.objects.filter(
notification_date__isnull=True,
cancellation_date__isnull=True,
payment_date__lt=now-datetime.timedelta(minutes=5),
payment_date__gt=now-datetime.timedelta(minutes=300)):
notification_date__isnull=True,
cancellation_date__isnull=True,
payment_date__lt=now - datetime.timedelta(minutes=5),
payment_date__gt=now - datetime.timedelta(minutes=300),
):
try:
item.notify_payment()
except:
logger.exception('error in async notification for basket item %s', item.id)
default_app_config = 'combo.apps.lingo.AppConfig'

View File

@ -22,4 +22,5 @@ from .models import Regie
class RegieAdmin(admin.ModelAdmin):
prepopulated_fields = {'slug': ('label',)}
admin.site.register(Regie, RegieAdmin)

View File

@ -36,6 +36,7 @@ def get_validator(func, err_msg):
if not func(value):
message = err_msg or _('Invalid value.')
raise ValidationError(message)
return validate
@ -52,9 +53,7 @@ def create_form_fields(parameters, json_field):
'help_text': param.get('help_text', ''),
}
if 'validation' in param:
field_params['validators'] = [
get_validator(param['validation'], param.get('validation_err_msg'))
]
field_params['validators'] = [get_validator(param['validation'], param.get('validation_err_msg'))]
_type = param.get('type', str)
choices = param.get('choices')
@ -84,18 +83,26 @@ def compute_json_field(parameters, cleaned_data):
class RegieForm(forms.ModelForm):
class Meta:
model = Regie
fields = ['label', 'slug', 'description', 'payment_backend', 'is_default',
'webservice_url', 'extra_fees_ws_url', 'payment_min_amount', 'text_on_success',
'can_pay_only_one_basket_item']
fields = [
'label',
'slug',
'description',
'payment_backend',
'is_default',
'webservice_url',
'extra_fees_ws_url',
'payment_min_amount',
'text_on_success',
'can_pay_only_one_basket_item',
]
def __init__(self, *args, **kwargs):
super(RegieForm, self).__init__(*args, **kwargs)
fields, initial = create_form_fields(
self.instance.payment_backend.get_payment().get_parameters(scope='transaction'),
self.instance.transaction_options
self.instance.transaction_options,
)
self.fields.update(fields)
self.initial.update(initial)
@ -103,15 +110,13 @@ class RegieForm(forms.ModelForm):
def save(self):
instance = super(RegieForm, self).save()
instance.transaction_options = compute_json_field(
self.instance.payment_backend.get_payment().get_parameters(scope='transaction'),
self.cleaned_data
self.instance.payment_backend.get_payment().get_parameters(scope='transaction'), self.cleaned_data
)
instance.save()
return instance
class PaymentBackendForm(forms.ModelForm):
class Meta:
model = PaymentBackend
fields = ['label', 'slug', 'service']
@ -119,8 +124,7 @@ class PaymentBackendForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(PaymentBackendForm, self).__init__(*args, **kwargs)
fields, initial = create_form_fields(
self.instance.get_payment().get_parameters(scope='global'),
self.instance.service_options
self.instance.get_payment().get_parameters(scope='global'), self.instance.service_options
)
self.fields.update(fields)
self.initial.update(initial)
@ -130,8 +134,7 @@ class PaymentBackendForm(forms.ModelForm):
def save(self):
instance = super(PaymentBackendForm, self).save()
instance.service_options = compute_json_field(
self.instance.get_payment().get_parameters(scope='global'),
self.cleaned_data
self.instance.get_payment().get_parameters(scope='global'), self.cleaned_data
)
instance.save()
return instance
@ -139,12 +142,10 @@ class PaymentBackendForm(forms.ModelForm):
class TransactionExportForm(forms.Form):
start_date = forms.DateField(
label=_('Start date'),
widget=forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d')
label=_('Start date'), widget=forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d')
)
end_date = forms.DateField(
label=_('End date'),
widget=forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d')
label=_('End date'), widget=forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d')
)
def __init__(self, *args, **kwargs):

View File

@ -26,7 +26,6 @@ from combo.apps.lingo.models import Regie
class Command(BaseCommand):
def handle(self, *args, **kwargs):
if not settings.LINGO_INVOICE_NOTIFICATIONS_ENABLED:
return

View File

@ -95,21 +95,19 @@ class TransactionListView(ListView):
def get_queryset(self):
qs = (
Transaction.objects
.select_related('user')
Transaction.objects.select_related('user')
.prefetch_related(Prefetch('items', to_attr='prefetched_items'))
.filter(status__in=(eopayment.PAID, eopayment.ACCEPTED))
.order_by('-start_date'))
.order_by('-start_date')
)
query = self.request.GET.get('q')
if query:
try:
date = date_parser.parse(query, dayfirst=True)
except:
qs = qs.filter(
Q(order_id=query) |
Q(bank_transaction_id=query) |
Q(items__subject__icontains=query)
)
Q(order_id=query) | Q(bank_transaction_id=query) | Q(items__subject__icontains=query)
)
else:
date = make_aware(date)
qs = qs.filter(start_date__gte=date, start_date__lt=date + datetime.timedelta(days=1))
@ -132,24 +130,24 @@ class BasketItemErrorListView(ListView):
'INNER JOIN lingo_transaction_items '
'ON "lingo_transaction"."id" = "lingo_transaction_items"."transaction_id" '
'AND "lingo_transaction_items"."basketitem_id"="lingo_basketitem"."id" '
'ORDER BY start_date DESC LIMIT 1')
'ORDER BY start_date DESC LIMIT 1'
)
queryset = (
BasketItem.objects
.annotate(bank_transaction_id=RawSQL(raw % 'bank_transaction_id', []))
BasketItem.objects.annotate(bank_transaction_id=RawSQL(raw % 'bank_transaction_id', []))
.annotate(transaction_status=RawSQL(raw % 'status', []))
.filter(regie__webservice_url='')
.order_by())
.order_by()
)
terms = self.request.GET.get('q')
if terms:
queryset = queryset.filter(subject__icontains=terms)
queryset1 = (
queryset
.filter(
notification_date__isnull=True,
cancellation_date__isnull=True,
payment_date__lt=now() - datetime.timedelta(minutes=300)))
queryset1 = queryset.filter(
notification_date__isnull=True,
cancellation_date__isnull=True,
payment_date__lt=now() - datetime.timedelta(minutes=300),
)
queryset2 = queryset.filter(transaction_status=eopayment.ERROR)
return queryset1.union(queryset2).order_by('-creation_date')
@ -162,7 +160,8 @@ class BasketItemMarkAsNotifiedView(View):
regie__webservice_url='',
notification_date__isnull=True,
cancellation_date__isnull=True,
payment_date__lt=now() - datetime.timedelta(minutes=300))
payment_date__lt=now() - datetime.timedelta(minutes=300),
)
item.notify_payment(notify_origin=False)
return HttpResponseRedirect(reverse('lingo-manager-payment-error-list'))
@ -175,19 +174,19 @@ def download_transactions_csv(request):
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="transactions.csv"'
writer = csv.writer(response)
transactions = (
Transaction.objects
.filter(
status__in=(eopayment.PAID, eopayment.ACCEPTED),
start_date__date__gte=form.cleaned_data['start_date'],
start_date__date__lte=form.cleaned_data['end_date'],
).order_by('-start_date'))
transactions = Transaction.objects.filter(
status__in=(eopayment.PAID, eopayment.ACCEPTED),
start_date__date__gte=form.cleaned_data['start_date'],
start_date__date__lte=form.cleaned_data['end_date'],
).order_by('-start_date')
for transaction in transactions:
row = [transaction.order_id,
transaction.bank_transaction_id,
transaction.start_date.strftime('%Y-%m-%d %H:%M:%S'),
transaction.get_user_name(),
str(transaction.amount)]
row = [
transaction.order_id,
transaction.bank_transaction_id,
transaction.start_date.strftime('%Y-%m-%d %H:%M:%S'),
transaction.get_user_name(),
str(transaction.amount),
]
for item in transaction.items.all():
row.extend([item.subject, str(item.amount)])
writer.writerow([x for x in row])

View File

@ -18,7 +18,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='BasketItem',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('subject', models.CharField(max_length=64, verbose_name='Subject')),
('source_url', models.URLField(verbose_name='Source URL')),
('details', models.TextField(verbose_name='Details', blank=True)),
@ -27,14 +30,16 @@ class Migration(migrations.Migration):
('cancellation_date', models.DateTimeField(null=True)),
('payment_date', models.DateTimeField(null=True)),
],
options={
},
options={},
bases=(models.Model,),
),
migrations.CreateModel(
name='LingoBasketCell',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('placeholder', models.CharField(max_length=20)),
('order', models.PositiveIntegerField()),
('slug', models.SlugField(verbose_name='Slug', blank=True)),
@ -50,12 +55,39 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Regie',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('label', models.CharField(max_length=64, verbose_name='Label')),
('slug', models.SlugField(unique=True, verbose_name='Identifier', help_text='The identifier is used in webservice calls.')),
(
'slug',
models.SlugField(
unique=True,
verbose_name='Identifier',
help_text='The identifier is used in webservice calls.',
),
),
('description', models.TextField(verbose_name='Description')),
('service', models.CharField(max_length=64, verbose_name='Payment Service', choices=[(b'dummy', 'Dummy (for tests)'), (b'systempayv2', b'systempay (Banque Populaire)'), (b'sips', b'SIPS'), (b'spplus', "SP+ (Caisse d'epargne)")])),
('service_options', jsonfield.fields.JSONField(default=dict, verbose_name='Payment Service Options', blank=True)),
(
'service',
models.CharField(
max_length=64,
verbose_name='Payment Service',
choices=[
(b'dummy', 'Dummy (for tests)'),
(b'systempayv2', b'systempay (Banque Populaire)'),
(b'sips', b'SIPS'),
(b'spplus', "SP+ (Caisse d'epargne)"),
],
),
),
(
'service_options',
jsonfield.fields.JSONField(
default=dict, verbose_name='Payment Service Options', blank=True
),
),
],
options={
'verbose_name': 'Regie',

View File

@ -15,15 +15,17 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Transaction',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('start_date', models.DateTimeField(auto_now_add=True)),
('end_date', models.DateTimeField(null=True)),
('bank_data', jsonfield.fields.JSONField(default=dict, blank=True)),
('order_id', models.CharField(max_length=200)),
('items', models.ManyToManyField(to='lingo.BasketItem', blank=True)),
],
options={
},
options={},
bases=(models.Model,),
),
]

View File

@ -14,7 +14,17 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='regie',
name='service',
field=models.CharField(max_length=64, verbose_name='Payment Service', choices=[(b'dummy', 'Dummy (for tests)'), (b'systempayv2', b'systempay (Banque Populaire)'), (b'sips', b'SIPS'), (b'spplus', "SP+ (Caisse d'epargne)"), (b'ogone', 'Ingenico (formerly Ogone)')]),
field=models.CharField(
max_length=64,
verbose_name='Payment Service',
choices=[
(b'dummy', 'Dummy (for tests)'),
(b'systempayv2', b'systempay (Banque Populaire)'),
(b'sips', b'SIPS'),
(b'spplus', "SP+ (Caisse d'epargne)"),
(b'ogone', 'Ingenico (formerly Ogone)'),
],
),
preserve_default=True,
),
]

View File

@ -18,7 +18,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='LingoRecentTransactionsCell',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('placeholder', models.CharField(max_length=20)),
('order', models.PositiveIntegerField()),
('slug', models.SlugField(verbose_name='Slug', blank=True)),

View File

@ -16,7 +16,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='LingoBasketLinkCell',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('placeholder', models.CharField(max_length=20)),
('order', models.PositiveIntegerField()),
('slug', models.SlugField(verbose_name='Slug', blank=True)),

View File

@ -17,7 +17,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='ActiveItems',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('placeholder', models.CharField(max_length=20)),
('order', models.PositiveIntegerField()),
('slug', models.SlugField(verbose_name='Slug', blank=True)),
@ -35,7 +38,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='ItemsHistory',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('placeholder', models.CharField(max_length=20)),
('order', models.PositiveIntegerField()),
('slug', models.SlugField(verbose_name='Slug', blank=True)),
@ -59,7 +65,18 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='regie',
name='service',
field=models.CharField(max_length=64, verbose_name='Payment Service', choices=[(b'dummy', 'Dummy (for tests)'), (b'systempayv2', b'systempay (Banque Populaire)'), (b'sips', b'SIPS'), (b'spplus', "SP+ (Caisse d'epargne)"), (b'ogone', 'Ingenico (formerly Ogone)'), (b'paybox', 'Paybox')]),
field=models.CharField(
max_length=64,
verbose_name='Payment Service',
choices=[
(b'dummy', 'Dummy (for tests)'),
(b'systempayv2', b'systempay (Banque Populaire)'),
(b'sips', b'SIPS'),
(b'spplus', "SP+ (Caisse d'epargne)"),
(b'ogone', 'Ingenico (formerly Ogone)'),
(b'paybox', 'Paybox'),
],
),
preserve_default=True,
),
]

View File

@ -14,7 +14,9 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='regie',
name='payment_min_amount',
field=models.DecimalField(default=0, verbose_name='Minimal payment amount', max_digits=7, decimal_places=2),
field=models.DecimalField(
default=0, verbose_name='Minimal payment amount', max_digits=7, decimal_places=2
),
preserve_default=True,
),
]

View File

@ -14,7 +14,19 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='regie',
name='service',
field=models.CharField(max_length=64, verbose_name='Payment Service', choices=[(b'dummy', 'Dummy (for tests)'), (b'systempayv2', b'systempay (Banque Populaire)'), (b'sips', b'SIPS'), (b'spplus', "SP+ (Caisse d'epargne)"), (b'ogone', 'Ingenico (formerly Ogone)'), (b'paybox', 'Paybox'), (b'payzen', 'PayZen')]),
field=models.CharField(
max_length=64,
verbose_name='Payment Service',
choices=[
(b'dummy', 'Dummy (for tests)'),
(b'systempayv2', b'systempay (Banque Populaire)'),
(b'sips', b'SIPS'),
(b'spplus', "SP+ (Caisse d'epargne)"),
(b'ogone', 'Ingenico (formerly Ogone)'),
(b'paybox', 'Paybox'),
(b'payzen', 'PayZen'),
],
),
preserve_default=True,
),
]

View File

@ -14,7 +14,21 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='regie',
name='service',
field=models.CharField(max_length=64, verbose_name='Payment Service', choices=[(b'dummy', 'Dummy (for tests)'), (b'systempayv2', b'systempay (Banque Populaire)'), (b'sips', 'SIPS (Atos, France)'), (b'sips2', 'SIPS (Atos, other countries)'), (b'spplus', "SP+ (Caisse d'epargne)"), (b'ogone', 'Ingenico (formerly Ogone)'), (b'paybox', 'Paybox'), (b'payzen', 'PayZen'), (b'tipi', 'TIPI')]),
field=models.CharField(
max_length=64,
verbose_name='Payment Service',
choices=[
(b'dummy', 'Dummy (for tests)'),
(b'systempayv2', b'systempay (Banque Populaire)'),
(b'sips', 'SIPS (Atos, France)'),
(b'sips2', 'SIPS (Atos, other countries)'),
(b'spplus', "SP+ (Caisse d'epargne)"),
(b'ogone', 'Ingenico (formerly Ogone)'),
(b'paybox', 'Paybox'),
(b'payzen', 'PayZen'),
(b'tipi', 'TIPI'),
],
),
preserve_default=True,
),
]

View File

@ -15,8 +15,17 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='TransactionOperation',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('kind', models.CharField(max_length=65, choices=[(b'validation', 'Validation'), (b'cancellation', 'Cancellation')])),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
(
'kind',
models.CharField(
max_length=65,
choices=[(b'validation', 'Validation'), (b'cancellation', 'Cancellation')],
),
),
('amount', models.DecimalField(max_digits=8, decimal_places=2)),
('creation_date', models.DateTimeField(auto_now_add=True)),
('bank_result', jsonfield.fields.JSONField(default=dict, blank=True)),

View File

@ -16,13 +16,24 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='SelfDeclaredInvoicePayment',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('placeholder', models.CharField(max_length=20)),
('order', models.PositiveIntegerField()),
('slug', models.SlugField(verbose_name='Slug', blank=True)),
('extra_css_class', models.CharField(max_length=100, verbose_name='Extra classes for CSS styling', blank=True)),
(
'extra_css_class',
models.CharField(
max_length=100, verbose_name='Extra classes for CSS styling', blank=True
),
),
('public', models.BooleanField(default=True, verbose_name='Public')),
('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
(
'restricted_to_unlogged',
models.BooleanField(default=False, verbose_name='Restrict to unlogged users'),
),
('regie', models.CharField(max_length=50, verbose_name='Regie', blank=True)),
('title', models.CharField(max_length=200, verbose_name='Title', blank=True)),
('text', ckeditor.fields.RichTextField(null=True, verbose_name='Text', blank=True)),

View File

@ -17,18 +17,46 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='TipiPaymentFormCell',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('placeholder', models.CharField(max_length=20)),
('order', models.PositiveIntegerField()),
('slug', models.SlugField(verbose_name='Slug', blank=True)),
('extra_css_class', models.CharField(max_length=100, verbose_name='Extra classes for CSS styling', blank=True)),
(
'extra_css_class',
models.CharField(
max_length=100, verbose_name='Extra classes for CSS styling', blank=True
),
),
('public', models.BooleanField(default=True, verbose_name='Public')),
('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
(
'restricted_to_unlogged',
models.BooleanField(default=False, verbose_name='Restrict to unlogged users'),
),
('last_update_timestamp', models.DateTimeField(auto_now=True)),
('title', models.CharField(max_length=150, verbose_name='Title', blank=True)),
('url', models.URLField(default=b'https://www.tipi.budget.gouv.fr/tpa/paiement.web', verbose_name='TIPI payment service URL')),
('regies', models.CharField(help_text='separated by commas', max_length=256, verbose_name='Regies')),
('control_protocol', models.CharField(default=b'pesv2', max_length=8, verbose_name='Control protocol', choices=[(b'pesv2', 'Indigo/PES v2'), (b'rolmre', 'ROLMRE')])),
(
'url',
models.URLField(
default=b'https://www.tipi.budget.gouv.fr/tpa/paiement.web',
verbose_name='TIPI payment service URL',
),
),
(
'regies',
models.CharField(help_text='separated by commas', max_length=256, verbose_name='Regies'),
),
(
'control_protocol',
models.CharField(
default=b'pesv2',
max_length=8,
verbose_name='Control protocol',
choices=[(b'pesv2', 'Indigo/PES v2'), (b'rolmre', 'ROLMRE')],
),
),
('test_mode', models.BooleanField(default=False, verbose_name='Test mode')),
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
('page', models.ForeignKey(to='data.Page', on_delete=models.CASCADE)),

View File

@ -14,31 +14,43 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='tipipaymentformcell',
name='exer',
field=models.CharField(help_text='Default value to be used in form', max_length=4, verbose_name='Exer', blank=True),
field=models.CharField(
help_text='Default value to be used in form', max_length=4, verbose_name='Exer', blank=True
),
),
migrations.AddField(
model_name='tipipaymentformcell',
name='idligne',
field=models.CharField(help_text='Default value to be used in form', max_length=6, verbose_name='IDLIGNE', blank=True),
field=models.CharField(
help_text='Default value to be used in form', max_length=6, verbose_name='IDLIGNE', blank=True
),
),
migrations.AddField(
model_name='tipipaymentformcell',
name='idpce',
field=models.CharField(help_text='Default value to be used in form', max_length=8, verbose_name='IDPCE', blank=True),
field=models.CharField(
help_text='Default value to be used in form', max_length=8, verbose_name='IDPCE', blank=True
),
),
migrations.AddField(
model_name='tipipaymentformcell',
name='roldeb',
field=models.CharField(help_text='Default value to be used in form', max_length=2, verbose_name='ROLDEB', blank=True),
field=models.CharField(
help_text='Default value to be used in form', max_length=2, verbose_name='ROLDEB', blank=True
),
),
migrations.AddField(
model_name='tipipaymentformcell',
name='roldet',
field=models.CharField(help_text='Default value to be used in form', max_length=13, verbose_name='ROLDET', blank=True),
field=models.CharField(
help_text='Default value to be used in form', max_length=13, verbose_name='ROLDET', blank=True
),
),
migrations.AddField(
model_name='tipipaymentformcell',
name='rolrec',
field=models.CharField(help_text='Default value to be used in form', max_length=2, verbose_name='ROLREC', blank=True),
field=models.CharField(
help_text='Default value to be used in form', max_length=2, verbose_name='ROLREC', blank=True
),
),
]

View File

@ -17,16 +17,50 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='PaymentBackend',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
(
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('label', models.CharField(max_length=64, verbose_name='Label')),
('slug', models.SlugField(help_text='The identifier is used in webservice calls.', unique=True, verbose_name='Identifier')),
('service', models.CharField(choices=[(b'dummy', 'Dummy (for tests)'), (b'systempayv2', b'systempay (Banque Populaire)'), (b'sips', 'SIPS (Atos, France)'), (b'sips2', 'SIPS (Atos, other countries)'), (b'spplus', "SP+ (Caisse d'epargne)"), (b'ogone', 'Ingenico (formerly Ogone)'), (b'paybox', 'Paybox'), (b'payzen', 'PayZen'), (b'tipi', 'TIPI')], max_length=64, verbose_name='Payment Service')),
('service_options', jsonfield.fields.JSONField(blank=True, default=dict, verbose_name='Payment Service Options')),
(
'slug',
models.SlugField(
help_text='The identifier is used in webservice calls.',
unique=True,
verbose_name='Identifier',
),
),
(
'service',
models.CharField(
choices=[
(b'dummy', 'Dummy (for tests)'),
(b'systempayv2', b'systempay (Banque Populaire)'),
(b'sips', 'SIPS (Atos, France)'),
(b'sips2', 'SIPS (Atos, other countries)'),
(b'spplus', "SP+ (Caisse d'epargne)"),
(b'ogone', 'Ingenico (formerly Ogone)'),
(b'paybox', 'Paybox'),
(b'payzen', 'PayZen'),
(b'tipi', 'TIPI'),
],
max_length=64,
verbose_name='Payment Service',
),
),
(
'service_options',
jsonfield.fields.JSONField(
blank=True, default=dict, verbose_name='Payment Service Options'
),
),
],
),
migrations.AddField(
model_name='regie',
name='payment_backend',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='lingo.PaymentBackend'),
field=models.ForeignKey(
null=True, on_delete=django.db.models.deletion.CASCADE, to='lingo.PaymentBackend'
),
),
]

View File

@ -10,8 +10,8 @@ def create_backends(apps, schema_editor):
Regie = apps.get_model('lingo', 'Regie')
for regie in Regie.objects.all():
pb = PaymentBackend(
label=regie.label, slug=regie.slug, service=regie.service,
service_options=regie.service_options)
label=regie.label, slug=regie.slug, service=regie.service, service_options=regie.service_options
)
pb.save()
regie.payment_backend = pb
regie.save()

View File

@ -25,7 +25,9 @@ class Migration(migrations.Migration):
model_name='regie',
name='payment_backend',
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to='lingo.PaymentBackend',
verbose_name='Payment backend'),
on_delete=django.db.models.deletion.CASCADE,
to='lingo.PaymentBackend',
verbose_name='Payment backend',
),
),
]

View File

@ -28,21 +28,35 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='tipipaymentformcell',
name='control_protocol',
field=models.CharField(choices=[('pesv2', 'Indigo/PES v2'), ('rolmre', 'ROLMRE')], default='pesv2', max_length=8, verbose_name='Control protocol'),
field=models.CharField(
choices=[('pesv2', 'Indigo/PES v2'), ('rolmre', 'ROLMRE')],
default='pesv2',
max_length=8,
verbose_name='Control protocol',
),
),
migrations.AlterField(
model_name='tipipaymentformcell',
name='regies',
field=models.CharField(help_text='Values separated by commas. It is possible to add a label after a regie identifier. Example: "1234 - Regie A,5678 - Regie B"', max_length=256, verbose_name='Regies'),
field=models.CharField(
help_text='Values separated by commas. It is possible to add a label after a regie identifier. Example: "1234 - Regie A,5678 - Regie B"',
max_length=256,
verbose_name='Regies',
),
),
migrations.AlterField(
model_name='tipipaymentformcell',
name='url',
field=models.URLField(default='https://www.tipi.budget.gouv.fr/tpa/paiement.web', verbose_name='TIPI payment service URL'),
field=models.URLField(
default='https://www.tipi.budget.gouv.fr/tpa/paiement.web',
verbose_name='TIPI payment service URL',
),
),
migrations.AlterField(
model_name='transactionoperation',
name='kind',
field=models.CharField(choices=[('validation', 'Validation'), ('cancellation', 'Cancellation')], max_length=65),
field=models.CharField(
choices=[('validation', 'Validation'), ('cancellation', 'Cancellation')], max_length=65
),
),
]

View File

@ -88,20 +88,23 @@ class RemoteInvoiceException(Exception):
def build_remote_item(data, regie):
return RemoteItem(id=data.get('id'), regie=regie,
creation_date=data['created'],
payment_limit_date=data['pay_limit_date'],
display_id=data.get('display_id'),
total_amount=data.get('total_amount'),
amount=data.get('amount'),
amount_paid=data.get('amount_paid'),
subject=data.get('label'),
has_pdf=data.get('has_pdf'),
online_payment=data.get('online_payment'),
paid=data.get('paid'),
payment_date=data.get('payment_date'),
no_online_payment_reason=data.get('no_online_payment_reason'),
reference_id=data.get('reference_id'))
return RemoteItem(
id=data.get('id'),
regie=regie,
creation_date=data['created'],
payment_limit_date=data['pay_limit_date'],
display_id=data.get('display_id'),
total_amount=data.get('total_amount'),
amount=data.get('amount'),
amount_paid=data.get('amount_paid'),
subject=data.get('label'),
has_pdf=data.get('has_pdf'),
online_payment=data.get('online_payment'),
paid=data.get('paid'),
payment_date=data.get('payment_date'),
no_online_payment_reason=data.get('no_online_payment_reason'),
reference_id=data.get('reference_id'),
)
class PaymentBackendManager(models.Manager):
@ -113,10 +116,9 @@ class PaymentBackendManager(models.Manager):
class PaymentBackend(models.Model):
label = models.CharField(verbose_name=_('Label'), max_length=64)
slug = models.SlugField(
unique=True, verbose_name=_('Identifier'),
help_text=_('The identifier is used in webservice calls.'))
service = models.CharField(
verbose_name=_('Payment Service'), max_length=64, choices=SERVICES)
unique=True, verbose_name=_('Identifier'), help_text=_('The identifier is used in webservice calls.')
)
service = models.CharField(verbose_name=_('Payment Service'), max_length=64, choices=SERVICES)
service_options = JSONField(blank=True, verbose_name=_('Payment Service Options'))
objects = PaymentBackendManager()
@ -145,9 +147,9 @@ class PaymentBackend(models.Model):
return [x.get_as_serialized_object() for x in cls.objects.all()]
def get_as_serialized_object(self):
serialized_backend = json.loads(
serializers.serialize('json', [self], use_natural_primary_keys=True)
)[0]
serialized_backend = json.loads(serializers.serialize('json', [self], use_natural_primary_keys=True))[
0
]
del serialized_backend['model']
return serialized_backend
@ -172,22 +174,23 @@ class PaymentBackend(models.Model):
@python_2_unicode_compatible
class Regie(models.Model):
label = models.CharField(verbose_name=_('Label'), max_length=64)
slug = models.SlugField(unique=True, verbose_name=_('Identifier'),
help_text=_('The identifier is used in webservice calls.'))
slug = models.SlugField(
unique=True, verbose_name=_('Identifier'), help_text=_('The identifier is used in webservice calls.')
)
description = models.TextField(verbose_name=_('Description'))
is_default = models.BooleanField(verbose_name=_('Default Regie'), default=False)
webservice_url = models.URLField(_('Webservice URL to retrieve remote items'),
blank=True)
extra_fees_ws_url = models.URLField(_('Webservice URL to compute extra fees'),
blank=True)
payment_min_amount = models.DecimalField(_('Minimal payment amount'),
max_digits=7, decimal_places=2, default=0)
webservice_url = models.URLField(_('Webservice URL to retrieve remote items'), blank=True)
extra_fees_ws_url = models.URLField(_('Webservice URL to compute extra fees'), blank=True)
payment_min_amount = models.DecimalField(
_('Minimal payment amount'), max_digits=7, decimal_places=2, default=0
)
text_on_success = models.TextField(
verbose_name=_('Custom text displayed on success'),
blank=True, null=True)
verbose_name=_('Custom text displayed on success'), blank=True, null=True
)
payment_backend = models.ForeignKey(
PaymentBackend, on_delete=models.CASCADE, verbose_name=_('Payment backend'))
PaymentBackend, on_delete=models.CASCADE, verbose_name=_('Payment backend')
)
transaction_options = JSONField(blank=True, verbose_name=_('Transaction Options'))
can_pay_only_one_basket_item = models.BooleanField(
default=True, verbose_name=_('Basket items must be paid individually')
@ -198,7 +201,10 @@ class Regie(models.Model):
class Meta:
verbose_name = _('Regie')
ordering = ('-is_default', 'label',)
ordering = (
'-is_default',
'label',
)
def save(self, *args, **kwargs):
if self.webservice_url and self.webservice_url.endswith('/'):
@ -257,7 +263,9 @@ class Regie(models.Model):
if not self.is_remote():
return self.basketitem_set.get(pk=invoice_id)
url = self.webservice_url + '/invoice/%s/' % invoice_id
response = requests.get(url, user=user, remote_service='auto', cache_duration=0, log_errors=log_errors)
response = requests.get(
url, user=user, remote_service='auto', cache_duration=0, log_errors=log_errors
)
if raise_4xx and 400 <= response.status_code < 500:
raise ObjectDoesNotExist()
if response.status_code == 404:
@ -285,13 +293,11 @@ class Regie(models.Model):
transaction_date = transaction_date.astimezone(utc)
data = {
'transaction_id': transaction_id,
'transaction_date': transaction_date.strftime('%Y-%m-%dT%H:%M:%S')
'transaction_date': transaction_date.strftime('%Y-%m-%dT%H:%M:%S'),
}
headers = {'content-type': 'application/json'}
try:
response = requests.post(
url, remote_service='auto',
data=json.dumps(data), headers=headers)
response = requests.post(url, remote_service='auto', data=json.dumps(data), headers=headers)
if 400 <= response.status_code < 500:
raise ObjectDoesNotExist()
response.raise_for_status()
@ -306,9 +312,7 @@ class Regie(models.Model):
return resp
def as_api_dict(self):
return {'id': self.slug,
'text': self.label,
'description': self.description}
return {'id': self.slug, 'text': self.label, 'description': self.description}
def compute_extra_fees(self, user):
if not self.extra_fees_ws_url:
@ -321,35 +325,40 @@ class Regie(models.Model):
'source_url': basketitem.source_url,
'details': basketitem.details,
'amount': str(basketitem.amount),
'request_data': basketitem.request_data
'request_data': basketitem.request_data,
}
post_data['data'].append(basketitem_data)
if not post_data['data']:
basketitems.filter(extra_fee=True).delete()
return
response = requests.post(
self.extra_fees_ws_url,
remote_service='auto',
data=json.dumps(post_data),
headers={'content-type': 'application/json'})
self.extra_fees_ws_url,
remote_service='auto',
data=json.dumps(post_data),
headers={'content-type': 'application/json'},
)
if response.status_code != 200 or response.json().get('err'):
logger = logging.getLogger(__name__)
logger.error('failed to compute extra fees (user: %r)', user)
return
basketitems.filter(extra_fee=True).delete()
for extra_fee in response.json().get('data'):
BasketItem(user=user, regie=self,
subject=extra_fee.get('subject'),
amount=extra_fee.get('amount'),
extra_fee=True,
user_cancellable=False).save()
BasketItem(
user=user,
regie=self,
subject=extra_fee.get('subject'),
amount=extra_fee.get('amount'),
extra_fee=True,
user_cancellable=False,
).save()
def get_remote_pending_invoices(self):
if not self.is_remote() or UserSAMLIdentifier is None:
return {}
url = self.webservice_url + '/users/with-pending-invoices/'
response = requests.get(url, remote_service='auto', cache_duration=0,
log_errors=False, without_user=True)
response = requests.get(
url, remote_service='auto', cache_duration=0, log_errors=False, without_user=True
)
if not response.ok:
return {}
return response.json()['data']
@ -389,12 +398,15 @@ class Regie(models.Model):
payment_limit_date = datetime.datetime(
invoice.payment_limit_date.year,
invoice.payment_limit_date.month,
invoice.payment_limit_date.day)
Notification.notify(user,
summary=message,
id=notification_id,
url=items_page_url,
end_timestamp=make_aware(payment_limit_date))
invoice.payment_limit_date.day,
)
Notification.notify(
user,
summary=message,
id=notification_id,
url=items_page_url,
end_timestamp=make_aware(payment_limit_date),
)
return notification_id
def notify_new_remote_invoices(self):
@ -411,12 +423,11 @@ class Regie(models.Model):
for invoice in items['invoices']:
remote_invoice = build_remote_item(invoice, self)
if remote_invoice.total_amount >= self.payment_min_amount:
notification_ids.append(
self.notify_invoice(user, remote_invoice))
notification_ids.append(self.notify_invoice(user, remote_invoice))
# clear old notifications for invoice not in the source anymore
Notification.objects.namespace(self.get_notification_namespace())\
.exclude(external_id__in=notification_ids) \
.forget()
Notification.objects.namespace(self.get_notification_namespace()).exclude(
external_id__in=notification_ids
).forget()
def notify_remote_invoice_by_email(self, user, invoice):
@ -424,8 +435,7 @@ class Regie(models.Model):
text_body_template = 'lingo/combo/invoice_email_notification_body.txt'
html_body_template = 'lingo/combo/invoice_email_notification_body.html'
payment_url = reverse('view-item', kwargs={'regie_id': self.id,
'item_crypto_id': invoice.crypto_id})
payment_url = reverse('view-item', kwargs={'regie_id': self.id, 'item_crypto_id': invoice.crypto_id})
ctx = settings.TEMPLATE_VARS.copy()
ctx['invoice'] = invoice
ctx['payment_url'] = urlparse.urljoin(settings.SITE_BASE_URL, payment_url)
@ -476,8 +486,7 @@ class BasketItem(models.Model):
subject = models.CharField(verbose_name=_('Subject'), max_length=200)
source_url = models.URLField(_('Source URL'), blank=True)
details = models.TextField(verbose_name=_('Details'), blank=True)
amount = models.DecimalField(verbose_name=_('Amount'),
decimal_places=2, max_digits=8)
amount = models.DecimalField(verbose_name=_('Amount'), decimal_places=2, max_digits=8)
request_data = JSONField(blank=True)
extra_fee = models.BooleanField(default=False)
user_cancellable = models.BooleanField(default=True)
@ -496,10 +505,8 @@ class BasketItem(models.Model):
@classmethod
def get_items_to_be_paid(cls, user):
return cls.objects.filter(
user=user,
payment_date__isnull=True,
waiting_date__isnull=True,
cancellation_date__isnull=True)
user=user, payment_date__isnull=True, waiting_date__isnull=True, cancellation_date__isnull=True
)
def notify(self, status):
if not self.source_url:
@ -507,8 +514,7 @@ class BasketItem(models.Model):
url = self.source_url + 'jump/trigger/%s' % status
message = {'result': 'ok'}
if status == 'paid':
transaction = self.transaction_set.filter(
status__in=(eopayment.ACCEPTED, eopayment.PAID))[0]
transaction = self.transaction_set.filter(status__in=(eopayment.ACCEPTED, eopayment.PAID))[0]
message['transaction_id'] = transaction.id
message['order_id'] = transaction.order_id
message['bank_transaction_id'] = transaction.bank_transaction_id
@ -517,8 +523,7 @@ class BasketItem(models.Model):
message['bank_transaction_date'] = bank_transaction_date.strftime('%Y-%m-%dT%H:%M:%S')
message['bank_data'] = transaction.bank_data
headers = {'content-type': 'application/json'}
r = requests.post(url, remote_service='auto',
data=json.dumps(message), headers=headers, timeout=15)
r = requests.post(url, remote_service='auto', data=json.dumps(message), headers=headers, timeout=15)
r.raise_for_status()
def notify_payment(self, notify_origin=True):
@ -542,16 +547,30 @@ class BasketItem(models.Model):
@property
def payment_url(self):
signature = signing_dumps(self.pk)
return reverse('basket-item-pay-view', kwargs={'item_signature': signature })
return reverse('basket-item-pay-view', kwargs={'item_signature': signature})
class RemoteItem(object):
payment_date = None
def __init__(self, id, regie, creation_date, payment_limit_date,
total_amount, amount, amount_paid, display_id, subject, has_pdf,
online_payment, paid, payment_date, no_online_payment_reason,
reference_id):
def __init__(
self,
id,
regie,
creation_date,
payment_limit_date,
total_amount,
amount,
amount_paid,
display_id,
subject,
has_pdf,
online_payment,
paid,
payment_date,
no_online_payment_reason,
reference_id,
):
self.id = id
self.regie = regie
self.creation_date = dateparse.parse_date(creation_date)
@ -572,12 +591,14 @@ class RemoteItem(object):
@property
def no_online_payment_reason_details(self):
reasons = {'litigation': _('This invoice is in litigation.'),
'autobilling': _('Autobilling has been set for this invoice.'),
'past-due-date': _('Due date is over.'),
}
return settings.LINGO_NO_ONLINE_PAYMENT_REASONS.get(self.no_online_payment_reason,
reasons.get(self.no_online_payment_reason))
reasons = {
'litigation': _('This invoice is in litigation.'),
'autobilling': _('Autobilling has been set for this invoice.'),
'past-due-date': _('Due date is over.'),
}
return settings.LINGO_NO_ONLINE_PAYMENT_REASONS.get(
self.no_online_payment_reason, reasons.get(self.no_online_payment_reason)
)
@property
def crypto_id(self):
@ -616,7 +637,7 @@ class Transaction(models.Model):
eopayment.PAID: _('Paid'),
eopayment.ACCEPTED: _('Paid (accepted)'),
eopayment.CANCELLED: _('Cancelled'),
EXPIRED: _('Expired')
EXPIRED: _('Expired'),
}.get(self.status) or _('Unknown')
def first_notify_remote_items_of_payments(self):
@ -641,30 +662,33 @@ class Transaction(models.Model):
except ObjectDoesNotExist:
# 4xx error
logger.error(
'unable to retrieve or pay remote item %s from transaction %s, ignore it',
item_id, self)
'unable to retrieve or pay remote item %s from transaction %s, ignore it', item_id, self
)
except (RequestException, RemoteInvoiceException):
# 5xx, err or requests error
to_be_paid_remote_items.append(item_id)
logger.warning(
'unable to notify payment for remote item %s from transaction %s, retry later',
item_id, self)
item_id,
self,
)
except Exception:
# unknown error
to_be_paid_remote_items.append(item_id)
logger.exception(
'unable to notify payment for remote item %s from transaction %s',
item_id, self)
'unable to notify payment for remote item %s from transaction %s', item_id, self
)
else:
logger.info(u'notified payment for remote item %s from transaction %s',
item_id, self)
logger.info(u'notified payment for remote item %s from transaction %s', item_id, self)
subject = _('Invoice #%s') % remote_item.display_id
local_item = BasketItem.objects.create(user=self.user,
regie=regie,
source_url='',
subject=subject,
amount=remote_item.amount,
payment_date=self.end_date)
local_item = BasketItem.objects.create(
user=self.user,
regie=regie,
source_url='',
subject=subject,
amount=remote_item.amount,
payment_date=self.end_date,
)
self.items.add(local_item)
self.to_be_paid_remote_items = ','.join(to_be_paid_remote_items) or None
@ -691,7 +715,10 @@ class LingoBasketCell(CellBase):
verbose_name = _('Basket')
class Media:
js = ('xstatic/jquery-ui.min.js', 'js/gadjo.js',)
js = (
'xstatic/jquery-ui.min.js',
'js/gadjo.js',
)
@classmethod
def is_enabled(cls):
@ -745,10 +772,8 @@ class LingoRecentTransactionsCell(CellBase):
# list transactions :
# * paid by the user
# * or linked to a BasketItem of the user
return (
Transaction.objects
.filter(models.Q(user=user) | models.Q(items__user=user))
.filter(start_date__gte=timezone.now() - datetime.timedelta(days=7))
return Transaction.objects.filter(models.Q(user=user) | models.Q(items__user=user)).filter(
start_date__gte=timezone.now() - datetime.timedelta(days=7)
)
def is_relevant(self, context):
@ -804,14 +829,17 @@ class Items(CellBase):
abstract = True
class Media:
js = ('xstatic/jquery-ui.min.js', 'js/gadjo.js',)
js = (
'xstatic/jquery-ui.min.js',
'js/gadjo.js',
)
@classmethod
def is_enabled(cls):
return Regie.objects.exclude(webservice_url='').exists()
def is_relevant(self, context):
return (getattr(context['request'], 'user', None) and context['request'].user.is_authenticated)
return getattr(context['request'], 'user', None) and context['request'].user.is_authenticated
def get_default_form_class(self):
fields = ['title', 'text']
@ -843,12 +871,14 @@ class Items(CellBase):
items, errors = self.get_invoices(user=context['user'])
none_date = datetime.datetime(1900, 1, 1) # to avoid None-None comparison errors
items.sort(key=lambda i: i.creation_date or none_date, reverse=True)
ctx.update({
'items': items,
'errors': errors,
'with_payment_limit_date': any(i.payment_limit_date for i in items),
'with_amount_paid': any(getattr(i, 'amount_paid', None) for i in items),
})
ctx.update(
{
'items': items,
'errors': errors,
'with_payment_limit_date': any(i.payment_limit_date for i in items),
'with_amount_paid': any(getattr(i, 'amount_paid', None) for i in items),
}
)
return ctx
def render(self, context):
@ -860,7 +890,6 @@ class Items(CellBase):
@register_cell_class
class ItemsHistory(Items):
class Meta:
verbose_name = _('Items History Cell')
@ -919,20 +948,36 @@ TIPI_CONTROL_PROCOTOLS = (
@register_cell_class
class TipiPaymentFormCell(CellBase):
title = models.CharField(_('Title'), max_length=150, blank=True)
url = models.URLField(_('TIPI payment service URL'), default='https://www.tipi.budget.gouv.fr/tpa/paiement.web')
url = models.URLField(
_('TIPI payment service URL'), default='https://www.tipi.budget.gouv.fr/tpa/paiement.web'
)
regies = models.CharField(
_('Regies'),
help_text=_('Values separated by commas. It is possible to add a label after a regie identifier. '
'Example: "1234 - Regie A,5678 - Regie B"'),
max_length=256)
control_protocol = models.CharField(_('Control protocol'), max_length=8, choices=TIPI_CONTROL_PROCOTOLS,
default='pesv2')
help_text=_(
'Values separated by commas. It is possible to add a label after a regie identifier. '
'Example: "1234 - Regie A,5678 - Regie B"'
),
max_length=256,
)
control_protocol = models.CharField(
_('Control protocol'), max_length=8, choices=TIPI_CONTROL_PROCOTOLS, default='pesv2'
)
exer = models.CharField('Exer', max_length=4, blank=True, help_text=_('Default value to be used in form'))
idpce = models.CharField('IDPCE', max_length=8, blank=True, help_text=_('Default value to be used in form'))
idligne = models.CharField('IDLIGNE', max_length=6, blank=True, help_text=_('Default value to be used in form'))
rolrec = models.CharField('ROLREC', max_length=2, blank=True, help_text=_('Default value to be used in form'))
roldeb = models.CharField('ROLDEB', max_length=2, blank=True, help_text=_('Default value to be used in form'))
roldet = models.CharField('ROLDET', max_length=13, blank=True, help_text=_('Default value to be used in form'))
idpce = models.CharField(
'IDPCE', max_length=8, blank=True, help_text=_('Default value to be used in form')
)
idligne = models.CharField(
'IDLIGNE', max_length=6, blank=True, help_text=_('Default value to be used in form')
)
rolrec = models.CharField(
'ROLREC', max_length=2, blank=True, help_text=_('Default value to be used in form')
)
roldeb = models.CharField(
'ROLDEB', max_length=2, blank=True, help_text=_('Default value to be used in form')
)
roldet = models.CharField(
'ROLDET', max_length=13, blank=True, help_text=_('Default value to be used in form')
)
test_mode = models.BooleanField(_('Test mode'), default=False)
template_name = 'lingo/tipi_form.html'
@ -946,18 +991,9 @@ class TipiPaymentFormCell(CellBase):
extra_context = super(TipiPaymentFormCell, self).get_cell_extra_context(context)
form_fields = self.get_default_form_class().base_fields
field_definitions = (
{
'protocol': 'any',
'fields': ['exer']
},
{
'protocol': 'pesv2',
'fields': ['idpce', 'idligne']
},
{
'protocol': 'rolmre',
'fields': ['rolrec', 'roldeb', 'roldet']
},
{'protocol': 'any', 'fields': ['exer']},
{'protocol': 'pesv2', 'fields': ['idpce', 'idligne']},
{'protocol': 'rolmre', 'fields': ['rolrec', 'roldeb', 'roldet']},
)
reference_fields = []
for definition in field_definitions:
@ -966,13 +1002,15 @@ class TipiPaymentFormCell(CellBase):
# special pattern for rolrec
if field == 'rolrec':
field_pattern = '[A-Z0-9]+'
reference_fields.append({
'name': field,
'length': form_fields[field].max_length,
'placeholder': '0' * form_fields[field].max_length,
'pattern': field_pattern,
'protocol': definition['protocol']
})
reference_fields.append(
{
'name': field,
'length': form_fields[field].max_length,
'placeholder': '0' * form_fields[field].max_length,
'pattern': field_pattern,
'protocol': definition['protocol'],
}
)
context['title'] = self.title
context['url'] = self.url
context['mode'] = 'T' if self.test_mode else 'M'

View File

@ -18,71 +18,110 @@ from django.conf.urls import url, include
from combo.urls_utils import decorated_includes, manager_required
from .views import (RegiesApiView, AddBasketItemApiView, PayView, CallbackView,
ReturnView, ItemDownloadView, ItemView, CancelItemView,
RemoveBasketItemApiView, ValidateTransactionApiView,
CancelTransactionApiView, SelfInvoiceView, BasketItemPayView,
TransactionStatusApiView, PaymentStatusView)
from .manager_views import (RegieListView, RegieCreateView, RegieUpdateView,
RegieDeleteView, TransactionListView, BasketItemErrorListView,
download_transactions_csv, PaymentBackendListView,
PaymentBackendCreateView, PaymentBackendUpdateView,
PaymentBackendDeleteView, BasketItemMarkAsNotifiedView)
from .views import (
RegiesApiView,
AddBasketItemApiView,
PayView,
CallbackView,
ReturnView,
ItemDownloadView,
ItemView,
CancelItemView,
RemoveBasketItemApiView,
ValidateTransactionApiView,
CancelTransactionApiView,
SelfInvoiceView,
BasketItemPayView,
TransactionStatusApiView,
PaymentStatusView,
)
from .manager_views import (
RegieListView,
RegieCreateView,
RegieUpdateView,
RegieDeleteView,
TransactionListView,
BasketItemErrorListView,
download_transactions_csv,
PaymentBackendListView,
PaymentBackendCreateView,
PaymentBackendUpdateView,
PaymentBackendDeleteView,
BasketItemMarkAsNotifiedView,
)
lingo_manager_urls = [
url('^$', TransactionListView.as_view(), name='lingo-manager-homepage'),
url('^payments/error/$', BasketItemErrorListView.as_view(), name='lingo-manager-payment-error-list'),
url(r'^item/(?P<item_id>\d+)/mark-as-notified/$',
BasketItemMarkAsNotifiedView.as_view(), name='lingo-manager-basket-item-mark-as-notified'),
url('^transactions/download-csv/$', download_transactions_csv, name='lingo-manager-transactions-download'),
url(
r'^item/(?P<item_id>\d+)/mark-as-notified/$',
BasketItemMarkAsNotifiedView.as_view(),
name='lingo-manager-basket-item-mark-as-notified',
),
url(
'^transactions/download-csv/$', download_transactions_csv, name='lingo-manager-transactions-download'
),
url('^regies/$', RegieListView.as_view(), name='lingo-manager-regie-list'),
url('^regies/add/$', RegieCreateView.as_view(), name='lingo-manager-regie-add'),
url(r'^regies/(?P<pk>\w+)/edit$', RegieUpdateView.as_view(),
name='lingo-manager-regie-edit'),
url(r'^regies/(?P<pk>\w+)/delete$', RegieDeleteView.as_view(),
name='lingo-manager-regie-delete'),
url('^paymentbackends/$', PaymentBackendListView.as_view(),
name='lingo-manager-paymentbackend-list'),
url('^paymentbackends/add/$', PaymentBackendCreateView.as_view(),
name='lingo-manager-paymentbackend-add'),
url(r'^paymentbackends/(?P<pk>\w+)/edit$', PaymentBackendUpdateView.as_view(),
name='lingo-manager-paymentbackend-edit'),
url(r'^paymentbackends/(?P<pk>\w+)/delete$', PaymentBackendDeleteView.as_view(),
name='lingo-manager-paymentbackend-delete'),
url(r'^regies/(?P<pk>\w+)/edit$', RegieUpdateView.as_view(), name='lingo-manager-regie-edit'),
url(r'^regies/(?P<pk>\w+)/delete$', RegieDeleteView.as_view(), name='lingo-manager-regie-delete'),
url('^paymentbackends/$', PaymentBackendListView.as_view(), name='lingo-manager-paymentbackend-list'),
url(
'^paymentbackends/add/$', PaymentBackendCreateView.as_view(), name='lingo-manager-paymentbackend-add'
),
url(
r'^paymentbackends/(?P<pk>\w+)/edit$',
PaymentBackendUpdateView.as_view(),
name='lingo-manager-paymentbackend-edit',
),
url(
r'^paymentbackends/(?P<pk>\w+)/delete$',
PaymentBackendDeleteView.as_view(),
name='lingo-manager-paymentbackend-delete',
),
]
urlpatterns = [
url('^api/lingo/regies$', RegiesApiView.as_view(), name='api-regies'),
url('^api/lingo/add-basket-item$', AddBasketItemApiView.as_view(),
name='api-add-basket-item'),
url('^api/lingo/remove-basket-item$', RemoveBasketItemApiView.as_view(),
name='api-remove-basket-item'),
url('^api/lingo/validate-transaction$', ValidateTransactionApiView.as_view(),
name='api-validate-transaction'),
url('^api/lingo/cancel-transaction$', CancelTransactionApiView.as_view(),
name='api-cancel-transaction'),
url('^api/lingo/add-basket-item$', AddBasketItemApiView.as_view(), name='api-add-basket-item'),
url('^api/lingo/remove-basket-item$', RemoveBasketItemApiView.as_view(), name='api-remove-basket-item'),
url(
'^api/lingo/transaction-status/(?P<transaction_signature>.+)/$', TransactionStatusApiView.as_view(),
name='api-transaction-status'
'^api/lingo/validate-transaction$',
ValidateTransactionApiView.as_view(),
name='api-validate-transaction',
),
url('^api/lingo/cancel-transaction$', CancelTransactionApiView.as_view(), name='api-cancel-transaction'),
url(
'^api/lingo/transaction-status/(?P<transaction_signature>.+)/$',
TransactionStatusApiView.as_view(),
name='api-transaction-status',
),
url(r'^lingo/pay$', PayView.as_view(), name='lingo-pay'),
url(r'^lingo/cancel/(?P<pk>\w+)/$', CancelItemView.as_view(), name='lingo-cancel-item'),
url(r'^lingo/callback/(?P<regie_pk>\w+)/$', CallbackView.as_view(), name='lingo-callback'),
url(r'^lingo/callback-payment-backend/(?P<payment_backend_pk>\w+)/$',
CallbackView.as_view(), name='lingo-callback-payment-backend'),
url(
r'^lingo/callback-payment-backend/(?P<payment_backend_pk>\w+)/$',
CallbackView.as_view(),
name='lingo-callback-payment-backend',
),
url(r'^lingo/return/(?P<regie_pk>\w+)/$', ReturnView.as_view(), name='lingo-return'),
url(r'^lingo/return-payment-backend/(?P<payment_backend_pk>\w+)/(?P<transaction_signature>.+)/$',
ReturnView.as_view(), name='lingo-return-payment-backend'),
url(r'^manage/lingo/', decorated_includes(manager_required,
include(lingo_manager_urls))),
url(r'^lingo/item/(?P<regie_id>[\w,-]+)/(?P<item_crypto_id>[\w,-]+)/pdf$',
ItemDownloadView.as_view(), name='download-item-pdf'),
url(r'^lingo/item/(?P<regie_id>[\w,-]+)/(?P<item_crypto_id>[\w,-]+)/$',
ItemView.as_view(), name='view-item'),
url(r'^lingo/item/(?P<item_signature>.+)/pay$',
BasketItemPayView.as_view(), name='basket-item-pay-view'),
url(r'^lingo/payment-status$',
PaymentStatusView.as_view(), name='payment-status'),
url(r'^lingo/self-invoice/(?P<cell_id>\w+)/$', SelfInvoiceView.as_view(),
name='lingo-self-invoice'),
url(
r'^lingo/return-payment-backend/(?P<payment_backend_pk>\w+)/(?P<transaction_signature>.+)/$',
ReturnView.as_view(),
name='lingo-return-payment-backend',
),
url(r'^manage/lingo/', decorated_includes(manager_required, include(lingo_manager_urls))),
url(
r'^lingo/item/(?P<regie_id>[\w,-]+)/(?P<item_crypto_id>[\w,-]+)/pdf$',
ItemDownloadView.as_view(),
name='download-item-pdf',
),
url(
r'^lingo/item/(?P<regie_id>[\w,-]+)/(?P<item_crypto_id>[\w,-]+)/$',
ItemView.as_view(),
name='view-item',
),
url(r'^lingo/item/(?P<item_signature>.+)/pay$', BasketItemPayView.as_view(), name='basket-item-pay-view'),
url(r'^lingo/payment-status$', PaymentStatusView.as_view(), name='payment-status'),
url(r'^lingo/self-invoice/(?P<cell_id>\w+)/$', SelfInvoiceView.as_view(), name='lingo-self-invoice'),
]

View File

@ -45,9 +45,17 @@ from combo.utils import check_request_signature, aes_hex_decrypt, DecryptionErro
from combo.profile.utils import get_user_from_name_id
from combo.public.views import publish_page
from .models import (Regie, BasketItem, Transaction, TransactionOperation,
LingoBasketCell, SelfDeclaredInvoicePayment, PaymentBackend, EXPIRED,
RemoteInvoiceException)
from .models import (
Regie,
BasketItem,
Transaction,
TransactionOperation,
LingoBasketCell,
SelfDeclaredInvoicePayment,
PaymentBackend,
EXPIRED,
RemoteInvoiceException,
)
from .utils import signing_dumps, signing_loads
logger = logging.getLogger(__name__)
@ -68,18 +76,23 @@ def get_eopayment_object(request, regie_or_payment_backend, transaction_id=None)
if isinstance(regie_or_payment_backend, Regie):
payment_backend = regie_or_payment_backend.payment_backend
options = payment_backend.service_options
options.update({
'automatic_return_url': request.build_absolute_uri(
reverse('lingo-callback-payment-backend',
kwargs={'payment_backend_pk': payment_backend.id})),
})
options.update(
{
'automatic_return_url': request.build_absolute_uri(
reverse('lingo-callback-payment-backend', kwargs={'payment_backend_pk': payment_backend.id})
),
}
)
if transaction_id:
options['normal_return_url'] = request.build_absolute_uri(
reverse('lingo-return-payment-backend', kwargs={
'payment_backend_pk': payment_backend.id,
'transaction_signature': signing_dumps(transaction_id)
})
reverse(
'lingo-return-payment-backend',
kwargs={
'payment_backend_pk': payment_backend.id,
'transaction_signature': signing_dumps(transaction_id),
},
)
)
return eopayment.Payment(payment_backend.service, options)
@ -144,8 +157,7 @@ class AddBasketItemApiView(View):
extra = request_body.get('extra', {})
if 'amount' not in request.GET and not 'amount' in request_body and \
not 'amount' in extra:
if 'amount' not in request.GET and not 'amount' in request_body and not 'amount' in extra:
return BadRequestJsonResponse('missing amount parameter')
if 'display_name' not in request_body:
@ -227,8 +239,13 @@ class AddBasketItemApiView(View):
if item.regie.extra_fees_ws_url:
BadRequestJsonResponse('can not compute extra fees with anonymous user')
return JsonResponse({'result': 'success', 'id': str(item.id),
'payment_url': request.build_absolute_uri(item.payment_url)})
return JsonResponse(
{
'result': 'success',
'id': str(item.id),
'payment_url': request.build_absolute_uri(item.payment_url),
}
)
class RemoveBasketItemApiView(View):
@ -293,8 +310,9 @@ class ValidateTransactionApiView(View):
try:
transaction = Transaction.objects.get(id=request.GET['transaction_id'])
except Transaction.DoesNotExist:
logger.warning(u'received validate request for unknown transaction %s',
request.GET['transaction_id'])
logger.warning(
u'received validate request for unknown transaction %s', request.GET['transaction_id']
)
raise Http404
payment = get_eopayment_object(request, transaction.regie)
@ -309,10 +327,8 @@ class ValidateTransactionApiView(View):
logger.info(u'bank validation result: %r', result)
operation = TransactionOperation(
transaction=transaction,
kind='validation',
amount=amount,
bank_result=result)
transaction=transaction, kind='validation', amount=amount, bank_result=result
)
operation.save()
return JsonResponse({'err': 0, 'extra': result})
@ -332,8 +348,9 @@ class CancelTransactionApiView(View):
try:
transaction = Transaction.objects.get(id=request.GET['transaction_id'])
except Transaction.DoesNotExist:
logger.warning(u'received validate request for unknown transaction %s',
request.GET['transaction_id'])
logger.warning(
u'received validate request for unknown transaction %s', request.GET['transaction_id']
)
raise Http404
payment = get_eopayment_object(request, transaction.regie)
@ -347,8 +364,9 @@ class CancelTransactionApiView(View):
return JsonResponse({'err': 1, 'e': force_text(e)})
logger.info(u'bank cancellation result: %r', result)
operation = TransactionOperation(transaction=transaction,
kind='cancellation', amount=amount, bank_result=result)
operation = TransactionOperation(
transaction=transaction, kind='cancellation', amount=amount, bank_result=result
)
operation.save()
return JsonResponse({'err': 0, 'extra': result})
@ -357,8 +375,8 @@ class CancelTransactionApiView(View):
class PayMixin(object):
@atomic
def handle_payment(
self, request, regie, items, remote_items, next_url='/', email='', firstname='',
lastname=''):
self, request, regie, items, remote_items, next_url='/', email='', firstname='', lastname=''
):
# check contract
if bool(len(items)) == bool(len(remote_items)):
messages.error(request, _('Items to pay are missing or are not of the same type (local/remote).'))
@ -372,7 +390,8 @@ class PayMixin(object):
if total_amount < regie.payment_min_amount:
messages.warning(request, _(u'Minimal payment amount is %s €.') % regie.payment_min_amount)
return HttpResponseRedirect(
get_payment_status_view(next_url=next_url if remote_items else items[0].source_url))
get_payment_status_view(next_url=next_url if remote_items else items[0].source_url)
)
for item in items:
if item.regie != regie:
@ -395,17 +414,17 @@ class PayMixin(object):
transaction.amount = total_amount
payment = get_eopayment_object(request, regie, transaction.pk)
kwargs = {
'email': email, 'first_name': firstname, 'last_name': lastname
}
kwargs = {'email': email, 'first_name': firstname, 'last_name': lastname}
kwargs['merchant_name'] = settings.TEMPLATE_VARS.get('global_title') or 'Compte Citoyen'
kwargs['items_info'] = []
for item in remote_items or items:
kwargs['items_info'].append({
'text': item.subject,
'amount': item.amount,
'reference_id': item.reference_id,
})
kwargs['items_info'].append(
{
'text': item.subject,
'amount': item.amount,
'reference_id': item.reference_id,
}
)
if items:
capture_date = items[0].capture_date
@ -427,7 +446,7 @@ class PayMixin(object):
EOPAYMENT_REQUEST_KWARGS_PREFIX = 'eopayment_request_kwargs_'
for key in item.request_data:
if key.startswith(EOPAYMENT_REQUEST_KWARGS_PREFIX):
arg_name = key[len(EOPAYMENT_REQUEST_KWARGS_PREFIX):]
arg_name = key[len(EOPAYMENT_REQUEST_KWARGS_PREFIX) :]
kwargs[arg_name] = item.request_data[key]
if regie.transaction_options:
kwargs.update(regie.transaction_options)
@ -437,16 +456,20 @@ class PayMixin(object):
logger.error('failed to initiate payment request: %s', e)
messages.error(request, _('Failed to initiate payment request'))
return HttpResponseRedirect(get_payment_status_view(next_url=next_url))
logger.info(u'emitted payment request with id %s', smart_text(order_id), extra={
'eopayment_order_id': smart_text(order_id), 'eopayment_data': repr(data)})
logger.info(
u'emitted payment request with id %s',
smart_text(order_id),
extra={'eopayment_order_id': smart_text(order_id), 'eopayment_data': repr(data)},
)
transaction.order_id = order_id
transaction.save()
# store the next url in session in order to be able to redirect to
# it if payment is canceled
if next_url:
request.session.setdefault('lingo_next_url',
{})[str(transaction.pk)] = request.build_absolute_uri(next_url)
request.session.setdefault('lingo_next_url', {})[
str(transaction.pk)
] = request.build_absolute_uri(next_url)
request.session.modified = True
if kind == eopayment.URL:
@ -496,17 +519,17 @@ class PayView(PayMixin, View):
if regie.can_pay_only_one_basket_item and (len(items) > 1 or len(remote_items) > 1):
messages.error(request, _('Grouping basket items is not allowed.'))
logger.error('lingo: regie can only pay one basket item, but handle_payment() received',
extra={'regie': str(regie), 'items': items, 'remote_items': remote_items})
logger.error(
'lingo: regie can only pay one basket item, but handle_payment() received',
extra={'regie': str(regie), 'items': items, 'remote_items': remote_items},
)
return HttpResponseRedirect(next_url)
if items:
capture_date = items[0].capture_date
for item in items:
if item.capture_date != capture_date:
messages.error(
request,
_(u'Invalid grouping for basket items: different capture dates.'))
messages.error(request, _(u'Invalid grouping for basket items: different capture dates.'))
return HttpResponseRedirect(next_url)
if user:
@ -534,7 +557,6 @@ def get_payment_status_view(transaction_id=None, next_url=None):
class BasketItemPayView(PayMixin, View):
def get(self, request, *args, **kwargs):
next_url = request.GET.get('next_url')
firstname = request.GET.get('firstname', '')
@ -565,8 +587,14 @@ class BasketItemPayView(PayMixin, View):
next_url = item.source_url
return self.handle_payment(
request=request, regie=regie, items=[item], remote_items=[], next_url=next_url, email=email,
firstname=firstname, lastname=lastname
request=request,
regie=regie,
items=[item],
remote_items=[],
next_url=next_url,
email=email,
firstname=firstname,
lastname=lastname,
)
@ -575,7 +603,6 @@ class PaymentException(Exception):
class UnsignedPaymentException(PaymentException):
def __init__(self, transaction, *args, **kwargs):
super(UnsignedPaymentException, self).__init__(*args, **kwargs)
self.transaction = transaction
@ -588,14 +615,9 @@ class UnknownPaymentException(PaymentException):
class PaymentView(View):
def handle_response(self, request, backend_response, **kwargs):
if 'regie_pk' in kwargs:
payment_backend = get_object_or_404(
Regie,
pk=kwargs['regie_pk']
).payment_backend
payment_backend = get_object_or_404(Regie, pk=kwargs['regie_pk']).payment_backend
elif 'payment_backend_pk' in kwargs:
payment_backend = get_object_or_404(
PaymentBackend,
pk=kwargs['payment_backend_pk'])
payment_backend = get_object_or_404(PaymentBackend, pk=kwargs['payment_backend_pk'])
else:
return HttpResponseBadRequest("A payment backend or regie primary key must be specified")
@ -605,8 +627,11 @@ class PaymentView(View):
try:
payment_response = payment.response(backend_response, **extra_info)
except eopayment.PaymentException as e:
logger.error(u'failed to process payment response: %s', e,
extra={'eopayment_raw_response': repr(backend_response)})
logger.error(
u'failed to process payment response: %s',
e,
extra={'eopayment_raw_response': repr(backend_response)},
)
raise PaymentException('Failed to process payment response')
extra_info = {
@ -618,16 +643,22 @@ class PaymentView(View):
try:
transaction = Transaction.objects.get(order_id=payment_response.order_id)
except Transaction.DoesNotExist:
logger.warning(u'received unknown payment response with id %s',
smart_text(payment_response.order_id), extra=extra_info)
logger.warning(
u'received unknown payment response with id %s',
smart_text(payment_response.order_id),
extra=extra_info,
)
raise UnknownPaymentException('Received unknown payment response')
else:
extra_info['lingo_transaction_id'] = transaction.pk
if transaction.user:
# let hobo logger filter handle the extraction of user's infos
extra_info['user'] = transaction.user
logger.info(u'received known payment response with id %s',
smart_text(payment_response.order_id), extra=extra_info)
logger.info(
u'received known payment response with id %s',
smart_text(payment_response.order_id),
extra=extra_info,
)
if transaction.status == payment_response.result:
# return early if transaction status didn't change (it means the
@ -636,22 +667,27 @@ class PaymentView(View):
return transaction
if transaction.status and transaction.status != payment_response.result:
logger.info(u'received payment notification on existing transaction '
'(status: %s, new status: %s)' % (
transaction.status, payment_response.result))
logger.info(
u'received payment notification on existing transaction '
'(status: %s, new status: %s)' % (transaction.status, payment_response.result)
)
# check if transaction belongs to right regie
if not transaction.regie.payment_backend == payment_backend:
logger.warning(u'received payment for inappropriate payment backend '
'(expecteds: %s, received: %s)' % (
transaction.regie.payment_backend, payment_backend))
logger.warning(
u'received payment for inappropriate payment backend '
'(expecteds: %s, received: %s)' % (transaction.regie.payment_backend, payment_backend)
)
raise PaymentException('Invalid payment regie')
if not payment_response.signed and not payment_response.result == eopayment.CANCELLED:
# we accept unsigned cancellation requests as some platforms do
# that :/
logger.warning(u'received unsigned payment response with id %s',
smart_text(payment_response.order_id), extra=extra_info)
logger.warning(
u'received unsigned payment response with id %s',
smart_text(payment_response.order_id),
extra=extra_info,
)
raise UnsignedPaymentException(transaction, 'Received unsigned payment response')
transaction.status = payment_response.result
@ -666,8 +702,12 @@ class PaymentView(View):
elif payment_response.transaction_date != transaction.bank_transaction_date:
# XXX: don't know if it can happen, but I would like to know when it does
# as for differed payments there can be multiple notifications.
logger.error('new transaction_date for transaction %s was %s, received %s',
transaction.id, transaction.bank_transaction_date, payment_response.transaction_date)
logger.error(
'new transaction_date for transaction %s was %s, received %s',
transaction.id,
transaction.bank_transaction_date,
payment_response.transaction_date,
)
transaction.save()
if payment_response.result == eopayment.WAITING:
@ -729,7 +769,9 @@ class ReturnView(PaymentView):
return self.handle_return(request, request.environ['QUERY_STRING'], **kwargs)
def post(self, request, *args, **kwargs):
return self.handle_return(request, force_text(request.body) or request.environ['QUERY_STRING'], **kwargs)
return self.handle_return(
request, force_text(request.body) or request.environ['QUERY_STRING'], **kwargs
)
def handle_return(self, request, backend_response, **kwargs):
payment_extra_info = {'redirect': True}
@ -764,8 +806,9 @@ class ReturnView(PaymentView):
return HttpResponseRedirect(get_payment_status_view(transaction_id))
return HttpResponseRedirect(get_basket_url())
except PaymentException as e:
messages.error(request, _('We are sorry but the payment service '
'failed to provide a correct answer.'))
messages.error(
request, _('We are sorry but the payment service ' 'failed to provide a correct answer.')
)
if transaction_id:
return HttpResponseRedirect(get_payment_status_view(transaction_id))
return HttpResponseRedirect(get_basket_url())
@ -778,8 +821,7 @@ class ReturnView(PaymentView):
# return to basket page if there are still items to pay
if request.user.is_authenticated:
remaining_basket_items = BasketItem.get_items_to_be_paid(
user=self.request.user).count()
remaining_basket_items = BasketItem.get_items_to_be_paid(user=self.request.user).count()
if remaining_basket_items:
return HttpResponseRedirect(get_basket_url())
return HttpResponseRedirect('/')
@ -865,8 +907,7 @@ class CancelItemView(DetailView):
def post(self, request, *args, **kwargs):
if not request.user.is_authenticated:
messages.error(request, _('An error occured when removing the item. '
'(no authenticated user)'))
messages.error(request, _('An error occured when removing the item. ' '(no authenticated user)'))
return HttpResponseRedirect(get_basket_url())
if not self.get_object().user_cancellable:
messages.error(request, _('This item cannot be removed.'))
@ -879,7 +920,7 @@ class CancelItemView(DetailView):
class SelfInvoiceView(View):
http_method_names = ['get', 'options']
http_method_names = ['get', 'options']
@csrf_exempt
def dispatch(self, *args, **kwargs):
@ -907,8 +948,7 @@ class SelfInvoiceView(View):
continue
if invoice.total_amount != invoice_amount:
continue
url = reverse('view-item', kwargs={
'regie_id': regie.id, 'item_crypto_id': invoice.crypto_id})
url = reverse('view-item', kwargs={'regie_id': regie.id, 'item_crypto_id': invoice.crypto_id})
break
else:
msg = _('Sorry, no invoice were found with that number and amount.')
@ -989,20 +1029,14 @@ class TransactionStatusApiView(View):
return HttpResponseForbidden(error_msg)
if transaction.is_paid():
data = {
'wait': False,
'error': False,
'error_msg': ''
}
data = {'wait': False, 'error': False, 'error_msg': ''}
return JsonResponse(data=data)
if transaction.status in (
eopayment.ERROR, eopayment.DENIED, EXPIRED
):
if transaction.status in (eopayment.ERROR, eopayment.DENIED, EXPIRED):
data = {
'wait': True,
'error': True,
'error_msg': _('Payment error, you can continue and make another payment')
'error_msg': _('Payment error, you can continue and make another payment'),
}
return JsonResponse(data=data)
@ -1010,13 +1044,9 @@ class TransactionStatusApiView(View):
data = {
'wait': True,
'error': False,
'error_msg': _('Payment cancelled, you can continue and make another payment')
'error_msg': _('Payment cancelled, you can continue and make another payment'),
}
return JsonResponse(data=data)
data = {
'wait': True,
'error': False,
'error_msg': ''
}
data = {'wait': True, 'error': False, 'error_msg': ''}
return JsonResponse(data=data)

View File

@ -25,10 +25,11 @@ class AppConfig(django.apps.AppConfig):
def get_before_urls(self):
from . import urls
return urls.urlpatterns
def get_extra_manager_actions(self):
return [{'href': reverse('maps-manager-homepage'),
'text': _('Maps')}]
return [{'href': reverse('maps-manager-homepage'), 'text': _('Maps')}]
default_app_config = 'combo.apps.maps.AppConfig'

View File

@ -49,13 +49,22 @@ class MapLayerForm(forms.ModelForm):
# new widget for icon field
self.fields['icon'].widget = IconRadioSelect()
self.fields['icon'].choices = list(
sorted(self.fields['icon'].choices, key=lambda x: slugify(force_text(x[1]))))
sorted(self.fields['icon'].choices, key=lambda x: slugify(force_text(x[1])))
)
if self.instance.kind == 'geojson':
todelete_fields = ['tiles_template_url', 'tiles_attribution', 'tiles_default']
else:
todelete_fields = [
'geojson_url', 'marker_colour', 'icon', 'icon_colour', 'cache_duration',
'include_user_identifier', 'properties', 'geojson_query_parameter', 'geojson_accepts_circle_param']
'geojson_url',
'marker_colour',
'icon',
'icon_colour',
'cache_duration',
'include_user_identifier',
'properties',
'geojson_query_parameter',
'geojson_accepts_circle_param',
]
for field in todelete_fields:
if field in self.fields:
del self.fields[field]
@ -74,9 +83,7 @@ class MapLayerOptionsForm(forms.ModelForm):
class Meta:
model = MapLayerOptions
fields = ['map_layer', 'opacity']
widgets = {
'opacity': forms.NumberInput(attrs={'step': 0.1, 'min': 0, 'max': 1})
}
widgets = {'opacity': forms.NumberInput(attrs={'step': 0.1, 'min': 0, 'max': 1})}
def __init__(self, *args, **kwargs):
self.kind = kwargs.pop('kind')

View File

@ -82,13 +82,16 @@ class MapCellAddLayer(CreateView):
PageSnapshot.take(
self.cell.page,
request=self.request,
comment=_('added layer "%(layer)s" to cell "%(cell)s"') % {'layer': form.instance.map_layer, 'cell': self.cell})
comment=_('added layer "%(layer)s" to cell "%(cell)s"')
% {'layer': form.instance.map_layer, 'cell': self.cell},
)
return super(MapCellAddLayer, self).form_valid(form)
def get_success_url(self):
return '%s#cell-%s' % (
reverse('combo-manager-page-view', kwargs={'pk': self.kwargs.get('page_pk')}),
self.kwargs['cell_reference'])
self.kwargs['cell_reference'],
)
map_cell_add_layer = MapCellAddLayer.as_view()
@ -103,10 +106,7 @@ class MapCellEditLayer(UpdateView):
self.cell = CellBase.get_cell(kwargs['cell_reference'], page=kwargs['page_pk'])
except Map.DoesNotExist:
raise Http404
self.object = get_object_or_404(
MapLayerOptions,
pk=kwargs['layeroptions_pk'],
map_cell=self.cell)
self.object = get_object_or_404(MapLayerOptions, pk=kwargs['layeroptions_pk'], map_cell=self.cell)
return super(MapCellEditLayer, self).dispatch(request, *args, **kwargs)
def get_object(self, *args, **kwargs):
@ -121,13 +121,16 @@ class MapCellEditLayer(UpdateView):
PageSnapshot.take(
self.cell.page,
request=self.request,
comment=_('changed options of layer "%(layer)s" in cell "%(cell)s"') % {'layer': form.instance.map_layer, 'cell': self.cell})
comment=_('changed options of layer "%(layer)s" in cell "%(cell)s"')
% {'layer': form.instance.map_layer, 'cell': self.cell},
)
return super(MapCellEditLayer, self).form_valid(form)
def get_success_url(self):
return '%s#cell-%s' % (
reverse('combo-manager-page-view', kwargs={'pk': self.kwargs.get('page_pk')}),
self.kwargs['cell_reference'])
self.kwargs['cell_reference'],
)
map_cell_edit_layer = MapCellEditLayer.as_view()
@ -141,10 +144,7 @@ class MapCellDeleteLayer(DeleteView):
self.cell = CellBase.get_cell(kwargs['cell_reference'], page=kwargs['page_pk'])
except Map.DoesNotExist:
raise Http404
self.object = get_object_or_404(
MapLayerOptions,
pk=kwargs['layeroptions_pk'],
map_cell=self.cell)
self.object = get_object_or_404(MapLayerOptions, pk=kwargs['layeroptions_pk'], map_cell=self.cell)
return super(MapCellDeleteLayer, self).dispatch(request, *args, **kwargs)
def get_object(self, *args, **kwargs):
@ -155,13 +155,16 @@ class MapCellDeleteLayer(DeleteView):
PageSnapshot.take(
self.cell.page,
request=self.request,
comment=_('removed layer "%(layer)s" from cell "%(cell)s"') % {'layer': self.object.map_layer, 'cell': self.cell})
comment=_('removed layer "%(layer)s" from cell "%(cell)s"')
% {'layer': self.object.map_layer, 'cell': self.cell},
)
return response
def get_success_url(self):
return '%s#cell-%s' % (
reverse('combo-manager-page-view', kwargs={'pk': self.kwargs.get('page_pk')}),
self.kwargs['cell_reference'])
self.kwargs['cell_reference'],
)
map_cell_delete_layer = MapCellDeleteLayer.as_view()

View File

@ -7,21 +7,34 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
]
dependencies = []
operations = [
migrations.CreateModel(
name='MapLayer',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('label', models.CharField(max_length=128, verbose_name='Label')),
('slug', models.SlugField(verbose_name='Identifier')),
('geojson_url', models.CharField(max_length=1024, verbose_name='Geojson URL')),
('marker_colour', models.CharField(default=b'#0000FF', max_length=7, verbose_name='Marker colour')),
('icon', models.CharField(blank=True, max_length=32, null=True, verbose_name='Marker icon', choices=ICONS)),
('icon_colour', models.CharField(default=b'#000000', max_length=7, verbose_name='Icon colour')),
(
'marker_colour',
models.CharField(default=b'#0000FF', max_length=7, verbose_name='Marker colour'),
),
(
'icon',
models.CharField(
blank=True, max_length=32, null=True, verbose_name='Marker icon', choices=ICONS
),
),
(
'icon_colour',
models.CharField(default=b'#000000', max_length=7, verbose_name='Icon colour'),
),
],
options={'ordering': ('label',)}
options={'ordering': ('label',)},
),
]

View File

@ -16,18 +16,77 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Map',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('placeholder', models.CharField(max_length=20)),
('order', models.PositiveIntegerField()),
('slug', models.SlugField(verbose_name='Slug', blank=True)),
('extra_css_class', models.CharField(max_length=100, verbose_name='Extra classes for CSS styling', blank=True)),
(
'extra_css_class',
models.CharField(
max_length=100, verbose_name='Extra classes for CSS styling', blank=True
),
),
('public', models.BooleanField(default=True, verbose_name='Public')),
('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
(
'restricted_to_unlogged',
models.BooleanField(default=False, verbose_name='Restrict to unlogged users'),
),
('last_update_timestamp', models.DateTimeField(auto_now=True)),
('title', models.CharField(max_length=150, verbose_name='Title', blank=True)),
('initial_zoom', models.CharField(default=b'13', max_length=2, verbose_name='Initial zoom level', choices=[(b'0', 'Whole world'), (b'9', 'Wide area'), (b'11', 'Area'), (b'13', 'Town'), (b'16', 'Small road'), (b'18', 'Neighbourhood'), (b'19', 'Ant')])),
('min_zoom', models.CharField(default=b'0', max_length=2, verbose_name='Minimal zoom level', choices=[(b'0', 'Whole world'), (b'9', 'Wide area'), (b'11', 'Area'), (b'13', 'Town'), (b'16', 'Small road'), (b'18', 'Neighbourhood'), (b'19', 'Ant')])),
('max_zoom', models.CharField(default=19, max_length=2, verbose_name='Maximal zoom level', choices=[(b'0', 'Whole world'), (b'9', 'Wide area'), (b'11', 'Area'), (b'13', 'Town'), (b'16', 'Small road'), (b'18', 'Neighbourhood'), (b'19', 'Ant')])),
(
'initial_zoom',
models.CharField(
default=b'13',
max_length=2,
verbose_name='Initial zoom level',
choices=[
(b'0', 'Whole world'),
(b'9', 'Wide area'),
(b'11', 'Area'),
(b'13', 'Town'),
(b'16', 'Small road'),
(b'18', 'Neighbourhood'),
(b'19', 'Ant'),
],
),
),
(
'min_zoom',
models.CharField(
default=b'0',
max_length=2,
verbose_name='Minimal zoom level',
choices=[
(b'0', 'Whole world'),
(b'9', 'Wide area'),
(b'11', 'Area'),
(b'13', 'Town'),
(b'16', 'Small road'),
(b'18', 'Neighbourhood'),
(b'19', 'Ant'),
],
),
),
(
'max_zoom',
models.CharField(
default=19,
max_length=2,
verbose_name='Maximal zoom level',
choices=[
(b'0', 'Whole world'),
(b'9', 'Wide area'),
(b'11', 'Area'),
(b'13', 'Town'),
(b'16', 'Small road'),
(b'18', 'Neighbourhood'),
(b'19', 'Ant'),
],
),
),
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
('layers', models.ManyToManyField(to='maps.MapLayer', verbose_name='Layers', blank=True)),
('page', models.ForeignKey(to='data.Page', on_delete=models.CASCADE)),

View File

@ -14,7 +14,9 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='maplayer',
name='cache_duration',
field=models.PositiveIntegerField(default=60, help_text='In seconds.', verbose_name='Cache duration'),
field=models.PositiveIntegerField(
default=60, help_text='In seconds.', verbose_name='Cache duration'
),
),
migrations.AddField(
model_name='maplayer',

View File

@ -14,6 +14,15 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='map',
name='initial_state',
field=models.CharField(default=b'default-position', max_length=20, verbose_name='Initial state', choices=[(b'default-position', 'Centered on default position'), (b'device-location', 'Centered on device location'), (b'fit-markers', 'Centered to fit all markers')]),
field=models.CharField(
default=b'default-position',
max_length=20,
verbose_name='Initial state',
choices=[
(b'default-position', 'Centered on default position'),
(b'device-location', 'Centered on device location'),
(b'fit-markers', 'Centered to fit all markers'),
],
),
),
]

View File

@ -14,6 +14,11 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='map',
name='marker_behaviour_onclick',
field=models.CharField(default=b'none', max_length=32, verbose_name='Marker behaviour on click', choices=[(b'none', 'Nothing'), (b'display_data', 'Display data in popup')]),
field=models.CharField(
default=b'none',
max_length=32,
verbose_name='Marker behaviour on click',
choices=[(b'none', 'Nothing'), (b'display_data', 'Display data in popup')],
),
),
]

View File

@ -17,32 +17,57 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='maplayer',
name='properties',
field=models.CharField(blank=True, help_text='List of properties to include, separated by commas', max_length=500, verbose_name='Properties'),
field=models.CharField(
blank=True,
help_text='List of properties to include, separated by commas',
max_length=500,
verbose_name='Properties',
),
),
migrations.AlterField(
model_name='map',
name='initial_state',
field=models.CharField(choices=[('default-position', 'Centered on default position'), ('device-location', 'Centered on device location'), ('fit-markers', 'Centered to fit all markers')], default='default-position', max_length=20, verbose_name='Initial state'),
field=models.CharField(
choices=[
('default-position', 'Centered on default position'),
('device-location', 'Centered on device location'),
('fit-markers', 'Centered to fit all markers'),
],
default='default-position',
max_length=20,
verbose_name='Initial state',
),
),
migrations.AlterField(
model_name='map',
name='initial_zoom',
field=models.CharField(choices=ZOOM_LEVELS, default='13', max_length=2, verbose_name='Initial zoom level'),
field=models.CharField(
choices=ZOOM_LEVELS, default='13', max_length=2, verbose_name='Initial zoom level'
),
),
migrations.AlterField(
model_name='map',
name='marker_behaviour_onclick',
field=models.CharField(choices=[('none', 'Nothing'), ('display_data', 'Display data in popup')], default='none', max_length=32, verbose_name='Marker behaviour on click'),
field=models.CharField(
choices=[('none', 'Nothing'), ('display_data', 'Display data in popup')],
default='none',
max_length=32,
verbose_name='Marker behaviour on click',
),
),
migrations.AlterField(
model_name='map',
name='max_zoom',
field=models.CharField(choices=ZOOM_LEVELS, default=19, max_length=2, verbose_name='Maximal zoom level'),
field=models.CharField(
choices=ZOOM_LEVELS, default=19, max_length=2, verbose_name='Maximal zoom level'
),
),
migrations.AlterField(
model_name='map',
name='min_zoom',
field=models.CharField(choices=ZOOM_LEVELS, default='0', max_length=2, verbose_name='Minimal zoom level'),
field=models.CharField(
choices=ZOOM_LEVELS, default='0', max_length=2, verbose_name='Minimal zoom level'
),
),
migrations.AlterField(
model_name='maplayer',

View File

@ -17,7 +17,12 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='MapLayerOptions',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
(
'id',
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name='ID'
),
),
],
options={
'db_table': 'maps_map_layers',
@ -26,23 +31,32 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='map',
name='layers',
field=models.ManyToManyField(blank=True, through='maps.MapLayerOptions', to='maps.MapLayer', verbose_name='Layers'),
field=models.ManyToManyField(
blank=True, through='maps.MapLayerOptions', to='maps.MapLayer', verbose_name='Layers'
),
),
migrations.AddField(
model_name='maplayeroptions',
name='map_cell',
field=models.ForeignKey(db_column='map_id', on_delete=django.db.models.deletion.CASCADE, to='maps.Map'),
field=models.ForeignKey(
db_column='map_id', on_delete=django.db.models.deletion.CASCADE, to='maps.Map'
),
),
migrations.AddField(
model_name='maplayeroptions',
name='map_layer',
field=models.ForeignKey(db_column='maplayer_id', verbose_name='Layer', on_delete=django.db.models.deletion.CASCADE, to='maps.MapLayer'),
field=models.ForeignKey(
db_column='maplayer_id',
verbose_name='Layer',
on_delete=django.db.models.deletion.CASCADE,
to='maps.MapLayer',
),
),
migrations.AlterUniqueTogether(
name='maplayeroptions',
unique_together=set([('map_cell', 'map_layer')]),
),
],
database_operations=[]
database_operations=[],
)
]

View File

@ -15,7 +15,9 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='maplayer',
name='kind',
field=models.CharField(choices=[('tiles', 'Tiles'), ('geojson', 'GeoJSON')], default='geojson', max_length=10),
field=models.CharField(
choices=[('tiles', 'Tiles'), ('geojson', 'GeoJSON')], default='geojson', max_length=10
),
),
migrations.AddField(
model_name='maplayer',
@ -24,10 +26,12 @@ class Migration(migrations.Migration):
blank=True,
help_text=(
'For example: Map data &amp;copy; &lt;a href=&quot;https://openstreetmap.org&quot;&gt;OpenStreetMap&lt;/a&gt; contributors, '
'&lt;a href=&quot;http://creativecommons.org/licenses/by-sa/2.0/&quot;&gt;CC-BY-SA&lt;/a&gt;'),
'&lt;a href=&quot;http://creativecommons.org/licenses/by-sa/2.0/&quot;&gt;CC-BY-SA&lt;/a&gt;'
),
max_length=1024,
null=True,
verbose_name='Attribution'),
verbose_name='Attribution',
),
),
migrations.AddField(
model_name='maplayer',
@ -42,6 +46,7 @@ class Migration(migrations.Migration):
help_text='For example: https://tiles.entrouvert.org/hdm/{z}/{x}/{y}.png',
max_length=1024,
null=True,
verbose_name='Tiles URL'),
verbose_name='Tiles URL',
),
),
]

View File

@ -16,7 +16,13 @@ class Migration(migrations.Migration):
model_name='maplayeroptions',
name='opacity',
field=models.FloatField(
help_text='Float value between 0 (transparent) and 1 (opaque)', null=True,
validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='Opacity'),
help_text='Float value between 0 (transparent) and 1 (opaque)',
null=True,
validators=[
django.core.validators.MinValueValidator(0),
django.core.validators.MaxValueValidator(1),
],
verbose_name='Opacity',
),
),
]

View File

@ -15,7 +15,10 @@ class Migration(migrations.Migration):
model_name='maplayer',
name='geojson_query_parameter',
field=models.CharField(
blank=True, help_text='Name of the parameter to use for querying the GeoJSON layer (typically, q)',
max_length=100, verbose_name='Query parameter for fulltext requests'),
blank=True,
help_text='Name of the parameter to use for querying the GeoJSON layer (typically, q)',
max_length=100,
verbose_name='Query parameter for fulltext requests',
),
),
]

View File

@ -84,14 +84,16 @@ MARKER_BEHAVIOUR_ONCLICK = [
('display_data', _('Display data in popup')),
]
ZOOM_LEVELS = [ ('0', _('Whole world')),
('6', _('Country')),
('9', _('Wide area')),
('11', _('Area')),
('13', _('Town')),
('16', _('Small road')),
('18', _('Neighbourhood')),
('19', _('Ant')),]
ZOOM_LEVELS = [
('0', _('Whole world')),
('6', _('Country')),
('9', _('Wide area')),
('11', _('Area')),
('13', _('Town')),
('16', _('Small road')),
('18', _('Neighbourhood')),
('19', _('Ant')),
]
class MapLayerManager(models.Manager):
@ -112,33 +114,39 @@ class MapLayer(models.Model):
_('Tiles URL'),
max_length=1024,
help_text=_('For example: %s') % settings.COMBO_MAP_TILE_URLTEMPLATE,
blank=True, null=True)
blank=True,
null=True,
)
tiles_attribution = models.CharField(
_('Attribution'),
max_length=1024,
help_text=_('For example: %s') % escape(settings.COMBO_MAP_ATTRIBUTION),
blank=True, null=True)
blank=True,
null=True,
)
tiles_default = models.BooleanField(_('Default tiles layer'), default=False)
geojson_url = models.CharField(_('Geojson URL'), max_length=1024)
marker_colour = models.CharField(_('Marker colour'), max_length=7, default='#0000FF')
icon = models.CharField(_('Marker icon'), max_length=32, blank=True, null=True,
choices=ICONS)
icon = models.CharField(_('Marker icon'), max_length=32, blank=True, null=True, choices=ICONS)
icon_colour = models.CharField(_('Icon colour'), max_length=7, default='#000000')
cache_duration = models.PositiveIntegerField(_('Cache duration'), default=60, help_text=_('In seconds.'))
include_user_identifier = models.BooleanField(
_('Include user identifier in request'),
default=True)
properties = models.CharField(_('Properties'), max_length=500, blank=True,
help_text=_('List of properties to include, separated by commas'))
include_user_identifier = models.BooleanField(_('Include user identifier in request'), default=True)
properties = models.CharField(
_('Properties'),
max_length=500,
blank=True,
help_text=_('List of properties to include, separated by commas'),
)
geojson_query_parameter = models.CharField(
_("Query parameter for fulltext requests"),
max_length=100,
blank=True,
help_text=_('Name of the parameter to use for querying the GeoJSON layer (typically, q)'))
help_text=_('Name of the parameter to use for querying the GeoJSON layer (typically, q)'),
)
geojson_accepts_circle_param = models.BooleanField(
_('GeoJSON URL accepts a "cirle" parameter'),
default=False)
_('GeoJSON URL accepts a "cirle" parameter'), default=False
)
class Meta:
ordering = ('label',)
@ -158,7 +166,7 @@ class MapLayer(models.Model):
return self.label
def natural_key(self):
return (self.slug, )
return (self.slug,)
@classmethod
def get_default_tiles_layer(cls):
@ -169,8 +177,11 @@ class MapLayer(models.Model):
return [x.get_as_serialized_object() for x in MapLayer.objects.all()]
def get_as_serialized_object(self):
serialized_layer = json.loads(serializers.serialize('json', [self],
use_natural_foreign_keys=True, use_natural_primary_keys=True))[0]
serialized_layer = json.loads(
serializers.serialize(
'json', [self], use_natural_foreign_keys=True, use_natural_primary_keys=True
)
)[0]
del serialized_layer['model']
return serialized_layer
@ -214,8 +225,9 @@ class MapLayer(models.Model):
remote_service='auto',
cache_duration=self.cache_duration,
user=request.user if (request and self.include_user_identifier) else None,
without_user=not(self.include_user_identifier),
headers={'accept': 'application/json'})
without_user=not (self.include_user_identifier),
headers={'accept': 'application/json'},
)
if not response.ok:
return []
data = response.json()
@ -231,12 +243,13 @@ class MapLayer(models.Model):
if 'display_fields' in feature['properties']:
# w.c.s. content, filter fields on varnames
feature['properties']['display_fields'] = [
x for x in feature['properties']['display_fields']
if x.get('varname') in properties]
x for x in feature['properties']['display_fields'] if x.get('varname') in properties
]
else:
# classic geojson, filter properties
feature['properties'] = dict(
[x for x in feature['properties'].items() if x[0] in properties])
[x for x in feature['properties'].items() if x[0] in properties]
)
if request and not self.geojson_accepts_circle_param and distance_params:
geod = pyproj.Geod(ellps='WGS84')
@ -314,12 +327,9 @@ class MapLayer(models.Model):
label = '%(prefix)s%(label)s%(suffix)s' % {
'prefix': slot_template_data['prefix'],
'label': self.get_label_for_asset(),
'suffix': suffix
}
short_label = '%(prefix)s%(suffix)s' % {
'prefix': slot_template_data['prefix'],
'suffix': suffix
'suffix': suffix,
}
short_label = '%(prefix)s%(suffix)s' % {'prefix': slot_template_data['prefix'], 'suffix': suffix}
slots[slot_key] = {
'label': label,
'short_label': short_label,
@ -332,29 +342,23 @@ class MapLayer(models.Model):
class Map(CellBase):
title = models.CharField(_('Title'), max_length=150, blank=True)
initial_state = models.CharField(
_('Initial state'),
max_length=20,
choices=[
('default-position', _('Centered on default position')),
('device-location', _('Centered on device location')),
('fit-markers', _('Centered to fit all markers')),
],
default='default-position')
initial_zoom = models.CharField(_('Initial zoom level'), max_length=2,
choices=ZOOM_LEVELS, default='13')
min_zoom = models.CharField(_('Minimal zoom level'), max_length=2,
choices=ZOOM_LEVELS, default='0')
max_zoom = models.CharField(_('Maximal zoom level'), max_length=2,
choices=ZOOM_LEVELS, default=19)
group_markers = models.BooleanField(_('Group markers in clusters'), default=False)
marker_behaviour_onclick = models.CharField(_('Marker behaviour on click'), max_length=32,
default='none', choices=MARKER_BEHAVIOUR_ONCLICK)
layers = models.ManyToManyField(
MapLayer,
through='MapLayerOptions',
verbose_name=_('Layers'),
blank=True
_('Initial state'),
max_length=20,
choices=[
('default-position', _('Centered on default position')),
('device-location', _('Centered on device location')),
('fit-markers', _('Centered to fit all markers')),
],
default='default-position',
)
initial_zoom = models.CharField(_('Initial zoom level'), max_length=2, choices=ZOOM_LEVELS, default='13')
min_zoom = models.CharField(_('Minimal zoom level'), max_length=2, choices=ZOOM_LEVELS, default='0')
max_zoom = models.CharField(_('Maximal zoom level'), max_length=2, choices=ZOOM_LEVELS, default=19)
group_markers = models.BooleanField(_('Group markers in clusters'), default=False)
marker_behaviour_onclick = models.CharField(
_('Marker behaviour on click'), max_length=32, default='none', choices=MARKER_BEHAVIOUR_ONCLICK
)
layers = models.ManyToManyField(MapLayer, through='MapLayerOptions', verbose_name=_('Layers'), blank=True)
template_name = 'maps/map_cell.html'
manager_form_template = 'maps/map_cell_form.html'
@ -363,19 +367,31 @@ class Map(CellBase):
verbose_name = _('Map')
class Media:
js = ('/jsi18n',
'xstatic/leaflet.js', 'js/leaflet-gps.js', 'js/combo.map.js',
'xstatic/leaflet.markercluster.js',
'xstatic/leaflet-gesture-handling.min.js',
)
css = {'all': ('xstatic/leaflet.css', 'css/combo.map.css', 'xstatic/leaflet-gesture-handling.min.css')}
js = (
'/jsi18n',
'xstatic/leaflet.js',
'js/leaflet-gps.js',
'js/combo.map.js',
'xstatic/leaflet.markercluster.js',
'xstatic/leaflet-gesture-handling.min.js',
)
css = {
'all': ('xstatic/leaflet.css', 'css/combo.map.css', 'xstatic/leaflet-gesture-handling.min.css')
}
def get_default_position(self):
return settings.COMBO_MAP_DEFAULT_POSITION
def get_default_form_class(self):
fields = ('title', 'initial_state', 'initial_zoom', 'min_zoom',
'max_zoom', 'group_markers', 'marker_behaviour_onclick')
fields = (
'title',
'initial_state',
'initial_zoom',
'min_zoom',
'max_zoom',
'group_markers',
'marker_behaviour_onclick',
)
return forms.models.modelform_factory(self.__class__, fields=fields)
@classmethod
@ -385,45 +401,57 @@ class Map(CellBase):
def get_tiles_layers(self):
tiles_layers = []
options_qs = (
self.maplayeroptions_set
.filter(map_layer__kind='tiles')
self.maplayeroptions_set.filter(map_layer__kind='tiles')
.select_related('map_layer')
.order_by('-opacity'))
.order_by('-opacity')
)
for options in options_qs:
tiles_layers.append({
'tile_urltemplate': options.map_layer.tiles_template_url,
'map_attribution': options.map_layer.tiles_attribution,
'opacity': options.opacity or 0,
})
tiles_layers.append(
{
'tile_urltemplate': options.map_layer.tiles_template_url,
'map_attribution': options.map_layer.tiles_attribution,
'opacity': options.opacity or 0,
}
)
# check if at least one layer with opacity set to 1 exists
if any([l['opacity'] == 1 for l in tiles_layers]):
return tiles_layers
# add the default tiles layer
default_tiles_layer = MapLayer.get_default_tiles_layer()
if default_tiles_layer is not None:
tiles_layers.insert(0, {
'tile_urltemplate': default_tiles_layer.tiles_template_url,
'map_attribution': default_tiles_layer.tiles_attribution,
'opacity': 1,
})
tiles_layers.insert(
0,
{
'tile_urltemplate': default_tiles_layer.tiles_template_url,
'map_attribution': default_tiles_layer.tiles_attribution,
'opacity': 1,
},
)
else:
tiles_layers.insert(0, {
'tile_urltemplate': settings.COMBO_MAP_TILE_URLTEMPLATE,
'map_attribution': settings.COMBO_MAP_ATTRIBUTION,
'opacity': 1,
})
tiles_layers.insert(
0,
{
'tile_urltemplate': settings.COMBO_MAP_TILE_URLTEMPLATE,
'map_attribution': settings.COMBO_MAP_ATTRIBUTION,
'opacity': 1,
},
)
return tiles_layers
def get_geojson_layers(self):
if not self.pk:
return []
return [{'url': reverse('mapcell-geojson', kwargs={'cell_id': self.pk, 'layer_slug': l.slug}),
'slug': l.slug,
'icon': l.icon,
'icon_colour': l.icon_colour,
'marker_colour': l.marker_colour,
'properties': [x.strip() for x in l.properties.split(',')],
} for l in self.layers.filter(kind='geojson')]
return [
{
'url': reverse('mapcell-geojson', kwargs={'cell_id': self.pk, 'layer_slug': l.slug}),
'slug': l.slug,
'icon': l.icon,
'icon_colour': l.icon_colour,
'marker_colour': l.marker_colour,
'properties': [x.strip() for x in l.properties.split(',')],
}
for l in self.layers.filter(kind='geojson')
]
def get_cell_extra_context(self, context):
ctx = super(Map, self).get_cell_extra_context(context)
@ -454,7 +482,9 @@ class Map(CellBase):
return MapLayer.objects.filter(kind='tiles').exclude(pk__in=used_layers)
def export_subobjects(self):
return {'layers': [x.get_as_serialized_object() for x in MapLayerOptions.objects.filter(map_cell=self)]}
return {
'layers': [x.get_as_serialized_object() for x in MapLayerOptions.objects.filter(map_cell=self)]
}
@classmethod
def prepare_serialized_data(cls, cell_data):
@ -462,10 +492,7 @@ class Map(CellBase):
if 'layers' in cell_data['fields']:
layers = cell_data['fields'].pop('layers')
cell_data['layers'] = [
{
'fields': {'map_layer': layer},
'model': 'maps.maplayeroptions'
} for layer in layers
{'fields': {'map_layer': layer}, 'model': 'maps.maplayeroptions'} for layer in layers
]
return cell_data
@ -485,12 +512,12 @@ class Map(CellBase):
class MapLayerOptions(models.Model):
map_cell = models.ForeignKey(Map, on_delete=models.CASCADE, db_column='map_id')
map_layer = models.ForeignKey(MapLayer, verbose_name=_('Layer'), on_delete=models.CASCADE, db_column='maplayer_id')
map_layer = models.ForeignKey(
MapLayer, verbose_name=_('Layer'), on_delete=models.CASCADE, db_column='maplayer_id'
)
opacity = models.FloatField(
verbose_name=_('Opacity'),
validators=[
validators.MinValueValidator(0),
validators.MaxValueValidator(1)],
validators=[validators.MinValueValidator(0), validators.MaxValueValidator(1)],
null=True,
help_text=_('Float value between 0 (transparent) and 1 (opaque)'),
)
@ -501,7 +528,9 @@ class MapLayerOptions(models.Model):
def get_as_serialized_object(self):
serialized_options = json.loads(
serializers.serialize('json', [self], use_natural_foreign_keys=True, use_natural_primary_keys=True)
serializers.serialize(
'json', [self], use_natural_foreign_keys=True, use_natural_primary_keys=True
)
)[0]
del serialized_options['fields']['map_cell']
del serialized_options['pk']

View File

@ -24,25 +24,43 @@ from .views import GeojsonView
maps_manager_urls = [
url('^$', manager_views.ManagerHomeView.as_view(), name='maps-manager-homepage'),
url('^layers/add/(?P<kind>geojson|tiles)/$', manager_views.LayerAddView.as_view(), name='maps-manager-layer-add'),
url(r'^layers/(?P<slug>[\w-]+)/edit/$', manager_views.LayerEditView.as_view(),
name='maps-manager-layer-edit'),
url(r'^layers/(?P<slug>[\w-]+)/delete/$', manager_views.LayerDeleteView.as_view(),
name='maps-manager-layer-delete'),
url(r'^pages/(?P<page_pk>\d+)/cell/(?P<cell_reference>[\w_-]+)/add-layer/(?P<kind>geojson|tiles)/$',
url(
'^layers/add/(?P<kind>geojson|tiles)/$',
manager_views.LayerAddView.as_view(),
name='maps-manager-layer-add',
),
url(
r'^layers/(?P<slug>[\w-]+)/edit/$',
manager_views.LayerEditView.as_view(),
name='maps-manager-layer-edit',
),
url(
r'^layers/(?P<slug>[\w-]+)/delete/$',
manager_views.LayerDeleteView.as_view(),
name='maps-manager-layer-delete',
),
url(
r'^pages/(?P<page_pk>\d+)/cell/(?P<cell_reference>[\w_-]+)/add-layer/(?P<kind>geojson|tiles)/$',
manager_views.map_cell_add_layer,
name='maps-manager-cell-add-layer'),
url(r'^pages/(?P<page_pk>\d+)/cell/(?P<cell_reference>[\w_-]+)/layer/(?P<layeroptions_pk>\d+)/edit/$',
name='maps-manager-cell-add-layer',
),
url(
r'^pages/(?P<page_pk>\d+)/cell/(?P<cell_reference>[\w_-]+)/layer/(?P<layeroptions_pk>\d+)/edit/$',
manager_views.map_cell_edit_layer,
name='maps-manager-cell-edit-layer'),
url(r'^pages/(?P<page_pk>\d+)/cell/(?P<cell_reference>[\w_-]+)/layer/(?P<layeroptions_pk>\d+)/delete/$',
name='maps-manager-cell-edit-layer',
),
url(
r'^pages/(?P<page_pk>\d+)/cell/(?P<cell_reference>[\w_-]+)/layer/(?P<layeroptions_pk>\d+)/delete/$',
manager_views.map_cell_delete_layer,
name='maps-manager-cell-delete-layer'),
name='maps-manager-cell-delete-layer',
),
]
urlpatterns = [
url(r'^manage/maps/', decorated_includes(manager_required,
include(maps_manager_urls))),
url(r'^ajax/mapcell/geojson/(?P<cell_id>\d+)/(?P<layer_slug>[\w-]+)/$', GeojsonView.as_view(),
name='mapcell-geojson'),
url(r'^manage/maps/', decorated_includes(manager_required, include(maps_manager_urls))),
url(
r'^ajax/mapcell/geojson/(?P<cell_id>\d+)/(?P<layer_slug>[\w-]+)/$',
GeojsonView.as_view(),
name='mapcell-geojson',
),
]

View File

@ -24,15 +24,9 @@ from .models import Map
class GeojsonView(View):
def get(self, request, *args, **kwargs):
cell = get_object_or_404(
Map,
pk=kwargs['cell_id'])
layer = get_object_or_404(
cell.layers.all(),
kind='geojson',
slug=kwargs['layer_slug'])
cell = get_object_or_404(Map, pk=kwargs['cell_id'])
layer = get_object_or_404(cell.layers.all(), kind='geojson', slug=kwargs['layer_slug'])
if not cell.page.is_visible(request.user) or not cell.is_visible(user=request.user):
return HttpResponseForbidden()
geojson = layer.get_geojson(request)

View File

@ -17,12 +17,15 @@
import django.apps
from django.utils.translation import ugettext_lazy as _
class AppConfig(django.apps.AppConfig):
name = 'combo.apps.newsletters'
verbose_name = _('Newsletters')
def get_before_urls(self):
from . import urls
return urls.urlpatterns
default_app_config = 'combo.apps.newsletters.AppConfig'

View File

@ -66,19 +66,18 @@ class NewslettersManageForm(forms.Form):
for subscription in subscriptions:
if subscription['id'] == newsletter['id']:
initial = [t['id'] for t in subscription['transports']]
self.fields[newsletter['id']] = \
forms.MultipleChoiceField(label=newsletter['text'],
help_text=transport['id'],
choices=choices, initial=initial,
widget=forms.CheckboxSelectMultiple(),
required=False)
self.fields[newsletter['id']] = forms.MultipleChoiceField(
label=newsletter['text'],
help_text=transport['id'],
choices=choices,
initial=initial,
widget=forms.CheckboxSelectMultiple(),
required=False,
)
def save(self):
self.full_clean()
subscriptions = []
for key, value in self.cleaned_data.items():
subscriptions.append({
'id': key,
'transports': value
})
subscriptions.append({'id': key, 'transports': value})
self.instance.set_subscriptions(subscriptions, self.user, **self.params)

View File

@ -15,16 +15,38 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='NewslettersCell',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('placeholder', models.CharField(max_length=20)),
('order', models.PositiveIntegerField()),
('slug', models.SlugField(verbose_name='Slug', blank=True)),
('public', models.BooleanField(default=True, verbose_name='Public')),
('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
(
'restricted_to_unlogged',
models.BooleanField(default=False, verbose_name='Restrict to unlogged users'),
),
('title', models.CharField(max_length=128, verbose_name='Title')),
('url', models.URLField(max_length=128, verbose_name='Newsletters service url')),
('resources_restrictions', models.CharField(help_text='list of resources(themes) separated by commas', max_length=1024, verbose_name='resources restrictions', blank=True)),
('transports_restrictions', models.CharField(help_text='list of transports separated by commas', max_length=1024, verbose_name='transports restrictions', blank=True)),
(
'resources_restrictions',
models.CharField(
help_text='list of resources(themes) separated by commas',
max_length=1024,
verbose_name='resources restrictions',
blank=True,
),
),
(
'transports_restrictions',
models.CharField(
help_text='list of transports separated by commas',
max_length=1024,
verbose_name='transports restrictions',
blank=True,
),
),
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
('page', models.ForeignKey(to='data.Page', on_delete=models.CASCADE)),
],

View File

@ -40,14 +40,19 @@ class SubscriptionsSaveError(Exception):
@register_cell_class
class NewslettersCell(CellBase):
title = models.CharField(verbose_name=_('Title'), max_length=128)
url = models.URLField(verbose_name=_('Newsletters service url'),
max_length=128)
resources_restrictions = models.CharField(_('resources restrictions'),
blank=True, max_length=1024,
help_text=_('list of resources(themes) separated by commas'))
transports_restrictions = models.CharField(_('transports restrictions'),
blank=True, max_length=1024,
help_text=_('list of transports separated by commas'))
url = models.URLField(verbose_name=_('Newsletters service url'), max_length=128)
resources_restrictions = models.CharField(
_('resources restrictions'),
blank=True,
max_length=1024,
help_text=_('list of resources(themes) separated by commas'),
)
transports_restrictions = models.CharField(
_('transports restrictions'),
blank=True,
max_length=1024,
help_text=_('list of transports separated by commas'),
)
template_name = 'newsletters/newsletters.html'
user_dependant = True
@ -60,11 +65,9 @@ class NewslettersCell(CellBase):
verbose_name = _('Newsletters')
def get_default_form_class(self):
model_fields = ('title', 'url', 'resources_restrictions',
'transports_restrictions')
model_fields = ('title', 'url', 'resources_restrictions', 'transports_restrictions')
return model_forms.modelform_factory(self.__class__, fields=model_fields)
def simplify(self, name):
return slugify(name.strip())
@ -109,8 +112,9 @@ class NewslettersCell(CellBase):
def get_subscriptions(self, user, **kwargs):
endpoint = self.url + 'subscriptions/'
try:
response = requests.get(endpoint, remote_service='auto',
user=user, cache_duration=0, params=kwargs)
response = requests.get(
endpoint, remote_service='auto', user=user, cache_duration=0, params=kwargs
)
response.raise_for_status()
except RequestException:
return []
@ -125,18 +129,27 @@ class NewslettersCell(CellBase):
headers = {'Content-type': 'application/json', 'Accept': 'application/json'}
endpoint = self.url + 'subscriptions/'
try:
response = requests.post(endpoint, remote_service='auto', data=json.dumps(subscriptions),
user=user, federation_key='email', params=kwargs, headers=headers)
response = requests.post(
endpoint,
remote_service='auto',
data=json.dumps(subscriptions),
user=user,
federation_key='email',
params=kwargs,
headers=headers,
)
response.raise_for_status()
if not response.json()['data']:
raise SubscriptionsSaveError
except HTTPError as e:
logger.error(u'set subscriptions on %s returned an HTTP error code: %s',
e.response.request.url, e.response.status_code)
logger.error(
u'set subscriptions on %s returned an HTTP error code: %s',
e.response.request.url,
e.response.status_code,
)
raise SubscriptionsSaveError
except RequestException as e:
logger.error(u'set subscriptions on %s failed with exception: %s',
endpoint, e)
logger.error(u'set subscriptions on %s failed with exception: %s', endpoint, e)
raise SubscriptionsSaveError
def render(self, context):

View File

@ -4,6 +4,9 @@ from django.conf.urls import url
from .views import NewslettersView
urlpatterns = [
url(r'^newsletters/(?P<pk>\w+)/update$', login_required(NewslettersView.as_view()),
name='newsletters-update'),
url(
r'^newsletters/(?P<pk>\w+)/update$',
login_required(NewslettersView.as_view()),
name='newsletters-update',
),
]

View File

@ -22,6 +22,7 @@ from django.http import HttpResponseRedirect
from .forms import NewslettersManageForm
from .models import NewslettersCell, SubscriptionsSaveError
class NewslettersView(FormView):
http_method_names = ['post']
form_class = NewslettersManageForm
@ -31,7 +32,9 @@ class NewslettersView(FormView):
form.save()
messages.info(self.request, _('Your subscriptions are successfully saved'))
except SubscriptionsSaveError:
messages.error(self.request, _('An error occured while saving your subscriptions. Please try later.'))
messages.error(
self.request, _('An error occured while saving your subscriptions. Please try later.')
)
return super(NewslettersView, self).form_valid(form)
def get_form_kwargs(self):

View File

@ -17,12 +17,14 @@
import django.apps
from django.utils.translation import ugettext_lazy as _
class AppConfig(django.apps.AppConfig):
name = 'combo.apps.notifications'
verbose_name = _('Notification')
def get_before_urls(self):
from . import urls
return urls.urlpatterns

View File

@ -63,6 +63,7 @@ class Add(GenericAPIView):
response = {'err': 0, 'data': {'id': notification.public_id}}
return Response(response)
add = Add.as_view()
@ -74,6 +75,7 @@ class Ack(GenericAPIView):
Notification.objects.find(request.user, notification_id).ack()
return Response({'err': 0})
ack = Ack.as_view()
@ -85,6 +87,7 @@ class Forget(GenericAPIView):
Notification.objects.find(request.user, notification_id).forget()
return Response({'err': 0})
forget = Forget.as_view()
@ -97,5 +100,6 @@ class Count(GenericAPIView):
new = Notification.objects.visible(request.user).new().count()
return Response({'err': 0, 'total': total, 'new': new})
count = Count.as_view()
count.mellon_no_passive = True

Some files were not shown because too many files have changed in this diff Show More