diff --git a/src/bornhack/settings.py b/src/bornhack/settings.py index 8bf4cd27..91590962 100644 --- a/src/bornhack/settings.py +++ b/src/bornhack/settings.py @@ -46,6 +46,7 @@ INSTALLED_APPS = [ 'bar', 'backoffice', 'events', + 'rideshare', 'allauth', 'allauth.account', diff --git a/src/bornhack/urls.py b/src/bornhack/urls.py index cda0e36c..fb665d83 100644 --- a/src/bornhack/urls.py +++ b/src/bornhack/urls.py @@ -182,6 +182,11 @@ urlpatterns = [ include('teams.urls', namespace='teams') ), + path( + 'rideshare/', + include('rideshare.urls', namespace='rideshare') + ), + path( 'backoffice/', include('backoffice.urls', namespace='backoffice') diff --git a/src/rideshare/__init__.py b/src/rideshare/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/rideshare/admin.py b/src/rideshare/admin.py new file mode 100644 index 00000000..6a674531 --- /dev/null +++ b/src/rideshare/admin.py @@ -0,0 +1,9 @@ +from django.contrib import admin + +from .models import Ride + + +@admin.register(Ride) +class RideModelAdmin(admin.ModelAdmin): + list_display = ('location', 'when', 'seats', 'user') + list_filter = ('camp', 'user') diff --git a/src/rideshare/apps.py b/src/rideshare/apps.py new file mode 100644 index 00000000..9a8b366f --- /dev/null +++ b/src/rideshare/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class RideshareConfig(AppConfig): + name = 'rideshare' diff --git a/src/rideshare/migrations/0001_initial.py b/src/rideshare/migrations/0001_initial.py new file mode 100644 index 00000000..b1a843f3 --- /dev/null +++ b/src/rideshare/migrations/0001_initial.py @@ -0,0 +1,36 @@ +# Generated by Django 2.0.4 on 2018-08-08 20:18 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('camps', '0028_auto_20180525_1025'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Ride', + fields=[ + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('seats', models.PositiveIntegerField()), + ('location', models.CharField(max_length=100)), + ('when', models.DateTimeField()), + ('description', models.TextField()), + ('camp', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='camps.Camp')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/src/rideshare/migrations/__init__.py b/src/rideshare/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/rideshare/models.py b/src/rideshare/models.py new file mode 100644 index 00000000..42e5a033 --- /dev/null +++ b/src/rideshare/models.py @@ -0,0 +1,30 @@ +from django.db import models +from django.urls import reverse + +from utils.models import UUIDModel, CampRelatedModel + + +class Ride(UUIDModel, CampRelatedModel): + camp = models.ForeignKey('camps.Camp', on_delete=models.PROTECT) + user = models.ForeignKey('auth.User', on_delete=models.PROTECT) + seats = models.PositiveIntegerField() + location = models.CharField(max_length=100) + when = models.DateTimeField() + description = models.TextField() + + def get_absolute_url(self): + return reverse( + 'rideshare:detail', + kwargs={ + 'pk': self.pk, + 'camp_slug': self.camp.slug + } + ) + + def __str__(self): + return "{} seats from {} at {} by {}".format( + self.seats, + self.location, + self.when, + self.user + ) diff --git a/src/rideshare/templates/rideshare/emails/contact_mail.html b/src/rideshare/templates/rideshare/emails/contact_mail.html new file mode 100644 index 00000000..6855a686 --- /dev/null +++ b/src/rideshare/templates/rideshare/emails/contact_mail.html @@ -0,0 +1,13 @@ +Hello!
+
+The following message has been submitted to your rideshare on {{ rideshare_url }}.
+
+<message>
+
+ {{ message }}
+
+</message>
+
+Best regards,
+
+The BornHack Teamp diff --git a/src/rideshare/templates/rideshare/emails/contact_mail.txt b/src/rideshare/templates/rideshare/emails/contact_mail.txt new file mode 100644 index 00000000..8225c0d5 --- /dev/null +++ b/src/rideshare/templates/rideshare/emails/contact_mail.txt @@ -0,0 +1,13 @@ +Hello! + +The following message has been submitted to your rideshare on {{ rideshare_url }}. + + + +{{ message }} + + + +Best regards, + +The BornHack Team diff --git a/src/rideshare/templates/rideshare/ride_confirm_delete.html b/src/rideshare/templates/rideshare/ride_confirm_delete.html new file mode 100644 index 00000000..73d954b8 --- /dev/null +++ b/src/rideshare/templates/rideshare/ride_confirm_delete.html @@ -0,0 +1,10 @@ +{% extends 'base.html' %} + +{% block content %} + +
{% csrf_token %} +

Are you sure you want to delete {{ object }}?

+ +
+ +{% endblock %} diff --git a/src/rideshare/templates/rideshare/ride_detail.html b/src/rideshare/templates/rideshare/ride_detail.html new file mode 100644 index 00000000..bb9e90f5 --- /dev/null +++ b/src/rideshare/templates/rideshare/ride_detail.html @@ -0,0 +1,60 @@ +{% extends 'base.html' %} +{% load commonmark %} +{% load bootstrap3 %} + +{% block content %} + + + + Back + + +
+ +
+
+

+ {{ object.seats }} + seats free, going from + {{ object.location }} + at + {{ object.when|date:"jS \o\f F \a\t H:i T" }} +

+
+
+ Description: +

+ {{ object.description|untrustedcommonmark }} +

+
+ {% if user == object.user %} + + {% else %} + + + {% endif %} +
+ + + +{% endblock %} diff --git a/src/rideshare/templates/rideshare/ride_form.html b/src/rideshare/templates/rideshare/ride_form.html new file mode 100644 index 00000000..c520b609 --- /dev/null +++ b/src/rideshare/templates/rideshare/ride_form.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} +{% load commonmark %} +{% load bootstrap3 %} + +{% block content %} + +
+ {% csrf_token %} + {% bootstrap_form form %} + +
+ +{% endblock %} diff --git a/src/rideshare/templates/rideshare/ride_list.html b/src/rideshare/templates/rideshare/ride_list.html new file mode 100644 index 00000000..7b39ac5e --- /dev/null +++ b/src/rideshare/templates/rideshare/ride_list.html @@ -0,0 +1,54 @@ +{% extends 'base.html' %} + +{% block content %} + + + +

+On this page participants of {{ camp.title }} can communicate about ridesharing to and from the festival. +

+ + + + Create ride + + + +
+ + + + + +{% for ride in ride_list %} + + +
+ When + + Location + + Seats + +
+ {{ ride.when|date:"c" }} + + {{ ride.location }} + + {{ ride.seats }} + + + Details + + +{% empty %} + +
+ No rideshares yet! + +{% endfor %} +
+ +{% endblock %} diff --git a/src/rideshare/tests.py b/src/rideshare/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/src/rideshare/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/src/rideshare/urls.py b/src/rideshare/urls.py new file mode 100644 index 00000000..e528e248 --- /dev/null +++ b/src/rideshare/urls.py @@ -0,0 +1,43 @@ +from django.urls import path, include + +from .views import ( + RideList, + RideCreate, + RideDetail, + RideUpdate, + RideDelete, +) + +app_name = 'rideshare' + +urlpatterns = [ + path( + '', + RideList.as_view(), + name='list' + ), + path( + 'create/', + RideCreate.as_view(), + name='create' + ), + path( + '/', include([ + path( + '', + RideDetail.as_view(), + name='detail' + ), + path( + 'update/', + RideUpdate.as_view(), + name='update' + ), + path( + 'delete/', + RideDelete.as_view(), + name='delete' + ), + ]) + ) +] diff --git a/src/rideshare/views.py b/src/rideshare/views.py new file mode 100644 index 00000000..94305103 --- /dev/null +++ b/src/rideshare/views.py @@ -0,0 +1,87 @@ +from django.contrib import messages +from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin +from django.urls import reverse +from django.views.generic import ( + ListView, + DetailView, + CreateView, + UpdateView, + DeleteView, + FormView +) +from django.http import HttpResponseRedirect +from django import forms + +from camps.mixins import CampViewMixin +from utils.email import add_outgoing_email + +from .models import Ride + + +class ContactRideForm(forms.Form): + message = forms.CharField( + widget=forms.Textarea(attrs={"placeholder": "Remember to include your contact information!"}), + label="Write a message to this rideshare", + help_text="ATTENTION!: Pressing send will send an email with the above text. It is up to you to include your contact information so the person receiving the email can contact you.", + ) + + +class RideList(LoginRequiredMixin, CampViewMixin, ListView): + model = Ride + + +class RideDetail(LoginRequiredMixin, CampViewMixin, DetailView): + model = Ride + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['form'] = ContactRideForm() + return context + + def post(self, request, **kwargs): + form = ContactRideForm(request.POST) + if form.is_valid(): + ride = self.get_object() + add_outgoing_email( + text_template='rideshare/emails/contact_mail.txt', + to_recipients=[ride.user.emailaddress_set.get(primary=True).email], + formatdict=dict( + rideshare_url="https://bornhack.dk{}".format( + reverse( + 'rideshare:detail', + kwargs={"camp_slug": self.camp.slug, "pk": ride.pk} + ) + ), + message=form.cleaned_data['message'], + ), + subject="BornHack rideshare message!", + ) + messages.info(request, "Your message has been sent.") + return HttpResponseRedirect(ride.get_absolute_url()) + + +class RideCreate(LoginRequiredMixin, CampViewMixin, CreateView): + model = Ride + fields = ['location', 'when', 'seats', 'description'] + + def form_valid(self, form, **kwargs): + ride = form.save(commit=False) + ride.camp = self.camp + ride.user = self.request.user + ride.save() + self.object = ride + return HttpResponseRedirect(self.get_success_url()) + + +class IsRideOwnerMixin(UserPassesTestMixin): + def test_func(self): + return self.get_object().user == self.request.user + + +class RideUpdate(LoginRequiredMixin, CampViewMixin, IsRideOwnerMixin, UpdateView): + model = Ride + fields = ['location', 'when', 'seats', 'description'] + + +class RideDelete(LoginRequiredMixin, CampViewMixin, IsRideOwnerMixin, DeleteView): + model = Ride diff --git a/src/templates/includes/menuitems.html b/src/templates/includes/menuitems.html index 9b021d07..fe144075 100644 --- a/src/templates/includes/menuitems.html +++ b/src/templates/includes/menuitems.html @@ -5,6 +5,9 @@ Villages Sponsors Teams + {% if request.user.is_authenticated %} + Rideshare + {% endif %} {% if request.user.is_staff %} Backoffice {% endif %}