creme-crm 2.7.1__py3-none-any.whl → 2.7.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. creme/__init__.py +1 -1
  2. creme/commercial/templates/commercial/bricks/approaches.html +1 -1
  3. creme/creme_config/bricks.py +3 -5
  4. creme/creme_config/static/creme_config/js/tests/button-menu-editor.js +2 -2
  5. creme/creme_config/templates/creme_config/bricks/users.html +1 -1
  6. creme/creme_config/templates/creme_config/bricks/workflows.html +1 -1
  7. creme/creme_core/gui/button_menu.py +2 -2
  8. creme/creme_core/gui/history.py +18 -4
  9. creme/creme_core/gui/menu.py +1 -1
  10. creme/creme_core/locale/fr/LC_MESSAGES/django.mo +0 -0
  11. creme/creme_core/locale/fr/LC_MESSAGES/django.po +21 -9
  12. creme/creme_core/models/entity_filter.py +79 -18
  13. creme/creme_core/templates/authent/creme_login.html +1 -1
  14. creme/creme_core/templates/creme_core/auth/password_reset/base.html +1 -1
  15. creme/creme_core/templates/creme_core/auth/password_reset/complete.html +2 -2
  16. creme/creme_core/templates/creme_core/auth/password_reset/done.html +2 -2
  17. creme/creme_core/templates/creme_core/auth/password_reset/email/body.txt +2 -2
  18. creme/creme_core/templates/creme_core/history/html/auxiliary-creation.html +1 -1
  19. creme/creme_core/templates/creme_core/history/html/auxiliary-deletion.html +1 -1
  20. creme/creme_core/templates/creme_core/history/html/auxiliary-edition.html +1 -1
  21. creme/creme_core/templates/creme_core/templatetags/widgets/enumerator.html +1 -1
  22. creme/creme_core/tests/gui/test_history.py +142 -3
  23. creme/creme_core/tests/models/test_entity_filter.py +147 -2
  24. creme/creme_core/tests/templatetags/test_creme_listview.py +4 -2
  25. creme/creme_core/tests/templatetags/test_entity_filter.py +2 -1
  26. creme/creme_core/tests/views/test_creme_property.py +3 -1
  27. creme/emails/forms/mail.py +22 -6
  28. creme/emails/templates/emails/bricks/sending-config-items.html +1 -1
  29. creme/emails/templates/emails/bricks/sync-config-items.html +1 -1
  30. creme/emails/tests/test_mail.py +50 -30
  31. creme/emails/tests/test_sending.py +1 -0
  32. creme/emails/views/mail.py +1 -0
  33. creme/reports/bricks.py +4 -3
  34. creme/reports/tests/test_bricks.py +108 -73
  35. creme/reports/tests/test_report.py +11 -2
  36. creme/reports/tests/test_views.py +35 -46
  37. creme/reports/urls.py +1 -0
  38. creme/reports/views/graph.py +7 -2
  39. creme/sketch/templates/sketch/bricks/chart.html +2 -2
  40. creme/sms/tests/test_campaign.py +2 -0
  41. creme/sms/tests/test_messaging_list.py +2 -0
  42. creme/sms/tests/test_template.py +2 -0
  43. {creme_crm-2.7.1.dist-info → creme_crm-2.7.2.dist-info}/METADATA +1 -1
  44. {creme_crm-2.7.1.dist-info → creme_crm-2.7.2.dist-info}/RECORD +48 -48
  45. {creme_crm-2.7.1.dist-info → creme_crm-2.7.2.dist-info}/WHEEL +0 -0
  46. {creme_crm-2.7.1.dist-info → creme_crm-2.7.2.dist-info}/entry_points.txt +0 -0
  47. {creme_crm-2.7.1.dist-info → creme_crm-2.7.2.dist-info}/licenses/LICENSE.txt +0 -0
  48. {creme_crm-2.7.1.dist-info → creme_crm-2.7.2.dist-info}/top_level.txt +0 -0
creme/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = '2.7.1'
1
+ __version__ = '2.7.2'
2
2
 
3
3
 
4
4
  def get_version():
@@ -21,7 +21,7 @@
21
21
  {% brick_table_column title=_('Related entity') %}
22
22
  {% endif %}
