django-bom 1.237__py3-none-any.whl → 1.238__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.
bom/auth_backends.py ADDED
@@ -0,0 +1,45 @@
1
+ from typing import Optional
2
+
3
+ from . import constants
4
+ from .models import Organization
5
+
6
+
7
+ class OrganizationPermissionBackend:
8
+ """
9
+ Object-level permission backend for django-bom.
10
+
11
+ - Uses Django's has_perm(user, perm, obj) to evaluate permissions tied to an Organization.
12
+ - Superusers are granted all permissions.
13
+ - For `bom.manage_members`: User must be owner or admin within the organization.
14
+ """
15
+
16
+ def authenticate(self, request, **credentials): # pragma: no cover - not used for auth
17
+ return None
18
+
19
+ def has_perm(self, user_obj, perm: str, obj: Optional[object] = None):
20
+ # Only handle our specific object-level permission. Let other backends process others.
21
+ if not user_obj or not user_obj.is_authenticated:
22
+ return False
23
+
24
+ if user_obj.is_superuser:
25
+ return True
26
+
27
+ if perm != 'bom.manage_members':
28
+ return None
29
+
30
+ if obj is None or not isinstance(obj, Organization):
31
+ return False
32
+
33
+ profile = user_obj.bom_profile()
34
+ if not profile or profile.organization_id != obj.id:
35
+ return False
36
+
37
+ if obj.subscription != constants.SUBSCRIPTION_TYPE_PRO:
38
+ return False
39
+
40
+ is_owner = obj.owner_id == user_obj.id
41
+ is_admin = getattr(profile, 'role', None) == constants.ROLE_TYPE_ADMIN
42
+ if not (is_owner or is_admin):
43
+ return False
44
+
45
+ return True
bom/models.py CHANGED
@@ -1,7 +1,6 @@
1
1
  from __future__ import unicode_literals
2
2
 
3
3
  import logging
4
- from math import ceil
5
4
 
6
5
  from django.conf import settings
7
6
  from django.contrib.auth import get_user_model
@@ -9,45 +8,16 @@ from django.core.cache import cache
9
8
  from django.core.validators import MaxValueValidator, MinValueValidator
10
9
  from django.db import models
11
10
  from django.utils import timezone
12
-
13
11
  from djmoney.models.fields import CURRENCY_CHOICES, CurrencyField, MoneyField
12
+ from math import ceil
14
13
  from social_django.models import UserSocialAuth
15
14
 
16
15
  from .base_classes import AsDictModel
17
- from .constants import (
18
- CONFIGURATION_TYPES,
19
- CURRENT_UNITS,
20
- DISTANCE_UNITS,
21
- FREQUENCY_UNITS,
22
- INTERFACE_TYPES,
23
- MEMORY_UNITS,
24
- NUMBER_CLASS_CODE_LEN_DEFAULT,
25
- NUMBER_CLASS_CODE_LEN_MAX,
26
- NUMBER_CLASS_CODE_LEN_MIN,
27
- NUMBER_ITEM_LEN_DEFAULT,
28
- NUMBER_ITEM_LEN_MAX,
29
- NUMBER_ITEM_LEN_MIN,
30
- NUMBER_SCHEME_INTELLIGENT,
31
- NUMBER_SCHEME_SEMI_INTELLIGENT,
32
- NUMBER_SCHEMES,
33
- NUMBER_VARIATION_LEN_DEFAULT,
34
- NUMBER_VARIATION_LEN_MAX,
35
- NUMBER_VARIATION_LEN_MIN,
36
- PACKAGE_TYPES,
37
- POWER_UNITS,
38
- ROLE_TYPES,
39
- SUBSCRIPTION_TYPES,
40
- TEMPERATURE_UNITS,
41
- VALUE_UNITS,
42
- VOLTAGE_UNITS,
43
- WAVELENGTH_UNITS,
44
- WEIGHT_UNITS,
45
- )
16
+ from .constants import *
46
17
  from .csv_headers import PartsListCSVHeaders, PartsListCSVHeadersSemiIntelligent
