Skip to content

Commit

Permalink
Added mail project
Browse files Browse the repository at this point in the history
  • Loading branch information
Ekaterina-Vititneva committed Oct 30, 2024
1 parent d29d3de commit 27afcbb
Show file tree
Hide file tree
Showing 19 changed files with 573 additions and 0 deletions.
Empty file added project_3/mail/mail/__init__.py
Empty file.
3 changes: 3 additions & 0 deletions project_3/mail/mail/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.contrib import admin

# Register your models here.
5 changes: 5 additions & 0 deletions project_3/mail/mail/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class MailConfig(AppConfig):
name = 'mail'
29 changes: 29 additions & 0 deletions project_3/mail/mail/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from django.contrib.auth.models import AbstractUser
from django.db import models


class User(AbstractUser):
pass


class Email(models.Model):
user = models.ForeignKey("User", on_delete=models.CASCADE, related_name="emails")
sender = models.ForeignKey("User", on_delete=models.PROTECT, related_name="emails_sent")
recipients = models.ManyToManyField("User", related_name="emails_received")
subject = models.CharField(max_length=255)
body = models.TextField(blank=True)
timestamp = models.DateTimeField(auto_now_add=True)
read = models.BooleanField(default=False)
archived = models.BooleanField(default=False)

def serialize(self):
return {
"id": self.id,
"sender": self.sender.email,
"recipients": [user.email for user in self.recipients.all()],
"subject": self.subject,
"body": self.body,
"timestamp": self.timestamp.strftime("%b %d %Y, %I:%M %p"),
"read": self.read,
"archived": self.archived
}
33 changes: 33 additions & 0 deletions project_3/mail/mail/static/mail/inbox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
document.addEventListener('DOMContentLoaded', function() {

// Use buttons to toggle between views
document.querySelector('#inbox').addEventListener('click', () => load_mailbox('inbox'));
document.querySelector('#sent').addEventListener('click', () => load_mailbox('sent'));
document.querySelector('#archived').addEventListener('click', () => load_mailbox('archive'));
document.querySelector('#compose').addEventListener('click', compose_email);

// By default, load the inbox
load_mailbox('inbox');
});

function compose_email() {

// Show compose view and hide other views
document.querySelector('#emails-view').style.display = 'none';
document.querySelector('#compose-view').style.display = 'block';

// Clear out composition fields
document.querySelector('#compose-recipients').value = '';
document.querySelector('#compose-subject').value = '';
document.querySelector('#compose-body').value = '';
}

function load_mailbox(mailbox) {

// Show the mailbox and hide other views
document.querySelector('#emails-view').style.display = 'block';
document.querySelector('#compose-view').style.display = 'none';

// Show the mailbox name
document.querySelector('#emails-view').innerHTML = `<h3>${mailbox.charAt(0).toUpperCase() + mailbox.slice(1)}</h3>`;
}
3 changes: 3 additions & 0 deletions project_3/mail/mail/static/mail/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
textarea {
min-height: 400px;
}
37 changes: 37 additions & 0 deletions project_3/mail/mail/templates/mail/inbox.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{% extends "mail/layout.html" %}
{% load static %}

{% block body %}
<h2>{{ request.user.email }}</h2>

<button class="btn btn-sm btn-outline-primary" id="inbox">Inbox</button>
<button class="btn btn-sm btn-outline-primary" id="compose">Compose</button>
<button class="btn btn-sm btn-outline-primary" id="sent">Sent</button>
<button class="btn btn-sm btn-outline-primary" id="archived">Archived</button>
<a class="btn btn-sm btn-outline-primary" href="{% url 'logout' %}">Log Out</a>
<hr>

<div id="emails-view">
</div>

<div id="compose-view">
<h3>New Email</h3>
<form id="compose-form">
<div class="form-group">
From: <input disabled class="form-control" value="{{ request.user.email }}">
</div>
<div class="form-group">
To: <input id="compose-recipients" class="form-control">
</div>
<div class="form-group">
<input class="form-control" id="compose-subject" placeholder="Subject">
</div>
<textarea class="form-control" id="compose-body" placeholder="Body"></textarea>
<input type="submit" class="btn btn-primary"/>
</form>
</div>
{% endblock %}

{% block script %}
<script src="{% static 'mail/inbox.js' %}"></script>
{% endblock %}
18 changes: 18 additions & 0 deletions project_3/mail/mail/templates/mail/layout.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{% load static %}

<!DOCTYPE html>
<html lang="en">
<head>
<title>{% block title %}Mail{% endblock %}</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<link rel="stylesheet" href="{% static 'mail/styles.css' %}">
{% block script %}
{% endblock %}
</head>
<body>
<div class="container">
{% block body %}
{% endblock %}
</div>
</body>
</html>
24 changes: 24 additions & 0 deletions project_3/mail/mail/templates/mail/login.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{% extends "mail/layout.html" %}

{% block body %}

<h2>Login</h2>

{% if message %}
<div>{{ message }}</div>
{% endif %}

<form action="{% url 'login' %}" method="post">
{% csrf_token %}
<div class="form-group">
<input autofocus class="form-control" type="email" name="email" placeholder="Email">
</div>
<div class="form-group">
<input class="form-control" type="password" name="password" placeholder="Password">
</div>
<input class="btn btn-primary" type="submit" value="Login">
</form>

Don't have an account? <a href="{% url 'register' %}">Register here.</a>

