django-bom 1.240__py3-none-any.whl → 1.252__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/admin.py CHANGED
@@ -6,29 +6,27 @@ from .models import (
6
6
  Assembly,
7
7
  Manufacturer,
8
8
  ManufacturerPart,
9
- Organization,
10
9
  Part,
11
10
  PartClass,
12
11
  PartRevision,
13
12
  Seller,
14
13
  SellerPart,
15
14
  Subpart,
16
- UserMeta,
15
+ get_organization_model,
16
+ get_user_meta_model
17
17
  )
18
18
 
19
-
20
19
  User = get_user_model()
20
+ UserMeta = get_user_meta_model()
21
+ Organization = get_organization_model()
21
22
 
22
23
  class UserMetaInline(admin.TabularInline):
23
24
  model = UserMeta
25
+ verbose_name = 'BOM User Meta'
24
26
  raw_id_fields = ('organization',)
25
27
  can_delete = False
26
28
 
27
29
 
28
- class UserAdmin(UserAdmin):
29
- inlines = (UserMetaInline,)
30
-
31
-
32
30
  class OrganizationAdmin(admin.ModelAdmin):
33
31
  list_display = ('name',)
34
32
 
@@ -142,13 +140,21 @@ class AssemblyAdmin(admin.ModelAdmin):
142
140
  ]
143
141
 
144
142
 
145
- # Try to unregister User model
146
- try:
147
- admin.site.unregister(User)
148
- except admin.sites.NotRegistered:
149
- pass
143
+ current_admin = admin.site._registry.get(User)
144
+
145
+ if current_admin:
146
+ admin_class = current_admin.__class__
147
+ inlines = list(admin_class.inlines or [])
148
+ if UserMetaInline not in inlines:
149
+ inlines.append(UserMetaInline)
150
+ admin_class.inlines = inlines
151
+ else:
152
+ class BomUserAdmin(UserAdmin):
153
+ inlines = [UserMetaInline]
154
+
155
+
156
+ admin.site.register(User, BomUserAdmin)
150
157
 
151
- admin.site.register(User, UserAdmin)
152
158
  admin.site.register(Organization, OrganizationAdmin)
153
159
  admin.site.register(Seller, SellerAdmin)
154
160
  admin.site.register(SellerPart, SellerPartAdmin)
bom/auth_backends.py CHANGED
@@ -1,7 +1,9 @@
1
1
  from typing import Optional
2
2
 
3
3
  from . import constants
4
- from .models import Organization
4
+ from .models import get_organization_model
5
+
6
+ Organization = get_organization_model()
5
7
 
6
8
 
7
9
  class OrganizationPermissionBackend:
bom/forms.py CHANGED
@@ -1,41 +1,27 @@
1
1
  import codecs
2
2
  import csv
3
3
  import logging
4
- from typing import Type, TypeVar
5
4
 
6
5
  from django import forms
7
6
  from django.contrib.auth.forms import UserCreationForm
8
7
  from django.core.exceptions import ValidationError
9
- from django.core.validators import MaxLengthValidator, MaxValueValidator, MinLengthValidator, MinValueValidator
8
+ from django.core.validators import MaxLengthValidator, MinLengthValidator
10
9
  from django.db import IntegrityError
11
10
  from django.forms.models import model_to_dict
12
11
  from django.utils.translation import gettext_lazy as _
13
-
14
12
  from djmoney.money import Money
15
13
 
16
14
  from .constants import (
17
- CONFIGURATION_TYPES,
18
15
  CURRENT_UNITS,
19
16
  DISTANCE_UNITS,
20
17
  FREQUENCY_UNITS,
21
18
  INTERFACE_TYPES,
22
19
  MEMORY_UNITS,
23
- NUMBER_CLASS_CODE_LEN_DEFAULT,
24
- NUMBER_CLASS_CODE_LEN_MAX,
25
- NUMBER_CLASS_CODE_LEN_MIN,
26
- NUMBER_ITEM_LEN_DEFAULT,
27
- NUMBER_ITEM_LEN_MAX,
28
- NUMBER_ITEM_LEN_MIN,
29
20
  NUMBER_SCHEME_INTELLIGENT,
30
21
  NUMBER_SCHEME_SEMI_INTELLIGENT,
31
- NUMBER_VARIATION_LEN_DEFAULT,
32
- NUMBER_VARIATION_LEN_MAX,
33
- NUMBER_VARIATION_LEN_MIN,
34
22
  PACKAGE_TYPES,
35
23
  POWER_UNITS,
36
24
  ROLE_TYPE_VIEWER,
37
- ROLE_TYPES,
38
- SUBSCRIPTION_TYPES,
39
25
  TEMPERATURE_UNITS,
40
26
  VALUE_UNITS,
41
27
  VOLTAGE_UNITS,
@@ -43,11 +29,9 @@ from .constants import (
43
29
  WEIGHT_UNITS,
44
30
  )
45
31
  from .csv_headers import (
46
- BOMFlatCSVHeaders,
47
32
  BOMIndentedCSVHeaders,
48
33
  CSVHeaderError,
49
34
  PartClassesCSVHeaders,
50
- PartsListCSVHeaders,
51
35
  )
52
36
  from .form_fields import AutocompleteTextInput
53
37
  from .models import (
@@ -55,7 +39,6 @@ from .models import (
55
39
  AssemblySubparts,
56
40
  Manufacturer,
57
41
  ManufacturerPart,
58
- Organization,
59
42
  Part,
60
43
  PartClass,
61
44
  PartRevision,
@@ -63,20 +46,18 @@ from .models import (
63
46
  SellerPart,
64
47
  Subpart,
65
48
  User,
66
- UserMeta,
49
+ get_user_meta_model,
50
+ get_organization_model,
67
51
  )
68
52
  from .utils import (
69
- check_references_for_duplicates,
70
- get_from_dict,
71
53
  listify_string,
72
- prep_for_sorting_nicely,
73
54
  stringify_list,
74
55
  )
75
- from .validators import alphanumeric, decimal, numeric
76
-
56
+ from .validators import alphanumeric
77
57
 
78
58
  logger = logging.getLogger(__name__)
79
-
59
+ Organization = get_organization_model()
60
+ UserMeta = get_user_meta_model()
80
61
 
81
62
  class UserModelChoiceField(forms.ModelChoiceField):
82
63
  def label_from_instance(self, user):
@@ -0,0 +1,17 @@
1
+ # Generated by Django 5.2.8 on 2025-12-16 18:43
2
+
3
+ from django.db import migrations
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('bom', '0049_alter_assembly_id_alter_assemblysubparts_id_and_more'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterModelOptions(
14
+ name='organization',
15
+ options={'permissions': (('manage_members', 'Can manage organization members'),)},
16
+ ),
17
+ ]
@@ -0,0 +1,41 @@
1
+ # Generated by Django 5.2.8 on 2026-01-04 00:59
2
+
3
+ import django.db.models.deletion
4
+ from django.conf import settings
5
+ from django.db import migrations, models
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ dependencies = [
11
+ ('bom', '0050_alter_organization_options'),
12
+ migrations.swappable_dependency(settings.BOM_ORGANIZATION_MODEL),
13
+ ]
14
+
15
+ operations = [
16
+ migrations.AlterField(
17
+ model_name='manufacturer',
18
+ name='organization',
19
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.BOM_ORGANIZATION_MODEL),
20
+ ),
21
+ migrations.AlterField(
22
+ model_name='part',
23
+ name='organization',
24
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.BOM_ORGANIZATION_MODEL),
25
+ ),
26
+ migrations.AlterField(
27
+ model_name='partclass',
28
+ name='organization',
29
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.BOM_ORGANIZATION_MODEL),
30
+ ),
31
+ migrations.AlterField(
32
+ model_name='seller',
33
+ name='organization',
34
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.BOM_ORGANIZATION_MODEL),
35
+ ),
36
+ migrations.AlterField(
37
+ model_name='usermeta',
38
+ name='organization',
39
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.BOM_ORGANIZATION_MODEL),
40
+ ),
41
+ ]
bom/models.py CHANGED
@@ -2,6 +2,7 @@ from __future__ import unicode_literals
2
2
 