23
23
 
24
- {% trans 'Created on' context 'commercial-approach' as creation_label %}
24
+ {% translate 'Created on' context 'commercial-approach' as creation_label %}
25
25
  {% brick_table_column_for_field ctype=objects_ctype field='creation_date' status='nowrap' title=creation_label %}
26
26
 
27
27
  {% brick_table_column_for_field ctype=objects_ctype field='description' title=_('Details') class='approaches-details' %}
@@ -1339,12 +1339,12 @@ class EntityFiltersBrick(PaginatedBrick):
1339
1339
 
1340
1340
  # NB: efilters[content_type.id][user.id] -> List[EntityFilter]
1341
1341
  efilters = defaultdict(lambda: defaultdict(list))
1342
- user_ids = set()
1342
+ users = {} # Dict[user_id, user]
1343
1343
 
1344
1344
  for efilter in core_models.EntityFilter.objects.filter(
1345
1345
  filter_type=self.filter_type,
1346
1346
  entity_type__in=[ctw.ctype for ctw in ctypes_wrappers],
1347
- ):
1347
+ ).prefetch_related('user', 'user__teammates_set'):
1348
1348
  # TODO: templatetags instead? (+ reason in tooltip if forbidden)
1349
1349
  # efilter.view_perm = efilter.can_view(user)[0]
1350
1350
  efilter.edition_url = reverse(self.edition_url_name, args=(efilter.id,))
@@ -1353,9 +1353,7 @@ class EntityFiltersBrick(PaginatedBrick):
1353
1353
 
1354
1354
  user_id = efilter.user_id
1355
1355
  efilters[efilter.entity_type_id][user_id].append(efilter)
1356
- user_ids.add(user_id)
1357
-
1358
- users = get_user_model().objects.in_bulk(user_ids)
1356
+ users[user_id] = efilter.user
1359
1357
 
1360
1358
  def efilter_key(efilter):
1361
1359
  return sort_key(efilter.name)
@@ -19,11 +19,11 @@ QUnit.module("creme.ButtonMenuEditor", new QUnitMixin(QUnitEventMixin,
19
19
  var html = (
20
20
  '<div class="buttonmenu-edit-widget" id="${id}">' +
21
21
  '<div class="widget-available buttons-list instance-buttons">' +
22
- '<div class="buttons-list-header">{% trans "Available buttons" %}</div>' +
22
+ '<div class="buttons-list-header">Available buttons</div>' +
23
23
  '<div class="widget-container"></div>' +
24
24
  '</div>' +
25
25
  '<div class="widget-selected buttons-list instance-buttons">' +
26
- '<div class="buttons-list-header">{% trans "Selected buttons" %}</div>' +
26
+ '<div class="buttons-list-header">Selected buttons</div>' +
27
27
  '<div class="widget-container" style="width: 100px;height: 100px;"></div>' +
28
28
  '</div>' +
29
29
  '</div>'
@@ -10,7 +10,7 @@
10
10
  {% endcomment %}
11
11
  {% block brick_before %}
12
12
  {% if search_fields and not request|is_ajax %}
13
- <div class="creme_config-users-brick-search">{% trans 'Filter the users' as search_label %}
13
+ <div class="creme_config-users-brick-search">{% translate 'Filter the users' as search_label %}
14
14
  {% widget_icon name='search' size='form-widget' label=search_label %}
15
15
  <input type="search" class="creme_config-users-brick-search-input" placeholder="{{search_label}}" title="{% blocktranslate with fields=search_fields|join:', ' %}Filter the displayed users. Fields used: {{fields}}.{% endblocktranslate %}" />
16
16
  </div>
@@ -18,7 +18,7 @@
18
18
  {% widget_icon ctype=ctype size='brick-list' class='workflow-config-type-icon' %} {{ctype}}
19
19
  </div>
20
20
  <div class="workflow-config-group-action">
21
- {% blocktrans asvar creation_label %}Create a Workflow for «{{ctype}}»{% endblocktrans %}
21
+ {% blocktranslate asvar creation_label %}Create a Workflow for «{{ctype}}»{% endblocktranslate %}
22
22
  {% brick_action id='add' url='creme_config__create_workflow'|url:ctype.id label=creation_label enabled=admin_perm %}
23
23
  </div>
24
24
  </div>
@@ -73,8 +73,8 @@ class Button:
73
73
 
74
74
  # Permission string(s) ; an empty value means no permission is needed.
75
75
  # Example: <'myapp.add_mymodel'>
76
- # BEWARE: you have to use the template context variable "button.is_allowed"
77
- # (computed from 'permissions' -- see 'is_allowed()' ) yourself !!
76
+ # BEWARE: you have to use the template context variable "button.permission_error"
77
+ # (computed from 'permissions' -- see 'check_permissions()' ) yourself.
78
78
  permissions: str | Sequence[str] = ''
79
79
 
80
80
  def __eq__(self, other):
@@ -664,7 +664,12 @@ class HTMLAuxCreationExplainer(HistoryLineExplainer):
664
664
 
665
665
  # TODO: use aux_id to display an up-to-date value ??
666
666
  ct_id, aux_id, str_obj = self.hline.modifications
667
- context['auxiliary_ctype'] = ContentType.objects.get_for_id(ct_id)
667
+ try:
668
+ ctype = ContentType.objects.get_for_id(ct_id)
669
+ except ContentType.DoesNotExist:
670
+ ctype = None
671
+
672
+ context['auxiliary_ctype'] = ctype
668
673
  context['auxiliary_value'] = str_obj
669
674
 
670
675
  return context
@@ -678,7 +683,10 @@ class _AuxiliaryEditionExplainer(HistoryLineExplainer):
678
683
 
679
684
  # TODO: use aux_id to display an up-to-date value ??
680
685
  ct_id, __aux_id, str_obj = modifications[0]
681
- ctype = ContentType.objects.get_for_id(ct_id)
686
+ try:
687
+ ctype = ContentType.objects.get_for_id(ct_id)
688
+ except ContentType.DoesNotExist:
689
+ ctype = None
682
690
 
683
691
  self._aux_ctype = ctype
684
692
  self._aux_value = str_obj
@@ -687,7 +695,7 @@ class _AuxiliaryEditionExplainer(HistoryLineExplainer):
687
695
  model_class=ctype.model_class(),
688
696
  modifications=modifications[1:],
689
697
  ),
690
- ]
698
+ ] if ctype else []
691
699
 
