Compare commits

..

3 Commits

38 changed files with 490 additions and 418 deletions

View File

@@ -1,6 +1,6 @@
from rest_framework import serializers from rest_framework import serializers
from apps.dashboard.models import Item, List from apps.dashboard.models import Item, Note
from apps.lyric.models import User from apps.lyric.models import User
@@ -8,8 +8,8 @@ class ItemSerializer(serializers.ModelSerializer):
text = serializers.CharField() text = serializers.CharField()
def validate_text(self, value): def validate_text(self, value):
list_ = self.context["list"] note = self.context["note"]
if list_.item_set.filter(text=value).exists(): if note.item_set.filter(text=value).exists():
raise serializers.ValidationError("duplicate") raise serializers.ValidationError("duplicate")
return value return value
@@ -17,13 +17,13 @@ class ItemSerializer(serializers.ModelSerializer):
model = Item model = Item
fields = ["id", "text"] fields = ["id", "text"]
class ListSerializer(serializers.ModelSerializer): class NoteSerializer(serializers.ModelSerializer):
name = serializers.ReadOnlyField() name = serializers.ReadOnlyField()
url = serializers.CharField(source="get_absolute_url", read_only=True) url = serializers.CharField(source="get_absolute_url", read_only=True)
items = ItemSerializer(many=True, read_only=True, source="item_set") items = ItemSerializer(many=True, read_only=True, source="item_set")
class Meta: class Meta:
model = List model = Note
fields = ["id", "name", "url", "items"] fields = ["id", "name", "url", "items"]
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):

View File

@@ -1,7 +1,7 @@
from django.test import TestCase from django.test import TestCase
from rest_framework.test import APIClient from rest_framework.test import APIClient
from apps.dashboard.models import Item, List from apps.dashboard.models import Item, Note
from apps.lyric.models import User from apps.lyric.models import User
class BaseAPITest(TestCase): class BaseAPITest(TestCase):
@@ -11,24 +11,24 @@ class BaseAPITest(TestCase):
self.user = User.objects.create_user("test@example.com") self.user = User.objects.create_user("test@example.com")
self.client.force_authenticate(user=self.user) self.client.force_authenticate(user=self.user)
class ListDetailAPITest(BaseAPITest): class NoteDetailAPITest(BaseAPITest):
def test_returns_list_with_items(self): def test_returns_note_with_items(self):
list_ = List.objects.create(owner=self.user) note = Note.objects.create(owner=self.user)
Item.objects.create(text="item 1", list=list_) Item.objects.create(text="item 1", note=note)
Item.objects.create(text="item 2", list=list_) Item.objects.create(text="item 2", note=note)
response = self.client.get(f"/api/lists/{list_.id}/") response = self.client.get(f"/api/notes/{note.id}/")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["id"], str(list_.id)) self.assertEqual(response.data["id"], str(note.id))
self.assertEqual(len(response.data["items"]), 2) self.assertEqual(len(response.data["items"]), 2)
class ListItemsAPITest(BaseAPITest): class NoteItemsAPITest(BaseAPITest):
def test_can_add_item_to_list(self): def test_can_add_item_to_note(self):
list_ = List.objects.create(owner=self.user) note = Note.objects.create(owner=self.user)
response = self.client.post( response = self.client.post(
f"/api/lists/{list_.id}/items/", f"/api/notes/{note.id}/items/",
{"text": "a new item"}, {"text": "a new item"},
) )
@@ -36,50 +36,50 @@ class ListItemsAPITest(BaseAPITest):
self.assertEqual(Item.objects.count(), 1) self.assertEqual(Item.objects.count(), 1)
self.assertEqual(Item.objects.first().text, "a new item") self.assertEqual(Item.objects.first().text, "a new item")
def test_cannot_add_empty_item_to_list(self): def test_cannot_add_empty_item_to_note(self):
list_ = List.objects.create(owner=self.user) note = Note.objects.create(owner=self.user)
response = self.client.post( response = self.client.post(
f"/api/lists/{list_.id}/items/", f"/api/notes/{note.id}/items/",
{"text": ""}, {"text": ""},
) )
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
self.assertEqual(Item.objects.count(), 0) self.assertEqual(Item.objects.count(), 0)
def test_cannot_add_duplicate_item_to_list(self): def test_cannot_add_duplicate_item_to_note(self):
list_ = List.objects.create(owner=self.user) note = Note.objects.create(owner=self.user)
Item.objects.create(text="list item", list=list_) Item.objects.create(text="note item", note=note)
duplicate_response = self.client.post( duplicate_response = self.client.post(
f"/api/lists/{list_.id}/items/", f"/api/notes/{note.id}/items/",
{"text": "list item"}, {"text": "note item"},
) )
self.assertEqual(duplicate_response.status_code, 400) self.assertEqual(duplicate_response.status_code, 400)
self.assertEqual(Item.objects.count(), 1) self.assertEqual(Item.objects.count(), 1)
class ListsAPITest(BaseAPITest): class NotesAPITest(BaseAPITest):
def test_get_returns_only_users_lists(self): def test_get_returns_only_users_notes(self):
list1 = List.objects.create(owner=self.user) note1 = Note.objects.create(owner=self.user)
Item.objects.create(text="item 1", list=list1) Item.objects.create(text="item 1", note=note1)
other_user = User.objects.create_user("other@example.com") other_user = User.objects.create_user("other@example.com")
List.objects.create(owner=other_user) Note.objects.create(owner=other_user)
response = self.client.get("/api/lists/") response = self.client.get("/api/notes/")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data), 1) self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]["id"], str(list1.id)) self.assertEqual(response.data[0]["id"], str(note1.id))
def test_post_creates_list_with_item(self): def test_post_creates_note_with_item(self):
response = self.client.post( response = self.client.post(
"/api/lists/", "/api/notes/",
{"text": "first item"}, {"text": "first item"},
) )
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
self.assertEqual(List.objects.count(), 1) self.assertEqual(Note.objects.count(), 1)
self.assertEqual(List.objects.first().owner, self.user) self.assertEqual(Note.objects.first().owner, self.user)
self.assertEqual(Item.objects.first().text, "first item") self.assertEqual(Item.objects.first().text, "first item")
class UserSearchAPITest(BaseAPITest): class UserSearchAPITest(BaseAPITest):

View File

@@ -1,5 +1,5 @@
from django.test import SimpleTestCase from django.test import SimpleTestCase
from apps.api.serializers import ItemSerializer, ListSerializer from apps.api.serializers import ItemSerializer, NoteSerializer
class ItemSerializerTest(SimpleTestCase): class ItemSerializerTest(SimpleTestCase):
@@ -10,11 +10,10 @@ class ItemSerializerTest(SimpleTestCase):
{"id", "text"}, {"id", "text"},
) )
class ListSerializerTest(SimpleTestCase): class NoteSerializerTest(SimpleTestCase):
def test_fields(self): def test_fields(self):
serializer = ListSerializer() serializer = NoteSerializer()
self.assertEqual( self.assertEqual(
set(serializer.fields.keys()), set(serializer.fields.keys()),
{"id", "name", "url", "items"}, {"id", "name", "url", "items"},
) )

View File

@@ -4,9 +4,8 @@ from . import views
urlpatterns = [ urlpatterns = [
path('lists/', views.ListsAPI.as_view(), name='api_lists'), path('notes/', views.NotesAPI.as_view(), name='api_notes'),
path('lists/<uuid:list_id>/', views.ListDetailAPI.as_view(), name='api_list_detail'), path('notes/<uuid:note_id>/', views.NoteDetailAPI.as_view(), name='api_note_detail'),
path('lists/<uuid:list_id>/items/', views.ListItemsAPI.as_view(), name='api_list_items'), path('notes/<uuid:note_id>/items/', views.NoteItemsAPI.as_view(), name='api_note_items'),
path('users/', views.UserSearchAPI.as_view(), name='api_users'), path('users/', views.UserSearchAPI.as_view(), name='api_users'),
] ]

View File

@@ -2,38 +2,38 @@ from django.shortcuts import get_object_or_404
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from apps.api.serializers import ItemSerializer, ListSerializer, UserSerializer from apps.api.serializers import ItemSerializer, NoteSerializer, UserSerializer
from apps.dashboard.models import Item, List from apps.dashboard.models import Item, Note
from apps.lyric.models import User from apps.lyric.models import User
class ListDetailAPI(APIView): class NoteDetailAPI(APIView):
def get(self, request, list_id): def get(self, request, note_id):
list_ = get_object_or_404(List, id=list_id) note = get_object_or_404(Note, id=note_id)
serializer = ListSerializer(list_) serializer = NoteSerializer(note)
return Response(serializer.data) return Response(serializer.data)
class ListItemsAPI(APIView): class NoteItemsAPI(APIView):
def post(self, request, list_id): def post(self, request, note_id):
list_ = get_object_or_404(List, id=list_id) note = get_object_or_404(Note, id=note_id)
serializer = ItemSerializer(data=request.data, context={"list": list_}) serializer = ItemSerializer(data=request.data, context={"note": note})
if serializer.is_valid(): if serializer.is_valid():
serializer.save(list=list_) serializer.save(note=note)
return Response(serializer.data, status=201) return Response(serializer.data, status=201)
return Response(serializer.errors, status=400) return Response(serializer.errors, status=400)
class ListsAPI(APIView): class NotesAPI(APIView):
def get(self, request): def get(self, request):
lists = List.objects.filter(owner=request.user) notes = Note.objects.filter(owner=request.user)
serializer = ListSerializer(lists, many=True) serializer = NoteSerializer(notes, many=True)
return Response(serializer.data) return Response(serializer.data)
def post(self, request): def post(self, request):
list_ = List.objects.create(owner=request.user) note = Note.objects.create(owner=request.user)
item = Item.objects.create(text=request.data.get("text", ""), list=list_) item = Item.objects.create(text=request.data.get("text", ""), note=note)
serializer = ListSerializer(list_) serializer = NoteSerializer(note)
return Response(serializer.data, status=201) return Response(serializer.data, status=201)
class UserSearchAPI(APIView): class UserSearchAPI(APIView):
def get(self, request): def get(self, request):
q = request.query_params.get("q", "") q = request.query_params.get("q", "")

