diff --git a/fedireads/migrations/0001_initial.py b/fedireads/migrations/0001_initial.py index f647d8866..fcc00f97b 100644 --- a/fedireads/migrations/0001_initial.py +++ b/fedireads/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.0.13 on 2020-01-25 21:32 +# Generated by Django 2.0.13 on 2020-01-25 23:55 from django.conf import settings import django.contrib.auth.models @@ -50,25 +50,36 @@ class Migration(migrations.Migration): ('objects', django.contrib.auth.models.UserManager()), ], ), + migrations.CreateModel( + name='Author', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('openlibary_key', models.CharField(max_length=255)), + ('data', django.contrib.postgres.fields.jsonb.JSONField()), + ('added_date', models.DateTimeField(auto_now_add=True)), + ('updated_date', models.DateTimeField(auto_now=True)), + ], + ), migrations.CreateModel( name='Book', fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('openlibary_key', models.CharField(max_length=255)), ('data', django.contrib.postgres.fields.jsonb.JSONField()), ('added_date', models.DateTimeField(auto_now_add=True)), ('updated_date', models.DateTimeField(auto_now=True)), ('added_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ('authors', models.ManyToManyField(to='fedireads.Author')), ], ), migrations.CreateModel( name='Review', fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=255)), ('content', django.contrib.postgres.fields.jsonb.JSONField(max_length=5000)), ('created_date', models.DateTimeField(auto_now_add=True)), ('updated_date', models.DateTimeField(auto_now=True)), - ('id', models.AutoField(primary_key=True, serialize=False)), ('star_rating', models.IntegerField(default=0)), ('author', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), ('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fedireads.Book')), @@ -80,26 +91,44 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Shelf', fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=100)), ('editable', models.BooleanField(default=True)), + ('shelf_type', models.CharField(default='custom', max_length=100)), ('created_date', models.DateTimeField(auto_now_add=True)), ('updated_date', models.DateTimeField(auto_now=True)), - ('books', models.ManyToManyField(to='fedireads.Book')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='ShelfBook', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('added_date', models.DateTimeField(auto_now_add=True)), + ('added_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fedireads.Book')), + ('shelf', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fedireads.Shelf')), ], ), migrations.CreateModel( name='Work', fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('openlibary_key', models.CharField(max_length=255)), ('data', django.contrib.postgres.fields.jsonb.JSONField()), ('added_date', models.DateTimeField(auto_now_add=True)), ('updated_date', models.DateTimeField(auto_now=True)), - ('added_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), ], ), + migrations.AddField( + model_name='shelf', + name='books', + field=models.ManyToManyField(through='fedireads.ShelfBook', to='fedireads.Book'), + ), + migrations.AddField( + model_name='shelf', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + ), migrations.AddField( model_name='book', name='works', diff --git a/fedireads/models.py b/fedireads/models.py index 76d9630e6..b32a0621e 100644 --- a/fedireads/models.py +++ b/fedireads/models.py @@ -1,5 +1,6 @@ ''' database schema for the whole dang thing ''' from django.db import models +from django.dispatch import receiver from django.contrib.auth.models import AbstractUser from django.contrib.postgres.fields import JSONField from Crypto.PublicKey import RSA @@ -28,10 +29,27 @@ class User(AbstractUser): super().save(*args, **kwargs) +@receiver(models.signals.post_save, sender=User) +def execute_after_save(sender, instance, created, *args, **kwargs): + if not created: + return + shelves = [{ + 'name': 'To Read', + 'type': 'to-read', + }, { + 'name': 'Currently Reading', + 'type': 'reading', + }, { + 'name': 'Read', + 'type': 'read', + }] + + for shelf in shelves: + Shelf(name=shelf['name'], shelf_type=shelf['type'], user=instance, editable=False).save() + class Message(models.Model): ''' any kind of user post, incl. reviews, replies, and status updates ''' - id = models.AutoField(primary_key=True) author = models.ForeignKey('User', on_delete=models.PROTECT) name = models.CharField(max_length=255) content = JSONField(max_length=5000) @@ -43,35 +61,52 @@ class Message(models.Model): class Review(Message): - id = models.AutoField(primary_key=True) book = models.ForeignKey('Book', on_delete=models.PROTECT) star_rating = models.IntegerField(default=0) class Shelf(models.Model): - id = models.AutoField(primary_key=True) name = models.CharField(max_length=100) user = models.ForeignKey('User', on_delete=models.PROTECT) editable = models.BooleanField(default=True) - books = models.ManyToManyField('Book', symmetrical=False) + shelf_type = models.CharField(default='custom', max_length=100) + books = models.ManyToManyField( + 'Book', + symmetrical=False, + through='ShelfBook', + through_fields=('shelf', 'book') + ) created_date = models.DateTimeField(auto_now_add=True) updated_date = models.DateTimeField(auto_now=True) +class ShelfBook(models.Model): + # many to many join table for books and shelves + book = models.ForeignKey('Book', on_delete=models.PROTECT) + shelf = models.ForeignKey('Shelf', on_delete=models.PROTECT) + added_by = models.ForeignKey('User', blank=True, null=True, on_delete=models.PROTECT) + added_date = models.DateTimeField(auto_now_add=True) + + class Book(models.Model): ''' a non-canonical copy from open library ''' - id = models.AutoField(primary_key=True) openlibary_key = models.CharField(max_length=255) data = JSONField() works = models.ManyToManyField('Work') + authors = models.ManyToManyField('Author') added_by = models.ForeignKey('User', on_delete=models.PROTECT, blank=True, null=True) added_date = models.DateTimeField(auto_now_add=True) updated_date = models.DateTimeField(auto_now=True) class Work(models.Model): - id = models.AutoField(primary_key=True) openlibary_key = models.CharField(max_length=255) data = JSONField() - added_by = models.ForeignKey('User', on_delete=models.PROTECT, blank=True, null=True) added_date = models.DateTimeField(auto_now_add=True) updated_date = models.DateTimeField(auto_now=True) + +class Author(models.Model): + openlibary_key = models.CharField(max_length=255) + data = JSONField() + added_date = models.DateTimeField(auto_now_add=True) + updated_date = models.DateTimeField(auto_now=True) + diff --git a/fedireads/openlibrary.py b/fedireads/openlibrary.py index 3953da2d4..66fc20bbf 100644 --- a/fedireads/openlibrary.py +++ b/fedireads/openlibrary.py @@ -2,7 +2,7 @@ from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest from django.core.exceptions import ObjectDoesNotExist from django.core import serializers -from fedireads.models import Book, Work +from fedireads.models import Author, Book, Work import requests openlibrary_url = 'https://openlibrary.org' @@ -22,15 +22,28 @@ def get_book(request, olkey): for work_id in data['works']: work_id = work_id['key'] book.works.add(get_or_create_work(work_id)) + for author_id in data['authors']: + author_id = author_id['key'] + book.authors.add(get_or_create_author(author_id)) return HttpResponse(serializers.serialize('json', [book])) def get_or_create_work(olkey): try: work = Work.objects.get(openlibary_key=olkey) except ObjectDoesNotExist: - response = requests.get(openlibrary_url + '/work/' + olkey +'.json') + response = requests.get(openlibrary_url + olkey + '.json') data = response.json() work = Work(openlibary_key=olkey, data=data) work.save() return work +def get_or_create_author(olkey): + try: + author = Author.objects.get(openlibary_key=olkey) + except ObjectDoesNotExist: + response = requests.get(openlibrary_url + olkey + '.json') + data = response.json() + author = Author(openlibary_key=olkey, data=data) + author.save() + return author + diff --git a/fedireads/settings.py b/fedireads/settings.py index 4ba4a2ba1..c08cb7c2d 100644 --- a/fedireads/settings.py +++ b/fedireads/settings.py @@ -55,7 +55,7 @@ ROOT_URLCONF = 'fedireads.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], + 'DIRS': ['templates'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -68,6 +68,7 @@ TEMPLATES = [ }, ] + WSGI_APPLICATION = 'fedireads.wsgi.application' @@ -85,6 +86,7 @@ DATABASES = { } } +LOGIN_URL = 'login/' AUTH_USER_MODEL = 'fedireads.User' # Password validation diff --git a/fedireads/static/format.css b/fedireads/static/format.css new file mode 100644 index 000000000..7e0ea6c51 --- /dev/null +++ b/fedireads/static/format.css @@ -0,0 +1,69 @@ +* { + margin: 0; + padding: 0; + line-height: 1.3em; + overflow: auto; +} + +body > * > * { + margin: 0 auto; + padding: 1rem; + max-width: 75rem; + min-width: 30rem; +} + +#top-bar { + height: 4rem; + border-bottom: 1px solid #aaa; + box-shadow: 0 0.5em 0.5em -0.6em #666; + margin-bottom: 1em; + overflow: auto; +} + +#branding { + font-size: 2em; +} + +header > div:first-child { + float: left; +} + +header > div:last-child { + float: right; +} + +#sidebar { + width: 30%; + float: left; +} + +.user-pic { + width: 2em; + height: auto; + border-radius: 50%; + vertical-align: middle; +} + +.book-preview { + overflow: auto; + margin-bottom: 1em; +} + +.book-preview img { + float: left; + margin-right: 0.5em; +} + +.update { + border: 1px solid #333; + border-radius: 0.2rem; + margin-bottom: 1em; +} + +.update > * { + padding: 1em; +} + +.interact { + background-color: #eee; +} diff --git a/fedireads/static/images/med.jpg b/fedireads/static/images/med.jpg new file mode 100644 index 000000000..c275cd1c8 Binary files /dev/null and b/fedireads/static/images/med.jpg differ diff --git a/fedireads/static/images/profile.jpg b/fedireads/static/images/profile.jpg new file mode 100644 index 000000000..f150ceabe Binary files /dev/null and b/fedireads/static/images/profile.jpg differ diff --git a/fedireads/static/images/small.jpg b/fedireads/static/images/small.jpg new file mode 100644 index 000000000..158163b61 Binary files /dev/null and b/fedireads/static/images/small.jpg differ diff --git a/fedireads/templates/feed.html b/fedireads/templates/feed.html new file mode 100644 index 000000000..1d0c7f458 --- /dev/null +++ b/fedireads/templates/feed.html @@ -0,0 +1,51 @@ +{% extends 'layout.html' %} +{% block content %} + + +
+
+
+ + Mouse is currently reading +
+
+ +

