mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-16 20:15:44 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
3
.github/release.yml
vendored
3
.github/release.yml
vendored
@ -1,6 +1,9 @@
|
||||
# .github/release.yml
|
||||
|
||||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- translation
|
||||
categories:
|
||||
- title: Breaking Changes
|
||||
labels:
|
||||
|
@ -7,7 +7,7 @@ exclude: |
|
||||
)$
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.2.0
|
||||
rev: v4.3.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
|
@ -2,11 +2,14 @@
|
||||
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 59
|
||||
INVENTREE_API_VERSION = 60
|
||||
|
||||
"""
|
||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||
|
||||
v60 -> 2022-06-08 : https://github.com/inventree/InvenTree/pull/3148
|
||||
- Add availability data fields to the SupplierPart model
|
||||
|
||||
v59 -> 2022-06-07 : https://github.com/inventree/InvenTree/pull/3154
|
||||
- Adds further improvements to BulkDelete mixin class
|
||||
- Fixes multiple bugs in custom OPTIONS metadata implementation
|
||||
|
24
InvenTree/company/migrations/0044_auto_20220607_2204.py
Normal file
24
InvenTree/company/migrations/0044_auto_20220607_2204.py
Normal file
@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.2.13 on 2022-06-07 22:04
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('company', '0043_manufacturerpartattachment'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='supplierpart',
|
||||
name='availability_updated',
|
||||
field=models.DateTimeField(blank=True, help_text='Date of last update of availability data', null=True, verbose_name='Availability Updated'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='supplierpart',
|
||||
name='available',
|
||||
field=models.DecimalField(decimal_places=3, default=0, help_text='Quantity available from supplier', max_digits=10, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Available'),
|
||||
),
|
||||
]
|
@ -1,6 +1,7 @@
|
||||
"""Company database model definitions."""
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
from django.apps import apps
|
||||
from django.core.exceptions import ValidationError
|
||||
@ -528,6 +529,25 @@ class SupplierPart(models.Model):
|
||||
# TODO - Reimplement lead-time as a charfield with special validation (pattern matching).
|
||||
# lead_time = models.DurationField(blank=True, null=True)
|
||||
|
||||
available = models.DecimalField(
|
||||
max_digits=10, decimal_places=3, default=0,
|
||||
validators=[MinValueValidator(0)],
|
||||
verbose_name=_('Available'),
|
||||
help_text=_('Quantity available from supplier'),
|
||||
)
|
||||
|
||||
availability_updated = models.DateTimeField(
|
||||
null=True, blank=True, verbose_name=_('Availability Updated'),
|
||||
help_text=_('Date of last update of availability data'),
|
||||
)
|
||||
|
||||
def update_available_quantity(self, quantity):
|
||||
"""Update the available quantity for this SupplierPart"""
|
||||
|
||||
self.available = quantity
|
||||
self.availability_updated = datetime.now()
|
||||
self.save()
|
||||
|
||||
@property
|
||||
def manufacturer_string(self):
|
||||
"""Format a MPN string for this SupplierPart.
|
||||
|
@ -209,6 +209,10 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize this serializer with extra detail fields as required"""
|
||||
|
||||
# Check if 'available' quantity was supplied
|
||||
self.has_available_quantity = 'available' in kwargs.get('data', {})
|
||||
|
||||
part_detail = kwargs.pop('part_detail', True)
|
||||
supplier_detail = kwargs.pop('supplier_detail', True)
|
||||
manufacturer_detail = kwargs.pop('manufacturer_detail', True)
|
||||
@ -242,6 +246,8 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
|
||||
|
||||
model = SupplierPart
|
||||
fields = [
|
||||
'available',
|
||||
'availability_updated',
|
||||
'description',
|
||||
'link',
|
||||
'manufacturer',
|
||||
@ -260,11 +266,34 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
|
||||
'supplier_detail',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
'availability_updated',
|
||||
]
|
||||
|
||||
def update(self, supplier_part, data):
|
||||
"""Custom update functionality for the serializer"""
|
||||
|
||||
available = data.pop('available', None)
|
||||
|
||||
response = super().update(supplier_part, data)
|
||||
|
||||
if available is not None and self.has_available_quantity:
|
||||
supplier_part.update_available_quantity(available)
|
||||
|
||||
return response
|
||||
|
||||
def create(self, validated_data):
|
||||
"""Extract manufacturer data and process ManufacturerPart."""
|
||||
|
||||
# Extract 'available' quantity from the serializer
|
||||
available = validated_data.pop('available', None)
|
||||
|
||||
# Create SupplierPart
|
||||
supplier_part = super().create(validated_data)
|
||||
|
||||
if available is not None and self.has_available_quantity:
|
||||
supplier_part.update_available_quantity(available)
|
||||
|
||||
# Get ManufacturerPart raw data (unvalidated)
|
||||
manufacturer = self.initial_data.get('manufacturer', None)
|
||||
MPN = self.initial_data.get('MPN', None)
|
||||
|
@ -30,18 +30,32 @@
|
||||
{% url 'admin:company_supplierpart_change' part.pk as url %}
|
||||
{% include "admin_button.html" with url=url %}
|
||||
{% endif %}
|
||||
{% if roles.purchase_order.add %}
|
||||
<button type='button' class='btn btn-outline-secondary' id='order-part' title='{% trans "Order part" %}'>
|
||||
<span class='fas fa-shopping-cart'></span>
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type='button' class='btn btn-outline-secondary' id='edit-part' title='{% trans "Edit supplier part" %}'>
|
||||
<span class='fas fa-edit icon-green'/>
|
||||
</button>
|
||||
{% if roles.purchase_order.delete %}
|
||||
<button type='button' class='btn btn-outline-secondary' id='delete-part' title='{% trans "Delete supplier part" %}'>
|
||||
<span class='fas fa-trash-alt icon-red'/>
|
||||
</button>
|
||||
{% if roles.purchase_order.change or roles.purchase_order.add or roles.purchase_order.delete %}
|
||||
<div class='btn-group'>
|
||||
<button id='supplier-part-actions' title='{% trans "Supplier part actions" %}' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle='dropdown'>
|
||||
<span class='fas fa-tools'></span> <span class='caret'></span>
|
||||
</button>
|
||||
<ul class='dropdown-menu'>
|
||||
{% if roles.purchase_order.add %}
|
||||
<li><a class='dropdown-item' href='#' id='order-part' title='{% trans "Order Part" %}'>
|
||||
<span class='fas fa-shopping-cart'></span> {% trans "Order Part" %}
|
||||
</a></li>
|
||||
{% endif %}
|
||||
{% if roles.purchase_order.change %}
|
||||
<li><a class='dropdown-item' href='#' id='update-part-availability' title='{% trans "Update Availability" %}'>
|
||||
<span class='fas fa-building'></span> {% trans "Update Availability" %}
|
||||
</a></li>
|
||||
<li><a class='dropdown-item' href='#' id='edit-part' title='{% trans "Edit Supplier Part" %}'>
|
||||
<span class='fas fa-edit icon-green'></span> {% trans "Edit Supplier Part" %}
|
||||
</a></li>
|
||||
{% endif %}
|
||||
{% if roles.purchase_order.delete %}
|
||||
<li><a class='dropdown-item' href='#' id='delete-part' title='{% trans "Delete Supplier Part" %}'>
|
||||
<span class='fas fa-trash-alt icon-red'></span> {% trans "Delete Supplier Part" %}
|
||||
</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock actions %}
|
||||
|
||||
@ -74,6 +88,13 @@ src="{% static 'img/blank_image.png' %}"
|
||||
<td>{{ part.description }}{% include "clip.html"%}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if part.availability_updated %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>{% trans "Available" %}</td>
|
||||
<td>{% decimal part.available %}<span class='badge bg-dark rounded-pill float-right'>{% render_date part.availability_updated %}</span></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
{% endblock details %}
|
||||
@ -351,6 +372,20 @@ $('#order-part, #order-part2').click(function() {
|
||||
);
|
||||
});
|
||||
|
||||
{% if roles.purchase_order.change %}
|
||||
|
||||
$('#update-part-availability').click(function() {
|
||||
editSupplierPart({{ part.pk }}, {
|
||||
fields: {
|
||||
available: {},
|
||||
},
|
||||
title: '{% trans "Update Part Availability" %}',
|
||||
onSuccess: function() {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#edit-part').click(function () {
|
||||
|
||||
editSupplierPart({{ part.pk }}, {
|
||||
@ -360,6 +395,8 @@ $('#edit-part').click(function () {
|
||||
});
|
||||
});
|
||||
|
||||
{% endif %}
|
||||
|
||||
$('#delete-part').click(function() {
|
||||
inventreeGet(
|
||||
'{% url "api-supplier-part-detail" part.pk %}',
|
||||
|
@ -6,7 +6,7 @@ from rest_framework import status
|
||||
|
||||
from InvenTree.api_tester import InvenTreeAPITestCase
|
||||
|
||||
from .models import Company
|
||||
from .models import Company, SupplierPart
|
||||
|
||||
|
||||
class CompanyTest(InvenTreeAPITestCase):
|
||||
@ -146,6 +146,7 @@ class ManufacturerTest(InvenTreeAPITestCase):
|
||||
'location',
|
||||
'company',
|
||||
'manufacturer_part',
|
||||
'supplier_part',
|
||||
]
|
||||
|
||||
roles = [
|
||||
@ -238,3 +239,111 @@ class ManufacturerTest(InvenTreeAPITestCase):
|
||||
url = reverse('api-manufacturer-part-detail', kwargs={'pk': manufacturer_part_id})
|
||||
response = self.get(url)
|
||||
self.assertEqual(response.data['MPN'], 'PART_NUMBER')
|
||||
|
||||
|
||||
class SupplierPartTest(InvenTreeAPITestCase):
|
||||
"""Unit tests for the SupplierPart API endpoints"""
|
||||
|
||||
fixtures = [
|
||||
'category',
|
||||
'part',
|
||||
'location',
|
||||
'company',
|
||||
'manufacturer_part',
|
||||
'supplier_part',
|
||||
]
|
||||
|
||||
roles = [
|
||||
'part.add',
|
||||
'part.change',
|
||||
'part.add',
|
||||
'purchase_order.change',
|
||||
]
|
||||
|
||||
def test_supplier_part_list(self):
|
||||
"""Test the SupplierPart API list functionality"""
|
||||
url = reverse('api-supplier-part-list')
|
||||
|
||||
# Return *all* SupplierParts
|
||||
response = self.get(url, {}, expected_code=200)
|
||||
|
||||
self.assertEqual(len(response.data), SupplierPart.objects.count())
|
||||
|
||||
# Filter by Supplier reference
|
||||
for supplier in Company.objects.filter(is_supplier=True):
|
||||
response = self.get(url, {'supplier': supplier.pk}, expected_code=200)
|
||||
self.assertEqual(len(response.data), supplier.supplied_parts.count())
|
||||
|
||||
# Filter by Part reference
|
||||
expected = {
|
||||
1: 4,
|
||||
25: 2,
|
||||
}
|
||||
|
||||
for pk, n in expected.items():
|
||||
response = self.get(url, {'part': pk}, expected_code=200)
|
||||
self.assertEqual(len(response.data), n)
|
||||
|
||||
def test_available(self):
|
||||
"""Tests for updating the 'available' field"""
|
||||
|
||||
url = reverse('api-supplier-part-list')
|
||||
|
||||
# Should fail when sending an invalid 'available' field
|
||||
response = self.post(
|
||||
url,
|
||||
{
|
||||
'part': 1,
|
||||
'supplier': 2,
|
||||
'SKU': 'QQ',
|
||||
'available': 'not a number',
|
||||
},
|
||||
expected_code=400,
|
||||
)
|
||||
|
||||
self.assertIn('A valid number is required', str(response.data))
|
||||
|
||||
# Create a SupplierPart without specifying available quantity
|
||||
response = self.post(
|
||||
url,
|
||||
{
|
||||
'part': 1,
|
||||
'supplier': 2,
|
||||
'SKU': 'QQ',
|
||||
},
|
||||
expected_code=201
|
||||
)
|
||||
|
||||
sp = SupplierPart.objects.get(pk=response.data['pk'])
|
||||
|
||||
self.assertIsNone(sp.availability_updated)
|
||||
self.assertEqual(sp.available, 0)
|
||||
|
||||
# Now, *update* the availabile quantity via the API
|
||||
self.patch(
|
||||
reverse('api-supplier-part-detail', kwargs={'pk': sp.pk}),
|
||||
{
|
||||
'available': 1234,
|
||||
},
|
||||
expected_code=200,
|
||||
)
|
||||
|
||||
sp.refresh_from_db()
|
||||
self.assertIsNotNone(sp.availability_updated)
|
||||
self.assertEqual(sp.available, 1234)
|
||||
|
||||
# We should also be able to create a SupplierPart with initial 'available' quantity
|
||||
response = self.post(
|
||||
url,
|
||||
{
|
||||
'part': 1,
|
||||
'supplier': 2,
|
||||
'SKU': 'QQQ',
|
||||
'available': 999,
|
||||
},
|
||||
expected_code=201,
|
||||
)
|
||||
|
||||
sp = SupplierPart.objects.get(pk=response.data['pk'])
|
||||
self.assertEqual(sp.available, 999)
|
||||
self.assertIsNotNone(sp.availability_updated)
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1010,7 +1010,7 @@ function loadBomTable(table, options={}) {
|
||||
can_build = available / row.quantity;
|
||||
}
|
||||
|
||||
return +can_build.toFixed(2);
|
||||
return formatDecimal(can_build, 2);
|
||||
},
|
||||
sorter: function(valA, valB, rowA, rowB) {
|
||||
// Function to sort the "can build" quantity
|
||||
|
@ -795,7 +795,7 @@ function sumAllocationsForBomRow(bom_row, allocations) {
|
||||
quantity += allocation.quantity;
|
||||
});
|
||||
|
||||
return parseFloat(quantity).toFixed(15);
|
||||
return formatDecimal(quantity, 10);
|
||||
}
|
||||
|
||||
|
||||
@ -1490,8 +1490,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
|
||||
// Store the required quantity in the row data
|
||||
// Prevent weird rounding issues
|
||||
row.required = parseFloat(quantity.toFixed(15));
|
||||
|
||||
row.required = formatDecimal(quantity, 15);
|
||||
return row.required;
|
||||
}
|
||||
|
||||
@ -2043,7 +2042,7 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
|
||||
}
|
||||
|
||||
// Ensure the quantity sent to the form field is correctly formatted
|
||||
remaining = parseFloat(remaining.toFixed(15));
|
||||
remaining = formatDecimal(remaining, 15);
|
||||
|
||||
// We only care about entries which are not yet fully allocated
|
||||
if (remaining > 0) {
|
||||
|
@ -189,14 +189,16 @@ function createSupplierPart(options={}) {
|
||||
|
||||
function editSupplierPart(part, options={}) {
|
||||
|
||||
var fields = supplierPartFields();
|
||||
var fields = options.fields || supplierPartFields();
|
||||
|
||||
// Hide the "part" field
|
||||
fields.part.hidden = true;
|
||||
if (fields.part) {
|
||||
fields.part.hidden = true;
|
||||
}
|
||||
|
||||
constructForm(`/api/company/part/${part}/`, {
|
||||
fields: fields,
|
||||
title: '{% trans "Edit Supplier Part" %}',
|
||||
title: options.title || '{% trans "Edit Supplier Part" %}',
|
||||
onSuccess: options.onSuccess
|
||||
});
|
||||
}
|
||||
@ -952,6 +954,21 @@ function loadSupplierPartTable(table, url, options) {
|
||||
title: '{% trans "Packaging" %}',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'available',
|
||||
title: '{% trans "Available" %}',
|
||||
sortable: true,
|
||||
formatter: function(value, row) {
|
||||
if (row.availability_updated) {
|
||||
var html = formatDecimal(value);
|
||||
var date = renderDate(row.availability_updated, {showTime: true});
|
||||
html += `<span class='fas fa-info-circle float-right' title='{% trans "Last Updated" %}: ${date}'></span>`;
|
||||
return html;
|
||||
} else {
|
||||
return '-';
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
title: '',
|
||||
|
@ -4,6 +4,7 @@
|
||||
blankImage,
|
||||
deleteButton,
|
||||
editButton,
|
||||
formatDecimal,
|
||||
imageHoverIcon,
|
||||
makeIconBadge,
|
||||
makeIconButton,
|
||||
@ -34,6 +35,13 @@ function deleteButton(url, text='{% trans "Delete" %}') {
|
||||
}
|
||||
|
||||
|
||||
/* Format a decimal (floating point) number, to strip trailing zeros
|
||||
*/
|
||||
function formatDecimal(number, places=5) {
|
||||
return +parseFloat(number).toFixed(places);
|
||||
}
|
||||
|
||||
|
||||
function blankImage() {
|
||||
return `/static/img/blank_image.png`;
|
||||
}
|
||||
|
@ -1191,7 +1191,7 @@ function noResultBadge() {
|
||||
|
||||
function formatDate(row) {
|
||||
// Function for formatting date field
|
||||
var html = row.date;
|
||||
var html = renderDate(row.date);
|
||||
|
||||
if (row.user_detail) {
|
||||
html += `<span class='badge badge-right rounded-pill bg-secondary'>${row.user_detail.username}</span>`;
|
||||
@ -1707,13 +1707,13 @@ function loadStockTable(table, options) {
|
||||
val = '# ' + row.serial;
|
||||
} else if (row.quantity != available) {
|
||||
// Some quantity is available, show available *and* quantity
|
||||
var ava = +parseFloat(available).toFixed(5);
|
||||
var tot = +parseFloat(row.quantity).toFixed(5);
|
||||
var ava = formatDecimal(available);
|
||||
var tot = formatDecimal(row.quantity);
|
||||
|
||||
val = `${ava} / ${tot}`;
|
||||
} else {
|
||||
// Format floating point numbers with this one weird trick
|
||||
val = +parseFloat(value).toFixed(5);
|
||||
val = formatDecimal(value);
|
||||
}
|
||||
|
||||
var html = renderLink(val, `/stock/item/${row.pk}/`);
|
||||
|
@ -34,6 +34,11 @@ INVENTREE_DB_PORT=5432
|
||||
#INVENTREE_DB_USER=pguser
|
||||
#INVENTREE_DB_PASSWORD=pgpassword
|
||||
|
||||
# Redis cache setup
|
||||
# Refer to settings.py for other cache options
|
||||
INVENTREE_CACHE_HOST=inventree-cache
|
||||
INVENTREE_CACHE_PORT=6379
|
||||
|
||||
# Enable plugins?
|
||||
INVENTREE_PLUGINS_ENABLED=False
|
||||
|
||||
|
@ -1,10 +1,11 @@
|
||||
version: "3.8"
|
||||
|
||||
# Docker compose recipe for InvenTree production server
|
||||
# Docker compose recipe for InvenTree production server, with the following containerized processes
|
||||
# - PostgreSQL as the database backend
|
||||
# - gunicorn as the InvenTree web server
|
||||
# - django-q as the InvenTree background worker process
|
||||
# - nginx as a reverse proxy
|
||||
# - redis as the cache manager
|
||||
|
||||
# ---------------------
|
||||
# READ BEFORE STARTING!
|
||||
@ -112,6 +113,18 @@ services:
|
||||
- inventree_data:/var/www
|
||||
restart: unless-stopped
|
||||
|
||||
# redis acts as database cache manager
|
||||
inventree-cache:
|
||||
container_name: inventree-cache
|
||||
image: redis:7.0
|
||||
depends_on:
|
||||
- inventree-db
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- ${INVENTREE_CACHE_PORT:-6379}:6379
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
# NOTE: Change /path/to/data to a directory on your local machine
|
||||
# Persistent data, stored external to the container(s)
|
||||
|
52
tasks.py
52
tasks.py
@ -224,7 +224,7 @@ def update(c):
|
||||
def style(c):
|
||||
"""Run PEP style checks against InvenTree sourcecode"""
|
||||
print("Running PEP style checks...")
|
||||
c.run('flake8 InvenTree')
|
||||
c.run('flake8 InvenTree tasks.py')
|
||||
|
||||
|
||||
@task
|
||||
@ -282,9 +282,30 @@ def content_excludes():
|
||||
return output
|
||||
|
||||
|
||||
@task(help={'filename': "Output filename (default = 'data.json')"})
|
||||
def export_records(c, filename='data.json'):
|
||||
"""Export all database records to a file"""
|
||||
@task(help={
|
||||
'filename': "Output filename (default = 'data.json')",
|
||||
'overwrite': "Overwrite existing files without asking first (default = off/False)",
|
||||
'include_permissions': "Include user and group permissions in the output file (filename) (default = off/False)",
|
||||
'delete_temp': "Delete temporary files (containing permissions) at end of run. Note that this will delete temporary files from previous runs as well. (default = off/False)"
|
||||
})
|
||||
def export_records(c, filename='data.json', overwrite=False, include_permissions=False, delete_temp=False):
|
||||
"""Export all database records to a file.
|
||||
|
||||
Write data to the file defined by filename.
|
||||
If --overwrite is not set, the user will be prompted about overwriting an existing files.
|
||||
If --include-permissions is not set, the file defined by filename will have permissions specified for a user or group removed.
|
||||
If --delete-temp is not set, the temporary file (which includes permissions) will not be deleted. This file is named filename.tmp
|
||||
|
||||
For historical reasons, calling this function without any arguments will thus result in two files:
|
||||
- data.json: does not include permissions
|
||||
- data.json.tmp: includes permissions
|
||||
|
||||
If you want the script to overwrite any existing files without asking, add argument -o / --overwrite.
|
||||
|
||||
If you only want one file, add argument - d / --delete-temp.
|
||||
|
||||
If you want only one file, with permissions, then additionally add argument -i / --include-permissions
|
||||
"""
|
||||
# Get an absolute path to the file
|
||||
if not os.path.isabs(filename):
|
||||
filename = os.path.join(localDir(), filename)
|
||||
@ -292,7 +313,7 @@ def export_records(c, filename='data.json'):
|
||||
|
||||
print(f"Exporting database records to file '{filename}'")
|
||||
|
||||
if os.path.exists(filename):
|
||||
if os.path.exists(filename) and overwrite is False:
|
||||
response = input("Warning: file already exists. Do you want to overwrite? [y/N]: ")
|
||||
response = str(response).strip().lower()
|
||||
|
||||
@ -313,16 +334,17 @@ def export_records(c, filename='data.json'):
|
||||
with open(tmpfile, "r") as f_in:
|
||||
data = json.loads(f_in.read())
|
||||
|
||||
for entry in data:
|
||||
if "model" in entry:
|
||||
if include_permissions is False:
|
||||
for entry in data:
|
||||
if "model" in entry:
|
||||
|
||||
# Clear out any permissions specified for a group
|
||||
if entry["model"] == "auth.group":
|
||||
entry["fields"]["permissions"] = []
|
||||
# Clear out any permissions specified for a group
|
||||
if entry["model"] == "auth.group":
|
||||
entry["fields"]["permissions"] = []
|
||||
|
||||
# Clear out any permissions specified for a user
|
||||
if entry["model"] == "auth.user":
|
||||
entry["fields"]["user_permissions"] = []
|
||||
# Clear out any permissions specified for a user
|
||||
if entry["model"] == "auth.user":
|
||||
entry["fields"]["user_permissions"] = []
|
||||
|
||||
# Write the processed data to file
|
||||
with open(filename, "w") as f_out:
|
||||
@ -330,6 +352,10 @@ def export_records(c, filename='data.json'):
|
||||
|
||||
print("Data export completed")
|
||||
|
||||
if delete_temp is True:
|
||||
print("Removing temporary file")
|
||||
os.remove(tmpfile)
|
||||
|
||||
|
||||
@task(help={'filename': 'Input filename', 'clear': 'Clear existing data before import'}, post=[rebuild_models, rebuild_thumbnails])
|
||||
def import_records(c, filename='data.json', clear=False):
|
||||
|
Reference in New Issue
Block a user