View File

@@ -0,0 +1,24 @@
from django.db import migrations
def rename_list_slugs(apps, schema_editor):
Applet = apps.get_model('applets', 'Applet')
Applet.objects.filter(slug='new-list').update(slug='new-note', name='New Note')
Applet.objects.filter(slug='my-lists').update(slug='my-notes', name='My Notes')
def reverse_rename_list_slugs(apps, schema_editor):
Applet = apps.get_model('applets', 'Applet')
Applet.objects.filter(slug='new-note').update(slug='new-list', name='New List')
Applet.objects.filter(slug='my-notes').update(slug='my-lists', name='My Lists')
class Migration(migrations.Migration):
dependencies = [
('applets', '0003_wallet_applets'),
]
operations = [
migrations.RunPython(rename_list_slugs, reverse_rename_list_slugs),
]

View File

@@ -2,8 +2,8 @@ from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from .models import Item from .models import Item
DUPLICATE_ITEM_ERROR = "You've already logged this to your list" DUPLICATE_ITEM_ERROR = "You've already logged this to your note"
EMPTY_ITEM_ERROR = "You can't have an empty list item" EMPTY_ITEM_ERROR = "You can't have an empty note item"
class ItemForm(forms.Form): class ItemForm(forms.Form):
text = forms.CharField( text = forms.CharField(
@@ -11,22 +11,22 @@ class ItemForm(forms.Form):
required=True, required=True,
) )
def save(self, for_list): def save(self, for_note):
return Item.objects.create( return Item.objects.create(
list=for_list, note=for_note,
text=self.cleaned_data["text"], text=self.cleaned_data["text"],
) )
class ExistingListItemForm(ItemForm): class ExistingNoteItemForm(ItemForm):
def __init__(self, for_list, *args, **kwargs): def __init__(self, for_note, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._for_list = for_list self._for_note = for_note
def clean_text(self): def clean_text(self):
text = self.cleaned_data["text"] text = self.cleaned_data["text"]
if self._for_list.item_set.filter(text=text).exists(): if self._for_note.item_set.filter(text=text).exists():
raise forms.ValidationError(DUPLICATE_ITEM_ERROR) raise forms.ValidationError(DUPLICATE_ITEM_ERROR)
return text return text
def save(self): def save(self):
return super().save(for_list=self._for_list) return super().save(for_note=self._for_note)

View File

@@ -0,0 +1,20 @@
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dashboard', '0001_initial'),
]
operations = [
migrations.RenameModel(
old_name='List',
new_name='Note',
),
migrations.RenameField(
model_name='Item',
old_name='list',
new_name='note',
),
]

View File

@@ -4,11 +4,11 @@ from django.db import models
from django.urls import reverse from django.urls import reverse
class List(models.Model): class Note(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
owner = models.ForeignKey( owner = models.ForeignKey(
"lyric.User", "lyric.User",
related_name="lists", related_name="notes",
blank=True, blank=True,
null=True, null=True,
on_delete=models.CASCADE, on_delete=models.CASCADE,
@@ -16,7 +16,7 @@ class List(models.Model):
shared_with = models.ManyToManyField( shared_with = models.ManyToManyField(
"lyric.User", "lyric.User",
related_name="shared_lists", related_name="shared_notes",
blank=True, blank=True,
) )
@@ -25,15 +25,15 @@ class List(models.Model):
return self.item_set.first().text return self.item_set.first().text
def get_absolute_url(self): def get_absolute_url(self):
return reverse("view_list", args=[self.id]) return reverse("view_note", args=[self.id])
class Item(models.Model): class Item(models.Model):
text = models.TextField(default="") text = models.TextField(default="")
list = models.ForeignKey(List, default=None, on_delete=models.CASCADE) note = models.ForeignKey(Note, default=None, on_delete=models.CASCADE)
class Meta: class Meta:
ordering = ("id",) ordering = ("id",)
unique_together = ("list", "text") unique_together = ("note", "text")
def __str__(self): def __str__(self):
return self.text return self.text

View File

@@ -23,15 +23,22 @@ const initWallet = () => {
const section = addBtn.closest('section'); const section = addBtn.closest('section');
const rowPx = 3 * parseFloat(getComputedStyle(document.documentElement).fontSize); const rowPx = 3 * parseFloat(getComputedStyle(document.documentElement).fontSize);
const updateRows = () => { const updateRows = () => {
const rows = Math.ceil(section.scrollHeight / rowPx) + 1; const sectionTop = section.getBoundingClientRect().top;
let maxBottom = sectionTop;
for (const child of section.children) {
if (child.hidden) continue;
maxBottom = Math.max(maxBottom, child.getBoundingClientRect().bottom);
}
const padBot = parseFloat(getComputedStyle(section).paddingBottom);
const rows = Math.ceil((maxBottom - sectionTop + padBot) / rowPx) + 1;
section.style.setProperty('--applet-rows', String(rows)); section.style.setProperty('--applet-rows', String(rows));
}; };
paymentEl.on('ready', () => { paymentEl.on('ready', () => {
updateRows(); updateRows();
const iframe = document.querySelector('#id_stripe_payment_element iframe'); const stripeContainer = document.getElementById('id_stripe_payment_element');
if (iframe) { if (stripeContainer) {
const obs = new MutationObserver(updateRows); const obs = new ResizeObserver(updateRows);
obs.observe(iframe, { attributes: true, attributeFilter: ['style'] }); obs.observe(stripeContainer);
section._stripeObs = obs; section._stripeObs = obs;
} }
}); });

View File

@@ -3,39 +3,39 @@ from django.test import TestCase
from apps.dashboard.forms import ( from apps.dashboard.forms import (
DUPLICATE_ITEM_ERROR, DUPLICATE_ITEM_ERROR,
EMPTY_ITEM_ERROR, EMPTY_ITEM_ERROR,
ExistingListItemForm, ExistingNoteItemForm,
ItemForm, ItemForm,
) )
from apps.dashboard.models import Item, List from apps.dashboard.models import Item, Note
class ItemFormTest(TestCase): class ItemFormTest(TestCase):
def test_form_save_handles_saving_to_a_list(self): def test_form_save_handles_saving_to_a_note(self):
mylist = List.objects.create() mynote = Note.objects.create()
form = ItemForm(data={"text": "do re mi"}) form = ItemForm(data={"text": "do re mi"})
self.assertTrue(form.is_valid()) self.assertTrue(form.is_valid())
new_item = form.save(for_list=mylist) new_item = form.save(for_note=mynote)
self.assertEqual(new_item, Item.objects.get()) self.assertEqual(new_item, Item.objects.get())
self.assertEqual(new_item.text, "do re mi") self.assertEqual(new_item.text, "do re mi")
self.assertEqual(new_item.list, mylist) self.assertEqual(new_item.note, mynote)
class ExistingListItemFormTest(TestCase): class ExistingNoteItemFormTest(TestCase):
def test_form_validation_for_blank_items(self): def test_form_validation_for_blank_items(self):
list_ = List.objects.create() note = Note.objects.create()
form = ExistingListItemForm(for_list=list_, data={"text": ""}) form = ExistingNoteItemForm(for_note=note, data={"text": ""})
self.assertFalse(form.is_valid()) self.assertFalse(form.is_valid())
self.assertEqual(form.errors["text"], [EMPTY_ITEM_ERROR]) self.assertEqual(form.errors["text"], [EMPTY_ITEM_ERROR])
def test_form_validation_for_duplicate_items(self): def test_form_validation_for_duplicate_items(self):
list_ = List.objects.create() note = Note.objects.create()
Item.objects.create(list=list_, text="twins, basil") Item.objects.create(note=note, text="twins, basil")
form = ExistingListItemForm(for_list=list_, data={"text": "twins, basil"}) form = ExistingNoteItemForm(for_note=note, data={"text": "twins, basil"})
self.assertFalse(form.is_valid()) self.assertFalse(form.is_valid())
self.assertEqual(form.errors["text"], [DUPLICATE_ITEM_ERROR]) self.assertEqual(form.errors["text"], [DUPLICATE_ITEM_ERROR])
def test_form_save(self): def test_form_save(self):
mylist = List.objects.create() mynote = Note.objects.create()
form = ExistingListItemForm(for_list=mylist, data={"text": "howdy"}) form = ExistingNoteItemForm(for_note=mynote, data={"text": "howdy"})
self.assertTrue(form.is_valid()) self.assertTrue(form.is_valid())
new_item = form.save() new_item = form.save()
self.assertEqual(new_item, Item.objects.get()) self.assertEqual(new_item, Item.objects.get())

View File

@@ -2,69 +2,69 @@ from django.core.exceptions import ValidationError
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from django.test import TestCase from django.test import TestCase
from apps.dashboard.models import Item, List from apps.dashboard.models import Item, Note
from apps.lyric.models import User from apps.lyric.models import User
class ItemModelTest(TestCase): class ItemModelTest(TestCase):
def test_item_is_related_to_list(self): def test_item_is_related_to_note(self):
mylist = List.objects.create() mynote = Note.objects.create()
item = Item() item = Item()
item.list = mylist item.note = mynote
item.save() item.save()
self.assertIn(item, mylist.item_set.all()) self.assertIn(item, mynote.item_set.all())
def test_cannot_save_null_list_items(self): def test_cannot_save_null_note_items(self):
mylist = List.objects.create() mynote = Note.objects.create()
item = Item(list=mylist, text=None) item = Item(note=mynote, text=None)
with self.assertRaises(IntegrityError): with self.assertRaises(IntegrityError):
item.save() item.save()
def test_cannot_save_empty_list_items(self): def test_cannot_save_empty_note_items(self):
mylist = List.objects.create() mynote = Note.objects.create()
item = Item(list=mylist, text="") item = Item(note=mynote, text="")
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
item.full_clean() item.full_clean()
def test_duplicate_items_are_invalid(self): def test_duplicate_items_are_invalid(self):
mylist = List.objects.create() mynote = Note.objects.create()
Item.objects.create(list=mylist, text="jklol") Item.objects.create(note=mynote, text="jklol")
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
item = Item(list=mylist, text="jklol") item = Item(note=mynote, text="jklol")
item.full_clean() item.full_clean()
def test_still_can_save_same_item_to_different_lists(self): def test_still_can_save_same_item_to_different_notes(self):
list1 = List.objects.create() note1 = Note.objects.create()
list2 = List.objects.create() note2 = Note.objects.create()
Item.objects.create(list=list1, text="nojk") Item.objects.create(note=note1, text="nojk")
item = Item(list=list2, text="nojk") item = Item(note=note2, text="nojk")
item.full_clean() # should not raise item.full_clean() # should not raise
class ListModelTest(TestCase): class NoteModelTest(TestCase):
def test_get_absolute_url(self): def test_get_absolute_url(self):
mylist = List.objects.create() mynote = Note.objects.create()
self.assertEqual(mylist.get_absolute_url(), f"/dashboard/list/{mylist.id}/") self.assertEqual(mynote.get_absolute_url(), f"/dashboard/note/{mynote.id}/")
def test_list_items_order(self): def test_note_items_order(self):
list1 = List.objects.create() note1 = Note.objects.create()
item1 = Item.objects.create(list=list1, text="i1") item1 = Item.objects.create(note=note1, text="i1")
item2 = Item.objects.create(list=list1, text="item 2") item2 = Item.objects.create(note=note1, text="item 2")
item3 = Item.objects.create(list=list1, text="3") item3 = Item.objects.create(note=note1, text="3")
self.assertEqual( self.assertEqual(
list(list1.item_set.all()), list(note1.item_set.all()),
[item1, item2, item3], [item1, item2, item3],
) )
def test_lists_can_have_owners(self): def test_notes_can_have_owners(self):
user = User.objects.create(email="a@b.cde") user = User.objects.create(email="a@b.cde")
mylist = List.objects.create(owner=user) mynote = Note.objects.create(owner=user)
self.assertIn(mylist, user.lists.all()) self.assertIn(mynote, user.notes.all())
def test_list_owner_is_optional(self): def test_note_owner_is_optional(self):
List.objects.create() Note.objects.create()
def test_list_name_is_first_item_text(self): def test_note_name_is_first_item_text(self):
list_ = List.objects.create() note = Note.objects.create()
Item.objects.create(list=list_, text="first item") Item.objects.create(note=note, text="first item")
Item.objects.create(list=list_, text="second item") Item.objects.create(note=note, text="second item")
self.assertEqual(list_.name, "first item") self.assertEqual(note.name, "first item")

View File

@@ -10,7 +10,7 @@ from apps.dashboard.forms import (
DUPLICATE_ITEM_ERROR, DUPLICATE_ITEM_ERROR,
EMPTY_ITEM_ERROR, EMPTY_ITEM_ERROR,
) )
from apps.dashboard.models import Item, List from apps.dashboard.models import Item, Note
from apps.lyric.models import User from apps.lyric.models import User
@@ -18,7 +18,7 @@ class HomePageTest(TestCase):
def setUp(self): def setUp(self):
self.user = User.objects.create(email="disco@test.io") self.user = User.objects.create(email="disco@test.io")
self.client.force_login(self.user) self.client.force_login(self.user)
Applet.objects.get_or_create(slug="new-list", defaults={"name": "New List"}) Applet.objects.get_or_create(slug="new-note", defaults={"name": "New Note"})
def test_uses_home_template(self): def test_uses_home_template(self):
response = self.client.get('/') response = self.client.get('/')
@@ -28,36 +28,36 @@ class HomePageTest(TestCase):
response = self.client.get('/') response = self.client.get('/')
parsed = lxml.html.fromstring(response.content) parsed = lxml.html.fromstring(response.content)
forms = parsed.cssselect('form[method=POST]') forms = parsed.cssselect('form[method=POST]')
self.assertIn("/dashboard/new_list", [form.get("action") for form in forms]) self.assertIn("/dashboard/new_note", [form.get("action") for form in forms])
[form] = [form for form in forms if form.get("action") == "/dashboard/new_list"] [form] = [form for form in forms if form.get("action") == "/dashboard/new_note"]
inputs = form.cssselect("input") inputs = form.cssselect("input")
self.assertIn("text", [input.get("name") for input in inputs]) self.assertIn("text", [input.get("name") for input in inputs])
class NewListTest(TestCase): class NewNoteTest(TestCase):
def setUp(self): def setUp(self):
user = User.objects.create(email="disco@test.io") user = User.objects.create(email="disco@test.io")
self.client.force_login(user) self.client.force_login(user)
def test_can_save_a_POST_request(self): def test_can_save_a_POST_request(self):
self.client.post("/dashboard/new_list", data={"text": "A new list item"}) self.client.post("/dashboard/new_note", data={"text": "A new note item"})
self.assertEqual(Item.objects.count(), 1) self.assertEqual(Item.objects.count(), 1)
new_item = Item.objects.get() new_item = Item.objects.get()
self.assertEqual(new_item.text, "A new list item") self.assertEqual(new_item.text, "A new note item")
def test_redirects_after_POST(self): def test_redirects_after_POST(self):
response = self.client.post("/dashboard/new_list", data={"text": "A new list item"}) response = self.client.post("/dashboard/new_note", data={"text": "A new note item"})
new_list = List.objects.get() new_note = Note.objects.get()
self.assertRedirects(response, f"/dashboard/list/{new_list.id}/") self.assertRedirects(response, f"/dashboard/note/{new_note.id}/")
# Post invalid input helper # Post invalid input helper
def post_invalid_input(self): def post_invalid_input(self):
return self.client.post("/dashboard/new_list", data={"text": ""}) return self.client.post("/dashboard/new_note", data={"text": ""})
def test_for_invalid_input_nothing_saved_to_db(self): def test_for_invalid_input_nothing_saved_to_db(self):
self.post_invalid_input() self.post_invalid_input()
self.assertEqual(Item.objects.count(), 0) self.assertEqual(Item.objects.count(), 0)
def test_for_invalid_input_renders_list_template(self): def test_for_invalid_input_renders_home_template(self):
response = self.post_invalid_input() response = self.post_invalid_input()
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "apps/dashboard/home.html") self.assertTemplateUsed(response, "apps/dashboard/home.html")
@@ -67,15 +67,15 @@ class NewListTest(TestCase):
self.assertContains(response, html.escape(EMPTY_ITEM_ERROR)) self.assertContains(response, html.escape(EMPTY_ITEM_ERROR))
@override_settings(COMPRESS_ENABLED=False) @override_settings(COMPRESS_ENABLED=False)
class ListViewTest(TestCase): class NoteViewTest(TestCase):
def test_uses_list_template(self): def test_uses_note_template(self):
mylist = List.objects.create() mynote = Note.objects.create()
response = self.client.get(f"/dashboard/list/{mylist.id}/") response = self.client.get(f"/dashboard/note/{mynote.id}/")
self.assertTemplateUsed(response, "apps/dashboard/list.html") self.assertTemplateUsed(response, "apps/dashboard/note.html")
def test_renders_input_form(self): def test_renders_input_form(self):
mylist = List.objects.create() mynote = Note.objects.create()
url = f"/dashboard/list/{mylist.id}/" url = f"/dashboard/note/{mynote.id}/"
response = self.client.get(url) response = self.client.get(url)
parsed = lxml.html.fromstring(response.content) parsed = lxml.html.fromstring(response.content)
forms = parsed.cssselect("form[method=POST]") forms = parsed.cssselect("form[method=POST]")
@@ -84,58 +84,58 @@ class ListViewTest(TestCase):
inputs = form.cssselect("input") inputs = form.cssselect("input")
self.assertIn("text", [input.get("name") for input in inputs]) self.assertIn("text", [input.get("name") for input in inputs])
def test_displays_only_items_for_that_list(self): def test_displays_only_items_for_that_note(self):
# Given/Arrange # Given/Arrange
correct_list = List.objects.create() correct_note = Note.objects.create()
Item.objects.create(text="itemey 1", list=correct_list) Item.objects.create(text="itemey 1", note=correct_note)
Item.objects.create(text="itemey 2", list=correct_list) Item.objects.create(text="itemey 2", note=correct_note)
other_list = List.objects.create() other_note = Note.objects.create()
Item.objects.create(text="other list item", list=other_list) Item.objects.create(text="other note item", note=other_note)
# When/Act # When/Act
response = self.client.get(f"/dashboard/list/{correct_list.id}/") response = self.client.get(f"/dashboard/note/{correct_note.id}/")
# Then/Assert # Then/Assert
self.assertContains(response, "itemey 1") self.assertContains(response, "itemey 1")
self.assertContains(response, "itemey 2") self.assertContains(response, "itemey 2")
self.assertNotContains(response, "other list item") self.assertNotContains(response, "other note item")
def test_can_save_a_POST_request_to_an_existing_list(self): def test_can_save_a_POST_request_to_an_existing_note(self):
other_list = List.objects.create() other_note = Note.objects.create()
correct_list = List.objects.create() correct_note = Note.objects.create()
self.client.post( self.client.post(
f"/dashboard/list/{correct_list.id}/", f"/dashboard/note/{correct_note.id}/",
data={"text": "A new item for an existing list"}, data={"text": "A new item for an existing note"},
) )
self.assertEqual(Item.objects.count(), 1) self.assertEqual(Item.objects.count(), 1)
new_item = Item.objects.get() new_item = Item.objects.get()
self.assertEqual(new_item.text, "A new item for an existing list") self.assertEqual(new_item.text, "A new item for an existing note")
self.assertEqual(new_item.list, correct_list) self.assertEqual(new_item.note, correct_note)
def test_POST_redirects_to_list_view(self): def test_POST_redirects_to_note_view(self):
other_list = List.objects.create() other_note = Note.objects.create()
correct_list = List.objects.create() correct_note = Note.objects.create()
response = self.client.post( response = self.client.post(
f"/dashboard/list/{correct_list.id}/", f"/dashboard/note/{correct_note.id}/",
data={"text": "A new item for an existing list"}, data={"text": "A new item for an existing note"},
) )
self.assertRedirects(response, f"/dashboard/list/{correct_list.id}/") self.assertRedirects(response, f"/dashboard/note/{correct_note.id}/")
# Post invalid input helper # Post invalid input helper
def post_invalid_input(self): def post_invalid_input(self):
mylist = List.objects.create() mynote = Note.objects.create()
return self.client.post(f"/dashboard/list/{mylist.id}/", data={"text": ""}) return self.client.post(f"/dashboard/note/{mynote.id}/", data={"text": ""})
def test_for_invalid_input_nothing_saved_to_db(self): def test_for_invalid_input_nothing_saved_to_db(self):
self.post_invalid_input() self.post_invalid_input()
self.assertEqual(Item.objects.count(), 0) self.assertEqual(Item.objects.count(), 0)
def test_for_invalid_input_renders_list_template(self): def test_for_invalid_input_renders_note_template(self):
response = self.post_invalid_input() response = self.post_invalid_input()
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "apps/dashboard/list.html") self.assertTemplateUsed(response, "apps/dashboard/note.html")
def test_for_invalid_input_shows_error_on_page(self): def test_for_invalid_input_shows_error_on_page(self):
response = self.post_invalid_input() response = self.post_invalid_input()
@@ -147,26 +147,26 @@ class ListViewTest(TestCase):
[input] = parsed.cssselect("input[name=text]") [input] = parsed.cssselect("input[name=text]")
self.assertIn("is-invalid", set(input.classes)) self.assertIn("is-invalid", set(input.classes))
def test_duplicate_item_validation_errors_end_up_on_lists_page(self): def test_duplicate_item_validation_errors_end_up_on_note_page(self):
list1 = List.objects.create() note1 = Note.objects.create()
Item.objects.create(list=list1, text="lorem ipsum") Item.objects.create(note=note1, text="lorem ipsum")
response = self.client.post( response = self.client.post(
f"/dashboard/list/{list1.id}/", f"/dashboard/note/{note1.id}/",
data={"text": "lorem ipsum"}, data={"text": "lorem ipsum"},
) )
expected_error = html.escape(DUPLICATE_ITEM_ERROR) expected_error = html.escape(DUPLICATE_ITEM_ERROR)
self.assertContains(response, expected_error) self.assertContains(response, expected_error)
self.assertTemplateUsed(response, "apps/dashboard/list.html") self.assertTemplateUsed(response, "apps/dashboard/note.html")
self.assertEqual(Item.objects.all().count(), 1) self.assertEqual(Item.objects.all().count(), 1)
class MyListsTest(TestCase): class MyNotesTest(TestCase):
def test_my_lists_url_renders_my_lists_template(self): def test_my_notes_url_renders_my_notes_template(self):
user = User.objects.create(email="a@b.cde") user = User.objects.create(email="a@b.cde")
self.client.force_login(user) self.client.force_login(user)
response = self.client.get(f"/dashboard/users/{user.id}/") response = self.client.get(f"/dashboard/users/{user.id}/")
self.assertTemplateUsed(response, "apps/dashboard/my_lists.html") self.assertTemplateUsed(response, "apps/dashboard/my_notes.html")
def test_passes_correct_owner_to_template(self): def test_passes_correct_owner_to_template(self):
User.objects.create(email="wrongowner@example.com") User.objects.create(email="wrongowner@example.com")
@@ -175,20 +175,20 @@ class MyListsTest(TestCase):
response = self.client.get(f"/dashboard/users/{correct_user.id}/") response = self.client.get(f"/dashboard/users/{correct_user.id}/")
self.assertEqual(response.context["owner"], correct_user) self.assertEqual(response.context["owner"], correct_user)
def test_list_owner_is_saved_if_user_is_authenticated(self): def test_note_owner_is_saved_if_user_is_authenticated(self):
user = User.objects.create(email="a@b.cde") user = User.objects.create(email="a@b.cde")
self.client.force_login(user) self.client.force_login(user)
self.client.post("/dashboard/new_list", data={"text": "new item"}) self.client.post("/dashboard/new_note", data={"text": "new item"})
new_list = List.objects.get() new_note = Note.objects.get()
self.assertEqual(new_list.owner, user) self.assertEqual(new_note.owner, user)
def test_my_lists_redirects_if_not_logged_in(self): def test_my_notes_redirects_if_not_logged_in(self):
user = User.objects.create(email="a@b.cde") user = User.objects.create(email="a@b.cde")
response = self.client.get(f"/dashboard/users/{user.id}/") response = self.client.get(f"/dashboard/users/{user.id}/")
self.assertRedirects(response, "/") self.assertRedirects(response, "/")
def test_my_lists_returns_403_for_wrong_user(self): def test_my_notes_returns_403_for_wrong_user(self):
# create two users, login as user_a, request user_b's my_lists url # create two users, login as user_a, request user_b's my_notes url
user1 = User.objects.create(email="a@b.cde") user1 = User.objects.create(email="a@b.cde")
user2 = User.objects.create(email="wrongowner@example.com") user2 = User.objects.create(email="wrongowner@example.com")
self.client.force_login(user2) self.client.force_login(user2)
@@ -196,50 +196,50 @@ class MyListsTest(TestCase):
# assert 403 # assert 403
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
class ShareListTest(TestCase): class ShareNoteTest(TestCase):
def test_post_to_share_list_url_redirects_to_list(self): def test_post_to_share_note_url_redirects_to_note(self):
our_list = List.objects.create() our_note = Note.objects.create()
alice = User.objects.create(email="alice@example.com") alice = User.objects.create(email="alice@example.com")
response = self.client.post( response = self.client.post(
f"/dashboard/list/{our_list.id}/share_list", f"/dashboard/note/{our_note.id}/share_note",
data={"recipient": "alice@example.com"}, data={"recipient": "alice@example.com"},
) )
self.assertRedirects(response, f"/dashboard/list/{our_list.id}/") self.assertRedirects(response, f"/dashboard/note/{our_note.id}/")
def test_post_with_email_adds_user_to_shared_with(self): def test_post_with_email_adds_user_to_shared_with(self):
our_list = List.objects.create() our_note = Note.objects.create()
alice = User.objects.create(email="alice@example.com") alice = User.objects.create(email="alice@example.com")
self.client.post( self.client.post(
f"/dashboard/list/{our_list.id}/share_list", f"/dashboard/note/{our_note.id}/share_note",
data={"recipient": "alice@example.com"}, data={"recipient": "alice@example.com"},
) )
self.assertIn(alice, our_list.shared_with.all()) self.assertIn(alice, our_note.shared_with.all())
def test_post_with_nonexistent_email_redirects_to_list(self): def test_post_with_nonexistent_email_redirects_to_note(self):
our_list = List.objects.create() our_note = Note.objects.create()
response = self.client.post( response = self.client.post(
f"/dashboard/list/{our_list.id}/share_list", f"/dashboard/note/{our_note.id}/share_note",
data={"recipient": "nobody@example.com"}, data={"recipient": "nobody@example.com"},
) )
self.assertRedirects( self.assertRedirects(
response, response,
f"/dashboard/list/{our_list.id}/", f"/dashboard/note/{our_note.id}/",
fetch_redirect_response=False, fetch_redirect_response=False,
) )
def test_share_list_does_not_add_owner_as_recipient(self): def test_share_note_does_not_add_owner_as_recipient(self):
owner = User.objects.create(email="owner@example.com") owner = User.objects.create(email="owner@example.com")
our_list = List.objects.create(owner=owner) our_note = Note.objects.create(owner=owner)
self.client.force_login(owner) self.client.force_login(owner)
self.client.post(reverse("share_list", args=[our_list.id]), self.client.post(reverse("share_note", args=[our_note.id]),
data={"recipient": "owner@example.com"}) data={"recipient": "owner@example.com"})
self.assertNotIn(owner, our_list.shared_with.all()) self.assertNotIn(owner, our_note.shared_with.all())
@override_settings(MESSAGE_STORAGE='django.contrib.messages.storage.session.SessionStorage') @override_settings(MESSAGE_STORAGE='django.contrib.messages.storage.session.SessionStorage')
def test_share_list_shows_privacy_safe_message(self): def test_share_note_shows_privacy_safe_message(self):
our_list = List.objects.create() our_note = Note.objects.create()
response = self.client.post( response = self.client.post(
f"/dashboard/list/{our_list.id}/share_list", f"/dashboard/note/{our_note.id}/share_note",
data={"recipient": "nobody@example.com"}, data={"recipient": "nobody@example.com"},
follow=True, follow=True,
) )
@@ -249,26 +249,26 @@ class ShareListTest(TestCase):
"An invite has been sent if that address is registered.", "An invite has been sent if that address is registered.",
) )
class ViewAuthListTest(TestCase): class ViewAuthNoteTest(TestCase):
def setUp(self): def setUp(self):
self.owner = User.objects.create(email="disco@example.com") self.owner = User.objects.create(email="disco@example.com")
self.our_list = List.objects.create(owner=self.owner) self.our_note = Note.objects.create(owner=self.owner)
def test_anonymous_user_is_redirected(self): def test_anonymous_user_is_redirected(self):
response = self.client.get(reverse("view_list", args=[self.our_list.id])) response = self.client.get(reverse("view_note", args=[self.our_note.id]))
self.assertRedirects(response, "/", fetch_redirect_response=False) self.assertRedirects(response, "/", fetch_redirect_response=False)
def test_non_owner_non_shared_user_gets_403(self): def test_non_owner_non_shared_user_gets_403(self):
stranger = User.objects.create(email="stranger@example.com") stranger = User.objects.create(email="stranger@example.com")
self.client.force_login(stranger) self.client.force_login(stranger)
response = self.client.get(reverse("view_list", args=[self.our_list.id])) response = self.client.get(reverse("view_note", args=[self.our_note.id]))
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
def test_shared_with_user_can_access_list(self): def test_shared_with_user_can_access_note(self):
guest = User.objects.create(email="guest@example.com") guest = User.objects.create(email="guest@example.com")
self.our_list.shared_with.add(guest) self.our_note.shared_with.add(guest)
self.client.force_login(guest) self.client.force_login(guest)
response = self.client.get(reverse("view_list", args=[self.our_list.id])) response = self.client.get(reverse("view_note", args=[self.our_note.id]))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@override_settings(COMPRESS_ENABLED=False) @override_settings(COMPRESS_ENABLED=False)
@@ -278,7 +278,7 @@ class SetPaletteTest(TestCase):
self.client.force_login(self.user) self.client.force_login(self.user)
self.url = reverse("home") self.url = reverse("home")
Applet.objects.get_or_create(slug="palette", defaults={"name": "Palette"}) Applet.objects.get_or_create(slug="palette", defaults={"name": "Palette"})
def test_anonymous_user_is_redirected_home(self): def test_anonymous_user_is_redirected_home(self):
response = self.client.post("/dashboard/set_palette") response = self.client.post("/dashboard/set_palette")
self.assertRedirects(response, "/", fetch_redirect_response=False) self.assertRedirects(response, "/", fetch_redirect_response=False)
@@ -299,7 +299,7 @@ class SetPaletteTest(TestCase):
response = self.client.post("/dashboard/set_palette", data={"palette": "palette-default"}) response = self.client.post("/dashboard/set_palette", data={"palette": "palette-default"})
self.assertRedirects(response, "/", fetch_redirect_response=False) self.assertRedirects(response, "/", fetch_redirect_response=False)
def test_my_lists_contains_set_palette_form(self): def test_dashboard_contains_set_palette_form(self):
response = self.client.get(self.url) response = self.client.get(self.url)
parsed = lxml.html.fromstring(response.content) parsed = lxml.html.fromstring(response.content)
forms = parsed.cssselect('form[action="/dashboard/set_palette"]') forms = parsed.cssselect('form[action="/dashboard/set_palette"]')
@@ -434,7 +434,7 @@ class FooterNavTest(TestCase):
parsed = lxml.html.fromstring(response.content) parsed = lxml.html.fromstring(response.content)
[nav] = parsed.cssselect("#id_footer_nav") [nav] = parsed.cssselect("#id_footer_nav")
self.assertIsNotNone(nav) self.assertIsNotNone(nav)
def test_footer_nav_has_dashboard_link(self): def test_footer_nav_has_dashboard_link(self):
response = self.client.get("/") response = self.client.get("/")
parsed = lxml.html.fromstring(response.content) parsed = lxml.html.fromstring(response.content)
@@ -447,7 +447,7 @@ class FooterNavTest(TestCase):
parsed = lxml.html.fromstring(response.content) parsed = lxml.html.fromstring(response.content)
[nav] = parsed.cssselect("#id_footer_nav") [nav] = parsed.cssselect("#id_footer_nav")
links = [a.get("href") for a in nav.cssselect("a")] links = [a.get("href") for a in nav.cssselect("a")]
self.assertIn("/gameboard/", links) self.assertIn("/gameboard/", links)
class WalletAppletTest(TestCase): class WalletAppletTest(TestCase):
def setUp(self): def setUp(self):

