diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 06908e76bf..d48743f166 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -208,7 +208,6 @@ INSTALLED_APPS = [ 'mptt', # Modified Preorder Tree Traversal 'markdownx', # Markdown editing 'markdownify', # Markdown template rendering - 'django_tex', # LaTeX output 'django_admin_shell', # Python shell for the admin interface 'djmoney', # django-money integration 'djmoney.contrib.exchange', # django-money exchange rates @@ -265,14 +264,6 @@ TEMPLATES = [ ], }, }, - # Backend for LaTeX report rendering - { - 'NAME': 'tex', - 'BACKEND': 'django_tex.engine.TeXEngine', - 'DIRS': [ - os.path.join(MEDIA_ROOT, 'report'), - ] - }, ] REST_FRAMEWORK = { @@ -485,22 +476,6 @@ DATE_INPUT_FORMATS = [ "%Y-%m-%d", ] -# LaTeX rendering settings (django-tex) -LATEX_SETTINGS = CONFIG.get('latex', {}) - -# Is LaTeX rendering enabled? (Off by default) -LATEX_ENABLED = LATEX_SETTINGS.get('enabled', False) - -# Set the latex interpreter in the config.yaml settings file -LATEX_INTERPRETER = LATEX_SETTINGS.get('interpreter', 'pdflatex') - -LATEX_INTERPRETER_OPTIONS = LATEX_SETTINGS.get('options', '') - -LATEX_GRAPHICSPATH = [ - # Allow LaTeX files to access the report assets directory - os.path.join(MEDIA_ROOT, "report", "assets"), -] - # crispy forms use the bootstrap templates CRISPY_TEMPLATE_PACK = 'bootstrap3' diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 2c56b8eeb1..1df5e83d5d 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -79,6 +79,7 @@ settings_urls = [ url(r'^theme/?', ColorThemeSelectView.as_view(), name='settings-theme'), url(r'^global/?', SettingsView.as_view(template_name='InvenTree/settings/global.html'), name='settings-global'), + url(r'^report/?', SettingsView.as_view(template_name='InvenTree/settings/report.html'), name='settings-report'), url(r'^category/?', SettingCategorySelectView.as_view(), name='settings-category'), url(r'^part/?', SettingsView.as_view(template_name='InvenTree/settings/part.html'), name='settings-part'), url(r'^stock/?', SettingsView.as_view(template_name='InvenTree/settings/stock.html'), name='settings-stock'), diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 053cc12864..a81d06ed25 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -174,6 +174,13 @@ class InvenTreeSetting(models.Model): 'validator': bool, }, + 'REPORT_ENABLE_TEST_REPORT': { + 'name': _('Test Reports'), + 'description': _('Enable generation of test reports'), + 'default': True, + 'validator': bool, + }, + 'STOCK_ENABLE_EXPIRY': { 'name': _('Stock Expiry'), 'description': _('Enable stock expiry functionality'), diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml index c655ffdc5c..18e3197cca 100644 --- a/InvenTree/config_template.yaml +++ b/InvenTree/config_template.yaml @@ -107,19 +107,6 @@ static_root: '../inventree_static' # If unspecified, the local user's temp directory will be used #backup_dir: '/home/inventree/backup/' -# LaTeX report rendering -# InvenTree uses the django-tex plugin to enable LaTeX report rendering -# Ref: https://pypi.org/project/django-tex/ -# Note: Ensure that a working LaTeX toolchain is installed and working *before* starting the server -latex: - # Select the LaTeX interpreter to use for PDF rendering - # Note: The intepreter needs to be installed on the system! - # e.g. to install pdflatex: apt-get texlive-latex-base - enabled: False - interpreter: pdflatex - # Extra options to pass through to the LaTeX interpreter - options: '' - # Permit custom authentication backends #authentication_backends: # - 'django.contrib.auth.backends.ModelBackend' diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index 84eac7e7f4..f35d616a91 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -44,7 +44,6 @@ - {% settings_value 'BARCODE_ENABLE' as barcodes %} {% if barcodes %}
diff --git a/InvenTree/report/admin.py b/InvenTree/report/admin.py index 7d6403f5d9..390964b590 100644 --- a/InvenTree/report/admin.py +++ b/InvenTree/report/admin.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals from django.contrib import admin -from .models import TestReport, ReportAsset +from .models import ReportSnippet, TestReport, ReportAsset class ReportTemplateAdmin(admin.ModelAdmin): @@ -11,10 +11,16 @@ class ReportTemplateAdmin(admin.ModelAdmin): list_display = ('name', 'description', 'template', 'filters', 'enabled') +class ReportSnippetAdmin(admin.ModelAdmin): + + list_display = ('id', 'snippet', 'description') + + class ReportAssetAdmin(admin.ModelAdmin): - list_display = ('asset', 'description') + list_display = ('id', 'asset', 'description') +admin.site.register(ReportSnippet, ReportSnippetAdmin) admin.site.register(TestReport, ReportTemplateAdmin) admin.site.register(ReportAsset, ReportAssetAdmin) diff --git a/InvenTree/report/apps.py b/InvenTree/report/apps.py index 138ba20404..bb1c5f0cb7 100644 --- a/InvenTree/report/apps.py +++ b/InvenTree/report/apps.py @@ -1,5 +1,92 @@ +import os +import shutil +import logging + from django.apps import AppConfig +from django.conf import settings + + +logger = logging.getLogger(__name__) class ReportConfig(AppConfig): name = 'report' + + def ready(self): + """ + This function is called whenever the report app is loaded + """ + + self.create_default_test_reports() + + def create_default_test_reports(self): + """ + Create database entries for the default TestReport templates, + if they do not already exist + """ + + try: + from .models import TestReport + except: + # Database is not ready yet + return + + src_dir = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + 'templates', + 'report', + ) + + dst_dir = os.path.join( + settings.MEDIA_ROOT, + 'report', + 'inventree', # Stored in secret directory! + 'test', + ) + + if not os.path.exists(dst_dir): + logger.info(f"Creating missing directory: '{dst_dir}'") + os.makedirs(dst_dir, exist_ok=True) + + # List of test reports to copy across + reports = [ + { + 'file': 'inventree_test_report.html', + 'name': 'InvenTree Test Report', + 'description': 'Stock item test report', + }, + ] + + for report in reports: + + # Create destination file name + filename = os.path.join( + 'report', + 'inventree', + 'test', + report['file'] + ) + + src_file = os.path.join(src_dir, report['file']) + dst_file = os.path.join(settings.MEDIA_ROOT, filename) + + if not os.path.exists(dst_file): + logger.info(f"Copying test report template '{dst_file}'") + shutil.copyfile(src_file, dst_file) + + try: + # Check if a report matching the template already exists + if TestReport.objects.filter(template=filename).exists(): + continue + + logger.info(f"Creating new TestReport for '{report['name']}'") + + TestReport.objects.create( + name=report['name'], + description=report['description'], + template=filename, + filters='', + enabled=True + ) + except: + pass diff --git a/InvenTree/report/migrations/0006_reportsnippet.py b/InvenTree/report/migrations/0006_reportsnippet.py new file mode 100644 index 0000000000..6875ebb530 --- /dev/null +++ b/InvenTree/report/migrations/0006_reportsnippet.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.7 on 2021-02-04 04:37 + +import django.core.validators +from django.db import migrations, models +import report.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('report', '0005_auto_20210119_0815'), + ] + + operations = [ + migrations.CreateModel( + name='ReportSnippet', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('snippet', models.FileField(help_text='Report snippet file', upload_to=report.models.rename_snippet, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['html', 'htm'])])), + ('description', models.CharField(help_text='Snippet file description', max_length=250)), + ], + ), + ] diff --git a/InvenTree/report/migrations/0007_auto_20210204_1617.py b/InvenTree/report/migrations/0007_auto_20210204_1617.py new file mode 100644 index 0000000000..b110f63365 --- /dev/null +++ b/InvenTree/report/migrations/0007_auto_20210204_1617.py @@ -0,0 +1,20 @@ +# Generated by Django 3.0.7 on 2021-02-04 05:17 + +import django.core.validators +from django.db import migrations, models +import report.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('report', '0006_reportsnippet'), + ] + + operations = [ + migrations.AlterField( + model_name='testreport', + name='template', + field=models.FileField(help_text='Report template file', upload_to=report.models.rename_template, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['html', 'htm'])], verbose_name='Template'), + ), + ] diff --git a/InvenTree/report/migrations/0008_auto_20210204_2100.py b/InvenTree/report/migrations/0008_auto_20210204_2100.py new file mode 100644 index 0000000000..35136676d4 --- /dev/null +++ b/InvenTree/report/migrations/0008_auto_20210204_2100.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.7 on 2021-02-04 10:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('report', '0007_auto_20210204_1617'), + ] + + operations = [ + migrations.AlterField( + model_name='testreport', + name='name', + field=models.CharField(help_text='Template name', max_length=100, verbose_name='Name'), + ), + ] diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py index 224f2800d8..422216f9a9 100644 --- a/InvenTree/report/models.py +++ b/InvenTree/report/models.py @@ -14,7 +14,6 @@ from django.db import models from django.conf import settings from django.core.validators import FileExtensionValidator -from django.core.exceptions import ValidationError import stock.models @@ -29,32 +28,10 @@ except OSError as err: print("You may require some further system packages to be installed.") sys.exit(1) -# Conditional import if LaTeX templating is enabled -if settings.LATEX_ENABLED: - try: - from django_tex.shortcuts import render_to_pdf - from django_tex.core import render_template_with_context - from django_tex.exceptions import TexError - except OSError as err: - print("OSError: {e}".format(e=err)) - print("You may not have a working LaTeX toolchain installed?") - sys.exit(1) - -from django.http import HttpResponse - - -class TexResponse(HttpResponse): - def __init__(self, content, filename=None): - super().__init__(content_type="application/txt") - self["Content-Disposition"] = 'filename="{}"'.format(filename) - self.write(content) - def rename_template(instance, filename): - filename = os.path.basename(filename) - - return os.path.join('report', 'report_template', instance.getSubdir(), filename) + return instance.rename_file(filename) def validate_stock_item_report_filters(filters): @@ -77,17 +54,27 @@ class WeasyprintReportMixin(WeasyTemplateResponseMixin): self.pdf_filename = kwargs.get('filename', 'report.pdf') -class ReportTemplateBase(models.Model): +class ReportBase(models.Model): """ - Reporting template model. + Base class for uploading html templates """ + class Meta: + abstract = True + def __str__(self): return "{n} - {d}".format(n=self.name, d=self.description) def getSubdir(self): return '' + def rename_file(self, filename): + # Function for renaming uploaded file + + filename = os.path.basename(filename) + + return os.path.join('report', 'report_template', self.getSubdir(), filename) + @property def extension(self): return os.path.splitext(self.template.name)[1].lower() @@ -96,15 +83,45 @@ class ReportTemplateBase(models.Model): def template_name(self): """ Returns the file system path to the template file. - Required for passing the file to an external process (e.g. LaTeX) + Required for passing the file to an external process """ - template = os.path.join('report_template', self.getSubdir(), os.path.basename(self.template.name)) + template = self.template.name template = template.replace('/', os.path.sep) template = template.replace('\\', os.path.sep) + template = os.path.join(settings.MEDIA_ROOT, template) + return template + name = models.CharField( + blank=False, max_length=100, + verbose_name=_('Name'), + help_text=_('Template name'), + ) + + template = models.FileField( + upload_to=rename_template, + verbose_name=_('Template'), + help_text=_("Report template file"), + validators=[FileExtensionValidator(allowed_extensions=['html', 'htm'])], + ) + + description = models.CharField( + max_length=250, + verbose_name=_('Description'), + help_text=_("Report template description") + ) + + +class ReportTemplateBase(ReportBase): + """ + Reporting template model. + + Able to be passed context data + + """ + def get_context_data(self, request): """ Supply context data to the template for rendering @@ -116,56 +133,34 @@ class ReportTemplateBase(models.Model): """ Render the template to a PDF file. - Supported template formats: - .tex - Uses django-tex plugin to render LaTeX template against an installed LaTeX engine - .html - Uses django-weasyprint plugin to render HTML template against Weasyprint + Uses django-weasyprint plugin to render HTML template against Weasyprint """ - filename = kwargs.get('filename', 'report.pdf') + # TODO: Support custom filename generation! + # filename = kwargs.get('filename', 'report.pdf') context = self.get_context_data(request) + context['media'] = settings.MEDIA_ROOT + + context['report_name'] = self.name + context['report_description'] = self.description context['request'] = request context['user'] = request.user + context['date'] = datetime.datetime.now().date() context['datetime'] = datetime.datetime.now() - if self.extension == '.tex': - # Render LaTeX template to PDF - if settings.LATEX_ENABLED: - # Attempt to render to LaTeX template - # If there is a rendering error, return the (partially rendered) template, - # so at least we can debug what is going on - try: - rendered = render_template_with_context(self.template_name, context) - return render_to_pdf(request, self.template_name, context, filename=filename) - except TexError: - return TexResponse(rendered, filename="error.tex") - else: - raise ValidationError("Enable LaTeX support in config.yaml") - elif self.extension in ['.htm', '.html']: - # Render HTML template to PDF - wp = WeasyprintReportMixin(request, self.template_name, **kwargs) - return wp.render_to_response(context, **kwargs) + # Render HTML template to PDF + wp = WeasyprintReportMixin( + request, + self.template_name, + base_url=request.build_absolute_uri("/"), + presentational_hints=True, + **kwargs) - name = models.CharField( - blank=False, max_length=100, - verbose_name=_('Name'), - help_text=_('Template name'), - unique=True, - ) - - template = models.FileField( - upload_to=rename_template, - verbose_name=_('Template'), - help_text=_("Report template file"), - validators=[FileExtensionValidator(allowed_extensions=['html', 'htm', 'tex'])], - ) - - description = models.CharField( - max_length=250, - verbose_name=_('Description'), - help_text=_("Report template description") - ) + return wp.render_to_response( + context, + **kwargs) enabled = models.BooleanField( default=True, @@ -221,6 +216,30 @@ class TestReport(ReportTemplateBase): } +def rename_snippet(instance, filename): + + filename = os.path.basename(filename) + + return os.path.join('report', 'snippets', filename) + + +class ReportSnippet(models.Model): + """ + Report template 'snippet' which can be used to make templates + that can then be included in other reports. + + Useful for 'common' template actions, sub-templates, etc + """ + + snippet = models.FileField( + upload_to=rename_snippet, + help_text=_('Report snippet file'), + validators=[FileExtensionValidator(allowed_extensions=['html', 'htm'])], + ) + + description = models.CharField(max_length=250, help_text=_("Snippet file description")) + + def rename_asset(instance, filename): filename = os.path.basename(filename) diff --git a/InvenTree/report/templates/report/inventree_report_base.html b/InvenTree/report/templates/report/inventree_report_base.html new file mode 100644 index 0000000000..084b100cab --- /dev/null +++ b/InvenTree/report/templates/report/inventree_report_base.html @@ -0,0 +1,103 @@ +{% load report %} + + + + + + + +
+ {% block header_content %} + {% endblock %} +
+ +
+ {% block page_content %} + {% endblock %} +
+ + + + \ No newline at end of file diff --git a/InvenTree/report/templates/report/inventree_test_report.html b/InvenTree/report/templates/report/inventree_test_report.html new file mode 100644 index 0000000000..e076a7919e --- /dev/null +++ b/InvenTree/report/templates/report/inventree_test_report.html @@ -0,0 +1,112 @@ +{% extends "report/inventree_report_base.html" %} + +{% load i18n %} +{% load report %} +{% load inventree_extras %} + +{% block style %} +.test-table { + width: 100%; +} + +{% block bottom_left %} +content: "{{ date.isoformat }}"; +{% endblock %} + +{% block bottom_center %} +content: "InvenTree v{% inventree_version %}"; +{% endblock %} + +{% block top_center %} +content: "{% trans 'Stock Item Test Report' %}"; +{% endblock %} + +.test-row { + padding: 3px; +} + +.test-pass { + color: #5f5; +} + +.test-fail { + color: #F55; +} + +.container { + padding: 5px; + border: 1px solid; +} + +.text-left { + display: inline-block; + width: 50%; +} + +.img-right { + display: inline; + align-content: right; + align-items: right; + width: 50%; +} + +{% endblock %} + +{% block page_content %} + +
+
+

+ {{ part.full_name }} +

+

{{ part.description }}

+

{{ stock_item.location }}

+

Stock Item ID: {{ stock_item.pk }}

+
+
+ +
+

+ {% if stock_item.is_serialized %} + {% trans "Serial Number" %}: {{ stock_item.serial }} + {% else %} + {% trans "Quantity" %}: {% decimal stock_item.quantity %} + {% endif %} +

+
+
+ +

{% trans "Test Results" %}

+ + + + + + + + + + + + + + + + {% for test in result_list %} + + + {% if test.result %} + + {% else %} + + {% endif %} + + + + + {% endfor %} + + +
{% trans "Test" %}{% trans "Result" %}{% trans "Value" %}{% trans "User" %}{% trans "Date" %}

{{ test.test }}{% trans "Pass" %}{% trans "Fail" %}{{ test.value }}{{ test.user.username }}{{ test.date.date.isoformat }}
+ +{% endblock %} \ No newline at end of file diff --git a/InvenTree/report/templatetags/report.py b/InvenTree/report/templatetags/report.py new file mode 100644 index 0000000000..0383aad562 --- /dev/null +++ b/InvenTree/report/templatetags/report.py @@ -0,0 +1,52 @@ +""" +Custom template tags for report generation +""" + +import os + +from django import template +from django.conf import settings + +from part.models import Part +from stock.models import StockItem + +register = template.Library() + + +@register.simple_tag() +def asset(filename): + """ + Return fully-qualified path for an upload report asset file. + """ + + path = os.path.join(settings.MEDIA_ROOT, 'report', 'assets', filename) + path = os.path.abspath(path) + + return f"file://{path}" + + +@register.simple_tag() +def part_image(part): + """ + Return a fully-qualified path for a part image + """ + + if type(part) is Part: + img = part.image.name + + elif type(part) is StockItem: + img = part.part.image.name + + else: + img = '' + + path = os.path.join(settings.MEDIA_ROOT, img) + path = os.path.abspath(path) + + if not os.path.exists(path) or not os.path.isfile(path): + # Image does not exist + # Return the 'blank' image + path = os.path.join(settings.STATIC_ROOT, 'img', 'blank_image.png') + path = os.path.abspath(path) + + return f"file://{path}" diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index d284a74e98..93d60fb3d1 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -120,7 +120,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
- {% settings_value 'BARCODE_ENABLE' as barcodes %} + {% if barcodes %}
@@ -139,19 +139,15 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
{% endif %} - {% if item.has_labels or item.has_test_reports %}
- {% endif %} {% if owner_control.value == "False" or owner_control.value == "True" and user in owners or user.is_superuser %} diff --git a/InvenTree/stock/templates/stock/location.html b/InvenTree/stock/templates/stock/location.html index 4f8268b037..817b096f74 100644 --- a/InvenTree/stock/templates/stock/location.html +++ b/InvenTree/stock/templates/stock/location.html @@ -36,8 +36,7 @@ {% endif %} - {% endif %} - {% settings_value 'BARCODE_ENABLE' as barcodes %} + {% endif %} {% if barcodes %} {% if location %} diff --git a/InvenTree/templates/InvenTree/settings/report.html b/InvenTree/templates/InvenTree/settings/report.html new file mode 100644 index 0000000000..0cba8c449b --- /dev/null +++ b/InvenTree/templates/InvenTree/settings/report.html @@ -0,0 +1,22 @@ +{% extends "InvenTree/settings/settings.html" %} +{% load i18n %} +{% load inventree_extras %} + +{% block tabs %} +{% include "InvenTree/settings/tabs.html" with tab='report' %} +{% endblock %} + +{% block subtitle %} +{% trans "Report Settings" %} +{% endblock %} + +{% block settings %} + + + {% include "InvenTree/settings/header.html" %} + + {% include "InvenTree/settings/setting.html" with key="REPORT_ENABLE_TEST_REPORT" %} + +
+ +{% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/settings/tabs.html b/InvenTree/templates/InvenTree/settings/tabs.html index ff2bcdb61e..bb660519b8 100644 --- a/InvenTree/templates/InvenTree/settings/tabs.html +++ b/InvenTree/templates/InvenTree/settings/tabs.html @@ -15,6 +15,9 @@
  • {% trans "Global" %}
  • +
  • + {% trans "Report" %} +
  • {% trans "Categories" %} diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index d2b8226982..c1640892de 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -3,6 +3,7 @@ {% load inventree_extras %} {% settings_value 'BARCODE_ENABLE' as barcodes %} +{% settings_value 'REPORT_ENABLE_TEST_REPORT' as test_report_enabled %} diff --git a/InvenTree/templates/js/report.js b/InvenTree/templates/js/report.js index da84433bac..6e47706917 100644 --- a/InvenTree/templates/js/report.js +++ b/InvenTree/templates/js/report.js @@ -10,6 +10,15 @@ function selectTestReport(reports, items, options={}) { * (via AJAX) from the server. */ + // If there is only a single report available, just print! + if (reports.length == 1) { + if (options.success) { + options.success(reports[0].pk); + } + + return; + } + var modal = options.modal || '#modal-form'; var report_list = makeOptionsList( diff --git a/InvenTree/templates/stock_table.html b/InvenTree/templates/stock_table.html index 4caf2f59fd..8eff0f4aed 100644 --- a/InvenTree/templates/stock_table.html +++ b/InvenTree/templates/stock_table.html @@ -38,7 +38,9 @@
    {% if roles.stock.change or roles.stock.delete %} diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 57cee2774f..351b6f7370 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -118,6 +118,7 @@ class RuleSet(models.Model): 'label_stockitemlabel', 'label_stocklocationlabel', 'report_reportasset', + 'report_reportsnippet', 'report_testreport', 'part_partstar', 'users_owner', diff --git a/requirements.txt b/requirements.txt index e4ffe6be75..8d237527c2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,7 +23,6 @@ coverage==5.3 # Unit test coverage coveralls==2.1.2 # Coveralls linking (for Travis) rapidfuzz==0.7.6 # Fuzzy string matching django-stdimage==5.1.1 # Advanced ImageField management -django-tex==1.1.7 # LaTeX PDF export django-weasyprint==1.0.1 # HTML PDF export django-debug-toolbar==2.2 # Debug / profiling toolbar django-admin-shell==0.1.2 # Python shell for the admin interface