diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py
index eafbe4071..0e3ac9c1f 100644
--- a/bookwyrm/forms.py
+++ b/bookwyrm/forms.py
@@ -188,3 +188,8 @@ class ShelfForm(CustomForm):
class Meta:
model = models.Shelf
fields = ['user', 'name', 'privacy']
+
+class GoalForm(CustomForm):
+ class Meta:
+ model = models.AnnualGoal
+ fields = ['user', 'year', 'goal', 'privacy']
diff --git a/bookwyrm/migrations/0036_annualgoal.py b/bookwyrm/migrations/0036_annualgoal.py
new file mode 100644
index 000000000..fb12833ea
--- /dev/null
+++ b/bookwyrm/migrations/0036_annualgoal.py
@@ -0,0 +1,32 @@
+# Generated by Django 3.0.7 on 2021-01-16 18:43
+
+import bookwyrm.models.fields
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('bookwyrm', '0035_edition_edition_rank'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='AnnualGoal',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_date', models.DateTimeField(auto_now_add=True)),
+ ('updated_date', models.DateTimeField(auto_now=True)),
+ ('remote_id', bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id])),
+ ('goal', models.IntegerField()),
+ ('year', models.IntegerField(default=2021)),
+ ('privacy', models.CharField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255)),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'unique_together': {('user', 'year')},
+ },
+ ),
+ ]
diff --git a/bookwyrm/models/__init__.py b/bookwyrm/models/__init__.py
index 48852cfe4..e71a150ba 100644
--- a/bookwyrm/models/__init__.py
+++ b/bookwyrm/models/__init__.py
@@ -17,7 +17,7 @@ from .readthrough import ReadThrough
from .tag import Tag, UserTag
-from .user import User, KeyPair
+from .user import User, KeyPair, AnnualGoal
from .relationship import UserFollows, UserFollowRequest, UserBlocks
from .federated_server import FederatedServer
diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py
index ef68f9928..6697b1b8e 100644
--- a/bookwyrm/models/user.py
+++ b/bookwyrm/models/user.py
@@ -6,6 +6,7 @@ from django.apps import apps
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.dispatch import receiver
+from django.utils import timezone
from bookwyrm import activitypub
from bookwyrm.connectors import get_data
@@ -18,7 +19,7 @@ from bookwyrm.utils import regex
from .base_model import OrderedCollectionPageMixin
from .base_model import ActivitypubMixin, BookWyrmModel
from .federated_server import FederatedServer
-from . import fields
+from . import fields, Review
class User(OrderedCollectionPageMixin, AbstractUser):
@@ -221,6 +222,57 @@ class KeyPair(ActivitypubMixin, BookWyrmModel):
return activity_object
+class AnnualGoal(BookWyrmModel):
+ ''' set a goal for how many books you read in a year '''
+ user = models.ForeignKey('User', on_delete=models.PROTECT)
+ goal = models.IntegerField()
+ year = models.IntegerField(default=timezone.now().year)
+ privacy = models.CharField(
+ max_length=255,
+ default='public',
+ choices=fields.PrivacyLevels.choices
+ )
+
+ class Meta:
+ ''' unqiueness constraint '''
+ unique_together = ('user', 'year')
+
+ def get_remote_id(self):
+ ''' put the year in the path '''
+ return '%s/goal/%d' % (self.user.remote_id, self.year)
+
+ @property
+ def books(self):
+ ''' the books you've read this year '''
+ return self.user.readthrough_set.filter(
+ finish_date__year__gte=self.year
+ ).order_by('finish_date').all()
+
+
+ @property
+ def ratings(self):
+ ''' ratings for books read this year '''
+ book_ids = [r.book.id for r in self.books]
+ reviews = Review.objects.filter(
+ user=self.user,
+ book__in=book_ids,
+ )
+ return {r.book.id: r.rating for r in reviews}
+
+
+ @property
+ def progress_percent(self):
+ return int(float(self.book_count / self.goal) * 100)
+
+
+ @property
+ def book_count(self):
+ ''' how many books you've read this year '''
+ return self.user.readthrough_set.filter(
+ finish_date__year__gte=self.year).count()
+
+
+
@receiver(models.signals.post_save, sender=User)
#pylint: disable=unused-argument
def execute_after_save(sender, instance, created, *args, **kwargs):
diff --git a/bookwyrm/static/css/fonts/icomoon.eot b/bookwyrm/static/css/fonts/icomoon.eot
index 30ae2cd57..48bd3f629 100644
Binary files a/bookwyrm/static/css/fonts/icomoon.eot and b/bookwyrm/static/css/fonts/icomoon.eot differ
diff --git a/bookwyrm/static/css/fonts/icomoon.svg b/bookwyrm/static/css/fonts/icomoon.svg
index aa0a9e5d4..00ee337f0 100644
--- a/bookwyrm/static/css/fonts/icomoon.svg
+++ b/bookwyrm/static/css/fonts/icomoon.svg
@@ -7,31 +7,35 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bookwyrm/static/css/fonts/icomoon.ttf b/bookwyrm/static/css/fonts/icomoon.ttf
index 40d6e8862..6abaa5913 100644
Binary files a/bookwyrm/static/css/fonts/icomoon.ttf and b/bookwyrm/static/css/fonts/icomoon.ttf differ
diff --git a/bookwyrm/static/css/fonts/icomoon.woff b/bookwyrm/static/css/fonts/icomoon.woff
index 6cfa9a4da..2b8d33301 100644
Binary files a/bookwyrm/static/css/fonts/icomoon.woff and b/bookwyrm/static/css/fonts/icomoon.woff differ
diff --git a/bookwyrm/static/css/format.css b/bookwyrm/static/css/format.css
index cec44f4ae..e99513b04 100644
--- a/bookwyrm/static/css/format.css
+++ b/bookwyrm/static/css/format.css
@@ -148,11 +148,11 @@ input.toggle-control:checked ~ .modal.toggle-content {
position: absolute;
}
.quote blockquote:before {
- content: "\e905";
+ content: "\e906";
top: 0;
left: 0;
}
.quote blockquote:after {
- content: "\e904";
+ content: "\e905";
right: 0;
}
diff --git a/bookwyrm/static/css/icons.css b/bookwyrm/static/css/icons.css
index 536db5600..8f1f4e903 100644
--- a/bookwyrm/static/css/icons.css
+++ b/bookwyrm/static/css/icons.css
@@ -1,10 +1,10 @@
@font-face {
font-family: 'icomoon';
- src: url('fonts/icomoon.eot?rd4abb');
- src: url('fonts/icomoon.eot?rd4abb#iefix') format('embedded-opentype'),
- url('fonts/icomoon.ttf?rd4abb') format('truetype'),
- url('fonts/icomoon.woff?rd4abb') format('woff'),
- url('fonts/icomoon.svg?rd4abb#icomoon') format('svg');
+ src: url('fonts/icomoon.eot?uh765c');
+ src: url('fonts/icomoon.eot?uh765c#iefix') format('embedded-opentype'),
+ url('fonts/icomoon.ttf?uh765c') format('truetype'),
+ url('fonts/icomoon.woff?uh765c') format('woff'),
+ url('fonts/icomoon.svg?uh765c#icomoon') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
@@ -25,81 +25,102 @@
-moz-osx-font-smoothing: grayscale;
}
-.icon-dots-three-vertical:before {
- content: "\e918";
+.icon-sparkle:before {
+ content: "\e91a";
}
-.icon-check:before {
- content: "\e917";
+.icon-warning:before {
+ content: "\e91b";
}
-.icon-dots-three:before {
- content: "\e916";
-}
-.icon-envelope:before {
+.icon-book:before {
content: "\e900";
}
-.icon-arrow-right:before {
+.icon-bookmark:before {
+ content: "\e91c";
+}
+.icon-envelope:before {
content: "\e901";
}
-.icon-bell:before {
+.icon-arrow-right:before {
content: "\e902";
}
-.icon-x:before {
+.icon-bell:before {
content: "\e903";
}
-.icon-quote-close:before {
+.icon-x:before {
content: "\e904";
}
-.icon-quote-open:before {
+.icon-quote-close:before {
content: "\e905";
}
-.icon-image:before {
+.icon-quote-open:before {
content: "\e906";
}
-.icon-pencil:before {
+.icon-image:before {
content: "\e907";
}
-.icon-list:before {
+.icon-pencil:before {
content: "\e908";
}
-.icon-unlock:before {
+.icon-list:before {
content: "\e909";
}
-.icon-globe:before {
+.icon-unlock:before {
content: "\e90a";
}
-.icon-lock:before {
+.icon-unlisted:before {
+ content: "\e90a";
+}
+.icon-globe:before {
content: "\e90b";
}
-.icon-chain-broken:before {
+.icon-public:before {
+ content: "\e90b";
+}
+.icon-lock:before {
content: "\e90c";
}
-.icon-chain:before {
+.icon-followers:before {
+ content: "\e90c";
+}
+.icon-chain-broken:before {
content: "\e90d";
}
-.icon-comments:before {
+.icon-chain:before {
content: "\e90e";
}
-.icon-comment:before {
+.icon-comments:before {
content: "\e90f";
}
-.icon-boost:before {
+.icon-comment:before {
content: "\e910";
}
-.icon-arrow-left:before {
+.icon-boost:before {
content: "\e911";
}
-.icon-arrow-up:before {
+.icon-arrow-left:before {
content: "\e912";
}
-.icon-arrow-down:before {
+.icon-arrow-up:before {
content: "\e913";
}
-.icon-home:before {
+.icon-arrow-down:before {
content: "\e914";
}
-.icon-local:before {
+.icon-home:before {
content: "\e915";
}
+.icon-local:before {
+ content: "\e916";
+}
+.icon-dots-three:before {
+ content: "\e917";
+}
+.icon-check:before {
+ content: "\e918";
+}
+.icon-dots-three-vertical:before {
+ content: "\e919";
+}
.icon-search:before {
content: "\e986";
}
diff --git a/bookwyrm/static/js/shared.js b/bookwyrm/static/js/shared.js
index de6d44f99..b2de57368 100644
--- a/bookwyrm/static/js/shared.js
+++ b/bookwyrm/static/js/shared.js
@@ -21,11 +21,38 @@ window.onload = function() {
// handle aria settings on menus
Array.from(document.getElementsByClassName('pulldown-menu'))
.forEach(t => t.onclick = toggleMenu);
+
+ // display based on localstorage vars
+ document.querySelectorAll('[data-hide]')
+ .forEach(t => setDisplay(t));
+
+ // update localstorage
+ Array.from(document.getElementsByClassName('set-display'))
+ .forEach(t => t.onclick = updateDisplay);
};
+function updateDisplay(e) {
+ var key = e.target.getAttribute('data-id');
+ var value = e.target.getAttribute('data-value');
+ window.localStorage.setItem(key, value);
+
+ document.querySelectorAll('[data-hide="' + key + '"]')
+ .forEach(t => setDisplay(t));
+}
+
+function setDisplay(el) {
+ var key = el.getAttribute('data-hide');
+ var value = window.localStorage.getItem(key)
+ if (!value) {
+ el.className = el.className.replace('hidden', '');
+ } else if (value != null && !!value) {
+ el.className += ' hidden';
+ }
+}
+
function toggleAction(e) {
// set hover, if appropriate
- var hover = e.target.getAttribute('data-hover-target')
+ var hover = e.target.getAttribute('data-hover-target');
if (hover) {
document.getElementById(hover).focus();
}
diff --git a/bookwyrm/templates/feed.html b/bookwyrm/templates/feed.html
index b77da819b..79dd4b85e 100644
--- a/bookwyrm/templates/feed.html
+++ b/bookwyrm/templates/feed.html
@@ -47,8 +47,8 @@
{% endif %}
+
+ {% if goal %}
+
+
+
{{ goal.year }} Reading Goal
+ {% include 'snippets/goal_progress.html' with goal=goal %}
+
+
+ {% endif %}
@@ -85,6 +94,33 @@
+ {# announcements and system messages #}
+ {% if not goal and tab == 'home' %}
+ {% now 'Y' as year %}
+
+
+
+
+ Set a goal for how many books you'll finish reading in {{ year }}, and track your progress throughout the year.
+
+ {% include 'snippets/goal_form.html' %}
+
+
+
+
+
+ {% endif %}
+
+ {# activity feed #}
{% if not activities %}
There aren't any activities right now! Try following a user to get started
{% endif %}
diff --git a/bookwyrm/templates/goal.html b/bookwyrm/templates/goal.html
new file mode 100644
index 000000000..4f477a216
--- /dev/null
+++ b/bookwyrm/templates/goal.html
@@ -0,0 +1,60 @@
+{% extends 'layout.html' %}
+{% block content %}
+
+
+ {{ year }} Reading Progress
+ {% if user == request.user %}
+
+ {% if goal %}
+
+
+ {% include 'snippets/toggle/toggle_button.html' with text="Edit goal" controls_text="show-edit-goal" %}
+
+ {% endif %}
+
+
+
+
+ {% now 'Y' as year %}
+
+
+
+ Set a goal for how many books you'll finish reading in {{ year }}, and track your progress throughout the year.
+
+ {% include 'snippets/goal_form.html' with goal=goal year=year %}
+
+
+
+
+ {% endif %}
+
+ {% if not goal and user != request.user %}
+ {{ user.display_name }} hasn't set a reading goal for {{ year }}.
+ {% endif %}
+
+ {% if goal %}
+ {% include 'snippets/goal_progress.html' with goal=goal %}
+ {% endif %}
+
+
+{% if goal.books %}
+
+ {% if goal.user == request.user %}Your{% else %}{{ goal.user.display_name }}'s{% endif %} {{ year }} Books
+
+ {% for book in goal.books %}
+
+ {% endfor %}
+
+
+{% endif %}
+{% endblock %}
diff --git a/bookwyrm/templates/snippets/discover/small-book.html b/bookwyrm/templates/snippets/discover/small-book.html
index 76fd2db78..be399df66 100644
--- a/bookwyrm/templates/snippets/discover/small-book.html
+++ b/bookwyrm/templates/snippets/discover/small-book.html
@@ -1,7 +1,9 @@
{% load bookwyrm_tags %}
{% if book %}
{% include 'snippets/book_cover.html' with book=book %}
+{% if ratings %}
{% include 'snippets/stars.html' with rating=ratings|dict_key:book.id %}
+{% endif %}
{% if book.authors %}
diff --git a/bookwyrm/templates/snippets/finish_reading_modal.html b/bookwyrm/templates/snippets/finish_reading_modal.html
index 06874c069..79bcd9449 100644
--- a/bookwyrm/templates/snippets/finish_reading_modal.html
+++ b/bookwyrm/templates/snippets/finish_reading_modal.html
@@ -29,8 +29,8 @@