3
3
  import logging
4
4
 
5
+ from django.apps import apps
5
6
  from django.conf import settings
6
7
  from django.contrib.auth import get_user_model
7
8
  from django.core.cache import cache
@@ -23,32 +24,54 @@ logger = logging.getLogger(__name__)
23
24
  User = get_user_model()
24
25
 
25
26
 
27
+ def get_user_meta_model():
28
+ from django.apps import apps
29
+ from django.conf import settings
30
+ return apps.get_model(settings.BOM_USER_META_MODEL)
31
+
32
+
33
+ def get_organization_model():
34
+ from django.apps import apps
35
+ from django.conf import settings
36
+ return apps.get_model(settings.BOM_ORGANIZATION_MODEL)
37
+
38
+
39
+ def _user_meta(self, organization=None):
40
+ from django.apps import apps
41
+ from django.conf import settings
42
+ UserMetaModel = apps.get_model(settings.BOM_USER_META_MODEL)
43
+ meta, created = UserMetaModel.objects.get_or_create(
44
+ user=self,
45
+ defaults={'organization': organization}
46
+ )
47
+ return meta
48
+
49
+
26
50
  class OrganizationScopedModel(models.Model):
27
- organization = models.ForeignKey('Organization', on_delete=models.CASCADE, db_index=True)
51
+ organization = models.ForeignKey(settings.BOM_ORGANIZATION_MODEL, on_delete=models.CASCADE, db_index=True)
28
52
 
29
53
  class Meta:
30
54
  abstract = True
31
55
 
32
56
 
33
- class Organization(models.Model):
57
+ class AbstractOrganization(models.Model):
34
58
  name = models.CharField(max_length=255, default=None)
35
- subscription = models.CharField(max_length=1, choices=SUBSCRIPTION_TYPES)
36
- subscription_quantity = models.IntegerField(default=0)
37
59
  owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
38
60
  number_scheme = models.CharField(max_length=1, choices=NUMBER_SCHEMES, default=NUMBER_SCHEME_SEMI_INTELLIGENT)
39
61
  number_class_code_len = models.PositiveIntegerField(default=NUMBER_CLASS_CODE_LEN_DEFAULT,
40
- validators=[MinValueValidator(NUMBER_CLASS_CODE_LEN_MIN), MaxValueValidator(NUMBER_CLASS_CODE_LEN_MAX)])
62
+ validators=[MinValueValidator(NUMBER_CLASS_CODE_LEN_MIN),
63
+ MaxValueValidator(NUMBER_CLASS_CODE_LEN_MAX)])
41
64
  number_item_len = models.PositiveIntegerField(default=NUMBER_ITEM_LEN_DEFAULT,
42
- validators=[MinValueValidator(NUMBER_ITEM_LEN_MIN), MaxValueValidator(NUMBER_ITEM_LEN_MAX)])
65
+ validators=[MinValueValidator(NUMBER_ITEM_LEN_MIN),
66
+ MaxValueValidator(NUMBER_ITEM_LEN_MAX)])
43
67
  number_variation_len = models.PositiveIntegerField(default=NUMBER_VARIATION_LEN_DEFAULT,
44
- validators=[MinValueValidator(NUMBER_VARIATION_LEN_MIN), MaxValueValidator(NUMBER_VARIATION_LEN_MAX)])
68
+ validators=[MinValueValidator(NUMBER_VARIATION_LEN_MIN),
69
+ MaxValueValidator(NUMBER_VARIATION_LEN_MAX)])
45
70
  google_drive_parent = models.CharField(max_length=128, blank=True, default=None, null=True)
