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 @@ - - - {% 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 %} 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",)