Commit uncommitted but migrated work on prod

This commit is contained in:
app 2020-09-01 16:56:12 +00:00
parent 2806ccbb44
commit a96a6eeb49
18 changed files with 472 additions and 90 deletions

View file

@ -1,5 +1,5 @@
from django.contrib import admin
from .models import Job, Application, Tag, Question, Answer
from .models import Job, Application, Tag, Question, Answer, JobMessage
admin.site.register([Job, Application, Tag, Question, Answer])
admin.site.register([Job, Application, Tag, Question, Answer, JobMessage])

View file

@ -2,7 +2,7 @@ from django import forms
from django.forms import inlineformset_factory
from dal.autocomplete import ModelSelect2Multiple
from .models import Job, Question, Application, Answer
from .models import Job, Question, Application, Answer, JobMessage
class JobForm(forms.ModelForm):
@ -13,6 +13,12 @@ class JobForm(forms.ModelForm):
'tags': ModelSelect2Multiple(url='jobs:tag-autocomplete')
}
class MessageForm(forms.ModelForm):
text = forms.CharField(label='', widget=forms.TextInput(attrs={"placeholder": "Write your message"}))
class Meta:
model = JobMessage
fields = ('text',)
class QuestionForm(forms.ModelForm):
class Meta:

View file

@ -0,0 +1,26 @@
# Generated by Django 2.1.2 on 2019-03-16 03:23
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('jobs', '0003_auto_20181020_0622'),
]
operations = [
migrations.CreateModel(
name='JobMessage',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('text', models.TextField()),
('date', models.DateTimeField(auto_now_add=True)),
('receiver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='receiver', to=settings.AUTH_USER_MODEL)),
('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sender', to=settings.AUTH_USER_MODEL)),
],
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 2.1.2 on 2019-03-17 02:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('jobs', '0004_jobmessage'),
]
operations = [
migrations.AddField(
model_name='job',
name='active',
field=models.BooleanField(default=True),
),
]

View file

@ -42,6 +42,8 @@ class Job(models.Model):
posted_by = models.ForeignKey(
User, related_name="jobs", on_delete=models.CASCADE)
active = models.BooleanField(default=True)
def renew(self):
self.expires = after_30_days()
self.save()
@ -96,3 +98,13 @@ class Answer(models.Model):
def __str__(self):
return "Answer for : {0} - {1}".format(self.question, self.application)
class JobMessage(models.Model):
"""Basic user to user messaging app"""
sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name="sender")
receiver = models.ForeignKey(User, on_delete=models.CASCADE, related_name="receiver")
text = models.TextField()
date = models.DateTimeField(auto_now_add=True)
def __str__(self):
return '{}, {}'.format(self.sender, self.text[:50])

View file

@ -13,19 +13,43 @@
<title>{% block title %}{% endblock %}</title>
</head>
<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">
<a class="navbar-brand" href="{% url 'jobs:index' %}"><img src="{% static 'img/logo.svg' %}" alt=""></a>
{% if request.user.is_authenticated %}
<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 %}
</div>
</nav>
<h5 class="my-0 mr-md-auto font-weight-normal"><a href="{% url 'jobs:index' %}"><img src="{% static 'img/logo.svg' %}"/></a></h5>
<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 %}
<a class="btn text-dark" href="{% url 'logout' %}">Logout</a>
{% endif %}
</div>
<div class="container">
{% for message in messages %}
@ -34,23 +58,43 @@
{{ message }}
</div>
{% endfor %}
{% block body_content %}{% endblock %}
{% block body_content %}
{% endblock %}
<footer class="pt-4 my-md-5 pt-md-5 border-top">
<div class="row">
<div class="col-12 text-center">
<small class="text-muted">&copy; {% now 'Y' %}
This service is provided to you
by <a href="https://ungleich.ch/"
target="_blank">ungleich</a>.
IPv6.work is
<small class="text-muted">&copy; {% now 'Y' %}</small>
</div>
</div>
<div class="row">
<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">
Open Source</a>.</small>
</div>
</div>
</footer>
<style>
.my-custom-scrollbar {
position: relative;
height: 400px;
overflow: auto;
word-break: break-all;
}
.table-wrapper-scroll-y {
display: block;
}
</style>
</div>
<!-- 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://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,31 +0,0 @@
{% extends 'base.html' %}
{% block body_content %}
<div class="row">
<div class="col-md-12">
<div class="pricing-header px-3 py-3 pt-md-5 pb-md-4 mx-auto text-center">
<h1 class="display-4">Applications</h1>
<p class="lead"></p>
</div>
{% for application in applications %}
<div class="card">
<div class="card-body">
<h4 class="card-title">
<a href="{{ application.get_absolute_url }}">Submitted by: {{ application.applicant }}</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

