Compare commits
10 commits
cd9bc0cba3
...
1246c86691
Author | SHA1 | Date | |
---|---|---|---|
|
1246c86691 | ||
|
e727904323 | ||
|
af1a55138c | ||
|
15f5a4d281 | ||
|
300e535b5c | ||
|
35ada3edff | ||
|
a53a6f9344 | ||
|
35ec98f830 | ||
|
4e01a6ff90 | ||
|
3691061f71 |
14 changed files with 236 additions and 87 deletions
|
@ -7,7 +7,7 @@ A platform where people can trade their plants. You can post what you have and s
|
|||
|
||||
## Tech stack
|
||||
- [Django](https://djangoproject.com/)
|
||||
- [MariaDB](https://www.postgresql.org/)
|
||||
- [Postgres](https://www.postgresql.org/)
|
||||
|
||||
## Admin dashboard
|
||||
Find it under `/admin`
|
||||
|
@ -31,4 +31,8 @@ Searching with filters such as:
|
|||
| Category | `Category` | dropdown |
|
||||
|
||||
## Development
|
||||
To get started with development, see [DEVELOPMENT.md](DEVELOPMENT.md)
|
||||
To get started with development, see [DEVELOPMENT.md](DEVELOPMENT.md)
|
||||
|
||||
## Open Source Data
|
||||
For calculating distances between zip codes, the `PLZ_Verzeichnis` is used.
|
||||
Source: https://opendata.swiss/de/dataset/plz_verzeichnis
|
|
@ -1,9 +1,19 @@
|
|||
from django import forms
|
||||
from django.contrib.auth.forms import UserCreationForm
|
||||
from friendly_captcha.fields import FrcCaptchaField
|
||||
|
||||
from .models import User, Offer
|
||||
from .models import Offer, PflaenzliUser
|
||||
|
||||
|
||||
class CreateOfferForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Offer
|
||||
fields = ['title', 'description', 'zipcode', 'image']
|
||||
|
||||
|
||||
class RegistrationForm(UserCreationForm):
|
||||
class Meta(UserCreationForm.Meta):
|
||||
model = PflaenzliUser
|
||||
fields = UserCreationForm.Meta.fields + ('zipcode',)
|
||||
|
||||
captcha = FrcCaptchaField()
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
# Generated by Django 4.1.7 on 2023-02-19 12:38
|
||||
# Generated by Django 4.1.7 on 2023-04-05 21:38
|
||||
|
||||
from django.conf import settings
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -10,10 +13,148 @@ class Migration(migrations.Migration):
|
|||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("auth", "0012_alter_user_first_name_max_length"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="PflaenzliUser",
|
||||
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"
|
||||
),
|
||||
),
|
||||
("zipcode", models.PositiveIntegerField(blank=True)),
|
||||
(
|
||||
"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="Wish",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created", models.DateTimeField(default=django.utils.timezone.now)),
|
||||
("title", models.CharField(max_length=200)),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Offer",
|
||||
fields=[
|
||||
|
@ -26,9 +167,11 @@ class Migration(migrations.Migration):
|
|||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created", models.DateTimeField(default=django.utils.timezone.now)),
|
||||
("title", models.CharField(max_length=50)),
|
||||
("description", models.TextField(max_length=5000)),
|
||||
("zipcode", models.IntegerField(blank=True, default=0)),
|
||||
("image", models.ImageField(upload_to="uploads/")),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-02-19 22:03
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("pflaenzli", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="offer",
|
||||
name="created",
|
||||
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Wish",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created", models.DateTimeField(default=django.utils.timezone.now)),
|
||||
("title", models.CharField(max_length=200)),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -1,19 +0,0 @@
|
|||
# Generated by Django 4.1.7 on 2023-02-19 22:09
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pflaenzli", "0002_offer_created_wish"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="offer",
|
||||
name="image",
|
||||
field=models.ImageField(default="placeholder.png", upload_to="uplaods"),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
|
@ -1,13 +1,17 @@
|
|||
from django.db import models
|
||||
from django.dispatch import receiver
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.utils import timezone
|
||||
from django.core.files.storage import default_storage
|
||||
|
||||
|
||||
class PflaenzliUser(AbstractUser):
|
||||
zipcode = models.PositiveIntegerField(blank=True)
|
||||
|
||||
|
||||
class Offer(models.Model):
|
||||
created = models.DateTimeField(default=timezone.now)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(PflaenzliUser, on_delete=models.CASCADE)
|
||||
title = models.CharField(max_length=50)
|
||||
description = models.TextField(max_length=5000)
|
||||
zipcode = models.IntegerField(blank=True, default=0)
|
||||
|
@ -16,7 +20,7 @@ class Offer(models.Model):
|
|||
|
||||
class Wish(models.Model):
|
||||
created = models.DateTimeField(default=timezone.now)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(PflaenzliUser, on_delete=models.CASCADE)
|
||||
title = models.CharField(max_length=200)
|
||||
|
||||
|
||||
|
|
|
@ -13,7 +13,9 @@
|
|||
</p>
|
||||
<hr />
|
||||
<p>To offer your plants, please register first.</p>
|
||||
<a href="#" class="btn btn-pfl btn-lg mb-3" type="button">Register</a>
|
||||
<a href="{% url 'register_user' %}"
|
||||
class="btn btn-pfl btn-lg mb-3"
|
||||
type="button">Register</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
{% block title %}{{ title }}{% endblock %}
|
||||
{% block meta %}<meta name="description" content="{{ page_description }}">{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{{ title }}</h1>
|
||||
<form method="post" enctype="multipart/form-data" class="mb-5">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
|
|
|
@ -16,5 +16,6 @@ urlpatterns = [
|
|||
path("accounts/<int:user_id>", views.user_detail, name="user_detail"),
|
||||
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'), name='user_profile'),
|
||||
path('accounts/register/', views.register_user, name='register_user'),
|
||||
path('accounts/', include('django.contrib.auth.urls')),
|
||||
]
|
||||
|
|
16
pflaenzli/pflaenzli/utils/distance.py
Normal file
16
pflaenzli/pflaenzli/utils/distance.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
from geopy.distance import distance
|
||||
import os
|
||||
from pandas import read_pickle
|
||||
|
||||
path = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
df = read_pickle(os.path.join(path, 'plz.pkl'))
|
||||
|
||||
|
||||
def calculate_distance(zip_1, zip_2):
|
||||
zip_1_coords = tuple(df[df.index == zip_1].values)
|
||||
zip_2_coords = tuple(df[df.index == zip_2].values)
|
||||
|
||||
dist = round(distance((zip_1_coords), (zip_2_coords)).kilometers)
|
||||
|
||||
return None if dist > 400 else dist
|
BIN
pflaenzli/pflaenzli/utils/plz.pkl
Normal file
BIN
pflaenzli/pflaenzli/utils/plz.pkl
Normal file
Binary file not shown.
|
@ -1,12 +1,13 @@
|
|||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django.utils import timezone
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import login
|
||||
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 .models import Offer, Wish
|
||||
from .forms import CreateOfferForm, RegistrationForm
|
||||
|
||||
from .models import PflaenzliUser, Offer, Wish
|
||||
from .mail import send_offer_email
|
||||
from .upload import generate_unique_filename
|
||||
|
||||
|
@ -35,7 +36,7 @@ def create_offer(request):
|
|||
else:
|
||||
form = CreateOfferForm()
|
||||
|
||||
return render(request, "basic_form.html", {"form": form, "button_label": "Create"})
|
||||
return render(request, "basic_form.html", {"form": form, "button_label": "Create", "title": "Create Offer"})
|
||||
|
||||
|
||||
@ login_required
|
||||
|
@ -74,7 +75,7 @@ def offer_edit(request, offer_id):
|
|||
else:
|
||||
form = CreateOfferForm(instance=offer)
|
||||
|
||||
return render(request, "basic_form.html", {"form": form, "button_label": "Update"})
|
||||
return render(request, "basic_form.html", {"form": form, "button_label": "Update", "title": "Edit Offer"})
|
||||
|
||||
|
||||
@login_required
|
||||
|
@ -93,8 +94,21 @@ def offer_trade(request, offer_id):
|
|||
|
||||
|
||||
def user_detail(request, user_id):
|
||||
user = get_object_or_404(User, id=user_id)
|
||||
user = get_object_or_404(PflaenzliUser, 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})
|
||||
|
||||
|
||||
def register_user(request):
|
||||
if request.method == "POST":
|
||||
form = RegistrationForm(request.POST)
|
||||
if form.is_valid():
|
||||
user = form.save()
|
||||
login(request, user)
|
||||
return redirect("index")
|
||||
else:
|
||||
form = RegistrationForm()
|
||||
|
||||
return render(request, "basic_form.html", {"form": form, "button_label": "Register", "title": "Registeration"})
|
||||
|
|
|
@ -12,6 +12,12 @@ https://docs.djangoproject.com/en/4.1/ref/settings/
|
|||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
# Parse .env
|
||||
load_dotenv()
|
||||
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
@ -42,6 +48,7 @@ INSTALLED_APPS = [
|
|||
"fontawesomefree",
|
||||
"crispy_forms",
|
||||
"crispy_bootstrap5",
|
||||
"friendly_captcha",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
@ -116,6 +123,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||
},
|
||||
]
|
||||
|
||||
AUTH_USER_MODEL = 'pflaenzli.PflaenzliUser'
|
||||
LOGOUT_REDIRECT_URL = "/"
|
||||
|
||||
# Internationalization
|
||||
|
@ -145,11 +153,19 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
|||
|
||||
CRISPY_TEMPLATE_PACK = 'bootstrap5'
|
||||
|
||||
if DEBUG:
|
||||
# Mailhog configuration
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_HOST = '0.0.0.0'
|
||||
EMAIL_PORT = 1025
|
||||
EMAIL_HOST_USER = ''
|
||||
EMAIL_HOST_PASSWORD = ''
|
||||
EMAIL_USE_TLS = False
|
||||
|
||||
# Email Settings
|
||||
# SMTP Server
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
|
||||
EMAIL_HOST = os.getenv('SMTP_HOST')
|
||||
EMAIL_PORT = os.getenv('SMTP_PORT')
|
||||
EMAIL_HOST_USER = os.getenv('SMTP_USER')
|
||||
EMAIL_HOST_PASSWORD = os.getenv('SMTP_PASSWORD')
|
||||
EMAIL_USE_SSL = True
|
||||
|
||||
|
||||
# Friendly Captcha setting
|
||||
FRC_CAPTCHA_SECRET = os.getenv('FRC_SECRET')
|
||||
FRC_CAPTCHA_SITE_KEY = os.getenv('FRC_SITEKEY')
|
||||
FRC_CAPTCHA_VERIFICATION_URL = 'https://api.friendlycaptcha.com/api/v1/siteverify'
|
||||
|
|
|
@ -3,7 +3,9 @@ Django==4.1.7
|
|||
django-bootstrap5==22.2
|
||||
django-crispy-forms==2.0
|
||||
django-jquery==3.1.0
|
||||
geopy==2.3.0
|
||||
gunicorn==20.1.0
|
||||
fontawesomefree==6.3.0
|
||||
Pillow==9.4.0
|
||||
psycopg2-binary==2.9.5
|
||||
python-dotenv==1.0.0
|
||||
|
|
Loading…
Reference in a new issue