Compare commits

...

30 Commits

Author SHA1 Message Date
app e7c044f87b Merge local changes 2020-09-02 14:35:01 +00:00
app a96a6eeb49 Commit uncommitted but migrated work on prod 2020-09-01 16:56:12 +00:00
wcolmenares 7702b51385 fixed toggle message/added logs in case of errors 2019-03-23 11:57:11 -04:00
wcolmenares 7108f538e2 fixed toggle message / added logs in case of errors 2019-03-23 11:53:28 -04:00
wcolmenares 1b893ac2f8 set active status in navbar 2019-03-23 09:43:55 -04:00
wcolmenares e1f6537165 added messages support, more clean html/bootstrap 2019-03-18 23:29:07 -04:00
wcolmenares 318feb7e97 added messages support, more clean html 2019-03-18 23:25:32 -04:00
wcolmenares 9529067c8b added messages support, more clean html 2019-03-18 23:24:37 -04:00
Nico Schottelius 2806ccbb44 Add open Source note 2019-02-09 20:37:40 +01:00
PCoder 763ce32159 Remove unused config 2019-02-04 20:19:29 +01:00
PCoder a0ecec665e Refactor code 2019-02-04 20:12:56 +01:00
PCoder 67c1dcb1f3 Change default user/search DNs 2019-02-04 19:59:41 +01:00
PCoder b0a09969ec Refactor LDAP_USER_DN to .env 2019-02-04 19:56:17 +01:00
PCoder 7131ed6d5b Update changelog for 1.1.0 2019-02-03 14:18:59 +01:00
PCoder 24a9e3ae60 Add /staticfiles to .gitignore 2019-02-03 14:17:21 +01:00
PCoder 93041f4539 Update changelog 2019-02-03 14:13:46 +01:00
PCoder 6abb78db2d Update .gitignore 2019-02-03 14:11:56 +01:00
PCoder 2196390d7a Handle more errors
Internal Server Error: /ipv6/work/signup/
Traceback (most recent call last):
  File "/home/app/pyvenv/lib/python3.5/site-packages/django/core/handlers/exception.py", line 34, in inner
    response = get_response(request)
  File "/home/app/pyvenv/lib/python3.5/site-packages/django/core/handlers/base.py", line 126, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/home/app/pyvenv/lib/python3.5/site-packages/django/core/handlers/base.py", line 124, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "./users/views.py", line 19, in signup
    create_user(username, raw_password, first_name, last_name, email)
  File "./users/ldap_funcs.py", line 18, in create_user
    uidNumber = get_max_uid() + 1
  File "./users/ldap_funcs.py", line 79, in get_max_uid
    return int(handler.read())
ValueError: invalid literal for int() with base 10: ''
2019-02-03 14:04:06 +01:00
PCoder 506679b8a0 Bugfix: TypeError: write() argument must be str, not int
Internal Server Error: /ipv6/work/signup/
Traceback (most recent call last):
  File "/home/app/pyvenv/lib/python3.5/site-packages/django/core/handlers/exception.py", line 34, in inner
    response = get_response(request)
  File "/home/app/pyvenv/lib/python3.5/site-packages/django/core/handlers/base.py", line 126, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/home/app/pyvenv/lib/python3.5/site-packages/django/core/handlers/base.py", line 124, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "./users/views.py", line 19, in signup
    create_user(username, raw_password, first_name, last_name, email)
  File "./users/ldap_funcs.py", line 38, in create_user
    set_max_uid(uidNumber)
  File "./users/ldap_funcs.py", line 68, in set_max_uid
    handler.write(max_uid)
TypeError: write() argument must be str, not int
2019-02-03 13:55:37 +01:00
PCoder 0fb34dc560 Save uidNumber used for the latest user created for future reference 2019-02-03 13:50:47 +01:00
PCoder a2550e4bff Remove redundant code 2019-02-03 13:43:23 +01:00
PCoder 1bd4b42fe9 Bugfix: use correct setting name 2019-02-03 13:43:08 +01:00
PCoder fd8f5fc1d3 Bugfix: use correct function and return value 2019-02-03 13:34:29 +01:00
PCoder 88a58595fb Refactor code 2019-02-03 13:31:36 +01:00
PCoder c72f97de42 Add code to create user's unix profile also 2019-02-03 13:05:44 +01:00
PCoder f3d90a2b7d Add utility code to get max UID
Other changes:
- Introduce logging
- Introduce .env config parameters
  - LDAP_SEARCH_BASE: The base used in the LDAP search to find uid
  - IPV6_WORK_USER_GROUP: The LDAP group to which the newly added
      user should belong to
