added messages support, more clean html/bootstrap

This commit is contained in:
wcolmenares 2019-03-18 23:29:07 -04:00
parent 318feb7e97
commit e1f6537165
11 changed files with 385 additions and 117 deletions

View file

@ -13,19 +13,43 @@
<title>{% block title %}{% endblock %}</title> <title>{% block title %}{% endblock %}</title>
</head> </head>
<body> <body>
<div class="d-flex flex-column flex-md-row align-items-center p-3 px-md-4 mb-3 bg-white border-bottom shadow-sm"> <nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container">
<h5 class="my-0 mr-md-auto font-weight-normal"><a href="{% url 'jobs:index' %}"><img src="{% static 'img/logo.svg' %}"/></a></h5> <a class="navbar-brand" href="{% url 'jobs:index' %}"><img src="{% static 'img/logo.svg' %}" alt=""></a>
<nav class="my-2 my-md-0 mr-md-3">
{# <a class="p-2 text-dark" href="#">Search</a>#}
{# <a class="p-2 text-dark" href="#">Tags</a>#}
<a class="p-2 text-dark" href="{% url 'jobs:job_list' %}">Jobs</a>
</nav>
<a class="btn btn-outline-primary" href="{% url 'jobs:job_create' %}">Post a job</a>
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<a class="btn text-dark" href="{% url 'logout' %}">Logout</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse justify-content-end" id="navbarSupportedContent">
<ul class="navbar-nav ml-auto">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Applications</a>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuLink">
<a class="dropdown-item" href="{% url 'jobs:my_applications' %}">Your applications</a>
<a class="dropdown-item" href="{% url 'jobs:applications_others' %}">Applications to your jobs</a>
</ul>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'jobs:messages_inbox' %}">Messages</a>
</li>
<li class="nav-item justify-content-end">
<a class="nav-link" href="{% url 'jobs:my_jobs' %}">Your Jobs</a>
{# {% url 'jobs:job_list' %}#}
</li>
<a class="btn btn-outline-primary mx-1" href="{% url 'jobs:job_create' %}">Post a job</a>
<div class="dropdown-divider"></div>
<a class="btn btn-danger mx-1" href="{% url 'logout' %}">Logout</a>
</ul>
</div>
{% else %}
<div class="ml-auto">
<a href="{% url 'jobs:job_create' %}" class="btn btn-outline-primary ml-auto" role="button" aria-pressed="true">Post a job</a>
</div>
{% endif %} {% endif %}
</div> </div>
</nav>
<div class="container"> <div class="container">
{% for message in messages %} {% for message in messages %}
@ -34,23 +58,43 @@
{{ message }} {{ message }}
</div> </div>
{% endfor %} {% endfor %}
{% block body_content %}{% endblock %}
{% block body_content %}
{% endblock %}
<footer class="pt-4 my-md-5 pt-md-5 border-top"> <footer class="pt-4 my-md-5 pt-md-5 border-top">
<div class="row"> <div class="row">
<div class="col-12 text-center"> <div class="col-12 text-center">
<small class="text-muted">&copy; {% now 'Y' %} <small class="text-muted">&copy; {% now 'Y' %}</small>
This service is provided to you </div>
by <a href="https://ungleich.ch/" </div>
target="_blank">ungleich</a>. <div class="row">
IPv6.work is <div class="col-12 text-center">
<small>This service is provided to you
by <a href="https://ungleich.ch/" target="_blank">ungleich</a>. IPv6.work is
<a href="https://code.ungleich.ch/ungleich-public/ipv6-dot-work" target="_blank"> <a href="https://code.ungleich.ch/ungleich-public/ipv6-dot-work" target="_blank">
Open Source</a>.</small> Open Source</a>.</small>
</div> </div>
</div> </div>
</footer> </footer>
<style>
.my-custom-scrollbar {
position: relative;
height: 400px;
overflow: auto;
word-break: break-all;
}
.table-wrapper-scroll-y {
display: block;
}
</style>
</div> </div>
<!-- jQuery first, then Popper.js, then Bootstrap JS --> <!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script> <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>

View file

@ -1,10 +1,36 @@
<!DOCTYPE html> {% extends 'base.html' %}
<html lang="en"> {% block body_content %}
<head> {% load crispy_forms_tags %}
<meta charset="UTF-8"> <div class="row">
<title>Title</title> <div class="col-md-12">
</head> <div class="pricing-header px-3 py-3 pt-md-5 pb-md-4 mx-auto text-center">
<body> <h1 class="h2 text-center">{{ title }}</h1>
<p class="lead"></p>
</div>
</div>
</div>
<div class="table-wrapper-scroll-y my-custom-scrollbar">
<table class="table table-hover table-bordered">
<thead>
<tr>
<th scope="col">Conversation with {{ person }}</th>
</tr>
</thead>
<tbody>
{% for message in jobmessage %}
<tr>
<td>{{ message.sender }}: {{ message.text }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="dropdown-divider"></div>
<form action="{% url 'jobs:send_message' %}" method="POST">
{% csrf_token %}
<input type="hidden" value="{{ person.id }}" name="receiver_id">
{{ form|crispy }}
<input class="btn btn-primary btn-block" type="submit" value="Send Message">
</form>
</body> {% endblock %}
</html>

View file

@ -11,12 +11,12 @@
<section class="bg-light-gray"> <section class="bg-light-gray">
<div class="row"> <div class="row">
{% for job in jobs %} {% for job in jobs %}
<div class="col-md-4 col-sm-6 portfolio-item wow fadeInUp" data-wow-delay="0.25s" style="visibility: visible; animation-delay: 0.25s; animation-name: fadeInUp;"> <div class="col-md-4 col-sm-6 portfolio-item wow fadeInUp d-flex" data-wow-delay="0.25s" style="visibility: visible; animation-delay: 0.25s; animation-name: fadeInUp;">
<div class="portfolio-caption inline-block"> <div class="portfolio-caption inline-block">
<h4><a href="{{ job.get_absolute_url }}" style="color:black;">{{ job.title }}</a></h4> <h4><a href="{{ job.get_absolute_url }}" style="color:black;">{{ job.title|truncatechars:50 }}</a></h4>
<p>&nbsp;</p> <p>&nbsp;</p>
<p class="text-muted"></p> <p class="text-muted"></p>
<p>{{job.description|truncatewords:20}}</p> <p>{{job.description|truncatechars:120}}</p>
<p class="text-right"><a href="{% url 'jobs:job_apply' job.pk %}">Apply</a></p> <p class="text-right"><a href="{% url 'jobs:job_apply' job.pk %}">Apply</a></p>
</div> </div>
</div> </div>

View file

@ -1,10 +1,42 @@
<!DOCTYPE html> {% extends 'base.html' %}
<html lang="en"> {% block body_content %}
<head> <div class="row">
<meta charset="UTF-8"> <div class="col-md-12">
<title>Title</title> <div class="pricing-header px-3 py-3 pt-md-5 pb-md-4 mx-auto text-center">
</head> <h1 class="display-4">{{ title }}</h1>
<body> <p class="lead"></p>
</div>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover table-bordered">
<thead>
<tr>
<th scope="col">Job Title</th>
<th scope="col">Date</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody>
{% for job in jobs %}
<tr>
<td style="vertical-align: middle">{{ job.title|truncatechars:65 }}</td>
<td style="vertical-align: middle">{{ job.created.date }}</td>
<td style="vertical-align: middle">
<form action="{% url 'jobs:job_toggle' %}" method="post">{% csrf_token %}
<input type="hidden" name="job_id" value="{{ job.id }}">
{% if job.active %}
<button type="submit" class="btn btn-warning btn-block">Deactivate</button>
{% else %}
<button type="submit" class="btn btn-success btn-block">Activate</button>
{% endif %}
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
</body>
</html>

View file

@ -13,7 +13,7 @@
{{ question_form|crispy }} {{ question_form|crispy }}
<a id="add_more" href="">Add More</a><br/> <a id="add_more" href="">Add More</a><br/>
<input class="btn btn-primary submit" type="submit" value="Post Job"/> <input class="btn btn-primary btn-block my-2" type="submit" value="Post Job"/>
</form> </form>
{{form.media}} {{form.media}}
</div> </div>

View file

@ -7,37 +7,24 @@
<p class="lead"></p> <p class="lead"></p>
</div> </div>
</div> </div>
<table class="table table-hover"> </div>
<div class="table-wrapper-scroll-y my-custom-scrollbar">
<table class="table table-hover table-bordered">
<thead> <thead>
<tr> <tr>
<th scope="col">#</th> <th scope="col">User</th>
<th scope="col">First</th> <th scope="col">Last Message</th>
<th scope="col">Last</th>
<th scope="col">Handle</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> {% for message in jobmessage %}
<th scope="row">1</th> <tr onclick="window.location.href = '{{ message.sender_id }}';">
<td>Mark</td> <td>{{ message.sender }}</td>
<td>Otto</td> <td>{{ message.text|truncatechars:40 }}</td>
<td>@mdo</td>
</tr>
<tr>
<th scope="row">2</th>
<td>Jacob</td>
<td>Thornton</td>
<td>@fat</td>
</tr>
<tr>
<th scope="row">3</th>
<td colspan="2">Larry the Bird</td>
<td>@twitter</td>
</tr> </tr>
{% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
{% endblock %}
{% for jobmessage in jobmessages %} {% endblock %}
{% endfor %}

View file

@ -1,10 +1,57 @@
<!DOCTYPE html> {% extends 'base.html' %}
<html lang="en"> {% load crispy_forms_tags %}
<head> {% block body_content %}
<meta charset="UTF-8"> <div class="row">
<title>$Title$</title> <div class="col-md-12">
</head> <div class="pricing-header px-3 py-3 pt-md-5 pb-md-4 mx-auto text-center">
<body> <h1 class="h2 text-center">{{ title }}</h1>
$END$ <p class="lead"></p>
</body> </div>
</html> {% for application in applications %}
<div class="card">
<div class="card-body">
<h4 class="card-title">
<a href="{{ application.job.get_absolute_url }}">{{ application.job }}</a>
</h4>
<p class="card-text">
Submitted: {{ application.created }}
</p>
<p class="card-text">
{{ application.cover_text }}
</p>
<ol>
{% for answer in application.answers.all %}
<li>{{answer.question}}<p>{{answer.text}}</p></li>
{% endfor %}
</ol>
<button class="btn btn-outline-primary btn-block" data-toggle="modal" data-target="#messageModal">Contact Applicant</button>
<div class="modal fade" id="messageModal" tabindex="-1" role="dialog" aria-labelledby="messageModalLabel" aria-hidden="true" style="vertical-align: center">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Contact Applicant {{ application.applicant }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form action="{% url 'jobs:send_message' %}" method="POST">{% csrf_token %}
<div class="modal-body">
<input type="hidden" value="{{ application.applicant_id }}" name="receiver_id">
{{ form|crispy }}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<input class="btn btn-primary" type="submit" value="Send Message">
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

View file

@ -1,10 +1,31 @@
<!DOCTYPE html> {% extends 'base.html' %}
<html lang="en"> {% block body_content %}
<head> <div class="row">
<meta charset="UTF-8"> <div class="col-md-12">
<title>Title</title> <div class="pricing-header px-3 py-3 pt-md-5 pb-md-4 mx-auto text-center">
</head> <h1 class="h2 text-center">{{ title }}</h1>
<body> <p class="lead"></p>
</div>
</body> {% for application in applications %}
</html> <div class="card">
<div class="card-body">
<h4 class="card-title">
<a href="{{ application.job.get_absolute_url }}">{{ application.job }}</a>
</h4>
<p class="card-text">
Submitted: {{ application.created }}
</p>
<p class="card-text">
{{ application.cover_text }}
</p>
<ol>
{% for answer in application.answers.all %}
<li>{{answer.question}}<p>{{answer.text}}</p></li>
{% endfor %}
</ol>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

View file

@ -7,22 +7,20 @@ app_name = 'jobs'
urlpatterns = [ urlpatterns = [
path('', views.Index.as_view(), name='index'), path('', views.Index.as_view(), name='index'),
path('jobs/create/', views.JobCreate.as_view(), name='job_create'), path('jobs/create/', views.JobCreate.as_view(), name='job_create'),
path('jobs/', views.JobList.as_view(), name='job_list'), path('jobs/me/', views.MyJobs.as_view(), name='my_jobs'),
path( path('jobs/<int:pk>/detail/', views.JobDetail.as_view(), name='job_detail'),
'jobs/<int:pk>/detail/', views.JobDetail.as_view(), name='job_detail'),
path('jobs/<int:pk>/renew/', views.JobRenew.as_view(), name='job_renew'), path('jobs/<int:pk>/renew/', views.JobRenew.as_view(), name='job_renew'),
path('jobs/messages/<int:pk>/', views.Conversation.as_view(), name='conversation'),
path('jobs/toggle/', views.change_status, name='job_toggle'),
path('jobs/<int:job_pk>/applications/', views.ApplicationList.as_view(), name='job_applications'), path('jobs/<int:job_pk>/applications/', views.ApplicationList.as_view(), name='job_applications'),
path( path('jobs/applications/me/', views.ListOwnApplications.as_view(), name='my_applications'),
'jobs/<int:job_pk>/apply/', path('jobs/applications/', views.ListJobApplications.as_view(), name='applications_others'),
views.ApplicationCreate.as_view(), path('jobs/messages/', views.MesssageInbox.as_view(), name='messages_inbox'),
name='job_apply'), path('jobs/messages/send/', views.send_message, name='send_message'),
path('jobs/<int:job_pk>/apply/', views.ApplicationCreate.as_view(), name='job_apply'),
] ]
# autocomplete endpoints # autocomplete endpoints
urlpatterns += [ urlpatterns += [
path( path('tag-autocomplete/', autocomplete_views.TagAutocomplete.as_view(create_field='name'), name='tag-autocomplete',),
'tag-autocomplete/',
autocomplete_views.TagAutocomplete.as_view(create_field='name'),
name='tag-autocomplete',
),
] ]

View file

@ -1,31 +1,28 @@
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import (View, TemplateView, ListView, CreateView, from django.views.generic import (View, TemplateView, ListView, CreateView, DetailView)
DetailView)
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from django.contrib import messages from django.contrib import messages
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404, redirect
from rules.contrib.views import PermissionRequiredMixin from rules.contrib.views import PermissionRequiredMixin
from django.contrib.auth import get_user_model
from .models import Job, Application, Question from .models import Job, Application, Question, JobMessage
from .forms import JobForm, QuestionFormSet, ApplicationForm, AnswerForm from .forms import JobForm, QuestionFormSet, ApplicationForm, AnswerForm, MessageForm
User = get_user_model()
class Index(TemplateView): class Index(TemplateView):
template_name = 'jobs/index.html' template_name = 'jobs/index.html'
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs) context = super().get_context_data(*args, **kwargs)
context['jobs'] = Job.objects.all().order_by('-updated')[:6] context['jobs'] = Job.objects.filter(active=True).order_by('-updated')[:6]
return context return context
class JobList(ListView):
context_object_name = 'jobs'
model = Job
class JobDetail(DetailView): class JobDetail(DetailView):
context_object_name = 'job' context_object_name = 'job'
model = Job model = Job
@ -88,7 +85,7 @@ class ApplicationCreate(LoginRequiredMixin, CreateView):
# TODO: restrict users from re-application # TODO: restrict users from re-application
model = Application model = Application
form_class = ApplicationForm form_class = ApplicationForm
success_url = reverse_lazy("jobs:job_list") success_url = reverse_lazy("jobs:index")
def get_question_queryset(self): def get_question_queryset(self):
# filter questions for particular job and order it so same queryset # filter questions for particular job and order it so same queryset
@ -153,5 +150,113 @@ class ApplicationList(LoginRequiredMixin, PermissionRequiredMixin, ListView):
return super().get_queryset().filter(job_id=self.kwargs['job_pk']) return super().get_queryset().filter(job_id=self.kwargs['job_pk'])
class ListOwnApplications(ListView):
model = Application
context_object_name = 'applications'
template_name = 'jobs/list_own_applications.html'
def get_queryset(self):
return Application.objects.filter(applicant=self.request.user)
def get_context_data(self, **kwargs):
context = super(ListOwnApplications, self).get_context_data(**kwargs)
context['title'] = 'Your Applications'
return context
class ListJobApplications(ListView):
model = Application
context_object_name = 'applications'
template_name = 'jobs/list_applications_others.html'
def get_queryset(self):
return Application.objects.filter(job__posted_by=self.request.user)
def get_context_data(self, **kwargs):
context = super(ListJobApplications, self).get_context_data(**kwargs)
context['title'] = 'Applications to your jobs'
context['form'] = MessageForm
return context
class ApplicationDetail(LoginRequiredMixin, DetailView): class ApplicationDetail(LoginRequiredMixin, DetailView):
model = Application model = Application
class MesssageInbox(LoginRequiredMixin, ListView):
model = JobMessage
context_object_name = 'jobmessage'
def get_queryset(self):
qs_list = []
qs = JobMessage.objects.filter(receiver=self.request.user).order_by('-date')
lookup = set(qs.values_list('sender', flat=True))
for sender_id in lookup:
for elem in qs:
if elem.sender_id == sender_id:
qs_list.append(elem.id)
break
return JobMessage.objects.filter(pk__in=qs_list).order_by('-date')
def get_context_data(self, **kwargs):
context = super(MesssageInbox, self).get_context_data(**kwargs)
context['title'] = 'Messages Inbox'
return context
class Conversation(LoginRequiredMixin, ListView):
model = JobMessage
context_object_name = 'jobmessage'
template_name = 'jobs/conversation.html'
def get_queryset(self):
queryset1 = JobMessage.objects.filter(
receiver=self.request.user, sender=self.kwargs['pk'])
queryset2 = JobMessage.objects.filter(
receiver=self.kwargs['pk'], sender=self.request.user)
return queryset1.union(queryset2).order_by('date')
def get_context_data(self, **kwargs):
context = super(Conversation, self).get_context_data(**kwargs)
context['form'] = MessageForm
context['person'] = User.objects.get(id=self.kwargs['pk'])
return context
class MyJobs(LoginRequiredMixin, ListView):
model = Job
context_object_name = 'jobs'
template_name = 'jobs/job.html'
def get_queryset(self):
return Job.objects.filter(posted_by=self.request.user).order_by('expires')
def get_context_data(self, **kwargs):
context = super(MyJobs, self).get_context_data(**kwargs)
context['title'] = 'Your jobs'
return context
def send_message(request):
form = MessageForm(request.POST)
if form.is_valid():
user = User.objects.get(id=request.POST.get('receiver_id'))
JobMessage.objects.create(sender=request.user, receiver=user, text=request.POST.get('text'))
else:
print("Error")
return redirect('jobs:conversation', pk=request.POST.get('receiver_id'))
def change_status(request):
print(request.POST)
job_id = request.POST.get('job_id')
if job_id is not None:
job = Job.objects.get(id=job_id)
if job.active:
job.active = False
else:
job.active = True
job.save()
else:
print("Error")
return redirect('jobs:my_jobs')

View file

@ -1,10 +1,18 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block title %}Login{% endblock %} {% block title %}Login{% endblock %}
{% block body_content %} {% block body_content %}
<h2>Login</h2>
<div class="row justify-content-md-center">
<div class="col-lg-6">
<h1 class="my-3 text-center">Login</h1>
<form method="post"> <form method="post">
{% csrf_token %} {{ form.as_p }} {% csrf_token %} {{ form|crispy }}
<button type="submit">Login</button> <button class="btn btn-primary btn-block" type="submit">Login</button>
</form> </form>
<div class="mt-4 mb-2">
Don't have an account? Sign up <a href="{% url 'signup' %}">here</a>. Don't have an account? Sign up <a href="{% url 'signup' %}">here</a>.
</div>
</div>
</div>
{% endblock %} {% endblock %}