creme-crm 2.7.1__py3-none-any.whl → 2.7.3__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 (66) hide show
  1. creme/__init__.py +1 -1
  2. creme/activities/bricks.py +1 -1
  3. creme/commercial/templates/commercial/bricks/approaches.html +1 -1
  4. creme/creme_config/bricks.py +3 -5
  5. creme/creme_config/static/chantilly/creme_config/css/creme_config.css +5 -1
  6. creme/creme_config/static/creme_config/js/tests/button-menu-editor.js +2 -2
  7. creme/creme_config/static/icecream/creme_config/css/creme_config.css +5 -1
  8. creme/creme_config/templates/creme_config/bricks/users.html +1 -1
  9. creme/creme_config/templates/creme_config/bricks/workflows.html +1 -1
  10. creme/creme_core/gui/button_menu.py +2 -2
  11. creme/creme_core/gui/history.py +18 -4
  12. creme/creme_core/gui/listview/smart_columns.py +67 -8
  13. creme/creme_core/gui/menu.py +1 -1
  14. creme/creme_core/locale/fr/LC_MESSAGES/django.mo +0 -0
  15. creme/creme_core/locale/fr/LC_MESSAGES/django.po +21 -9
  16. creme/creme_core/models/entity_filter.py +79 -18
  17. creme/creme_core/models/setting_value.py +3 -2
  18. creme/creme_core/static/chantilly/creme_core/css/list_view.css +2 -1
  19. creme/creme_core/static/icecream/creme_core/css/list_view.css +8 -4
  20. creme/creme_core/templates/authent/creme_login.html +1 -1
  21. creme/creme_core/templates/creme_core/auth/password_reset/base.html +1 -1
  22. creme/creme_core/templates/creme_core/auth/password_reset/complete.html +2 -2
  23. creme/creme_core/templates/creme_core/auth/password_reset/done.html +2 -2
  24. creme/creme_core/templates/creme_core/auth/password_reset/email/body.txt +2 -2
  25. creme/creme_core/templates/creme_core/bricks/base/hat-card.html +1 -1
  26. creme/creme_core/templates/creme_core/history/html/auxiliary-creation.html +1 -1
  27. creme/creme_core/templates/creme_core/history/html/auxiliary-deletion.html +1 -1
  28. creme/creme_core/templates/creme_core/history/html/auxiliary-edition.html +1 -1
  29. creme/creme_core/templates/creme_core/listview/content.html +69 -72
  30. creme/creme_core/templates/creme_core/templatetags/widgets/enumerator.html +1 -1
  31. creme/creme_core/tests/base.py +3 -2
  32. creme/creme_core/tests/gui/listview/test_smart_columns.py +129 -0
  33. creme/creme_core/tests/gui/test_history.py +142 -3
  34. creme/creme_core/tests/models/test_entity_filter.py +147 -2
  35. creme/creme_core/tests/models/test_setting_value.py +151 -129
  36. creme/creme_core/tests/templatetags/test_creme_core_tags.py +2 -0
  37. creme/creme_core/tests/templatetags/test_creme_listview.py +4 -2
  38. creme/creme_core/tests/templatetags/test_entity_filter.py +2 -1
  39. creme/creme_core/tests/views/test_creme_property.py +3 -1
  40. creme/creme_core/tests/views/test_search.py +1 -1
  41. creme/emails/forms/mail.py +22 -6
  42. creme/emails/templates/emails/bricks/sending-config-items.html +1 -1
  43. creme/emails/templates/emails/bricks/sync-config-items.html +1 -1
  44. creme/emails/tests/test_mail.py +50 -30
  45. creme/emails/tests/test_sending.py +1 -0
  46. creme/emails/views/mail.py +1 -0
  47. creme/opportunities/locale/fr/LC_MESSAGES/django.mo +0 -0
  48. creme/opportunities/locale/fr/LC_MESSAGES/django.po +2 -2
  49. creme/opportunities/populate.py +1 -1
  50. creme/polls/bricks.py +1 -1
  51. creme/reports/bricks.py +4 -3
  52. creme/reports/tests/test_bricks.py +108 -73
  53. creme/reports/tests/test_report.py +11 -2
  54. creme/reports/tests/test_views.py +35 -46
  55. creme/reports/urls.py +1 -0
  56. creme/reports/views/graph.py +7 -2
  57. creme/sketch/templates/sketch/bricks/chart.html +2 -2
  58. creme/sms/tests/test_campaign.py +2 -0
  59. creme/sms/tests/test_messaging_list.py +2 -0
  60. creme/sms/tests/test_template.py +2 -0
  61. {creme_crm-2.7.1.dist-info → creme_crm-2.7.3.dist-info}/METADATA +3 -2
  62. {creme_crm-2.7.1.dist-info → creme_crm-2.7.3.dist-info}/RECORD +66 -65
  63. {creme_crm-2.7.1.dist-info → creme_crm-2.7.3.dist-info}/WHEEL +0 -0
  64. {creme_crm-2.7.1.dist-info → creme_crm-2.7.3.dist-info}/entry_points.txt +0 -0
  65. {creme_crm-2.7.1.dist-info → creme_crm-2.7.3.dist-info}/licenses/LICENSE.txt +0 -0
  66. {creme_crm-2.7.1.dist-info → creme_crm-2.7.3.dist-info}/top_level.txt +0 -0
