diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6a7d7936..73fd03fb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,6 @@
+# v1.23.0 (XXXX-XX-XX)
+- Simplify user model by moving Consultant information over to user instead
+
# v1.22.0 (2024-07-31)
- Add new reports in process analysis: count process per interview numbers, candidates source per subsidiary
- Add subsidiary model field: show the subsidiary by default in the report analysis
diff --git a/interview/decorators.py b/interview/decorators.py
index e47b131a..bb58ba50 100644
--- a/interview/decorators.py
+++ b/interview/decorators.py
@@ -2,7 +2,7 @@
from django.contrib.auth.views import redirect_to_login
-from ref.models import Consultant
+from ref.models import PyouPyouUser
# Strongly inspired by django's user_passes_test decorator
@@ -20,8 +20,8 @@ def decorator(view_func):
def wrapper(request, *args, **kwargs):
authorised = authorised_level
if authorised is None:
- authorised = [Consultant.PrivilegeLevel.ALL]
- if request.user.is_authenticated and request.user.consultant.privilege in authorised:
+ authorised = [PyouPyouUser.PrivilegeLevel.ALL]
+ if request.user.is_authenticated and request.user.privilege in authorised:
return view_func(request, *args, **kwargs)
return redirect_to_login(next=request.path)
diff --git a/interview/factory.py b/interview/factory.py
index abb53e05..7087f327 100644
--- a/interview/factory.py
+++ b/interview/factory.py
@@ -8,7 +8,7 @@
from interview.models import Interview, Sources, SourcesCategory, ContractType, InterviewKind, Process
from ref.factory import SubsidiaryFactory
-from ref.models import Subsidiary, Consultant
+from ref.models import Subsidiary, PyouPyouUser
test_tz = pytz.timezone("Europe/Paris")
@@ -49,12 +49,12 @@ def negative_end_process(process, itw, next_planned_date):
def get_available_consultants_for_itw(subsidiary, all_itw_given_process):
# retrieve available consultants that have not yet been involved in the process
- possible_interviewer = Consultant.objects.filter(company=subsidiary).exclude(
+ possible_interviewer = PyouPyouUser.objects.filter(company=subsidiary).exclude(
id__in=list(all_itw_given_process.values_list("interviewers", flat=True))
)
# if all consultants were already involved in the process, choose one at random
if not possible_interviewer:
- possible_interviewer = Consultant.objects.filter(company=subsidiary)
+ possible_interviewer = PyouPyouUser.objects.filter(company=subsidiary)
return possible_interviewer
diff --git a/interview/feeds.py b/interview/feeds.py
index 91428117..89ad339a 100644
--- a/interview/feeds.py
+++ b/interview/feeds.py
@@ -7,7 +7,7 @@
from django.utils import timezone
from interview.models import Interview
-from ref.models import Subsidiary, PyouPyouUser, Consultant
+from ref.models import Subsidiary, PyouPyouUser
class AbstractPyoupyouInterviewFeed(ICalFeed):
@@ -17,12 +17,12 @@ class AbstractPyoupyouInterviewFeed(ICalFeed):
def __call__(self, request, *args, **kwargs):
user = PyouPyouUser.objects.filter(token=kwargs["token"]).first()
del kwargs["token"]
- if not user or not user.is_active or user.consultant.privilege != Consultant.PrivilegeLevel.ALL:
+ if not user or not user.is_active or user.privilege != PyouPyouUser.PrivilegeLevel.ALL:
return HttpResponse("Unauthenticated user", status=401)
return super().__call__(request, *args, **kwargs)
def item_title(self, item):
- itws = ", ".join([i.user.trigramme for i in item.interviewers.all()])
+ itws = ", ".join([i.trigramme for i in item.interviewers.all()])
return escape(force_str("#{} {} [{}]".format(item.rank, item.process.candidate.name, itws)))
def item_description(self, item):
@@ -102,4 +102,4 @@ def get_object(self, request, user_id=None):
def items(self, user):
last_month = timezone.now() - timedelta(days=30)
- return Interview.objects.filter(interviewers__user=user, planned_date__gte=last_month).order_by("-planned_date")
+ return Interview.objects.filter(interviewers=user, planned_date__gte=last_month).order_by("-planned_date")
diff --git a/interview/filters.py b/interview/filters.py
index c76f7498..8a7864af 100644
--- a/interview/filters.py
+++ b/interview/filters.py
@@ -1,7 +1,8 @@
import django_filters
from interview.models import Process, Interview
-from ref.models import Subsidiary, Consultant
+
+from ref.models import Subsidiary, PyouPyouUser
from django.utils.translation import gettext_lazy as _
@@ -22,7 +23,7 @@ class InterviewSummaryFilter(django_filters.FilterSet):
class InterviewListFilter(django_filters.FilterSet):
last_state_change = django_filters.DateFromToRangeFilter(field_name="planned_date")
interviewer = django_filters.ModelChoiceFilter(
- queryset=Consultant.objects.filter(user__is_active=True).select_related("user"), field_name="interviewers"
+ queryset=PyouPyouUser.objects.filter(is_active=True), field_name="interviewers"
)
state = django_filters.ChoiceFilter(choices=Interview.ITW_STATE, field_name="state", empty_label=_("All states"))
diff --git a/interview/forms.py b/interview/forms.py
index 21577eea..3d50ca5d 100644
--- a/interview/forms.py
+++ b/interview/forms.py
@@ -9,19 +9,20 @@
from crispy_forms.layout import Layout, Div, Submit, Column, Field
from django_select2.forms import ModelSelect2MultipleWidget, ModelSelect2Widget
-from interview.models import Consultant, Interview, Candidate, Process, Sources, Offer
+from interview.models import Interview, Candidate, Process, Sources, Offer
+from ref.models import PyouPyouUser
class MultipleConsultantWidget(ModelSelect2MultipleWidget):
- model = Consultant
- queryset = Consultant.objects.filter(user__is_active=True)
- search_fields = ["user__trigramme__icontains", "user__full_name__icontains"]
+ model = PyouPyouUser
+ queryset = PyouPyouUser.objects.filter(is_active=True)
+ search_fields = ["trigramme__icontains", "full_name__icontains"]
class SingleConsultantWidget(ModelSelect2Widget):
- model = Consultant
- queryset = Consultant.objects.filter(user__is_active=True)
- search_fields = ["user__trigramme__icontains", "user__full_name__icontains"]
+ model = PyouPyouUser
+ queryset = PyouPyouUser.objects.filter(is_active=True)
+ search_fields = ["trigramme__icontains", "full_name__icontains"]
class SourcesWidget(ModelSelect2Widget):
diff --git a/interview/management/commands/create_dev_dataset.py b/interview/management/commands/create_dev_dataset.py
index 91997f74..3673d1f2 100644
--- a/interview/management/commands/create_dev_dataset.py
+++ b/interview/management/commands/create_dev_dataset.py
@@ -22,8 +22,7 @@
InterviewFactory,
)
from interview.models import ContractType, SourcesCategory, InterviewKind, Interview, Process, Sources
-from ref.factory import SubsidiaryFactory, PyouPyouUserFactory, ConsultantFactory
-from ref.models import Consultant
+from ref.factory import SubsidiaryFactory, PyouPyouUserFactory
from interview.factory import (
date_minus_time_ago,
date_random_plus_minus_time,
@@ -89,11 +88,11 @@ def handle(self, *args, **options):
# create consultants for this subsidiary
subsidiary_consultants = []
for k in range(5):
- subsidiary_consultants.append(ConsultantFactory(company=subsidiary))
+ subsidiary_consultants.append(PyouPyouUserFactory(company=subsidiary))
# we need at least one consultant which is both a superuser and staff to access the admin board
# note: superusers cannot be created with manage.py because they also need a consultant
- admin = subsidiary_consultants[0].user
+ admin = subsidiary_consultants[0]
admin.is_superuser = True
admin.is_staff = True
admin.save()
diff --git a/interview/migrations/0025_auto_20230116_1355.py b/interview/migrations/0025_auto_20230116_1355.py
new file mode 100644
index 00000000..c79c760b
--- /dev/null
+++ b/interview/migrations/0025_auto_20230116_1355.py
@@ -0,0 +1,38 @@
+# Generated by Django 3.2.16 on 2023-01-16 12:55
+
+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),
+ ("interview", "0024_merge_20221212_1657"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="interview",
+ name="interviewers",
+ field=models.ManyToManyField(to=settings.AUTH_USER_MODEL),
+ ),
+ migrations.AlterField(
+ model_name="process",
+ name="creator",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="process_creator",
+ to=settings.AUTH_USER_MODEL,
+ verbose_name="Process creator",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="process",
+ name="responsible",
+ field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL),
+ ),
+ ]
diff --git a/interview/migrations/0028_merge_20241021_1741.py b/interview/migrations/0028_merge_20241021_1741.py
new file mode 100644
index 00000000..01e1e790
--- /dev/null
+++ b/interview/migrations/0028_merge_20241021_1741.py
@@ -0,0 +1,13 @@
+# Generated by Django 4.2.4 on 2024-10-21 15:41
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("interview", "0025_auto_20230116_1355"),
+ ("interview", "0027_alter_offer_options"),
+ ]
+
+ operations = []
diff --git a/interview/models.py b/interview/models.py
index 91d77300..06aa2701 100644
--- a/interview/models.py
+++ b/interview/models.py
@@ -19,7 +19,7 @@
from django.utils.translation import gettext_lazy as _
from pyoupyou.settings import MINUTE_FORMAT, STALE_DAYS
-from ref.models import Consultant, Subsidiary, PyouPyouUser
+from ref.models import Subsidiary, PyouPyouUser
CharField.register_lookup(Lower)
@@ -219,22 +219,18 @@ def for_user(self, user):
q = (
super()
.get_queryset()
- .filter(
- Q(start_date__gte=user.date_joined)
- | Q(responsible__in=[user.consultant])
- | Q(interview__interviewers__user=user)
- )
+ .filter(Q(start_date__gte=user.date_joined) | Q(responsible__in=[user]) | Q(interview__interviewers=user))
.distinct()
)
- if user.consultant.is_external:
- q = q.filter(sources=user.consultant.limited_to_source)
+ if user.is_external:
+ q = q.filter(sources=user.limited_to_source)
return q
def for_table(self, user):
qs = (
self.for_user(user)
.select_related("subsidiary", "candidate", "contract_type")
- .prefetch_related("responsible__user")
+ .prefetch_related("responsible")
.annotate(current_rank=Count("interview", distinct=True))
)
return qs
@@ -311,7 +307,7 @@ class Process(models.Model):
contract_duration = models.PositiveIntegerField(verbose_name=_("Contract duration in month"), null=True, blank=True)
contract_start_date = models.DateField(null=True, blank=True)
sources = models.ForeignKey(Sources, null=True, blank=True, on_delete=models.SET_NULL)
- responsible = models.ManyToManyField(Consultant, blank=True)
+ responsible = models.ManyToManyField(PyouPyouUser, blank=True)
state = models.CharField(
max_length=3, choices=PROCESS_STATE, verbose_name=_("Closed reason"), default=WAITING_INTERVIEWER_TO_BE_DESIGNED
)
@@ -323,7 +319,7 @@ class Process(models.Model):
other_informations = models.TextField(verbose_name=_("Other informations"), blank=True)
creator = models.ForeignKey(
- Consultant,
+ PyouPyouUser,
null=True,
blank=True,
on_delete=models.SET_NULL,
@@ -456,20 +452,22 @@ def trigger_notification(self, is_new):
# add subsidiary responsible to recipient list
if self.subsidiary.responsible:
- recipient_list.append(self.subsidiary.responsible.user.email)
+ recipient_list.append(self.subsidiary.responsible.email)
# add users subscribed to offer's notification
+
if self.offer:
recipient_list = recipient_list + [user.email for user in self.offer.subscribers.all()]
recipient_list = recipient_list + [user.email for user in self.subscribers.all()]
+ # not sure about line 465 about the merge conflict...
mail.send_mail(
subject=subject, message=body, from_email=settings.MAIL_FROM, recipient_list=set(recipient_list)
)
def get_all_interviewers_for_process(self):
- return PyouPyouUser.objects.filter(consultant__interview__process=self)
+ return PyouPyouUser.objects.filter(interview__process=self)
class InterviewKind(models.Model):
@@ -484,11 +482,11 @@ def for_user(self, user):
q = (
super(InterviewManager, self)
.get_queryset()
- .filter(Q(process__start_date__gte=user.date_joined) | Q(interviewers__in=[user.consultant]))
+ .filter(Q(process__start_date__gte=user.date_joined) | Q(interviewers__in=[user]))
.distinct()
)
- if user.consultant.is_external:
- q = q.filter(process__sources=user.consultant.limited_to_source)
+ if user.is_external:
+ q = q.filter(process__sources=user.limited_to_source)
return q
def for_table(self, user):
@@ -502,7 +500,7 @@ def for_table(self, user):
"kind_of_interview",
"process__offer__subsidiary",
)
- .prefetch_related("interviewers__user")
+ .prefetch_related("interviewers")
)
return qs
@@ -542,7 +540,7 @@ class Interview(models.Model):
state = models.CharField(max_length=3, choices=ITW_STATE, verbose_name=_("next state"))
rank = models.IntegerField(verbose_name=_("Rank"), blank=True, null=True)
planned_date = models.DateTimeField(verbose_name=_("Planned date"), blank=True, null=True)
- interviewers = models.ManyToManyField(Consultant)
+ interviewers = models.ManyToManyField(PyouPyouUser)
minute = models.TextField(verbose_name=_("Minute"), blank=True)
minute_format = models.CharField(max_length=3, choices=MINUTE_FORMAT, default=MINUTE_FORMAT[0][0])
@@ -554,7 +552,7 @@ class Interview(models.Model):
)
def __str__(self):
- interviewers = ", ".join(i.user.trigramme for i in self.interviewers.all())
+ interviewers = ", ".join(i.trigramme for i in self.interviewers.all())
return "#{rank} - {process} - {itws}".format(rank=self.rank, process=self.process, itws=interviewers)
def save(self, force_insert=False, force_update=False, using=None, update_fields=None, trigger_notification=True):
@@ -639,19 +637,21 @@ def needs_attention(self):
@property
def interviewers_str(self):
if self.id:
- return ", ".join(i.user.get_full_name() for i in self.interviewers.all())
+ return ", ".join(i.get_full_name() for i in self.interviewers.all())
return ""
@property
def interviewers_trigram_slug(self):
if self.id:
- return "-".join(i.user.trigramme for i in self.interviewers.all())
+ return "-".join(i.trigramme for i in self.interviewers.all())
return ""
def trigger_notification(self):
recipient_list = self.process.subsidiary.notification_emails
+ if self.process.subsidiary.responsible:
+ recipient_list.append(self.process.subsidiary.responsible.email)
if self.id:
- recipient_list = recipient_list + [i.user.email for i in self.interviewers.all()]
+ recipient_list = recipient_list + [i.email for i in self.interviewers.all()]
subject = None
body_template = None
diff --git a/interview/templates/interview/base.html b/interview/templates/interview/base.html
index 588e7349..eecb016a 100644
--- a/interview/templates/interview/base.html
+++ b/interview/templates/interview/base.html
@@ -35,7 +35,7 @@
-
{% blocktrans with subsidiary=user.consultant.company %} {{subsidiary}} processes{% endblocktrans %}
+
{% blocktrans with subsidiary=user.company %} {{subsidiary}} processes{% endblocktrans %}
{% render_table subsidiary_processes_table %}
diff --git a/interview/templates/interview/interview_minute.html b/interview/templates/interview/interview_minute.html
index e0f06497..ca1bc7eb 100644
--- a/interview/templates/interview/interview_minute.html
+++ b/interview/templates/interview/interview_minute.html
@@ -32,7 +32,7 @@
{% trans "Minute" %}
{% elif interview.state == "NO" %}
{% endif %}
- {% if user.consultant in interview.interviewers.all %}
+ {% if user in interview.interviewers.all %}
{% trans "Change minute" %}
{% endif %}
diff --git a/interview/templates/interview/process_detail.html b/interview/templates/interview/process_detail.html
index 61e98495..03642359 100644
--- a/interview/templates/interview/process_detail.html
+++ b/interview/templates/interview/process_detail.html
@@ -167,7 +167,7 @@
{% trans 'Contract information' %}
{% trans "Interviews for this process" %}
{% render_table interviews_for_process_table %}
- {% if user.consultant.privilege != user.consultant.PrivilegeLevel.EXTERNAL_READONLY %}
+ {% if user.privilege != user.PrivilegeLevel.EXTERNAL_READONLY %}
@@ -192,9 +192,8 @@ Close Process
-
-
- {% if user.consultant.privilege == user.consultant.PrivilegeLevel.ALL or user.consultant.privilege == user.consultant.PrivilegeLevel.EXTERNAL_RPO %}
+
+ {% if user.privilege == user.PrivilegeLevel.ALL or user.privilege == user.PrivilegeLevel.EXTERNAL_RPO %}
{% if process.is_open %}
{% trans "Close this process" %}
diff --git a/interview/templates/interview/tables/interview_actions.html b/interview/templates/interview/tables/interview_actions.html
index a0e8fa08..6d0739a8 100644
--- a/interview/templates/interview/tables/interview_actions.html
+++ b/interview/templates/interview/tables/interview_actions.html
@@ -1,5 +1,5 @@
{% load i18n %}
-
{{ record.get_state_display }}
- {% if user.consultant in record.interviewers.all %}
+ {% if user in record.interviewers.all %}
{% if not record.planned_date %}
{% csrf_token %}
{% if record.planning_request_sent %}
diff --git a/interview/templates/interview/tables/process_actions.html b/interview/templates/interview/tables/process_actions.html
index 29318b83..de531830 100644
--- a/interview/templates/interview/tables/process_actions.html
+++ b/interview/templates/interview/tables/process_actions.html
@@ -1,6 +1,6 @@
{% load i18n %}
{trigramme}'.format(
- fullname=c.user.full_name, trigramme=c.user.trigramme
- )
+ '{trigramme} '.format(fullname=c.full_name, trigramme=c.trigramme)
for c in value.all()
]
)
@@ -241,9 +239,7 @@ def process(request, process_id, slug_info=None):
except Process.DoesNotExist:
return HttpResponseNotFound()
interviews = (
- Interview.objects.filter(process=process)
- .select_related("process__candidate")
- .prefetch_related("interviewers__user")
+ Interview.objects.filter(process=process).select_related("process__candidate").prefetch_related("interviewers")
)
interviews_for_process_table = InterviewTable(interviews)
RequestConfig(request).configure(interviews_for_process_table)
@@ -316,8 +312,8 @@ def switch_process_subscription_ajax(request, process_id):
@require_http_methods(["POST"])
@privilege_level_check(
authorised_level=[
- Consultant.PrivilegeLevel.ALL,
- Consultant.PrivilegeLevel.EXTERNAL_RPO,
+ PyouPyouUser.PrivilegeLevel.ALL,
+ PyouPyouUser.PrivilegeLevel.EXTERNAL_RPO,
]
)
def close_process(request, process_id):
@@ -340,8 +336,8 @@ def close_process(request, process_id):
@require_http_methods(["GET"])
@privilege_level_check(
authorised_level=[
- Consultant.PrivilegeLevel.ALL,
- Consultant.PrivilegeLevel.EXTERNAL_RPO,
+ PyouPyouUser.PrivilegeLevel.ALL,
+ PyouPyouUser.PrivilegeLevel.EXTERNAL_RPO,
]
)
def reopen_process(request, process_id):
@@ -384,7 +380,7 @@ def closed_processes(request):
@login_required
@require_http_methods(["GET"])
def processes_for_source(request, source_id):
- if request.user.consultant.is_external and request.user.consultant.limited_to_source.id != source_id:
+ if request.user.is_external and request.user.limited_to_source.id != source_id:
return redirect_to_login(next=request.path)
# override table's default sort by setting custom sort in request
@@ -415,7 +411,7 @@ def processes_for_source(request, source_id):
@login_required
@require_http_methods(["GET"])
-@user_passes_test(lambda u: not u.consultant.is_external)
+@user_passes_test(lambda u: not u.is_external)
def processes_for_offer(request, offer_id):
subsidiary_filter = get_global_filter(request)
try:
@@ -473,9 +469,9 @@ def processes(request):
@require_http_methods(["POST"])
@privilege_level_check(
authorised_level=[
- Consultant.PrivilegeLevel.ALL,
- Consultant.PrivilegeLevel.EXTERNAL_RPO,
- Consultant.PrivilegeLevel.EXTERNAL_FULL,
+ PyouPyouUser.PrivilegeLevel.ALL,
+ PyouPyouUser.PrivilegeLevel.EXTERNAL_RPO,
+ PyouPyouUser.PrivilegeLevel.EXTERNAL_FULL,
]
)
def reuse_candidate(request, candidate_id):
@@ -513,9 +509,9 @@ def new_candidate_POST_handler(
process = process_form.save(commit=False)
process.candidate = candidate
- process.creator = Consultant.objects.get(user=request.user)
- if request.user.consultant.limited_to_source:
- process.sources = request.user.consultant.limited_to_source
+ process.creator = PyouPyouUser.objects.get(id=request.user.id)
+ if request.user.limited_to_source:
+ process.sources = request.user.limited_to_source
process.save()
log_action(True, process, request.user, new_candidate)
@@ -534,9 +530,9 @@ def new_candidate_POST_handler(
@privilege_level_check(
authorised_level=[
- Consultant.PrivilegeLevel.ALL,
- Consultant.PrivilegeLevel.EXTERNAL_RPO,
- Consultant.PrivilegeLevel.EXTERNAL_FULL,
+ PyouPyouUser.PrivilegeLevel.ALL,
+ PyouPyouUser.PrivilegeLevel.EXTERNAL_RPO,
+ PyouPyouUser.PrivilegeLevel.EXTERNAL_FULL,
]
)
def new_candidate(request, past_candidate_id=None):
@@ -571,9 +567,9 @@ def new_candidate(request, past_candidate_id=None):
candidate_form = ProcessCandidateForm()
process_form = ProcessForm()
interviewers_form = InterviewersForm(prefix="interviewers")
- process_form.fields["subsidiary"].initial = request.user.consultant.company.id
+ process_form.fields["subsidiary"].initial = request.user.company.id
- if request.user.consultant.is_external:
+ if request.user.is_external:
process_form.fields.pop("sources")
# restrict available interviewers to process creator
@@ -603,9 +599,9 @@ def new_candidate(request, past_candidate_id=None):
@transaction.atomic
@privilege_level_check(
authorised_level=[
- Consultant.PrivilegeLevel.ALL,
- Consultant.PrivilegeLevel.EXTERNAL_RPO,
- Consultant.PrivilegeLevel.EXTERNAL_FULL,
+ PyouPyouUser.PrivilegeLevel.ALL,
+ PyouPyouUser.PrivilegeLevel.EXTERNAL_RPO,
+ PyouPyouUser.PrivilegeLevel.EXTERNAL_FULL,
]
)
def interview(request, process_id=None, interview_id=None, action=None):
@@ -615,10 +611,7 @@ def interview(request, process_id=None, interview_id=None, action=None):
if interview_id is not None:
try:
interview_model = Interview.objects.for_user(request.user).get(id=interview_id)
- if (
- action in ["plan", "planning-request"]
- and request.user.consultant not in interview_model.interviewers.all()
- ):
+ if action in ["plan", "planning-request"] and request.user not in interview_model.interviewers.all():
return HttpResponseNotFound()
except Interview.DoesNotExist:
@@ -636,13 +629,10 @@ def interview(request, process_id=None, interview_id=None, action=None):
interview_model.toggle_planning_request()
return ret
- if request.user.consultant.privilege not in [
- Consultant.PrivilegeLevel.ALL,
- Consultant.PrivilegeLevel.EXTERNAL_RPO,
- ]:
+ if request.user.is_external:
# set interviewer to be external consultant
tmp = request.POST.copy()
- tmp["interviewers"] = request.user.consultant.id
+ tmp["interviewers"] = request.user.id
request.POST = tmp
form = InterviewForm(request.POST, instance=interview_model)
@@ -653,7 +643,9 @@ def interview(request, process_id=None, interview_id=None, action=None):
else:
form = InterviewForm(instance=interview_model)
- if request.user.consultant.privilege not in [Consultant.PrivilegeLevel.ALL, Consultant.PrivilegeLevel.EXTERNAL_RPO]:
+ # if request.user.privilege not in [PyouPyouUser.PrivilegeLevel.ALL, PyouPyouUser.PrivilegeLevel.EXTERNAL_RPO]:
+ # OR ou AND ?
+ if request.user.is_external:
form.fields.pop("interviewers", None) # interviewer will always be user
return render(
@@ -671,9 +663,9 @@ def interview(request, process_id=None, interview_id=None, action=None):
@require_http_methods(["GET", "POST"])
@privilege_level_check(
authorised_level=[
- Consultant.PrivilegeLevel.ALL,
- Consultant.PrivilegeLevel.EXTERNAL_RPO,
- Consultant.PrivilegeLevel.EXTERNAL_FULL,
+ PyouPyouUser.PrivilegeLevel.ALL,
+ PyouPyouUser.PrivilegeLevel.EXTERNAL_RPO,
+ PyouPyouUser.PrivilegeLevel.EXTERNAL_FULL,
]
)
def minute_edit(request, interview_id):
@@ -683,7 +675,7 @@ def minute_edit(request, interview_id):
return HttpResponseNotFound()
# check if user is allowed to edit
- if request.user.consultant not in interview.interviewers.all():
+ if request.user not in interview.interviewers.all():
return HttpResponseNotFound()
if request.method == "POST":
if "itw-go" in request.POST:
@@ -759,11 +751,11 @@ def minute(request, interview_id, slug_info=None):
@login_required
@require_http_methods(["GET"])
def dashboard(request):
- if request.user.consultant.limited_to_source: # if None, dashboard will be empty anyways
- return processes_for_source(request, request.user.consultant.limited_to_source.id)
+ if request.user.limited_to_source: # if None, dashboard will be empty anyways
+ return processes_for_source(request, request.user.limited_to_source.id)
a_week_ago = timezone.now() - datetime.timedelta(days=7)
- c = request.user.consultant
+ c = request.user
actions_needed_processes = (
Process.objects.for_table(request.user).exclude(state__in=Process.CLOSED_STATE_VALUES).filter(responsible=c)
)
@@ -772,7 +764,7 @@ def dashboard(request):
related_processes = (
Process.objects.for_table(request.user)
- .filter(interview__interviewers__user=request.user)
+ .filter(interview__interviewers=request.user)
.filter(Q(end_date__gte=a_week_ago) | Q(state__in=Process.OPEN_STATE_VALUES))
.distinct()
)
@@ -803,7 +795,7 @@ def dashboard(request):
@login_required
@require_http_methods(["POST"])
-@user_passes_test(lambda u: not u.consultant.is_external)
+@user_passes_test(lambda u: not u.is_external)
def create_source_ajax(request):
form = SourceForm(request.POST, prefix="source")
if form.is_valid():
@@ -818,7 +810,7 @@ def create_source_ajax(request):
@login_required
@require_http_methods(["POST"])
-@user_passes_test(lambda u: not u.consultant.is_external)
+@user_passes_test(lambda u: not u.is_external)
def create_offer_ajax(request):
form = OfferForm(request.POST, prefix="offer")
if form.is_valid():
@@ -834,7 +826,7 @@ def create_offer_ajax(request):
@csrf_exempt
@require_http_methods(["POST"])
@user_passes_test(lambda u: u.is_superuser)
-@user_passes_test(lambda u: not u.consultant.is_external)
+@user_passes_test(lambda u: not u.is_external)
def create_account(request):
data = json.loads(request.body)
subsidiary = Subsidiary.objects.filter(code=data["company"]).first()
@@ -846,7 +838,7 @@ def create_account(request):
if parse_date(data["date_joined"]) is None:
return JsonResponse({"error": "ISO 8601 for date format"}, status=400)
extra_fields["date_joined"] = data["date_joined"]
- consultant = Consultant.objects.create_consultant(
+ consultant = PyouPyouUser.objects.create_user(
trigramme=data["trigramme"].lower(),
email=data["email"],
company=subsidiary,
@@ -861,7 +853,7 @@ def create_account(request):
@csrf_exempt
@require_http_methods(["DELETE"])
@user_passes_test(lambda u: u.is_superuser)
-@user_passes_test(lambda u: not u.consultant.is_external)
+@user_passes_test(lambda u: not u.is_external)
def delete_account(request, trigramme):
user = PyouPyouUser.objects.filter(trigramme=trigramme.lower()).first()
if not user:
@@ -874,9 +866,9 @@ def delete_account(request, trigramme):
@require_http_methods(["GET", "POST"])
@privilege_level_check(
authorised_level=[
- Consultant.PrivilegeLevel.ALL,
- Consultant.PrivilegeLevel.EXTERNAL_RPO,
- Consultant.PrivilegeLevel.EXTERNAL_FULL,
+ PyouPyouUser.PrivilegeLevel.ALL,
+ PyouPyouUser.PrivilegeLevel.EXTERNAL_RPO,
+ PyouPyouUser.PrivilegeLevel.EXTERNAL_FULL,
]
)
def edit_candidate(request, process_id):
@@ -909,7 +901,7 @@ def edit_candidate(request, process_id):
source_form = SourceForm(prefix="source")
offer_form = OfferForm(prefix="offer")
- if request.user.consultant.is_external:
+ if request.user.is_external:
process_form.fields.pop("sources")
data = {
@@ -956,6 +948,190 @@ def process_stats(process):
return process_length, process_interview_count
+@login_required
+@require_http_methods(["GET"])
+def export_processes_tsv(request):
+ processes = Process.objects.for_user(request.user).prefetch_related("interview_set")
+
+ ret = []
+
+ ret.append(
+ "\t".join(
+ str(x).replace("\t", " ")
+ for x in [
+ "process.id",
+ "candidate.name",
+ "subsidiary",
+ "start_date",
+ "end_date",
+ "process length",
+ "sources",
+ "source_category",
+ "offer",
+ "contract_type",
+ "contract_start_date",
+ "contract_duration",
+ "process state",
+ "process state label",
+ "process itw count",
+ "mean days between itws",
+ ]
+ )
+ )
+
+ for process in processes:
+ process_length, process_interview_count = process_stats(process)
+ columns = [
+ process.id,
+ process.candidate.name,
+ process.subsidiary,
+ process.start_date,
+ process.end_date,
+ process_length,
+ process.sources,
+ "" if process.sources is None else process.sources.category.name,
+ process.offer,
+ process.contract_type,
+ process.contract_start_date,
+ process.contract_duration,
+ process.state,
+ process.get_state_display(),
+ process_interview_count,
+ 0 if process_interview_count == 0 else int(process_length / process_interview_count),
+ ]
+ ret.append("\t".join(str(c).replace("\t", " ") for c in columns))
+
+ response = HttpResponse("\n".join(ret), content_type="text/plain; charset=utf-8")
+ response["Content-Disposition"] = "attachment; filename=all_processes.tsv"
+
+ return response
+
+
+@login_required
+@require_http_methods(["GET"])
+def export_interviews_tsv(request):
+ consultants = PyouPyouUser.objects.filter(is_active=True).select_related("user").select_related("company")
+ interviews = (
+ Interview.objects.for_user(request.user)
+ .select_related("process")
+ .select_related("process__sources")
+ .select_related("process__contract_type")
+ .select_related("process__candidate")
+ .select_related("process__subsidiary")
+ .select_related("process__offer")
+ .prefetch_related(Prefetch("interviewers", queryset=consultants))
+ )
+
+ ret = []
+
+ ret.append(
+ "\t".join(
+ str(x).replace("\t", " ")
+ for x in [
+ "process.id",
+ "candidate.name",
+ "subsidiary",
+ "start_date",
+ "end_date",
+ "process length",
+ "source",
+ "source category",
+ "offer",
+ "contract_type",
+ "contract_start_date",
+ "contract_duration",
+ "process state",
+ "process state label",
+ "process itw count",
+ "mean days between itws",
+ "interview.id",
+ "state",
+ "interviewers",
+ "interview rank",
+ "days since last",
+ "planned_date",
+ "prequalification",
+ "kind",
+ ]
+ )
+ )
+ processes_length = {}
+ processes_itw_count = {}
+
+ for interview in interviews:
+ interviewers = ""
+ for i in interview.interviewers.all():
+ interviewers += i.trigramme + "_"
+ interviewers = interviewers[:-1]
+
+ if interview.process.id not in processes_length:
+ process_length, process_interview_count = process_stats(interview.process)
+ processes_length[interview.process.id] = process_length
+ processes_itw_count[interview.process.id] = process_interview_count
+ else:
+ process_length = processes_length[interview.process.id]
+ process_interview_count = processes_itw_count[interview.process.id]
+
+ # Compute time elapsed since last event (previous interview or beginning of process)
+ time_since_last_is_sound = True
+ last_event_date = interview.process.start_date
+ next_event_date = None
+ if interview.rank > 1:
+ last_itw = (
+ Interview.objects.for_user(request.user)
+ .filter(process=interview.process, rank=interview.rank - 1)
+ .first()
+ )
+ if last_itw and last_itw.planned_date is not None:
+ last_event_date = last_itw.planned_date.date()
+ else:
+ time_since_last_is_sound = False
+ if interview.planned_date is None:
+ time_since_last_is_sound = False
+ else:
+ next_event_date = interview.planned_date.date()
+ if time_since_last_is_sound:
+ time_since_last_event = int((next_event_date - last_event_date).days)
+ # we have some processes that were created after the first itw was planned
+ if time_since_last_event < 0:
+ time_since_last_event = ""
+ else:
+ time_since_last_event = ""
+
+ columns = [
+ interview.process.id,
+ interview.process.candidate.name,
+ interview.process.subsidiary,
+ interview.process.start_date,
+ interview.process.end_date,
+ process_length,
+ interview.process.sources,
+ "" if interview.process.sources is None else interview.process.sources.category.name,
+ interview.process.offer,
+ interview.process.contract_type,
+ interview.process.contract_start_date,
+ interview.process.contract_duration,
+ interview.process.state,
+ interview.process.get_state_display(),
+ process_interview_count,
+ 0 if process_interview_count == 0 else int(process_length / process_interview_count),
+ interview.id,
+ interview.state,
+ interviewers,
+ interview.rank,
+ time_since_last_event,
+ interview.planned_date,
+ interview.prequalification,
+ interview.kind_of_interview,
+ ]
+ ret.append("\t".join(str(c).replace("\t", " ") for c in columns))
+
+ response = HttpResponse("\n".join(ret), content_type="text/plain; charset=utf-8")
+ response["Content-Disposition"] = "attachment; filename=all_interviews.tsv"
+
+ return response
+
+
class LoadTable(tables.Table):
subsidiary = tables.Column(verbose_name=_("Subsidiary"))
interviewer = tables.TemplateColumn(
@@ -1028,17 +1204,17 @@ def calculate_load(itws):
@login_required
@require_http_methods(["GET"])
-@user_passes_test(lambda u: not u.consultant.is_external)
+@user_passes_test(lambda u: not u.is_external)
def interviewers_load(request):
subsidiary_filter = get_global_filter(request)
subsidiary = subsidiary_filter.form.cleaned_data.get("subsidiary", None)
if subsidiary:
- consultants_qs = Consultant.objects.filter(company=subsidiary)
+ consultants_qs = PyouPyouUser.objects.filter(company=subsidiary)
else:
- consultants_qs = Consultant.objects.all()
+ consultants_qs = PyouPyouUser.objects.all()
data = []
- for c in consultants_qs.filter(user__is_active=True).order_by("company", "user__full_name"):
+ for c in consultants_qs.filter(is_active=True).order_by("company", "full_name"):
load = _interviewer_load(c)
data.append(
{
@@ -1070,7 +1246,7 @@ def interviewers_load(request):
@login_required
@require_http_methods(["GET"])
-@user_passes_test(lambda u: not u.consultant.is_external)
+@user_passes_test(lambda u: not u.is_external)
def search(request):
q = request.GET.get("q", "").strip()
@@ -1093,7 +1269,7 @@ def search(request):
@login_required
@require_http_methods(["GET"])
-@user_passes_test(lambda u: not u.consultant.is_external)
+@user_passes_test(lambda u: not u.is_external)
def gantt(request):
state_filter = Process.OPEN_STATE_VALUES + [Process.JOB_OFFER, Process.HIRED]
today = timezone.now().date()
@@ -1222,7 +1398,7 @@ class Meta:
@login_required
@require_http_methods(["GET"])
-@user_passes_test(lambda u: not u.consultant.is_external)
+@user_passes_test(lambda u: not u.is_external)
def active_sources(request):
subsidiary_filter = get_global_filter(request)
subsidiary = subsidiary_filter.form.cleaned_data.get("subsidiary")
@@ -1302,7 +1478,7 @@ def active_sources(request):
@login_required
@require_http_methods(["GET"])
-@user_passes_test(lambda u: not u.consultant.is_external)
+@user_passes_test(lambda u: not u.is_external)
def offers(request):
subsidiary_filter = get_global_filter(request)
subsidiary = subsidiary_filter.form.cleaned_data.get("subsidiary")
@@ -1353,7 +1529,7 @@ def offers(request):
@login_required
@require_http_methods(["GET"])
-@user_passes_test(lambda u: not u.consultant.is_external)
+@user_passes_test(lambda u: not u.is_external)
def activity_summary(request):
subsidiary_filter = get_global_filter(request)
process_filter = ProcessSummaryFilter(
@@ -1499,7 +1675,10 @@ def interviews_list(request):
# By default (if no filter data was sent in request), filter for
# - last month interviews
- interview_filter.data.setdefault("last_state_change_after", a_month_ago.strftime("%d/%m/%Y"))
+ # User can still view interviews for all subsidiaries by selecting "---" in the dropdown list
+ if {} == interview_filter.data:
+ interview_filter.data["subsidiary"] = request.user.company
+ interview_filter.data["last_state_change_after"] = a_month_ago.strftime("%d/%m/%Y")
# interview that are not planned are not selected by default filter as it is a range on planned_date, hence this query
interviews_not_planned = (
@@ -1554,7 +1733,7 @@ def process_from_cognito_form(request, source_id, subsidiary_id):
@login_required
-@user_passes_test(lambda u: not u.consultant.is_external)
+@user_passes_test(lambda u: not u.is_external)
def interviews_pivotable(request):
data = []
@@ -1567,7 +1746,7 @@ def interviews_pivotable(request):
.select_related("candidate")
.select_related("subsidiary")
.select_related("offer__subsidiary")
- .prefetch_related("interview_set__kind_of_interview", "interview_set__interviewers__user")
+ .prefetch_related("interview_set__kind_of_interview", "interview_set__interviewers")
.annotate(interview_last_planned_date=Max("interview__planned_date"))
)
@@ -1612,7 +1791,7 @@ def interviews_pivotable(request):
for idx, interview in enumerate(process.interview_set.all()):
interviewers = ""
for i in interview.interviewers.all():
- interviewers += i.user.trigramme + "_"
+ interviewers += i.trigramme + "_"
interviewers = interviewers[:-1]
# Compute time elapsed since last event (previous interview or beginning of process)
@@ -1719,7 +1898,7 @@ def interviews_pivotable(request):
@login_required
-@user_passes_test(lambda u: not u.consultant.is_external)
+@user_passes_test(lambda u: not u.is_external)
def processes_pivotable(request):
data = []
@@ -1732,7 +1911,7 @@ def processes_pivotable(request):
.select_related("candidate")
.select_related("subsidiary")
.select_related("offer__subsidiary")
- .prefetch_related("interview_set__kind_of_interview", "interview_set__interviewers__user")
+ .prefetch_related("interview_set__kind_of_interview", "interview_set__interviewers")
.annotate(interview_last_planned_date=Max("interview__planned_date"))
)
diff --git a/pyoupyou/middleware.py b/pyoupyou/middleware.py
index c3fff4ae..819ca781 100644
--- a/pyoupyou/middleware.py
+++ b/pyoupyou/middleware.py
@@ -3,7 +3,7 @@
from django.urls import resolve
from interview.views import get_global_filter
-from ref.models import Consultant
+from ref.models import PyouPyouUser
class ProxyRemoteUserMiddleware(RemoteUserMiddleware):
@@ -18,8 +18,8 @@ def __init__(self, get_response):
def __call__(self, request):
if (
request.user.is_authenticated
- and request.user.consultant.privilege != Consultant.PrivilegeLevel.ALL
- and request.user.consultant.limited_to_source is None
+ and request.user.privilege != PyouPyouUser.PrivilegeLevel.ALL
+ and request.user.limited_to_source is None
):
return HttpResponseForbidden(content=self.forbidden_content)
diff --git a/ref/admin.py b/ref/admin.py
index d5dac91e..f1de9797 100644
--- a/ref/admin.py
+++ b/ref/admin.py
@@ -2,16 +2,25 @@
from django.contrib.auth.admin import UserAdmin
from django.utils.translation import gettext_lazy as _
-from ref.models import Subsidiary, Consultant, PyouPyouUser
+from ref.models import Subsidiary, PyouPyouUser
class PyouPyouUserAdmin(UserAdmin):
fieldsets = (
- (None, {"fields": ("trigramme", "password")}),
- (_("Personal info"), {"fields": ("full_name", "email")}),
+ (_("Personal info"), {"fields": ("full_name", "trigramme", "password", "email", "company")}),
(
_("Permissions"),
- {"fields": ("is_active", "token", "is_staff", "is_superuser", "groups", "user_permissions")},
+ {
+ "fields": (
+ "privilege",
+ "limited_to_source",
+ "is_active",
+ "is_staff",
+ "is_superuser",
+ "groups",
+ "user_permissions",
+ )
+ },
),
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
)
@@ -23,20 +32,5 @@ class PyouPyouUserAdmin(UserAdmin):
ordering = ("trigramme",)
-class SubsidiaryAdmin(admin.ModelAdmin):
- filter_horizontal = ("informed",)
-
-
-class InformedInline(admin.TabularInline):
- model = Subsidiary.informed.through
-
-
-class ConsultantAdmin(admin.ModelAdmin):
- inlines = [
- InformedInline,
- ]
-
-
-admin.site.register(Subsidiary, SubsidiaryAdmin)
-admin.site.register(Consultant, ConsultantAdmin)
+admin.site.register(Subsidiary)
admin.site.register(PyouPyouUser, PyouPyouUserAdmin)
diff --git a/ref/factory.py b/ref/factory.py
index 50cba3fa..dc3c47d2 100644
--- a/ref/factory.py
+++ b/ref/factory.py
@@ -17,6 +17,7 @@ class Meta:
full_name = factory.Faker("name")
trigramme = factory.LazyAttribute(lambda n: n.full_name[0:1].upper() + n.full_name.split(" ")[1][0:2].upper())
email = factory.LazyAttribute(lambda u: u.trigramme.lower() + "@mail.com")
+ company = factory.Iterator(Subsidiary.objects.all())
date_joined = factory.fuzzy.FuzzyDateTime(
start_dt=datetime.datetime(2010, 1, 1, tzinfo=test_tz), end_dt=datetime.datetime(2020, 1, 1, tzinfo=test_tz)
@@ -38,11 +39,3 @@ class Meta:
name = factory.Faker("company")
code = factory.LazyAttribute(lambda n: compute_subsidiary_code(n.name))
-
-
-class ConsultantFactory(factory.django.DjangoModelFactory):
- class Meta:
- model = "ref.Consultant"
-
- user = factory.SubFactory(PyouPyouUserFactory)
- company = factory.Iterator(Subsidiary.objects.all())
diff --git a/ref/migrations/0007_auto_20230116_1656.py b/ref/migrations/0007_auto_20230116_1656.py
new file mode 100644
index 00000000..2f18af17
--- /dev/null
+++ b/ref/migrations/0007_auto_20230116_1656.py
@@ -0,0 +1,66 @@
+# Generated by Django 3.2.16 on 2023-01-16 15:56
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("interview", "0025_auto_20230116_1355"),
+ ("ref", "0006_alter_consultant_privilege"),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name="pyoupyouuser",
+ options={"ordering": ("trigramme",), "verbose_name": "user", "verbose_name_plural": "users"},
+ ),
+ migrations.AddField(
+ model_name="pyoupyouuser",
+ name="company",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="ref.subsidiary",
+ verbose_name="Subsidiary",
+ ),
+ ),
+ migrations.AddField(
+ model_name="pyoupyouuser",
+ name="limited_to_source",
+ field=models.ForeignKey(
+ blank=True,
+ default=None,
+ help_text="This field must be set if user is not an internal consultant",
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="interview.sources",
+ verbose_name="Limit user to a source",
+ ),
+ ),
+ migrations.AddField(
+ model_name="pyoupyouuser",
+ name="privilege",
+ field=models.PositiveSmallIntegerField(
+ choices=[
+ (1, "User is an insider consultant"),
+ (2, "User is an external consultant with additional rights"),
+ (3, "User is an external consultant"),
+ (4, "User is external and has only read rights"),
+ ],
+ default=1,
+ help_text="Designates what a user can or cannot do",
+ verbose_name="Authority level",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="subsidiary",
+ name="responsible",
+ field=models.ForeignKey(
+ null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL
+ ),
+ ),
+ ]
diff --git a/ref/migrations/0007_subsidiary_informed.py b/ref/migrations/0007_subsidiary_informed.py
index 76202981..f227f0e7 100644
--- a/ref/migrations/0007_subsidiary_informed.py
+++ b/ref/migrations/0007_subsidiary_informed.py
@@ -16,7 +16,7 @@ class Migration(migrations.Migration):
field=models.ManyToManyField(
blank=True,
related_name="subsidiary_notifications",
- to="ref.Consultant",
+ to="ref.PyouPyouUser",
verbose_name="Informed consultants",
),
),
diff --git a/ref/migrations/0008_auto_20230116_1656.py b/ref/migrations/0008_auto_20230116_1656.py
new file mode 100644
index 00000000..6c28b38a
--- /dev/null
+++ b/ref/migrations/0008_auto_20230116_1656.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.2.16 on 2023-01-16 15:56
+
+from django.db import migrations
+
+
+def copy_field(apps, schema):
+ PyouPyouUser = apps.get_model("ref", "PyouPyouUser")
+ for user in PyouPyouUser.objects.all():
+ user.company = user.consultant.company
+ user.privilege = user.consultant.privilege
+ user.limited_to_source = user.consultant.limited_to_source
+ user.save()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("ref", "0007_auto_20230116_1656"),
+ ]
+
+ operations = [
+ # Copy consultant fields to user
+ migrations.RunPython(code=copy_field),
+ ]
diff --git a/ref/migrations/0009_auto_20230116_1656.py b/ref/migrations/0009_auto_20230116_1656.py
new file mode 100644
index 00000000..d3f85098
--- /dev/null
+++ b/ref/migrations/0009_auto_20230116_1656.py
@@ -0,0 +1,16 @@
+# Generated by Django 3.2.16 on 2023-01-16 15:56
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("ref", "0008_auto_20230116_1656"),
+ ]
+
+ operations = [
+ migrations.DeleteModel(
+ name="Consultant",
+ ),
+ ]
diff --git a/ref/migrations/0012_merge_20241021_1741.py b/ref/migrations/0012_merge_20241021_1741.py
new file mode 100644
index 00000000..c166e239
--- /dev/null
+++ b/ref/migrations/0012_merge_20241021_1741.py
@@ -0,0 +1,13 @@
+# Generated by Django 4.2.4 on 2024-10-21 15:41
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("ref", "0009_auto_20230116_1656"),
+ ("ref", "0011_merge_20240715_1041"),
+ ]
+
+ operations = []
diff --git a/ref/migrations/0013_remove_subsidiary_show_in_report_by_default_and_more.py b/ref/migrations/0013_remove_subsidiary_show_in_report_by_default_and_more.py
new file mode 100644
index 00000000..a791fdd0
--- /dev/null
+++ b/ref/migrations/0013_remove_subsidiary_show_in_report_by_default_and_more.py
@@ -0,0 +1,65 @@
+# Generated by Django 4.2.4 on 2024-10-21 15:42
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("interview", "0028_merge_20241021_1741"),
+ ("ref", "0012_merge_20241021_1741"),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name="subsidiary",
+ name="show_in_report_by_default",
+ ),
+ migrations.CreateModel(
+ name="Consultant",
+ fields=[
+ ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
+ (
+ "privilege",
+ models.PositiveSmallIntegerField(
+ choices=[
+ (1, "User is an insider consultant"),
+ (2, "User is an external consultant with additional rights"),
+ (3, "User is an external consultant"),
+ (4, "User is external and has only read rights"),
+ ],
+ default=1,
+ verbose_name="Authority level",
+ ),
+ ),
+ (
+ "company",
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="ref.subsidiary",
+ verbose_name="Subsidiary",
+ ),
+ ),
+ (
+ "limited_to_source",
+ models.ForeignKey(
+ blank=True,
+ default=None,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="interview.sources",
+ ),
+ ),
+ (
+ "user",
+ models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
+ ),
+ ],
+ options={
+ "ordering": ("user__trigramme",),
+ },
+ ),
+ ]
diff --git a/ref/migrations/0014_subsidiary_show_in_report_by_default.py b/ref/migrations/0014_subsidiary_show_in_report_by_default.py
new file mode 100644
index 00000000..185e0296
--- /dev/null
+++ b/ref/migrations/0014_subsidiary_show_in_report_by_default.py
@@ -0,0 +1,20 @@
+# Generated by Django 4.2.4 on 2024-10-21 15:45
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("ref", "0013_remove_subsidiary_show_in_report_by_default_and_more"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="subsidiary",
+ name="show_in_report_by_default",
+ field=models.BooleanField(
+ default=True, verbose_name="Show the subsidiary in the report analysis by default"
+ ),
+ ),
+ ]
diff --git a/ref/migrations/0015_move_consultant_data.py b/ref/migrations/0015_move_consultant_data.py
new file mode 100644
index 00000000..29ab3313
--- /dev/null
+++ b/ref/migrations/0015_move_consultant_data.py
@@ -0,0 +1,23 @@
+from django.db import migrations
+
+
+def transfer_consultant_data(apps, schema_editor):
+ Consultant = apps.get_model("ref", "Consultant")
+
+ for consultant in Consultant.objects.all():
+ user = consultant.user
+ user.company = consultant.company
+ user.limited_to_source = consultant.limited_to_source
+ user.privilege = consultant.privilege
+ user.save()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("ref", "0014_subsidiary_show_in_report_by_default"),
+ ]
+
+ operations = [
+ migrations.RunPython(transfer_consultant_data),
+ ]
diff --git a/ref/migrations/0016_delete_consultant.py b/ref/migrations/0016_delete_consultant.py
new file mode 100644
index 00000000..ec96e298
--- /dev/null
+++ b/ref/migrations/0016_delete_consultant.py
@@ -0,0 +1,16 @@
+# Generated by Django 4.2.4 on 2024-10-23 13:46
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("ref", "0015_move_consultant_data"),
+ ]
+
+ operations = [
+ migrations.DeleteModel(
+ name="Consultant",
+ ),
+ ]
diff --git a/ref/models.py b/ref/models.py
index 5a4385d3..ac6673e5 100644
--- a/ref/models.py
+++ b/ref/models.py
@@ -14,9 +14,9 @@ class Subsidiary(models.Model):
name = models.CharField(_("Name"), max_length=200, unique=True)
code = models.CharField(_("Code"), max_length=3, unique=True)
- responsible = models.ForeignKey("Consultant", null=True, on_delete=models.SET_NULL)
+ responsible = models.ForeignKey("PyouPyouUser", null=True, on_delete=models.SET_NULL)
informed = models.ManyToManyField(
- "Consultant", blank=True, related_name="subsidiary_notifications", verbose_name=_("Informed consultants")
+ "PyouPyouUser", blank=True, related_name="subsidiary_notifications", verbose_name=_("Informed consultants")
)
show_in_report_by_default = models.BooleanField(
default=True, verbose_name=_("Show the subsidiary in the report analysis by default")
@@ -24,9 +24,9 @@ class Subsidiary(models.Model):
@property
def notification_emails(self):
- res = [email for email in self.informed.all().values_list("user__email", flat=True)]
+ res = [email for email in self.informed.all().values_list("email", flat=True)]
if self.responsible:
- res.append(self.responsible.user.email)
+ res.append(self.responsible.email)
return res
def __str__(self):
@@ -77,6 +77,12 @@ def generate_token():
class PyouPyouUser(AbstractBaseUser, PermissionsMixin):
+ class PrivilegeLevel(models.IntegerChoices):
+ ALL = 1, _("User is an insider consultant")
+ EXTERNAL_RPO = 2, _("User is an external consultant with additional rights")
+ EXTERNAL_FULL = 3, _("User is an external consultant")
+ EXTERNAL_READONLY = 4, _("User is external and has only read rights")
+
trigramme = models.CharField(max_length=40, unique=True)
full_name = models.CharField(_("full name"), max_length=50, blank=True)
email = models.EmailField(_("email address"), blank=True)
@@ -93,6 +99,27 @@ class PyouPyouUser(AbstractBaseUser, PermissionsMixin):
)
date_joined = models.DateTimeField(_("date joined"), default=timezone.now)
+ company = models.ForeignKey(
+ Subsidiary, verbose_name=_("Subsidiary"), null=True, blank=True, on_delete=models.SET_NULL
+ )
+
+ # dst class written with string to avoid circular imports issues
+ limited_to_source = models.ForeignKey(
+ "interview.Sources",
+ null=True,
+ blank=True,
+ default=None,
+ on_delete=models.SET_NULL,
+ verbose_name=_("Limit user to a source"),
+ help_text=_("This field must be set if user is not an internal consultant"),
+ )
+ privilege = models.PositiveSmallIntegerField(
+ choices=PrivilegeLevel.choices,
+ verbose_name=_("Authority level"),
+ default=PrivilegeLevel.ALL,
+ help_text=_("Designates what a user can or cannot do"),
+ )
+
objects = PyouPyouUserManager()
USERNAME_FIELD = "trigramme"
@@ -101,6 +128,14 @@ class PyouPyouUser(AbstractBaseUser, PermissionsMixin):
class Meta:
verbose_name = _("user")
verbose_name_plural = _("users")
+ ordering = ("trigramme",)
+
+ def __str__(self):
+ return self.get_full_name()
+
+ @property
+ def is_external(self):
+ return self.limited_to_source is not None
def get_full_name(self):
return "{} ({})".format(self.full_name, self.trigramme)
@@ -113,45 +148,3 @@ def email_user(self, subject, message, from_email=None, **kwargs):
Sends an email to this User.
"""
send_mail(subject, message, from_email, [self.email], **kwargs)
-
-
-class ConsultantManager(models.Manager):
- @transaction.atomic
- def create_consultant(self, trigramme, email, company, full_name, **extra_fields):
- user = PyouPyouUser.objects.create_user(trigramme, email, full_name=full_name, **extra_fields)
- consultant = self.model(user=user, company=company)
- consultant.save()
- return consultant
-
-
-class Consultant(models.Model):
- """A consultant that can do recruitment meeting"""
-
- @property
- def is_external(self):
- return self.limited_to_source is not None
-
- class PrivilegeLevel(models.IntegerChoices):
- ALL = 1, _("User is an insider consultant")
- EXTERNAL_RPO = 2, _("User is an external consultant with additional rights")
- EXTERNAL_FULL = 3, _("User is an external consultant")
- EXTERNAL_READONLY = 4, _("User is external and has only read rights")
-
- user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
- company = models.ForeignKey(Subsidiary, verbose_name=_("Subsidiary"), null=True, on_delete=models.SET_NULL)
-
- # dst class written with string to avoid circular imports issues
- limited_to_source = models.ForeignKey(
- "interview.Sources", null=True, blank=True, default=None, on_delete=models.SET_NULL
- )
- privilege = models.PositiveSmallIntegerField(
- choices=PrivilegeLevel.choices, verbose_name=_("Authority level"), default=PrivilegeLevel.ALL
- )
-
- objects = ConsultantManager()
-
- def __str__(self):
- return self.user.get_full_name()
-
- class Meta:
- ordering = ("user__trigramme",)