Moby Dick

+

by Herman Melville

+

"Command the murderous chalices! Drink ye harpooners! Drink and swear, ye men that man the deathful whaleboat's bow -- Death to Moby Dick!" So Captain Ahab binds his crew to fulfil his obsession -- the destruction of the great white whale. Under his lordly but maniacal command the Pequod's commercial mission is perverted to one of vengeance...

+
+
+ ⭐️ Like + 💬 +
+
+
+ + Mouse is currently reading +
+ +

Moby Dick

+

by Herman Melville

+

"Command the murderous chalices! Drink ye harpooners! Drink and swear, ye men that man the deathful whaleboat's bow -- Death to Moby Dick!" So Captain Ahab binds his crew to fulfil his obsession -- the destruction of the great white whale. Under his lordly but maniacal command the Pequod's commercial mission is perverted to one of vengeance...

+
+
+ ⭐️ Like + 💬 +
+
+
+{% endblock %} diff --git a/fedireads/templates/layout.html b/fedireads/templates/layout.html new file mode 100644 index 000000000..c1329102a --- /dev/null +++ b/fedireads/templates/layout.html @@ -0,0 +1,60 @@ + + + + FediReads + + + + + + + + + + + + + + + + + +
+
+
📚FediReads
+
+
+ {% if user.is_authenticated %} +
+ Welcome, {{ user.username }} + +
+ {% else %} +
+ + + +
+ {% endif %} +
+ +
+
+
+ +
+
+ {% block content %} + {% endblock %} +
+ +
+ + + + diff --git a/fedireads/templates/login.html b/fedireads/templates/login.html new file mode 100644 index 000000000..fe6a319d9 --- /dev/null +++ b/fedireads/templates/login.html @@ -0,0 +1,8 @@ +{% extends 'layout.html' %} +{% block content %} +
+ + + +
+{% endblock %} diff --git a/fedireads/urls.py b/fedireads/urls.py index e4a94ebda..9c9644823 100644 --- a/fedireads/urls.py +++ b/fedireads/urls.py @@ -19,6 +19,9 @@ from fedireads import activitystream, openlibrary, views urlpatterns = [ path('admin/', admin.site.urls), + path('', views.home), + path('login/', views.user_login), + path('logout/', views.user_logout), path('api/book/', openlibrary.get_book), path('webfinger/', activitystream.webfinger), ] diff --git a/fedireads/views.py b/fedireads/views.py index 1fbaa8073..7b2e360c2 100644 --- a/fedireads/views.py +++ b/fedireads/views.py @@ -1,11 +1,39 @@ +''' application views/pages ''' from django.contrib.auth.decorators import login_required +from django.contrib.auth import authenticate, login, logout +from django.shortcuts import redirect +from django.template.response import TemplateResponse +from django.views.decorators.csrf import csrf_exempt +from fedireads.models import Shelf @login_required -def account_page(request): - return 'hi' +def home(request): + ''' user feed ''' + shelves = Shelf.objects.filter(user=request.user.id) + data = { + 'user': request.user, + 'shelves': shelves, + } + return TemplateResponse(request, 'feed.html', data) -def webfinger(request): - return 'hello' +@csrf_exempt +def user_login(request): + ''' authentication ''' + # send user to the login page + if request.method == 'GET': + return TemplateResponse(request, 'login.html') -def api(request): - return 'hey' + # authenticate user + username = request.POST['username'] + password = request.POST['password'] + user = authenticate(request, username=username, password=password) + if user is not None: + login(request, user) + return redirect(request.GET.get('next', '/')) + return TemplateResponse(request, 'login.html') + +@csrf_exempt +@login_required +def user_logout(request): + logout(request) + return redirect('/')