47
18
  from .part_bom import PartBom, PartBomItem, PartIndentedBomItem
48
19
  from .utils import increment_str, listify_string, prep_for_sorting_nicely, stringify_list, strip_trailing_zeros
49
- from .validators import alphanumeric, numeric, validate_pct
50
-
20
+ from .validators import alphanumeric, validate_pct
51
21
 
52
22
  logger = logging.getLogger(__name__)
53
23
  User = get_user_model()
@@ -75,6 +45,11 @@ class Organization(models.Model):
75
45
  google_drive_parent = models.CharField(max_length=128, blank=True, default=None, null=True)
76
46
  currency = CurrencyField(max_length=3, choices=CURRENCY_CHOICES, default='USD')
77
47
 
48
+ class Meta:
49
+ permissions = (
50
+ ("manage_members", "Can manage organization members"),
51
+ )
52
+
78
53
  def number_cs(self):
79
54
  return "C" * self.number_class_code_len
80
55
 
@@ -0,0 +1,49 @@
1
+ {% load materializecss %}
2
+ {% load static %}
3
+
4
+ <a class="waves-effect waves-light btn btn-primary modal-trigger modal-{{ name }}"
5
+ href="#modal-{{ name }}">{{ modal_title }}</a>
6
+
7
+ <div id="modal-{{ name }}" class="modal left-align">
8
+ <form name="seller" action="{{ action }}" method="post" class="col s12" enctype="multipart/form-data">
9
+ <div class="modal-content">
10
+ <div class="row">
11
+ {% if modal_title %}<h2 class="light" style="margin-top: 0;">{{ modal_title }}</h2>{% endif %}
12
+ <div class="col s12">
13
+ {% if is_pro and user_can_manage_members %}
14
+ {% csrf_token %}
15
+ <p class="flow-text-small grey-text text-darken-2">To add a user, ensure they have already
16
+ created an IndaBOM account and are not currently part of another organization.
17
+ Then, enter their <b>username</b> (not email) and select their role below.</p>
18
+ {{ form|materializecss:'m6 s12' }}
19
+ {% elif is_pro and not user_can_manage_members %}
20
+ <p class="flow-text-small red-text text-darken-1">
21
+ <b>Access Restricted:</b> Please contact an organization administrator to add new members to
22
+ your team.
23
+ </p>
24
+ {% else %}
25
+ <p>Upgrade your organization subscription to professional to manage members.</p>
26
+ {% endif %}
27
+ </div>
28
+ </div>
29
+ </div>
30
+ <div class="modal-footer">
31
+ <a href="#!" class="modal-close waves-effect waves-green btn-flat">Cancel</a>
32
+ {% if is_pro and user_can_manage_members %}
33
+ <button class="waves-effect waves-light btn btn-primary" type="submit" name="{{ name }}">Submit</button>
34
+ {% elif is_pro and not user_can_manage_members %}
35
+ <a href="#!" class="modal-close waves-effect green lighten-1 waves-green btn-flat">Ok</a>
36
+ {% else %}
37
+ <a href="#!" class="modal-close waves-effect green lighten-1 waves-green btn-flat">Ok</a>
38
+ {% endif %}
39
+ </div>
40
+ </form>
41
+ </div>
42
+
43
+ {% block script %}
44
+ <script>
45
+ $(document).ready(function () {
46
+ $('.modal').modal();
47
+ });
48
+ </script>
49
+ {% endblock %}
@@ -207,7 +207,6 @@
207
207
  <div class="row">
208
208
  <div class="col s12">
209
209
  <h3>Users</h3>
210
- {% if users_in_organization.count > 0 %}
211
210
  <form name="seller" action="{% url 'bom:settings' tab_anchor=ORGANIZATION_TAB %}" method="post" class="col s12" enctype="multipart/form-data">
212
211
  {% csrf_token %}
213
212
  <table>
@@ -237,6 +236,13 @@
237
236
  class="material-icons left">edit</i>Edit</a>
238
237
  </td>
239
238
  </tr>