{% endblock %}
27 changes: 27 additions & 0 deletions project_3/mail/mail/templates/mail/register.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{% extends "mail/layout.html" %}

{% block body %}

<h2>Register</h2>

{% if message %}
<div>{{ message }}</div>
{% endif %}

<form action="{% url 'register' %}" method="post">
{% csrf_token %}
<div class="form-group">
<input class="form-control" type="email" name="email" placeholder="Email Address">
</div>
<div class="form-group">
<input class="form-control" type="password" name="password" placeholder="Password">
</div>
<div class="form-group">
<input class="form-control" type="password" name="confirmation" placeholder="Confirm Password">
</div>
<input class="btn btn-primary" type="submit" value="Register">
</form>

Already have an account? <a href="{% url 'login' %}">Log In here.</a>

{% endblock %}
3 changes: 3 additions & 0 deletions project_3/mail/mail/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.test import TestCase

# Create your tests here.
15 changes: 15 additions & 0 deletions project_3/mail/mail/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from django.urls import path

from . import views

urlpatterns = [
path("", views.index, name="index"),
path("login", views.login_view, name="login"),
path("logout", views.logout_view, name="logout"),
path("register", views.register, name="register"),

# API Routes
path("emails", views.compose, name="compose"),
path("emails/<int:email_id>", views.email, name="email"),
path("emails/<str:mailbox>", views.mailbox, name="mailbox"),
]
179 changes: 179 additions & 0 deletions project_3/mail/mail/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import json
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required
from django.db import IntegrityError
from django.http import JsonResponse
from django.shortcuts import HttpResponse, HttpResponseRedirect, render
from django.urls import reverse
from django.views.decorators.csrf import csrf_exempt

from .models import User, Email


def index(request):

# Authenticated users view their inbox
if request.user.is_authenticated:
return render(request, "mail/inbox.html")

# Everyone else is prompted to sign in
else:
return HttpResponseRedirect(reverse("login"))


@csrf_exempt
@login_required
def compose(request):

# Composing a new email must be via POST
if request.method != "POST":
return JsonResponse({"error": "POST request required."}, status=400)

# Check recipient emails
data = json.loads(request.body)
emails = [email.strip() for email in data.get("recipients").split(",")]
if emails == [""]:
return JsonResponse({
"error": "At least one recipient required."
}, status=400)

# Convert email addresses to users
recipients = []
for email in emails:
try:
user = User.objects.get(email=email)
recipients.append(user)
except User.DoesNotExist:
return JsonResponse({
"error": f"User with email {email} does not exist."
}, status=400)

# Get contents of email
subject = data.get("subject", "")
body = data.get("body", "")

# Create one email for each recipient, plus sender
users = set()
users.add(request.user)
users.update(recipients)
for user in users:
email = Email(
user=user,
sender=request.user,
subject=subject,
body=body,
read=user == request.user
)
email.save()
for recipient in recipients:
email.recipients.add(recipient)
email.save()

return JsonResponse({"message": "Email sent successfully."}, status=201)


@login_required
def mailbox(request, mailbox):

# Filter emails returned based on mailbox
if mailbox == "inbox":
emails = Email.objects.filter(
user=request.user, recipients=request.user, archived=False
)
elif mailbox == "sent":
emails = Email.objects.filter(
user=request.user, sender=request.user
)
elif mailbox == "archive":
emails = Email.objects.filter(
user=request.user, recipients=request.user, archived=True
)
else:
return JsonResponse({"error": "Invalid mailbox."}, status=400)

# Return emails in reverse chronologial order
emails = emails.order_by("-timestamp").all()
return JsonResponse([email.serialize() for email in emails], safe=False)


@csrf_exempt
@login_required
def email(request, email_id):

# Query for requested email
try:
email = Email.objects.get(user=request.user, pk=email_id)
except Email.DoesNotExist:
return JsonResponse({"error": "Email not found."}, status=404)

# Return email contents
if request.method == "GET":
return JsonResponse(email.serialize())

# Update whether email is read or should be archived
elif request.method == "PUT":
data = json.loads(request.body)
if data.get("read") is not None:
email.read = data["read"]
if data.get("archived") is not None:
email.archived = data["archived"]
email.save()
return HttpResponse(status=204)

# Email must be via GET or PUT
else:
return JsonResponse({
"error": "GET or PUT request required."
}, status=400)


def login_view(request):
if request.method == "POST":

# Attempt to sign user in
email = request.POST["email"]
password = request.POST["password"]
user = authenticate(request, username=email, password=password)

# Check if authentication successful
if user is not None:
login(request, user)
return HttpResponseRedirect(reverse("index"))
else:
return render(request, "mail/login.html", {
"message": "Invalid email and/or password."
})
else:
return render(request, "mail/login.html")


def logout_view(request):
logout(request)
return HttpResponseRedirect(reverse("index"))


def register(request):
if request.method == "POST":
email = request.POST["email"]

# Ensure password matches confirmation
password = request.POST["password"]
confirmation = request.POST["confirmation"]
if password != confirmation:
return render(request, "mail/register.html", {
"message": "Passwords must match."
})

# Attempt to create new user
try:
user = User.objects.create_user(email, email, password)
user.save()
except IntegrityError as e:
print(e)
return render(request, "mail/register.html", {
"message": "Email address already taken."
})
login(request, user)
return HttpResponseRedirect(reverse("index"))
else:
return render(request, "mail/register.html")
Loading

0 comments on commit 27afcbb

Please sign in to comment.