Compare commits

...

10 commits

Author SHA1 Message Date
Jannis Portmann e147c67026 Remove pint 2023-03-31 23:57:01 +02:00
Jannis Portmann 85e7440f0f Improve layout 2023-03-31 23:56:46 +02:00
Jannis Portmann 9d2b219a70 Send request mail 2023-03-31 23:56:19 +02:00
Jannis Portmann 79b29d0d95 Add public user page 2023-03-31 23:55:24 +02:00
Jannis Portmann 13e4e0fabc Fix deleting of old image 2023-03-31 22:51:53 +02:00
Jannis Portmann 18967d9178 Remove print 2023-03-31 18:11:37 +02:00
Jannis Portmann ca9a91c864 Implement edit and delete 2023-03-31 18:10:52 +02:00
Jannis Portmann 805c3b645e Add admin 2023-03-26 23:07:59 +02:00
Jannis Portmann 27e4779816 Increase padding 2023-02-20 11:33:21 +01:00
Jannis Portmann 840a1b186f Upload and show offers 2023-02-20 11:31:10 +01:00
13 changed files with 222 additions and 144 deletions

View file

@ -1,3 +1,12 @@
from django.contrib import admin from django.contrib import admin
from .models import Offer, Wish
# Register your models here.
@admin.register(Offer)
class OfferAdmin(admin.ModelAdmin):
list_display = ['id', 'title', 'user', 'zipcode']
@admin.register(Wish)
class WishAdmin(admin.ModelAdmin):
list_display = ['id', 'title', 'user']

25
pflaenzli/mail.py Normal file
View file

@ -0,0 +1,25 @@
from django.core.mail import EmailMessage, EmailMultiAlternatives
from django.urls import reverse
from django.template.loader import render_to_string
def send_offer_email(request, offer, sender_user, recipient_user):
html_content = render_to_string('user/trade/offer_email.html',
{'request': request, 'offer': offer, 'sender_user': sender_user, 'recipient_user': recipient_user})
plain_text = get_offer_text(request, offer, sender_user, recipient_user)
message = EmailMultiAlternatives(
f'{sender_user.username} wants to trade',
plain_text,
'no-reply@pflaenz.li',
[recipient_user.email],
reply_to=[sender_user.email],
)
message.attach_alternative(html_content, 'text/html')
message.send()
def get_offer_text(request, offer, sender_user, recipient_user):
return f"Hello {recipient_user.username},\n\nThe user {sender_user.username} would like to trade '{offer.title}' with you!\n\nIf you would like to trade with {sender_user.username}, just reply to this email to get in touch with them.\n\nYou can also view their offers here: {request.scheme}://{request.get_host()}{reverse('user_detail', args=[sender_user.id])}"

View file

@ -1,6 +1,8 @@
from django.db import models from django.db import models
from django.dispatch import receiver
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils import timezone from django.utils import timezone
from django.core.files.storage import default_storage
class Offer(models.Model): class Offer(models.Model):
@ -16,3 +18,22 @@ class Wish(models.Model):
created = models.DateTimeField(default=timezone.now) created = models.DateTimeField(default=timezone.now)
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField(max_length=200) title = models.CharField(max_length=200)
@receiver(models.signals.post_delete, sender=Offer)
def auto_delete_image_on_delete(sender, instance, **kwargs):
"""
Deletes image from filesystem when corresponding `Offer` object is deleted.
"""
if instance.image and os.path.isfile(instance.image.path):
default_storage.delete(instance.image.path)
@receiver(models.signals.pre_save, sender=Offer)
def delete_old_image(sender, instance, **kwargs):
try:
old_image = sender.objects.get(pk=instance.pk).image
except sender.DoesNotExist:
return False
default_storage.delete(old_image.path)

View file