692
700
  def get_context(self):
693
701
  context = super().get_context()
@@ -714,7 +722,13 @@ class HTMLAuxDeletionExplainer(HistoryLineExplainer):
714
722
  context = super().get_context()
715
723
 
716
724
  ct_id, str_obj = self.hline.modifications
717
- context['auxiliary_ctype'] = ContentType.objects.get_for_id(ct_id)
725
+
726
+ try:
727
+ ctype = ContentType.objects.get_for_id(ct_id)
728
+ except ContentType.DoesNotExist:
729
+ ctype = None
730
+
731
+ context['auxiliary_ctype'] = ctype
718
732
  context['auxiliary_value'] = str_obj
719
733
 
720
734
  return context
@@ -409,7 +409,7 @@ class MenuRegistry:
409
409
  # TODO: to be removed in creme2.8
410
410
  if hasattr(entry_cls, '_has_perm'):
411
411
  logger.critical(
412
- 'The class %s still defines a method "is_allowed()"; '
412
+ 'The class %s still defines a method "_has_perm()"; '
413
413
  'define the new method "check_permissions()" instead.',
414
414
  entry_cls,
415
415
  )
@@ -8,7 +8,7 @@ msgid ""
8
8
  msgstr ""
9
9
  "Project-Id-Version: Creme Creme-Core 2.7\n"
10
10
  "Report-Msgid-Bugs-To: \n"
11
- "POT-Creation-Date: 2025-07-31 10:52+0200\n"
11
+ "POT-Creation-Date: 2025-10-20 17:01+0200\n"
12
12
  "Last-Translator: Hybird <contact@hybird.org>\n"
13
13
  "Language: fr\n"
14
14
  "MIME-Version: 1.0\n"
@@ -2445,22 +2445,34 @@ msgstr "Filtre de fiche"
2445
2445
  msgid "Filters of Entity"
2446
2446
  msgstr "Filtres de fiche"
2447
2447
 
