Merge pull request #260 from bornhack/feature/rideshare
Ridesharing functionality
This commit is contained in:
commit
426a3f56be
|
@ -46,6 +46,7 @@ INSTALLED_APPS = [
|
||||||
'bar',
|
'bar',
|
||||||
'backoffice',
|
'backoffice',
|
||||||
'events',
|
'events',
|
||||||
|
'rideshare',
|
||||||
|
|
||||||
'allauth',
|
'allauth',
|
||||||
'allauth.account',
|
'allauth.account',
|
||||||
|
|
|
@ -182,6 +182,11 @@ urlpatterns = [
|
||||||
include('teams.urls', namespace='teams')
|
include('teams.urls', namespace='teams')
|
||||||
),
|
),
|
||||||
|
|
||||||
|
path(
|
||||||
|
'rideshare/',
|
||||||
|
include('rideshare.urls', namespace='rideshare')
|
||||||
|
),
|
||||||
|
|
||||||
path(
|
path(
|
||||||
'backoffice/',
|
'backoffice/',
|
||||||
include('backoffice.urls', namespace='backoffice')
|
include('backoffice.urls', namespace='backoffice')
|
||||||
|
|
0
src/rideshare/__init__.py
Normal file
0
src/rideshare/__init__.py
Normal file
9
src/rideshare/admin.py
Normal file
9
src/rideshare/admin.py
Normal file
|
@ -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')
|
5
src/rideshare/apps.py
Normal file
5
src/rideshare/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class RideshareConfig(AppConfig):
|
||||||
|
name = 'rideshare'
|
36
src/rideshare/migrations/0001_initial.py
Normal file
36
src/rideshare/migrations/0001_initial.py
Normal file
|
@ -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,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
0
src/rideshare/migrations/__init__.py
Normal file
0
src/rideshare/migrations/__init__.py
Normal file
30
src/rideshare/models.py
Normal file
30
src/rideshare/models.py
Normal file
|
@ -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
|
||||||
|
)
|
13
src/rideshare/templates/rideshare/emails/contact_mail.html
Normal file
13
src/rideshare/templates/rideshare/emails/contact_mail.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
Hello!<br />
|
||||||
|
<br />
|
||||||
|
The following message has been submitted to your rideshare on <a href="{{ rideshare_url }}">{{ rideshare_url }}</a>.<br />
|
||||||
|
<br />
|
||||||
|
<message><br />
|
||||||
|
<br />
|
||||||
|
{{ message }}<br />
|
||||||
|
<br />
|
||||||
|
</message><br />
|
||||||
|
<br />
|
||||||
|
Best regards,<br />
|
||||||
|
<br />
|
||||||
|
The BornHack Teamp
|
13
src/rideshare/templates/rideshare/emails/contact_mail.txt
Normal file
13
src/rideshare/templates/rideshare/emails/contact_mail.txt
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
Hello!
|
||||||
|
|
||||||
|
The following message has been submitted to your rideshare on {{ rideshare_url }}.
|
||||||
|
|
||||||
|
<message>
|
||||||
|
|
||||||
|
{{ message }}
|
||||||
|
|
||||||
|
</message>
|
||||||
|
|
||||||
|
Best regards,
|
||||||
|
|
||||||
|
The BornHack Team
|
10
src/rideshare/templates/rideshare/ride_confirm_delete.html
Normal file
10
src/rideshare/templates/rideshare/ride_confirm_delete.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<form method="post">{% csrf_token %}
|
||||||
|
<p>Are you sure you want to delete {{ object }}?</p>
|
||||||
|
<input type="submit" class="btn btn-danger" value="Confirm" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
60
src/rideshare/templates/rideshare/ride_detail.html
Normal file
60
src/rideshare/templates/rideshare/ride_detail.html
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load commonmark %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<a class="btn btn-primary" href="{% url 'rideshare:list' camp_slug=camp.slug %}">
|
||||||
|
<i class="fas fa-chevron-left"></i>
|
||||||
|
Back
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h4>
|
||||||
|
<strong>{{ object.seats }}</strong>
|
||||||
|
seats free, going from
|
||||||
|
<strong>{{ object.location }}</strong>
|
||||||
|
at
|
||||||
|
<strong>{{ object.when|date:"jS \o\f F \a\t H:i T" }}</strong>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<strong>Description:</strong>
|
||||||
|
<p>
|
||||||
|
{{ object.description|untrustedcommonmark }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% if user == object.user %}
|
||||||
|
<div class="panel-footer">
|
||||||
|
<a class="btn btn-danger pull-right" href="{% url 'rideshare:delete' camp_slug=camp.slug pk=object.pk %}">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
Delete
|
||||||
|
</a>
|
||||||
|
<a class="btn btn-primary pull-right" href="{% url 'rideshare:update' camp_slug=camp.slug pk=object.pk %}">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
Edit
|
||||||
|
</a>
|
||||||
|
<span class="clearfix"></span>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="panel-footer">
|
||||||
|
<form method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% bootstrap_form form %}
|
||||||
|
<button type="submit" class="btn btn-success pull-right">
|
||||||
|
<i class="fas fa-envelope"></i>
|
||||||
|
Send
|
||||||
|
</button>
|
||||||
|
<span class="clearfix"></span>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
15
src/rideshare/templates/rideshare/ride_form.html
Normal file
15
src/rideshare/templates/rideshare/ride_form.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load commonmark %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% bootstrap_form form %}
|
||||||
|
<button type="submit" class="btn btn-success">
|
||||||
|
{% if object.pk %}Update{% else %}Create{% endif %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
54
src/rideshare/templates/rideshare/ride_list.html
Normal file
54
src/rideshare/templates/rideshare/ride_list.html
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>Ridesharing</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
On this page participants of {{ camp.title }} can communicate about ridesharing to and from the festival.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a class="btn btn-success pull-right" href="{% url 'rideshare:create' camp_slug=camp.slug %}">
|
||||||
|
<i class="fas fa-car"></i>
|
||||||
|
Create ride
|
||||||
|
</a>
|
||||||
|
<span class="clearfix"></span>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<table class="table table-condensed table-striped">
|
||||||
|
<thead>
|
||||||
|
<th>
|
||||||
|
When
|
||||||
|
<th>
|
||||||
|
Location
|
||||||
|
<th>
|
||||||
|
Seats
|
||||||
|
<th>
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
{% for ride in ride_list %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{{ ride.when|date:"c" }}
|
||||||
|
<td>
|
||||||
|
{{ ride.location }}
|
||||||
|
<td>
|
||||||
|
{{ ride.seats }}
|
||||||
|
<td>
|
||||||
|
<a class="btn btn-primary" href="{% url 'rideshare:detail' camp_slug=camp.slug pk=ride.pk %}">
|
||||||
|
<i class="fas fa-eye"></i> Details
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{% empty %}
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td colspan=4>
|
||||||
|
No rideshares yet!
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% endblock %}
|
3
src/rideshare/tests.py
Normal file
3
src/rideshare/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
43
src/rideshare/urls.py
Normal file
43
src/rideshare/urls.py
Normal file
|
@ -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(
|
||||||
|
'<uuid:pk>/', include([
|
||||||
|
path(
|
||||||
|
'',
|
||||||
|
RideDetail.as_view(),
|
||||||
|
name='detail'
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'update/',
|
||||||
|
RideUpdate.as_view(),
|
||||||
|
name='update'
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'delete/',
|
||||||
|
RideDelete.as_view(),
|
||||||
|
name='delete'
|
||||||
|
),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
]
|
87
src/rideshare/views.py
Normal file
87
src/rideshare/views.py
Normal file
|
@ -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
|
|
@ -5,6 +5,9 @@
|
||||||
<a class="btn {% menubuttonclass 'villages' %}" href="{% url 'village_list' camp_slug=camp.slug %}">Villages</a>
|
<a class="btn {% menubuttonclass 'villages' %}" href="{% url 'village_list' camp_slug=camp.slug %}">Villages</a>
|
||||||
<a class="btn {% menubuttonclass 'sponsors' %}" href="{% url 'sponsors' camp_slug=camp.slug %}">Sponsors</a>
|
<a class="btn {% menubuttonclass 'sponsors' %}" href="{% url 'sponsors' camp_slug=camp.slug %}">Sponsors</a>
|
||||||
<a class="btn {% menubuttonclass 'teams' %}" href="{% url 'teams:list' camp_slug=camp.slug %}">Teams</a>
|
<a class="btn {% menubuttonclass 'teams' %}" href="{% url 'teams:list' camp_slug=camp.slug %}">Teams</a>
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
<a class="btn {% menubuttonclass 'rideshare' %}" href="{% url 'rideshare:list' camp_slug=camp.slug %}">Rideshare</a>
|
||||||
|
{% endif %}
|
||||||
{% if request.user.is_staff %}
|
{% if request.user.is_staff %}
|
||||||
<a class="btn {% menubuttonclass 'backoffice' %}" href="{% url 'backoffice:index' camp_slug=camp.slug %}">Backoffice</a>
|
<a class="btn {% menubuttonclass 'backoffice' %}" href="{% url 'backoffice:index' camp_slug=camp.slug %}">Backoffice</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
Loading…
Reference in a new issue