@ -48,7 +48,7 @@
id="navbarSupportedContent"> id="navbarSupportedContent">
<ul class="navbar-nav mr-auto"> <ul class="navbar-nav mr-auto">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'index' %}"><i class="fa-solid fa-seedling"></i><span class="ms-2">Offers</span></a> <a class="nav-link" href="{% url 'list_offers' %}"><i class="fa-solid fa-seedling"></i><span class="ms-2">Offers</span></a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'create_offer' %}"><i class="fa-solid fa-square-plus"></i><span class="ms-2">New Offer</span></a> <a class="nav-link" href="{% url 'create_offer' %}"><i class="fa-solid fa-square-plus"></i><span class="ms-2">New Offer</span></a>
@ -109,7 +109,7 @@
</div> </div>
</div> </div>
</nav> </nav>
<div class="container pt-4 mb-5transparent"> <div class="container pt-4 mb-5">
<div class="container mt-5"> <div class="container mt-5">
{% if messages %} {% if messages %}
{% for message in messages %} {% for message in messages %}
@ -124,7 +124,7 @@
</div> </div>
</div> </div>
</main> </main>
<footer class="mt-auto p-5 align-items-center bg-body-tertiary"> <footer class="mt-auto p-4 align-items-center bg-body-tertiary">
<div class="container d-flex justify-content-around align-items-center"> <div class="container d-flex justify-content-around align-items-center">
<p> <p>
<a href="#" class="me-3"><i class="fab fa-mastodon"></i></a> <a href="#" class="me-3"><i class="fab fa-mastodon"></i></a>

View file

@ -7,16 +7,16 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% if offer.user == request.user %}<div class="alert alert-info" role="alert">This is your offer!</div>{% endif %} {% if offer.user == request.user %}<div class="alert alert-info" role="alert">This is your offer!</div>{% endif %}
<div class="show-offer-container d-flex flex-wrap"> <div class="show-offer-container row gx-5">
<div class="show-img-container"> <div class="show-img-container col-12 col-md-6">
<img class="mb-3 img-fluid rounded" <img class="mb-3 img-fluid rounded"
alt="Generic placeholder image" alt="Generic placeholder image"
src="{{ offer.image.url }}"> src="{{ offer.image.url }}">
</div> </div>
<div class="show-offer-info w-50"> <div class="show-offer-info col-12 col-md-6">
<h1 class="mb-3">{{ offer.title }}</h1> <h1 class="mb-3">{{ offer.title }}</h1>
<div class="mb-3 d-flex"> <div class="mb-3 d-flex column-gap-2">
<p class="pr-3"> <p class="mr-3">
<i class="fas fa-user"></i> <i class="fas fa-user"></i>
{% if offer.user == user %} {% if offer.user == user %}
Me Me
@ -24,7 +24,7 @@
{{ offer.user.username }} {{ offer.user.username }}
{% endif %} {% endif %}
</p> </p>
<p class="pr-3"> <p class="mr-3">
<i class="fas fa-map-marker-alt"></i> {{ offer.zipcode }} <i class="fas fa-map-marker-alt"></i> {{ offer.zipcode }}
</p> </p>
{% if distance > 0 %} {% if distance > 0 %}
@ -38,34 +38,35 @@
</div> </div>
</div> </div>
{% if offer.user == user %} {% if offer.user == user %}
<a href="{% url 'offer_edit' offer.id %}" class="btn btn-info mb-3"><i class="fas fa-pen"></i></a> <a href="{% url 'offer_edit' offer.id %}" class="btn btn-pfl"><i class="fas fa-pen"></i></a>
<!-- Button trigger modal -->
<button type="button" <button type="button"
class="btn btn-danger mb-3" class="btn btn-danger"
data-toggle="modal" data-bs-toggle="modal"
data-target="#exampleModal"> data-bs-target="#exampleModal">
<i class="fas fa-trash-alt"></i> <i class="fas fa-trash-alt"></i>
</button> </button>
<!-- Modal --> <!-- Modal -->
<div class="modal fade" <div class="modal fade"
id="exampleModal" id="exampleModal"
tabindex="-1" tabindex="-1"
role="dialog"
aria-labelledby="exampleModalLabel" aria-labelledby="exampleModalLabel"
aria-hidden="true"> aria-hidden="true">
<div class="modal-dialog" role="document"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Warning</h5> <h1 class="modal-title fs-5" id="exampleModalLabel">Delete offer</h1>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> <button type="button"
<span aria-hidden="true">&times;</span> class="btn-close"
</button> data-bs-dismiss="modal"
aria-label="Close"></button>
</div> </div>
<div class="modal-body">Are you sure you want to delete this offer?</div> <div class="modal-body">Do you really want to delete this offer? This can't be undone.</div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<a type="button" <a href="{% url 'offer_delete' offer.id %}"
class="btn btn-danger" type="button"
href="{% url 'offer_delete' offer.id %}">Delete</a> class="btn btn-danger">Delete</a>
</div> </div>
</div> </div>
</div> </div>
@ -82,6 +83,6 @@
</ul> </ul>
{% endif %} {% endif %}
</div> </div>
<a class="btn btn-pfl mb-3" href="{% url 'trade' offer.id %}">Offer trade</a> <a class="btn btn-pfl mb-3" href="{% url 'offer_trade' offer.id %}">Offer trade</a>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -1,45 +1,29 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static %}
{% block title %}Offers{% endblock %} {% block title %}Offers{% endblock %}
{% block content %} {% block content %}
<div class="mb-5">
<a class="btn btn-pfl"
data-toggle="collapse"
href="#collapseExample"
role="button"
aria-expanded="false"
aria-controls="collapseExample">
<div class="btn btn-pfl">
<i class="fas fa-filter mr-3"></i>Filter<i class="fas fa-chevron-down ml-3 dropdown-collapse"></i>
</div>
</a>
<div class="collapse" id="collapseExample">{{ form(filter_form, {attr: {novalidate: 'novalidate' }}) }}</div>
</div>
<h1>Offers</h1> <h1>Offers</h1>
{% if offers|length > 0 %} {% if offers %}
<div class="card-deck d-flex justify-content-around justify-content-sm-around justify-content-md-between flex-wrap"> <div class="row row-cols-1 row-cols-md-2 row-cols-xl-3 mb-3 row-gap-5">
{% for offer in offers %} {% for offer in offers %}
<div class="mb-5"> <div class="col">
<div class="card offer h-100"> <div class="card h-100 p-0">
<a href="{{ path('show_offer', {'urlId': offer.urlId }) }}"> <a href="{% url 'offer_detail' offer.id %}">
{% if offer.photoFilename %} {% if offer.image %}
<img class="card-img-top offer-img" <img class="card-img-top offer-img" src="{{ offer.image.url }}"/>
src="{{ asset('uploads/photos/' ~ offer.photofilename) }}"/>
{% else %} {% else %}
<img class="card-img-top offer-img" src="{{ asset('placeholder.jpg') }}" /> <img class="card-img-top offer-img" src="{% static 'placeholder.jpg' %}" />
{% endif %} {% endif %}
<div class="card-body"> <div class="card-body">
<h5 class="card-title">{{ offer.title }}</h5> <h5 class="h5">{{ offer.title }}</h5>
</div> </div>
</a> </a>
<div class="card-footer offer-footer"> <div class="card-footer d-flex justify-content-between">
<a class="user-link" <a href="#">
href="{{ path('user_public', { 'urlId': offer.byuser.urlId }) }}"> <i class="fas fa-user mt-3"></i> {{ offer.user.username }}
<p class="username">
<i class="fas fa-user mt-3"></i> {{ offer.byUser }}
</p>
</a> </a>
<p class="zip"> <p class="zip">
<i class="fas fa-map-marker-alt mt-3"></i> {{ offer.zipCode }} <i class="fas fa-map-marker-alt mt-3"></i> {{ offer.zipcode }}
</p> </p>
</div> </div>
</div> </div>

