class LibreBormeCalendar(HTMLCalendar):
    """
    Este calendario tiene enlaces al día si hay Borme.
    """

    def formatday(self, day, weekday):
        """
        Return a day as a table cell.
        """
        if day == self.day:
            css_selected = 'selected'
        else:
            css_selected = ''

        if day == 0:
            return '<td class="noday">&nbsp;</td>'  # day outside month
        elif weekday in (5, 6):
            return '<td class="day %s %s">%d</td>' % (self.cssclasses[weekday], css_selected, day)
        elif self.today == datetime.date(self.year, self.month, day):
            if (self.month, day) in self.days_bormes:
                url = reverse('borme-fecha', args=['-'.join([str(self.year), str(self.month), str(day)])])
                return '<td class="day bormeday today %s"><a href="%s">%d</a></td>' % (css_selected, url, day)
            else:
                return '<td class="day nobormeday today %s">%d</td>' % (css_selected, day)
        else:
            if (self.month, day) in self.days_bormes:
                url = reverse('borme-fecha', args=['-'.join([str(self.year), str(self.month), str(day)])])
                return '<td class="day bormeday %s"><a href="%s">%d</a></td>' % (css_selected, url, day)
            else:
                return '<td class="day nobormeday %s">%d</td>' % (css_selected, day)

    def formatmonth(self, date):
        self.year = date.year
        self.month = date.month
        self.day = date.day
        self.today = datetime.date.today()

        _, lastday = monthrange(self.year, self.month)
        bormes = Borme.objects.filter(
                    date__gte=datetime.date(self.year, self.month, 1),
                    date__lte=datetime.date(self.year, self.month, lastday)
                 ).distinct('date').order_by('date')
        self.days_bormes = {}
        for borme in bormes:
            self.days_bormes[(borme.date.month, borme.date.day)] = borme

        return super(LibreBormeCalendar, self).formatmonth(self.year,
                                                           self.month) class LibreBormeAvailableCalendar(HTMLCalendar):
    """
    Este calendario tiene enlaces al Borme si existe para ese día.
    """

    def formatday(self, day, weekday):
        """
        Return a day as a table cell.
        Este calendario tiene enlaces al día si hay Borme.
        """

        if day == 0:
            return '<td class="noday">&nbsp;</td>'  # day outside month
        elif weekday in (5, 6):
            return '<td class="day %s">%d</td>' % (self.cssclasses[weekday], day)
        elif self.today == datetime.date(self.year, self.month, day):
            if (self.month, day) in self.days_bormes:
                url = self.days_bormes[(self.month, day)].get_absolute_url()
                return '<td class="day bormeday today"><a href="%s">%d</a></td>' % (url, day)
            else:
                return '<td class="day nobormeday today">%d</td>' % day
        else:
            if (self.month, day) in self.days_bormes:
                url = self.days_bormes[(self.month, day)].get_absolute_url()
                return '<td class="day bormeday"><a href="%s">%d</a></td>' % (url, day)
            else:
                return '<td class="day nobormeday">%d</td>' % day

    def formatmonth(self, year, month, withyear=True):
        self.month = month
        return super(LibreBormeAvailableCalendar, self).formatmonth(
                                                            year,
                                                            month,
                                                            withyear=withyear)

    def formatyear(self, theyear, bormes, width=3):
        self.year = theyear
        self.today = datetime.date.today()

        self.days_bormes = {}
        for borme in bormes:
            self.days_bormes[(borme.date.month, borme.date.day)] = borme

        return super(LibreBormeAvailableCalendar, self).formatyear(
                                                            theyear,
                                                            width=width) def es_search_paginator(doc_type, text):
    """Perform an ElasticSearch query and return a paginable object.

    For full details of accepted parameters in ES query:
    https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-match-query.html
    https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-query-string-query.html

    :param doc_type: ES document name ('person_document' or 'company_document')
    :param query: Text to search
    :type doc_type: str
    :type query: str
    :rtype: (ElasticSearchPaginatorList)
    """

    # It is a better idea to use match query type because it doesn't go through
    # a "query parsing" process". It does not support field name prefixes,
    # wildcard characters, or other "advanced" features, but chances of
    # failing are non existent.
    # A parsed query will fail if it contains a colon or a curly brace
    """
    es_query = {
        "query": {
            "query_string": {
                "default_field": "name",
                "default_operator": "AND",
                "query": text,
                "allow_leading_wildcard": "false",
                "fuzziness": 0
            }
        }
    }
    """

    es_query = {
        "query": {
            "match": {
                "name": {
                    "query": text,
                    "operator": "AND"
                }
            }
        }
    }
    es = elasticsearch.Elasticsearch(settings.ELASTICSEARCH_URI)
    results = ElasticSearchPaginatorList(
            es, body=es_query,
            index='libreborme', doc_type=doc_type)

    return results class LBSearchForm(forms.Form):
    q = forms.CharField(label=_('Nombre'), max_length=100)
    type = forms.ChoiceField(choices=CHOICES, required=False, label=_('Tipo'),
                             widget=forms.RadioSelect)

    # 25 results per page * 400 pages = 10000 results
    # Easy way to forbid searches that exceed default max results in
    # ElasticSearch. See index.max_result_window property
    page = forms.IntegerField(min_value=1, max_value=400) class CacheMixin(object):
    cache_timeout = 3600

    def get_cache_timeout(self):
        return self.cache_timeout

    def dispatch(self, *args, **kwargs):
        return cache_page(self.get_cache_timeout())(super(CacheMixin, self).dispatch)(*args, **kwargs) class Company(m.Model):
    """ Sociedad """
    name = m.CharField(max_length=260, db_index=True)
    nif = m.CharField(max_length=10)
    slug = m.SlugField(max_length=260, primary_key=True)
    date_creation = m.DateField(blank=True, null=True)
    date_extinction = m.DateField(blank=True, null=True)
    is_active = m.BooleanField(default=True)
    type = m.CharField(max_length=50, choices=SOCIEDADES)

    date_updated = m.DateField(db_index=True)
    in_bormes = JSONField(default=list)
    anuncios = JSONField(default=list)

    cargos_actuales_p = JSONField(default=list)
    cargos_actuales_c = JSONField(default=list)
    cargos_historial_p = JSONField(default=list)
    cargos_historial_c = JSONField(default=list)

    document = SearchVectorField(null=True, db_index=True)

    def add_in_bormes(self, borme):
        if borme not in self.in_bormes:
            self.in_bormes.append(borme)

    @property
    def total_anuncios(self):
        return len(self.anuncios)

    @property
    def total_bormes(self):
        return len(self.in_bormes)

    @property
    def fullname(self):
        return '%s %s' % (self.name.title(), self.type)

    def get_cargos_actuales(self, offset=0, limit=settings.CARGOS_LIMIT):
        cargos_p = self.cargos_actuales_p.copy()
        cargos_c = self.cargos_actuales_c.copy()
        cargos_p = [dict(item, **{'type': 'person'}) for item in cargos_p]
        cargos_c = [dict(item, **{'type': 'company'}) for item in cargos_c]
        cargos = sorted(cargos_p + cargos_c, key=lambda k: k['date_from'])

        if limit == 0:
            limit = len(cargos)
        show_more = offset+limit < len(cargos)

        return cargos[offset:offset+limit], show_more

    def get_cargos_historial(self, offset=0, limit=settings.CARGOS_LIMIT):
        cargos_p = self.cargos_historial_p.copy()
        cargos_c = self.cargos_historial_c.copy()
        cargos_p = [dict(item, **{'type': 'person'}) for item in cargos_p]
        cargos_c = [dict(item, **{'type': 'company'}) for item in cargos_c]
        cargos = sorted(cargos_p + cargos_c, key=lambda k: k['date_to'])

        if limit == 0:
            limit = len(cargos)
        show_more = offset+limit < len(cargos)

        return cargos[offset:offset+limit], show_more

    @property
    def todos_cargos_c(self):
        return self.cargos_actuales_c + self.cargos_historial_c

    @property
    def todos_cargos_p(self):
        return self.cargos_actuales_p + self.cargos_historial_p

    # last access
    # number of visits

    def save(self, *args, **kwargs):
        # TODO:
        # Cambiar SOCIEDAD LIMITADA por SL,
        # SOCIEDAD ANONIMA por SA, COOP, SCL...
        # if self.name.endswith('SOCIEDAD LIMITADA'): ...

        self.slug = slugify(self.name)
        super(Company, self).save(*args, **kwargs)

    def update_cargos_entrantes(self, cargos):
        """ cargos = [dict] """

        for cargo in cargos:
            cargo_embed = cargo.copy()
            if cargo_embed['type'] == 'company':
                del cargo_embed['type']
                self.cargos_actuales_c.append(cargo_embed)
            elif cargo_embed['type'] == 'person':
                del cargo_embed['type']
                self.cargos_actuales_p.append(cargo_embed)

    def update_cargos_salientes(self, cargos):
        """ cargos = [dict] """

        for cargo in cargos:
            cargo_embed = cargo.copy()
            if cargo_embed['type'] == 'company':
                del cargo_embed['type']
                for cargo_a in self.cargos_actuales_c:
                    if all(cargo[k] == cargo_a[k] for k in ('name', 'title')):
                        self.cargos_actuales_c.remove(cargo_a)
                        cargo_embed['date_from'] = cargo_a['date_from']
                        break
                self.cargos_historial_c.append(cargo_embed)
            elif cargo_embed['type'] == 'person':
                del cargo_embed['type']
                for cargo_a in self.cargos_actuales_p:
                    if all(cargo[k] == cargo_a[k] for k in ('name', 'title')):
                        self.cargos_actuales_p.remove(cargo_a)
                        cargo_embed['date_from'] = cargo_a['date_from']
                        break
                self.cargos_historial_p.append(cargo_embed)
            else:
                raise ValueError('type: invalid value')

    def _cesar_cargo(self, company, date):
        """
            company: str
            date: str iso format
        """
        for cargo in self.cargos_actuales_c:
            if cargo['name'] == company:
                self.cargos_actuales_c.remove(cargo)
                cargo['date_to'] = date
                self.cargos_historial_c.append(cargo)

    def get_absolute_url(self):
        return reverse('borme-empresa', args=[str(self.slug)])

    def __str__(self):
        return self.fullname class Person(m.Model):
    """ Persona """
    name = m.CharField(max_length=200, db_index=True)
    slug = m.SlugField(max_length=200, primary_key=True)
    in_companies = JSONField(default=list)
    in_bormes = JSONField(default=list)

    date_updated = m.DateField(db_index=True)
    cargos_actuales = JSONField(default=list)
    cargos_historial = JSONField(default=list)

    document = SearchVectorField(null=True, db_index=True)

    # last access
    # number of visits

    def add_in_companies(self, company):
        if company not in self.in_companies:
            self.in_companies.append(company)

    def add_in_bormes(self, borme):
        if borme not in self.in_bormes:
            self.in_bormes.append(borme)

    def update_cargos_entrantes(self, cargos):
        """ cargos = [dict] """
        self.cargos_actuales.extend(cargos)

    def update_cargos_salientes(self, cargos):
        """ cargos = [dict] """

        for cargo in cargos:
            for cargo_a in self.cargos_actuales:
                if all(cargo[k] == cargo_a[k] for k in ('name', 'title')):
                    self.cargos_actuales.remove(cargo_a)
                    cargo['date_from'] = cargo_a['date_from']
                    break
            self.cargos_historial.append(cargo)

    def _cesar_cargo(self, company, date):
        """ Se llama a este método cuando una sociedad se extingue.
        Todos los cargos vigentes que ocupe en la sociedad extinguida pasan a
        estar en la lista de cargos cesados.
        company: str
        date: str iso format
        """
        for cargo in self.cargos_actuales:
            if cargo['name'] == company:
                self.cargos_actuales.remove(cargo)
                cargo['date_to'] = date
                self.cargos_historial.append(cargo)

    def get_cargos_actuales(self, offset=0, limit=settings.CARGOS_LIMIT):
        """ Devuelve el listado de cargos actuales de una sociedad y si hace
        falta mostrar más (paginación)

        :rtype: (list, bool)
        """
        cargos = self.cargos_actuales.copy()
        cargos = [dict(item, **{'type': 'company'}) for item in cargos]
        cargos = sorted(cargos, key=lambda k: k['date_from'])

        if limit == 0:
            limit = len(cargos)
        show_more = offset+limit < len(cargos)

        return cargos[offset:offset+limit], show_more

    def get_cargos_historial(self, offset=0, limit=settings.CARGOS_LIMIT):
        """ Devuelve el listado de cargos pasados de una sociedad y si hace
        falta mostrar más (paginación)

        :rtype: (list, bool)
        """
        cargos = self.cargos_historial.copy()
        cargos = [dict(item, **{'type': 'company'}) for item in cargos]
        cargos = sorted(cargos, key=lambda k: k['date_to'])

        if limit == 0:
            limit = len(cargos)
        show_more = offset+limit < len(cargos)

        return cargos[offset:offset+limit], show_more

    @property
    def total_companies(self):
        return len(self.in_companies)

    @property
    def total_bormes(self):
        return len(self.in_bormes)

    @property
    def todos_cargos(self):
        return self.cargos_actuales + self.cargos_historial

    def save(self, *args, **kwargs):
        self.slug = slugify(self.name)
        super(Person, self).save(*args, **kwargs)

    def get_absolute_url(self):
        return reverse('borme-persona', args=[str(self.slug)])

    def __str__(self):
        return self.name class Anuncio(m.Model):
    id_anuncio = m.IntegerField()
    year = m.IntegerField()
    borme = m.ForeignKey('Borme', on_delete=m.PROTECT)
    company = m.ForeignKey('Company', on_delete=m.PROTECT)
    datos_registrales = m.CharField(max_length=70)
    actos = JSONField(default=dict)  # TODO: Actos repetidos

    class Meta:
        index_together = ['id_anuncio', 'year']

    @property
    def total_actos(self):
        return len(self.actos)

    def get_absolute_url(self):
        year = str(self.year)
        anuncio_id = str(self.id_anuncio)
        return reverse('borme-anuncio', args=[year, anuncio_id])

    def __str__(self):
        return '%d - %d (%d actos)' % (
                    self.id_anuncio, self.year, len(self.actos.keys())) class Config(m.Model):
    last_modified = m.DateTimeField()
    version = m.CharField(max_length=50) class Borme(m.Model):
    """ Edicion de BORME """
    cve = m.CharField(max_length=30, primary_key=True)
    date = m.DateField(db_index=True)
    url = m.URLField()
    from_reg = m.IntegerField()
    until_reg = m.IntegerField()
    # province = CharField(max_length=100, choices=PROVINCES)
    province = m.CharField(max_length=100)
    section = m.CharField(max_length=20)
    # pages = IntegerField()
    anuncios = JSONField(default=list)

    @property
    def total_anuncios(self):
        return len(self.anuncios)

    def get_absolute_url(self):
        return reverse('borme-borme', args=[str(self.cve)])

    def __str__(self):
        return self.cve def estimate_count_fast(table):
    ''' postgres really sucks at full table counts, this is a faster version
    see: http://wiki.postgresql.org/wiki/Slow_Counting '''
    cursor = connection.cursor()
    cursor.execute("select reltuples from pg_class where relname='%s';" % table)
    row = cursor.fetchone()
    return int(row[0])
