forked from wiktor/spejstore-new
Initial web UI, smart search, book import command
This commit is contained in:
parent
9ac7a56135
commit
b653ec8e25
|
@ -37,6 +37,7 @@ INSTALLED_APPS = [
|
||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
|
'django.contrib.postgres',
|
||||||
|
|
||||||
'django_hstore',
|
'django_hstore',
|
||||||
'tree',
|
'tree',
|
||||||
|
@ -59,7 +60,7 @@ ROOT_URLCONF = 'spejstore.urls'
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
'DIRS': [],
|
'DIRS': ['templates/'],
|
||||||
'APP_DIRS': True,
|
'APP_DIRS': True,
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
'context_processors': [
|
'context_processors': [
|
||||||
|
@ -126,3 +127,7 @@ USE_TZ = True
|
||||||
# https://docs.djangoproject.com/en/1.10/howto/static-files/
|
# https://docs.djangoproject.com/en/1.10/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = '/static/'
|
STATIC_URL = '/static/'
|
||||||
|
|
||||||
|
STATICFILES_DIRS = [
|
||||||
|
os.path.join(BASE_DIR, "static"),
|
||||||
|
]
|
||||||
|
|
|
@ -2,20 +2,15 @@
|
||||||
|
|
||||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||||
https://docs.djangoproject.com/en/1.10/topics/http/urls/
|
https://docs.djangoproject.com/en/1.10/topics/http/urls/
|
||||||
Examples:
|
|
||||||
Function views
|
|
||||||
1. Add an import: from my_app import views
|
|
||||||
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
|
|
||||||
Class-based views
|
|
||||||
1. Add an import: from other_app.views import Home
|
|
||||||
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
|
|
||||||
Including another URLconf
|
|
||||||
1. Import the include() function: from django.conf.urls import url, include
|
|
||||||
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
|
|
||||||
"""
|
"""
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url, include
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls.static import static
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^admin/', admin.site.urls),
|
url(r'^admin/', admin.site.urls),
|
||||||
]
|
|
||||||
|
url(r'^', include('storage.urls')),
|
||||||
|
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,30 @@
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from storage.models import Item
|
||||||
|
from io import StringIO
|
||||||
|
import csv
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Imports book library from specified wiki page dump'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('parent')
|
||||||
|
parser.add_argument('file')
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
with open(options['file']) as fd:
|
||||||
|
sio = StringIO(fd.read())
|
||||||
|
reader = csv.reader(sio, delimiter='|')
|
||||||
|
parent = Item.objects.get(pk=options['parent'])
|
||||||
|
for line in reader:
|
||||||
|
line = list(map(str.strip, line))
|
||||||
|
item = Item(parent=parent)
|
||||||
|
item.name = line[2]
|
||||||
|
item.props['author'] = line[1]
|
||||||
|
item.props['owner'] = line[3]
|
||||||
|
item.props['can_borrow'] = line[4]
|
||||||
|
item.props['borrowed_by'] = line[5]
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
self.stdout.write(self.style.NOTICE('Book added: %r') % item)
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS('Successfully imported data'))
|
|
@ -46,7 +46,11 @@ class Item(models.Model, TreeModelMixin):
|
||||||
objects = hstore.HStoreManager()
|
objects = hstore.HStoreManager()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '-' * (self.get_level() or 0) + ' ' +self.name
|
return '- ' * (self.get_level() or 0) + self.name
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
from django.urls import reverse
|
||||||
|
return reverse('item-display', kwargs={'pk': str(self.pk)})
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('path',)
|
ordering = ('path',)
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8 col-md-offset-2">
|
||||||
|
<form action="/search">
|
||||||
|
<div class="input-group input-group-lg">
|
||||||
|
<input type="text" class="form-control" name="q" placeholder="search term">
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button class="btn btn-primary" type="submit"><i class="glyphicon glyphicon-search"></i></button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<input type="checkbox" id="smartsearch" name="smartsearch">
|
||||||
|
<label for="smartsearch">SmartSearch</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,38 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li><a href="/item/">Everything</a></li>
|
||||||
|
{% for ancestor in ancestors %}
|
||||||
|
<li><a href="{{ ancestor.get_absolute_url }}">{{ ancestor.name }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
<li class="active">{{ item.name }}</li>
|
||||||
|
</ol>
|
||||||
|
<h2>{{ item.name }} <small>{{ item.pk }}</small></h2>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
{% if item.props %}
|
||||||
|
<table class="table table-hover table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>key</th>
|
||||||
|
<th>value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
{% for k, v in item.props.items %}
|
||||||
|
<tr><td>{{ k }}</td><td>{{ v }}</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if children %}
|
||||||
|
<h3>Children</h3>
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
{% for child in children %}
|
||||||
|
<tr><td><a href="{{ child.get_absolute_url }}">{{ child.name }}</a></td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,16 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
{% for item in results %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{% for parent in item.get_ancestors %}
|
||||||
|
{{ parent.name }} »
|
||||||
|
{% endfor %}
|
||||||
|
<a href="{{ item.get_absolute_url }}">{{ item.name }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,8 @@
|
||||||
|
from django.conf.urls import include, url
|
||||||
|
from storage.views import index, search, item_display
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^$', index),
|
||||||
|
url(r'^search$', search),
|
||||||
|
url(r'^item/(?P<pk>.*)$', item_display, name='item-display'),
|
||||||
|
]
|
|
@ -1,3 +1,64 @@
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render, get_object_or_404
|
||||||
|
from storage.models import Item
|
||||||
|
from django.contrib.postgres.search import SearchVector
|
||||||
|
import shlex
|
||||||
|
|
||||||
# Create your views here.
|
def apply_smart_search(query, objects):
|
||||||
|
general_term = []
|
||||||
|
|
||||||
|
filters = {}
|
||||||
|
|
||||||
|
for prop in shlex.split(query):
|
||||||
|
if ':' not in prop:
|
||||||
|
general_term.append(prop)
|
||||||
|
else:
|
||||||
|
key, value = prop.split(':', 1)
|
||||||
|
if hasattr(Item, key):
|
||||||
|
filters[key + '__search'] = value
|
||||||
|
|
||||||
|
elif key == 'prop' or value:
|
||||||
|
if key == 'prop':
|
||||||
|
key, value = value.split(':', 1)
|
||||||
|
|
||||||
|
if 'props__contains' not in filters:
|
||||||
|
filters['props__contains'] = {}
|
||||||
|
filters['props__contains'] = {key: value}
|
||||||
|
|
||||||
|
else:
|
||||||
|
# "Whatever:"
|
||||||
|
general_term.append(prop)
|
||||||
|
|
||||||
|
if general_term:
|
||||||
|
objects = objects.annotate(
|
||||||
|
search=SearchVector('name', 'description', 'props'),
|
||||||
|
)
|
||||||
|
filters['search'] = ' '.join(general_term)
|
||||||
|
|
||||||
|
objects = objects.filter(**filters)
|
||||||
|
|
||||||
|
return objects
|
||||||
|
|
||||||
|
def index(request):
|
||||||
|
return render(request, 'index.html')
|
||||||
|
|
||||||
|
def search(request):
|
||||||
|
query = request.GET.get('q', '')
|
||||||
|
|
||||||
|
results = apply_smart_search(query, Item.objects)
|
||||||
|
return render(request, 'results.html', {
|
||||||
|
'query': query,
|
||||||
|
'results': results.all(),
|
||||||
|
})
|
||||||
|
|
||||||
|
def item_display(request, pk):
|
||||||
|
if not pk:
|
||||||
|
return render(request, 'results.html', {
|
||||||
|
'results': Item.get_roots()
|
||||||
|
})
|
||||||
|
item = get_object_or_404(Item, pk=pk)
|
||||||
|
|
||||||
|
return render(request, 'item.html', {
|
||||||
|
'item': item,
|
||||||
|
'ancestors': item.get_ancestors(),
|
||||||
|
'children': item.get_children(),
|
||||||
|
})
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
{% load static %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Hackerspace Storage System</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<link rel="stylesheet" href="{% static 'css/bootstrap.css' %}" media="screen">
|
||||||
|
<style>
|
||||||
|
.btn {
|
||||||
|
font-size: 1.4em;
|
||||||
|
text-transform: none;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn .glyphicon {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
.btn-alt {
|
||||||
|
background: #4b176d;
|
||||||
|
color:white;
|
||||||
|
border-color: #2d0d42; /*#301934;*/
|
||||||
|
}
|
||||||
|
.btn-alt:hover, .btn-alt:active, .btn-alt:link, .btn-alt:visited {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% block body %}
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="page-header">Warsaw Hackerspace <small class="hidden-sm hidden-xs">Enjoy your stay</small></h1>
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue