Move the server code inside server dir

This commit is contained in:
2025-12-18 02:17:55 +05:30
parent 2d90a4017f
commit c84866210a
32 changed files with 1 additions and 1 deletions

0
server/notes/__init__.py Normal file
View File

28
server/notes/admin.py Normal file
View File

@@ -0,0 +1,28 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from notes.models import Note, User
# Register your models here.
#
@admin.register(User)
class UserAdmin(BaseUserAdmin):
fieldsets = [
*BaseUserAdmin.fieldsets,
("Notes", {"fields": ("allow_notes_from", "expiry_seconds")}),
]
add_fieldsets = [
*BaseUserAdmin.add_fieldsets,
("Notes", {"fields": ("allow_notes_from", "expiry_seconds")}),
]
filter_horizontal = (
*BaseUserAdmin.filter_horizontal,
"allow_notes_from",
)
@admin.register(Note)
class NoteAdmin(admin.ModelAdmin):
pass

10
server/notes/api_views.py Normal file
View File

@@ -0,0 +1,10 @@
from rest_framework import generics
from notes.serializers import NoteSerializer
class NoteListView(generics.ListCreateAPIView):
serializer_class = NoteSerializer
def get_queryset(self):
return self.request.user.alive_received_notes

6
server/notes/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class NotesConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "notes"

9
server/notes/forms.py Normal file
View File

@@ -0,0 +1,9 @@
from django import forms
from notes.models import Note
class PostNoteForm(forms.ModelForm):
class Meta:
model = Note
fields = ["to_user", "note"]

View File

@@ -0,0 +1,176 @@
# Generated by Django 5.2.8 on 2025-12-07 08:46
import django.contrib.auth.models
import django.contrib.auth.validators
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
]
operations = [
migrations.CreateModel(
name="User",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("password", models.CharField(max_length=128, verbose_name="password")),
(
"last_login",
models.DateTimeField(
blank=True, null=True, verbose_name="last login"
),
),
(
"is_superuser",
models.BooleanField(
default=False,
help_text="Designates that this user has all permissions without explicitly assigning them.",
verbose_name="superuser status",
),
),
(
"username",
models.CharField(
error_messages={
"unique": "A user with that username already exists."
},
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
max_length=150,
unique=True,
validators=[
django.contrib.auth.validators.UnicodeUsernameValidator()
],
verbose_name="username",
),
),
(
"first_name",
models.CharField(
blank=True, max_length=150, verbose_name="first name"
),
),
(
"last_name",
models.CharField(
blank=True, max_length=150, verbose_name="last name"
),
),
(
"email",
models.EmailField(
blank=True, max_length=254, verbose_name="email address"
),
),
(
"is_staff",
models.BooleanField(
default=False,
help_text="Designates whether the user can log into this admin site.",
verbose_name="staff status",
),
),
(
"is_active",
models.BooleanField(
default=True,
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
verbose_name="active",
),
),
(
"date_joined",
models.DateTimeField(
default=django.utils.timezone.now, verbose_name="date joined"
),
),
("expiry_seconds", models.PositiveIntegerField(default=86400)),
(
"allow_notes_from",
models.ManyToManyField(
blank=True,
related_name="allowed_notes_to",
to=settings.AUTH_USER_MODEL,
),
),
(
"groups",
models.ManyToManyField(
blank=True,
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
related_name="user_set",
related_query_name="user",
to="auth.group",
verbose_name="groups",
),
),
(
"user_permissions",
models.ManyToManyField(
blank=True,
help_text="Specific permissions for this user.",
related_name="user_set",
related_query_name="user",
to="auth.permission",
verbose_name="user permissions",
),
),
],
options={
"verbose_name": "user",
"verbose_name_plural": "users",
"abstract": False,
},
managers=[
("objects", django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name="Note",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("note", models.TextField()),
("expiry", models.DateTimeField()),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"from_user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="created_notes",
to=settings.AUTH_USER_MODEL,
),
),
(
"to_user",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="received_notes",
to=settings.AUTH_USER_MODEL,
),
),
],
),
]

View File

39
server/notes/models.py Normal file
View File

@@ -0,0 +1,39 @@
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils import timezone
# Create your models here.
class User(AbstractUser):
allow_notes_from = models.ManyToManyField(
"User", related_name="allowed_notes_to", blank=True
)
expiry_seconds = models.PositiveIntegerField(default=86400)
@property
def alive_received_notes(self) -> models.QuerySet["Note"]:
return self.received_notes.filter(expiry__gt=timezone.now()).select_related(
"from_user", "to_user"
)
@property
def visible_name(self) -> str:
if self.first_name or self.last_name:
return self.get_full_name()
return self.username
class Note(models.Model):
from_user = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="created_notes"
)
to_user = models.ForeignKey(
User, on_delete=models.SET_NULL, null=True, related_name="received_notes"
)
note = models.TextField()
expiry = models.DateTimeField()
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.from_user.username} -> {self.to_user.username}: {self.note[:30]}"

View File

@@ -0,0 +1,56 @@
from datetime import timedelta
from django.utils import timezone
from rest_framework import serializers
from notes.models import Note, User
class UserSerializer(serializers.ModelSerializer):
username = serializers.CharField(max_length=150)
class Meta:
model = User
fields = ["id", "username", "first_name", "last_name", "visible_name"]
read_only_fields = ["id", "first_name", "last_name", "visible_name"]
class NoteSerializer(serializers.ModelSerializer):
from_user = UserSerializer(read_only=True)
to_user = UserSerializer()
class Meta:
model = Note
fields = [
"from_user",
"to_user",
"note",
"expiry",
"created_at",
]
read_only_fields = [
"from_user",
"created_at",
"expiry",
]
def validate_to_user(self, value):
try:
user = User.objects.get(username=value["username"])
except User.DoesNotExist:
raise serializers.ValidationError("User not found")
current_user = self.context["request"].user
if not current_user.allowed_notes_to.filter(pk=user.pk).exists():
raise serializers.ValidationError(
f"User not allowed to post notes to {user!r}"
)
return user
def save(self, **kwargs):
to_user = self.validated_data["to_user"]
return Note.objects.create(
from_user=self.context["request"].user,
to_user=to_user,
note=self.validated_data["note"],
expiry=timezone.now() + timedelta(seconds=to_user.expiry_seconds),
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,139 @@
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
background-color: #f4f0f0;
font-family: cursive;
}
header {
width: 100%;
display: flex;
background-color: #e7bbe0;
}
header > .home {
flex-grow: 1;
font-size: 3em;
text-decoration: none;
color: #000;
margin-left: 20px;
@media (max-width: 500px) {
font-size: 2em;
}
}
header img {
height: 40px;
margin: 20px;
@media (max-width: 500px) {
height: 25px;
margin: 10px 15px;
}
}
.logout {
padding: 0;
background: transparent;
border: none;
min-width: unset;
}
main {
padding: 0 30px;
}
input, select, textarea {
padding: 5px;
min-width: 200px;
display: block;
box-shadow: 15px 28px 25px -18px rgba(0, 0, 0, 0.1);
margin: 10px 0;
}
textarea {
resize: vertical;
padding: 20px;
}
button {
padding: 10px;
min-width: 100px;
}
.notes {
width: 100%;
columns: auto 300px;
column-gap: 80px;
justify-content: center;
align-content: center;
align-items: center;
margin: auto;
}
.notes.notes-grid {
display: grid;
}
.note {
display: inline-block;
width: 100%;
max-width: 300px;
padding: 30px;
border: 2px solid rgba(30, 30, 30, 0.4);
font-weight: 400;
font-style: normal;
box-shadow: 15px 28px 25px -18px rgba(0, 0, 0, 0.2);
text-align: center;
margin-bottom: 40px;
}
select:nth-child(3n), textarea:nth-child(3n), button:nth-child(3n), input:nth-child(3n), .note:nth-child(3n) {
border-radius: 155px 25px 15px 25px / 15px 225px 230px 150px;
}
select:nth-child(3n + 1), textarea:nth-child(3n + 1), button:nth-child(3n + 1), input:nth-child(3n + 1), .note:nth-child(3n + 1) {
border-radius: 25px 155px 15px 25px / 115px 25px 225px 150px;
}
select:nth-child(3n + 2), textarea:nth-child(3n + 2), button:nth-child(3n + 2), input:nth-child(3n + 2), .note:nth-child(3n + 2) {
border-radius: 25px 150px 25px 155px / 115px 25px 225px 50px;
}
.note:nth-child(4n) {
background-color: #d4f2d8;
}
.note:nth-child(4n + 1) {
background-color: #fac9dc;
}
.note:nth-child(4n + 2) {
background-color: #fff372;
}
.note:nth-child(4n + 3) {
background-color: #a4d6f7;
}
.note-from, .note-at {
text-align: right;
}
.card {
display: inline-block;
width: 100%;
padding: 30px;
border: 2px solid rgba(30, 30, 30, 0.4);
font-weight: 400;
font-style: normal;
box-shadow: 15px 28px 25px -18px rgba(0, 0, 0, 0.2);
text-align: center;
margin-bottom: 40px;
border-radius: 25px 155px 15px 25px / 115px 25px 225px 150px;
background-color: #fac9dc;
}

View File