2448
- msgid "This filter can't be edited/deleted"
2449
- msgstr "Ce filtre ne peut être modifié/effacé"
2448
+ msgid "This filter can't be deleted (system filter)"
2449
+ msgstr "Ce filtre ne peut être supprimé (filtre système)"
2450
2450
 
2451
2451
  msgid "You are not allowed to access to this app"
2452
2452
  msgstr "Vous n'avez pas la permission d'accéder à cette application"
2453
2453
 
2454
- msgid "Only superusers can edit/delete this filter (no owner)"
2454
+ msgid "Only superusers can delete this filter (no owner)"
2455
2455
  msgstr ""
2456
- "Seuls les super-utilisateurs peuvent modifier/supprimer ce filtre (pas de "
2456
+ "Seuls les super-utilisateurs peuvent supprimer ce filtre (pas de "
2457
2457
  "propriétaire)"
2458
2458
 
2459
- msgid ""
2460
- "You are not allowed to view/edit/delete this filter (you are not the owner)"
2459
+ msgid "You are not allowed to delete this filter (you are not the owner)"
2460
+ msgstr ""
2461
+ "Vous n'avez pas la permission de supprimer ce filtre (il ne vous appartient "
2462
+ "pas)"
2463
+
2464
+ msgid "Only superusers can edit this filter (no owner)"
2465
+ msgstr ""
2466
+ "Seuls les super-utilisateurs peuvent modifier ce filtre (pas de propriétaire)"
2467
+
2468
+ msgid "You are not allowed to edit this filter (you are not the owner)"
2469
+ msgstr ""
2470
+ "Vous n'avez pas la permission de modifier ce filtre (il ne vous appartient "
2471
+ "pas)"
2472
+
2473
+ msgid "You are not allowed to view this filter (you are not the owner)"
2461
2474
  msgstr ""
2462
- "Vous n'avez pas la permission de voir/modifier/supprimer ce filtre (il ne "
2463
- "vous appartient pas)"
2475
+ "Vous n'avez pas la permission de voir ce filtre (il ne vous appartient pas)"
2464
2476
 
2465
2477
  msgid "A condition can not reference its own filter."
2466
2478
  msgstr "Une condition ne peut pas référencer son propre filtre."