View file

@ -0,0 +1,51 @@
{% extends "base.html" %}
{% load static %}
{% block title %}User {{ username }}{% endblock %}
{% block content %}
<div class="mb-3">
<h1>{{ user.username }}'s Profile</h1>
</div>
<h2 class="h3">Wishlist</h2>
<div class="mb-4">
{% if wishes %}
<ul class="list-group">
{% for wish in wishes %}<li class="list-group-item">{{ wish.title }}</li>{% endfor %}
</ul>
{% else %}
<div class="alert alert-warning" role="alert">{{ user.username }} has currently no wishes!</div>
{% endif %}
</div>
<div class="mb-3">
<h2 class="h3">Offers</h2>
</div>
{% if offers %}
<div class="card-deck d-flex justify-content-around justify-content-sm-around justify-content-md-between flex-wrap">
{% for offer in offers %}
<div class="col">
<div class="card h-100 p-0 flex-column justify-content-between">
<a href="{% url 'offer_detail' offer.id %}">
{% if offer.image %}
<img class="card-img-top offer-img" src="{{ offer.image.url }}"/>
{% else %}
<img class="card-img-top offer-img" src="{% static 'placeholder.jpg' %}" />
{% endif %}
<div class="card-body">
<h5 class="h5">{{ offer.title }}</h5>
</div>
</a>
<div class="card-footer d-flex justify-content-between">
<a href="#">
<i class="fas fa-user mt-3"></i> {{ offer.user.username }}
</a>
<p class="zip">
<i class="fas fa-map-marker-alt mt-3"></i> {{ offer.zipcode }}
</p>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-warning" role="alert">There are currently no active offers.</div>
{% endif %}
{% endblock %}