from django.views.generic.list import ListView
from django.views.generic.detail import DetailView
from django.views.decorators.cache import cache_page
from django.shortcuts import redirect
from django.views.generic import TemplateView
from django.core.paginator import Paginator, EmptyPage
from django.http import Http404, HttpResponse
from django.utils.safestring import mark_safe
from django.urls import reverse
from django.template.loader import get_template
from django.conf import settings
from .calendar import LibreBormeCalendar, LibreBormeAvailableCalendar
from .documents import es_search_paginator
from .forms import LBSearchForm
from .mixins import CacheMixin
from .models import Company, Person, Anuncio, Config, Borme
from .utils.postgres import estimate_count_fast
        from collections import Counter
            from collections import Counter
            from collections import Counter
import csv
import datetime
    model = Borme
    context_object_name = 'borme'

    def get_object(self):
        try:
            self.borme = Borme.objects.get(cve=self.kwargs['cve'])
            return self.borme
        except Borme.DoesNotExist:
            raise Http404('Borme does not exist')

    def get_context_data(self, **kwargs):
        context = super(BormeView, self).get_context_data(**kwargs)

        anuncios = Anuncio.objects.filter(id_anuncio__gte=self.borme.from_reg,
                                          id_anuncio__lte=self.borme.until_reg,
                                          year=self.borme.date.year)
        resumen_dia = Counter()
        for anuncio in anuncios:
            resumen_dia += Counter(anuncio.actos.keys())

        bormes_dia = Borme.objects.filter(date=self.borme.date).order_by('province')
        bormes_dia = list(bormes_dia)
        bormes_dia.remove(self.borme)
        bormes_dia.sort(key=lambda b: b.province)

        context.update({
            "bormes_dia": bormes_dia,
            "total_anuncios": self.borme.until_reg - self.borme.from_reg + 1,
            "resumen_dia": sorted(resumen_dia.items(), key=lambda t: t[0]),
        })

        return context