239
+ {% empty %}
240
+ <tr>
241
+ <td colspan="5" style="font-style: italic;">There are no additional
242
+ users in this
243
+ organization.
244
+ </td>
245
+ </tr>
240
246
  {% endfor %}
241
247
  </tbody>
242
248
  </table>
@@ -247,26 +253,10 @@
247
253
  </button>
248
254
  </div>
249
255
  <div class="col s6 right-align">
250
- {% include 'bom/bom-form-modal.html' with modal_title='Add User' form=user_add_form action=user_add_form_action name='submit-add-user' modal_description='Adding users to your organization is a paid feature, but is free for a limited time while we are still developing the tool. Contact <a href="mailto:info@indabom.com">info@indabom.com</a> if you are interested.' %}
256
+ {% include 'bom/bom-modal-add-users.html' with modal_title='Add User' form=user_add_form action=user_add_form_action name='submit-add-user' %}
251
257
  </div>
252
258
  </div>
253
259
  </form>
254
- {% else %}
255
- <div class="row" style="padding-left: .75rem;">
256
- <div class="col s12">
257
- <p>There are no additional users in this organization: {{ organization }}.</p>
258
- {% if organization.subscription == 'F' %}
259
- {% include 'bom/bom-form-modal.html' with modal_title='Add User' name='submit-add-user-free' modal_description='Adding users to your organization is a paid feature, but is free for a limited time while we are still developing the tool. Contact <a href="mailto:info@indabom.com">info@indabom.com</a> if you are interested.' %}
260
- {% else %}
261
- {% if organization.subscription_quantity == 0 or users_in_organization.count < organization.subscription_quantity %}
262
- {% include 'bom/bom-form-modal.html' with modal_title='Add User' form=user_add_form action=user_add_form_action name='submit-add-user' modal_description='Add a user to your organization.' %}
263
- {% else %}
264
- <p>You've added the maximum number of users to this organization. Update your subscription to add more.</p>
265
- {% endif %}
266
- {% endif %}
267
- </div>
268
- </div>
269
- {% endif %}
270
260
  </div>
271
261
  </div>
272
262
  <div class="row">
bom/views/views.py CHANGED
@@ -20,7 +20,6 @@ from django.urls import reverse
20
20
  from django.utils.encoding import smart_str
21
21
  from django.utils.text import smart_split
22
22
  from django.views.generic.base import TemplateView
23
-
24
23
  from social_django.models import UserSocialAuth
25
24
 
26
25
  import bom.constants as constants
@@ -74,7 +73,6 @@ from bom.models import (
74
73
  )
75
74
  from bom.utils import check_references_for_duplicates, listify_string, prep_for_sorting_nicely
76
75
 
77
-
78
76
  logger = logging.getLogger(__name__)
79
77
  BOM_LOGIN_URL = getattr(settings, "BOM_LOGIN_URL", None) or settings.LOGIN_URL
80
78
 
@@ -361,6 +359,10 @@ def bom_settings(request, tab_anchor=None):
361
359
  users_in_organization = User.objects.filter(
362
360
  id__in=UserMeta.objects.filter(organization=organization).values_list('user', flat=True)).exclude(id__in=[organization.owner.id]).order_by(
363
361
  'first_name', 'last_name', 'email')
362
+ users_in_organization_count = users_in_organization.count()
363
+ has_member_capacity = users_in_organization_count < organization.subscription_quantity
364
+ is_pro = organization.subscription == constants.SUBSCRIPTION_TYPE_PRO
365
+ user_can_manage_members = request.user.has_perm('bom.manage_members', organization)
364
366
  google_authentication = UserSocialAuth.objects.filter(user=user).first()
365
367
 
366
368
  organization_parts_count = Part.objects.filter(organization=organization).count()
@@ -386,8 +388,10 @@ def bom_settings(request, tab_anchor=None):
386
388
 
387
389
  elif 'submit-add-user' in request.POST:
388
390
  tab_anchor = ORGANIZATION_TAB