@@ -423,15 +423,24 @@ class EntityFilter(models.Model): # TODO: CremeModel? MinionModel?
423
423
  """
424
424
  return all(c.handler.applicable_on_entity_base for c in self.get_conditions())
425
425
 
426
+ # TODO: can_*() methods:
427
+ # - move to a registry?
428
+ # - factorise
426
429
  def can_delete(self, user: CremeUser) -> tuple[bool, str]:
430
+ # if not self.is_custom:
431
+ # return False, gettext("This filter can't be edited/deleted")
432
+ #
433
+ # return self.can_edit(user)
434
+ assert not user.is_team
435
+
427
436
  if not self.is_custom:
428
- return False, gettext("This filter can't be edited/deleted")
437
+ return False, gettext("This filter can't be deleted (system filter)")
429
438
 
430
- return self.can_edit(user)
439
+ if user.is_staff:
440
+ return True, 'OK'
431
441
 
432
- # TODO: move to registry?
433
- def can_edit(self, user: CremeUser) -> tuple[bool, str]:
434
- assert not user.is_team
442
+ if user.is_superuser and not self.is_private:
443
+ return True, 'OK'
435
444
 
436
445
  if not user.has_perm(self.entity_type.app_label):
437
446
  return False, gettext('You are not allowed to access to this app')
@@ -444,42 +453,94 @@ class EntityFilter(models.Model): # TODO: CremeModel? MinionModel?
444
453
  (True, 'OK')
445
454
  if user.is_superuser
446
455
  or SettingValue.objects.get_4_key(global_filters_edition_key).value else
447
- # TODO: should the filter can be (detail-)viewed anyway?
448
- (False, gettext('Only superusers can edit/delete this filter (no owner)'))
456
+ (False, gettext('Only superusers can delete this filter (no owner)'))
449
457
  )
450
458
 
459
+ if not self.user.is_team:
460
+ if self.user_id == user.id:
461
+ return True, 'OK'
462
+ elif user.id in self.user.teammates: # TODO: move in a User method ??
463
+ return True, 'OK'
464
+
465
+ return (
466
+ False,
467
+ gettext('You are not allowed to delete this filter (you are not the owner)'),
468
+ )
469
+
470
+ def can_edit(self, user: CremeUser) -> tuple[bool, str]:
471
+ assert not user.is_team
472
+
473
+ if not user.has_perm(self.entity_type.app_label):
474
+ return False, gettext('You are not allowed to access to this app')
475
+
451
476
  if user.is_staff:
452
477
  return True, 'OK'
453
478
 
454
479
  if user.is_superuser and not self.is_private:
455
480
  return True, 'OK'
456
481
 
482
+ if not self.user_id: # All users allowed
483
+ from .setting_value import SettingValue
484
+
485
+ return (
486
+ (True, 'OK')
487
+ if user.is_superuser
488
+ or SettingValue.objects.get_4_key(global_filters_edition_key).value else
489
+ # (False, gettext('Only superusers can edit/delete this filter (no owner)'))
490
+ (False, gettext('Only superusers can edit this filter (no owner)'))
491
+ )
492
+
457
493
  if not self.user.is_team:
458
494
  if self.user_id == user.id:
459
495
  return True, 'OK'
460
- elif user.id in self.user.teammates: # TODO: move in a User method ??
496
+ elif user.id in self.user.teammates:
461
497
  return True, 'OK'
462
498
 
463
499
  return (
464
500
  False,
465
501
  gettext(
466
- 'You are not allowed to view/edit/delete this filter '
502
+ # 'You are not allowed to view/edit/delete this filter '
503
+ 'You are not allowed to edit this filter '
467
504
  '(you are not the owner)'
468
505
  )
469
506
  )
470
507
 
471
508
  # def can_view(self, user: CremeUser, content_type=_NOT_PASSED) -> tuple[bool, str]:
472
509
  def can_view(self, user: CremeUser) -> tuple[bool, str]:
473
- # if content_type is not _NOT_PASSED:
474
- # warnings.warn(
475
- # 'In EntityFilter.can_view(), the argument "content_type" is deprecated.',
476
- # DeprecationWarning,
477
- # )
478
- #
479
- # if content_type and content_type != self.entity_type:
480
- # return False, 'Invalid entity type'
510
+ # # if content_type is not _NOT_PASSED:
511
+ # # warnings.warn(
512
+ # # 'In EntityFilter.can_view(), the argument "content_type" is deprecated.',
513
+ # # DeprecationWarning,
514
+ # # )
515
+ # #
516
+ # # if content_type and content_type != self.entity_type:
517
+ # # return False, 'Invalid entity type'
518
+ # return self.can_edit(user)
519
+ assert not user.is_team
481
520
 
482
- return self.can_edit(user)
521
+ if user.is_staff:
522
+ return True, 'OK'
523
+
524
+ if not self.is_private:
525
+ return (
526
+ (True, 'OK')
527
+ if user.is_superuser or user.has_perm(self.entity_type.app_label) else
528
+ (False, gettext('You are not allowed to access to this app'))
529
+ )
530
+
531
+ if not self.user.is_team:
532
+ if self.user_id == user.id:
533
+ return True, 'OK'
534
+ elif user.id in self.user.teammates:
535
+ return True, 'OK'
536
+
537
+ return (
538
+ False,
539
+ gettext(
540
+ 'You are not allowed to view this filter '
541
+ '(you are not the owner)'
542
+ ),
543
+ )
483
544
 
484
545
  def check_cycle(self, conditions: Iterable[EntityFilterCondition]) -> None:
485
546
  assert self.id
@@ -24,7 +24,7 @@
24
24
 
25
25
  {% if world_settings.password_reset_enabled %}
26
26
  <div class="lost-password">
27
- <a href="{% url 'creme_core__reset_password' %}">{% trans 'You lost your password?' %}</a>
27
+ <a href="{% url 'creme_core__reset_password' %}">{% translate 'You lost your password?' %}</a>
28
28
  </div>
29
29
  {% endif %}
30
30
  {% endblock %}
@@ -109,7 +109,7 @@
109
109
  }
110
110
  {% endblock %}
111
111
  </style>
112
- <title>{% trans 'Reset your password' %} - Creme CRM</title>
112
+ <title>{% translate 'Reset your password' %} - Creme CRM</title>
113
113
  <link rel="shortcut icon" href="{% media_url 'common/images/favicon.ico' %}" type="image/x-icon" />
114
114
  </head>
115
115
  <body>
@@ -28,8 +28,8 @@
28
28
  {% endblock %}
29
29
 
30
30
  {% block main %}
31
- <p>{% trans 'Your new password has been saved.' %}</p>
31
+ <p>{% translate 'Your new password has been saved.' %}</p>
32
32
  <div class="login">
33
- <a href="{% url 'creme_login' %}">{% trans 'Log in' %}</a>
33
+ <a href="{% url 'creme_login' %}">{% translate 'Log in' %}</a>
34
34
  </div>
35
35
  {% endblock %}
@@ -10,6 +10,6 @@
10
10
  {% endblock %}
11
11
 
12
12
  {% block main %}
13
- <p>{% trans 'If the given email address exists in Creme CRM database then instructions to reset your password has been sent.' %}</p>
14
- <p>{% trans 'They should not be long!' %}</p>
13
+ <p>{% translate 'If the given email address exists in Creme CRM database then instructions to reset your password has been sent.' %}</p>
14
+ <p>{% translate 'They should not be long!' %}</p>
15
15
  {% endblock %}
@@ -1,4 +1,4 @@
1
- {% load i18n %}{% load templatize from creme_core_tags %}{% url 'creme_core__password_reset_confirm' uidb64=uid token=token as rel_url %}{% templatize '{{protocol}}://{{domain}}{{rel_url}}' as url %}{% blocktrans with username=user.username %}Hi,
1
+ {% load i18n %}{% load templatize from creme_core_tags %}{% url 'creme_core__password_reset_confirm' uidb64=uid token=token as rel_url %}{% templatize '{{protocol}}://{{domain}}{{rel_url}}' as url %}{% blocktranslate with username=user.username %}Hi,
2
2
 
3
3
  You receive this email because a reset of your password for {{software}} has been requested.
4
4
 
@@ -9,4 +9,4 @@ Here your username in case you forgot it too: {{username}}
9
9
  Thanks for show an interest in {{software}}.
10
10
 
11
11
  Your {{software}} administrator
12
- {% endblocktrans %}
12
+ {% endblocktranslate %}
@@ -1,5 +1,5 @@
1
1
  {% extends 'creme_core/history/html/base.html' %}
2
2
  {% load i18n %}
3
3
  {% block content %}
4
- {% blocktranslate %}“{{auxiliary_ctype}}“ added: {{auxiliary_value}}{% endblocktranslate %}
4
+ {% blocktranslate with auxiliary_ctype=auxiliary_ctype|default_if_none:'??' %}“{{auxiliary_ctype}}“ added: {{auxiliary_value}}{% endblocktranslate %}
5
5
  {% endblock %}
@@ -1,5 +1,5 @@
1
1
  {% extends 'creme_core/history/html/base.html' %}
2
2
  {% load i18n %}
3
3
  {% block content %}
4
- {% blocktranslate %}“{{auxiliary_ctype}}“ deleted: “{{auxiliary_value}}”{% endblocktranslate %}
4
+ {% blocktranslate with auxiliary_ctype=auxiliary_ctype|default_if_none:'??' %}“{{auxiliary_ctype}}“ deleted: “{{auxiliary_value}}”{% endblocktranslate %}
5
5
  {% endblock %}
@@ -8,7 +8,7 @@
8
8
  <div class="toggle-icon-container toggle-icon-expand" title="{% translate 'Expand' %}"><div class="toggle-icon"></div></div>
9
9
  <div class="toggle-icon-container toggle-icon-collapse" title="{% translate 'Close' %}"><div class="toggle-icon"></div></div>
10
10
 
11
- <span class="history-line-title">{% blocktranslate %}“{{auxiliary_ctype}}“ edited: {{auxiliary_value}}{% endblocktranslate %}</span>
11
+ <span class="history-line-title">{% blocktranslate with auxiliary_ctype=auxiliary_ctype|default_if_none:'??' %}“{{auxiliary_ctype}}“ edited: {{auxiliary_value}}{% endblocktranslate %}</span>
12
12
  </div>
13
13
  <ul class="history-line-details">{% for modif in modifications %}<li>{{modif}}</li>{% endfor %}</ul>
14
14
  {% endblock %}
@@ -5,7 +5,7 @@
5
5
  {% else %}
6
6
  <a data-action="popover">
7
7
  {% if summary is None %}
8
- {% blocktrans with count=length %}{{count}} items{% endblocktrans %}
8
+ {% blocktranslate with count=length %}{{count}} items{% endblocktranslate %}
9
9
  {% else %}
10
10
  {% format_string_brace_named summary count=length %}
11
11
  {% endif %}
@@ -1413,7 +1413,47 @@ class HistoryRenderTestCase(CremeTestCase):
1413
1413
  self.render_line(hline, user),
1414
1414
  )
1415
1415
 
1416
- def test_render_auxiliary_edition01(self):
1416
+ def test_render_auxiliary_creation__invalid_ctype_id(self):
1417
+ user = self.get_root_user()
1418
+ gainax = FakeOrganisation.objects.create(user=user, name='Gainax')
1419
+ address = FakeAddress.objects.create(
1420
+ entity=gainax, country='Japan', city='Tokyo',
1421
+ )
1422
+
1423
+ hline = self.get_hline()
1424
+ self.assertEqual(history.TYPE_AUX_CREATION, hline.type)
1425
+ # print(hline.value)
1426
+
1427
+ self.assertListEqual(
1428
+ [
1429
+ 'Gainax',
1430
+ ContentType.objects.get_for_model(FakeAddress).id,
1431
+ address.id,
1432
+ 'Tokyo Japan',
1433
+ ],
1434
+ json.loads(hline.value),
1435
+ )
1436
+ hline.value = json.dumps([
1437
+ 'Gainax',
1438
+ self.UNUSED_PK, # <==
1439
+ address.id,
1440
+ 'Tokyo Japan',
1441
+ ])
1442
+ hline.save()
1443
+
1444
+ self.assertHTMLEqual(
1445
+ '<div class="history-line history-line-auxiliary_creation">'
1446
+ '{title}'
1447
+ '<div>'.format(
1448
+ title=_('“%(auxiliary_ctype)s“ added: %(auxiliary_value)s') % {
1449
+ 'auxiliary_ctype': '??',
1450
+ 'auxiliary_value': escape(address),
1451
+ },
1452
+ ),
1453
+ self.render_line(hline, user),
1454
+ )
1455
+
1456
+ def test_render_auxiliary_edition(self):
1417
1457
  user = self.get_root_user()
1418
1458
 
1419
1459
  country = 'Japan'
@@ -1467,7 +1507,7 @@ class HistoryRenderTestCase(CremeTestCase):
1467
1507
  self.render_line(hline, user),
1468
1508
  )
1469
1509
 
1470
- def test_render_auxiliary_edition02(self):
1510
+ def test_render_auxiliary_edition__entity(self):
1471
1511
  """FakeInvoiceLine:
