django-bom 1.237__py3-none-any.whl → 1.239__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,6 @@
1
+ {# Placeholder for subscription/billing UI. Host applications (e.g., indabom) can override this template by providing their own `bom/_subscription_panel.html` earlier in the template search path. #}
2
+ <div class="row">
3
+ <div class="col">
4
+ <i>As a self-hosted django-bom app, you can upgrade any organization using the admin dashboard by finding your <b>Organization</b> and changing the <b>Subscription</b> property to <b>Pro</b>.</i>
5
+ </div>
6
+ </div>
@@ -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 %}
@@ -186,6 +186,7 @@
186
186
  <div id="organization" class="col s12">
187
187
  {% if user.bom_profile.role == 'A' %}
188
188
  <h3>Organization</h3>
189
+ {% include 'bom/_subscription_panel.html' %}
189
190
  <div class="row">
190
191
  <div class="col s12">
191
192
  <form name="seller" action="{% url 'bom:settings' tab_anchor=ORGANIZATION_TAB %}" method="post" class="col s12" enctype="multipart/form-data">
@@ -207,7 +208,6 @@
207
208
  <div class="row">
208
209
  <div class="col s12">
209
210
  <h3>Users</h3>
210
- {% if users_in_organization.count > 0 %}
211
211
  <form name="seller" action="{% url 'bom:settings' tab_anchor=ORGANIZATION_TAB %}" method="post" class="col s12" enctype="multipart/form-data">
212
212
  {% csrf_token %}
213
213
  <table>
@@ -237,6 +237,13 @@
237
237
  class="material-icons left">edit</i>Edit</a>
238
238
  </td>
239
239
  </tr>
240
+ {% empty %}
241
+ <tr>
242
+ <td colspan="5" style="font-style: italic;">There are no additional
243
+ users in this
244
+ organization.
245
+ </td>
246
+ </tr>
240
247
  {% endfor %}
241
248
  </tbody>
242
249
  </table>
@@ -247,26 +254,10 @@
247
254
  </button>
248
255
  </div>
249
256
  <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.' %}