389
- if organization.subscription == 'F':
390
- messages.error(request, "Error: You must have a paid account to add users.")
391
+ if not is_pro:
392
+ messages.error(request, "You need a Pro subscription to add users.")
393
+ elif not user_can_manage_members:
394
+ messages.error(request, "You are not allowed to manage users, contact your organization admin.")
391
395
  else:
392
396
  user_add_form = UserAddForm(request.POST, organization=organization)
393
397
  if user_add_form.is_valid():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-bom
3
- Version: 1.237
3
+ Version: 1.238
4
4
  Summary: A simple Django app to manage a bill of materials.
5
5
  Author-email: Mike Kasparian <mpkasp@gmail.com>
6
6
  License: GPL-3.0-only
@@ -1,6 +1,7 @@
1
1
  bom/__init__.py,sha256=HuvSMR9cYQcppTZGD0XjUVUBtHWwWMh1yMQzk_2wTS4,41
2
2
  bom/admin.py,sha256=4xN38uSIGo54MVoo2MXUbvcfuPSAifDc8g0arrEAW4Q,4093
3
3
  bom/apps.py,sha256=TJMUTSX1h2genPwCq6SN6g1fhrrSmjEXGg2zQFa_ryM,147
4
+ bom/auth_backends.py,sha256=8ViZfP01fITPQnPvsTe_RuKx-ur9dhSOQE3aOp9ESV8,1429
4
5
  bom/base_classes.py,sha256=CrWD7wlIkwYb90VoGcVOcw2WoQszRrCje-Re5d_UW1Q,1183
5
6
  bom/constants.py,sha256=5CFE0uvKTL91w24rqdAB6EMgpIyw0HhfmmktxF4gCgs,4296
6
7
  bom/context_processors.py,sha256=OxMVCqxGtRoHR7aJfTs6xANpxJQzBDUIuNTG1f_Xjoo,312
@@ -10,7 +11,7 @@ bom/form_fields.py,sha256=tLqchl0j8izBCZnm82NMyHpQV6EuGgCjCl5lrnGR2V0,2816
10
11
  bom/forms.py,sha256=RUwiMKeaQkapBwz6JCi4Kkl4e5DlFAgSEMjytEDzmoE,69302
11
12
  bom/helpers.py,sha256=ONsDM0agG9sKJWMjN4IRNlWx2HNF7T0CXM-ts0GRiAY,15031
12
13
  bom/local_settings.py,sha256=yE4aupIquCWsFms44qoCrRrlIyM3sqpOkiwyj1WLxI8,820
13
- bom/models.py,sha256=nCh2EOJeLL9eJ8CbgfDklBwucVRRsfWDR5cHqS7ZJw4,37088
14
+ bom/models.py,sha256=poZoct60kAULCs8RlqC3MFtOl84l2ZfZzjb_d3t9z2g,36537
14
15
  bom/part_bom.py,sha256=30HYAKAEhtadiM9tk6vgCQnn7gNJeuXbzF5gXvMvKG4,8720
15
16
  bom/settings.py,sha256=jUuy7cKuz9gbZ301L0skMFrnU6qKaBpRUf-rHsTTYJM,8387
16
17
  bom/tests.py,sha256=ZqcTUYVXeWjAqzKAV6hp6SKTU0_IOTwIEboTujl7N_M,69905
@@ -135,6 +136,7 @@ bom/templates/bom/bom-base-menu.html,sha256=PeViadG1PKyljvYfPyBFOKgqWhYHBM0tX5Te
135
136
  bom/templates/bom/bom-base.html,sha256=SvQOyg8ILULjnJKMaYv6Rf4wS8SNXXIjVY2nl5bm7B4,691
136
137
  bom/templates/bom/bom-form-modal.html,sha256=U2EWKjOA41jiScKJHXduJJw_hPNGHtYfdiT3yUuwzRY,1389
137
138
  bom/templates/bom/bom-form.html,sha256=1oJetm0fJgPAY1zJ2RNv_Pv9MAja2rjz9pO2bR_UA28,1017