creme/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = '2.7.1'
1
+ __version__ = '2.7.3'
2
2
 
3
3
 
4
4
  def get_version():
@@ -156,7 +156,7 @@ class SubjectsBrick(QuerysetBrick):
156
156
  id = QuerysetBrick.generate_id('activities', 'subjects')
157
157
  verbose_name = _('Subjects')
158
158
 
159
- dependencies = (Relation, Organisation) # See ParticipantsBlock.dependencies
159
+ dependencies = (Relation, Organisation) # See ParticipantsBrick.dependencies
160
160
  relation_type_deps = (constants.REL_OBJ_ACTIVITY_SUBJECT,)
161
161
 
162
162
  template_name = 'activities/bricks/subjects.html'
@@ -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)
@@ -96,7 +96,11 @@ li.ui-creme-navigation-item-id_creme_config-main > svg:nth-child(2) {
96
96
  color: #818485;
97
97
  }
98
98
 
99
- /* Search/Bricks config */
99
+ /* Registered model config */
100
+
101
+ .creme_config-configurable-model-brick .brick-content {
102
+ overflow-x: auto;
103
+ }
100
104
 
101
105
  .creme_config-configurable-model-brick th.configmodel-column-order {
102
106
  width: 50px;
@@ -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>'
@@ -111,7 +111,11 @@ li.ui-creme-navigation-item-id_creme_config-main > svg:nth-child(2) {
111
111
  color: #777;
112
112
  }
113
113
 
114
- /* Search/Bricks config */
114
+ /* Registered model config */
115
+
116
+ .creme_config-configurable-model-brick .brick-content {
117
+ overflow-x: auto;
118
+ }
115
119
 
116
120
  .creme_config-configurable-model-brick th.configmodel-column-order {
117
121
  width: 50px;
@@ -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
@@ -84,30 +84,89 @@ class _ModelSmartColumnsRegistry:
84
84
 
85
85
  return rtype
86
86
 
87
- def register_function_field(self, func_field_name: str) -> _ModelSmartColumnsRegistry:
88
- self._cells.append((EntityCellFunctionField, func_field_name))
89
- return self
90
-
91
87
  def register_field(self, field_name: str) -> _ModelSmartColumnsRegistry:
88
+ """Register a field by its name."""
92
89
  self._cells.append((EntityCellRegularField, field_name))
93
90
  return self
94
91
 
92
+ def register_function_field(self, func_field_name: str) -> _ModelSmartColumnsRegistry:
93
+ """Register a function field by its name."""
94
+ self._cells.append((EntityCellFunctionField, func_field_name))
95
+ return self
96
+
95
97
  def register_relationtype(self, rtype_id: str) -> _ModelSmartColumnsRegistry:
98
+ """Register a RelationType by its ID."""
96
99
  self._cells.append((EntityCellRelation, rtype_id))
97
100
  return self
98
101
 
102
+ def _unregister(self, cell_type, cell_name, error_message) -> _ModelSmartColumnsRegistry:
103
+ try:
104
+ self._cells.remove((cell_type, cell_name))
105
+ except ValueError as e:
106
+ raise ValueError(error_message.format(cell_name)) from e
107
+
108
+ return self
109
+
110
+ def unregister_field(self, field_name: str) -> _ModelSmartColumnsRegistry:
111
+ """Unregister a field by its name."""
112
+ return self._unregister(
113
+ cell_type=EntityCellRegularField, cell_name=field_name,
114
+ error_message='The field "{}" in not registered.',
115
+ )
116
+
117
+ def unregister_function_field(self, func_field_name: str) -> _ModelSmartColumnsRegistry:
118
+ """Unregister a function field by its name."""
119
+ return self._unregister(
120
+ cell_type=EntityCellFunctionField, cell_name=func_field_name,
121
+ error_message='The function field "{}" in not registered.',
122
+ )
123
+
124
+ def unregister_relationtype(self, rtype_id: str) -> _ModelSmartColumnsRegistry:
125
+ """Unregister a RelationType by its ID."""
126
+ return self._unregister(
127
+ cell_type=EntityCellRelation, cell_name=rtype_id,
128
+ error_message='The relation type "{}" in not registered.',
129
+ )
130
+
99
131
 
100
132
  class SmartColumnsRegistry:
101
- def __init__(self) -> None:
102
- self._model_registries: \
103
- DefaultDict[type[CremeEntity], _ModelSmartColumnsRegistry] \
104
- = defaultdict(_ModelSmartColumnsRegistry)
133
+ """Registry to indicate with EntityCells should be selected by default by
134
+ the form for HeaderFilters (because these columns are often selected for
135
+ this model).
136
+
137
+ Example:
138
+ registry = SmartColumnsRegistry()
139
+ registry.register_model(
140
+ Contact
141
+ ).register_field('last_name').register_field('first_name')
142
+ registry.register_model(
143
+ Organisation
144
+ ).register_field('name').register_relationtype(REL_SUB_PROVIDER)
145
+
146
+ Hint: you'll probably use <CremeAppConfig.register_smart_columns()>.
147
+ """
148
+ _model_registries: DefaultDict[type[CremeEntity], _ModelSmartColumnsRegistry]
149
+
150
+ def __init__(self):
151
+ self._model_registries = defaultdict(_ModelSmartColumnsRegistry)
105
152
 
106
153
  def get_cells(self, model: type[CremeEntity]) -> list[EntityCell]:
154
+ """Get the "smart" cells for a given model."""
107
155
  return self._model_registries[model]._get_cells(model)
108
156
 
157
+ # TODO: rename just "model" (because it's used to retrieve existing too)
109
158
  def register_model(self, model: type[CremeEntity]) -> _ModelSmartColumnsRegistry:
159
+ """Get the sub-registry containing the registered cells for a given model.
160
+ Useful to register & unregister cells by chaining with:
161
+ - [un]register_field()
162
+ - [un]register_function_field()
163
+ - [un]register_relationtype()
164
+ """
110
165
  return self._model_registries[model]
111
166
 
167
+ def clear_model(self, model: type[CremeEntity]) -> None:
168
+ """Remove the sub-registry related to a madel (& so all related cells)."""
169
+ del self._model_registries[model]
170
+
112
171
 
113
172
  smart_columns_registry = SmartColumnsRegistry()
@@ -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
@@ -80,7 +80,7 @@ class SettingValueManager(models.Manager):
80
80
  """Get the SettingValue value or default if not filled.
81
81
 
82
82
  @param key: A SettingKey instance, or an ID of SettingKey (string).
83
- @param default: (optional) If given & the SettingValue does not exist,
83
+ @param default: (optional) If given & the SettingValue does not exist.
84
84
  """
85
85
  try:
86
86
  return self.get_4_key(key, default=default).value
@@ -96,7 +96,8 @@ class SettingValueManager(models.Manager):
96
96
  BEWARE: you should probably just use the "value" attribute of this dummy object
97
97
  (it's not a true SettingValue instance -- see 'DummySettingValue').
98
98
  @return: A SettingValue instance.
99
- @raise: SettingValue.DoesNotExist.
99
+ @raise: SettingValue.DoesNotExist if the SettingValue does not exist &
100
+ no default value has been given.
100
101
  @raise: KeyError if the SettingKey is not registered.
101
102
  """
102
103
  return next(iter(self.get_4_keys({'key': key, **kwargs}).values()))
@@ -364,7 +364,8 @@
364
364
  cursor: pointer;
365
365
  }
366
366
 
367
- .listview thead th:last-child {
367
+ /* .listview thead th:last-child { */
368
+ .listview thead th:nth-last-child(1 of :not(.lv-column-hidden)) {
368
369
  border-right: 1px solid #e1e9ec;
369
370
  }
370
371
 
@@ -259,7 +259,8 @@
259
259
  vertical-align: top;
260
260
 
261
261
  border-left: 0 !important;
262
- border-right: 0 !important;
262
+ /* border-right: 0 !important; */
263
+ border-right: 0;
263
264
 
264
265
  color: #222;
265
266
  }
@@ -882,9 +883,12 @@
882
883
  }
883
884
 
884
885
  /* right border fix for header rows, and body rows */
885
- .listview thead .lv-columns-header th:last-child,
886
- .listview thead .lv-search-header th:last-child,
887
- .listview > tbody > tr > td:last-child {
886
+ /*.listview thead .lv-columns-header th:last-child, */
887
+ .listview thead .lv-columns-header th:nth-last-child(1 of :not(.lv-column-hidden)),
888
+ /* .listview thead .lv-search-header th:last-child, */
889
+ .listview thead .lv-search-header th:nth-last-child(1 of :not(.lv-column-hidden)),
890
+ /*.listview > tbody > tr > td:last-child { */
891
+ .listview > tbody > tr > td:nth-last-child(1 of :not(.lv-cell-hidden)) {
888
892
  border-right: 1px solid rgba(84, 130, 149, 0.3);
889
893
  }
890
894
 
@@ -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 %}
@@ -100,7 +100,7 @@
100
100
  {% block clone_button %}
101
101
  {% get_cloning_info entity=object user=user as cloning %}
102
102
  {% if cloning.enabled %}
103
- <div class='bar-action'>
103
+ <div class='card-action'>
104
104
  {% if cloning.error %}
105
105
  {% blocktranslate asvar clone_button_help with error=cloning.error %}Cloning is forbidden ({{error}}){% endblocktranslate %}
106
106
  {% brick_card_button action='' label=_('Clone') help_text=clone_button_help url='#' icon='clone' enabled=False %}
@@ -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 %}