46
71
  currency = CurrencyField(max_length=3, choices=CURRENCY_CHOICES, default='USD')
47
72
 
48
- class Meta:
49
- permissions = (
50
- ("manage_members", "Can manage organization members"),
51
- )
73
+ subscription = models.CharField(max_length=1, choices=SUBSCRIPTION_TYPES)
74
+ subscription_quantity = models.IntegerField(default=0)
52
75
 
53
76
  def number_cs(self):
54
77
  return "C" * self.number_class_code_len
@@ -76,13 +99,25 @@ class Organization(models.Model):
76
99
  return self.owner.email
77
100
 
78
101
  def save(self, *args, **kwargs):
79
- super(Organization, self).save()
80
- SellerPart.objects.filter(seller__organization=self).update(unit_cost_currency=self.currency, nre_cost_currency=self.currency)
102
+ super(AbstractOrganization, self).save()
103
+ SellerPart.objects.filter(seller__organization=self).update(unit_cost_currency=self.currency,
104
+ nre_cost_currency=self.currency)
81
105
 
106
+ class Meta:
107
+ abstract = True
108
+
109
+
110
+ class Organization(AbstractOrganization):
111
+ class Meta:
112
+ swappable = 'BOM_ORGANIZATION_MODEL'
113
+ permissions = (
114
+ ("manage_members", "Can manage organization members"),
115
+ )
82
116
 
83
- class UserMeta(models.Model):
117
+
118
+ class AbstractUserMeta(models.Model):
84
119
  user = models.OneToOneField(settings.AUTH_USER_MODEL, db_index=True, on_delete=models.CASCADE)
85
- organization = models.ForeignKey(Organization, blank=True, null=True, on_delete=models.CASCADE)
120
+ organization = models.ForeignKey(settings.BOM_ORGANIZATION_MODEL, blank=True, null=True, on_delete=models.CASCADE)
86
121
  role = models.CharField(max_length=1, choices=ROLE_TYPES)
87
122
 
88
123
  def get_or_create_organization(self):
@@ -92,7 +127,9 @@ class UserMeta(models.Model):
92
127
  else:
93
128
  org_name = self.user.first_name + ' ' + self.user.last_name
94
129
 
95
- organization, created = Organization.objects.get_or_create(owner=self.user, defaults={'name': org_name, 'subscription': 'F'})
130
+ OrganizationModel = apps.get_model(settings.BOM_ORGANIZATION_MODEL)
131
+ organization, created = OrganizationModel.objects.get_or_create(owner=self.user, defaults={'name': org_name,
132
+ 'subscription': 'F'})
96
133
 
97
134
  self.organization = organization
98
135
  self.role = 'A'
@@ -109,10 +146,13 @@ class UserMeta(models.Model):
109
146
  def is_organization_owner(self) -> bool:
110
147
  return self.organization.owner == self.user if self.organization else False
111
148
 
112
- def _user_meta(self, organization=None):
113
- return UserMeta.objects.get_or_create(user=self, defaults={'organization': organization})[0]
149
+ class Meta:
150
+ abstract = True
114
151
 
115
- User.add_to_class('bom_profile', _user_meta)
152
+
153
+ class UserMeta(AbstractUserMeta):
154
+ class Meta:
155
+ swappable = 'BOM_USER_META_MODEL'
116
156
 
117
157
 
118
158
  class PartClass(OrganizationScopedModel):
@@ -730,3 +770,6 @@ class SellerPart(models.Model, AsDictModel):
730
770
 
731
771
  def __str__(self):
732
772
  return u'%s' % (self.manufacturer_part.part.full_part_number() + ' ' + self.seller.name)
773
+
774
+
775
+ User.add_to_class('bom_profile', _user_meta)
bom/settings.py CHANGED
@@ -1,16 +1,12 @@
1
- import os
2
1
  import logging
2
+ import os
3
3
  from pathlib import Path
4
- from django.utils.log import DEFAULT_LOGGING
5
4
 
6
- # --------------------------------------------------------------------------
7
- # BASE CONFIGURATION & LOCAL SETTINGS
8
- # --------------------------------------------------------------------------
5
+ from django.utils.log import DEFAULT_LOGGING
9
6
 
10
7
  logger = logging.getLogger(__name__)
11
8
  BASE_DIR = Path(__file__).resolve().parent.parent
12
9
 
13
- # Attempt to load local settings (similar pattern to your original, but clean)
14
10
  try:
15
11
  from .local_settings import *
16
12
  except ImportError:
@@ -27,6 +23,8 @@ BOM_CONFIG_DEFAULT = {
27
23
  'page_size': 50,
28
24
  }
29
25
  }
26
+ BOM_ORGANIZATION_MODEL = 'bom.Organization'
27
+ BOM_USER_META_MODEL = 'bom.UserMeta'
30
28
 
31
29
  # Apply custom settings over defaults
32
30
  bom_config_new = BOM_CONFIG_DEFAULT.copy()
@@ -40,9 +40,12 @@
40
40
 
41
41
  <div id="join-organization" style="display: none;">
42
42
  <h3 class="center">Join an existing Organization</h3>
43
- <h5 class="center" style="padding-top: 36px;">To join an existing organization, you must provide the e-mail address you signed up with to your IndaBOM organization owner. <br><br>Your e-mail address is {{ user.email }}, click
44
- <a href="mailto:?subject=Add me to your IndaBOM Organization&body=Hi,%0D%0A%0D%0APlease add me to your IndaBOM organization. To do so, log in, go to Settings > Organization and add me via my e-mail address {{ user.email }}">here</a>
45
- to send an email.</h5>
43
+ <h5 class="center" style="padding-top: 36px;">To join an existing organization, you must provide your
44
+ username (not e-mail) to your IndaBOM organization owner. <br><br>Your username is
45
+ <b>{{ user.username }}</b>
46
+ click
47
+ <a href="mailto:?subject=Add me to your IndaBOM Organization&body=Hi,%0D%0A%0D%0APlease add me to your IndaBOM organization. To do so, log in, go to Settings > Organization and add me via my username {{ user.username }}">here</a>
48
+ to send an e-mail to your organization owner with instructions.</h5>
46
49
  <br><br>
