diff --git a/events/forms.py b/events/forms.py index 4a20cafb..0aab6e73 100644 --- a/events/forms.py +++ b/events/forms.py @@ -1,7 +1,15 @@ # -*- coding: utf-8 -*- +import re from itertools import chain from collections import namedtuple from datetime import datetime +from datetime import timedelta +from datetime import date +from datetime import MAXYEAR, MINYEAR +from dateutil import relativedelta +import calendar +# from calendar import TimeEncoding, month_name + try: from collections import ChainMap @@ -10,12 +18,15 @@ except ImportError: from django import forms from django.utils.translation import get_language, ugettext as _ +from django.utils.translation import string_concat from django.utils.encoding import smart_text, force_text from django.utils.html import format_html from django.utils.safestring import mark_safe from django.db.models import Count, Sum, Q, ForeignKey, ManyToManyField from django.db import connection from django.core.exceptions import ValidationError +from django.core.cache import cache +from django.conf import settings from haystack.query import SearchQuerySet, RelatedSearchQuerySet, SQ @@ -65,7 +76,6 @@ class WidgetDefaultMixin(object): class CountModelMultipleChoiceField(WidgetDefaultMixin, forms.ModelMultipleChoiceField): - # widget = forms.CheckboxSelectMultiple widget = FilterCheckboxSelectMultiple def label_from_instance(self, obj): if obj.get('count', None) is None: @@ -105,6 +115,41 @@ class FilterTypedMultipleChoiceField(WidgetDefaultMixin, forms.TypedMultipleChoi pass +class MonthChoiceField(FilterTypedMultipleChoiceField): + year = None + additional_choices = [] + default_month = None + def make_year_choice(self, value): + return (str(value), _(u'{year} год').format(year=value)) + + def valid_value(self, value): + valid = super(MonthChoiceField, self).valid_value(value) + if not valid: + if self.year and (value == self.year or check_year(value) == self.year): + choice = self.make_year_choice(value) + self.additional_choices.append(choice) + self.choices.insert(0, choice) + return True + + value = str(value) + match = year_month_regex.match(value) + month = None + year = self.year or datetime.today().year + if match: + year, month = match.group('year'), match.group('month') + elif value in settings.MONTHES.keys(): + month = value + self.default_month = month + if month in settings.MONTHES.keys(): + choice = (value, string_concat(settings.MONTHES[month]['name'], ' ', year)) + self.additional_choices.append(choice) + self.choices.insert(0, choice) + # print(choice, self.choices) + + return super(MonthChoiceField, self).valid_value(value) + return valid + + fields_mapping = { 'members': 'members_choice', 'visitors': 'visitors_choice', @@ -118,32 +163,47 @@ values_mapping = { } +monthes_abr_to_num = {v.lower(): k for k, v in enumerate(calendar.month_abbr)} +year_month_regex = re.compile(r'^(?P\d{4})(?P\w{3})$') + + +def check_year(value): + year = None + try: + year = int(value) + except (ValueError, ): + return + else: + if not MAXYEAR >= year >= MINYEAR: + return + return year + class FilterForm(forms.Form): - # TYPES = TYPES - # MEMBERS = MEMBERS - # VISITORS = VISITORS - # PRICE = PRICE + _month_choices = None + _month = None + event_type = FilterTypedMultipleChoiceField( label=_(u'Тип события'), coerce=int, choices=TYPES, required=False, widget=FilterCheckboxSelectMultiple()) theme = CountModelMultipleChoiceField( - label=_(u'Тематики'), required=False, + label=_(u'Тематики'), required=False, cache_choices=True, queryset=Theme.objects.language().values('pk', 'name')) tag = CountModelMultipleChoiceField( - label=_(u'Теги'), required=False, + label=_(u'Теги'), required=False, cache_choices=True, queryset=Tag.objects.language().values('pk', 'name')) country = CountModelMultipleChoiceField( - label=_(u'Страны'), required=False, + label=_(u'Страны'), required=False, cache_choices=True, queryset=Country.objects.language().values('pk', 'name')) city = CountModelMultipleChoiceField( - label=_(u'Города'), required=False, + label=_(u'Города'), required=False, cache_choices=True, queryset=City.objects.language().values('pk', 'name')) price = FilterTypedMultipleChoiceField( label=_(u'Стоимость'), coerce=int, choices=PRICE, required=False, widget=FilterCheckboxSelectMultiple(), - help_text=_(u'За 1 м2 необорудованной площади')) + # help_text=_(u'За 1 м2 необорудованной площади') + ) members = FilterTypedMultipleChoiceField( label=_(u'Участники'), coerce=int, choices=MEMBERS, @@ -152,6 +212,22 @@ class FilterForm(forms.Form): label=_(u'Посетители'), coerce=int, choices=VISITORS, required=False, widget=FilterCheckboxSelectMultiple()) + year = forms.RegexField(r'^(?P\d{4})$', max_length=4, min_length=4, + required=False, widget=forms.HiddenInput()) + default_month = forms.CharField(max_length=3, min_length=3, + required=False, widget=forms.HiddenInput()) + default_year = forms.RegexField(r'^(?P\d{4})$', max_length=4, min_length=4, + required=False, widget=forms.HiddenInput()) + month = MonthChoiceField( + label=_(u'Период'), coerce=str, + choices=monthes_abr_to_num.iteritems(), + required=False, widget=FilterCheckboxSelectMultiple()) + date_from = forms.DateField(required=False, input_formats=('%d.%m.%Y',), + widget=forms.DateInput(attrs={'class': 'date', 'id': 'dateFrom', + 'placeholder': _(u'дд.мм.гггг')})) + date_to = forms.DateField(required=False, input_formats=('%d.%m.%Y',), + widget=forms.DateInput(attrs={'class': 'date', 'id': 'dateTo', + 'placeholder': _(u'дд.мм.гггг')})) def __init__(self, *args, **kwargs): super(FilterForm, self).__init__(*args, **kwargs) @@ -159,7 +235,86 @@ class FilterForm(forms.Form): self._models = None self._lookup_kwargs = None self._lookup_args = None + self._related_fields = [] + self._local_fields = [] self.lang = get_language() + self.data = self.data.copy() + self.fill_default_choices_from_cache() + self.fields['month'].choices = self.month_choices() + + def fill_default_choices_from_cache(self): + timeout = timedelta(hours=1).seconds + for field in ['theme', 'tag', 'country', 'city']: + key ='filter_{field}_{lang}'.format(field=field, lang=self.lang) + choices = cache.get(key) + if choices is None: + choices = list(self.fields[field].choices) + cache.set(key, choices, timeout) + else: + self.fields[field].choice_cache = choices + + @classmethod + def month_choices(cls): + month = datetime.today().month + if cls._month != month or cls._month_choices is None: + year = datetime.today().year + depth = 7 # на сколько сколько месяцев вперед делать выборку (включая текущий) + monthes = dict([(v.get('value'), {'abr': k, 'name': v.get('name')}) for k, v in settings.MONTHES.iteritems()]) + choices = [] + for month_num in xrange(month, month + depth): + _year = year + if month_num > 12: + _year = year + 1 + month_num -= 12 + month_values = monthes.get(month_num) + # month_values['name'] = string_concat(month_values['name'], ' ', str(_year)) + choices.append((str(_year) + month_values['abr'], string_concat(month_values['name'], ' ', str(_year)))) + cls._month_choices = choices + cls._month = month + return cls._month_choices + + def clean_year(self, value=None): + year = value or self.cleaned_data.get('year') + year = check_year(year) + self.fields['month'].year = year + return year + + def clean_default_month(self): + v = self.cleaned_data.get('default_month') + if v and v in settings.MONTHES.keys(): + self.fields['month'].default_month = v + return v + return + + def clean_default_year(self): + y = self.cleaned_data.get('default_year') + y = check_year(y) + return y + + def clean_month(self): + values = self.cleaned_data.get('month', []) + field = self.fields['month'] + new_values = [] + for val in values: + if len(val) == 3 and list(filter(lambda x: x[0] == str(field.year or datetime.today().year) + val, field.choices)): + new_values.append(str(field.year or datetime.today().year) + val) + field.choices = filter(lambda x: x[0] != val, field.choices) + else: + new_values.append(val) + if new_values != values: + self.data.setlist('month', new_values) + return new_values + + def _post_clean(self): + # нужно для того, чтобы год зашел в поле month + self.get_date_begin_periods() + if not self.cleaned_data.get('default_month') and self.fields['month'].default_month: + self.data['default_month'] = self.fields['month'].default_month + year = self.cleaned_data.get('year') + # if year: + # for val, name in self.fields['month'].additional_choices: + # self. + # additional_choices def is_valid(self): # if getattr(self, '_is_valid', None) is None: @@ -215,6 +370,7 @@ class FilterForm(forms.Form): def filter(self, qs=None): qs = qs or self.default_filter() # lookup_kwargs = dict(ChainMap({}, *(lookup_kwargs or self.lookup_kwargs).values())) + qs = self.make_data_begin_filter(qs) return qs.filter(**self.lookup_kwargs).order_by('data_begin') def default_filter(self, load_all=True, _models=None): @@ -227,6 +383,12 @@ class FilterForm(forms.Form): qs = qs.filter(data_begin__gte=datetime.now().date()) return qs + def make_data_begin_filter(self, qs): + params = self.make_date_begin_sqs_params() + if params is not None: + qs = qs.filter(params) + return qs + def recalculate_choices(self): for field in ['theme', 'tag', 'city', 'country']: # field_qs = self.default_filter(load_all=False) @@ -255,12 +417,18 @@ class FilterForm(forms.Form): qs.query.having.add(ExtraWhere(having, [], OR), AND) qs = qs.values(*values).order_by(*order_by) self.fields[field].queryset = qs - # print(self.fields[field].queryset.query) + + # для того чтобы взяло чойсы из новых результатов + self.fields[field].cache_choices = False + self.fields[field].choice_cache = None + print(self.fields[field].queryset.query) for field in ['members', 'visitors', 'price']: self.fields[field].choices = self.make_local_field_count(field) or self.fields[field].choices self.make_event_type_choices_count() + # self.fields['month'].choices = self.month_choices() + # for field in self.fields: # field = self.fields[field] # if hasattr(field, 'queryset'): @@ -271,11 +439,15 @@ class FilterForm(forms.Form): choices = [] for _type, label in TYPES: qs = self.default_filter(load_all=False, _models=[types.get(_type)]) + qs = self.make_data_begin_filter(qs) count = qs.filter(**self.lookup_kwargs).count() choices.append((_type, label + ' ({count})'.format(count=count))) self.fields['event_type'].choices = choices def make_ids_in_sql_format(self, values): + # stance(values, (list, tuple, set)): + # return tuple(values) if len(values) > 1 else '({})'.format(*values) + # return return tuple(values) if len(values) > 1 else '({})'.format(*values) def make_joins_from_selected(self, field, model): @@ -364,6 +536,7 @@ class FilterForm(forms.Form): joins.extend(_joins) where.extend(_where) where.append(self.make_default_where(db_table=model._meta.db_table)) + self.make_date_begin_where(where, db_table=model._meta.db_table) selects.append(select + ''.join(joins) + ' where ' + ' and '.join(where) + group_by) @@ -381,8 +554,7 @@ class FilterForm(forms.Form): choices.append((val.get('value'), val.get('label') + ' ({count})'.format(count=count))) finally: c.close() - # some bug with these! - # AttributeError: __exit__ + # some bug with these! AttributeError: __exit__ # with connection.cursor() as c: return choices @@ -390,6 +562,183 @@ class FilterForm(forms.Form): return ''' (`{db_table}`.`is_published` = True) AND (`{db_table}`.`data_begin` >= '{date_today}') '''\ .format(date_today=datetime.now().strftime('%Y-%m-%d'), **kwargs) + def get_date_begin_periods(self): + periods = getattr(self, '_periods', None) + if periods is None: + periods = {} + cleaned_year = self.cleaned_data.get('year') + cleaned_year_str = str(cleaned_year) + year = cleaned_year or datetime.today().year + current_month = datetime.today().month + for value in self.cleaned_data['month']: + match = year_month_regex.match(value) + if match: + _year, month = int(match.group('year')), match.group('month') + elif cleaned_year and cleaned_year == self.clean_year(value=value): + periods.setdefault(cleaned_year, []) + continue + else: + _year = year + month = value + month_num = monthes_abr_to_num.get(month) + periods.setdefault(_year, []).append(month_num) + if cleaned_year\ + and not cleaned_year_str in self.cleaned_data.get('month', [])\ + and not periods.get(cleaned_year)\ + and not self.fields['month'].default_month\ + and not self.cleaned_data.get('default_year'): + periods[cleaned_year] = [] + if self.fields['month'].valid_value(cleaned_year): + self.cleaned_data.setdefault('month', []).append(cleaned_year_str) + self.data.update({'month': cleaned_year_str}) + self.data['default_year'] = cleaned_year_str + elif cleaned_year\ + and not cleaned_year_str in self.cleaned_data.get('month', [])\ + and not periods.get(cleaned_year)\ + and not self.fields['month'].default_month\ + and self.cleaned_data.get('default_year'): + self.fields['month'].choices.insert(0, self.fields['month'].make_year_choice(cleaned_year)) + elif cleaned_year and cleaned_year_str in self.cleaned_data.get('month', []) and periods.get(cleaned_year): + periods[cleaned_year] = [] + cleaned = self.data.getlist('month', []) + cleaned.remove(cleaned_year_str) + self.cleaned_data.setdefault('month', []).remove(cleaned_year_str) + self.data.setlist('month', cleaned) + elif cleaned_year and periods.get(cleaned_year) and not self.fields['month'].default_month: + self.fields['month'].valid_value(cleaned_year) + elif cleaned_year and self.fields['month'].default_month\ + and not list(filter(lambda x: x[0] == cleaned_year_str + self.fields['month'].default_month, self.fields['month'].choices)): + self.fields['month'].valid_value(self.fields['month'].default_month) + self._periods = periods + return periods + + def make_date_begin_where(self, where, **kwargs): + _where = [] + # years = {} + # current_month = datetime.today().month + # year = datetime.today().year + # for month in self.cleaned_data['month']: + # month_num = monthes_abr_to_num.get(month) + # _year = year + # if month_num < current_month: + # _year += 1 + # years.setdefault(_year, []).append(month_num) + # kwargs.update(years) + add_years = set() + for year, monthes in self.get_date_begin_periods().iteritems(): + if monthes: + _where.append( + ''' (month(`{db_table}`.`data_begin`) in {monthes} and year(`{db_table}`.`data_begin`) = {year}) '''\ + .format(year=year, monthes=self.make_ids_in_sql_format(monthes), **kwargs) + ) + else: + add_years.add(year) + if add_years: + _where.append( + ''' (year(`{db_table}`.`data_begin`) in {year}) '''\ + .format(year=self.make_ids_in_sql_format(add_years), **kwargs) + ) + if len(_where) == 1: + where.extend(_where) + elif len(_where) > 1: + where.append(''' ({}) '''.format(' OR '.join(_where))) + return + + # def get_prev_month(self, date): + # year = date.year + # month = date.month + # day = date.day + # last_day = False + # if day == calendar.monthrange(year, month)[1]: + # last_day = True + # if date.month == 1: + # year -= 1 + # month = 12 + # if last_day: + # day = calendar.monthrange(year, month)[1] + # return date.replace(month=month, year=year, day=day) + + # def get_next_month(self, date): + # year = date.year + # month = date.month + # day = date.day + # last_day = False + # if day == calendar.monthrange(year, month)[1]: + # last_day = True + # if date.month == 12: + # year += 1 + # month = 1 + # if last_day: + # day = calendar.monthrange(year, month)[1] + # return date.replace(month=month, year=year, day=day) + + def get_start_of_period(self, year, periods, month=None): + if month is None: + return year, 1 + self.checked_monthes.setdefault(year, set()).add(month) + if month == 12: + prev_month = month - 1 + prev_year = year + elif month == 1: + prev_month = 12 + prev_year = year - 1 + else: + prev_month = month - 1 + prev_year = year + if prev_month in periods.get(prev_year, []): + return self.get_start_of_period(prev_year, periods, prev_month) + return year, month + + def get_end_of_period(self, year, periods, month=None): + if month is None: + return year, 12 + self.checked_monthes.setdefault(year, set()).add(month) + if month == 12: + next_month = 1 + next_year = year + 1 + elif month == 1: + next_month = month + 1 + next_year = year + else: + next_month = month + 1 + next_year = year + if next_month in periods.get(next_year, []): + return self.get_end_of_period(next_year, periods, next_month) + return year, month + + def make_date_begin_sqs_params(self): + periods = [] + self.checked_monthes = {} + cleaned_periods = self.get_date_begin_periods() + # print(cleaned_periods) + for year, monthes in cleaned_periods.iteritems(): + for month in monthes: + if month in self.checked_monthes.get(year, []): + continue + start_year, start_month = self.get_start_of_period(year, cleaned_periods, month) + end_year, end_month = self.get_end_of_period(year, cleaned_periods, month) + _first_day, _last_day = calendar.monthrange(end_year, end_month) + periods.append(( + date.today().replace(day=1, month=start_month, year=start_year), + date.today().replace(day=_last_day, month=end_month, year=end_year) + )) + else: + start_year, start_month = self.get_start_of_period(year, cleaned_periods) + end_year, end_month = self.get_end_of_period(year, cleaned_periods) + _first_day, _last_day = calendar.monthrange(end_year, end_month) + periods.append(( + date.today().replace(day=1, month=start_month, year=start_year), + date.today().replace(day=_last_day, month=end_month, year=end_year) + )) + + params = None + for start, end in periods: + if params is None: + params = SQ(data_begin__range=(start, end)) + else: + params |= SQ(data_begin__range=(start, end)) + return params + def make_count_select(self, field): selects = [] case = None @@ -452,7 +801,7 @@ class FilterForm(forms.Form): joins.extend(_joins) where.extend(_where) where.append(self.make_default_where(db_table=model._meta.db_table)) - + self.make_date_begin_where(where, db_table=model._meta.db_table) selects.append(select + ''.join(joins) + ' where ' + ' and '.join(where) + group_by) if len(selects) == 2: diff --git a/events/urls.py b/events/urls.py index 0dfa598a..548e156c 100644 --- a/events/urls.py +++ b/events/urls.py @@ -7,4 +7,5 @@ from .views import FilterListView urlpatterns = patterns('', url(r'^$', FilterListView.as_view(), name='main'), url(r'^results/$', FilterListView.as_view(), {'with_form': False}, name='results'), + url(r'^form/$', FilterListView.as_view(), {'with_results': False}, name='form'), ) diff --git a/events/views.py b/events/views.py index fe852572..bc5cd0dc 100644 --- a/events/views.py +++ b/events/views.py @@ -36,6 +36,7 @@ class FilterListView(ContextMixin, FormMixin, ListView): qs = self.form.default_filter() if self.kwargs.get('with_form', True): self.form.recalculate_choices() + print(self.form.data, self.form.cleaned_data, self.form.get_date_begin_periods()) # import pdb; pdb.set_trace() return qs @@ -46,10 +47,11 @@ class FilterListView(ContextMixin, FormMixin, ListView): # ajax if request.is_ajax(): ctx = RequestContext(request, self.get_context_data(object_list=self.get_queryset())) - data = { - 'success': True, - 'results': render_to_string(self._ajax_results_template_name, ctx), - } + data = {'success': True} + if kwargs.get('with_results', True): + data.update({ + 'results': render_to_string(self._ajax_results_template_name, ctx), + }) if kwargs.get('with_form', True): data.update({ 'form': render_to_string(self._ajax_form_template_name, ctx), @@ -66,9 +68,9 @@ class FilterListView(ContextMixin, FormMixin, ListView): context = super(FilterListView, self).get_context_data(**kwargs) # get params for paginator - get = self.request.GET.copy() + get = self.form.data.copy() if 'page' in get: del get['page'] - context['GETparams'] = get.urlencode() + context['GETparams'] = get.urlencode() if hasattr(get, 'urlencode') else '' return context diff --git a/proj/middleware.py b/proj/middleware.py index 33ce635f..a4425fb5 100644 --- a/proj/middleware.py +++ b/proj/middleware.py @@ -10,7 +10,7 @@ from country.models import Country from city.models import City -class RedirectFallbackMiddleware(object): +class ExpoRedirectFallbackMiddleware(object): def process_response(self, request, response): if response.status_code != 404: return response # No need to check for a redirect for non-404 responses. @@ -26,9 +26,9 @@ class RedirectFallbackMiddleware(object): check = re.compile(regex) match = check.match(full_path) if match: - response = handler(**match.groupdict()) - if response is not None: - return response + _response = handler(**match.groupdict()) + if _response is not None: + return _response return response diff --git a/templates/client/includes/events/filter_form.html b/templates/client/includes/events/filter_form.html index 5bc31d19..f5ce6fbd 100644 --- a/templates/client/includes/events/filter_form.html +++ b/templates/client/includes/events/filter_form.html @@ -1,6 +1,6 @@ {% load i18n static %} -
+
{% trans 'Выбрать по критериям:' %}
@@ -12,8 +12,11 @@
{# {% csrf_token %} #} + {% for hidden in form.hidden_fields %} + {{ hidden }} + {% endfor %} - {% for field in form %} + {% for field in form.visible_fields %} {% if field.errors %}error{% endif %}