feat: implement can_tag permission for team mentions and restrict search visibility based on access control
This commit is contained in:
parent
19810d101c
commit
89794fa294
10 changed files with 125 additions and 18 deletions
|
|
@ -532,7 +532,7 @@ class UserCompanyTeamAccessInline(admin.TabularInline):
|
|||
model = UserCompanyTeamAccess
|
||||
extra = 1
|
||||
autocomplete_fields = ['team']
|
||||
fields = ['team', 'can_view', 'can_edit']
|
||||
fields = ['team', 'can_view', 'can_edit', 'can_tag']
|
||||
verbose_name = "Accès équipe"
|
||||
verbose_name_plural = "Accès aux équipes"
|
||||
|
||||
|
|
|
|||
|
|
@ -189,4 +189,5 @@ class Command(BaseCommand):
|
|||
'company': ta.team.company.name,
|
||||
'can_view': ta.can_view,
|
||||
'can_edit': ta.can_edit,
|
||||
'can_tag': ta.can_tag,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -398,5 +398,6 @@ class Command(BaseCommand):
|
|||
defaults={
|
||||
'can_view': data.get('can_view', True),
|
||||
'can_edit': data.get('can_edit', False),
|
||||
'can_tag': data.get('can_tag', data.get('can_view', False)),
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -284,6 +284,7 @@ def copy_user_config(source_user_or_config: Any, target_user_or_config: Any, cop
|
|||
team=ta.team,
|
||||
can_view=ta.can_view,
|
||||
can_edit=ta.can_edit,
|
||||
can_tag=ta.can_tag,
|
||||
)
|
||||
|
||||
# 9. Accès spécifiques aux assets
|
||||
|
|
|
|||
|
|
@ -354,8 +354,8 @@ class ClaimDeclarationAttachmentAdmin(admin.ModelAdmin):
|
|||
|
||||
@admin.register(UserCompanyTeamAccess)
|
||||
class UserCompanyTeamAccessAdmin(admin.ModelAdmin):
|
||||
list_display = ('user_config_user', 'team', 'can_view', 'can_edit')
|
||||
list_filter = ('can_view', 'can_edit', 'team__company')
|
||||
list_display = ('user_config_user', 'team', 'can_view', 'can_edit', 'can_tag')
|
||||
list_filter = ('can_view', 'can_edit', 'can_tag', 'team__company')
|
||||
search_fields = ('user_config__user__username', 'user_config__user__first_name', 'user_config__user__last_name', 'team__name')
|
||||
autocomplete_fields = ('team',)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
# Generated by Django 6.0.7 on 2026-09-09 11:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def populate_can_tag(apps, schema_editor):
|
||||
UserCompanyTeamAccess = apps.get_model('contracts', 'UserCompanyTeamAccess')
|
||||
UserCompanyTeamAccess.objects.filter(can_view=True).update(can_tag=True)
|
||||
|
||||
|
||||
def reverse_populate(apps, schema_editor):
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contracts', '0005_companymemberabsence'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='usercompanyteamaccess',
|
||||
name='can_tag',
|
||||
field=models.BooleanField(default=False, help_text="L'utilisateur peut tagger / mentionner cette équipe (@) dans les discussions.", verbose_name='Peut tagger'),
|
||||
),
|
||||
migrations.RunPython(populate_can_tag, reverse_populate),
|
||||
]
|
||||
|
||||
|
|
@ -275,6 +275,11 @@ class UserCompanyTeamAccess(models.Model):
|
|||
verbose_name=_("Peut éditer"),
|
||||
help_text=_("L'utilisateur peut créer/supprimer des équipes et des membres, et déplacer des membres entre équipes."),
|
||||
)
|
||||
can_tag = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Peut tagger"),
|
||||
help_text=_("L'utilisateur peut tagger / mentionner cette équipe (@) dans les discussions."),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user_config.user.username} → {self.team.name}"
|
||||
|
|
|
|||
|
|
@ -176,20 +176,22 @@ class TeamPlanningAjaxViewsTests(TestCase):
|
|||
self.assertEqual(CompanyMemberAbsence.objects.filter(member=self.member_alice, date=target_date).count(), 1)
|
||||
|
||||
def test_planning_duplicate_ajax(self):
|
||||
# Assigner Alice le 07/09
|
||||
set_member_team_schedule(self.member_alice, self.team_a.id, date(2026, 9, 7))
|
||||
source = timezone.now().date() + timedelta(days=1)
|
||||
target = timezone.now().date() + timedelta(days=2)
|
||||
# Assigner Alice à source
|
||||
set_member_team_schedule(self.member_alice, self.team_a.id, source)
|
||||
|
||||
url = reverse('contracts:team_planning_duplicate_ajax')
|
||||
payload = {
|
||||
'mode': 'day',
|
||||
'source_date': '2026-09-07',
|
||||
'target_date': '2026-09-08',
|
||||
'source_date': source.isoformat(),
|
||||
'target_date': target.isoformat(),
|
||||
'copy_absences': False,
|
||||
}
|
||||
response = self.client.post(url, data=json.dumps(payload), content_type='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(response.json()['success'])
|
||||
self.assertEqual(get_member_team_for_date(self.member_alice, date(2026, 9, 8)), self.team_a)
|
||||
self.assertEqual(get_member_team_for_date(self.member_alice, target), self.team_a)
|
||||
|
||||
def test_planning_duplicate_friday_to_monday_ajax(self):
|
||||
"""Vérifie la duplication d'un vendredi vers le lundi suivant via l'endpoint AJAX."""
|
||||
|
|
|
|||
|
|
@ -392,11 +392,14 @@ class WebPushTests(TestCase):
|
|||
|
||||
class TeamMentionTests(TestCase):
|
||||
def setUp(self):
|
||||
from contracts.models import Company, CompanyTeam, CompanyMember
|
||||
from contracts.models import Company, CompanyTeam, CompanyMember, UserCompanyTeamAccess
|
||||
from common.models import UserConfig
|
||||
self.company = Company.objects.create(name="Bruxelles Mobilité")
|
||||
self.team = CompanyTeam.objects.create(name="BM - Régie - Team 1", company=self.company)
|
||||
|
||||
self.author = User.objects.create_user(username='author_user', password='pass')
|
||||
self.author_config = UserConfig.objects.create(user=self.author, is_intern=True)
|
||||
|
||||
self.team_member_user = User.objects.create_user(username='team_user', password='pass')
|
||||
self.other_user = User.objects.create_user(username='other_user', password='pass')
|
||||
|
||||
|
|
@ -426,6 +429,52 @@ class TeamMentionTests(TestCase):
|
|||
team_results = [r for r in results if r['type'] == 'team']
|
||||
self.assertTrue(any(r['id'] == self.team.pk for r in team_results))
|
||||
|
||||
def test_external_user_can_only_search_teams_with_can_tag_true(self):
|
||||
from contracts.models import Company, CompanyTeam, UserCompanyTeamAccess
|
||||
from common.models import UserConfig
|
||||
|
||||
provider_co = Company.objects.create(name="Société Prestataire")
|
||||
team_own_allowed = CompanyTeam.objects.create(name="Prestataire - Team Alpha", company=provider_co)
|
||||
team_own_forbidden = CompanyTeam.objects.create(name="Prestataire - Team Beta", company=provider_co)
|
||||
team_internal = CompanyTeam.objects.create(name="BM - Régie - Team 2", company=self.company)
|
||||
|
||||
ext_user = User.objects.create_user(username='ext_user', password='pass')
|
||||
ext_config = UserConfig.objects.create(user=ext_user, is_intern=False, company=provider_co)
|
||||
|
||||
# Autoriser explicitement team_own_allowed et team_internal avec can_tag=True
|
||||
UserCompanyTeamAccess.objects.create(user_config=ext_config, team=team_own_allowed, can_tag=True, can_view=True)
|
||||
UserCompanyTeamAccess.objects.create(user_config=ext_config, team=team_internal, can_tag=True, can_view=True)
|
||||
# team_own_forbidden a can_tag=False même si c'est la même société
|
||||
UserCompanyTeamAccess.objects.create(user_config=ext_config, team=team_own_forbidden, can_tag=False, can_view=True)
|
||||
|
||||
self.client.force_login(ext_user)
|
||||
|
||||
# Recherche de toutes les équipes prestataires
|
||||
url = reverse('notifications:api_mention_search') + '?q=Prestataire'
|
||||
res = self.client.get(url)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
team_ids = [r['id'] for r in res.json().get('results', []) if r['type'] == 'team']
|
||||
self.assertIn(team_own_allowed.pk, team_ids)
|
||||
self.assertNotIn(team_own_forbidden.pk, team_ids)
|
||||
|
||||
# Recherche de l'équipe interne autorisée
|
||||
url = reverse('notifications:api_mention_search') + '?q=Régie'
|
||||
res = self.client.get(url)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
team_ids = [r['id'] for r in res.json().get('results', []) if r['type'] == 'team']
|
||||
self.assertIn(team_internal.pk, team_ids)
|
||||
# L'équipe self.team (BM - Régie - Team 1) n'a pas d'accès configuré, donc elle ne doit PAS apparaître
|
||||
self.assertNotIn(self.team.pk, team_ids)
|
||||
|
||||
def test_external_user_without_config_sees_no_teams(self):
|
||||
user_no_cfg = User.objects.create_user(username='user_no_cfg', password='pass')
|
||||
self.client.force_login(user_no_cfg)
|
||||
url = reverse('notifications:api_mention_search') + '?q=Régie'
|
||||
res = self.client.get(url)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
team_results = [r for r in res.json().get('results', []) if r['type'] == 'team']
|
||||
self.assertEqual(team_results, [])
|
||||
|
||||
def test_team_mention_notifies_company_team_members_and_adds_participants(self):
|
||||
from notifications.models import Discussion, NotificationRecipient
|
||||
from notifications.services import post_message
|
||||
|
|
|
|||
|
|
@ -706,13 +706,32 @@ def api_mention_search(request):
|
|||
|
||||
# Teams (CompanyTeam)
|
||||
from contracts.models import CompanyTeam
|
||||
|
||||
user_config = getattr(request.user, 'config', None)
|
||||
is_internal_user = bool(
|
||||
request.user.is_superuser or (user_config and getattr(user_config, 'is_intern', False))
|
||||
)
|
||||
|
||||
if is_internal_user:
|
||||
company_team_qs = CompanyTeam.objects.filter(
|
||||
Q(name__icontains=q) | Q(company__name__icontains=q)
|
||||
).select_related('company').order_by('name')[:15]
|
||||
else:
|
||||
if user_config:
|
||||
company_team_qs = CompanyTeam.objects.filter(
|
||||
user_accesses__user_config=user_config,
|
||||
user_accesses__can_tag=True,
|
||||
).filter(
|
||||
Q(name__icontains=q) | Q(company__name__icontains=q)
|
||||
).select_related('company').distinct().order_by('name')[:15]
|
||||
else:
|
||||
company_team_qs = CompanyTeam.objects.none()
|
||||
|
||||
for t in company_team_qs:
|
||||
results.append({'type': 'team', 'id': t.pk, 'display': t.name})
|
||||
|
||||
# Legacy notifications.models.Team (si existant)
|
||||
# Legacy notifications.models.Team (si existant, réservé aux utilisateurs internes)
|
||||
if is_internal_user:
|
||||
existing_team_ids = {r['id'] for r in results if r['type'] == 'team'}
|
||||
notif_team_qs = Team.objects.filter(name__icontains=q).order_by('name')[:5]
|
||||
for t in notif_team_qs:
|
||||
|
|
|
|||
Loading…
Reference in a new issue