47
50
  <p class="center">Oops! I meant to <a class="modal-trigger" onclick="resetView()">create a new organization</a>.</p>
48
51
  </div>
@@ -7,7 +7,7 @@
7
7
 
8
8
  {% block content %}
9
9
  <div class="row container-app">
10
- <div class="col s12">
10
+ <div class="col s12 l8 offset-l2">
11
11
  <ul id="tabs" class="tabs tabs-fixed-width">
12
12
  <li class="tab"><a id="user-tab" href="#user">User</a></li>
13
13
  <li class="tab"><a id="indabom-tab" href="#indabom">IndaBOM</a></li>
@@ -15,7 +15,7 @@
15
15
  </ul>
16
16
  </div>
17
17
 
18
- <div id="user" class="col s12">
18
+ <div id="user" class="col l8 offset-l2 s12">
19
19
  <div class="section">
20
20
  <h4 class="section-title"><i class="material-icons teal-text text-darken-1">person</i>User</h4>
21
21
  <form name="seller" action="{% url 'bom:settings' tab_anchor=USER_TAB %}" method="post">
@@ -30,9 +30,9 @@
30
30
  </div>
31
31
  </div>
32
32
  <div class="row">
33
- {{ user_form.first_name|materializecss:'s12 m4' }}
34
- {{ user_form.last_name|materializecss:'s12 m4' }}
35
- {{ user_form.email|materializecss:'s12 m4' }}
33
+ {{ user_form.first_name|materializecss:'s12 l6' }}
34
+ {{ user_form.last_name|materializecss:'s12 l6' }}
35
+ {{ user_form.email|materializecss:'s12' }}
36
36
  </div>
37
37
  <div class="row">
38
38
  <div class="col s12 right-align">
@@ -57,18 +57,17 @@
57
57
  </div>
58
58
  </div>
59
59
 
60
- <div id="indabom" class="col s12">
60
+ <div id="indabom" class="col s12 l8 offset-l2">
61
61
  {% if profile.role == 'A' %}
62
62
  {% if organization.number_scheme == 'S' %}
63
63
  <div class="section">
64
64
  <h4 class="section-title"><i class="material-icons teal-text text-darken-1">category</i>Part
65
65
  Classes</h4>
66
- <p><a href="{% url 'bom:help' %}#part-numbering" target="_blank">What is a part class?</a></p>
67
66
  <form name="seller" action="{% url 'bom:settings' tab_anchor=INDABOM_TAB %}" method="post" enctype="multipart/form-data">
68
67
  {% csrf_token %}
69
68
  {% if part_classes.count > 0 %}
70
69
  <div class="row" style="margin-bottom: 0;">
71
- <div class="input-field col s8 m4">
70
+ <div class="input-field col s8 l4">
72
71
  <select name="part-class-action">
73
72
  <option value="" disabled selected>Choose your action</option>
74
73
  <option value="submit-part-class-enable-mouser">Enable Mouser</option>
@@ -77,13 +76,13 @@
77
76
  </select>
78
77
  <label>Action</label>
79
78
  </div>
80
- <div class="col s2 m2">
79
+ <div class="col s2 l2">
81
80
  <div class="input-field">
82
81
  <button class="waves-effect waves-light btn btn-primary" type="submit">Go
83
82
  </button>
84
83
  </div>
85
84
  </div>
86
- <div class="col s2 m6 right-align">
85
+ <div class="col s2 l6 right-align">
87
86
  <div class="input-field">
88
87
  <button class="waves-effect btn-flat btn-icon-round tooltipped"
89
88
  type="submit" name="submit-part-class-export"
@@ -130,18 +129,15 @@
130
129
  </tbody>
131
130
  </table>
132
131
  {% else %}
133
- <p>No part classes have been defined yet.</p>
132
+ <p>No part classes have been defined yet. <a href="{% url 'bom:help' %}#part-numbering"
133
+ target="_blank">What is a part class?</a>
134
+ </p>
135
+ <p>To get started, add your first part class, or upload some here. To help, here is <a
136
+ href="{% static 'bom/doc/sample_part_classes.csv' %}">a sample
137
+ CSV file</a>.</p>
134
138
  {% endif %}
135
139
  </form>
136
- <div class="right-align" style="margin-top: 8px;">
137
- {% if part_classes.count == 0 %}
138
- <div class="left-align" style="margin-bottom: 8px;">
139
- <a href="{% url 'bom:help' %}#part-numbering" target="_blank">What is a part
140
- class?</a>
141
- <p>You may also use <a href="{% static 'bom/doc/sample_part_classes.csv' %}">this
142
- sample CSV file</a>.</p>
143
- </div>
144
- {% endif %}
140
+ <div class="right-align" style="margin-top: 16px;">
145
141
  {% include 'bom/bom-form-modal.html' with modal_title='Upload Part Classes' form=part_class_csv_form action=part_class_form_action name='submit-part-class-upload' modal_description='To batch add part classes, upload a csv that contains columns with the headers<b>`name`</b> and <b>`code`</b>. You may optionally specify a description or comment by including a column with the header <b>`description`</b> or <b>`comment`</b>.' %}
146
142
  {% include 'bom/bom-form-modal.html' with modal_title='Add Part Class' form=part_class_form action=part_class_form_action name='submit-part-class-create' %}
147
143
  </div>
@@ -161,8 +157,8 @@
161
157
  <b>{{ organization.number_cs }}-{{ organization.number_ns }}