View File

@@ -2,12 +2,12 @@ from django.urls import path
from . import views from . import views
urlpatterns = [ urlpatterns = [
path('new_list', views.new_list, name='new_list'), path('new_note', views.new_note, name='new_note'),
path('list/<uuid:list_id>/', views.view_list, name='view_list'), path('note/<uuid:note_id>/', views.view_note, name='view_note'),
path('list/<uuid:list_id>/share_list', views.share_list, name="share_list"), path('note/<uuid:note_id>/share_note', views.share_note, name="share_note"),
path('set_palette', views.set_palette, name='set_palette'), path('set_palette', views.set_palette, name='set_palette'),
path('set_profile', views.set_profile, name='set_profile'), path('set_profile', views.set_profile, name='set_profile'),
path('users/<uuid:user_id>/', views.my_lists, name='my_lists'), path('users/<uuid:user_id>/', views.my_notes, name='my_notes'),
path('toggle_applets', views.toggle_applets, name="toggle_applets"), path('toggle_applets', views.toggle_applets, name="toggle_applets"),
path('wallet/', views.wallet, name='wallet'), path('wallet/', views.wallet, name='wallet'),
path('wallet/toggle-applets', views.toggle_wallet_applets, name='toggle_wallet_applets'), path('wallet/toggle-applets', views.toggle_wallet_applets, name='toggle_wallet_applets'),

View File

@@ -10,12 +10,12 @@ from django.views.decorators.csrf import ensure_csrf_cookie
from apps.applets.models import Applet, UserApplet from apps.applets.models import Applet, UserApplet
from apps.applets.utils import applet_context from apps.applets.utils import applet_context
from apps.dashboard.forms import ExistingListItemForm, ItemForm from apps.dashboard.forms import ExistingNoteItemForm, ItemForm
from apps.dashboard.models import Item, List from apps.dashboard.models import Item, Note
from apps.lyric.models import PaymentMethod, Token, User, Wallet from apps.lyric.models import PaymentMethod, Token, User, Wallet
APPLET_ORDER = ["wallet", "new-list", "my-lists", "username", "palette"] APPLET_ORDER = ["wallet", "new-note", "my-notes", "username", "palette"]
UNLOCKED_PALETTES = frozenset(["palette-default"]) UNLOCKED_PALETTES = frozenset(["palette-default"])
PALETTES = [ PALETTES = [
{"name": "palette-default", "label": "Earthman", "locked": False}, {"name": "palette-default", "label": "Earthman", "locked": False},
@@ -27,9 +27,9 @@ PALETTES = [
] ]
def _recent_lists(user, limit=3): def _recent_notes(user, limit=3):
return ( return (
List Note
.objects .objects
.filter(Q(owner=user) | Q(shared_with=user)) .filter(Q(owner=user) | Q(shared_with=user))
.annotate(last_item=Max('item__id')) .annotate(last_item=Max('item__id'))
@@ -45,18 +45,18 @@ def home_page(request):
} }
if request.user.is_authenticated: if request.user.is_authenticated:
context["applets"] = applet_context(request.user, "dashboard") context["applets"] = applet_context(request.user, "dashboard")
context["recent_lists"] = _recent_lists(request.user) context["recent_notes"] = _recent_notes(request.user)
return render(request, "apps/dashboard/home.html", context) return render(request, "apps/dashboard/home.html", context)
def new_list(request): def new_note(request):
form = ItemForm(data=request.POST) form = ItemForm(data=request.POST)
if form.is_valid(): if form.is_valid():
nulist = List.objects.create() nunote = Note.objects.create()
if request.user.is_authenticated: if request.user.is_authenticated:
nulist.owner = request.user nunote.owner = request.user
nulist.save() nunote.save()
form.save(for_list=nulist) form.save(for_note=nunote)
return redirect(nulist) return redirect(nunote)
else: else:
context = { context = {
"form": form, "form": form,
@@ -65,46 +65,46 @@ def new_list(request):
} }
if request.user.is_authenticated: if request.user.is_authenticated:
context["applets"] = applet_context(request.user, "dashboard") context["applets"] = applet_context(request.user, "dashboard")
context["recent_lists"] = _recent_lists(request.user) context["recent_notes"] = _recent_notes(request.user)
return render(request, "apps/dashboard/home.html", context) return render(request, "apps/dashboard/home.html", context)
def view_list(request, list_id): def view_note(request, note_id):
our_list = List.objects.get(id=list_id) our_note = Note.objects.get(id=note_id)
if our_list.owner: if our_note.owner:
if not request.user.is_authenticated: if not request.user.is_authenticated:
return redirect("/") return redirect("/")
if request.user != our_list.owner and request.user not in our_list.shared_with.all(): if request.user != our_note.owner and request.user not in our_note.shared_with.all():
return HttpResponseForbidden() return HttpResponseForbidden()
form = ExistingListItemForm(for_list=our_list) form = ExistingNoteItemForm(for_note=our_note)
if request.method == "POST": if request.method == "POST":
form = ExistingListItemForm(for_list=our_list, data=request.POST) form = ExistingNoteItemForm(for_note=our_note, data=request.POST)
if form.is_valid(): if form.is_valid():
form.save() form.save()
return redirect(our_list) return redirect(our_note)
return render(request, "apps/dashboard/list.html", {"list": our_list, "form": form}) return render(request, "apps/dashboard/note.html", {"note": our_note, "form": form})
def my_lists(request, user_id): def my_notes(request, user_id):
owner = User.objects.get(id=user_id) owner = User.objects.get(id=user_id)
if not request.user.is_authenticated: if not request.user.is_authenticated:
return redirect("/") return redirect("/")
if request.user.id != owner.id: if request.user.id != owner.id:
return HttpResponseForbidden() return HttpResponseForbidden()
return render(request, "apps/dashboard/my_lists.html", {"owner": owner}) return render(request, "apps/dashboard/my_notes.html", {"owner": owner})
def share_list(request, list_id): def share_note(request, note_id):
our_list = List.objects.get(id=list_id) our_note = Note.objects.get(id=note_id)
try: try:
recipient = User.objects.get(email=request.POST["recipient"]) recipient = User.objects.get(email=request.POST["recipient"])
if recipient == request.user: if recipient == request.user:
return redirect(our_list) return redirect(our_note)
our_list.shared_with.add(recipient) our_note.shared_with.add(recipient)
except User.DoesNotExist: except User.DoesNotExist:
pass pass
messages.success(request, "An invite has been sent if that address is registered.") messages.success(request, "An invite has been sent if that address is registered.")
return redirect(our_list) return redirect(our_note)
@login_required(login_url="/") @login_required(login_url="/")
def set_palette(request): def set_palette(request):
@@ -137,7 +137,7 @@ def toggle_applets(request):
"applets": applet_context(request.user, "dashboard"), "applets": applet_context(request.user, "dashboard"),
"palettes": PALETTES, "palettes": PALETTES,
"form": ItemForm(), "form": ItemForm(),
"recent_lists": _recent_lists(request.user), "recent_notes": _recent_notes(request.user),
}) })
return redirect("home") return redirect("home")