1472
1512
  - an auxiliary + CremeEntity at the same time.
1473
1513
  - DecimalField.
@@ -1542,7 +1582,7 @@ class HistoryRenderTestCase(CremeTestCase):
1542
1582
  self.render_line(hline, user),
1543
1583
  )
1544
1584
 
1545
- def test_render_auxiliary_edition_m2m(self):
1585
+ def test_render_auxiliary_edition__m2m(self):
1546
1586
  user = self.get_root_user()
1547
1587
  cat = FakeTodoCategory.objects.create(name='Very <b>Important</b>')
1548
1588
 
@@ -1591,6 +1631,66 @@ class HistoryRenderTestCase(CremeTestCase):
1591
1631
  self.render_line(hline, user),
1592
1632
  )
1593
1633
 
1634
+ def test_render_auxiliary_edition__invalid_ctype_id(self):
1635
+ user = self.get_root_user()
1636
+
1637
+ gainax = FakeOrganisation.objects.create(user=user, name='Gainax')
1638
+ address = FakeAddress.objects.create(entity=gainax, country='Japan')
1639
+
1640
+ address = self.refresh(address)
1641
+ address.department = 'Tokyo'
1642
+ address.save()
1643
+
1644
+ hline = self.get_hline()
1645
+ self.assertEqual(history.TYPE_AUX_EDITION, hline.type)
1646
+ self.assertListEqual(
1647
+ [
1648
+ 'Gainax',
1649
+ [
1650
+ ContentType.objects.get_for_model(FakeAddress).id,
1651
+ address.id,
1652
+ 'Tokyo Japan',
1653
+ ],
1654
+ ['department', 'Tokyo'],
1655
+ ],
1656
+ json.loads(hline.value),
1657
+ )
1658
+ hline.value = json.dumps([
1659
+ 'Gainax',
1660
+ [
1661
+ self.UNUSED_PK, # <==
1662
+ address.id,
1663
+ 'Tokyo Japan',
1664
+ ],
1665
+ ['department', 'Tokyo'],
1666
+ ])
1667
+ hline.save()
1668
+ self.maxDiff = None
1669
+ self.assertHTMLEqual(
1670
+ '<div class="history-line history-line-auxiliary_edition'
1671
+ ' history-line-collapsable history-line-collapsed">'
1672
+ ' <div class="history-line-main">'
1673
+ ' <div class="toggle-icon-container toggle-icon-expand" title="{expand_title}">'
1674
+ ' <div class="toggle-icon"></div>'
1675
+ ' </div>'
1676
+ ' <div class="toggle-icon-container toggle-icon-collapse"'
1677
+ ' title="{collapse_title}">'
1678
+ ' <div class="toggle-icon"></div>'
1679
+ ' </div>'
1680
+ ' <span class="history-line-title">{title}</span>'
1681
+ ' </div>'
1682
+ ' <ul class="history-line-details"></ul>'
1683
+ '<div>'.format(
1684
+ title=_('“%(auxiliary_ctype)s“ edited: %(auxiliary_value)s') % {
1685
+ 'auxiliary_ctype': '??',
1686
+ 'auxiliary_value': address,
1687
+ },
1688
+ expand_title=_('Expand'),
1689
+ collapse_title=_('Close'),
1690
+ ),
1691
+ self.render_line(hline, user),
1692
+ )
1693
+
1594
1694
  def test_render_auxiliary_deletion(self):