@@ -0,0 +1,29 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="stylesheet" href="{% static "notes/stylesheets/style.css" %}" type="text/css">
<title>{% block title %}{% endblock %} | Appunti</title>
</head>
<body>
<header>
<a href="{% url "home" %}" class="home">Appunti</a>
<a href="{% url "post-a-note" %}">
<img src="{% static "notes/images/icons/post-a-note.png" %}" alt="Post a note" title="Post a note" />
</a>
<a href="{% url "profile" %}">
<img src="{% static "notes/images/icons/profile.png" %}" alt="Profile" title="Profile" />
</a>
<form method="post" action="{% url "logout" %}">
{% csrf_token %}
<button class="logout"><img src="{% static "notes/images/icons/logout.png" %}" alt="Logout" title="Logout" /></button>
</form>
</header>
<main>
{% block body %}
{% endblock %}
</main>
</body>
</html>

View File

@@ -0,0 +1,23 @@
{% extends "notes/base.html" %}
{% block title %}Dashboard{% endblock %}
{% block body %}
<h1>Dashboard</h1>
{% include "notes/messages.html" %}
<h2>Notes for you</h2>
{% if notes %}
<div class="notes">
{% for note in notes %}
<div class="note">
<p>{{ note.note }}</p>
<p class="note-from">From {{ note.from_user.visible_name }}</p>
<p class="note-at">{{ note.created_at|date:"j M, H:i"}}</p>
</div>
{% endfor %}
</div>
{% else %}
<div class="card">No Notes found</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,7 @@
{% if messages %}
<div class="messages">
{% for message in messages %}
<div class="card">{{ message }}</li>
{% endfor %}
</div>
{% endif %}

View File

@@ -0,0 +1,12 @@
{% extends "notes/base.html" %}
{% block title %}Post a Note{% endblock %}
{% block body %}
<h1>Post a Note</h1>
{% include "notes/messages.html" %}
<form method="post">{% csrf_token %}
{{ form.as_p }}
<button type="submit">Post!</button>
</form>
{% endblock %}

View File

@@ -0,0 +1,13 @@
{% extends "notes/base.html" %}
{% block title %}Profile{% endblock %}
{% block body %}
<h1>Profile</h1>
{% include "notes/messages.html" %}
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Save</button>
</form>
{% endblock %}

View File

@@ -0,0 +1,18 @@
{% extends "notes/base.html" %}
{% block title %}Login{% endblock %}
{% block body %}
<div id="header">
<h1>Appunti Login Screen</h1>
</div>
<div class="body">
{% include "notes/messages.html" %}
<form action="{% url 'login' %}" method="POST">
{% csrf_token %}
<h2>Welcome!</h2>
<p>Please login to continue</p>
{{ form.as_p }}
<button type="submit">Login</button>
</form>
</div>
{% endblock %}

0
server/notes/tests.py Normal file
View File

15
server/notes/urls.py Normal file
View File

@@ -0,0 +1,15 @@
from django.contrib.auth.views import LoginView, LogoutView
from django.urls import include, path
from notes import views, api_views
urlpatterns = [
path("", views.HomePage.as_view(), name="home"),
path("post-a-note/", views.PostNoteView.as_view(), name="post-a-note"),
path("profile/", views.ProfileView.as_view(), name="profile"),
path("login/", LoginView.as_view(), name="login"),
path("logout/", LogoutView.as_view(), name="logout"),
path("api/notes/", api_views.NoteListView.as_view(), name="api-notes-list"),
path("api-auth/", include("rest_framework.urls", namespace="rest_framework")),
]

52
server/notes/views.py Normal file
View File

@@ -0,0 +1,52 @@
from datetime import timedelta
from typing import Any
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from django.utils import timezone
from django.views.generic import CreateView, TemplateView, UpdateView
from django.views.generic.edit import FormMixin
from notes.models import Note, User
# Create your views here.
class HomePage(LoginRequiredMixin, TemplateView):
template_name = "notes/home.html"
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
ctx = super().get_context_data(**kwargs)
ctx["notes"] = self.request.user.alive_received_notes
return ctx
class PostNoteView(LoginRequiredMixin, CreateView):
model = Note
fields = ["to_user", "note"]
success_url = reverse_lazy("post-a-note")
def get_form(self, form_class=None):
form = super().get_form(form_class)
form.fields["to_user"].queryset = self.request.user.allowed_notes_to.all()
return form
def form_valid(self, form):
self.object = note = form.save(commit=False)
note.expiry = timezone.now() + timedelta(seconds=note.to_user.expiry_seconds)
note.from_user = self.request.user
note.save()
messages.success(self.request, "Note has been posted!")
return FormMixin.form_valid(self, form)
class ProfileView(LoginRequiredMixin, UpdateView):
model = User
fields = ["first_name", "last_name", "allow_notes_from", "expiry_seconds"]
success_url = reverse_lazy("profile")
def get_object(self):
return self.request.user
def form_valid(self, form):
ret = super().form_valid(form)
messages.success(self.request, "Profile has been updated!")
return ret