import base64 import operator from itertools import chain from functools import reduce from urllib.parse import unquote from django.views import View from django.http import Http404, HttpResponseBadRequest from django.db.utils import OperationalError, ProgrammingError from django.db.models import F, Q, Count from django.shortcuts import render, get_object_or_404, get_list_or_404 from django.core.exceptions import ObjectDoesNotExist from django.core.paginator import Paginator from portal.utils import get_site_conf from portal.models import Flatpage from roster.models import RollingStock from consist.models import Consist from bookshelf.models import Book, Catalog from metadata.models import ( Company, Manufacturer, Scale, RollingStockType, Tag, ) def get_items_per_page(): try: items_per_page = get_site_conf().items_per_page except (OperationalError, ProgrammingError): items_per_page = 6 return items_per_page def get_order_by_field(): try: order_by = get_site_conf().items_ordering except (OperationalError, ProgrammingError): order_by = "type" fields = [ "rolling_class__type", "rolling_class__company", "rolling_class__identifier", "road_number_int", ] if order_by == "type": return (fields[0], fields[1], fields[2], fields[3]) elif order_by == "company": return (fields[1], fields[0], fields[2], fields[3]) elif order_by == "identifier": return (fields[2], fields[0], fields[1], fields[3]) class Render404(View): def get(self, request, exception): return render(request, "base.html", {"title": "404 page not found"}) class GetData(View): title = "Home" template = "pagination.html" item_type = "roster" filter = Q() # empty filter by default def get_data(self, request): return ( RollingStock.objects.get_published(request.user) .order_by(*get_order_by_field()) .filter(self.filter) ) def get(self, request, page=1): data = [] for item in self.get_data(request): data.append({"type": self.item_type, "item": item}) paginator = Paginator(data, get_items_per_page()) data = paginator.get_page(page) page_range = paginator.get_elided_page_range( data.number, on_each_side=1, on_ends=1 ) return render( request, self.template, { "title": self.title, "type": self.item_type, "data": data, "matches": paginator.count, "page_range": page_range, }, ) class GetRoster(GetData): title = "The Roster" item_type = "roster" def get_data(self, request): return RollingStock.objects.get_published(request.user).order_by( *get_order_by_field() ) class SearchObjects(View): def run_search(self, request, search, _filter, page=1): if _filter is None: query = reduce( operator.or_, ( Q( Q(rolling_class__identifier__icontains=s) | Q(rolling_class__description__icontains=s) | Q(rolling_class__type__type__icontains=s) | Q(road_number__icontains=s) | Q(item_number=s) | Q(rolling_class__company__name__icontains=s) | Q(rolling_class__company__country__icontains=s) | Q(manufacturer__name__icontains=s) | Q(scale__scale__icontains=s) | Q(tags__name__icontains=s) ) for s in search.split() ), ) elif _filter == "type": query = Q( Q(rolling_class__type__type__icontains=search) | Q(rolling_class__type__category__icontains=search) ) elif _filter == "company": query = Q( Q(rolling_class__company__name__icontains=search) | Q(rolling_class__company__extended_name__icontains=search) ) elif _filter == "manufacturer": query = Q( Q(manufacturer__name__icontains=search) | Q(rolling_class__manufacturer__name__icontains=search) ) elif _filter == "scale": query = Q(scale__scale__icontains=search) else: raise Http404 # FIXME duplicated code! # FIXME see if it makes sense to filter calatogs and books by scale # and manufacturer as well data = [] roster = ( RollingStock.objects.get_published(request.user) .filter(query) .distinct() .order_by(*get_order_by_field()) ) for item in roster: data.append({"type": "roster", "item": item}) if _filter is None: consists = ( Consist.objects.get_published(request.user) .filter( Q( Q(identifier__icontains=search) | Q(company__name__icontains=search) ) ) .distinct() ) for item in consists: data.append({"type": "consist", "item": item}) books = ( Book.objects.get_published(request.user) .filter(title__icontains=search) .distinct() ) catalogs = ( Catalog.objects.get_published(request.user) .filter(manufacturer__name__icontains=search) .distinct() ) for item in list(chain(books, catalogs)): data.append({"type": "book", "item": item}) paginator = Paginator(data, get_items_per_page()) data = paginator.get_page(page) page_range = paginator.get_elided_page_range( data.number, on_each_side=1, on_ends=1 ) return data, paginator.count, page_range def split_search(self, search): search = search.strip().split(":") if not search: raise Http404 elif len(search) == 1: # no filter _filter = None search = search[0].strip() elif len(search) == 2: # filter: search _filter = search[0].strip().lower() search = search[1].strip() else: return HttpResponseBadRequest return _filter, search def get(self, request, search, page=1): try: encoded_search = search search = base64.b64decode(search.encode()).decode() except Exception: encoded_search = base64.b64encode(search.encode()).decode() _filter, keyword = self.split_search(search) data, matches, page_range = self.run_search( request, keyword, _filter, page ) return render( request, "search.html", { "title": 'Search: "{}"'.format(search), "search": search, "encoded_search": encoded_search, "data": data, "matches": matches, "page_range": page_range, }, ) def post(self, request, page=1): search = request.POST.get("search") return self.get(request, search, page) class GetManufacturerItem(View): def get(self, request, manufacturer, search="all", page=1): manufacturer = get_object_or_404( Manufacturer, slug__iexact=manufacturer ) if search != "all": roster = get_list_or_404( RollingStock.objects.get_published(request.user).order_by( *get_order_by_field() ), Q( Q(manufacturer=manufacturer) & Q(item_number_slug__exact=search) ), ) title = "{0}: {1}".format( manufacturer, # all returned records must have the same `item_number``; # just pick it up the first result, otherwise `search` roster[0].item_number if roster else search, ) else: roster = ( RollingStock.objects.get_published(request.user) .filter( Q(manufacturer=manufacturer) | Q(rolling_class__manufacturer=manufacturer) ) .distinct() .order_by(*get_order_by_field()) ) title = "Manufacturer: {0}".format(manufacturer) data = [] for item in roster: data.append({"type": "roster", "item": item}) paginator = Paginator(data, get_items_per_page()) data = paginator.get_page(page) page_range = paginator.get_elided_page_range( data.number, on_each_side=1, on_ends=1 ) return render( request, "manufacturer.html", { "title": title, "manufacturer": manufacturer, "search": search, "data": data, "matches": paginator.count, "page_range": page_range, }, ) class GetObjectsFiltered(View): def run_filter(self, request, search, _filter, page=1): if _filter == "type": title = get_object_or_404(RollingStockType, slug__iexact=search) query = Q(rolling_class__type__slug__iexact=search) elif _filter == "company": title = get_object_or_404(Company, slug__iexact=search) query = Q(rolling_class__company__slug__iexact=search) query_2nd = Q(company__slug__iexact=search) elif _filter == "scale": title = get_object_or_404(Scale, slug__iexact=search) query = Q(scale__slug__iexact=search) query_2nd = Q( consist_item__rolling_stock__scale__slug__iexact=search ) elif _filter == "tag": title = get_object_or_404(Tag, slug__iexact=search) query = Q(tags__slug__iexact=search) query_2nd = query # For tags the 2nd level query doesn't change else: raise Http404 roster = ( RollingStock.objects.get_published(request.user) .filter(query) .distinct() .order_by(*get_order_by_field()) ) data = [] for item in roster: data.append({"type": "roster", "item": item}) try: # Execute only if query_2nd is defined consists = ( Consist.objects.get_published(request.user) .filter(query_2nd) .distinct() ) for item in consists: data.append({"type": "consist", "item": item}) if _filter == "tag": # Books can be filtered only by tag books = ( Book.objects.get_published(request.user) .filter(query_2nd) .distinct() ) for item in books: data.append({"type": "book", "item": item}) catalogs = ( Catalog.objects.get_published(request.user) .filter(query_2nd) .distinct() ) for item in catalogs: data.append({"type": "catalog", "item": item}) except NameError: pass paginator = Paginator(data, get_items_per_page()) data = paginator.get_page(page) page_range = paginator.get_elided_page_range( data.number, on_each_side=1, on_ends=1 ) return data, title, paginator.count, page_range def get(self, request, search, _filter, page=1): data, title, matches, page_range = self.run_filter( request, unquote(search), _filter, page ) return render( request, "filter.html", { "title": "{0}: {1}".format(_filter.capitalize(), title), "search": search, "filter": _filter, "data": data, "matches": matches, "page_range": page_range, }, ) class GetRollingStock(View): def get(self, request, uuid): try: rolling_stock = RollingStock.objects.get_published( request.user ).get(uuid=uuid) except ObjectDoesNotExist: raise Http404 # FIXME there's likely a better and more efficient way of doing this # but keeping KISS for now decoder_documents = [] class_properties = rolling_stock.rolling_class.property.get_public( request.user ) properties = rolling_stock.property.get_public(request.user) documents = rolling_stock.document.get_public(request.user) journal = rolling_stock.journal.get_public(request.user) if rolling_stock.decoder: decoder_documents = rolling_stock.decoder.document.get_public( request.user ) consists = [ {"type": "consist", "item": c} for c in Consist.objects.get_published(request.user).filter( consist_item__rolling_stock=rolling_stock ) ] # A dict with "item" is required by the consists card set = [ {"type": "set", "item": s} for s in RollingStock.objects.get_published(request.user) .filter( Q( Q(item_number__exact=rolling_stock.item_number) & Q(set=True) ) ) .order_by(*get_order_by_field()) ] return render( request, "rollingstock.html", { "title": rolling_stock, "rolling_stock": rolling_stock, "class_properties": class_properties, "properties": properties, "decoder_documents": decoder_documents, "documents": documents, "journal": journal, "set": set, "consists": consists, }, ) class Consists(GetData): title = "Consists" item_type = "consist" def get_data(self, request): return Consist.objects.get_published(request.user).all() class GetConsist(View): def get(self, request, uuid, page=1): try: consist = Consist.objects.get_published(request.user).get( uuid=uuid ) except ObjectDoesNotExist: raise Http404 data = [ { "type": "roster", "item": RollingStock.objects.get_published(request.user).get( uuid=r.rolling_stock_id ), } for r in consist.consist_item.all() ] paginator = Paginator(data, get_items_per_page()) data = paginator.get_page(page) page_range = paginator.get_elided_page_range( data.number, on_each_side=1, on_ends=1 ) return render( request, "consist.html", { "title": consist, "consist": consist, "data": data, "page_range": page_range, }, ) class Manufacturers(GetData): title = "Manufacturers" item_type = "manufacturer" def get_data(self, request): return ( Manufacturer.objects.filter(self.filter) .annotate( num_items=Count("rollingstock") + Count("rollingclass"), ) .order_by("name") ) # overload get method to filter by category def get(self, request, category, page=1): if category not in ("real", "model"): raise Http404 self.filter = Q(category=category) return super().get(request, page) class Companies(GetData): title = "Companies" item_type = "company" def get_data(self, request): return ( Company.objects.annotate( num_rollingstock=( Count( "rollingclass__rolling_class", filter=Q( rollingclass__rolling_class__in=( RollingStock.objects.get_published( request.user ) ) ), distinct=True, ) ) ) .annotate( num_consists=( Count( "consist", filter=Q( consist__in=( Consist.objects.get_published(request.user) ), ), distinct=True, ) ) ) .annotate(num_items=F("num_rollingstock") + F("num_consists")) .order_by("name") ) class Scales(GetData): title = "Scales" item_type = "scale" def get_data(self, request): return Scale.objects.annotate( num_items=Count( "rollingstock", filter=Q( rollingstock__in=RollingStock.objects.get_published( request.user ) ), ), ).order_by("-ratio_int", "-tracks", "scale") class Types(GetData): title = "Types" item_type = "rolling_stock_type" def get_data(self, request): return RollingStockType.objects.annotate( num_items=Count( "rollingclass__rolling_class", filter=Q( rollingclass__rolling_class__in=( RollingStock.objects.get_published(request.user) ) ), ) ).order_by("order") class Books(GetData): title = "Books" item_type = "book" def get_data(self, request): return Book.objects.get_published(request.user).all() class Catalogs(GetData): title = "Catalogs" item_type = "catalog" def get_data(self, request): return Catalog.objects.get_published(request.user).all() class GetBookCatalog(View): def get_object(self, request, uuid, selector): if selector == "book": return Book.objects.get_published(request.user).get(uuid=uuid) elif selector == "catalog": return Catalog.objects.get_published(request.user).get(uuid=uuid) else: raise Http404 def get(self, request, uuid, selector): try: book = self.get_object(request, uuid, selector) except ObjectDoesNotExist: raise Http404 properties = book.property.get_public(request.user) documents = book.document.get_public(request.user) return render( request, "bookshelf/book.html", { "title": book, "book": book, "documents": documents, "properties": properties, "type": selector, }, ) class GetFlatpage(View): def get(self, request, flatpage): try: flatpage = Flatpage.objects.get_published(request.user).get( path=flatpage ) except ObjectDoesNotExist: raise Http404 return render( request, "flatpages/flatpage.html", {"title": flatpage.name, "flatpage": flatpage}, )