View File

@@ -47,7 +47,7 @@ class FunctionalTest(StaticLiveServerTestCase):
if self.test_server: if self.test_server:
self.live_server_url = 'http://' + self.test_server self.live_server_url = 'http://' + self.test_server
reset_database(self.test_server) reset_database(self.test_server)
Applet.objects.get_or_create(slug="new-list", defaults={"name": "New List"}) Applet.objects.get_or_create(slug="new-note", defaults={"name": "New Note"})
def tearDown(self): def tearDown(self):
if self._test_has_failed(): if self._test_has_failed():

View File

@@ -3,11 +3,11 @@ from selenium.webdriver.common.by import By
from apps.lyric.models import User from apps.lyric.models import User
class MyListsPage: class MyNotesPage:
def __init__(self, test): def __init__(self, test):
self.test = test self.test = test
def go_to_my_lists_page(self, email): def go_to_my_notes_page(self, email):
self.test.browser.get(self.test.live_server_url) self.test.browser.get(self.test.live_server_url)
user = User.objects.get(email=email) user = User.objects.get(email=email)
self.test.browser.get( self.test.browser.get(
@@ -16,7 +16,7 @@ class MyListsPage:
self.test.wait_for( self.test.wait_for(
lambda: self.test.assertIn( lambda: self.test.assertIn(
email, email,
self.test.browser.find_element(By.TAG_NAME, "h2").text.lower(), self.test.browser.find_element(By.CSS_SELECTOR, ".navbar-identity").text.lower(),
) )
) )
return self return self

View File

@@ -4,42 +4,42 @@ from selenium.webdriver.common.keys import Keys
from .base import wait from .base import wait
class ListPage: class NotePage:
def __init__(self, test): def __init__(self, test):
self.test = test self.test = test
def get_table_rows(self): def get_table_rows(self):
return self.test.browser.find_elements(By.CSS_SELECTOR, "#id_list_table tr") return self.test.browser.find_elements(By.CSS_SELECTOR, "#id_note_table tr")
@wait @wait
def wait_for_row_in_list_table(self, item_text, item_number): def wait_for_row_in_note_table(self, item_text, item_number):
expected_row_text = f"{item_number}. {item_text}" expected_row_text = f"{item_number}. {item_text}"
rows = self.get_table_rows() rows = self.get_table_rows()
self.test.assertIn(expected_row_text, [row.text for row in rows]) self.test.assertIn(expected_row_text, [row.text for row in rows])
def get_item_input_box(self): def get_item_input_box(self):
return self.test.browser.find_element(By.ID, "id_text") return self.test.browser.find_element(By.ID, "id_text")
def add_list_item(self, item_text): def add_note_item(self, item_text):
new_item_no = len(self.get_table_rows()) + 1 new_item_no = len(self.get_table_rows()) + 1
self.get_item_input_box().send_keys(item_text) self.get_item_input_box().send_keys(item_text)
self.get_item_input_box().send_keys(Keys.ENTER) self.get_item_input_box().send_keys(Keys.ENTER)
self.wait_for_row_in_list_table(item_text, new_item_no) self.wait_for_row_in_note_table(item_text, new_item_no)
return self return self
def get_share_box(self): def get_share_box(self):
return self.test.browser.find_element( return self.test.browser.find_element(
By.CSS_SELECTOR, By.CSS_SELECTOR,
'input[name="recipient"]', 'input[name="recipient"]',
) )
def get_shared_with_list(self): def get_shared_with_list(self):
return self.test.browser.find_elements( return self.test.browser.find_elements(
By.CSS_SELECTOR, By.CSS_SELECTOR,
".list-recipient" ".note-recipient"
) )
def share_list_with(self, email): def share_note_with(self, email):
self.get_share_box().send_keys(email) self.get_share_box().send_keys(email)
self.get_share_box().send_keys(Keys.ENTER) self.get_share_box().send_keys(Keys.ENTER)
self.test.wait_for( self.test.wait_for(
@@ -48,5 +48,5 @@ class ListPage:
) )
) )
def get_list_owner(self): def get_note_owner(self):
return self.test.browser.find_element(By.ID, "id_list_owner").text return self.test.browser.find_element(By.ID, "id_note_owner").text

