Redesign (front page, login page)

This commit is contained in:
Mouse Reeve 2020-03-15 14:15:36 -07:00
parent 67e7eaaf85
commit 3efc8d45c3
30 changed files with 769 additions and 249 deletions

View file

@ -28,15 +28,15 @@ class RegisterForm(ModelForm):
class ReviewForm(ModelForm): class ReviewForm(ModelForm):
class Meta: class Meta:
model = models.Review model = models.Review
fields = ['name', 'content', 'rating'] fields = ['name', 'rating', 'content']
help_texts = {f: None for f in fields} help_texts = {f: None for f in fields}
review_content = IntegerField(validators=[ content = IntegerField(validators=[
MinValueValidator(0), MaxValueValidator(5) MinValueValidator(0), MaxValueValidator(5)
]) ])
labels = { labels = {
'name': 'Title', 'name': 'Title',
'review_content': 'Review',
'rating': 'Rating (out of 5)', 'rating': 'Rating (out of 5)',
'content': 'Review',
} }

View file

@ -46,7 +46,6 @@ class Status(FedireadsModel):
return '%s/%s/%d' % (base_path, model_name, self.id) return '%s/%s/%d' % (base_path, model_name, self.id)
class Review(Status): class Review(Status):
''' a book review ''' ''' a book review '''
name = models.CharField(max_length=255) name = models.CharField(max_length=255)

View file

@ -111,12 +111,14 @@ def handle_outgoing_accept(user, to_follow, follow_request):
broadcast(to_follow, activity, recipient) broadcast(to_follow, activity, recipient)
def handle_outgoing_reject(user, to_follow, relationship): def handle_outgoing_reject(user, to_follow, relationship):
''' a local user who managed follows rejects a follow request '''
relationship.delete() relationship.delete()
activity = activitypub.get_reject(to_follow, relationship) activity = activitypub.get_reject(to_follow, relationship)
recipient = get_recipients(to_follow, 'direct', direct_recipients=[user]) recipient = get_recipients(to_follow, 'direct', direct_recipients=[user])
broadcast(to_follow, activity, recipient) broadcast(to_follow, activity, recipient)
def handle_shelve(user, book, shelf): def handle_shelve(user, book, shelf):
''' a local user is getting a book put on their shelf ''' ''' a local user is getting a book put on their shelf '''
# update the database # update the database
@ -132,9 +134,10 @@ def handle_shelve(user, book, shelf):
'reading': 'started reading', 'reading': 'started reading',
'read': 'finished reading' 'read': 'finished reading'
}[shelf.identifier] }[shelf.identifier]
name = user.name if user.name else user.localname message = '%s %s' % (verb, book.title)
message = '%s %s %s' % (name, verb, book.title)
status = create_status(user, message, mention_books=[book]) status = create_status(user, message, mention_books=[book])
status.status_type = 'Update'
status.save()
activity = activitypub.get_status(status) activity = activitypub.get_status(status)
create_activity = activitypub.get_create(user, activity) create_activity = activitypub.get_create(user, activity)

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Binary file not shown.

View file