2019-02-03 12:59:22 +01:00
PCoder 8e07151837 Add ldap3 specific logging 2019-02-03 10:22:17 +01:00
PCoder c670eb52e9 Fix bug reading boolean config casting 2019-02-03 10:20:00 +01:00
PCoder 78098a3692 Update changelog 2019-02-03 10:15:29 +01:00
PCoder 38c7283cb6 Enable logging by setting by setting `ENABLE_DEBUG_LOG` in .env
Also set the modules to log by using `MODULES_TO_LOG` config
parameter in .env
2019-02-03 10:13:18 +01:00
22 changed files with 613 additions and 95 deletions

3
.gitignore vendored
View File

@ -94,3 +94,6 @@ db.sqlite3
/static
media/
uwsgi.ini
/staticfiles
ipv6work/ldap_max_uid_file

View File

@ -2,6 +2,11 @@
## [Unreleased]
## [1.1.0] - 2019-02-03
- Enable logging by setting by setting `ENABLE_DEBUG_LOG` and `MODULES_TO_LOG` in .env
- Create unix profiles for the registered users also
## [1.0.0] - 2019-02-02
### Added
- The first version of the project

View File

@ -12,9 +12,12 @@ https://docs.djangoproject.com/en/2.1/ref/settings/
import os
import ldap
import logging
from decouple import config, Csv
from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion
logger = logging.getLogger(__name__)
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@ -174,6 +177,18 @@ MEDIA_URL = FORCE_SCRIPT_NAME + 'media/'
CRISPY_TEMPLATE_PACK = 'bootstrap4'
LDAP_MAX_UID_PATH = os.path.join(
os.path.abspath(os.path.dirname(__file__)),
'ldap_max_uid_file'
)
LDAP_IPV6_WORK_USER_GROUP = config('LDAP_IPV6_WORK_USER_GROUP', cast=int)
LDAP_DEFAULT_START_UID = config(
'LDAP_DEFAULT_START_UID', cast=int, default=10000
)
LDAP_CUSTOMER_DN = config('LDAP_CUSTOMER_DN', default='ou=customer,dc=ungleich,dc=ch')
AUTH_LDAP_SERVER_URI = config('AUTH_LDAP_SERVER_URI')
AUTH_LDAP_BIND_DN = config('AUTH_LDAP_BIND_DN')
@ -181,7 +196,7 @@ AUTH_LDAP_BIND_PASSWORD = config('AUTH_LDAP_BIND_PASSWORD')
AUTH_LDAP_USER_SEARCH = LDAPSearchUnion(
LDAPSearch("ou=users,dc=ungleich,dc=ch",
ldap.SCOPE_SUBTREE, "(uid=%(user)s)"),
LDAPSearch("ou=customers,dc=ungleich,dc=ch",
LDAPSearch(LDAP_CUSTOMER_DN,
ldap.SCOPE_SUBTREE, "(uid=%(user)s)"),
)
@ -206,3 +221,34 @@ LOGGING = {
},
},
}
if config('ENABLE_DEBUG_LOG', cast=bool, default=False):
loggers_dict = {}
MODULES_TO_LOG = config('MODULES_TO_LOG', 'django')
LOGGING['handlers']['file'] = {
'level': 'DEBUG',
'class': 'logging.FileHandler',
'filename': os.path.join(BASE_DIR, 'debug.log'),
}
if MODULES_TO_LOG is None:
# set MODULES_TO_LOG to django, if it is not set
MODULES_TO_LOG = 'django'
modules_to_log_list = MODULES_TO_LOG.split(',')
for custom_module in modules_to_log_list:
logger_item = {
custom_module: {
'handlers': ['file'],
'level': 'DEBUG',
'propagate': True
}
}
loggers_dict.update(logger_item)
LOGGING['loggers'] = loggers_dict
if 'ldap3' in modules_to_log_list:
from ldap3.utils.log import (
set_library_log_detail_level, OFF, BASIC, NETWORK, EXTENDED
)
set_library_log_detail_level(BASIC)

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,20 +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">
<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>
<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 {{ application_page }} 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 {{ messages_page }}">
<a class="nav-link" href="{% url 'jobs:messages_inbox' %}">Messages</a>
</li>
<li class="nav-item {{ my_jobs_page }} justify-content-end">
<a class="nav-link" href="{% url 'jobs:my_jobs' %}">Your Jobs</a>
</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>
<div class="container">
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissable">
@ -34,20 +57,46 @@
{{ 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>.</small>
<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>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
</body>
</html>
</html>

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,30 @@
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 django.contrib.auth.decorators import login_required
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
import logging
logger = logging.getLogger(__name__)
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
@ -153,5 +152,120 @@ 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'
context['application_page'] = 'active'
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)
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'
context['messages_page'] = 'active'
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'])
context['messages_page'] = 'active'
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'
context['my_jobs_page'] = 'active'
return context
@login_required()
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'))
@login_required()
def change_status(request):
try:
job_obj = Job.objects.get(id=request.POST.get('job_id'))
if job_obj.posted_by == request.user:
if job_obj.active:
job_obj.active = False
else:
job_obj.active = True
job_obj.save()
else:
logger.error(
"the user {} tried to toggle the job id {}. But it doesn't belong to him".format(
request.user, request.POST.get('job_id'))
)
except Job.DoesNotExist:
logger.error(
"the user {} tried to toggle the job id {} but it doesn't exist.".format(
request.user, request.POST.get('job_id'))
)
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' %}
{% 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 %}

View File

@ -1,28 +1,96 @@
from django.conf import settings
from ldap3 import Server, ServerPool, Connection, ObjectDef, AttrDef, Reader, Writer
from ldap3 import Server, Connection, ObjectDef, Writer, SUBTREE
import logging
logger = logging.getLogger(__name__)
server = Server(settings.AUTH_LDAP_SERVER_URI)
def create_user(user, password, firstname, lastname, email):
logger.debug("In create_user")
conn = Connection(server, settings.AUTH_LDAP_BIND_DN,
settings.AUTH_LDAP_BIND_PASSWORD)
if not conn.bind():
logger.error("conn.bind() returned False. Could not connect.")
raise Exception('Could not connect to LDAP Server')
obj_new_user = ObjectDef(
['inetOrgPerson'], conn)
obj_new_user = ObjectDef(['inetOrgPerson', 'posixAccount'], conn)
uidNumber = get_max_uid() + 1
logger.debug("uidNumber={uidNumber}".format(uidNumber=uidNumber))
results = True
while results:
results = conn.search(
search_base=settings.LDAP_CUSTOMER_DN,
search_filter=(
'(&(objectClass=inetOrgPerson)(objectClass=posixAccount)'
'(objectClass=top)(uidNumber={uidNumber}))'.format(
uidNumber=uidNumber
)
),
search_scope=SUBTREE,
attributes=['uidNumber'],
)
if results:
logger.debug("{uid} exists. Trying next.".format(uid=uidNumber))
uidNumber += 1
logger.debug("{uid} does not exist. Using it".format(uid=uidNumber))
set_max_uid(uidNumber)
w = Writer(conn, obj_new_user)
dn = 'uid=%s,ou=users,dc=ungleich,dc=ch' % user
dn = 'uid=%s,%s' % (user, settings.LDAP_CUSTOMER_DN)
w.new(dn)
w[0].givenName = firstname
w[0].sn = lastname
w[0].cn = firstname + " " + lastname
w[0].mail = email
w[0].userPassword = password
w[0].gidNumber = settings.LDAP_IPV6_WORK_USER_GROUP
w[0].uidNumber = uidNumber
w[0].homeDirectory = "/home/" + user
if not w.commit():
conn.unbind()
logger.error("w.commit() returned False. Could not write user.")
raise Exception("Couldn't write user")
logger.debug("Created user {user} successfully.".format(user=user))
conn.unbind()
return True
def set_max_uid(max_uid):
"""
a utility function to save max_uid value to a file
:param max_uid: an integer representing the max uid
:return:
"""
with open(settings.LDAP_MAX_UID_PATH, 'w+') as handler:
handler.write(str(max_uid))
def get_max_uid():
"""
A utility function to read the max uid value that was previously set
:return: An integer representing the max uid value that was previously set
"""
try:
with open(settings.LDAP_MAX_UID_PATH, 'r+') as handler:
try:
return_value = int(handler.read())
except ValueError as ve:
logger.error(
"Error reading int value from {}. {}"
"Returning default value {} instead".format(
settings.LDAP_MAX_UID_PATH,
str(ve),
settings.LDAP_DEFAULT_START_UID
)
)
return_value = settings.LDAP_DEFAULT_START_UID
return return_value
except FileNotFoundError as fnfe:
logger.error("File not found : " + str(fnfe))
retrun_value = settings.LDAP_DEFAULT_START_UID
logger.error("So, returning UID={}".format(retrun_value))
return retrun_value