1595
1695
  user = self.get_root_user()
1596
1696
  gainax = FakeOrganisation.objects.create(user=user, name='Gainax')
@@ -1615,6 +1715,45 @@ class HistoryRenderTestCase(CremeTestCase):
1615
1715
  self.render_line(hline, user),
1616
1716
  )
1617
1717
 
1718
+ def test_render_auxiliary_deletion__invalid_ctype_id(self):
1719
+ user = self.get_root_user()
1720
+ gainax = FakeOrganisation.objects.create(user=user, name='Gainax')
1721
+ address = FakeAddress.objects.create(
1722
+ entity=gainax, country='Japan', city='Tokyo',
1723
+ )
1724
+
1725
+ address.delete()
1726
+
1727
+ hline = self.get_hline()
1728
+ self.assertEqual(history.TYPE_AUX_DELETION, hline.type)
1729
+ self.assertListEqual(
1730
+ [
1731
+ 'Gainax',
1732
+ ContentType.objects.get_for_model(FakeAddress).id,
1733
+ 'Tokyo Japan',
1734
+ ],
1735
+ json.loads(hline.value),
1736
+ )
1737
+
1738
+ hline.value = json.dumps([
1739
+ 'Gainax',
1740
+ self.UNUSED_PK, # <==
1741
+ 'Tokyo Japan',
1742
+ ])
1743
+
1744
+ self.assertHTMLEqual(
1745
+ format_html(
1746
+ '<div class="history-line history-line-auxiliary_deletion">'
1747
+ '{title}'
1748
+ '<div>',
1749
+ title=_('“%(auxiliary_ctype)s“ deleted: “%(auxiliary_value)s”') % {
1750
+ 'auxiliary_ctype': '??',
1751
+ 'auxiliary_value': address,
1752
+ },
1753
+ ),
1754
+ self.render_line(hline, user),
1755
+ )
1756
+
1618
1757
  def test_render_trash(self):
1619
1758
  user = self.get_root_user()
1620
1759
  gainax = self.refresh(