@ -0,0 +1,36 @@
{% extends 'base.html' %}
{% block body_content %}
{% load crispy_forms_tags %}
<div class="row">
<div class="col-md-12">
<div class="pricing-header px-3 py-3 pt-md-5 pb-md-4 mx-auto text-center">
<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>
{% endblock %}

View file

@ -11,12 +11,12 @@
<section class="bg-light-gray">
<div class="row">
{% 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">
<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 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>
</div>
</div>

View file

@ -0,0 +1,42 @@
{% extends 'base.html' %}
{% block body_content %}
<div class="row">
<div class="col-md-12">
<div class="pricing-header px-3 py-3 pt-md-5 pb-md-4 mx-auto text-center">
<h1 class="display-4">{{ title }}</h1>
<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 %}

View file

@ -8,12 +8,12 @@
<div class="col-md-12">
<form method="POST" action=".">
{% csrf_token %}
{{ form |crispy }}
{{ form|crispy }}
<h4>Screening Questions</h4>
{{ question_form |crispy }}
{{ question_form|crispy }}
<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.media}}
</div>

View file

@ -0,0 +1,30 @@
{% extends 'base.html' %}
{% block body_content %}
<div class="row">
<div class="col-md-12">
<div class="pricing-header px-3 py-3 pt-md-5 pb-md-4 mx-auto text-center">
<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">User</th>
<th scope="col">Last Message</th>
</tr>
</thead>
<tbody>
{% for message in jobmessage %}
<tr onclick="window.location.href = '{{ message.sender_id }}';">
<td>{{ message.sender }}</td>
<td>{{ message.text|truncatechars:40 }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View file

@ -0,0 +1,57 @@
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block body_content %}
<div class="row">
<div class="col-md-12">
<div class="pricing-header px-3 py-3 pt-md-5 pb-md-4 mx-auto text-center">
<h1 class="h2 text-center">{{ title }}</h1>
<p class="lead"></p>
</div>
{% 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

@ -0,0 +1,31 @@
{% extends 'base.html' %}
{% block body_content %}
<div class="row">
<div class="col-md-12">
<div class="pricing-header px-3 py-3 pt-md-5 pb-md-4 mx-auto text-center">
<h1 class="h2 text-center">{{ title }}</h1>
<p class="lead"></p>
</div>
{% 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>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

View file

@ -7,22 +7,20 @@ app_name = 'jobs'
urlpatterns = [
path('', views.Index.as_view(), name='index'),
path('jobs/create/', views.JobCreate.as_view(), name='job_create'),
path('jobs/', views.JobList.as_view(), name='job_list'),
path(
'jobs/<int:pk>/detail/', views.JobDetail.as_view(), name='job_detail'),
path('jobs/me/', views.MyJobs.as_view(), name='my_jobs'),
path('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/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>/apply/',
views.ApplicationCreate.as_view(),
name='job_apply'),
path('jobs/applications/me/', views.ListOwnApplications.as_view(), name='my_applications'),
path('jobs/applications/', views.ListJobApplications.as_view(), name='applications_others'),
path('jobs/messages/', views.MesssageInbox.as_view(), name='messages_inbox'),
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
urlpatterns += [
path(
'tag-autocomplete/',
autocomplete_views.TagAutocomplete.as_view(create_field='name'),
name='tag-autocomplete',
),
path('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.http import HttpResponseRedirect
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import (View, TemplateView, ListView, CreateView,
DetailView)
from django.views.generic import (View, TemplateView, ListView, CreateView, DetailView)
from django.views.generic.detail import SingleObjectMixin
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 django.contrib.auth import get_user_model
from .models import Job, Application, Question
from .forms import JobForm, QuestionFormSet, ApplicationForm, AnswerForm
from .models import Job, Application, Question, JobMessage
from .forms import JobForm, QuestionFormSet, ApplicationForm, AnswerForm, MessageForm
User = get_user_model()
class Index(TemplateView):
template_name = 'jobs/index.html'
def get_context_data(self, *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
class JobList(ListView):
context_object_name = 'jobs'
model = Job
class JobDetail(DetailView):
context_object_name = 'job'
model = Job
@ -88,7 +85,7 @@ class ApplicationCreate(LoginRequiredMixin, CreateView):
# TODO: restrict users from re-application
model = Application
form_class = ApplicationForm
success_url = reverse_lazy("jobs:job_list")
success_url = reverse_lazy("jobs:index")
def get_question_queryset(self):
# 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'])
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):
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,4 +1,4 @@
Django==2.1.2
Django==2.2.16
django-auth-ldap==1.7.0
django-autocomplete-light==3.3.2
django-crispy-forms==1.7.2

View file

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