trivial: apply black
This commit is contained in:
parent
bc9f6b9fba
commit
47d67c395e
|
@ -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'
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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')),
|
||||
],
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'),
|
||||
]
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -24,6 +24,7 @@ class AppConfig(django.apps.AppConfig):
|
|||
|
||||
def get_before_urls(self):
|
||||
from . import urls
|
||||
|
||||
return urls.urlpatterns
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)),
|
||||
],
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -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)),
|
||||
],
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -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')),
|
||||
],
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'),
|
||||
]
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'})
|
||||
)
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -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.')}
|
||||
|
|
|
@ -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'),
|
||||
]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.'))
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)),
|
||||
],
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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}))
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -22,4 +22,5 @@ from .models import Regie
|
|||
class RegieAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {'slug': ('label',)}
|
||||
|
||||
|
||||
admin.site.register(Regie, RegieAdmin)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -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
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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'
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'),
|
||||
]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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',)},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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'),
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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')],
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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=[],
|
||||
)
|
||||
]
|
||||
|
|
|
@ -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 &copy; <a href="https://openstreetmap.org">OpenStreetMap</a> contributors, '
|
||||
'<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>'),
|
||||
'<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>'
|
||||
),
|
||||
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',
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)),
|
||||
],
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue