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 Meta:
model = models.Review
fields = ['name', 'content', 'rating']
fields = ['name', 'rating', 'content']
help_texts = {f: None for f in fields}
review_content = IntegerField(validators=[
content = IntegerField(validators=[
MinValueValidator(0), MaxValueValidator(5)
])
labels = {
'name': 'Title',
'review_content': 'Review',
'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)
class Review(Status):
''' a book review '''
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)
def handle_outgoing_reject(user, to_follow, relationship):
''' a local user who managed follows rejects a follow request '''
relationship.delete()
activity = activitypub.get_reject(to_follow, relationship)
recipient = get_recipients(to_follow, 'direct', direct_recipients=[user])
broadcast(to_follow, activity, recipient)
def handle_shelve(user, book, shelf):
''' a local user is getting a book put on their shelf '''
# update the database
@ -132,9 +134,10 @@ def handle_shelve(user, book, shelf):
'reading': 'started reading',
'read': 'finished reading'
}[shelf.identifier]
name = user.name if user.name else user.localname
message = '%s %s %s' % (name, verb, book.title)
message = '%s %s' % (verb, book.title)
status = create_status(user, message, mention_books=[book])
status.status_type = 'Update'
status.save()
activity = activitypub.get_status(status)
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;
color: black;
}
a {
color: #00F;
body {
padding-top: 90px;
}
a:visited {
color: #808;
a {
color: #247BA0;
}
input, button {
padding: 0.2em 0.5em;
}
button {
cursor: pointer;
width: max-content;
}
h1, h2, h3, h4 {
@ -29,16 +37,21 @@ h1 {
h2 {
font-size: 1rem;
background-color: #B2DBBF;
padding: 0.5rem 0.2rem;
margin-bottom: 1rem;
height: 1rem;
}
#top-bar {
background-color: #70C1B3;
overflow: hidden;
overflow: visible;
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 {
@ -46,62 +59,183 @@ h2 {
text-align: center;
}
#branding, #actions {
margin: 0 1rem;
}
#branding {
flex-grow: 1;
font-size: 2rem;
}
#branding a {
text-decoration: none;
color: black;
}
#actions {
flex-grow: 0;
text-align: right;
margin-top: 1em;
}
#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;
max-width: 55rem;
padding-right: 1em;
}
header {
display: flex;
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;
flex-direction: column;
margin: 0 1rem 1rem 0;
padding-top: 70px;
position: relative;
top: -50px;
}
#sidebar {
min-width: 20rem;
margin-right: 0;
.row {
display: flex;
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 {
display: flex;
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;
}
.tabs.secondary .tab.active {
background-color: #247BA0;
}
.tab.active a {
color: black;
}
.user-pic {
width: 2rem;
height: auto;
height: 2rem;
border-radius: 50%;
vertical-align: top;
position: relative;
bottom: 0.5em;
bottom: 0.35em;
}
.review-form label {
display: block;
}
.time-ago {
@ -110,7 +244,7 @@ h2 {
}
.book-preview {
overflow: auto;
overflow: hidden;
}
.book-preview img {
@ -122,17 +256,89 @@ h2 {
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 {
width: 50px;
height: auto;
}
#content > div, #feed > div, #sidebar > div {
background-color: #EFEFEF;
margin: 1rem 0 0 1rem;
}
#content > div > *, #feed > div > *, #sidebar > div > * {
padding: 1rem;
.compose-suggestion .book-preview {
background-color: #EEE;
padding: 1em;
}
.tag {
@ -150,22 +356,14 @@ h2 {
width: 30rem;
height: 10rem;
}
.review {
margin-bottom: 1rem;
}
small {
display: block;
}
blockquote {
white-space: pre-wrap;
}
.interaction {
background-color: #F3FFBD;
clear: both;
margin-top: 1rem;
background-color: #B2DBBF;
border-radius: 0 0 0.5em 0.5em;
}
.interaction textarea {
@ -201,27 +399,44 @@ th, td {
.comment-thread .reply h2 {
background: none;
}
.post.main {
background-color: #F3FFBD;
}
.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;
border-left: 2px solid #247BA0;
}
.post.depth-1 {
.comment-thread .post.depth-1 {
margin-left: 0;
border: none;
}
.post.depth-2 {
.comment-thread .post.depth-2 {
margin-left: 1em;
}
.post.depth-3 {
.comment-thread .post.depth-3 {
margin-left: 2em;
}
.post.depth-4 {
.comment-thread .post.depth-4 {
margin-left: 3em;
}
.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>
<h2><q>{{ book.title }}</q> and You</h2>
<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">
{% for tag in user_tags %}

View file

@ -2,44 +2,52 @@
{% load fr_display %}
{% block content %}
<div id="sidebar">
<div>
{% if shelves %}
<div class="all-shelves content-container">
{% for shelf in shelves %}
<h2>{{ shelf.name }}</h2>
{% 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>
{% if shelf.books %}
<div>
<h2>Recently Added Books</h2>
{% for book in recent_books %}
<div class="book-preview">
{% include 'snippets/book.html' with book=book size="small" %}
<h2>{{ shelf.name }}
{% if shelf.size > shelf.books|length %}
<small>(<a href="/shelf/{{ user | username }}/{{ shelf.identifier }}">See all {{ shelf.size }}</a>)</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>
{% endfor %}
</div>
</div>
{% endif %}
{% endfor %}
</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 class="content-container tabs">
{% include 'snippets/tabs.html' with tabs=feed_tabs active_tab=active_tab %}
</div>
{% 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 %}
</div>
<script src="/static/js/feed.js"></script>
{% endblock %}

View file

@ -2,15 +2,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>FediReads</title>
<title>BookWyrm</title>
<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/icons.css">
<link rel="shortcut icon" type="image/x-icon" href="/static/images/favicon.ico">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="FediReads">
<meta name="og:title" content="FediReads">
<meta name="twitter:title" content="BookWyrm">
<meta name="og:title" content="BookWyrm">
<meta name="twitter:description" content="Federated Social Reading">
<meta name="og:description" content="Federated Social Reading">
<meta name="twitter:creator" content="@tripofmice">
@ -22,41 +23,44 @@
<div id="top-bar">
<header>
<div>
<div id="branding"><a href="/">📚FediReads</a></div>
<div id="branding"><a href="/"><img id="logo" src="/static/images/logo-small.png" alt="BookWyrm"></img></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="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">
<form action="/search/">
<input type="text" name="q" placeholder="search"></input>
<input type="submit" value="🔍"></input>
<input type="text" name="q" placeholder="Search for a book or user"></input>
<button type="submit">
<span class="icon icon-search">
<span class="hidden-text">search</span>
</span>
</button>
</form>
</div>
{% if request.user.is_authenticated %}
<div id="notification">
<div id="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>
</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 %}
</div>
</div>
</header>
</div>
@ -65,6 +69,10 @@
{% endblock %}
</div>
<script>
var csrf_token = {{ csrf_token }};
</script>
<script src="/static/js/shared.js"></script>
</body>
</html>

View file

@ -1,13 +1,33 @@
{% extends 'layout.html' %}
{% 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>
<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 %}
{{ login_form.as_p }}
<button type="submit">Log in</button>
</form>
<a href="/register/">Create a new account</a>
<p><small><a href="/reset-password">Forgot your password?</a></small></p>
</div>
</div>
</div>

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>
{% 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 %}
<input type="hidden" name="book" value="{{ book.id }}"></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>
{% else %}
@ -21,7 +21,7 @@
</option>
{% endfor %}
</select>
<button type="submit">Shelve</button>
<button class="secondary" type="submit">Shelve</button>
</form>
{% endif %}

View file

@ -1,21 +1,48 @@
{% load humanize %}
{% load fr_display %}
<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 %}
<div class="book-preview">
{% if status.status_type == 'Review' %}
{% 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>
{% endif %}
{% if not hide_book and status.book%}
<div class="book-preview">
{% if status.status_type == 'Review' %}
{% include 'snippets/book.html' with book=status.book %}
{% else %}
{% include 'snippets/book.html' with book=status.book description=True %}
{% endif %}
</div>
{% endif %}
{% if status.status_type == 'Review' %}<h4>{{ status.name }}
<small>{{ status.rating | stars }} stars, by {% include 'snippets/username.html' with user=status.user %}</small>
</h4>{% endif %}
{% if status.status_type != 'Update' %}
<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 %}
{% include 'snippets/interaction.html' with activity=status %}
</div>
{% include 'snippets/interaction.html' with activity=status %}

View file

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

View file

@ -1,2 +1,2 @@
{% 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 %}
<div class="interaction">
<a href="/edit_profile_page/">Edit profile</a>
<a href="/user-edit/">Edit profile</a>
</div>
{% endif %}
</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 view_actions as actions
username_regex = r'(?P<username>[\w@\-_]+)'
username_regex = r'(?P<username>[\w@\-_\.]+)'
localname_regex = r'(?P<username>[\w\-_]+)'
user_path = r'^user/%s' % username_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),
# ui views
re_path(r'^register/?$', views.register),
re_path(r'^login/?$', views.user_login),
re_path(r'^logout/?$', views.user_logout),
re_path(r'^login/?$', views.login_page),
# should return a ui view or activitypub json blob as requested
path('', views.home),
@ -40,7 +38,7 @@ urlpatterns = [
# users
re_path(r'%s/?$' % 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.json$' % local_user_path, views.followers_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),
# 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'^tag/?$', actions.tag),
re_path(r'^untag/?$', actions.untag),

View file

@ -1,4 +1,5 @@
''' 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.http import HttpResponseBadRequest
from django.shortcuts import redirect
@ -6,9 +7,54 @@ from django.template.response import TemplateResponse
import re
from fedireads import forms, models, books_manager, outgoing
from fedireads.settings import DOMAIN
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
def edit_profile(request):
''' les get fancy with images '''
@ -23,7 +69,8 @@ def edit_profile(request):
if 'avatar' in form.files:
request.user.avatar = form.files['avatar']
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()
return redirect('/user/%s' % request.user.localname)
@ -160,11 +207,14 @@ def search(request):
@login_required
def clear_notifications(request):
''' permanently delete notification for user '''
request.user.notification_set.filter(read=True).delete()
return redirect('/notifications')
@login_required
def accept_follow_request(request):
''' a user accepts a follow request '''
username = request.POST['user']
try:
requester = get_user_from_username(username)
@ -172,7 +222,10 @@ def accept_follow_request(request):
return HttpResponseBadRequest()
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:
# Request already dealt with.
pass
@ -181,8 +234,10 @@ def accept_follow_request(request):
return redirect('/user/%s' % request.user.localname)
@login_required
def delete_follow_request(request):
''' a user rejects a follow request '''
username = request.POST['user']
try:
requester = get_user_from_username(username)
@ -190,9 +245,13 @@ def delete_follow_request(request):
return HttpResponseBadRequest()
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:
return HttpResponseBadRequest()
outgoing.handle_outgoing_reject(requester, request.user, follow_request)
return redirect('/user/%s' % request.user.localname)

View file

@ -1,5 +1,4 @@
''' 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.db.models import Avg, Q
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 forms, models, books_manager
from fedireads.settings import DOMAIN
def get_user_from_username(username):
@ -22,7 +20,6 @@ def get_user_from_username(username):
return user
def is_api_request(request):
''' check whether a request is asking for html or data '''
# TODO: this should probably be the full content type? maybe?
@ -40,28 +37,44 @@ def home(request):
def home_tab(request, tab):
''' user's homepage with activity feed '''
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(
user=request.user,
identifier=identifier,
)
if not shelf.books.count():
continue
books = models.ShelfBook.objects.filter(
shelf=shelf,
).order_by(
'-updated_date'
)[:count]
book_count -= len(books)
shelves.append({
'name': shelf.name,
'identifier': shelf.identifier,
'books': shelf.books.all()[:3],
'books': [b.book for b in books],
'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
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
following = models.User.objects.filter(
Q(followers=request.user) | Q(id=request.user.id)
@ -89,65 +102,27 @@ def home_tab(request, tab):
data = {
'user': request.user,
'shelves': shelves,
'recent_books': recent_books,
'user_books': user_books,
'activities': activities,
'feed_tabs': ['home', 'local', 'federated'],
'feed_tabs': [
{'id': 'home', 'display': 'Home'},
{'id': 'local', 'display': 'Local'},
{'id': 'federated', 'display': 'Federated'}
],
'active_tab': tab,
'review_form': forms.ReviewForm(),
}
return TemplateResponse(request, 'feed.html', data)
def user_login(request):
def login_page(request):
''' authentication '''
# send user to the login page
if request.method == 'GET':
form = forms.LoginForm()
return TemplateResponse(request, 'login.html', {'login_form': form})
# authenticate user
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('/')
data = {
'login_form': forms.LoginForm(),
'register_form': forms.RegisterForm(),
}
return TemplateResponse(request, 'login.html', data)
@login_required