139
+ bom/templates/bom/bom-modal-add-users.html,sha256=1lLgj9acgEkYR3AH177D3xj6meWVmq5MwLDyCg5IV5s,2409
138
140
  bom/templates/bom/bom-signup.html,sha256=y0z_kQKCDb91ZdexCmUX1NRKH35l-I_N5f2SgO3G2gI,184
139
141
  bom/templates/bom/create-part.html,sha256=UjHx6rkPJl-BRt9gKs5zB3s5Y4vrImDTuD25lvABzAI,3610
140
142
  bom/templates/bom/dashboard-menu.html,sha256=4MnFSw0x4w7R0CsSwEDFP3FuZVBFxVChIHr58l3rpSk,799
@@ -157,7 +159,7 @@ bom/templates/bom/part-revision-release.html,sha256=voG7wmYc1Cm3e_H1IasvQcPuyqnn
157
159
  bom/templates/bom/search-help.html,sha256=Wh_tXBJtz0bznk0F1C7OSdRhMe2qpOs9NMCBb2i0CFI,4398
158
160
  bom/templates/bom/seller-info.html,sha256=MACsHMYQXMWfRslXuvh9hD2z28VXzVi0DSy4yg7WQMk,3595
159
161
  bom/templates/bom/sellers.html,sha256=6ut7LwRMGUKYB4BRjiSpDBP9BGgqT7nxpNQpUVWDvkw,5412
160
- bom/templates/bom/settings.html,sha256=atWF7AMoQaeFse3FbDPXwFRPYqZR0cYA-DciFus3ge4,26832
162
+ bom/templates/bom/settings.html,sha256=tS5PCRQLgWASP40qYsP5D4TZBBlC62KNGmymu1iGH7M,25391
161
163
  bom/templates/bom/signup.html,sha256=tB_x7q3IufSNXsd9Dfh8fdWpkiWSGH2_Zgw749B1PaU,884
162
164
  bom/templates/bom/table_of_contents.html,sha256=7wXWOfmVkk5Itjax5x1PE-g5QjxqmYBr7RW8NgtGRng,1763
163
165
  bom/templates/bom/upload-bom.html,sha256=qGlI9HoUNe9H2m5T5TqKWGphaNupz3Y0020h_7GebsU,5230
@@ -175,9 +177,9 @@ bom/third_party_apis/mouser.py,sha256=q2-p0k2n-LNel_QRlfak0kAXT-9hh59k_Pt51PTG09
175
177
  bom/third_party_apis/test_apis.py,sha256=2W0jtTisGTmktC7l556pn9-pZYseTQmmQfo6_4uP4Dc,679
176
178
  bom/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
177
179
  bom/views/json_views.py,sha256=CaDMxHGnp2182ZV9QZfNkgM7tc_rNmokkelav9rF2dE,2462
178
- bom/views/views.py,sha256=Fi3shpOhGaK-RNubngqcyXGzb74x4iwqDexPxMES6tE,71666
179
- django_bom-1.237.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
180
- django_bom-1.237.dist-info/METADATA,sha256=8o4lyQfOHBCegPHAs10f49BEnW-UXiO-YDJGgVVHi2w,7558
181
- django_bom-1.237.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
182
- django_bom-1.237.dist-info/top_level.txt,sha256=6zytg4lnnobI96dO-ZEadPOCslrrFmf4t2Pnv-y8x0Y,4
183
- django_bom-1.237.dist-info/RECORD,,
180
+ bom/views/views.py,sha256=hqQllo34l9CHrGig-uq44F7vf-vnceqfvC7OlX8K5Og,72110
181
+ django_bom-1.238.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
182
+ django_bom-1.238.dist-info/METADATA,sha256=JU7OY9kGkqHdFq0cMu-5aiht9UHwh3-NPWruRtQ8MNA,7558
183
+ django_bom-1.238.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
184
+ django_bom-1.238.dist-info/top_level.txt,sha256=6zytg4lnnobI96dO-ZEadPOCslrrFmf4t2Pnv-y8x0Y,4
185
+ django_bom-1.238.dist-info/RECORD,,