View File

@@ -9,7 +9,7 @@ from apps.applets.models import Applet
class DashboardMaintenanceTest(FunctionalTest): class DashboardMaintenanceTest(FunctionalTest):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
Applet.objects.get_or_create(slug="new-list", defaults={"name": "New List"}) Applet.objects.get_or_create(slug="new-note", defaults={"name": "New Note"})
Applet.objects.get_or_create(slug="username", defaults={"name": "Username"}) Applet.objects.get_or_create(slug="username", defaults={"name": "Username"})
Applet.objects.get_or_create(slug="palette", defaults={"name": "Palette"}) Applet.objects.get_or_create(slug="palette", defaults={"name": "Palette"})

View File

@@ -1,25 +1,25 @@
from .base import FunctionalTest from .base import FunctionalTest
from .list_page import ListPage from .note_page import NotePage
class LayoutAndStylingTest(FunctionalTest): class LayoutAndStylingTest(FunctionalTest):
def test_layout_and_styling(self): def test_layout_and_styling(self):
self.create_pre_authenticated_session("disco@test.io") self.create_pre_authenticated_session("disco@test.io")
self.browser.get(self.live_server_url) self.browser.get(self.live_server_url)
list_page = ListPage(self) note_page = NotePage(self)
self.browser.set_window_size(1024, 768) self.browser.set_window_size(1024, 768)
# print("Viewport width:", self.browser.execute_script("return window.innerWidth")) # print("Viewport width:", self.browser.execute_script("return window.innerWidth"))
inputbox = list_page.get_item_input_box() inputbox = note_page.get_item_input_box()
self.assertAlmostEqual( self.assertAlmostEqual(
inputbox.location['x'] + inputbox.size['width'] / 2, inputbox.location['x'] + inputbox.size['width'] / 2,
512, 512,
delta=10, delta=10,
) )
list_page.add_list_item("testing") note_page.add_note_item("testing")
inputbox = list_page.get_item_input_box() inputbox = note_page.get_item_input_box()
self.assertAlmostEqual( self.assertAlmostEqual(
inputbox.location['x'] + inputbox.size['width'] / 2, inputbox.location['x'] + inputbox.size['width'] / 2,
512, 512,

View File

@@ -1,36 +1,36 @@
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from .base import FunctionalTest from .base import FunctionalTest
from .list_page import ListPage from .note_page import NotePage
from .my_lists_page import MyListsPage from .my_notes_page import MyNotesPage
class MyListsTest(FunctionalTest): class MyNotesTest(FunctionalTest):
def test_logged_in_users_lists_are_saved_as_my_lists(self): def test_logged_in_users_notes_are_saved_as_my_notes(self):
self.create_pre_authenticated_session("disco@test.io") self.create_pre_authenticated_session("disco@test.io")
self.browser.get(self.live_server_url) self.browser.get(self.live_server_url)
list_page = ListPage(self) note_page = NotePage(self)
list_page.add_list_item("Reticulate splines") note_page.add_note_item("Reticulate splines")
list_page.add_list_item("Regurgitate spines") note_page.add_note_item("Regurgitate spines")
first_list_url = self.browser.current_url first_note_url = self.browser.current_url
MyListsPage(self).go_to_my_lists_page("disco@test.io") MyNotesPage(self).go_to_my_notes_page("disco@test.io")
self.wait_for( self.wait_for(
lambda: self.browser.find_element(By.LINK_TEXT, "Reticulate splines") lambda: self.browser.find_element(By.LINK_TEXT, "Reticulate splines")
) )
self.browser.find_element(By.LINK_TEXT, "Reticulate splines").click() self.browser.find_element(By.LINK_TEXT, "Reticulate splines").click()
self.wait_for( self.wait_for(
lambda: self.assertEqual(self.browser.current_url, first_list_url) lambda: self.assertEqual(self.browser.current_url, first_note_url)
) )
self.browser.get(self.live_server_url) self.browser.get(self.live_server_url)
list_page.add_list_item("Ribbon of death") note_page.add_note_item("Ribbon of death")
second_list_url = self.browser.current_url second_note_url = self.browser.current_url
MyListsPage(self).go_to_my_lists_page("disco@test.io") MyNotesPage(self).go_to_my_notes_page("disco@test.io")
self.wait_for( self.wait_for(
lambda: self.browser.find_element(By.LINK_TEXT, "Ribbon of death") lambda: self.browser.find_element(By.LINK_TEXT, "Ribbon of death")
) )
@@ -38,7 +38,7 @@ class MyListsTest(FunctionalTest):
self.browser.find_element(By.CSS_SELECTOR, "#id_logout").click() self.browser.find_element(By.CSS_SELECTOR, "#id_logout").click()
self.wait_for( self.wait_for(
lambda: self.assertEqual( lambda: self.assertEqual(
self.browser.find_elements(By.LINK_TEXT, "My lists"), self.browser.find_elements(By.LINK_TEXT, "My notes"),
[], [],
) )
) )

View File

@@ -2,7 +2,7 @@ from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.keys import Keys
from .base import FunctionalTest from .base import FunctionalTest
from .list_page import ListPage from .note_page import NotePage
class ItemValidationTest(FunctionalTest): class ItemValidationTest(FunctionalTest):
@@ -11,69 +11,69 @@ class ItemValidationTest(FunctionalTest):
return self.browser.find_element(By.CSS_SELECTOR, ".invalid-feedback") return self.browser.find_element(By.CSS_SELECTOR, ".invalid-feedback")
# Test methods # Test methods
def test_cannot_add_empty_list_items(self): def test_cannot_add_empty_note_items(self):
self.create_pre_authenticated_session("disco@test.io") self.create_pre_authenticated_session("disco@test.io")
self.browser.get(self.live_server_url) self.browser.get(self.live_server_url)
list_page = ListPage(self) note_page = NotePage(self)
list_page.get_item_input_box().send_keys(Keys.ENTER) note_page.get_item_input_box().send_keys(Keys.ENTER)
self.wait_for( self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_text:invalid") lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_text:invalid")
) )
list_page.get_item_input_box().send_keys("Purchase milk") note_page.get_item_input_box().send_keys("Purchase milk")
self.wait_for( self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_text:valid") lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_text:valid")
) )
list_page.get_item_input_box().send_keys(Keys.ENTER) note_page.get_item_input_box().send_keys(Keys.ENTER)
list_page.wait_for_row_in_list_table("Purchase milk", 1) note_page.wait_for_row_in_note_table("Purchase milk", 1)
list_page.get_item_input_box().send_keys(Keys.ENTER) note_page.get_item_input_box().send_keys(Keys.ENTER)
list_page.wait_for_row_in_list_table("Purchase milk", 1) note_page.wait_for_row_in_note_table("Purchase milk", 1)
self.wait_for( self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_text:invalid") lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_text:invalid")
) )
list_page.get_item_input_box().send_keys("Make tea") note_page.get_item_input_box().send_keys("Make tea")
self.wait_for( self.wait_for(
lambda: self.browser.find_element( lambda: self.browser.find_element(
By.CSS_SELECTOR, By.CSS_SELECTOR,
"#id_text:valid", "#id_text:valid",
) )
) )
list_page.get_item_input_box().send_keys(Keys.ENTER) note_page.get_item_input_box().send_keys(Keys.ENTER)
list_page.wait_for_row_in_list_table("Make tea", 2) note_page.wait_for_row_in_note_table("Make tea", 2)
def test_cannot_add_duplicate_items(self): def test_cannot_add_duplicate_items(self):
self.create_pre_authenticated_session("disco@test.io") self.create_pre_authenticated_session("disco@test.io")
self.browser.get(self.live_server_url) self.browser.get(self.live_server_url)
list_page = ListPage(self) note_page = NotePage(self)
list_page.add_list_item("Witness divinity") note_page.add_note_item("Witness divinity")
list_page.get_item_input_box().send_keys("Witness divinity") note_page.get_item_input_box().send_keys("Witness divinity")
list_page.get_item_input_box().send_keys(Keys.ENTER) note_page.get_item_input_box().send_keys(Keys.ENTER)
self.wait_for( self.wait_for(
lambda: self.assertEqual( lambda: self.assertEqual(
self.get_error_element().text, self.get_error_element().text,
"You've already logged this to your list", "You've already logged this to your note",
) )
) )
def test_error_messages_are_cleared_on_input(self): def test_error_messages_are_cleared_on_input(self):
self.create_pre_authenticated_session("disco@test.io") self.create_pre_authenticated_session("disco@test.io")
self.browser.get(self.live_server_url) self.browser.get(self.live_server_url)
list_page = ListPage(self) note_page = NotePage(self)
list_page.add_list_item("Gobbledygook") note_page.add_note_item("Gobbledygook")
list_page.get_item_input_box().send_keys("Gobbledygook") note_page.get_item_input_box().send_keys("Gobbledygook")
list_page.get_item_input_box().send_keys(Keys.ENTER) note_page.get_item_input_box().send_keys(Keys.ENTER)
self.wait_for( self.wait_for(
lambda: self.assertTrue(self.get_error_element().is_displayed()) lambda: self.assertTrue(self.get_error_element().is_displayed())
) )
list_page.get_item_input_box().send_keys("a") note_page.get_item_input_box().send_keys("a")
self.wait_for( self.wait_for(
lambda: self.assertFalse(self.get_error_element().is_displayed()) lambda: self.assertFalse(self.get_error_element().is_displayed())

View File

@@ -5,8 +5,8 @@ from selenium import webdriver
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from .base import FunctionalTest from .base import FunctionalTest
from .list_page import ListPage from .note_page import NotePage
from .my_lists_page import MyListsPage from .my_notes_page import MyNotesPage
# Helper fns # Helper fns
@@ -19,7 +19,7 @@ def quit_if_possible(browser):
# Test mdls # Test mdls
class SharingTest(FunctionalTest): class SharingTest(FunctionalTest):
def test_can_share_a_list_with_another_user(self): def test_can_share_a_note_with_another_user(self):
self.create_pre_authenticated_session("disco@test.io") self.create_pre_authenticated_session("disco@test.io")
disco_browser = self.browser disco_browser = self.browser
self.addCleanup(lambda: quit_if_possible(disco_browser)) self.addCleanup(lambda: quit_if_possible(disco_browser))
@@ -34,40 +34,39 @@ class SharingTest(FunctionalTest):
self.browser = disco_browser self.browser = disco_browser
self.browser.get(self.live_server_url) self.browser.get(self.live_server_url)
list_page = ListPage(self).add_list_item("Send help") note_page = NotePage(self).add_note_item("Send help")
share_box = list_page.get_share_box() share_box = note_page.get_share_box()
self.assertEqual( self.assertEqual(
share_box.get_attribute("placeholder"), share_box.get_attribute("placeholder"),
"friend@example.com", "friend@example.com",
) )
list_page.share_list_with("alice@test.io") note_page.share_note_with("alice@test.io")
self.browser = ali_browser self.browser = ali_browser
MyListsPage(self).go_to_my_lists_page("alice@test.io") MyNotesPage(self).go_to_my_notes_page("alice@test.io")
self.browser.find_element(By.LINK_TEXT, "Send help").click() self.browser.find_element(By.LINK_TEXT, "Send help").click()
self.wait_for( self.wait_for(
lambda: self.assertEqual(list_page.get_list_owner(), "disco@test.io") lambda: self.assertEqual(note_page.get_note_owner(), "disco@test.io")
) )
list_page.add_list_item("At your command, Disco King") note_page.add_note_item("At your command, Disco King")
self.browser = disco_browser self.browser = disco_browser
self.browser.refresh() self.browser.refresh()
list_page.wait_for_row_in_list_table("At your command, Disco King", 2) note_page.wait_for_row_in_note_table("At your command, Disco King", 2)
class ListAccessTest(FunctionalTest): class NoteAccessTest(FunctionalTest):
def test_stranger_cannot_access_owned_list(self): def test_stranger_cannot_access_owned_note(self):
self.create_pre_authenticated_session("disco@test.io") self.create_pre_authenticated_session("disco@test.io")
self.browser.get(self.live_server_url) self.browser.get(self.live_server_url)
list_page = ListPage(self).add_list_item("private eye") note_page = NotePage(self).add_note_item("private eye")
list_url = self.browser.current_url note_url = self.browser.current_url
self.browser.delete_cookie(settings.SESSION_COOKIE_NAME) self.browser.delete_cookie(settings.SESSION_COOKIE_NAME)
self.browser.get(list_url) self.browser.get(note_url)
self.assertNotEqual(self.browser.current_url, list_url)
self.assertNotEqual(self.browser.current_url, note_url)

View File

@@ -2,59 +2,59 @@ from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.keys import Keys
from .base import FunctionalTest from .base import FunctionalTest
from .list_page import ListPage from .note_page import NotePage
class NewVisitorTest(FunctionalTest): class NewVisitorTest(FunctionalTest):
# Test methods # Test methods
def test_can_start_a_todo_list(self): def test_can_start_a_note(self):
self.create_pre_authenticated_session("alice@test.io") self.create_pre_authenticated_session("alice@test.io")
self.browser.get(self.live_server_url) self.browser.get(self.live_server_url)
list_page = ListPage(self) note_page = NotePage(self)
self.assertIn('Earthman RPG', self.browser.title) self.assertIn('Earthman RPG', self.browser.title)
header_text = self.browser.find_element(By.TAG_NAME, 'h1').text header_text = self.browser.find_element(By.TAG_NAME, 'h1').text
self.assertIn('Welcome', header_text) self.assertIn('Welcome', header_text)
inputbox = list_page.get_item_input_box() inputbox = note_page.get_item_input_box()
self.assertEqual(inputbox.get_attribute('placeholder'), 'Enter a to-do item') self.assertEqual(inputbox.get_attribute('placeholder'), 'Enter a note item')
inputbox.send_keys('Buy peacock feathers') inputbox.send_keys('Buy peacock feathers')
inputbox.send_keys(Keys.ENTER) inputbox.send_keys(Keys.ENTER)
list_page.wait_for_row_in_list_table("Buy peacock feathers", 1) note_page.wait_for_row_in_note_table("Buy peacock feathers", 1)
list_page.add_list_item("Use peacock feathers to make a fly")
list_page.wait_for_row_in_list_table("Use peacock feathers to make a fly", 2)
list_page.wait_for_row_in_list_table("Buy peacock feathers", 1)
def test_multiple_users_can_start_lists_at_different_urls(self): note_page.add_note_item("Use peacock feathers to make a fly")
note_page.wait_for_row_in_note_table("Use peacock feathers to make a fly", 2)
note_page.wait_for_row_in_note_table("Buy peacock feathers", 1)
def test_multiple_users_can_start_notes_at_different_urls(self):
self.create_pre_authenticated_session("alice@test.io") self.create_pre_authenticated_session("alice@test.io")
self.browser.get(self.live_server_url) self.browser.get(self.live_server_url)
list_page = ListPage(self) note_page = NotePage(self)
list_page.add_list_item("Buy peacock feathers") note_page.add_note_item("Buy peacock feathers")
edith_dash_url = self.browser.current_url edith_dash_url = self.browser.current_url
self.assertRegex( self.assertRegex(
edith_dash_url, edith_dash_url,
r'/dashboard/list/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/$', r'/dashboard/note/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/$',
) )
self.browser.delete_all_cookies() self.browser.delete_all_cookies()
self.create_pre_authenticated_session("disco@test.io") self.create_pre_authenticated_session("disco@test.io")
self.browser.get(self.live_server_url) self.browser.get(self.live_server_url)
list_page = ListPage(self) note_page = NotePage(self)
page_text = self.browser.find_element(By.TAG_NAME, 'body').text page_text = self.browser.find_element(By.TAG_NAME, 'body').text
self.assertNotIn('Buy peacock feathers', page_text) self.assertNotIn('Buy peacock feathers', page_text)
list_page.add_list_item("Buy milk") note_page.add_note_item("Buy milk")
francis_dash_url = self.browser.current_url francis_dash_url = self.browser.current_url
self.assertRegex( self.assertRegex(
francis_dash_url, francis_dash_url,
r'/dashboard/list/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/$', r'/dashboard/note/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/$',
) )
self.assertNotEqual(francis_dash_url, edith_dash_url) self.assertNotEqual(francis_dash_url, edith_dash_url)

View File

@@ -105,6 +105,7 @@
inset -0.125rem -0.125rem 0 rgba(var(--ninUser), 0.125), inset -0.125rem -0.125rem 0 rgba(var(--ninUser), 0.125),
inset 0.125rem 0.125rem 0 rgba(0, 0, 0, 0.8) inset 0.125rem 0.125rem 0 rgba(0, 0, 0, 0.8)
; ;
background-color: rgba(0, 0, 0, 0.125);
border-radius: 0.75rem; border-radius: 0.75rem;
padding: 1rem; padding: 1rem;
overflow: hidden; overflow: hidden;

View File

@@ -30,16 +30,14 @@ body.page-dashboard {
} }
#id_applets_container { #id_applets_container {
#id_applet_my_lists { #id_applet_my_notes {
padding: 1.25rem 1.5rem; padding: 1.25rem 1.5rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.my-lists-main { h2 { flex-shrink: 0; margin-bottom: 0.25rem; }
font-size: 1.6rem;
}
.my-lists-container { .my-notes-container {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
overflow-y: auto; overflow-y: auto;
@@ -58,6 +56,14 @@ body.page-dashboard {
} }
} }
#id_applet_wallet {
display: flex;
flex-direction: column;
gap: 0.25rem;
h2 { flex-shrink: 0; margin-bottom: 0; }
}
#id_applet_palette { #id_applet_palette {
padding: 0; padding: 0;