View file

@ -1,63 +0,0 @@
{% extends 'base.html.twig' %}
{% block title %}User {{ username }}{% endblock %}
{% block content %}
{% for message in app.flashes('success') %}
<div class="alert alert-success" role="alert">
{{ message }}
</div>
{% endfor %}
<div class="mb-3">
<h1>{{ username }}'s Wishlist</h1>
</div>
<div class="mb-4">
{% if wishes == [] %}
<div class="alert alert-warning" role="alert">
There are currently no wishes!
</div>
{% else %}
<ul class="list-group">
{% for wish in wishes %}
<li class="list-group-item"> {{ wish.title }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
<hr>
<div class="mb-3">
<h1>{{ username }}'s Offers</h1>
</div>
{% if offers|length > 0 %}
<div class="card-deck d-flex justify-content-around justify-content-sm-around justify-content-md-between flex-wrap">
{% for offer in offers %}
<div class="mb-5">
<div class="card offer h-100">
<a href="{{ path('show_offer', {'urlId': offer.urlId }) }}">
{% if offer.photoFilename %}
<img class="card-img-top offer-img" src="{{ asset('uploads/photos/' ~ offer.photofilename) }}" />
{% else %}
<img class="card-img-top offer-img" src="{{ asset('placeholder.jpg') }}" />
{% endif %}
<div class="card-body">
<h5 class="card-title">{{ offer.title }}</h5>
<p class="card-text">{{ offer.description }}</p>
</div>
</a>
<div class="card-footer offer-footer">
<a class="user-link" href="{{ path('user_public', { 'urlId': offer.byuser.id }) }}">
<p class="username"><i class="fas fa-user mt-3"></i> {{ offer.byUser }}</p>
</a>
<p class="zip"><i class="fas fa-map-marker-alt mt-3"></i> {{ offer.zipCode }}</p>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-warning" role="alert">There are currently no active offers.</div>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,5 @@
<p>Hello {{ recipient_user.username }}</p>
<h1>{{ sender_user.username }} wants to trade '{{ offer.title }}'!</h1>
<p>Checkout their offers:</p>
<a href="{{ request.scheme }}://{{ request.get_host }}{% url 'user_detail' sender_user.id %}">Link</a>
<p>Just reply to get in contact witj to start trading with {{ sender_user.username }}.</p>

View file

@ -1,5 +0,0 @@
<h1>{{ user.username }} wants to trade!</h1>
<p>Checkout {{ user.username}}'s offers:</p>
<a href="{{ url('user_public', {'urlId': urlId}) }}">Link</a>
<p>Reply to this email to start trading.</p>

11
pflaenzli/upload.py Normal file
View file

@ -0,0 +1,11 @@
import os
from pathlib import Path
from uuid import uuid4
from django.core.files.storage import default_storage
def generate_unique_filename(filename):
file_type = Path(filename).suffix
new_filename = default_storage.get_available_name(uuid4())
return f'{new_filename}{file_type}'

View file

@ -7,11 +7,13 @@ from . import views
urlpatterns = [ urlpatterns = [
path("", views.index, name="index"), path("", views.index, name="index"),
path("offers/", views.list_offers, name="list_offers"),
path("offer/create/", views.create_offer, name="create_offer"), path("offer/create/", views.create_offer, name="create_offer"),
path("offer/<int:offer_id>/", views.offer_detail, name="offer_detail"), path("offer/<int:offer_id>/", views.offer_detail, name="offer_detail"),
path("offer/<int:offer_id>/delete", views.offer_delete, name="offer_delete"), path("offer/<int:offer_id>/delete/", views.offer_delete, name="offer_delete"),
path("offer/<int:offer_id>/edit", views.offer_edit, name="offer_edit"), path("offer/<int:offer_id>/edit/", views.offer_edit, name="offer_edit"),
path("offer/<int:offer_id>/trade", views.offer_trade, name="offer_trade"), path("offer/<int:offer_id>/trade/", views.offer_trade, name="offer_trade"),
path("user/<int:user_id>", views.user_detail, name="user_detail"),
path('accounts/login/', auth_views.LoginView.as_view(template_name='registration/login.html')), path('accounts/login/', auth_views.LoginView.as_view(template_name='registration/login.html')),
path('accounts/profile/', auth_views.LoginView.as_view(template_name='user/detail.html')), path('accounts/profile/', auth_views.LoginView.as_view(template_name='user/detail.html')),
path('accounts/', include('django.contrib.auth.urls')), path('accounts/', include('django.contrib.auth.urls')),

View file

@ -1,28 +1,32 @@
import os
import pathlib
import uuid
from django.core.files.storage import default_storage
from django.shortcuts import render, get_object_or_404, redirect from django.shortcuts import render, get_object_or_404, redirect
from django.utils import timezone from django.utils import timezone
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.http import HttpResponseForbidden
from .forms import CreateOfferForm from .forms import CreateOfferForm
from .models import Offer, Wish from .models import Offer, Wish
from .mail import send_offer_email
from .upload import generate_unique_filename
def index(request): def index(request):
return render(request, "app/index.html") return render(request, "app/index.html")
def list_offers(request, filters=None):
offers = Offer.objects.all()
return render(request, "offer/search.html", {"offers": offers})
@login_required @login_required
def create_offer(request): def create_offer(request):
if request.method == "POST": if request.method == "POST":
print(request.POST)
form = CreateOfferForm(request.POST, request.FILES) form = CreateOfferForm(request.POST, request.FILES)
if form.is_valid(): if form.is_valid():
offer = form.save(commit=False) offer = form.save(commit=False)
offer.image.name = generate_unique_filename(request.FILES['image'].name) offer.image.name = generate_unique_filename(form.cleaned_data['image'].name)
offer.user = request.user offer.user = request.user
offer.save() offer.save()
messages.success(request, "Offer uploaded successfully!") messages.success(request, "Offer uploaded successfully!")
@ -40,23 +44,56 @@ def offer_detail(request, offer_id):
return render(request, "offer/detail.html", {"offer": offer, "wishes": ["Monstera", "Tradescantia"]}) return render(request, "offer/detail.html", {"offer": offer, "wishes": ["Monstera", "Tradescantia"]})
def generate_unique_filename(filename):
file_type = pathlib.Path(filename).suffix
new_filename = default_storage.get_available_name(uuid.uuid4())
return f'{new_filename}{file_type}'
@login_required @login_required
def offer_delete(request, offer_id): def offer_delete(request, offer_id):
return 0 offer = get_object_or_404(Offer, id=offer_id)
if offer.user != request.user:
return HttpResponseForbidden()
offer.delete()
messages.success(request, "Offer deleted successfully!")
return redirect("list_offers")
@login_required @login_required
def offer_edit(request, offer_id): def offer_edit(request, offer_id):
return 0 offer = get_object_or_404(Offer, id=offer_id)
if offer.user != request.user:
return HttpResponseForbidden()
if request.method == "POST":
form = CreateOfferForm(request.POST, request.FILES, instance=offer)
if form.is_valid():
offer = form.save(commit=False)
offer.image.name = generate_unique_filename(form.cleaned_data['image'].name)
offer.user = request.user
offer.save()
messages.success(request, "Offer updated successfully!")
return redirect("offer_detail", offer.id)
else:
form = CreateOfferForm(instance=offer)
return render(request, "basic_form.html", {"form": form, "button_label": "Update"})
@login_required @login_required
def offer_trade(request, offer_id): def offer_trade(request, offer_id):
return 0 offer = get_object_or_404(Offer, id=offer_id)
sender = request.user
recipient = offer.user
if sender != recipient:
send_offer_email(request, offer, sender, recipient)
messages.success(request, f"{recipient.username} was successfully notified")
else:
messages.error(request, "You can't trade with yourself!")
return redirect("offer_detail", offer_id)
def user_detail(request, user_id):
user = get_object_or_404(User, id=user_id)
offers = Offer.objects.filter(user=user_id)
wishes = Wish.objects.filter(user=user_id)
return render(request, "user/public.html", {"user": user, "offers": offers, "wishes": wishes})