162
158
  {% if organization.number_vs %}-{{ organization.number_vs }}{% endif %}</b></p>
163
159
  <div class="row">
164
- {{ organization_number_len_form|materializecss:'s4 m2' }}
165
- <div class="col s12 m6 input-field">
160
+ {{ organization_number_len_form|materializecss:'s4 l2' }}
161
+ <div class="col s12 l6 input-field right-align">
166
162
  <button class="waves-effect waves-light btn btn-primary" type="submit"
167
163
  name="submit-number-item-len"
168
164
  onclick="return confirm('Are you sure you want to change the number of digits?')">
@@ -223,7 +219,7 @@
223
219
  {% endif %}
224
220
  </div>
225
221
 
226
- <div id="organization" class="col s12">
222
+ <div id="organization" class="col s12 l8 offset-l2">
227
223
  {% if user.bom_profile.role == 'A' %}
228
224
  <div class="section">
229
225
  <h4 class="section-title"><i class="material-icons teal-text text-darken-1">business</i>Organization
@@ -232,7 +228,7 @@
232
228
  enctype="multipart/form-data">
233
229
  {% csrf_token %}
234
230
  <div class="row">
235
- {{ organization_form|materializecss:'s12 m4' }}
231
+ {{ organization_form|materializecss:'s12' }}
236
232
  </div>
237
233
  <div class="row">
238
234
  <div class="col s12 right-align">
@@ -246,6 +242,9 @@
246
242
  </form>
247
243
  </div>
248
244
 