257
+ {% 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
258
  </div>
252
259
  </div>
253
260
  </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
261
  </div>
271
262
  </div>
272
263
  <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,12 @@ 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
+ # Seats available for adding new members (never negative)
365
+ seats_available = max(organization.subscription_quantity - users_in_organization_count, 0)
366
+ is_pro = organization.subscription == constants.SUBSCRIPTION_TYPE_PRO
367
+ user_can_manage_members = request.user.has_perm('bom.manage_members', organization)
364
368
  google_authentication = UserSocialAuth.objects.filter(user=user).first()
365
369
 
366
370
  organization_parts_count = Part.objects.filter(organization=organization).count()
@@ -386,8 +390,10 @@ def bom_settings(request, tab_anchor=None):
386
390
 
387
391
  elif 'submit-add-user' in request.POST:
388
392
  tab_anchor = ORGANIZATION_TAB
389
- if organization.subscription == 'F':
390
- messages.error(request, "Error: You must have a paid account to add users.")
393
+ if not is_pro:
394
+ messages.error(request, "You need a Pro subscription to add users.")
395
+ elif not user_can_manage_members:
396
+ messages.error(request, "You are not allowed to manage users, contact your organization admin.")
391
397
  else:
392
398
  user_add_form = UserAddForm(request.POST, organization=organization)
393
399
  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.239
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
@@ -124,6 +125,7 @@ bom/static/bom/js/jquery-3.4.1.min.js,sha256=CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFl
124
125
  bom/static/bom/js/jquery.ba-floatingscrollbar.min.js,sha256=NptA5rS2zjhkcu26WQJtMr34psHYzUYRsLrSziTusgg,1271
125
126
  bom/static/bom/js/jquery.treetable.js,sha256=JEXnh_LuKYxk8CUIT1anQ4es4nfOqC0bds_NjsbMBUI,16611
126
127
  bom/static/bom/js/materialize.min.js,sha256=9aWZlbcIvNSnb4BWaUYlFNGylNeTWUL_yffW_3Dbk_o,181114
128
+ bom/templates/bom/_subscription_panel.html,sha256=KE3f4DJ72c93GkW9O6bJw94ht05wNZxORP7o31qafcg,461
127
129
  bom/templates/bom/account-delete.html,sha256=YjtG5o-sJHuK4Np3vyEliySF-eAwmFn0ROynDdvPMsU,728
128
130
  bom/templates/bom/add-manufacturer-part.html,sha256=gM5_-RfgiuQwfns7s1J006d6ypKMbdG8F_kdLGQrJjA,3242
129
131
  bom/templates/bom/add-sellerpart.html,sha256=W9rUYrfpqw3yx45S86GIpepdmN_8Xrf23urYexgObBw,4279
@@ -135,6 +137,7 @@ bom/templates/bom/bom-base-menu.html,sha256=PeViadG1PKyljvYfPyBFOKgqWhYHBM0tX5Te
135
137
  bom/templates/bom/bom-base.html,sha256=SvQOyg8ILULjnJKMaYv6Rf4wS8SNXXIjVY2nl5bm7B4,691
136
138
  bom/templates/bom/bom-form-modal.html,sha256=U2EWKjOA41jiScKJHXduJJw_hPNGHtYfdiT3yUuwzRY,1389
137
139
  bom/templates/bom/bom-form.html,sha256=1oJetm0fJgPAY1zJ2RNv_Pv9MAja2rjz9pO2bR_UA28,1017
140
+ bom/templates/bom/bom-modal-add-users.html,sha256=1lLgj9acgEkYR3AH177D3xj6meWVmq5MwLDyCg5IV5s,2409
138
141
  bom/templates/bom/bom-signup.html,sha256=y0z_kQKCDb91ZdexCmUX1NRKH35l-I_N5f2SgO3G2gI,184
139
142
  bom/templates/bom/create-part.html,sha256=UjHx6rkPJl-BRt9gKs5zB3s5Y4vrImDTuD25lvABzAI,3610
140
143
  bom/templates/bom/dashboard-menu.html,sha256=4MnFSw0x4w7R0CsSwEDFP3FuZVBFxVChIHr58l3rpSk,799
@@ -157,7 +160,7 @@ bom/templates/bom/part-revision-release.html,sha256=voG7wmYc1Cm3e_H1IasvQcPuyqnn
157
160
  bom/templates/bom/search-help.html,sha256=Wh_tXBJtz0bznk0F1C7OSdRhMe2qpOs9NMCBb2i0CFI,4398
158
161
  bom/templates/bom/seller-info.html,sha256=MACsHMYQXMWfRslXuvh9hD2z28VXzVi0DSy4yg7WQMk,3595
159
162
  bom/templates/bom/sellers.html,sha256=6ut7LwRMGUKYB4BRjiSpDBP9BGgqT7nxpNQpUVWDvkw,5412
160
- bom/templates/bom/settings.html,sha256=atWF7AMoQaeFse3FbDPXwFRPYqZR0cYA-DciFus3ge4,26832
163
+ bom/templates/bom/settings.html,sha256=NNwmlPv2Uk3oaMLm7ESfJ9e9Q7kPFo2y9Q4yfe63O0Y,25452
161
164
  bom/templates/bom/signup.html,sha256=tB_x7q3IufSNXsd9Dfh8fdWpkiWSGH2_Zgw749B1PaU,884
162
165
  bom/templates/bom/table_of_contents.html,sha256=7wXWOfmVkk5Itjax5x1PE-g5QjxqmYBr7RW8NgtGRng,1763
163
166
  bom/templates/bom/upload-bom.html,sha256=qGlI9HoUNe9H2m5T5TqKWGphaNupz3Y0020h_7GebsU,5230
@@ -175,9 +178,9 @@ bom/third_party_apis/mouser.py,sha256=q2-p0k2n-LNel_QRlfak0kAXT-9hh59k_Pt51PTG09
175
178
  bom/third_party_apis/test_apis.py,sha256=2W0jtTisGTmktC7l556pn9-pZYseTQmmQfo6_4uP4Dc,679
176
179
  bom/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
177
180
  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,,
181
+ bom/views/views.py,sha256=SvD7Yku9rPwLFmrWC0c15g_a0nC-YkCOnIqEVC32DfU,72267
182
+ django_bom-1.239.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
183
+ django_bom-1.239.dist-info/METADATA,sha256=x2kwDE99WQthXTTXKswN4EOHZ2LFAhY9abKJyaAu1jE,7558
184
+ django_bom-1.239.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
185
+ django_bom-1.239.dist-info/top_level.txt,sha256=6zytg4lnnobI96dO-ZEadPOCslrrFmf4t2Pnv-y8x0Y,4
186
+ django_bom-1.239.dist-info/RECORD,,