class BormeDateView(CacheMixin, TemplateView):
    template_name = 'borme/borme_fecha.html'

    def dispatch(self, request, *args, **kwargs):
        year, month, day = self.kwargs['date'].split('-')
        redirect_today = False

        try:
            self.date = datetime.date(int(year), int(month), int(day))
            if self.date > datetime.date.today():
                redirect_today = True
        except ValueError:
            redirect_today = True

        # Redirect to today's date when date is not valid
        if redirect_today:
            return redirect('borme-fecha', date=datetime.date.today().isoformat())

        return super(BormeDateView, self).dispatch(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        context = super(BormeDateView, self).get_context_data(**kwargs)

        # TODO: LocaleHTMLCalendar(firstweekday=0, locale=None)
        lb_calendar = LibreBormeCalendar().formatmonth(self.date)

        bormes = Borme.objects.filter(date=self.date).order_by('province')
        if len(bormes) > 0:
            from_reg = min([b.from_reg for b in bormes])
            until_reg = max([b.until_reg for b in bormes])
            anuncios = Anuncio.objects.filter(id_anuncio__gte=from_reg,
                                              id_anuncio__lte=until_reg,
                                              year=self.date.year)

            # FIXME: performance-killer? Guardar resultados en el modelo Borme
            resumen_dia = Counter()
            for anuncio in anuncios:
                resumen_dia += Counter(anuncio.actos.keys())

            context['resumen_dia'] = sorted(resumen_dia.items(), key=lambda t: t[0])

        # TODO: Guardar la fecha en el anuncio?
        next_day = self.date + datetime.timedelta(days=1)
        prev_day = self.date - datetime.timedelta(days=1)

        context.update({
            "calendar": mark_safe(lb_calendar),
            "bormes": bormes,
            "date": self.date,
            "next_day": next_day.isoformat(),
            "prev_day": prev_day.isoformat(),
        })

        return context


class BormeProvinciaView(CacheMixin, TemplateView):
    template_name = 'borme/borme_provincia.html'

    def dispatch(self, request, *args, **kwargs):
        if 'year' not in self.kwargs:
            this_year = datetime.date.today().year
            return redirect('borme-provincia-fecha',
                            provincia=self.kwargs['provincia'],
                            year=this_year)

        return super(BormeProvinciaView, self).dispatch(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        context = super(BormeProvinciaView, self).get_context_data(**kwargs)

        year = int(self.kwargs['year'])
        bormes = Borme.objects.filter(date__gte=datetime.date(year, 1, 1),
                                      date__lte=datetime.date(year, 12, 31),
                                      province=self.kwargs['provincia'])

        # TODO: LocaleHTMLCalendar(firstweekday=0, locale=None)
        lb_calendar = LibreBormeAvailableCalendar().formatyear(year, bormes)

        if len(bormes) > 0:
            # FIXME: No se puede hacer minimo y maximo. Hacer where in
            # from_reg = min([b.from_reg for b in bormes])
            # until_reg = max([b.until_reg for b in bormes])
            from_reg = bormes[0].from_reg
            until_reg = bormes[0].until_reg


            anuncios = Anuncio.objects.filter(id_anuncio__gte=from_reg,