245
+ {# Subscription & Billing placed after Users for a more natural flow #}
246
+ {% include 'bom/subscription_panel.html' %}
247
+
249
248
  <div class="section">
250
249
  <h4 class="section-title"><i class="material-icons teal-text text-darken-1">group</i>Users</h4>
251
250
  <form name="seller" action="{% url 'bom:settings' tab_anchor=ORGANIZATION_TAB %}" method="post"
@@ -305,60 +304,67 @@
305
304
  </form>
306
305
  </div>
307
306
 
308
- {# Subscription & Billing placed after Users for a more natural flow #}
309
- {% include 'bom/subscription_panel.html' %}
310
-
311
307
  <div class="section">
312
308
  <h4 class="section-title"><i
313
309
  class="material-icons teal-text text-darken-1">integration_instructions</i>Integrations</h4>
314
- </div>
315
-
316
- <div class="section">
317
- <h5 class="section-title"><img title="Via Google Drive"
318
- src="{% static 'bom/img/google_drive_logo.svg' %}">Part File Storage
319
- with Google Drive</h5>
320
- {% if not google_authentication %}
321
- <p>Connect your Google account to access Google Drive features.
322
- {% if not organization.google_drive_parent %}Organization owners can enable file storage
323
- using Google Drive. {% if organization.owner == user %}Since you are the owner, you are
324
- able to enable file storage!{% else %}Contact your organization owner to enable.
325
- {% endif %}{% endif %}</p>
326
- <p>When you connect, we will create a folder called <b>IndaBOM Part Files</b> in your root of
327
- Google Drive (and it can be moved anywhere in your drive). To add files to a part, navigate
328
- to the part in IndaBOM, and on the part's <b>Specifications</b> tab, click the <img
329
- title="Via Google Drive" style="width: 16px; vertical-align: middle;"
330
- src="{% static 'bom/img/google_drive_logo.svg' %}"> Google Drive link. This will
331
- create a folder for your part in your root IndaBOM directory, or take you there if it
332
- already exists.</p>
333
- <p>You'll be able to access the files directly through Google Drive, and through IndaBOM.</p>
334
- {% else %}
335
- <p>You're connected with Google and can access Google Drive features.</p>
336
- {% endif %}
337
- <div>
338
- {% if google_authentication %}
339
- <p>Logged in to Google as: {{ google_authentication.uid }}</p>
340
- <form action="{% url 'social:disconnect' 'google-oauth2' %}" method="post">
341
- {% csrf_token %}
342
- <button class="waves-effect waves-light btn btn-primary" type="submit">Disconnect
343
- </button>
344
- </form>
310
+ <div class="section">
311
+ <h5 class="section-title"><img title="Via Google Drive"
312
+ src="{% static 'bom/img/google_drive_logo.svg' %}">Part File
313
+ Storage
314
+ with Google Drive</h5>
315
+ {% if not google_authentication %}
316
+ <p>Connect your Google account to access Google Drive features.
317
+ {% if not organization.google_drive_parent %}Organization owners can enable file storage
318
+ using Google Drive. {% if organization.owner == user %}Since you are the owner, you
319
+ are
320
+ able to enable file storage!{% else %}Contact your organization owner to enable.
321
+ {% endif %}{% endif %}</p>
322
+ <p>When you connect, we will create a folder called <b>IndaBOM Part Files</b> in your root
323
+ of
324
+ Google Drive (and it can be moved anywhere in your drive). To add files to a part,
325
+ navigate
326
+ to the part in IndaBOM, and on the part's <b>Specifications</b> tab, click the <img
327
+ title="Via Google Drive" style="width: 16px; vertical-align: middle;"
328
+ src="{% static 'bom/img/google_drive_logo.svg' %}"> Google Drive link. This will
329
+ create a folder for your part in your root IndaBOM directory, or take you there if it
330
+ already exists.</p>
331
+ <p>You'll be able to access the files directly through Google Drive, and through
332
+ IndaBOM.</p>
345
333
  {% else %}
346
- <p>To get started, sign in with Google:</p>
347
- <a href="{% url "social:begin" "google-oauth2" %}">
348
- <img title="Google sign-in."
349
- src="{% static 'bom/img/google/web/1x/btn_google_signin_dark_normal_web.png' %}">
350
- </a>
334
+ <p>You're connected with Google and can access Google Drive features.</p>
351
335
  {% endif %}
336
+ <div>
337
+ {% if google_authentication %}
338
+ <p>Logged in to Google as: {{ google_authentication.uid }}</p>
339
+ <div class="right-align">
340
+ <form action="{% url 'social:disconnect' 'google-oauth2' %}" method="post">
341
+ {% csrf_token %}
342
+ <button class="waves-effect waves-light btn btn-primary" type="submit">
343
+ Disconnect
344
+ </button>
345
+ </form>
346
+ </div>
347
+ {% else %}
348
+ <p>To get started, sign in with Google:</p>
349
+ <div class="right-align">
350
+ <a href="{% url "social:begin" "google-oauth2" %}">
351
+ <img title="Google sign-in."
352
+ src="{% static 'bom/img/google/web/1x/btn_google_signin_dark_normal_web.png' %}">
353
+ </a>
354
+ </div>
355
+ {% endif %}
356
+ </div>
357
+ </div>
358
+ <div class="section">
359
+ <h5 class="section-title"><img title="Sourcing via Mouser.com"
360
+ src="{% static 'bom/img/mouser.png' %}">Automagic Sourcing via
361
+ Mouser
362
+ </h5>
363
+ <!--<p>No connection required. To enable sourcing via Mouser, select which part classes you'd like
364
+ enabled on the Settings IndaBOM tab. Once enabled, sourcing information will appear on part
365
+ detail pages in which there are parts sourced via Mouser.</p>-->
366
+ <p>Currently under construction.</p>
352
367
  </div>
353
- </div>
354
- <div class="section">
355
- <h5 class="section-title"><img title="Sourcing via Mouser.com"
356
- src="{% static 'bom/img/mouser.png' %}">Automagic Sourcing via Mouser
357
- </h5>
358
- <!--<p>No connection required. To enable sourcing via Mouser, select which part classes you'd like
359
- enabled on the Settings IndaBOM tab. Once enabled, sourcing information will appear on part
360
- detail pages in which there are parts sourced via Mouser.</p>-->
361
- <p>Currently under construction.</p>
362
368
  </div>
363
369
  {% endif %}
364
370
 
bom/views/json_views.py CHANGED
@@ -5,9 +5,9 @@ from django.shortcuts import get_object_or_404
5
5
  from django.utils.decorators import method_decorator
6
6
  from django.views import View
7
7
 
8
- from bom.models import Part, PartClass, Subpart, SellerPart, Organization, Manufacturer, ManufacturerPart, User, UserMeta, PartRevision, Assembly, AssemblySubparts
9
- from bom.third_party_apis.mouser import Mouser
8
+ from bom.models import PartRevision
10
9
  from bom.third_party_apis.base_api import BaseApiError
10
+ from bom.third_party_apis.mouser import Mouser
11
11
 
12
12
 
13
13
  class BomJsonResponse(View):
bom/views/views.py CHANGED
@@ -69,11 +69,12 @@ from bom.models import (
69
69
  SellerPart,
70
70
  Subpart,
71
71
  User,
72
- UserMeta,
72
+ get_user_meta_model
73
73
  )
74
74
  from bom.utils import check_references_for_duplicates, listify_string, prep_for_sorting_nicely
75
75
 
76
76
  logger = logging.getLogger(__name__)
77
+ UserMeta = get_user_meta_model()
77
78
  BOM_LOGIN_URL = getattr(settings, "BOM_LOGIN_URL", None) or settings.LOGIN_URL
78
79
 
79
80
  def form_error_messages(form_errors) -> [str]:
@@ -357,7 +358,7 @@ def bom_settings(request, tab_anchor=None):
357
358
  part_classes = PartClass.objects.all().filter(organization=organization)
358
359
 
359
360
  users_in_organization = User.objects.filter(
360
- id__in=UserMeta.objects.filter(organization=organization).values_list('user', flat=True)).exclude(id__in=[organization.owner.id]).order_by(
361
+ id__in=UserMeta.objects.filter(organization=organization).values_list('user', flat=True)).order_by(
361
362
  'first_name', 'last_name', 'email')
362
363
  users_in_organization_count = users_in_organization.count()
363
364
  has_member_capacity = users_in_organization_count < organization.subscription_quantity
@@ -400,8 +401,13 @@ def bom_settings(request, tab_anchor=None):
400
401
  added_user_profile = user_add_form.save()
401
402
  messages.info(request, f"Added {added_user_profile.user.first_name} {added_user_profile.user.last_name} to your organization.")
402
403
  else:
403
- messages.error(request, user_add_form.errors)
404
-
404
+ for field, errors in user_add_form.errors.items():
405
+ for error in errors:
406
+ messages.error(request, f"{field.capitalize()}: {error}")
407
+ users_in_organization.all()
408
+ users_in_organization_count = users_in_organization.count()
409
+ has_member_capacity = users_in_organization_count < organization.subscription_quantity
410
+ seats_available = max(organization.subscription_quantity - users_in_organization_count, 0)
405
411
  elif 'clear-add-user' in request.POST:
406
412
  tab_anchor = ORGANIZATION_TAB
407
413
  user_add_form = UserAddForm()
@@ -421,7 +427,10 @@ def bom_settings(request, tab_anchor=None):
421
427
  user_meta.save()
422
428
  except UserMeta.DoesNotExist:
423
429
  messages.error(request, "No user found with given id {}.".format(user_meta_id))
424
-
430
+ users_in_organization.all()
431
+ users_in_organization_count = users_in_organization.count()
432
+ has_member_capacity = users_in_organization_count < organization.subscription_quantity
433
+ seats_available = max(organization.subscription_quantity - users_in_organization_count, 0)
425
434
  elif 'submit-edit-organization' in request.POST:
426
435
  tab_anchor = ORGANIZATION_TAB
427
436
  organization_form = OrganizationFormEditSettings(request.POST, instance=organization, user=user)
@@ -462,8 +471,7 @@ def bom_settings(request, tab_anchor=None):
462
471
  tab_anchor = INDABOM_TAB
463
472
  part_class_csv_form = PartClassCSVForm(request.POST, request.FILES, organization=organization)
464
473
  if part_class_csv_form.is_valid():
465
- for success in part_class_csv_form.successes:
466
- messages.info(request, success)
474
+ messages.info(request, f'Successfully uploaded {len(part_class_csv_form.successes)} part classes.')
467
475
  for warning in part_class_csv_form.warnings:
468
476
  messages.warning(request, warning)
469
477
  else:
@@ -522,6 +530,8 @@ def bom_settings(request, tab_anchor=None):
522
530
  profile.save()
523
531
  if users_in_organization == 0:
524
532
  organization.delete()
533
+ else:
534
+ messages.warning(request, "No action was taken because no form field was submitted.")
525
535
 
526
536
  user_form = UserForm(instance=user)
527
537
  user_add_form = UserAddForm()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-bom
3
- Version: 1.240
3
+ Version: 1.252
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,19 +1,19 @@
1
1
  bom/__init__.py,sha256=HuvSMR9cYQcppTZGD0XjUVUBtHWwWMh1yMQzk_2wTS4,41
2
- bom/admin.py,sha256=4xN38uSIGo54MVoo2MXUbvcfuPSAifDc8g0arrEAW4Q,4093
2
+ bom/admin.py,sha256=3fgD0_e5U76P1UT8CTeWbmqo3KK3PKz8ECz-5a_pXEM,4401
3
3
  bom/apps.py,sha256=TJMUTSX1h2genPwCq6SN6g1fhrrSmjEXGg2zQFa_ryM,147
4
- bom/auth_backends.py,sha256=8ViZfP01fITPQnPvsTe_RuKx-ur9dhSOQE3aOp9ESV8,1429
4
+ bom/auth_backends.py,sha256=Xj3MCUJg3r9M4v1c8wrxC3lwpE5F8_2_HD5s9M_5Rms,1480
5
5
  bom/base_classes.py,sha256=CrWD7wlIkwYb90VoGcVOcw2WoQszRrCje-Re5d_UW1Q,1183
6
6
  bom/constants.py,sha256=5CFE0uvKTL91w24rqdAB6EMgpIyw0HhfmmktxF4gCgs,4296
7
7
  bom/context_processors.py,sha256=OxMVCqxGtRoHR7aJfTs6xANpxJQzBDUIuNTG1f_Xjoo,312
8
8
  bom/csv_headers.py,sha256=1VDJ7aNqYjDhf5_rfWBZqO6wsIS99oD01yXx2nNmIHQ,12620
9
9
  bom/decorators.py,sha256=fCK-lxJTBp3LEdZtKMyQg5hRqxQm5RJny9eb7oyjbtE,1233
10
10
  bom/form_fields.py,sha256=tLqchl0j8izBCZnm82NMyHpQV6EuGgCjCl5lrnGR2V0,2816
11
- bom/forms.py,sha256=RUwiMKeaQkapBwz6JCi4Kkl4e5DlFAgSEMjytEDzmoE,69302
11
+ bom/forms.py,sha256=wmfurA8M0cxc72nLPsMo0-m6JOXzXOhFF2MSRXzZnVI,68836
12
12
  bom/helpers.py,sha256=ONsDM0agG9sKJWMjN4IRNlWx2HNF7T0CXM-ts0GRiAY,15031
13
13
  bom/local_settings.py,sha256=yE4aupIquCWsFms44qoCrRrlIyM3sqpOkiwyj1WLxI8,820
14
- bom/models.py,sha256=poZoct60kAULCs8RlqC3MFtOl84l2ZfZzjb_d3t9z2g,36537
14
+ bom/models.py,sha256=GWnSNEDP5po_fo9CphwtimZTFkSrbjKCs6zg0g-LKD8,37834
15
15
  bom/part_bom.py,sha256=30HYAKAEhtadiM9tk6vgCQnn7gNJeuXbzF5gXvMvKG4,8720
16
- bom/settings.py,sha256=jUuy7cKuz9gbZ301L0skMFrnU6qKaBpRUf-rHsTTYJM,8387
16
+ bom/settings.py,sha256=aBg0PHaK9NvNZNmfnPrU4-kp2GQeeAGMB_EVvMoAmJs,8197
17
17
  bom/tests.py,sha256=ZqcTUYVXeWjAqzKAV6hp6SKTU0_IOTwIEboTujl7N_M,69905
18
18
  bom/urls.py,sha256=sGNKO8BsTO_TDPsqB-c_fqRozaNHOf9WYRaOy-7OLAE,6841
19
19
  bom/utils.py,sha256=z_2jACSkRc0hsc0mdR8tOK10KiSDeM0a6rXIpztPDuA,7302
@@ -68,6 +68,8 @@ bom/migrations/0046_alter_sellerpart_unique_together.py,sha256=KcOwhf5Vh02wDq_Z3
68
68
  bom/migrations/0047_sellerpart_seller_part_number.py,sha256=QdjdWMNlSyLHB7uSTq_8xhPxnYAXR8yht-d_AsMvJBw,446
69
69
  bom/migrations/0048_rename_part_organization_number_class_bom_part_organiz_b333d6_idx_and_more.py,sha256=rZ_mfd_xFu4BglhYxkNuQVqwThZ721kjrlCAn9LkNRo,50969
70
70
  bom/migrations/0049_alter_assembly_id_alter_assemblysubparts_id_and_more.py,sha256=l1q5BCNVYWD6Ngf9pGzYq1hQMvvshmdFlm33On63YNc,3247
71
+ bom/migrations/0050_alter_organization_options.py,sha256=n-YGAoUdUxYdh5NY0Zpz2T4CWEOR7tDjSFFk-KZD_tw,432
72
+ bom/migrations/0051_alter_manufacturer_organization_and_more.py,sha256=xjnkZhEgsJDsfK9leBPxxQ2oG_Qf2FdrttTx-lldmjw,1539
71
73
  bom/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
72
74
  bom/static/bom/css/dashboard.css,sha256=saLXUpnVRjsV9HNdsmQD4Eq-zBlm8R2YePhvoc1uifk,245
73
75
  bom/static/bom/css/jquery.treetable.css,sha256=H37aGBAAFP3R6v08nui9gKSdLE2VGsGsmlttrIImzfE,652
@@ -149,7 +151,7 @@ bom/templates/bom/help.html,sha256=hi16fwtVqeWP3Vb3vOpLSKx0ioB1MODJ8lkh6bczfiM,8
149
151
  bom/templates/bom/manufacturer-info.html,sha256=jR98qXONqquEeda92djQgmFfmJiC8jbsGgyXuzJY100,3742
150
152
  bom/templates/bom/manufacturers.html,sha256=3ZF8Up-IiAvR6CCmrj_92qPPh7vKrVQaEN05KKX2yrA,5550
151
153
  bom/templates/bom/nothing-to-see.html,sha256=cRspNAHlSfv7KjQwp34gANhVqQbXzfFpqtRSm6NoV9s,657
152
- bom/templates/bom/organization-create.html,sha256=4o3C58CQDF3Jz1cZ1UPoLfX0qNvwMKK3UnFeXGMmPww,7899
154
+ bom/templates/bom/organization-create.html,sha256=yLrEJH8BlD-W1USk8pYAzTvr-Qw9ZM5rBGYO3n6lCu0,7982
153
155
  bom/templates/bom/part-info.html,sha256=lilYygmR8cQhQ5lJeYK6dj-OdIm72s1SerELHqyrPSs,25406
154
156
  bom/templates/bom/part-revision-display.html,sha256=t_wwzf910fhBc2vFjqoISnhX4OEr7pkfh8R-RGq_6ac,5509
155
157
  bom/templates/bom/part-revision-edit.html,sha256=gOWiRd8Vq0z912_fI9UtBC0yYkcF_lruMQfAWN4kqw0,1624
@@ -159,7 +161,7 @@ bom/templates/bom/part-revision-release.html,sha256=voG7wmYc1Cm3e_H1IasvQcPuyqnn
159
161
  bom/templates/bom/search-help.html,sha256=Wh_tXBJtz0bznk0F1C7OSdRhMe2qpOs9NMCBb2i0CFI,4398
160
162
  bom/templates/bom/seller-info.html,sha256=MACsHMYQXMWfRslXuvh9hD2z28VXzVi0DSy4yg7WQMk,3595
161
163
  bom/templates/bom/sellers.html,sha256=6ut7LwRMGUKYB4BRjiSpDBP9BGgqT7nxpNQpUVWDvkw,5412
162
- bom/templates/bom/settings.html,sha256=9uEAG_IpcVgGZT_lGQyKnc83qpoNjoN3qNs_PtBAj0s,27749
164
+ bom/templates/bom/settings.html,sha256=az0QXxrCPEDa8XRG7q3ASDe8dIP3G941qKmijrzwczw,28241
163
165
  bom/templates/bom/signup.html,sha256=tB_x7q3IufSNXsd9Dfh8fdWpkiWSGH2_Zgw749B1PaU,884
164
166
  bom/templates/bom/subscription_panel.html,sha256=Ute49APwiXONQW2z0AApJRaSwnwtsYt3_opn0bW5BX8,843
165
167
  bom/templates/bom/table_of_contents.html,sha256=7wXWOfmVkk5Itjax5x1PE-g5QjxqmYBr7RW8NgtGRng,1763
@@ -177,10 +179,10 @@ bom/third_party_apis/google_drive.py,sha256=8ECE9_8f1KAyNTtkalws-Gl51wxAaTLP40bD
177
179
  bom/third_party_apis/mouser.py,sha256=q2-p0k2n-LNel_QRlfak0kAXT-9hh59k_Pt51PTG09s,5576
178
180
  bom/third_party_apis/test_apis.py,sha256=2W0jtTisGTmktC7l556pn9-pZYseTQmmQfo6_4uP4Dc,679
179
181
  bom/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
180
- bom/views/json_views.py,sha256=CaDMxHGnp2182ZV9QZfNkgM7tc_rNmokkelav9rF2dE,2462
181
- bom/views/views.py,sha256=SvD7Yku9rPwLFmrWC0c15g_a0nC-YkCOnIqEVC32DfU,72267
182
- django_bom-1.240.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
183
- django_bom-1.240.dist-info/METADATA,sha256=yhPn8NcuAiVTvnEPvwxwH6j-N_jI8qDjyELM4Mv-6K4,7558
184
- django_bom-1.240.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
185
- django_bom-1.240.dist-info/top_level.txt,sha256=6zytg4lnnobI96dO-ZEadPOCslrrFmf4t2Pnv-y8x0Y,4
186
- django_bom-1.240.dist-info/RECORD,,
182
+ bom/views/json_views.py,sha256=LK3-njLZrILLqZxCuE-_sUEC2z2GBxQFRysX67-h14c,2334
183
+ bom/views/views.py,sha256=IB0pgdQojvuvykopTdpM42m8EMgy8oYJ4Ui01TR30Ys,73146
184
+ django_bom-1.252.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
185
+ django_bom-1.252.dist-info/METADATA,sha256=JCrxqDW9rFFK-tSg056gcNzbkcVU89iuahZ1tiKCK7o,7558
186
+ django_bom-1.252.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
187
+ django_bom-1.252.dist-info/top_level.txt,sha256=6zytg4lnnobI96dO-ZEadPOCslrrFmf4t2Pnv-y8x0Y,4
188
+ django_bom-1.252.dist-info/RECORD,,