View File

@@ -87,11 +87,23 @@ body.page-wallet {
.wallet-tokens { .wallet-tokens {
display: flex; display: flex;
flex-direction: row; flex-direction: column;
align-items: center;
justify-content: space-evenly;
overflow: visible; overflow: visible;
h2 {
flex-shrink: 0;
margin-bottom: 0;
}
.token-row {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-evenly;
overflow: visible;
}
.token { .token {
font-size: 1.5rem; font-size: 1.5rem;
} }

View File

@@ -1,17 +0,0 @@
<section
id="id_applet_my_lists"
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
>
<a href="{% url 'my_lists' user.id %}" class="my-lists-main">My lists:</a>
<div class="my-lists-container">
<ul>
{% for list in recent_lists %}
<li>
<a href="{{ list.get_absolute_url }}">{{ list.name }}</a>
</li>
{% empty %}
<li>No lists yet.</li>
{% endfor %}
</ul>
</div>
</section>

View File

@@ -0,0 +1,17 @@
<section
id="id_applet_my_notes"
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
>
<h2><a href="{% url 'my_notes' user.id %}" class="my-notes-main">My notes</a></h2>
<div class="my-notes-container">
<ul>
{% for note in recent_notes %}
<li>
<a href="{{ note.get_absolute_url }}">{{ note.name }}</a>
</li>
{% empty %}
<li>No notes yet.</li>
{% endfor %}
</ul>
</div>
</section>

View File

@@ -1,8 +1,8 @@
<section <section
id="id_applet_new_list" id="id_applet_new_note"
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};" style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
> >
<h2>Start a new to-do list</h2> <h2>Start a new note</h2>
{% url "new_list" as form_action %} {% url "new_note" as form_action %}
{% include "apps/dashboard/_partials/_form.html" with form=form form_action=form_action %} {% include "apps/dashboard/_partials/_form.html" with form=form form_action=form_action %}
</section> </section>

View File

@@ -2,6 +2,6 @@
id="id_applet_wallet" id="id_applet_wallet"
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};" style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
> >
<span>Writs: {{ user.wallet.writs }}</span> <h2><a href="{% url "wallet" %}" class="wallet-manage-link">Manage Wallet</a></h2>
<a href="{% url "wallet" %}" class="wallet-manage-link">Manage Wallet</a> <span><i class="fa-solid fa-ticket"></i>: {{ user.wallet.writs }}</span>
</section> </section>

View File

@@ -4,7 +4,7 @@
id="id_text" id="id_text"
name="text" name="text"
class="form-control form-control-lg{% if form.errors.text %} is-invalid{% endif %}" class="form-control form-control-lg{% if form.errors.text %} is-invalid{% endif %}"
placeholder="Enter a to-do item" placeholder="Enter a note item"
value="{{ form.text.value | default:'' }}" value="{{ form.text.value | default:'' }}"
aria-describedby="id_text_feedback" aria-describedby="id_text_feedback"
required required

View File

@@ -1,19 +0,0 @@
{% extends "core/base.html" %}
{% load lyric_extras %}
{% block header_text %}{{ user|display_name }}'s lists{% endblock header_text %}
{% block content %}
<h3>{{ owner|display_name }}'s lists</h3>
<ul>
{% for list in owner.lists.all %}
<li><a href="{{ list.get_absolute_url }}">{{ list.name }}</a></li>
{% endfor %}
</ul>
<h3>Lists shared with me</h3>
<ul>
{% for list in owner.shared_lists.all %}
<li><a href="{{ list.get_absolute_url }}">{{ list.name }}</a></li>
{% endfor %}
</ul>
{% endblock content %}

View File

@@ -0,0 +1,20 @@
{% extends "core/base.html" %}
{% load lyric_extras %}
{% block title_text %}Dashnotes{% endblock title_text %}
{% block header_text %}<span>Dash</span>notes{% endblock header_text %}
{% block content %}
<h3>{{ owner|display_name }}'s notes</h3>
<ul>
{% for note in owner.notes.all %}
<li><a href="{{ note.get_absolute_url }}">{{ note.name }}</a></li>
{% endfor %}
</ul>
<h3>Notes shared with me</h3>
<ul>
{% for note in owner.shared_notes.all %}
<li><a href="{{ note.get_absolute_url }}">{{ note.name }}</a></li>
{% endfor %}
</ul>
{% endblock content %}

View File

@@ -1,21 +1,21 @@
{% extends "core/base.html" %} {% extends "core/base.html" %}
{% load lyric_extras %} {% load lyric_extras %}
{% block title_text %}Your to-do list{% endblock title_text %} {% block title_text %}Dashnote{% endblock title_text %}
{% block header_text %}Your to-do list{% endblock header_text %} {% block header_text %}<span>Dash</span>note{% endblock header_text %}
{% block extra_header %} {% block extra_header %}
{% url "view_list" list.id as form_action %} {% url "view_note" note.id as form_action %}
{% include "apps/dashboard/_partials/_form.html" with form=form form_action=form_action %} {% include "apps/dashboard/_partials/_form.html" with form=form form_action=form_action %}
{% endblock extra_header %} {% endblock extra_header %}
{% block content %} {% block content %}
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-lg-6"> <div class="col-lg-6">
<small>List created by: <span id="id_list_owner">{{ list.owner|display_name }}</span></small> <small>Note created by: <span id="id_note_owner">{{ note.owner|display_name }}</span></small>
<table id="id_list_table" class="table"> <table id="id_note_table" class="table">
{% for item in list.item_set.all %} {% for item in note.item_set.all %}
<tr><td>{{ forloop.counter }}. {{ item.text }}</td></tr> <tr><td>{{ forloop.counter }}. {{ item.text }}</td></tr>
{% endfor %} {% endfor %}
</table> </table>
@@ -25,7 +25,7 @@
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-lg-6"> <div class="col-lg-6">
<form method="POST" action="{% url "share_list" list.id %}"> <form method="POST" action="{% url "share_note" note.id %}">
{% csrf_token %} {% csrf_token %}
<input <input
id="id_recipient" id="id_recipient"
@@ -43,9 +43,9 @@
<button type="submit" class="btn btn-primary btn-xl">Share</button> <button type="submit" class="btn btn-primary btn-xl">Share</button>
</form> </form>
<small>List shared with: <small>Note shared with:
{% for user in list.shared_with.all %} {% for user in note.shared_with.all %}
<span class="list-recipient">{{ user|display_name }}</span> <span class="note-recipient">{{ user|display_name }}</span>
{% endfor %} {% endfor %}
</small> </small>
</div> </div>

View File

@@ -2,6 +2,7 @@
id="id_wallet_balances" id="id_wallet_balances"
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};" style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
> >
<h2>Balances</h2>
<div><i class="fa-solid fa-ticket"></i>: <span id="id_writs_balance">{{ wallet.writs }}</span></div> <div><i class="fa-solid fa-ticket"></i>: <span id="id_writs_balance">{{ wallet.writs }}</span></div>
<div>Esteem: <span id="id_esteem_balance">{{ wallet.esteem }}</span></div> <div>Esteem: <span id="id_esteem_balance">{{ wallet.esteem }}</span></div>
<div id="id_tithe_token_shop"> <div id="id_tithe_token_shop">

View File

@@ -2,7 +2,7 @@
id="id_payment_methods" id="id_payment_methods"
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};" style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
> >
<h2>Payment Methods</h2> <h2>Payment methods</h2>
<button id="id_add_payment_method">Add Payment Method</button> <button id="id_add_payment_method">Add Payment Method</button>
<div id="id_stripe_payment_element"></div> <div id="id_stripe_payment_element"></div>
<button id="id_save_payment_method" hidden>Save Card</button> <button id="id_save_payment_method" hidden>Save Card</button>

View File

@@ -2,6 +2,8 @@
class="wallet-tokens" class="wallet-tokens"
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};" style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
> >
<h2>Tokens</h2>
<div class="token-row">
{% if coin %} {% if coin %}
<div id="id_coin_on_a_string" class="token"> <div id="id_coin_on_a_string" class="token">
<i class="fa-solid fa-clover"></i> <i class="fa-solid fa-clover"></i>
@@ -50,4 +52,5 @@
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div>
</section> </section>