353 lines
12 KiB
Python
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()
|