From 7dac9bd28f0ee2434f844804e3d5af4f4aca8de6 Mon Sep 17 00:00:00 2001 From: Luke Hollis <lukehollis@gmail.com> Date: Sun, 14 Mar 2021 01:22:38 -0800 Subject: [PATCH] add updated version of relationship between Collections and Items in Elasticsearch and add frontend for general public user to create Collections and add and remove items --- giza/admin.py | 10 +- giza/forms.py | 8 +- giza/migrations/0005_auto_20210314_0551.py | 27 ++++ giza/models.py | 16 +-- giza/settings.py | 2 +- giza/urls.py | 4 +- giza/views.py | 124 +++++++++++++++--- templates/pages/full.html | 28 ++++ templates/pages/mygiza-allcollections.html | 28 ++-- templates/pages/mygiza-collection-edit.html | 9 +- templates/pages/mygiza-collection.html | 38 ++++-- templates/partials/modals.html | 35 +++-- .../partials/mygiza-collection-blurb.html | 8 +- tms/templates/tms/full.html | 6 +- tms/views.py | 9 +- 15 files changed, 267 insertions(+), 85 deletions(-) create mode 100644 giza/migrations/0005_auto_20210314_0551.py diff --git a/giza/admin.py b/giza/admin.py index 1b1e40d..af3d405 100644 --- a/giza/admin.py +++ b/giza/admin.py @@ -4,7 +4,7 @@ from django.contrib.auth.admin import UserAdmin from .forms import CustomUserCreationForm, CustomUserChangeForm -from .models import CustomUser, Lesson, Topic, Collection +from .models import CustomUser, Lesson, Topic, Collection, ElasticsearchItem class CustomUserAdmin(UserAdmin): add_form = CustomUserCreationForm @@ -15,9 +15,17 @@ class CustomUserAdmin(UserAdmin): (None, {'fields': ('full_name', 'bio', 'tagline', 'picture')}), ) + +class ElasticsearchItemInline(admin.TabularInline): + model = ElasticsearchItem + class CollectionAdmin(admin.ModelAdmin): readonly_fields=('slug',) + inlines = [ + ElasticsearchItemInline, + ] + class LessonAdmin(admin.ModelAdmin): readonly_fields=('slug',) diff --git a/giza/forms.py b/giza/forms.py index 284f25a..cb28acf 100644 --- a/giza/forms.py +++ b/giza/forms.py @@ -2,7 +2,7 @@ from django.contrib.auth.forms import UserCreationForm, UserChangeForm from tinymce.widgets import TinyMCE -from .models import CustomUser +from .models import CustomUser, Collection class CustomUserCreationForm(UserCreationForm): class Meta: @@ -13,3 +13,9 @@ class CustomUserChangeForm(UserChangeForm): class Meta: model = CustomUser fields = ('full_name', 'username', 'email',) + +class CollectionForm(forms.ModelForm): + title = forms.CharField(required=False, label="Name your new collection") + class Meta: + model = Collection + fields = ('title', ) diff --git a/giza/migrations/0005_auto_20210314_0551.py b/giza/migrations/0005_auto_20210314_0551.py new file mode 100644 index 0000000..6f32866 --- /dev/null +++ b/giza/migrations/0005_auto_20210314_0551.py @@ -0,0 +1,27 @@ +# Generated by Django 2.2.12 on 2021-03-14 05:51 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('giza', '0004_lesson_summary'), + ] + + operations = [ + migrations.RemoveField( + model_name='collection', + name='items', + ), + migrations.CreateModel( + name='ElasticsearchItem', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('type', models.CharField(max_length=20)), + ('es_id', models.IntegerField()), + ('collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='giza.Collection')), + ], + ), + ] diff --git a/giza/models.py b/giza/models.py index b1bcf48..9b71c80 100644 --- a/giza/models.py +++ b/giza/models.py @@ -1,8 +1,6 @@ from django.contrib.auth.models import AbstractUser from django.db import models -from django.contrib.postgres.fields import ArrayField, JSONField from django.utils.text import slugify -from composite_field import CompositeField from tinymce.models import HTMLField @@ -15,16 +13,6 @@ ] -class EsCompositeField(CompositeField): - type = models.CharField(max_length=20) - id = models.IntegerField() - - def set_attributes_from_name(self, name="EsCompositeField"): - return name - - def check(self, **kwargs): - return [] - class CustomUser(AbstractUser): full_name = models.CharField(max_length=256, blank=True) @@ -110,10 +98,10 @@ def save(self, *args, **kwargs): super().save(*args, **kwargs) -class EsItem(models.Model): +class ElasticsearchItem(models.Model): collection = models.ForeignKey(Collection, related_name='items', on_delete=models.CASCADE) type = models.CharField(max_length=20) es_id = models.IntegerField() def __str__(self): - return "{}-{}".format(es_id, type) + return "{}-{}".format(self.es_id, self.type) diff --git a/giza/settings.py b/giza/settings.py index 91875fa..02b80dd 100644 --- a/giza/settings.py +++ b/giza/settings.py @@ -26,7 +26,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = ['giza-web2.rc.fas.harvard.edu', 'giza.fas.harvard.edu'] +ALLOWED_HOSTS = ['giza-web2.rc.fas.harvard.edu', 'giza.fas.harvard.edu', 'localhost'] # Application definition diff --git a/giza/urls.py b/giza/urls.py index 2eea868..2050546 100644 --- a/giza/urls.py +++ b/giza/urls.py @@ -15,6 +15,7 @@ """ from django.conf.urls import url from django.urls import path +from django.conf.urls import include from django.contrib import admin from tms import views as tms_views from search import views as search_views @@ -34,7 +35,7 @@ url(r'^collections/user$', views.collections_user, name="collections_user"), url(r'^collections/create$', views.collections_create, name="collections_create"), url(r'^collections/(?P<slug>[\w-]+)$', views.collection, name="collection"), - url(r'^collections/(?P<slug>[\w-]+)/edit$', views.collection, name="collection_edit"), + url(r'^collections/(?P<slug>[\w-]+)/edit$', views.collections_edit, name="collection_edit"), url(r'^collections/$', views.collections, name="collections"), url(r'^mygiza/$', views.mygiza, name="mygiza"), @@ -47,6 +48,7 @@ url(r'^search-results/$', search_views.results, name='results'), # auth + # TODO for password change/reset, implement django accounts auth # path('accounts/', include('django.contrib.auth.urls')), url('sign-up/', views.sign_up, name='sign_up'), url('login/', views.user_login, name='login'), diff --git a/giza/views.py b/giza/views.py index 07b471e..8324889 100644 --- a/giza/views.py +++ b/giza/views.py @@ -17,8 +17,8 @@ from utils.elastic_backend import es, ES_INDEX from utils.views_utils import CATEGORIES, FACETS_PER_CATEGORY, FIELDS_PER_CATEGORY -from .forms import CustomUserCreationForm -from .models import Collection, Lesson +from .forms import CustomUserCreationForm, CollectionForm +from .models import Collection, Lesson, ElasticsearchItem RESULTS_SIZE = 20 @@ -266,7 +266,40 @@ def collection(request, slug): hits = [] search_term = request.GET.get('q', '') - item_ids = collection.items.split(",") + query = {} + + if collection.items.all(): + query = { + 'bool': { + "should": [], + } + } + for elasticsearch_item in collection.items.all(): + query['bool']['should'].append({ + 'bool': { + 'must': [ + { + 'term': { + "_type": elasticsearch_item.type, + } + }, + { + 'term': { + "_id": elasticsearch_item.es_id, + } + }, + ] + } + }) + else: + # pass a query that will get no values returned + query = { + 'ids': { + 'type': '_doc', + 'values': [] + } + } + categorystring = "" current_category = request.GET.get('category', '') current_subfacets = {} @@ -291,11 +324,7 @@ def collection(request, slug): body_query = { "from": results_from, "size": RESULTS_SIZE, - "query": { - "ids": { - "values": item_ids, - } - }, + "query": query, "aggregations": { "aggregation": { "terms": { @@ -324,11 +353,7 @@ def collection(request, slug): search_results = es.search(index=ES_INDEX, body={ "from": results_from, "size": RESULTS_SIZE, - "query": { - "ids": { - "values": item_ids, - } - }, + "query": query, "aggregations": { "aggregation": { "terms": { @@ -399,11 +424,78 @@ def collection(request, slug): def collections_create(request): - return render(request, 'pages/mygiza-collection-edit.html') + # create a collection + if request.method == 'POST': + collection_form = CollectionForm(data=request.POST) + + # save user + if collection_form.is_valid(): + # create user + collection = collection_form.save() + collection.owners.add(request.user) + collection.save() -def collections_edit(request): + return redirect('/collections/{}'.format(collection.slug)) - return render(request, 'pages/mygiza-collection-edit.html') + else: + messages.error(request, "Error creating collection.") + + # show collection form + else: + collection_form = CollectionForm() + + return render(request, 'pages/mygiza-collection-edit.html', { + 'collection_form': collection_form, + }) + +def collections_edit(request, slug): + + # create a collection + if request.method == 'POST': + collection_form = CollectionForm(data=request.POST) + + # save user + if collection_form.is_valid(): + # create user + collection = collection_form.save() + collection.save() + + return redirect('/collections/{}'.format(collection.slug)) + + else: + messages.error(request, "Error creating collection.") + + # show collection form + else: + collection = get_object_or_404(Collection, slug=slug) + collection_form = CollectionForm(collection) + + # user does not own this collection, redirect to collections page + if not request.user in collection.owners.all(): + return redirect('/collections/') + + # handle adding new item id and type to collection + if request.GET.get('add_item_id') and request.GET.get('add_item_type'): + elasticsearch_item = ElasticsearchItem( + es_id=request.GET.get('add_item_id'), + type=request.GET.get('add_item_type'), + collection=collection + ) + elasticsearch_item.save() + return redirect('/collections/{}'.format(collection.slug)) + + elif request.GET.get('remove_item_id') and request.GET.get('remove_item_type'): + elasticsearch_item = ElasticsearchItem.objects.filter( + es_id=request.GET.get('remove_item_id'), + type=request.GET.get('remove_item_type'), + collection=collection + ) + elasticsearch_item.delete() + return redirect('/collections/{}'.format(collection.slug)) + + return render(request, 'pages/mygiza-collection-edit.html', { + 'collection_form': collection_form, + }) def lessons(request): lessons = Lesson.objects.all() diff --git a/templates/pages/full.html b/templates/pages/full.html index d6d1cf8..f20d0d9 100644 --- a/templates/pages/full.html +++ b/templates/pages/full.html @@ -29,6 +29,28 @@ <h1>{{ object.displaytext }}<a name="top"></a></h1> <div class="feature-block secondary" id="jumpmenu"> <div class="feature-block__body"> + <h5 class="heading-em">MyGiza:</h5> + <p> + {% if user.is_authenticated %} + <a data-open="add_collection" aria-controls="add_collection" aria-haspopup="true" tabindex="0"> + <span class="icon-plus-circle icon-fw"></span> Add this to a collection + </a> + {% else %} + <a href="/login" tabindex="0"> + <span class="icon-plus-circle icon-fw"></span> Add this to a collection + </a> + {% endif %} + </p> + + <h5 class="heading-em">Jump to:</h5> + <ul class="vertical menu show-for-medium"> + <li> + <a href="#details"> + <span class="icon-info-circle icon-fw"></span> Details + </a> + </li> + </ul> + <h5 class="heading-em">Jump to:</h5> @@ -829,6 +851,12 @@ <h3 class="feature-block__title"> <!-- /Modal --> + <!-- + TEMPORARY FIX: + Include modals and styles for Adding items to Collections + --> + {% include 'partials/modals.html' %} + <link rel="stylesheet" href="{% static 'css/app.css' %}"> {% endblock %} {% block extra_js %} diff --git a/templates/pages/mygiza-allcollections.html b/templates/pages/mygiza-allcollections.html index c362687..ca5126d 100644 --- a/templates/pages/mygiza-allcollections.html +++ b/templates/pages/mygiza-allcollections.html @@ -23,7 +23,7 @@ <!--a href="mygiza-saved-searches.html">Saved Searches</a--> </div> {% if user.is_superuser %} - <a href="/admin/giza/collection/add/" class="add" ><span class="sr-only">add</span></a> + <a href="/admin/giza/collection/add" class="add" ><span class="sr-only">add</span></a> {% elif user.is_authenticated %} <a href="/collections/create" class="add" ><span class="sr-only">add</span></a> {% else %} @@ -36,16 +36,20 @@ <ul class="dropdown menu" data-dropdown-menu> <li class="{% if request.path == '/collections/' %}active{% endif %}"> <a href="/collections" >All Collections</a> - <ul class="menu show-for-small-only"> - <li> - <a href="/collections/user">My Collections</a> - </li> - <!--li><a href="#">Collections Shared with Me</a></li--> - </ul> - </li> - <li class="hide-for-small-only {% if request.path == '/collections/user' %}active{% endif %}"> - <a href="/collections/user" >My Collections</a> + {% if user.is_authenticated %} + <ul class="menu show-for-small-only"> + <li> + <a href="/collections/user">My Collections</a> + </li> + <!--li><a href="#">Collections Shared with Me</a></li--> + </ul> + {% endif %} </li> + {% if user.is_authenticated %} + <li class="hide-for-small-only {% if request.path == '/collections/user' %}active{% endif %}"> + <a href="/collections/user" >My Collections</a> + </li> + {% endif %} <!--li class="hide-for-small-only"><a href="#">Collections Shared with Me</a></li--> </ul> <!-- a href="/collections/" class="go-back">Back to Individual Collections</a--> @@ -90,7 +94,7 @@ </picture> <div class="caption"> <strong>{{collection.title}}</strong> - <span>{{collection.items|length}} items</span> + <span>{{collection.items.all|length}} items</span> </div> <i class="fas fa-chevron-right show-for-small-only"></i> </a> @@ -100,7 +104,7 @@ <label> <input type="checkbox"> <span class="fake-input"></span> - <span>{{collection.title}} <em>({{collection.items|length}} items)</em></span> + <span>{{collection.title}} <em>({{collection.items.all|length}} items)</em></span> </label> </div> <div class="btn-hold"> diff --git a/templates/pages/mygiza-collection-edit.html b/templates/pages/mygiza-collection-edit.html index 582272e..2c7daad 100644 --- a/templates/pages/mygiza-collection-edit.html +++ b/templates/pages/mygiza-collection-edit.html @@ -26,16 +26,11 @@ <h1 class="title">Create a new collection</h1> Creating and editing collections is restricted to admin users currently. We hope to add this functionality in soon. </p> <div class="jcf-scrollable"> - <form class="new_form" action="#"> - <label> - Name your new collection: - <input type="text" disabled> - </label> - </form> + {{collection_form.as_p}} </div> <br> <div class="btn-row"> - <button type="submit" class="button" disabled>Create</button> + <button type="submit" class="button" >Create</button> </div> </form> </div> diff --git a/templates/pages/mygiza-collection.html b/templates/pages/mygiza-collection.html index e3efca9..065af57 100644 --- a/templates/pages/mygiza-collection.html +++ b/templates/pages/mygiza-collection.html @@ -47,7 +47,7 @@ <!--a href="mygiza-saved-searches.html">Saved Searches</a--> </div> {% if user.is_superuser %} - <a href="/admin/giza/collection/add/" class="add" ><span class="sr-only">add</span></a> + <a href="/admin/giza/collection/add" class="add" ><span class="sr-only">add</span></a> {% elif user.is_authenticated %} <a href="/collections/create" class="add" ><span class="sr-only">add</span></a> {% else %} @@ -60,16 +60,20 @@ <ul class="dropdown menu" data-dropdown-menu> <li class="{% if request.path == '/collections/' %}active{% endif %}"> <a href="/collections" >All Collections</a> - <ul class="menu show-for-small-only"> - <li> - <a href="/collections/user">My Collections</a> - </li> - <!--li><a href="#">Collections Shared with Me</a></li--> - </ul> - </li> - <li class="hide-for-small-only {% if request.path == '/collections/user' %}active{% endif %}"> - <a href="/collections/user" >My Collections</a> + {% if user.is_authenticated %} + <ul class="menu show-for-small-only"> + <li> + <a href="/collections/user">My Collections</a> + </li> + <!--li><a href="#">Collections Shared with Me</a></li--> + </ul> + {% endif %} </li> + {% if user.is_authenticated %} + <li class="hide-for-small-only {% if request.path == '/collections/user' %}active{% endif %}"> + <a href="/collections/user" >My Collections</a> + </li> + {% endif %} <!--li class="hide-for-small-only"><a href="#">Collections Shared with Me</a></li--> </ul> <!-- a href="/collections/" class="go-back">Back to Individual Collections</a--> @@ -263,7 +267,11 @@ <h1>{{collection.title}}</h1> Created by {% for owner in collection.owners.all %} <span class="collection-meta-creator"> - {{owner.full_name}} + {% if owner.full_name %} + {{owner.full_name}} + {% else %} + {{owner.username}} + {% endif %} </span> {% endfor %} </li> @@ -287,6 +295,7 @@ <h1>{{collection.title}}</h1> {% for result in hits %} <li> <div class="item-wrap"> + <a href="{% url 'get_type_html' result.type result.id 'full' %}"> <picture> {% if result.source.primarydisplay.thumbnail %} @@ -305,6 +314,13 @@ <h1>{{collection.title}}</h1> </div> <i class="fas fa-chevron-right show-for-small-only"></i> </a> + + {% if user in collection.owners.all %} + <a href="/collections/{{collection.slug}}/edit?remove_item_id={{result.id}}&remove_item_type={{result.type}}" tabindex="0"> + <span class="icon-minus-circle icon-fw"></span> Remove + </a> + {% endif %} + </div> </li> {% empty %} diff --git a/templates/partials/modals.html b/templates/partials/modals.html index eb313ef..402699e 100644 --- a/templates/partials/modals.html +++ b/templates/partials/modals.html @@ -155,26 +155,33 @@ <h2>Name of this image</h2> <div class="reveal modal add-reveal" id="add_collection" data-reveal> <span class="title">Add to a MyGiza Collection</span> - <span class="info-line">Hi Heather! (not you? <a href="#">Login to your account</a>.)</span> + <br> + <span class="info-line">Hi {% if user.full_name %}{{user.full_name}}{% else %}{{user.username}}{% endif %}! (not you? <a href="/login">Login to your account</a>.)</span> <div class="jcf-scrollable"> - <form class="new_form" action="#"> - <label> - Name your new collection: - <input type="text"> - </label> + <form id="collection_create_form" method="post" action="{% url 'collections_create' %}?redirect_to={{redirect_to}}" enctype="multipart/form-data"> + {% csrf_token %} + {% if messages %} + <ul class="messages"> + {% for message in messages %} + <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li> + {% endfor %} + </ul> + <br> + {% endif %} + + {{ collection_form.as_p }} <div class="btn-row"> - <button type="submit" class="button btn-white">Cancel</button> - <button type="submit" class="button">create</button> + <!--button type="submit" class="button btn-white">Cancel</button--> + <button type="submit" class="button">Create</button> </div> </form> <ul class="add-lists"> - <li><a href="#">My Favorite Pictures <span class="ico"></span></a></li> - <li><a href="#">My Latest Project <span class="ico"></span></a></li> - <li><a href="#">Khafre Pyramid Complex <span class="ico"></span></a></li> - <li><a href="#">Khafre Pyramid Complex <span class="ico"></span></a></li> + {% for collection in user_collections %} + <li><a href="/collections/{{collection.slug}}/edit?add_item_id={{object.id}}&add_item_type={{type}}">{{collection.title}} <span class="ico"></span></a></li> + {% endfor %} </ul> </div> - <div class="bottom-info-block"> + <!--div class="bottom-info-block"> <div class="img-wrap"> <picture> <source srcset="{% static 'images/thumb60-1.jpg' %}, {% static 'images/thumb60-1@2x.jpg' %} 2x"/> @@ -186,7 +193,7 @@ <h2>Name of this image</h2> <li>Tombs & Monuments</li> <li>Sphinx Complex</li> </ul> - </div> + </div--> {% include 'partials/close-button.html' with label="Close modal" %} </div> diff --git a/templates/partials/mygiza-collection-blurb.html b/templates/partials/mygiza-collection-blurb.html index e12975b..dfdf5ef 100644 --- a/templates/partials/mygiza-collection-blurb.html +++ b/templates/partials/mygiza-collection-blurb.html @@ -27,11 +27,15 @@ <h3 class="collection-title heading-mixedcase"> </a> </h3> <p class="text-smaller"> - <strong class="collection-meta-count">{{collection.items|length}} items</strong><br> + <strong class="collection-meta-count">{{collection.items.all|length}} items</strong><br> {% for owner in collection.owners.all %} <span class="collection-meta-creator"> Created by - {{owner.full_name}} + {% if owner.full_name %} + {{owner.full_name}} + {% else %} + {{owner.username}} + {% endif %} </span> {% endfor %} </p> diff --git a/tms/templates/tms/full.html b/tms/templates/tms/full.html index 5e076d6..f9a60dc 100644 --- a/tms/templates/tms/full.html +++ b/tms/templates/tms/full.html @@ -842,8 +842,8 @@ <h3 class="feature-block__title"> windows: [ { manifestId: url } ] - }); - } - }); + }); + } + }); </script> {% endblock %} diff --git a/tms/views.py b/tms/views.py index 1652f6f..546a7a8 100644 --- a/tms/views.py +++ b/tms/views.py @@ -3,6 +3,7 @@ from django.template.exceptions import TemplateDoesNotExist from tms import models +from giza.forms import CollectionForm from utils.elastic_backend import ES_INDEX import json @@ -22,7 +23,7 @@ def get_type_html_legacy(request, type, id, view): if view == "intro": view = "full" type_object = models.get_item(id, type, ES_INDEX) - return render(request, 'tms/'+view+'.html', {'object': type_object, 'type': type}) + return render(request, 'tms/'+view+'.html', {'object': type_object, 'type': type,}) # except: # raise Http404("There was an error getting this item") @@ -62,7 +63,11 @@ def get_type_html(request, type, id, view): if view == "intro": view = "full" type_object = models.get_item(id, type, ES_INDEX) - return render(request, 'pages/'+view+'.html', {'object': type_object, 'type': type}) + + # add form for creating a new collection in modal + collection_form = CollectionForm() + + return render(request, 'pages/'+view+'.html', {'object': type_object, 'type': type, 'collection_form': collection_form}) # except: # raise Http404("There was an error getting this item")