@ -10,13 +10,21 @@ html {
background-color: #FFF; background-color: #FFF;
color: black; color: black;
} }
body {
a { padding-top: 90px;
color: #00F;
} }
a:visited {
color: #808; a {
color: #247BA0;
}
input, button {
padding: 0.2em 0.5em;
}
button {
cursor: pointer;
width: max-content;
} }
h1, h2, h3, h4 { h1, h2, h3, h4 {
@ -29,16 +37,21 @@ h1 {
h2 { h2 {
font-size: 1rem; font-size: 1rem;
background-color: #B2DBBF;
padding: 0.5rem 0.2rem; padding: 0.5rem 0.2rem;
margin-bottom: 1rem; margin-bottom: 1rem;
height: 1rem;
} }
#top-bar { #top-bar {
background-color: #70C1B3; overflow: visible;
overflow: hidden;
padding: 0.5rem; padding: 0.5rem;
border-bottom: 3px solid #247BA0;
margin-bottom: 1em;
width: 100%;
background-color: #FFF;
position: fixed;
top: 0;
height: 47px;
z-index: 1;
} }
#warning { #warning {
@ -46,62 +59,183 @@ h2 {
text-align: center; text-align: center;
} }
#branding, #actions {
margin: 0 1rem;
}
#branding {
flex-grow: 1;
font-size: 2rem;
}
#branding a { #branding a {
text-decoration: none; text-decoration: none;
color: black;
} }
#actions { #actions {
flex-grow: 0; margin-top: 1em;
text-align: right;
} }
#main, header > div { #actions > * {
display: inline-block;
}
#notifications .icon {
font-size: 1.1rem;
}
#notifications a {
color: black;
text-decoration: none;
position: relative;
top: 0.2rem;
}
#notifications .count {
background-color: #FF1654;
color: white;
font-size: 0.85rem;
border-radius: 50%;
display: block;
position: absolute;
text-align: center;
top: -0.65rem;
right: -0.5rem;
height: 1rem;
width: 1rem;
}
button .icon {
font-size: 1.1rem;
vertical-align: sub;
}
#search button {
border: none;
}
#main, header {
margin: 0 auto; margin: 0 auto;
max-width: 55rem;
padding-right: 1em;
}
header {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
max-width: 75rem;
width: 100%;
}
@media (max-width: 600px) {
#main {
flex-direction: column-reverse;
}
} }
#feed, #content, #sidebar { ul.menu {
list-style: none;
text-align: center;
margin-top: 1.5rem;
flex-grow: 1;
font-size: 0.9em;
}
ul.menu li {
display: inline-block;
background-color: white;
padding: 0 0.5em;
text-transform: uppercase;
}
ul.menu a {
color: #555;
text-decoration: none;
font-size: 0.9em;
}
.pulldown-container {
position: relative;
}
.pulldown {
display: none;
position: absolute;
list-style: none;
background: white;
padding: 1em;
text-align: right;
right: 0;
box-shadow: 0 5px 10px rgba(0,0,0,0.15);
width: max-content;
}
.pulldown-container:hover .pulldown {
display: block;
}
#feed {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin: 0 1rem 1rem 0; padding-top: 70px;
position: relative;
top: -50px;
} }
#sidebar { .row {
min-width: 20rem; display: flex;
margin-right: 0; flex-direction: row;
}
.row > * {
flex-grow: 1;
width: min-content;
}
.login form {
margin-top: 1em;
}
.login form p {
display: flex;
flex-direction: row;
padding: 0.5em 0;
}
.login form label {
width: 0;
flex-grow: 1;
display: inline-block;
}
form input {
flex-grow: 1;
}
.content-container button {
border: none;
background-color: #247BA0;
color: white;
padding: 0.3em 0.8em;
font-size: 0.9em;
border-radius: 0.3em;
}
button.secondary {
background-color: #EEE;
border: 2px solid #247BA0;
color: #247BA0;
}
.login h2 {
border-bottom: 3px solid #B2DBBF;
} }
.tabs { .tabs {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
border-bottom: 3px solid #FF1654;
padding-left: 1em;
} }
.tab.active { .tabs.secondary {
border-bottom: 3px solid #247BA0;
}
.tab {
padding: 0.5em 1em;
border-radius: 0.25em 0.25em 0 0;
}
.secondary .tab {
padding: 0.25em 0.5em;
}
.tabs .tab.active {
background-color: #FF1654; background-color: #FF1654;
} }
.tabs.secondary .tab.active {
background-color: #247BA0;
}
.tab.active a {
color: black;
}
.user-pic { .user-pic {
width: 2rem; width: 2rem;
height: auto; height: 2rem;
border-radius: 50%; border-radius: 50%;
vertical-align: top; vertical-align: top;
position: relative; position: relative;
bottom: 0.5em; bottom: 0.35em;
}
.review-form label {
display: block;
} }
.time-ago { .time-ago {
@ -110,7 +244,7 @@ h2 {
} }
.book-preview { .book-preview {
overflow: auto; overflow: hidden;
} }
.book-preview img { .book-preview img {
@ -122,17 +256,89 @@ h2 {
float: left; float: left;
} }
.content-container {
margin: 1rem;
}
.content-container > * {
padding-left: 1em;
padding-right: 1em;
}
.all-shelves {
display: flex;
flex-direction: row;
overflow-y: hidden;
margin-left: 0;
}
.all-shelves > div {
flex-grow: 0;
}
.all-shelves > div:last-child {
padding-right: 0;
flex-grow: 1;
}
.all-shelves > div > * {
padding: 0;
}
.all-shelves > div:first-child > * {
padding-left: 1em;
}
.all-shelves h2 {
border-bottom: 3px solid #B2DBBF;
}
.covers-shelf {
display: flex;
flex-direction: row;
}
.covers-shelf .book-preview {
margin-right: 1em;
font-size: 0.9em;
overflow: unset;
width: min-content;
position: relative;
}
.covers-shelf .book-preview button {
display: block;
margin: 0 auto;
border: none;
}
.covers-shelf .book-preview:last-child {
margin-right: 0;
}
.covers-shelf .book-preview:hover {
cursor: pointer;
}
.covers-shelf .book-preview:hover img {
box-shadow: #F3FFBD 0em 0em 1em 1em;
}
.covers-shelf .book-preview img {
float: none;
height: 11rem;
width: auto;
margin: 0;
}
.close {
float: right;
cursor: pointer;
padding: 1rem;
}
.compose-suggestion {
display: none;
}
.compose-suggestion.visible {
display: block;
}
.book-cover.small { .book-cover.small {
width: 50px; width: 50px;
height: auto; height: auto;
} }
.compose-suggestion .book-preview {
#content > div, #feed > div, #sidebar > div { background-color: #EEE;
background-color: #EFEFEF; padding: 1em;
margin: 1rem 0 0 1rem;
}
#content > div > *, #feed > div > *, #sidebar > div > * {
padding: 1rem;
} }
.tag { .tag {
@ -150,22 +356,14 @@ h2 {
width: 30rem; width: 30rem;
height: 10rem; height: 10rem;
} }
.review {
margin-bottom: 1rem;
}
small {
display: block;
}
blockquote { blockquote {
white-space: pre-wrap; white-space: pre-wrap;
} }
.interaction { .interaction {
background-color: #F3FFBD; background-color: #B2DBBF;
clear: both; border-radius: 0 0 0.5em 0.5em;
margin-top: 1rem;
} }
.interaction textarea { .interaction textarea {
@ -201,27 +399,44 @@ th, td {
.comment-thread .reply h2 { .comment-thread .reply h2 {
background: none; background: none;
} }
.post.main {
background-color: #F3FFBD;
}
.post { .post {
background-color: #EFEFEF;
padding-top: 1em;
padding-bottom: 1em;
}
.post h2, .compose-suggestion h2 {
position: relative;
right: 2em;
}
.post .user-pic, .compose-suggestion .user-pic {
right: 0.25em;
}
.comment-thread .post {
margin-left: 4em; margin-left: 4em;
border-left: 2px solid #247BA0; border-left: 2px solid #247BA0;
} }
.post.depth-1 { .comment-thread .post.depth-1 {
margin-left: 0; margin-left: 0;
border: none; border: none;
} }
.post.depth-2 { .comment-thread .post.depth-2 {
margin-left: 1em; margin-left: 1em;
} }
.post.depth-3 { .comment-thread .post.depth-3 {
margin-left: 2em; margin-left: 2em;
} }
.post.depth-4 { .comment-thread .post.depth-4 {
margin-left: 3em; margin-left: 3em;
} }
.unread { .unread {
background-color: #F3FFBD; background-color: #DDD;
}
.hidden-text {
height: 0;
width: 0;
position: absolute;
overflow: hidden;
} }

138
fedireads/static/icons.css Normal file
View file

@ -0,0 +1,138 @@
@font-face {
font-family: 'icomoon';
src: url('fonts/icomoon.eot?v0wquk');
src: url('fonts/icomoon.eot?v0wquk#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?v0wquk') format('truetype'),
url('fonts/icomoon.woff?v0wquk') format('woff'),
url('fonts/icomoon.svg?v0wquk#icomoon') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
}
[class^="icon-"], [class*=" icon-"] {
/* use !important to prevent issues with browser extensions that change fonts */
font-family: 'icomoon' !important;
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-arrow-right:before {
content: "\e900";
}
.icon-arrow-left:before {
content: "\e910";
}
.icon-arrow-up:before {
content: "\e911";
}
.icon-arrow-down:before {
content: "\e912";
}
.icon-x:before {
content: "\e902";
}
.icon-cancel:before {
content: "\e902";
}
.icon-close:before {
content: "\e902";
}
.icon-search:before {
content: "\e986";
}
.icon-star-empty:before {
content: "\e9d7";
}
.icon-star-half:before {
content: "\e9d8";
}
.icon-star-full:before {
content: "\e9d9";
}
.icon-heart:before {
content: "\e9da";
}
.icon-local:before {
content: "\e914";
}
.icon-home:before {
content: "\e913";
}
.icon-quote-close:before {
content: "\e903";
}
.icon-quote-open:before {
content: "\e904";
}
.icon-image:before {
content: "\e905";
}
.icon-photo:before {
content: "\e905";
}
.icon-picture-o:before {
content: "\e905";
}
.icon-pencil:before {
content: "\e906";
}
.icon-list:before {
content: "\e907";
}
.icon-unlock:before {
content: "\e908";
}
.icon-unlisted:before {
content: "\e908";
}
.icon-globe:before {
content: "\e909";
}
.icon-global:before {
content: "\e909";
}
.icon-federated:before {
content: "\e909";
}
.icon-public:before {
content: "\e909";
}
.icon-lock:before {
content: "\e90a";
}
.icon-private:before {
content: "\e90a";
}
.icon-chain-broken:before {
content: "\e90b";
}
.icon-unlink:before {
content: "\e90b";
}
.icon-chain:before {
content: "\e90c";
}
.icon-link:before {
content: "\e90c";
}
.icon-comments:before {
content: "\e90d";
}
.icon-comment:before {
content: "\e90e";
}
.icon-boost:before {
content: "\e90f";
}
.icon-bell:before {
content: "\e901";
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,11 @@
function show_compose(element) {
var visible_compose_boxes = document.getElementsByClassName('visible');
for (var i = 0; i < visible_compose_boxes.length; i++) {
visible_compose_boxes[i].className = 'compose-suggestion';
}
var target_id = 'compose-' + element.id;
var target = document.getElementById(target_id);
target.className += ' visible';
}

View file

@ -0,0 +1,8 @@
function hide_element(element) {
var classes = element.parentElement.className;
element.parentElement.className = classes.replace('visible', '');
}
function favorite(element) {
}

View file

@ -5,7 +5,7 @@
<div> <div>
<h2><q>{{ book.title }}</q> and You</h2> <h2><q>{{ book.title }}</q> and You</h2>
<p>{% if shelf %}On shelf <q>{{ shelf.name }}</q>{% endif %}</p> <p>{% if shelf %}On shelf <q>{{ shelf.name }}</q>{% endif %}</p>
{% include 'snippets/shelve-button.html' with book=book pulldown=True %} {% include 'snippets/shelve_button.html' with book=book pulldown=True %}
<div id="tag-cloud"> <div id="tag-cloud">
{% for tag in user_tags %} {% for tag in user_tags %}

View file

@ -2,44 +2,52 @@
{% load fr_display %} {% load fr_display %}
{% block content %} {% block content %}
<div id="sidebar"> <div class="all-shelves content-container">
<div>
{% if shelves %}
{% for shelf in shelves %} {% for shelf in shelves %}
<h2>{{ shelf.name }}</h2> {% if shelf.books %}
{% for book in shelf.books %}
<div class="book-preview">
{% include 'snippets/book.html' with book=book size="small" %}
</div>
{% endfor %}
{% if shelf.size > shelf.books.count %}
<a href="/shelf/{{ user | username }}/{{ shelf.identifier }}">See all {{ shelf.size }}</a>
{% endif %}
{% endfor %}
{% else %}
<h2>Reading Activity</h2>
<p>Start a book!</p>
{% endif %}
</div>
<div> <div>
<h2>Recently Added Books</h2> <h2>{{ shelf.name }}
{% for book in recent_books %} {% if shelf.size > shelf.books|length %}
<div class="book-preview"> <small>(<a href="/shelf/{{ user | username }}/{{ shelf.identifier }}">See all {{ shelf.size }}</a>)</small>
{% include 'snippets/book.html' with book=book size="small" %} {% endif %}
</h2>
<div class="covers-shelf {{ shelf.identifier }}">
{% for book in shelf.books %}
<div class="book-preview" onclick="show_compose(this)" id="book-{{ book.id }}">
{% include 'snippets/book_cover.html' with book=book %}
{% include 'snippets/shelve_button.html' with book=book %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
</div>
{% endif %}
{% endfor %}
</div> </div>
{% for shelf in shelves %}
{% for book in shelf.books %}
<div class="compose-suggestion" id="compose-book-{{ book.id }}">
<span class="close icon icon-close" onclick="hide_element(this)">
<span class="hidden-text">Close</span>
</span>
<div class="content-container">
{% include 'snippets/create_status.html' with book=book user=request.user %}
</div>
</div>
{% endfor %}
{% endfor %}
<div id="feed"> <div id="feed">
<div class="content-container tabs">
{% include 'snippets/tabs.html' with tabs=feed_tabs active_tab=active_tab %} {% include 'snippets/tabs.html' with tabs=feed_tabs active_tab=active_tab %}
</div>
{% for activity in activities %} {% for activity in activities %}
{% include 'snippets/status.html' with status=activity depth=1 description=True %} <div class="content-container">
{% include 'snippets/status.html' with status=activity %}
</div>
{% endfor %} {% endfor %}
</div> </div>
<script src="/static/js/feed.js"></script>
{% endblock %} {% endblock %}

View file

@ -2,15 +2,16 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title>FediReads</title> <title>BookWyrm</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link type="text/css" rel="stylesheet" href="/static/format.css"> <link type="text/css" rel="stylesheet" href="/static/format.css">
<link type="text/css" rel="stylesheet" href="/static/icons.css">
<link rel="shortcut icon" type="image/x-icon" href="/static/images/favicon.ico"> <link rel="shortcut icon" type="image/x-icon" href="/static/images/favicon.ico">
<meta name="twitter:card" content="summary"> <meta name="twitter:card" content="summary">
<meta name="twitter:title" content="FediReads"> <meta name="twitter:title" content="BookWyrm">
<meta name="og:title" content="FediReads"> <meta name="og:title" content="BookWyrm">
<meta name="twitter:description" content="Federated Social Reading"> <meta name="twitter:description" content="Federated Social Reading">
<meta name="og:description" content="Federated Social Reading"> <meta name="og:description" content="Federated Social Reading">
<meta name="twitter:creator" content="@tripofmice"> <meta name="twitter:creator" content="@tripofmice">
@ -22,41 +23,44 @@
<div id="top-bar"> <div id="top-bar">
<header> <header>
<div> <div id="branding"><a href="/"><img id="logo" src="/static/images/logo-small.png" alt="BookWyrm"></img></a></div>
<div id="branding"><a href="/">📚FediReads</a></div>
<ul class="menu">
<li><a href="/user/{{request.user.localname}}">Your shelves</a></li>
<li><a href="/#feed">Updates</a></li>
<li><a href="/books">Discover Books</a></li>
</ul>
<div id="actions"> <div id="actions">
<div id="account">
{% if request.user.is_authenticated %}
<form name="logout" action="/logout/" method="post">
{% csrf_token %}
Welcome, {% include 'snippets/username.html' with user=request.user %}
<input type="submit" value="Log out"></input>
</form>
{% else %}
<form name="login" action="/login/" method="post">
{% csrf_token %}
{% for field in login_form %}
{{ field }}
{% endfor %}
<input type="submit" value="Log in"></input>
</form>
{% endif %}
</div>
<div id="search"> <div id="search">
<form action="/search/"> <form action="/search/">
<input type="text" name="q" placeholder="search"></input> <input type="text" name="q" placeholder="Search for a book or user"></input>
<input type="submit" value="🔍"></input> <button type="submit">
<span class="icon icon-search">
<span class="hidden-text">search</span>
</span>
</button>
</form> </form>
</div> </div>
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<div id="notification"> <div id="notifications">
<a href="/notifications"> <a href="/notifications">
🔔 ({{ request.user | notification_count }}) <span class="icon icon-bell">
<span class="hidden-text">Notitications</span>
</span>
{% if request.user|notification_count %}<span class="count">{{ request.user | notification_count }}</span>{% endif %}
</a> </a>
</div> </div>
<div class="pulldown-container">
{% include 'snippets/avatar.html' with user=request.user %}
<ul class="pulldown">
<li><a href="/user/{{ request.user }}">Your profile</a></li>
<li><a href="/user-edit/">Settings</a></li>
<li><a href="/logout/">Log out</a></li>
</ul>
</p>
{% endif %} {% endif %}
</div> </div>
</div>
</header> </header>
</div> </div>
@ -65,6 +69,10 @@
{% endblock %} {% endblock %}
</div> </div>
<script>
var csrf_token = {{ csrf_token }};
</script>
<script src="/static/js/shared.js"></script>
</body> </body>
</html> </html>

View file

@ -1,14 +1,34 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% block content %} {% block content %}
<div id="content"> <div class="row">
<div class="content-container login">
<h2>Create an Account</h2>
<p><small>
With a BookWyrm account, you can track and share your reading activity with
friends here and on any other federated server, like Mastodon and PixelFed.
</small></p>
<div> <div>
<form name="login" method="post"> <form name="register" method="post" action="/register">
{% csrf_token %}
{{ register_form.as_p }}
<button type="submit">Create account</button>
</form>
</div>
</div>
<div class="content-container login">
<h2>Log in</h2>
<div>
<form name="login" method="post" action="/user-login">
{% csrf_token %} {% csrf_token %}
{{ login_form.as_p }} {{ login_form.as_p }}
<button type="submit">Log in</button> <button type="submit">Log in</button>
</form> </form>
<a href="/register/">Create a new account</a> <p><small><a href="/reset-password">Forgot your password?</a></small></p>
</div> </div>
</div>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -1,14 +0,0 @@
{% extends 'layout.html' %}
{% block content %}
<div id="content">
<div>
<form name="register" method="post">
{% csrf_token %}
{{ register_form.as_p }}
<button type="submit">Create account</button>
</form>
<a href="/login/">Log in with existing account</a>
</div>
</div>
{% endblock %}

View file

@ -1,10 +0,0 @@
{% load humanize %}
<h2>
{% include 'snippets/avatar.html' with user=activity.user %}
{% include 'snippets/username.html' with user=activity.user %}
{{ content | safe }}
<span class="time-ago">
<a href="{{ activity.absolute_id }}">{{ activity.published_date | naturaltime }}</a>
</span>
</h2>

View file

@ -15,4 +15,4 @@
<blockquote>{{ book.description | description }}</blockquote> <blockquote>{{ book.description | description }}</blockquote>
{% endif %} {% endif %}
{% include 'snippets/shelve-button.html' with book=book pulldown=shelf_pulldown %} {% include 'snippets/shelve_button.html' with book=book pulldown=shelf_pulldown %}

View file

@ -0,0 +1,6 @@
{% load fr_display %}
{% for book in books %}
<div class="book-preview">
{% include 'snippets/book.html' with rating=rating %}
</div>
{% endfor %}

View file

@ -0,0 +1,32 @@
{% load humanize %}
{% load fr_display %}
<h2>
{% include 'snippets/avatar.html' with user=user %}
Your thoughts on
<a href="/book/{{ book.openlibrary_key }}">{{ book.title }}</a>
by {% include 'snippets/authors.html' with book=book %}
</h2>
<div class="tabs secondary">
<div class="tab active">
Review
</div>
<div class="tab">
Comment
</div>
<div class="tab">
Quote
</div>
</div>
<div class="book-preview">
{% include 'snippets/book_cover.html' with book=book %}
<form class="review-form" name="review" action="/review/" method="post">
{% csrf_token %}
{# TODO: this shouldn't use the openlibrary key #}
<input type="hidden" name="book" value="{{ book.openlibrary_key }}"></input>
{{ review_form.as_p }}
<button type="submit">Post review</button>
</form>
</div>

View file

@ -5,7 +5,7 @@
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}"></input> <input type="hidden" name="book" value="{{ book.id }}"></input>
<input type="hidden" name="shelf" value="{% shelve_button_identifier book %}"></input> <input type="hidden" name="shelf" value="{% shelve_button_identifier book %}"></input>
<button type="submit">{% shelve_button_text book %}</button> <button class="secondary" type="submit">{% shelve_button_text book %}</button>
</form> </form>
{% else %} {% else %}
@ -21,7 +21,7 @@
</option> </option>
{% endfor %} {% endfor %}
</select> </select>
<button type="submit">Shelve</button> <button class="secondary" type="submit">Shelve</button>
</form> </form>
{% endif %} {% endif %}

View file

@ -1,21 +1,48 @@
{% load humanize %}
{% load fr_display %} {% load fr_display %}
<div class="post {{ status.status_type|lower }} depth-{{ depth }} {% if main %}main{% else %}reply{% endif %}"> <div class="post {{ status.status_type|lower }} depth-{{ depth }} {% if main %}main{% else %}reply{% endif %}">
{% include 'snippets/activity_banner.html' with activity=status %} <h2>
{% include 'snippets/avatar.html' with user=status.user %}
{% include 'snippets/username.html' with user=status.user %}
{% if status.status_type == 'Update' %}
{{ status.content | safe }}
{% elif status.status_type == 'Review' %}
reviewed {{ status.book.title }}
{% elif status.reply_parent %}
{% with parent_status=status|parent %}
replied to {% include 'snippets/username.html' with user=parent_status.user possessive=True %} <a href="{{parent_status.absolute_id }}">{{ parent_status.status_type|lower }}</a>
{% endwith %}
{% endif %}
<span class="time-ago">
<a href="{{ status.absolute_id }}">{{ status.published_date | naturaltime }}</a>
</span>
</h2>
{% if not hide_book and status.mention_books.count %} {% if not hide_book and status.mention_books.count %}
<div class="book-preview"> <div class="book-preview">
{% if status.status_type == 'Review' %}
{% include 'snippets/book.html' with book=status.mention_books.first %} {% include 'snippets/book.html' with book=status.mention_books.first %}
{% else %}
{% include 'snippets/book.html' with book=status.mention_books.first description=True %}
{% endif %}
</div> </div>
{% endif %} {% endif %}
{% if not hide_book and status.book%} {% if not hide_book and status.book%}
<div class="book-preview"> <div class="book-preview">
{% if status.status_type == 'Review' %}
{% include 'snippets/book.html' with book=status.book %} {% include 'snippets/book.html' with book=status.book %}
{% else %}
{% include 'snippets/book.html' with book=status.book description=True %}
{% endif %}
</div> </div>
{% endif %} {% endif %}
{% if status.status_type == 'Review' %}<h4>{{ status.name }} {% if status.status_type == 'Review' %}<h4>{{ status.name }}
<small>{{ status.rating | stars }} stars, by {% include 'snippets/username.html' with user=status.user %}</small> <small>{{ status.rating | stars }} stars, by {% include 'snippets/username.html' with user=status.user %}</small>
</h4>{% endif %} </h4>{% endif %}
{% if status.status_type != 'Update' %}
<blockquote>{{ status.content | safe }}</blockquote> <blockquote>{{ status.content | safe }}</blockquote>
{% endif %}
{% if not max_depth and status.reply_parent or status|replies %}<p><a href="{{ status.absolute_id }}">Thread</a>{% endif %} {% if not max_depth and status.reply_parent or status|replies %}<p><a href="{{ status.absolute_id }}">Thread</a>{% endif %}
{% include 'snippets/interaction.html' with activity=status %}
</div> </div>
{% include 'snippets/interaction.html' with activity=status %}

View file

@ -1,8 +1,6 @@
<div class="tabs"> {% for tab in tabs %}
{% for tab in tabs %} <div class="tab {% if tab.id == active_tab %}active{% endif %}">
<div class="tab {% if tab == active_tab %}active{% endif %}"> <a href="{{ path }}/{{ tab.id }}">{{ tab.display }}</a>
<a href="{{ path }}/{{ tab }}">{{ tab }}</a>
</div>
{% endfor %}
</div> </div>
{% endfor %}

View file

@ -1,2 +1,2 @@
{% load fr_display %} {% load fr_display %}
<a href="/user/{{ user | username }}" class="user">{% if user.name %}{{ user.name }}{% else %}{{ user | username }}{% endif %}</a> {% if show_full and user.name or show_full and user.localname %} ({{ user.username }}){% endif %} <a href="/user/{{ user | username }}" class="user">{% if user.name %}{{ user.name }}{% else %}{{ user | username }}{% endif %}</a>{% if possessive %}'s{% endif %}{% if show_full and user.name or show_full and user.localname %} ({{ user.username }}){% endif %}

View file

@ -16,7 +16,7 @@
{% if is_self %} {% if is_self %}
<div class="interaction"> <div class="interaction">
<a href="/edit_profile_page/">Edit profile</a> <a href="/user-edit/">Edit profile</a>
</div> </div>
{% endif %} {% endif %}
</div> </div>

View file

@ -6,7 +6,7 @@ from django.urls import path, re_path
from fedireads import incoming, outgoing, views, settings, wellknown from fedireads import incoming, outgoing, views, settings, wellknown
from fedireads import view_actions as actions from fedireads import view_actions as actions
username_regex = r'(?P<username>[\w@\-_]+)' username_regex = r'(?P<username>[\w@\-_\.]+)'
localname_regex = r'(?P<username>[\w\-_]+)' localname_regex = r'(?P<username>[\w\-_]+)'
user_path = r'^user/%s' % username_regex user_path = r'^user/%s' % username_regex
local_user_path = r'^user/%s' % localname_regex local_user_path = r'^user/%s' % localname_regex
@ -28,9 +28,7 @@ urlpatterns = [
# TODO: re_path(r'^.well-known/host-meta/?$', incoming.host_meta), # TODO: re_path(r'^.well-known/host-meta/?$', incoming.host_meta),
# ui views # ui views
re_path(r'^register/?$', views.register), re_path(r'^login/?$', views.login_page),
re_path(r'^login/?$', views.user_login),
re_path(r'^logout/?$', views.user_logout),
# should return a ui view or activitypub json blob as requested # should return a ui view or activitypub json blob as requested
path('', views.home), path('', views.home),
@ -40,7 +38,7 @@ urlpatterns = [
# users # users
re_path(r'%s/?$' % user_path, views.user_page), re_path(r'%s/?$' % user_path, views.user_page),
re_path(r'%s\.json$' % local_user_path, views.user_page), re_path(r'%s\.json$' % local_user_path, views.user_page),
re_path(r'edit_profile_page/?$', views.edit_profile_page), re_path(r'user-edit/?$', views.edit_profile_page),
re_path(r'%s/followers/?$' % local_user_path, views.followers_page), re_path(r'%s/followers/?$' % local_user_path, views.followers_page),
re_path(r'%s/followers.json$' % local_user_path, views.followers_page), re_path(r'%s/followers.json$' % local_user_path, views.followers_page),
re_path(r'%s/following/?$' % local_user_path, views.following_page), re_path(r'%s/following/?$' % local_user_path, views.following_page),
@ -61,6 +59,9 @@ urlpatterns = [
re_path(r'^shelf/%s/(?P<shelf_identifier>[\w-]+)/?$' % username_regex, views.shelf_page), re_path(r'^shelf/%s/(?P<shelf_identifier>[\w-]+)/?$' % username_regex, views.shelf_page),
# internal action endpoints # internal action endpoints
re_path(r'^logout/?$', actions.user_logout),
re_path(r'^user-login/?$', actions.user_login),
re_path(r'^register/?$', actions.register),
re_path(r'^review/?$', actions.review), re_path(r'^review/?$', actions.review),
re_path(r'^tag/?$', actions.tag), re_path(r'^tag/?$', actions.tag),
re_path(r'^untag/?$', actions.untag), re_path(r'^untag/?$', actions.untag),

View file

@ -1,4 +1,5 @@
''' views for actions you can take in the application ''' ''' views for actions you can take in the application '''
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.http import HttpResponseBadRequest from django.http import HttpResponseBadRequest
from django.shortcuts import redirect from django.shortcuts import redirect
@ -6,9 +7,54 @@ from django.template.response import TemplateResponse
import re import re
from fedireads import forms, models, books_manager, outgoing from fedireads import forms, models, books_manager, outgoing
from fedireads.settings import DOMAIN
from fedireads.views import get_user_from_username from fedireads.views import get_user_from_username
def user_login(request):
''' authenticate user login '''
if request.method == 'GET':
return redirect('/login')
form = forms.LoginForm(request.POST)
if not form.is_valid():
return TemplateResponse(request, 'login.html', {'login_form': form})
username = form.data['username']
username = '%s@%s' % (username, DOMAIN)
password = form.data['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', {'login_form': form})
def register(request):
''' join the server '''
if request.method == 'GET':
return redirect('/login')
form = forms.RegisterForm(request.POST)
if not form.is_valid():
return redirect('/register/')
username = form.data['username']
email = form.data['email']
password = form.data['password']
user = models.User.objects.create_user(username, email, password)
login(request, user)
return redirect('/')
@login_required
def user_logout(request):
''' done with this place! outa here! '''
logout(request)
return redirect('/')
@login_required @login_required
def edit_profile(request): def edit_profile(request):
''' les get fancy with images ''' ''' les get fancy with images '''
@ -23,7 +69,8 @@ def edit_profile(request):
if 'avatar' in form.files: if 'avatar' in form.files:
request.user.avatar = form.files['avatar'] request.user.avatar = form.files['avatar']
request.user.summary = form.data['summary'] request.user.summary = form.data['summary']
request.user.manually_approves_followers = form.cleaned_data['manually_approves_followers'] request.user.manually_approves_followers = \
form.cleaned_data['manually_approves_followers']
request.user.save() request.user.save()
return redirect('/user/%s' % request.user.localname) return redirect('/user/%s' % request.user.localname)
@ -160,11 +207,14 @@ def search(request):
@login_required @login_required
def clear_notifications(request): def clear_notifications(request):
''' permanently delete notification for user '''
request.user.notification_set.filter(read=True).delete() request.user.notification_set.filter(read=True).delete()
return redirect('/notifications') return redirect('/notifications')
@login_required @login_required
def accept_follow_request(request): def accept_follow_request(request):
''' a user accepts a follow request '''
username = request.POST['user'] username = request.POST['user']
try: try:
requester = get_user_from_username(username) requester = get_user_from_username(username)
@ -172,7 +222,10 @@ def accept_follow_request(request):
return HttpResponseBadRequest() return HttpResponseBadRequest()
try: try:
follow_request = models.UserFollowRequest.objects.get(user_subject=requester, user_object=request.user) follow_request = models.UserFollowRequest.objects.get(
user_subject=requester,
user_object=request.user
)
except models.UserFollowRequest.DoesNotExist: except models.UserFollowRequest.DoesNotExist:
# Request already dealt with. # Request already dealt with.
pass pass
@ -181,8 +234,10 @@ def accept_follow_request(request):
return redirect('/user/%s' % request.user.localname) return redirect('/user/%s' % request.user.localname)
@login_required @login_required
def delete_follow_request(request): def delete_follow_request(request):
''' a user rejects a follow request '''
username = request.POST['user'] username = request.POST['user']
try: try:
requester = get_user_from_username(username) requester = get_user_from_username(username)
@ -190,9 +245,13 @@ def delete_follow_request(request):
return HttpResponseBadRequest() return HttpResponseBadRequest()
try: try:
follow_request = models.UserFollowRequest.objects.get(user_subject=requester, user_object=request.user) follow_request = models.UserFollowRequest.objects.get(
user_subject=requester,
user_object=request.user
)
except models.UserFollowRequest.DoesNotExist: except models.UserFollowRequest.DoesNotExist:
return HttpResponseBadRequest() return HttpResponseBadRequest()
outgoing.handle_outgoing_reject(requester, request.user, follow_request) outgoing.handle_outgoing_reject(requester, request.user, follow_request)
return redirect('/user/%s' % request.user.localname) return redirect('/user/%s' % request.user.localname)

View file

@ -1,5 +1,4 @@
''' views for pages you can go to in the application ''' ''' views for pages you can go to in the application '''
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.db.models import Avg, Q from django.db.models import Avg, Q
from django.http import HttpResponseBadRequest, HttpResponseNotFound, \ from django.http import HttpResponseBadRequest, HttpResponseNotFound, \
@ -10,7 +9,6 @@ from django.views.decorators.csrf import csrf_exempt
from fedireads import activitypub from fedireads import activitypub
from fedireads import forms, models, books_manager from fedireads import forms, models, books_manager
from fedireads.settings import DOMAIN
def get_user_from_username(username): def get_user_from_username(username):
@ -22,7 +20,6 @@ def get_user_from_username(username):
return user return user
def is_api_request(request): def is_api_request(request):
''' check whether a request is asking for html or data ''' ''' check whether a request is asking for html or data '''
# TODO: this should probably be the full content type? maybe? # TODO: this should probably be the full content type? maybe?
@ -40,28 +37,44 @@ def home(request):
def home_tab(request, tab): def home_tab(request, tab):
''' user's homepage with activity feed ''' ''' user's homepage with activity feed '''
shelves = [] shelves = []
for identifier in ['reading', 'to-read']: book_count = 6
for (identifier, count) in [('reading', 3), ('read', 1), ('to-read', 3)]:
if book_count <= 0:
break
shelf = models.Shelf.objects.get( shelf = models.Shelf.objects.get(
user=request.user, user=request.user,
identifier=identifier, identifier=identifier,
) )
if not shelf.books.count(): if not shelf.books.count():
continue continue
books = models.ShelfBook.objects.filter(
shelf=shelf,
).order_by(
'-updated_date'
)[:count]
book_count -= len(books)
shelves.append({ shelves.append({
'name': shelf.name, 'name': shelf.name,
'identifier': shelf.identifier, 'identifier': shelf.identifier,
'books': shelf.books.all()[:3], 'books': [b.book for b in books],
'size': shelf.books.count(), 'size': shelf.books.count(),
}) })
# books new to the instance, for discovery
if book_count > 0:
shelves.append({
'name': 'Recently added',
'identifier': None,
'books': models.Book.objects.order_by(
'-created_date'
)[:book_count],
'count': book_count,
})
# allows us to check if a user has shelved a book # allows us to check if a user has shelved a book
user_books = models.Book.objects.filter(shelves__user=request.user).all() user_books = models.Book.objects.filter(shelves__user=request.user).all()
# books new to the instance, for discovery
recent_books = models.Book.objects.order_by(
'-created_date'
)[:5]
# status updates for your follow network # status updates for your follow network
following = models.User.objects.filter( following = models.User.objects.filter(
Q(followers=request.user) | Q(id=request.user.id) Q(followers=request.user) | Q(id=request.user.id)
@ -89,65 +102,27 @@ def home_tab(request, tab):
data = { data = {
'user': request.user, 'user': request.user,
'shelves': shelves, 'shelves': shelves,
'recent_books': recent_books,
'user_books': user_books, 'user_books': user_books,
'activities': activities, 'activities': activities,
'feed_tabs': ['home', 'local', 'federated'], 'feed_tabs': [
{'id': 'home', 'display': 'Home'},
{'id': 'local', 'display': 'Local'},
{'id': 'federated', 'display': 'Federated'}
],
'active_tab': tab, 'active_tab': tab,
'review_form': forms.ReviewForm(),
} }
return TemplateResponse(request, 'feed.html', data) return TemplateResponse(request, 'feed.html', data)
def user_login(request): def login_page(request):
''' authentication ''' ''' authentication '''
# send user to the login page # send user to the login page
if request.method == 'GET': data = {
form = forms.LoginForm() 'login_form': forms.LoginForm(),
return TemplateResponse(request, 'login.html', {'login_form': form}) 'register_form': forms.RegisterForm(),
}
# authenticate user return TemplateResponse(request, 'login.html', data)
form = forms.LoginForm(request.POST)
if not form.is_valid():
return TemplateResponse(request, 'login.html', {'login_form': form})
username = form.data['username']
username = '%s@%s' % (username, DOMAIN)
password = form.data['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', {'login_form': form})
@login_required
def user_logout(request):
''' done with this place! outa here! '''
logout(request)
return redirect('/')
def register(request):
''' join the server '''
if request.method == 'GET':
form = forms.RegisterForm()
return TemplateResponse(
request,
'register.html',
{'register_form': form}
)
form = forms.RegisterForm(request.POST)
if not form.is_valid():
return redirect('/register/')
username = form.data['username']
email = form.data['email']
password = form.data['password']
user = models.User.objects.create_user(username, email, password)
login(request, user)
return redirect('/')
@login_required @login_required