authentic/src/authentic2/apps/journal/forms.py

353 lines
12 KiB
Python

# authentic2 - versatile identity manager
# Copyright (C) 2010-2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime
from django import forms
from django.http import QueryDict
from django.utils.formats import date_format
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from . import models, search_engine
class Page:
def __init__(self, form, events, is_first_page, is_last_page):
self.form = form
self.events = events
self.is_first_page = is_first_page
self.is_last_page = is_last_page
self.limit = form.limit
@property
def previous_page_cursor(self):
return None if self.is_first_page else self.events[0].cursor
@property
def next_page_cursor(self):
return None if self.is_last_page else self.events[-1].cursor
@cached_property
def next_page_url(self):
if self.is_last_page:
return None
else:
return self.form.make_url('after_cursor', self.events[-1].cursor)
@cached_property
def first_page_url(self):
return self.form.make_url('after_cursor', '0 0')
@cached_property
def previous_page_url(self):
if self.is_first_page:
return None
else:
return self.form.make_url('before_cursor', self.events[0].cursor)
@cached_property
def last_page_url(self):
return self.form.make_url('before_cursor', '%s 0' % (2 ** 31 - 1))
def __bool__(self):
return bool(self.events)
def __iter__(self):
return reversed(self.events)
class DateHierarchy:
def __init__(self, form, year=None, month=None, day=None):
self.form = form
self.year = year
self.month = month
self.day = day
@property
def title(self):
if self.day:
return date_format(self.current_datetime, 'DATE_FORMAT')
elif self.month:
return date_format(self.current_datetime, 'F Y')
elif self.year:
return str(self.year)
@cached_property
def back_urls(self):
def helper():
if self.year:
yield _('All dates'), self.form.make_url(exclude=['year', 'month', 'day'])
if self.month:
yield str(self.year), self.form.make_url(exclude=['month', 'day'])
current_datetime = datetime(self.year, self.month or 1, self.day or 1)
month_name = date_format(current_datetime, format='F Y').title()
if self.day:
yield month_name, self.form.make_url(exclude=['day'])
yield str(self.day), '#'
else:
yield month_name, '#'
else:
yield str(self.year), '#'
else:
yield _('All dates'), '#'
return list(helper())
@property
def current_datetime(self):
return datetime(self.year or 1900, self.month or 1, self.day or 1)
@property
def month_name(self):
return date_format(self.current_datetime, format='F')
@cached_property
def choice_urls(self):
def helper():
if self.day:
return
elif self.month:
for day in self.form.days:
yield str(day), self.form.make_url('day', day)
elif self.year:
for month in self.form.months:
dt = datetime(self.year, month, 1)
month_name = date_format(dt, format='F')
yield month_name, self.form.make_url('month', month, exclude=['day'])
else:
for year in self.form.years:
yield str(year), self.form.make_url('year', year, exclude=['month', 'day'])
return list(helper())
@property
def choice_name(self):
if self.day:
return
elif self.month:
return _('Days of %s') % self.month_name
elif self.year:
return _('Months of %s') % self.year
else:
return _('Years')
class SearchField(forms.CharField):
type = 'search'
class JournalForm(forms.Form):
year = forms.CharField(label=_('year'), widget=forms.HiddenInput(), required=False)
month = forms.CharField(label=_('month'), widget=forms.HiddenInput(), required=False)
day = forms.CharField(label=_('day'), widget=forms.HiddenInput(), required=False)
after_cursor = forms.CharField(widget=forms.HiddenInput(), required=False)
before_cursor = forms.CharField(widget=forms.HiddenInput(), required=False)
search = SearchField(required=False, label='')
search_engine_class = search_engine.JournalSearchEngine
def __init__(self, *args, **kwargs):
self.queryset = kwargs.pop('queryset', None)
if self.queryset is None:
self.queryset = models.Event.objects.all()
self.limit = kwargs.pop('limit', 20)
search_engine_class = kwargs.pop('search_engine_class', None)
if search_engine_class:
self.search_engine_class = search_engine_class
super().__init__(*args, **kwargs)
@cached_property
def years(self):
self.is_valid()
return [dt.year for dt in self.queryset.datetimes('timestamp', 'year')]
@cached_property
def months(self):
self.is_valid()
if self.cleaned_data.get('year'):
return [
dt.month
for dt in self.queryset.filter(timestamp__year=self.cleaned_data['year']).datetimes(
'timestamp', 'month'
)
]
return []
@cached_property
def days(self):
self.is_valid()
if self.cleaned_data.get('month') and self.cleaned_data.get('year'):
return [
dt.day
for dt in self.queryset.filter(
timestamp__year=self.cleaned_data['year'], timestamp__month=self.cleaned_data['month']
).datetimes('timestamp', 'day')
]
return []
@staticmethod
def _clean_integer_value(value):
try:
return int(value)
except ValueError:
return None
def clean_year(self):
return self._clean_integer_value(self.cleaned_data['year'])
def clean_month(self):
return self._clean_integer_value(self.cleaned_data['month'])
def clean_day(self):
return self._clean_integer_value(self.cleaned_data['day'])
def clean(self):
super().clean()
year = self.cleaned_data.get('year')
if year not in self.years:
self.cleaned_data['year'] = None
month = self.cleaned_data.get('month')
if month not in self.months:
self.cleaned_data['month'] = None
day = self.cleaned_data.get('day')
if day not in self.days:
self.cleaned_data['day'] = None
def clean_after_cursor(self):
return models.EventCursor.parse(self.cleaned_data['after_cursor'])
def clean_before_cursor(self):
return models.EventCursor.parse(self.cleaned_data['before_cursor'])
def clean_search(self):
self.cleaned_data['_search_query'] = self.search_engine_class().query(
query_string=self.cleaned_data['search']
)
return self.cleaned_data['search']
def get_queryset(self, limit=None):
self.is_valid()
qs = self.queryset
year = self.cleaned_data.get('year')
month = self.cleaned_data.get('month')
day = self.cleaned_data.get('day')
search_query = self.cleaned_data.get('_search_query')
if year:
qs = qs.filter(timestamp__year=year)
if month:
qs = qs.filter(timestamp__month=month)
if day:
qs = qs.filter(timestamp__day=day)
if search_query:
qs = qs.filter(search_query)
return qs
def make_querydict(self, name=None, value=None, exclude=()):
querydict = QueryDict(mutable=True)
for k, v in self.cleaned_data.items():
if k.startswith('_'):
continue
if k in exclude:
continue
if v:
querydict[k] = str(v)
if name:
if name in ['after_cursor', 'before_cursor']:
querydict.pop('after_cursor', None)
querydict.pop('before_cursor', None)
assert name in self.fields
assert value is not None
querydict[name] = value
return querydict
def make_url(self, name=None, value=None, exclude=()):
return '?' + self.make_querydict(name=name, value=value, exclude=exclude).urlencode()
@cached_property
def page(self):
self.is_valid()
after_cursor = self.cleaned_data['after_cursor']
before_cursor = self.cleaned_data['before_cursor']
first = False
last = False
limit = self.limit
qs = self.get_queryset()
if after_cursor:
page = list(qs[after_cursor : (limit + 2)])
first = not (qs[-1:after_cursor])
if len(page) > limit:
last = len(page) != (limit + 2)
if page[0].cursor == after_cursor:
page = page[1 : (limit + 1)]
else:
page = page[:limit]
else:
last = True
before_cursor = after_cursor if not page else page[-1].cursor
page = list(qs[-(limit + 1) : before_cursor])
first = len(page) < (limit + 1)
page = page[-limit:]
elif before_cursor:
page = list(qs[-(limit + 2) : before_cursor])
last = not (qs[before_cursor:1])
if len(page) > limit:
first = len(page) != (limit + 2)
page = page[-(limit + 1) : -1]
else:
first = True
after_cursor = before_cursor if not page else page[0].cursor
page = list(qs[after_cursor : (limit + 1)])
last = len(page) < (limit + 1)
page = page[:limit]
else:
qs = qs.order_by('-timestamp', '-id')
page = qs[: (limit + 1) : -1]
first = len(page) <= limit
last = True
page = page[-limit:]
models.prefetch_events_references(page)
if page:
self.data = self.data.copy()
self.cleaned_data['after_cursor'] = self.data['after_cursor'] = page[0].cursor.minus_one()
self.cleaned_data['before_cursor'] = ''
return Page(self, page, first, last)
@cached_property
def date_hierarchy(self):
self.is_valid()
return DateHierarchy(
self,
year=self.cleaned_data['year'],
month=self.cleaned_data['month'],
day=self.cleaned_data['day'],
)
@property
def url(self):
return self.make_url()