Compare commits
	
		
			31 Commits
		
	
	
		
			2d83c9ceec
			...
			port-to-dj
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| b8ff82fbda | |||
| 217606e51d | |||
| 566dafa87a | |||
| c567e38326 | |||
| 210d67a898 | |||
| 8636c513b7 | |||
| 3d263ca27c | |||
| 1c56dd44f9 | |||
| a300d66f66 | |||
| 191705c6a6 | |||
| ba0da19810 | |||
| 1e20cb458f | |||
| ac0f363a1e | |||
| b00cc19e61 | |||
| 52749da6e6 | |||
| 5fa6700bb4 | |||
| 8c5d017ed1 | |||
| c47350f449 | |||
| 0aadf4305f | |||
| b26c54dfce | |||
| 009ff5ae96 | |||
| a566e198b8 | |||
| ea623212bb | |||
| 2bc0f3124c | |||
| a34557499a | |||
| 35255cf4e9 | |||
| 873e13542a | |||
| 1e302e4595 | |||
| c6fae17154 | |||
| 3b1eb6118f | |||
| 76c79403f2 | 
							
								
								
									
										17
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| FROM ubuntu:22.04 | ||||
|  | ||||
| RUN apt-get update && apt-get install -y libpq-dev python3 python3-poetry nginx | ||||
| COPY shimatta_kenkyusho /app/shimatta_kenkyusho | ||||
| COPY poetry.lock /app/ | ||||
| COPY pyproject.toml /app/ | ||||
| RUN systemctl enable nginx | ||||
|  | ||||
| ARG UNAME=django | ||||
| ARG UID=1000 | ||||
| ARG GID=1000 | ||||
| RUN groupadd -g $GID -o $UNAME | ||||
| RUN useradd -m -u $UID -g $GID -o -s /bin/bash $UNAME | ||||
| USER $UNAME | ||||
|  | ||||
| WORKDIR "/app" | ||||
| RUN poetry install | ||||
							
								
								
									
										226
									
								
								poetry.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										226
									
								
								poetry.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,226 @@ | ||||
| [[package]] | ||||
| name = "annotated-types" | ||||
| version = "0.5.0" | ||||
| description = "Reusable constraint types to use with typing.Annotated" | ||||
| category = "main" | ||||
| optional = false | ||||
| python-versions = ">=3.7" | ||||
|  | ||||
| [[package]] | ||||
| name = "asgiref" | ||||
| version = "3.7.2" | ||||
| description = "ASGI specs, helper code, and adapters" | ||||
| category = "main" | ||||
| optional = false | ||||
| python-versions = ">=3.7" | ||||
|  | ||||
| [package.dependencies] | ||||
| typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} | ||||
|  | ||||
| [package.extras] | ||||
| tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] | ||||
|  | ||||
| [[package]] | ||||
| name = "crispy-bootstrap5" | ||||
| version = "0.7" | ||||
| description = "Bootstrap5 template pack for django-crispy-forms" | ||||
| category = "main" | ||||
| optional = false | ||||
| python-versions = ">=3.7" | ||||
|  | ||||
| [package.dependencies] | ||||
| django = ">=3.2" | ||||
| django-crispy-forms = ">=1.13.0" | ||||
|  | ||||
| [package.extras] | ||||
| test = ["pytest", "pytest-django"] | ||||
|  | ||||
| [[package]] | ||||
| name = "django" | ||||
| version = "4.2.5" | ||||
| description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." | ||||
| category = "main" | ||||
| optional = false | ||||
| python-versions = ">=3.8" | ||||
|  | ||||
| [package.dependencies] | ||||
| asgiref = ">=3.6.0,<4" | ||||
| sqlparse = ">=0.3.1" | ||||
| tzdata = {version = "*", markers = "sys_platform == \"win32\""} | ||||
|  | ||||
| [package.extras] | ||||
| argon2 = ["argon2-cffi (>=19.1.0)"] | ||||
| bcrypt = ["bcrypt"] | ||||
|  | ||||
| [[package]] | ||||
| name = "django-crispy-forms" | ||||
| version = "2.0" | ||||
| description = "Best way to have Django DRY forms" | ||||
| category = "main" | ||||
| optional = false | ||||
| python-versions = ">=3.7" | ||||
|  | ||||
| [package.dependencies] | ||||
| django = ">=3.2" | ||||
|  | ||||
| [[package]] | ||||
| name = "django-filter" | ||||
| version = "23.2" | ||||
| description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically." | ||||
| category = "main" | ||||
| optional = false | ||||
| python-versions = ">=3.7" | ||||
|  | ||||
| [package.dependencies] | ||||
| Django = ">=3.2" | ||||
|  | ||||
| [[package]] | ||||
| name = "django-qr-code" | ||||
| version = "3.1.1" | ||||
| description = "An application that provides tools for displaying QR codes on your Django site." | ||||
| category = "main" | ||||
| optional = false | ||||
| python-versions = ">=3.7" | ||||
|  | ||||
| [package.dependencies] | ||||
| django = ">=3.2" | ||||
| pydantic = "*" | ||||
| pytz = "*" | ||||
| segno = ">=1.5" | ||||
|  | ||||
| [[package]] | ||||
| name = "djangorestframework" | ||||
| version = "3.14.0" | ||||
| description = "Web APIs for Django, made easy." | ||||
| category = "main" | ||||
| optional = false | ||||
| python-versions = ">=3.6" | ||||
|  | ||||
| [package.dependencies] | ||||
| django = ">=3.0" | ||||
| pytz = "*" | ||||
|  | ||||
| [[package]] | ||||
| name = "pillow" | ||||
| version = "9.5.0" | ||||
| description = "Python Imaging Library (Fork)" | ||||
| category = "main" | ||||
| optional = false | ||||
| python-versions = ">=3.7" | ||||
|  | ||||
| [package.extras] | ||||
| docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] | ||||
| tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] | ||||
|  | ||||
| [[package]] | ||||
| name = "psycopg2" | ||||
| version = "2.9.7" | ||||
| description = "psycopg2 - Python-PostgreSQL Database Adapter" | ||||
| category = "main" | ||||
| optional = false | ||||
| python-versions = ">=3.6" | ||||
|  | ||||
| [[package]] | ||||
| name = "pydantic" | ||||
| version = "2.3.0" | ||||
| description = "Data validation using Python type hints" | ||||
| category = "main" | ||||
| optional = false | ||||
| python-versions = ">=3.7" | ||||
|  | ||||
| [package.dependencies] | ||||
| annotated-types = ">=0.4.0" | ||||
| pydantic-core = "2.6.3" | ||||
| typing-extensions = ">=4.6.1" | ||||
|  | ||||
| [package.extras] | ||||
| email = ["email-validator (>=2.0.0)"] | ||||
|  | ||||
| [[package]] | ||||
| name = "pydantic-core" | ||||
| version = "2.6.3" | ||||
| description = "" | ||||
| category = "main" | ||||
| optional = false | ||||
| python-versions = ">=3.7" | ||||
|  | ||||
| [package.dependencies] | ||||
| typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" | ||||
|  | ||||
| [[package]] | ||||
| name = "pytz" | ||||
| version = "2023.3" | ||||
| description = "World timezone definitions, modern and historical" | ||||
| category = "main" | ||||
| optional = false | ||||
| python-versions = "*" | ||||
|  | ||||
| [[package]] | ||||
| name = "segno" | ||||
| version = "1.5.2" | ||||
| description = "QR Code and Micro QR Code generator for Python 2 and Python 3" | ||||
| category = "main" | ||||
| optional = false | ||||
| python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" | ||||
|  | ||||
| [[package]] | ||||
| name = "sqlparse" | ||||
| version = "0.4.4" | ||||
| description = "A non-validating SQL parser." | ||||
| category = "main" | ||||
| optional = false | ||||
| python-versions = ">=3.5" | ||||
|  | ||||
| [package.extras] | ||||
| dev = ["flake8", "build"] | ||||
| doc = ["sphinx"] | ||||
| test = ["pytest", "pytest-cov"] | ||||
|  | ||||
| [[package]] | ||||
| name = "typing-extensions" | ||||
| version = "4.7.1" | ||||
| description = "Backported and Experimental Type Hints for Python 3.7+" | ||||
| category = "main" | ||||
| optional = false | ||||
| python-versions = ">=3.7" | ||||
|  | ||||
| [[package]] | ||||
| name = "tzdata" | ||||
| version = "2023.3" | ||||
| description = "Provider of IANA time zone data" | ||||
| category = "main" | ||||
| optional = false | ||||
| python-versions = ">=2" | ||||
|  | ||||
| [[package]] | ||||
| name = "uwsgi" | ||||
| version = "2.0.22" | ||||
| description = "The uWSGI server" | ||||
| category = "main" | ||||
| optional = false | ||||
| python-versions = "*" | ||||
|  | ||||
| [metadata] | ||||
| lock-version = "1.1" | ||||
| python-versions = "^3.10" | ||||
| content-hash = "6e065bbdff5da7659973fa67491d6093c83b8e57d4e8dde6968ee846d382bf65" | ||||
|  | ||||
| [metadata.files] | ||||
| annotated-types = [] | ||||
| asgiref = [] | ||||
| crispy-bootstrap5 = [] | ||||
| django = [] | ||||
| django-crispy-forms = [] | ||||
| django-filter = [] | ||||
| django-qr-code = [] | ||||
| djangorestframework = [] | ||||
| pillow = [] | ||||
| psycopg2 = [] | ||||
| pydantic = [] | ||||
| pydantic-core = [] | ||||
| pytz = [] | ||||
| segno = [] | ||||
| sqlparse = [] | ||||
| typing-extensions = [] | ||||
| tzdata = [] | ||||
| uwsgi = [] | ||||
							
								
								
									
										23
									
								
								pyproject.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								pyproject.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| [tool.poetry] | ||||
| name = "shimatta-kenkyusho" | ||||
| version = "0.1.0" | ||||
| description = "Shimatta Lab Inventory System" | ||||
| authors = ["Mario Hüttel <mario.huettel@gmx.net>"] | ||||
| license = "GPLv2" | ||||
|  | ||||
| [tool.poetry.dependencies] | ||||
| python = "^3.10" | ||||
| Django = "~4.2.5" | ||||
| django-qr-code = "^3.1.1" | ||||
| djangorestframework = "^3.14.0" | ||||
| django-filter = "^23.2" | ||||
| psycopg2 = "^2.9.7" | ||||
| Pillow = "^9.3.0" | ||||
| django-crispy-forms = "^2.0" | ||||
| crispy-bootstrap5 = "^0.7" | ||||
| uwsgi = "^2.0.22" | ||||
|  | ||||
|  | ||||
| [build-system] | ||||
| requires = ["poetry-core"] | ||||
| build-backend = "poetry.core.masonry.api" | ||||
| @@ -1,25 +0,0 @@ | ||||
| asgiref==3.4.1 | ||||
| astroid==2.6.5 | ||||
| crispy-bootstrap5==0.6 | ||||
| Django==3.2.5 | ||||
| django-crispy-forms==1.13.0 | ||||
| django-filter==2.4.0 | ||||
| django-qr-code==2.2.0 | ||||
| django-rest-framework==0.1.0 | ||||
| django-tex==1.1.9.post1 | ||||
| djangorestframework==3.12.4 | ||||
| isort==5.9.3 | ||||
| Jinja2==3.0.1 | ||||
| lazy-object-proxy==1.6.0 | ||||
| MarkupSafe==2.0.1 | ||||
| mccabe==0.6.1 | ||||
| Pillow==8.3.1 | ||||
| psycopg2==2.9.1 | ||||
| pylint==2.9.6 | ||||
| pytz==2021.1 | ||||
| qrcode==7.2 | ||||
| segno==1.3.3 | ||||
| six==1.16.0 | ||||
| sqlparse==0.4.1 | ||||
| toml==0.10.2 | ||||
| wrapt==1.12.1 | ||||
| @@ -1,7 +1,6 @@ | ||||
| from django.urls import include, path | ||||
| from django.urls import include, path, re_path | ||||
| from rest_framework import routers | ||||
| from .views import * | ||||
| from django.conf.urls import url | ||||
|  | ||||
| router = routers.DefaultRouter() | ||||
| router.register(r'users', UserViewSet) | ||||
| @@ -17,6 +16,6 @@ router.register(r'parts/component-param-types', PartsComponentParameterTypeViewS | ||||
|  | ||||
| urlpatterns = [ | ||||
|     path('', include(router.urls)), | ||||
|     url(r'^token-auth/', ObtainExpiringAuthToken.as_view()), | ||||
| 	url(r'^token-logout/', TokenLogout.as_view()), | ||||
|     re_path(r'^token-auth/', ObtainExpiringAuthToken.as_view()), | ||||
|     re_path(r'^token-logout/', TokenLogout.as_view()), | ||||
| ] | ||||
| @@ -50,8 +50,9 @@ class PartsComponentViewSet(viewsets.ModelViewSet): | ||||
|     queryset = parts_models.Component.objects.all() | ||||
|     serializer_class = ComponentSerializer | ||||
|     permission_classes = [permissions.DjangoModelPermissions] | ||||
|     filter_backends = [filters.SearchFilter] | ||||
|     filter_backends = [filters.SearchFilter, django_filters.rest_framework.DjangoFilterBackend] | ||||
|     search_fields = ['id', 'name', 'package__name', 'manufacturer__name'] | ||||
|     filterset_fields = ['id', 'name'] | ||||
|  | ||||
| class PartsComponentTypeViewSet(viewsets.ModelViewSet): | ||||
|     queryset = parts_models.ComponentType.objects.all() | ||||
|   | ||||
| @@ -9,6 +9,7 @@ admin.site.register(parts_models.Manufacturer) | ||||
| admin.site.register(parts_models.Storage) | ||||
| admin.site.register(parts_models.Stock) | ||||
| admin.site.register(parts_models.ComponentParameter) | ||||
| admin.site.register(parts_models.PackageParameter) | ||||
| admin.site.register(parts_models.ComponentParameterType) | ||||
| admin.site.register(parts_models.ComponentType) | ||||
| admin.site.register(parts_models.Distributor) | ||||
|   | ||||
| @@ -244,6 +244,7 @@ class AdvancedComponentSearchForm(forms.Form): | ||||
|         super().__init__(*args, **kwargs) | ||||
|         self.helper = FormHelper() | ||||
|         self.helper.form_tag = False | ||||
|         self.helper.disable_csrf = True | ||||
|         self.helper.layout = Layout( | ||||
|             Row( | ||||
|                 Column('name'), | ||||
| @@ -262,17 +263,27 @@ class AdvancedComponentSearchForm(forms.Form): | ||||
|             ), | ||||
|         ) | ||||
|  | ||||
|  | ||||
| PARAMETER_COMPARISON_TYPES = ( | ||||
|     ('eq', '=='), | ||||
|     ('lte', '<='), | ||||
|     ('gte', '>='), | ||||
| ) | ||||
|  | ||||
| class ComponentParameterSearchForm(forms.Form): | ||||
|     parameter = AutocompleteForeingKeyField(required=True, foreign_model=parts_models.ComponentParameterType, api_search_url='componentparametertype-list', image_field_name=None, name_field_name='parameter_name') | ||||
|     value = forms.CharField(max_length=100, required=False) | ||||
|     compare_method = forms.ChoiceField(choices=PARAMETER_COMPARISON_TYPES, required=True, initial=1) | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         super().__init__(*args, **kwargs) | ||||
|         self.helper = FormHelper() | ||||
|         self.helper.form_tag = False | ||||
|         self.helper.disable_csrf = True | ||||
|         self.helper.layout = Layout( | ||||
|             Row( | ||||
|                 Column('parameter'), | ||||
|                 Column('compare_method'), | ||||
|                 Column('value') | ||||
|             ) | ||||
|         ) | ||||
|   | ||||
| @@ -0,0 +1,59 @@ | ||||
| from django.core.management.base import BaseCommand | ||||
| from parts.models import Component, Package, Distributor, Manufacturer | ||||
| import os | ||||
| import shutil | ||||
|  | ||||
| class Command(BaseCommand): | ||||
| 	help = 'Migrate all media files to the current folder structure' | ||||
|  | ||||
| 	def move_files_of_model(self, queryset): | ||||
| 		for comp in queryset: | ||||
| 			img_path = comp.image.name | ||||
| 			img_path = os.path.normpath(img_path) | ||||
| 			path_components = img_path.split(os.sep) | ||||
|  | ||||
| 			if len(path_components) <= 2: | ||||
| 				self.stdout.write(f'Legacy path found: {img_path}. Will be moved') | ||||
| 				full_path_components = os.path.normpath(comp.image.path).split(os.sep) | ||||
| 				fname = full_path_components[-1] | ||||
| 				path_elem_count = len(full_path_components) | ||||
| 				full_path_components.insert(path_elem_count-1, str(fname[1])) | ||||
| 				full_path_components.insert(path_elem_count-1, str(fname[0])) | ||||
| 				dest_path = os.sep.join(full_path_components) | ||||
|  | ||||
| 				# Move file | ||||
| 				os.makedirs(os.path.dirname(dest_path), exist_ok=True) | ||||
| 				shutil.move(comp.image.path, dest_path) | ||||
|  | ||||
| 				# Update model | ||||
| 				new_rel_path_comps = path_components | ||||
| 				l = len(new_rel_path_comps) | ||||
| 				new_rel_path_comps.insert(l-1, str(fname[1])) | ||||
| 				new_rel_path_comps.insert(l-1, str(fname[0])) | ||||
| 				new_name = os.sep.join(new_rel_path_comps) | ||||
| 				self.stdout.write(f'New location: {dest_path}, new name: {new_name}') | ||||
| 				comp.image.name = new_name | ||||
| 				comp.save() | ||||
|  | ||||
| 	def handle(self, *args, **kwargs): | ||||
|  | ||||
| 		self.stdout.write('Querying components...') | ||||
| 		components = Component.objects.exclude(image='') | ||||
| 		self.stdout.write(f'Count of components with images: {components.count()}'); | ||||
| 		self.move_files_of_model(components) | ||||
|  | ||||
| 		self.stdout.write('Querying packages...') | ||||
| 		pkgs = Package.objects.exclude(image='') | ||||
| 		self.stdout.write(f'Count of components with images: {pkgs.count()}'); | ||||
| 		self.move_files_of_model(pkgs) | ||||
|  | ||||
| 		self.stdout.write('Querying manufacturers...') | ||||
| 		manufacturers = Manufacturer.objects.exclude(image='') | ||||
| 		self.stdout.write(f'Count of components with images: {manufacturers.count()}'); | ||||
| 		self.move_files_of_model(manufacturers) | ||||
|  | ||||
| 		self.stdout.write('Querying distributors...') | ||||
| 		distris = Distributor.objects.exclude(image='') | ||||
| 		self.stdout.write(f'Count of components with images: {distris.count()}'); | ||||
| 		self.move_files_of_model(distris) | ||||
|  | ||||
| @@ -0,0 +1,44 @@ | ||||
| # Generated by Django 3.2.5 on 2022-01-10 18:12 | ||||
|  | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
| import uuid | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('parts', '0010_auto_20220103_1606'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='componenttype', | ||||
|             name='key_parameter1', | ||||
|             field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='type_param1', to='parts.componentparametertype'), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='componenttype', | ||||
|             name='key_parameter2', | ||||
|             field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='type_param2', to='parts.componentparametertype'), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='componenttype', | ||||
|             name='key_parameter3', | ||||
|             field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='type_param3', to='parts.componentparametertype'), | ||||
|         ), | ||||
|         migrations.CreateModel( | ||||
|             name='PackageParameter', | ||||
|             fields=[ | ||||
|                 ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), | ||||
|                 ('value', models.FloatField(default=0)), | ||||
|                 ('text_value', models.TextField(blank=True)), | ||||
|                 ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='parts.package')), | ||||
|                 ('parameter_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='parts.componentparametertype')), | ||||
|             ], | ||||
|             options={ | ||||
|                 'ordering': ['id'], | ||||
|                 'unique_together': {('package', 'parameter_type')}, | ||||
|             }, | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,19 @@ | ||||
| # Generated by Django 3.2.5 on 2022-11-11 20:18 | ||||
|  | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('parts', '0011_auto_20220110_1812'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='componenttype', | ||||
|             name='parent_class', | ||||
|             field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='child_classes', to='parts.componenttype'), | ||||
|         ), | ||||
|     ] | ||||
| @@ -47,11 +47,35 @@ class ComponentType(models.Model): | ||||
| 	class Meta: | ||||
| 		ordering = ['class_name'] | ||||
| 	class_name = models.CharField(max_length=50, unique=True) | ||||
| 	parent_class = models.ForeignKey('self', on_delete=models.PROTECT, related_name='child_classes', null=True, blank=True) | ||||
| 	passive = models.BooleanField() | ||||
| 	possible_parameter = models.ManyToManyField(ComponentParameterType, blank=True) | ||||
|  | ||||
| 	key_parameter1 = models.ForeignKey(ComponentParameterType, on_delete=models.CASCADE, blank=True, null=True, related_name="type_param1") | ||||
| 	key_parameter2 = models.ForeignKey(ComponentParameterType, on_delete=models.CASCADE, blank=True, null=True, related_name="type_param2") | ||||
| 	key_parameter3 = models.ForeignKey(ComponentParameterType, on_delete=models.CASCADE, blank=True, null=True, related_name="type_param3") | ||||
| 	 | ||||
| 	def __str__(self): | ||||
| 		return '[' + self.class_name + ']' | ||||
| 		return self.get_full_path() | ||||
|  | ||||
| 	 | ||||
| 	def get_path_components(self): | ||||
| 		chain = [] | ||||
| 		iterator = self | ||||
| 		chain.append(self) | ||||
| 		while iterator.parent_class is not None: | ||||
| 			chain.append(iterator.parent_class) | ||||
| 			iterator = iterator.parent_class | ||||
|  | ||||
| 		return chain | ||||
|  | ||||
| 	def get_full_path(self): | ||||
| 		output = '' | ||||
| 		 | ||||
| 		chain = self.get_path_components() | ||||
|  | ||||
| 		for i in range(len(chain) - 1, -1, -1): | ||||
| 			output = output + ' / ' + chain[i].class_name | ||||
| 		return output | ||||
|  | ||||
| class Storage(models.Model): | ||||
| 	class Meta: | ||||
| @@ -144,6 +168,37 @@ class Package(models.Model): | ||||
| 	def __str__(self): | ||||
| 		return self.name | ||||
|  | ||||
| class PackageParameter(models.Model): | ||||
| 	class Meta: | ||||
| 		unique_together = ('package', 'parameter_type') | ||||
| 		ordering = ['id'] | ||||
| 	id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True) | ||||
| 	package = models.ForeignKey(Package, on_delete=models.CASCADE)  # A target package is required! | ||||
| 	parameter_type = models.ForeignKey(ComponentParameterType, on_delete=models.CASCADE) | ||||
| 	value = models.FloatField(default=0) | ||||
| 	text_value = models.TextField(null=False, blank=True) | ||||
|  | ||||
| 	def __str__(self): | ||||
| 		if self.parameter_type.parameter_type == 'F': | ||||
| 			value = self.text_value | ||||
| 		else: | ||||
| 			value = str(self.value) | ||||
|  | ||||
| 		return str(self.package)+ ': '+ str(self.parameter_type) + ': ' + value | ||||
|  | ||||
| 	def resolved_value_as_string(self): | ||||
| 		my_type = self.parameter_type.parameter_type | ||||
|  | ||||
| 		if my_type == 'E' or my_type == 'I': | ||||
| 			# Engineering float number | ||||
| 			(num, prefix) = NumConv.number_to_engineering(self.value, it_unit=(True if my_type=='I' else False)) | ||||
| 			return f'{num:.3f} {prefix}{self.parameter_type.unit}' | ||||
| 		elif my_type == 'N': | ||||
| 			# Standard float number | ||||
| 			return  f'{self.value:.3f} {self.parameter_type.unit}' | ||||
| 		elif my_type == 'F': | ||||
| 			return self.text_value | ||||
|  | ||||
|  | ||||
| class Manufacturer(models.Model): | ||||
| 	class Meta: | ||||
| @@ -206,7 +261,34 @@ class Component(models.Model): | ||||
| 			sum = 0 | ||||
| 		return sum | ||||
|  | ||||
| 		 | ||||
| 	def get_key_parameters(self): | ||||
| 		""" | ||||
| 		Get the key parameters of a component defined by its component type. | ||||
| 		Returns a tuple of 3 elements. All three might be None | ||||
| 		""" | ||||
| 		p1 = None | ||||
| 		p2 = None | ||||
| 		p3 = None | ||||
|  | ||||
| 		if self.component_type: | ||||
| 			t = (self.component_type.key_parameter1, self.component_type.key_parameter2, self.component_type.key_parameter3) | ||||
| 			if t[0]: | ||||
| 				p1 = ComponentParameter.objects.filter(component=self, parameter_type=t[0]).first() | ||||
| 			if t[1]: | ||||
| 				p2 = ComponentParameter.objects.filter(component=self, parameter_type=t[1]).first() | ||||
| 			if t[2]: | ||||
| 				p3 = ComponentParameter.objects.filter(component=self, parameter_type=t[2]).first() | ||||
|  | ||||
| 		return (p1, p2, p3) | ||||
| 	 | ||||
| 	def get_key_parameters_as_text(self): | ||||
| 		params = self.get_key_parameters() | ||||
| 		ret_strings = [] | ||||
| 		for p in params: | ||||
| 			if p: | ||||
| 				ret_strings.append(p.resolved_value_as_string()) | ||||
| 		return ret_strings | ||||
|  | ||||
| class ComponentParameter(models.Model): | ||||
| 	class Meta: | ||||
| 		unique_together = ('component', 'parameter_type') | ||||
| @@ -231,10 +313,12 @@ class ComponentParameter(models.Model): | ||||
| 		if my_type == 'E' or my_type == 'I': | ||||
| 			# Engineering float number | ||||
| 			(num, prefix) = NumConv.number_to_engineering(self.value, it_unit=(True if my_type=='I' else False)) | ||||
| 			return f'{num:.3f} {prefix}{self.parameter_type.unit}' | ||||
| 			num = round(num, 3) | ||||
| 			return f'{num} {prefix}{self.parameter_type.unit}' | ||||
| 		elif my_type == 'N': | ||||
| 			# Standard float number | ||||
| 			return  f'{self.value:.3f} {self.parameter_type.unit}' | ||||
| 			num = round(self.value, 3) | ||||
| 			return  f'{num} {self.parameter_type.unit}' | ||||
| 		elif my_type == 'F': | ||||
| 			return self.text_value | ||||
|  | ||||
|   | ||||
| @@ -4,6 +4,7 @@ from . import views as parts_views | ||||
| urlpatterns = [ | ||||
|     path('', parts_views.MainView.as_view(), name='parts-main'), | ||||
|     path('components/', parts_views.ComponentView.as_view(), name='parts-components'), | ||||
|     path('componenttypes/', parts_views.ComponentTypeView.as_view(), name='parts-componenttypes'), | ||||
|     path('packages/', parts_views.PackageView.as_view(), name='parts-packages'), | ||||
|     path('distributors/', parts_views.DistributorView.as_view(), name='parts-distributors'), | ||||
|     path('stocks/', parts_views.StockView.as_view(), name='parts-stocks'), | ||||
| @@ -16,4 +17,5 @@ urlpatterns = [ | ||||
|     path('distributors/<slug:uuid>/', parts_views.DistributorDetailView.as_view(), name='parts-distributors-detail'), | ||||
|     path('manufacturers/', parts_views.ManufacturersViewSet.as_view(), name='parts-manufacturers'), | ||||
|     path("manufacturers/<slug:uuid>/", parts_views.ManufacturerDetailViewSet.as_view(), name='parts-manufacturers-detail'), | ||||
|     path("componenttypes/<slug:uuid>/", parts_views.ComponentTypeDetailView.as_view(), name='parts-componenttypes-detail'),     | ||||
| ] | ||||
|   | ||||
| @@ -2,6 +2,7 @@ from django.shortcuts import render, redirect | ||||
| from django.urls import resolve, reverse | ||||
| from django.contrib.auth import logout, login | ||||
| from django.contrib.auth.models import User | ||||
| from django.utils.http import urlencode | ||||
| from django.http import HttpResponse | ||||
| from .navbar import NavBar | ||||
| from django.contrib.auth.forms import AuthenticationForm as AuthForm | ||||
| @@ -11,7 +12,9 @@ from django.views import View | ||||
| import django.forms as forms | ||||
| from django.views.generic import TemplateView, DetailView | ||||
| from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin | ||||
| from .models import Storage, Stock, Component, Distributor, Manufacturer, Package, ComponentParameter, ComponentParameterType, DistributorNum | ||||
| from .models import Storage, Stock, Component, Distributor, Manufacturer, Package | ||||
| from .models import ComponentParameter, ComponentParameterType, DistributorNum, PackageParameter | ||||
| from .models import ComponentType | ||||
| from .qr_parser import QrCodeValidator | ||||
| from django.core.paginator import Paginator | ||||
| from django.core.exceptions import ValidationError | ||||
| @@ -19,11 +22,12 @@ from django.db import IntegrityError | ||||
| from django.db.models import ProtectedError | ||||
| from .forms import * | ||||
| from django.db.models import Q | ||||
| from django.db.models import Prefetch | ||||
| from django.db.models.functions import Lower | ||||
| from django.forms import formset_factory | ||||
| import uuid | ||||
|  | ||||
| ParameterSearchFormSet = formset_factory(ComponentParameterSearchForm, extra=2) | ||||
| ParameterSearchFormSet = formset_factory(ComponentParameterSearchForm, extra=0) | ||||
|  | ||||
| class QrSearchForm(forms.Form): | ||||
|     my_qr_validator = QrCodeValidator() | ||||
| @@ -34,6 +38,15 @@ class QrSearchForm(forms.Form): | ||||
|  | ||||
|     qr_search = forms.CharField(label='qr_search', validators=[my_qr_validator]) | ||||
|  | ||||
| class KeepSearchParamMixin(object): | ||||
|     def get_context_data(self, **kwargs): | ||||
|         context = super().get_context_data(**kwargs) | ||||
|         search = self.request.GET.get('search', default=None) | ||||
|         if search: | ||||
|             context['additional_params'] = urlencode({'search': search}) | ||||
|         return context | ||||
|      | ||||
|  | ||||
| class BaseTemplateMixin(object): | ||||
|     navbar_selected = '' | ||||
|     base_title = '' | ||||
| @@ -142,26 +155,78 @@ def login_view(request): | ||||
|  | ||||
| # Create your views here. | ||||
|  | ||||
| class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView): | ||||
| class ComponentTypeView(LoginRequiredMixin, BaseTemplateMixin, TemplateView): | ||||
|     template_name = 'parts/component-types.html' | ||||
|     base_title = 'Component Types' | ||||
|     default_page_size = 25 | ||||
|  | ||||
|     def filter_queryset(self, queryset, search_string): | ||||
|         if search_string is None or search_string == '': | ||||
|             return queryset | ||||
|  | ||||
|         search_fragments = search_string.strip().split() | ||||
|         for search in search_fragments: | ||||
|             queryset = queryset.filter(Q(class_name__icontains = search)) | ||||
|  | ||||
|         return queryset | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         context = super().get_context_data(**kwargs) | ||||
|         search = self.request.GET.get('search', default=None) | ||||
|         page_num = self.request.GET.get('page', default=1) | ||||
|  | ||||
|         context['search_string'] = search | ||||
|  | ||||
|         queryset = ComponentType.objects.all()  | ||||
|         types = self.filter_queryset(queryset, search) | ||||
|          | ||||
|         comptypes = Paginator(types, self.default_page_size) | ||||
|          | ||||
|  | ||||
|         context['comptypes'] = comptypes.get_page(page_num) | ||||
|         return context | ||||
|      | ||||
|  | ||||
|     def post(self, request, *args, **kwargs): | ||||
|         return super().post(request, *args, **kwargs) | ||||
|  | ||||
| class ComponentTypeDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView): | ||||
|     model = ComponentType | ||||
|     base_title = '' | ||||
|     pk_url_kwarg = 'uuid' | ||||
|     template_name = 'parts/component-types-detail.html' | ||||
|  | ||||
|     def get_breadcrumbs(self): | ||||
|         crumbs = self.object.get_path_components() | ||||
|         # Reverse list and drop the last element of the reversed list | ||||
|         crumbs = crumbs[::-1][:-1] | ||||
|         return crumbs | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         context = super().get_context_data(**kwargs) | ||||
|         context['breadcrumbs'] = self.get_breadcrumbs() | ||||
|         return context | ||||
|      | ||||
|  | ||||
| class ComponentView(LoginRequiredMixin, KeepSearchParamMixin, BaseTemplateMixin, TemplateView): | ||||
|     template_name = 'parts/components.html' | ||||
|     base_title = 'Components' | ||||
|     navbar_selected = 'Components' | ||||
|     default_page_size = 25 | ||||
|  | ||||
|     def get_component_query_set(self, search_string): | ||||
|         queryset = Component.objects.all() | ||||
|         queryset = Component.objects.select_related('package', 'manufacturer', 'component_type').prefetch_related('componentparameter_set').all() | ||||
|          | ||||
|         if not search_string: | ||||
|         if search_string is None or search_string == '': | ||||
|             return queryset | ||||
|  | ||||
|         search_fragments = search_string.strip().split() | ||||
|         for search in search_fragments: | ||||
|             queryset = queryset.filter(Q(name__icontains = search) | Q(manufacturer__name__icontains = search) | Q(package__name__icontains = search)) | ||||
|  | ||||
|         return queryset | ||||
|  | ||||
|     def get_component_queryset_from_advanced_search(self, cleaned_data): | ||||
|         queryset = Component.objects.all() | ||||
|         queryset = Component.objects.select_related('manufacturer', 'package', 'component_type').prefetch_related('componentparameter_set').all() | ||||
|  | ||||
|         if cleaned_data['name']: | ||||
|             queryset = queryset.filter(Q(name__icontains=cleaned_data['name'])) | ||||
| @@ -181,15 +246,19 @@ class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView): | ||||
|             queryset = queryset.filter(manufacturer=cleaned_data['manufacturer']) | ||||
|         return queryset | ||||
|  | ||||
|     def filter_queryset_with_parameters(self, queryset, parameter, value): | ||||
|     def filter_queryset_with_parameters(self, queryset, parameter, value, compare_method): | ||||
|         if parameter and (value is None or value == ''): | ||||
|             return queryset.filter(Q(componentparameter__parameter_type=parameter)) | ||||
|         elif parameter and value is not None: | ||||
|             if parameter.parameter_type == 'F': | ||||
|                 return queryset.filter(Q(componentparameter__text_value__icontains=value)) | ||||
|                 return queryset.filter(Q(componentparameter__text_value__icontains=value) & Q(componentparameter__parameter_type=parameter)) | ||||
|             else: | ||||
|                 return queryset.filter(Q(componentparameter__value=value)) | ||||
|  | ||||
|                 if compare_method == 'lte': # <= | ||||
|                     return queryset.filter(Q(componentparameter__value__lte=value) & Q(componentparameter__parameter_type=parameter)) | ||||
|                 elif compare_method == 'gte': # >= | ||||
|                     return queryset.filter(Q(componentparameter__value__gte=value) & Q(componentparameter__parameter_type=parameter)) | ||||
|                 else: | ||||
|                     return queryset.filter(Q(componentparameter__value=value) & Q(componentparameter__parameter_type=parameter)) | ||||
|         return queryset | ||||
|  | ||||
|     def get_context_data_int(self, advanced_search, parameter_formset : ParameterSearchFormSet, **kwargs): | ||||
| @@ -205,15 +274,13 @@ class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView): | ||||
|             if advanced_search.is_valid(): | ||||
|                 paginator_queryset = self.get_component_queryset_from_advanced_search(advanced_search.cleaned_data) | ||||
|             else: | ||||
|                 paginator_queryset = Component.objects.all() | ||||
|                 paginator_queryset = self.get_component_query_set(None) | ||||
|  | ||||
|             if parameter_formset.is_valid(): | ||||
|                 # Process parameters | ||||
|                 for f in parameter_formset: | ||||
|                     # If the form is valid and has changed compared to its initial empty state | ||||
|                     if f.is_valid() and f.has_changed(): | ||||
|                         print(f.cleaned_data) | ||||
|                         paginator_queryset = self.filter_queryset_with_parameters(paginator_queryset, f.cleaned_data['parameter'], f.cleaned_data['value']) | ||||
|             # Process parameters | ||||
|             for f in parameter_formset: | ||||
|                 # If the form is valid and has changed compared to its initial empty state | ||||
|                 if f.is_valid() and f.has_changed(): | ||||
|                     paginator_queryset = self.filter_queryset_with_parameters(paginator_queryset, f.cleaned_data['parameter'], f.cleaned_data['value'], f.cleaned_data['compare_method']) | ||||
|                          | ||||
|              | ||||
|         else: | ||||
| @@ -228,15 +295,24 @@ class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView): | ||||
|  | ||||
|         if not parameter_formset: | ||||
|             context['advanced_search_param_formset'] = ParameterSearchFormSet() | ||||
|  | ||||
|  | ||||
|         if not advanced_search: | ||||
|             context['advanced_search_form'] = AdvancedComponentSearchForm(auto_id='adv_search_%s') | ||||
|  | ||||
|         return context | ||||
|      | ||||
|     def get_context_data(self, **kwargs): | ||||
|         return self.get_context_data_int(advanced_search = None, parameter_formset=None, **kwargs)     | ||||
|         adv_search_form = None | ||||
|         adv_param_search_formset = None | ||||
|         if 'submit-advanced-search' in self.request.GET: | ||||
|             adv_search_form = AdvancedComponentSearchForm(auto_id='adv_search_%s', data=self.request.GET) | ||||
|             adv_param_search_formset = ParameterSearchFormSet(data=self.request.GET) | ||||
|             if adv_search_form.is_valid(): | ||||
|                 pass | ||||
|             if adv_param_search_formset.is_valid(): | ||||
|                 pass | ||||
|              | ||||
|  | ||||
|         return self.get_context_data_int(advanced_search = adv_search_form, parameter_formset=adv_param_search_formset, **kwargs)     | ||||
|      | ||||
|  | ||||
|     def handle_new_component_post(self, request, open=False, **kwargs): | ||||
| @@ -252,33 +328,16 @@ class ComponentView(LoginRequiredMixin, BaseTemplateMixin, TemplateView): | ||||
|         if open and new_component: | ||||
|             return redirect(reverse('parts-components-detail', kwargs={'uuid':new_component.id})) | ||||
|         return self.render_to_response(context) | ||||
|      | ||||
|     def handle_advanced_search_post(self, request, **kwargs): | ||||
|          | ||||
|         form = AdvancedComponentSearchForm(auto_id='adv_search_%s', data=request.POST) | ||||
|         param_formset = ParameterSearchFormSet(data=request.POST) | ||||
|  | ||||
|         if form.is_valid(): | ||||
|             print('Valid') | ||||
|  | ||||
|         if param_formset.is_valid(): | ||||
|             print('Formset is valid!') | ||||
|  | ||||
|  | ||||
|         context = self.get_context_data_int(form, param_formset, **kwargs) | ||||
|         return self.render_to_response(context) | ||||
|  | ||||
|     def post(self, request, *args, **kwargs): | ||||
|         if 'submit-edit-component' in request.POST: | ||||
|             return self.handle_new_component_post(request, open=False, **kwargs) | ||||
|         elif 'submit-edit-component-open' in request.POST: | ||||
|             return self.handle_new_component_post(request, open=True, **kwargs) | ||||
|         elif 'submit-advanced-search' in request.POST: | ||||
|             return self.handle_advanced_search_post(request, **kwargs) | ||||
|         else: | ||||
|             return super().post(request, *args, **kwargs) | ||||
|      | ||||
| class PackageView(LoginRequiredMixin, BaseTemplateMixin, TemplateView): | ||||
| class PackageView(LoginRequiredMixin, KeepSearchParamMixin, BaseTemplateMixin, TemplateView): | ||||
|     template_name = 'parts/packages.html' | ||||
|     base_title = 'Packages' | ||||
|     navbar_selected = 'Packages' | ||||
| @@ -340,7 +399,7 @@ class PackageView(LoginRequiredMixin, BaseTemplateMixin, TemplateView): | ||||
|         return super().post(request, *args, **kwargs) | ||||
|      | ||||
|  | ||||
| class DistributorView(LoginRequiredMixin, BaseTemplateMixin, TemplateView): | ||||
| class DistributorView(LoginRequiredMixin, KeepSearchParamMixin, BaseTemplateMixin, TemplateView): | ||||
|     template_name = 'parts/distributors.html' | ||||
|     base_title = 'Distributors' | ||||
|     navbar_selected = 'Distributors' | ||||
| @@ -454,7 +513,7 @@ class StockView(LoginRequiredMixin, BaseTemplateMixin, TemplateView): | ||||
|  | ||||
|         return super().post(request, **kwargs) | ||||
|      | ||||
| class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView): | ||||
| class StockViewDetail(LoginRequiredMixin, KeepSearchParamMixin, BaseTemplateMixin, DetailView): | ||||
|     template_name = 'parts/stocks-detail.html' | ||||
|     model = Storage | ||||
|     pk_url_kwarg = 'uuid' | ||||
| @@ -516,7 +575,6 @@ class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView): | ||||
|         context['add_storage_form'] = add_storage_form | ||||
|         context['delete_storage_error'] = None | ||||
|         context['add_stock_form'] = AddStockForm() | ||||
|  | ||||
|         return context | ||||
|  | ||||
|     def handle_add_storage_post(self, request, **kwargs): | ||||
| @@ -627,7 +685,7 @@ class StockViewDetail(LoginRequiredMixin, BaseTemplateMixin, DetailView): | ||||
|  | ||||
| class ComponentDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView): | ||||
|     template_name = 'parts/components-detail.html' | ||||
|     model = Component | ||||
|     queryset = Component.objects.select_related('component_type', 'package', 'manufacturer').prefetch_related('componentparameter_set', 'distributornum_set') | ||||
|     pk_url_kwarg = 'uuid' | ||||
|     base_title = '' | ||||
|     navbar_selected = 'Components' | ||||
| @@ -640,8 +698,13 @@ class ComponentDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView): | ||||
|         context['comp_form'] = ComponentForm(instance=self.object) | ||||
|         context['new_distri_num_form'] = DistributorNumberCreateForm() | ||||
|         context['new_param_form'] = ComponentParameterCreateForm() | ||||
|         context['distri_nums'] = DistributorNum.objects.filter(component=self.object).order_by('distributor__name') | ||||
|         context['parameters'] = ComponentParameter.objects.filter(component=self.object).order_by('parameter_type__parameter_name') | ||||
|         context['distri_nums'] = self.object.distributornum_set.select_related('distributor').order_by('distributor__name') | ||||
|         context['parameters'] = self.object.componentparameter_set.order_by('parameter_type__parameter_name') | ||||
|         if self.object.package: | ||||
|             context['package_parameters'] = PackageParameter.objects.filter(package=self.object.package).order_by('parameter_type__parameter_name') | ||||
|          | ||||
|         parameter_texts = self.object.get_key_parameters_as_text() | ||||
|         context['key_parameter_string'] = ', '.join(parameter_texts) | ||||
|  | ||||
|         return context | ||||
|  | ||||
| @@ -851,7 +914,7 @@ class DistributorDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView): | ||||
|          | ||||
|         return super().post(request, *args, **kwargs) | ||||
|  | ||||
| class ManufacturersViewSet(LoginRequiredMixin, BaseTemplateMixin, TemplateView): | ||||
| class ManufacturersViewSet(LoginRequiredMixin, KeepSearchParamMixin, BaseTemplateMixin, TemplateView): | ||||
|     template_name = 'parts/manufacturers.html' | ||||
|     base_title = 'Manufacturers' | ||||
|     navbar_selected = 'Manufacturers' | ||||
|   | ||||
| @@ -8,6 +8,23 @@ https://docs.djangoproject.com/en/3.2/topics/settings/ | ||||
|  | ||||
| For the full list of settings and their values, see | ||||
| https://docs.djangoproject.com/en/3.2/ref/settings/ | ||||
|  | ||||
| Development mode is selected by setting env variable | ||||
| - DJANGO_DEV_MODE | ||||
|  | ||||
| The following environment variables have to be set for Production Mode but might be optional for dev mode: | ||||
| - DJANGO_SECRET_KEY | ||||
| - DJANGO_ALLOWED_HOST | ||||
| - DJANGO_STATIC_ROOT | ||||
| - DJANGO_MEDIA_URL | ||||
| - DJANGO_MEDIA_ROOT | ||||
| - DJANGO_POSTGRESQL_SOCKET | ||||
|  | ||||
|  | ||||
|  | ||||
| The following can be set | ||||
| - DJANGO_SECURE_HSTS_SECONDS (defaults to 120) | ||||
|  | ||||
| """ | ||||
|  | ||||
| from pathlib import Path | ||||
| @@ -16,17 +33,34 @@ import os | ||||
| # Build paths inside the project like this: BASE_DIR / 'subdir'. | ||||
| BASE_DIR = Path(__file__).resolve().parent.parent | ||||
|  | ||||
| def get_env_value(env_variable, default=None): | ||||
|     try: | ||||
|         return os.environ[env_variable] | ||||
|     except KeyError: | ||||
|         if default is not None: | ||||
|             return default | ||||
|         error_msg = 'Set the {} environment variable'.format(env_variable) | ||||
|         raise Exception(error_msg) | ||||
|  | ||||
| RUNS_IN_DEV_MODE = True if get_env_value('DJANGO_DEV_MODE', default=False) != False else False | ||||
|  | ||||
| # Quick-start development settings - unsuitable for production | ||||
| # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ | ||||
|  | ||||
| # SECURITY WARNING: keep the secret key used in production secret! | ||||
| SECRET_KEY = 'django-insecure-vq_@ue3ul@&4bz7wkcpf3pjrwf8o$7g!z-rw$ftr-$)7l3*m=^' | ||||
| SECRET_KEY='' | ||||
| if RUNS_IN_DEV_MODE: | ||||
|     SECRET_KEY = get_env_value('DJANGO_SECRET_KEY', | ||||
|         default='django-insecure-vq_@ue3ul@&4bz7wkcpf3pjrwf8o$7g!z-rw$ftr-$)7l3*m=^') | ||||
| else: | ||||
|     SECRET_KEY = get_env_value('DJANGO_SECRET_KEY') | ||||
|  | ||||
| # SECURITY WARNING: don't run with debug turned on in production! | ||||
| DEBUG = True | ||||
| DEBUG = True if RUNS_IN_DEV_MODE else False | ||||
|  | ||||
| ALLOWED_HOSTS = ['localhost'] | ||||
| if not RUNS_IN_DEV_MODE: | ||||
|     ALLOWED_HOSTS = ['localhost', get_env_value('DJANGO_ALLOWED_HOST')] | ||||
|  | ||||
|  | ||||
| # Application definition | ||||
| @@ -96,10 +130,34 @@ WSGI_APPLICATION = 'shimatta_kenkyusho.wsgi.application' | ||||
| # Database | ||||
| # https://docs.djangoproject.com/en/3.2/ref/settings/#databases | ||||
|  | ||||
| DATABASES = { | ||||
| DATABASES = {} | ||||
|  | ||||
| if RUNS_IN_DEV_MODE: | ||||
|     DATABASES = { | ||||
|         'default': { | ||||
|             'ENGINE': 'django.db.backends.sqlite3', | ||||
|             'NAME': BASE_DIR / 'db.sqlite3', | ||||
|         } | ||||
|     } | ||||
| else: | ||||
|     b_pw = '' | ||||
|     try: | ||||
|         db_pw = get_env_value('DJANGO_POSTGRESQL_PW') | ||||
|     except: | ||||
|         pass | ||||
|  | ||||
|     db_user = '' | ||||
|     try: | ||||
|         db_user = get_env_value('DJANGO_POSTGRESQL_USER') | ||||
|     except: | ||||
| 	    pass | ||||
|     DATABASES = { | ||||
|     'default': { | ||||
|         'ENGINE': 'django.db.backends.sqlite3', | ||||
|         'NAME': BASE_DIR / 'db.sqlite3', | ||||
|         'ENGINE': 'django.db.backends.postgresql', | ||||
|         'NAME': 'shimatta_kenkyusho', | ||||
|         'USER': db_user, | ||||
|         'PASSWORD': db_pw, | ||||
|         'HOST': get_env_value('DJANGO_POSTGRESQL_SOCKET'), | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -167,12 +225,17 @@ STATICFILES_DIRS = [ | ||||
|     os.path.join(BASE_DIR, "static"), | ||||
| ] | ||||
|  | ||||
| STATIC_ROOT = None | ||||
| if not RUNS_IN_DEV_MODE: | ||||
|     STATIC_ROOT = get_env_value('DJANGO_STATIC_ROOT') | ||||
|  | ||||
| # Default primary key field type | ||||
| # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field | ||||
|  | ||||
| DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' | ||||
| MEDIA_URL = '/media/' | ||||
| MEDIA_ROOT = os.path.join(BASE_DIR, "media") | ||||
|  | ||||
| MEDIA_URL = get_env_value('DJANGO_MEDIA_URL', default='/media/') | ||||
| MEDIA_ROOT = get_env_value('DJANGO_MEDIA_ROOT', default=os.path.join(BASE_DIR, "media")) | ||||
|  | ||||
| LOGIN_URL = '/login' | ||||
| LOGIN_REDIRECT_URL = '/' | ||||
| @@ -182,4 +245,17 @@ SHIMATTA_KENKYUSHO_TITLE = 'しまった・研究所' | ||||
|  | ||||
| CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" | ||||
|  | ||||
| CRISPY_TEMPLATE_PACK = "bootstrap5" | ||||
| CRISPY_TEMPLATE_PACK = "bootstrap5" | ||||
|  | ||||
| # Production only settings | ||||
| SESSION_COOKIE_SECURE = True | ||||
| CSRF_COOKIE_SECURE = True | ||||
|  | ||||
| SECURE_SSL_REDIRECT = True | ||||
|  | ||||
| SECURE_HSTS_SECONDS = get_env_value('DJANGO_SECURE_HSTS_SECONDS', default=120) | ||||
|  | ||||
| if RUNS_IN_DEV_MODE: | ||||
|     SESSION_COOKIE_SECURE = False | ||||
|     CSRF_COOKIE_SECURE = False | ||||
|     SECURE_SSL_REDIRECT = False | ||||
| @@ -1,234 +0,0 @@ | ||||
| """ | ||||
| Django settings for shimatta_kenkyusho project. | ||||
|  | ||||
| Generated by 'django-admin startproject' using Django 3.2.5. | ||||
|  | ||||
| For more information on this file, see | ||||
| https://docs.djangoproject.com/en/3.2/topics/settings/ | ||||
|  | ||||
| For the full list of settings and their values, see | ||||
| https://docs.djangoproject.com/en/3.2/ref/settings/ | ||||
|  | ||||
| The following environment variables have to be set: | ||||
| - DJANGO_SECRET_KEY | ||||
| - DJANGO_ALLOWED_HOST | ||||
| - DJANGO_STATIC_ROOT | ||||
| - DJANGO_MEDIA_URL | ||||
| - DJANGO_MEDIA_ROOT | ||||
| - DJANGO_POSTGRESQL_SOCKET | ||||
|  | ||||
| The following can be set | ||||
| - DJANGO_SECURE_HSTS_SECONDS (defaults to 120) | ||||
|  | ||||
| """ | ||||
|  | ||||
| from pathlib import Path | ||||
| import os | ||||
|  | ||||
| # Build paths inside the project like this: BASE_DIR / 'subdir'. | ||||
| BASE_DIR = Path(__file__).resolve().parent.parent | ||||
|  | ||||
| def get_env_value(env_variable, default=None): | ||||
|     try: | ||||
|         return os.environ[env_variable] | ||||
|     except KeyError: | ||||
|         if default is not None: | ||||
|             return default | ||||
|         error_msg = 'Set the {} environment variable'.format(env_variable) | ||||
|         raise Exception(error_msg) | ||||
|  | ||||
|  | ||||
| # Quick-start development settings - unsuitable for production | ||||
| # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ | ||||
|  | ||||
| # SECURITY WARNING: keep the secret key used in production secret! | ||||
| SECRET_KEY = get_env_value('DJANGO_SECRET_KEY') | ||||
|  | ||||
| # SECURITY WARNING: don't run with debug turned on in production! | ||||
| DEBUG = False | ||||
|  | ||||
| ALLOWED_HOSTS = ['localhost', get_env_value('DJANGO_ALLOWED_HOST')] | ||||
|  | ||||
|  | ||||
| # Application definition | ||||
|  | ||||
| INSTALLED_APPS = [ | ||||
|     'django.contrib.admin', | ||||
|     'django.contrib.auth', | ||||
|     'django.contrib.contenttypes', | ||||
|     'django.contrib.sessions', | ||||
|     'django.contrib.messages', | ||||
|     'django.contrib.staticfiles', | ||||
|     'parts.apps.PartsConfig', | ||||
|     'api.apps.ApiConfig', | ||||
|     'rest_framework.authtoken', | ||||
|     'django_filters', | ||||
|     'qr_code', | ||||
|     'rest_framework', | ||||
|     'crispy_forms', | ||||
|     'crispy_bootstrap5', | ||||
|     'django.forms', | ||||
| ] | ||||
|  | ||||
|  | ||||
| FORM_RENDERER = 'django.forms.renderers.TemplatesSetting' | ||||
|  | ||||
| MIDDLEWARE = [ | ||||
|     'django.middleware.security.SecurityMiddleware', | ||||
|     'django.contrib.sessions.middleware.SessionMiddleware', | ||||
|     'django.middleware.common.CommonMiddleware', | ||||
|     'django.middleware.csrf.CsrfViewMiddleware', | ||||
|     'django.contrib.auth.middleware.AuthenticationMiddleware', | ||||
|     'django.contrib.messages.middleware.MessageMiddleware', | ||||
|     'django.middleware.clickjacking.XFrameOptionsMiddleware', | ||||
| ] | ||||
|  | ||||
| ROOT_URLCONF = 'shimatta_kenkyusho.urls' | ||||
|  | ||||
| CACHES = { | ||||
| 	'default': { | ||||
| 		'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', | ||||
| 	}, | ||||
| 	'qr-code': { | ||||
| 		'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', | ||||
| 		'LOCATION': 'qr-code-cache', | ||||
| 		'TIMEOUT': 3600 | ||||
| 	} | ||||
| } | ||||
|  | ||||
| TEMPLATES = [ | ||||
|     { | ||||
|         'BACKEND': 'django.template.backends.django.DjangoTemplates', | ||||
|         'DIRS': [os.path.join(BASE_DIR, 'templates'),], | ||||
|         'APP_DIRS': True, | ||||
|         'OPTIONS': { | ||||
|             'context_processors': [ | ||||
|                 'django.template.context_processors.debug', | ||||
|                 'django.template.context_processors.request', | ||||
|                 'django.contrib.auth.context_processors.auth', | ||||
|                 'django.contrib.messages.context_processors.messages', | ||||
|             ], | ||||
|         }, | ||||
|     }, | ||||
| ] | ||||
|  | ||||
| WSGI_APPLICATION = 'shimatta_kenkyusho.wsgi.application' | ||||
|  | ||||
|  | ||||
| # Database | ||||
| # https://docs.djangoproject.com/en/3.2/ref/settings/#databases | ||||
|  | ||||
| db_pw = '' | ||||
| try: | ||||
| 	db_pw = get_env_value('DJANGO_POSTGRESQL_PW') | ||||
| except: | ||||
| 	pass | ||||
|  | ||||
| db_user = '' | ||||
| try: | ||||
| 	db_user = get_env_value('DJANGO_POSTGRESQL_USER') | ||||
| except: | ||||
| 	pass | ||||
|  | ||||
|  | ||||
| DATABASES = { | ||||
|     'default': { | ||||
|         'ENGINE': 'django.db.backends.postgresql', | ||||
|         'NAME': 'shimatta_kenkyusho', | ||||
|         'USER': db_user, | ||||
|         'PASSWORD': db_pw, | ||||
|         'HOST': get_env_value('DJANGO_POSTGRESQL_SOCKET'), | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| # Password validation | ||||
| # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators | ||||
|  | ||||
| AUTH_PASSWORD_VALIDATORS = [ | ||||
|     { | ||||
|         'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', | ||||
|     }, | ||||
|     { | ||||
|         'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', | ||||
|     }, | ||||
|     { | ||||
|         'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', | ||||
|     }, | ||||
|     { | ||||
|         'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', | ||||
|     }, | ||||
| ] | ||||
|  | ||||
| REST_FRAMEWORK = { | ||||
| 	'DEFAULT_AUTHENTICATION_CLASSES': [ | ||||
| 		'rest_framework.authentication.BasicAuthentication', | ||||
| 		'rest_framework.authentication.SessionAuthentication', | ||||
| 		'api.ExpiringAuthToken.ExpiringTokenAuthentication', | ||||
| 	], | ||||
| 	'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', | ||||
| 	'PAGE_SIZE': 10, | ||||
| 	'DEFAULT_THROTTLE_CLASSES': [ | ||||
| 		'rest_framework.throttling.AnonRateThrottle', | ||||
| 		'rest_framework.throttling.UserRateThrottle' | ||||
| 	], | ||||
| 	'DEFAULT_THROTTLE_RATES': { | ||||
| 		'anon': '100/hour', | ||||
| 		'user': '2000/hour' | ||||
| 	} | ||||
|  | ||||
| } | ||||
|  | ||||
| REST_FRAMEWORK_TOKEN_EXPIRE_HOURS = 4 | ||||
|  | ||||
|  | ||||
| # Internationalization | ||||
| # https://docs.djangoproject.com/en/3.2/topics/i18n/ | ||||
|  | ||||
| LANGUAGE_CODE = 'en-us' | ||||
|  | ||||
| TIME_ZONE = 'UTC' | ||||
|  | ||||
| USE_I18N = True | ||||
|  | ||||
| USE_L10N = True | ||||
|  | ||||
| USE_TZ = True | ||||
|  | ||||
|  | ||||
| # Static files (CSS, JavaScript, Images) | ||||
| # https://docs.djangoproject.com/en/3.2/howto/static-files/ | ||||
|  | ||||
| STATIC_URL = '/static/' | ||||
|  | ||||
| STATICFILES_DIRS = [ | ||||
|     os.path.join(BASE_DIR, "static"), | ||||
| ] | ||||
|  | ||||
| STATIC_ROOT = get_env_value('DJANGO_STATIC_ROOT') | ||||
|  | ||||
| # Default primary key field type | ||||
| # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field | ||||
|  | ||||
| DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' | ||||
| MEDIA_URL = get_env_value('DJANGO_MEDIA_URL') | ||||
| MEDIA_ROOT = get_env_value('DJANGO_MEDIA_ROOT') | ||||
|  | ||||
| LOGIN_URL = '/login' | ||||
| LOGIN_REDIRECT_URL = '/' | ||||
|  | ||||
|  | ||||
| SHIMATTA_KENKYUSHO_TITLE = 'しまった・研究所' | ||||
|  | ||||
| CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" | ||||
|  | ||||
| CRISPY_TEMPLATE_PACK = "bootstrap5" | ||||
|  | ||||
|  | ||||
| # Production only settings | ||||
| SESSION_COOKIE_SECURE = True | ||||
| CSRF_COOKIE_SECURE = True | ||||
|  | ||||
| SECURE_SSL_REDIRECT = True | ||||
|  | ||||
| SECURE_HSTS_SECONDS = get_env_value('DJANGO_SECURE_HSTS_SECONDS', default=120) | ||||
| @@ -14,14 +14,13 @@ Including another URLconf | ||||
|     2. Add a URL to urlpatterns:  path('blog/', include('blog.urls')) | ||||
| """ | ||||
| from django.contrib import admin | ||||
| from django.urls import path, include | ||||
| from django.conf.urls import url | ||||
| from django.urls import path, include, re_path | ||||
| from django.conf.urls.static import static | ||||
| from django.conf import settings | ||||
| from parts import views as parts_views | ||||
|  | ||||
| urlpatterns = [ | ||||
|     url(r'^admin/login/', parts_views.login_view), | ||||
|     re_path(r'^admin/login/', parts_views.login_view), | ||||
|     path('admin/', admin.site.urls), | ||||
|     path('api/v1/', include('api.urls'), name='api-root'), | ||||
|     path('', include('parts.urls')), | ||||
|   | ||||
| @@ -11,6 +11,6 @@ import os | ||||
|  | ||||
| from django.core.wsgi import get_wsgi_application | ||||
|  | ||||
| os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'shimatta_kenkyusho.settings_production') | ||||
| os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'shimatta_kenkyusho.settings') | ||||
|  | ||||
| application = get_wsgi_application() | ||||
|   | ||||
| @@ -1,67 +1,67 @@ | ||||
|  | ||||
| class EngineeringNumberConverter(): | ||||
|  | ||||
|     prefixes = [ | ||||
|         ('y', 1e-24), | ||||
|         ('z', 1e-21), | ||||
|         ('a', 1e-18), | ||||
|         ('f', 1e-15), | ||||
|         ('p', 1e-12), | ||||
|         ('n', 1e-9), | ||||
|         ('u', 1e-6), | ||||
|         ('m', 1e-3), | ||||
|         # We skip centi and dezi because no one really uses these besides for length measurements | ||||
|         ('', 1), | ||||
|         # We also skip h for hekto | ||||
|         ('k', 1e3), | ||||
|         ('M', 1e6), | ||||
|         ('G', 1e9), | ||||
|         ('T', 1e12), | ||||
|         ('P', 1e15), | ||||
|         ('E', 1e18), | ||||
|         ('Z', 1e21), | ||||
|         ('Y', 1e24), | ||||
|     ] | ||||
|     it_prefixes = [ | ||||
|         ('', 1), | ||||
|         ('Ki', 1024), | ||||
|         ('Mi', 1024*1024), | ||||
|         ('Gi', 1024*1024*1024), | ||||
|         ('Ti', 1024*1024*1024*1024) | ||||
|     ] | ||||
|      | ||||
|     @classmethod | ||||
|     def number_to_engineering(c, number, it_unit = False): | ||||
|         """ | ||||
|         Convert a number to engineering SI syntax with prefix. | ||||
|         This function will return a tuple of (new_number, prefix) | ||||
|         """ | ||||
|         if it_unit: | ||||
|             used_prefixes = c.it_prefixes | ||||
|         else: | ||||
|             used_prefixes = c.prefixes | ||||
| 	prefixes = [ | ||||
| 		('y', 1e-24), | ||||
| 		('z', 1e-21), | ||||
| 		('a', 1e-18), | ||||
| 		('f', 1e-15), | ||||
| 		('p', 1e-12), | ||||
| 		('n', 1e-9), | ||||
| 		('u', 1e-6), | ||||
| 		('m', 1e-3), | ||||
| 		# We skip centi and dezi because no one really uses these besides for length measurements | ||||
| 		('', 1), | ||||
| 		# We also skip h for hekto | ||||
| 		('k', 1e3), | ||||
| 		('M', 1e6), | ||||
| 		('G', 1e9), | ||||
| 		('T', 1e12), | ||||
| 		('P', 1e15), | ||||
| 		('E', 1e18), | ||||
| 		('Z', 1e21), | ||||
| 		('Y', 1e24), | ||||
| 	] | ||||
| 	it_prefixes = [ | ||||
| 		('', 1), | ||||
| 		('Ki', 1024), | ||||
| 		('Mi', 1024*1024), | ||||
| 		('Gi', 1024*1024*1024), | ||||
| 		('Ti', 1024*1024*1024*1024) | ||||
| 	] | ||||
|  | ||||
|         if (len(used_prefixes) < 2): | ||||
|             return (number / used_prefixes[0][1], used_prefixes[0]) | ||||
|          | ||||
|         for i, (prefix, scale) in enumerate(used_prefixes[1:], 1): | ||||
|             if number < scale: | ||||
|                 return (number / used_prefixes[i-1][1], used_prefixes[i-1][0]) | ||||
|          | ||||
|         return (number / used_prefixes[-1][1], used_prefixes[-1][0]) | ||||
| 	@classmethod | ||||
| 	def number_to_engineering(c, number, it_unit=False): | ||||
| 		""" | ||||
| 		Convert a number to engineering SI syntax with prefix. | ||||
| 		This function will return a tuple of (new_number, prefix) | ||||
| 		""" | ||||
| 		if it_unit: | ||||
| 			used_prefixes = c.it_prefixes | ||||
| 		else: | ||||
| 			used_prefixes = c.prefixes | ||||
|  | ||||
|     @classmethod | ||||
|     def engineering_to_number(c, input): | ||||
|         cleaned_input = input.strip().replace(' ', '') | ||||
| 		if (len(used_prefixes) < 2): | ||||
| 			return (number / used_prefixes[0][1], used_prefixes[0]) | ||||
|  | ||||
|         selected_scaling = 1 | ||||
| 		for i, (prefix, scale) in enumerate(used_prefixes[1:], 1): | ||||
| 			if number < scale: | ||||
| 				return (number / used_prefixes[i-1][1], used_prefixes[i-1][0]) | ||||
|  | ||||
|         for (prefix, scale) in c.prefixes+c.it_prefixes: | ||||
|             if prefix == '': | ||||
|                 continue | ||||
|             if cleaned_input.endswith(prefix): | ||||
|                 cleaned_input = cleaned_input.replace(prefix, '') | ||||
|                 selected_scaling = scale | ||||
|                 break | ||||
|          | ||||
|         return float(cleaned_input) * selected_scaling | ||||
| 		return (number / used_prefixes[-1][1], used_prefixes[-1][0]) | ||||
|  | ||||
| 	@classmethod | ||||
| 	def engineering_to_number(c, input): | ||||
| 		cleaned_input = input.strip().replace(' ', '') | ||||
|  | ||||
| 		selected_scaling = 1 | ||||
|  | ||||
| 		for (prefix, scale) in c.prefixes+c.it_prefixes: | ||||
| 			if prefix == '': | ||||
| 				continue | ||||
| 			if cleaned_input.endswith(prefix): | ||||
| 				cleaned_input = cleaned_input.replace(prefix, '') | ||||
| 				selected_scaling = scale | ||||
| 				break | ||||
|  | ||||
| 		return float(cleaned_input) * selected_scaling | ||||
|   | ||||
| @@ -6,8 +6,13 @@ from django.utils.deconstruct import deconstructible | ||||
| @deconstructible | ||||
| class RandomFileName(object): | ||||
| 	def __init__(self, path): | ||||
| 		self.path = os.path.join(path, "%s%s") | ||||
| 		self.path = os.path.join(path, "%s/%s/%s%s") | ||||
|  | ||||
| 	def __call__(self, _, filename): | ||||
| 		extension = os.path.splitext(filename)[1] | ||||
| 		return self.path % (uuid.uuid4(), extension) | ||||
| 		file_uuid = uuid.uuid4() | ||||
| 		uuid_str = str(file_uuid) | ||||
| 		first_char = uuid_str[0] | ||||
| 		second_char = uuid_str[1] | ||||
|  | ||||
| 		return self.path % (first_char, second_char, file_uuid, extension) | ||||
|   | ||||
| @@ -2,21 +2,21 @@ | ||||
| <nav aria-label="{{aria_label}}"> | ||||
|     <ul class="pagination"> | ||||
|         {% if paginator.has_previous %} | ||||
|             <li class="page-item"><a class="page-link" href="?{{get_param}}={{paginator.previous_page_number}}">«</a></li> | ||||
|             <li class="page-item"><a class="page-link" href="?{{get_param}}={{paginator.previous_page_number}}{% if additional_params %}&{{additional_params}}{% endif %}">«</a></li> | ||||
|         {% else %} | ||||
|             <li class="page-item disabled"><span class="page-link">«</span></li> | ||||
|         {% endif %} | ||||
|         {% for i in paginator.paginator.page_range %} | ||||
|             {% if i <= paginator.number|add:5 and i >= paginator.number|add:-5 %} | ||||
|                 {% if i == paginator.number %} | ||||
|                         <li class="page-item active"><a class="page-link" href="?{{get_param}}={{i}}">{{i}}</a></li> | ||||
|                         <li class="page-item active"><a class="page-link" href="?{{get_param}}={{i}}{% if additional_params %}&{{additional_params}}{% endif %}">{{i}}</a></li> | ||||
|                 {% else %} | ||||
|                     <li class="page-item"><a class="page-link" href="?{{get_param}}={{i}}">{{i}}</a></li> | ||||
|                     <li class="page-item"><a class="page-link" href="?{{get_param}}={{i}}{% if additional_params %}&{{additional_params}}{% endif %}">{{i}}</a></li> | ||||
|                 {% endif %} | ||||
|             {% endif %} | ||||
|         {% endfor %} | ||||
|         {% if paginator.has_next %} | ||||
|             <li class="page-item"><a class="page-link" href="?{{get_param}}={{paginator.next_page_number}}">»</a></li> | ||||
|             <li class="page-item"><a class="page-link" href="?{{get_param}}={{paginator.next_page_number}}{% if additional_params %}&{{additional_params}}{% endif %}">»</a></li> | ||||
|         {% else %} | ||||
|             <li class="page-item disabled"><span class="page-link">»</span></li> | ||||
|         {% endif %} | ||||
|   | ||||
| @@ -0,0 +1,27 @@ | ||||
| {% extends 'base.html' %} | ||||
| {% load crispy_forms_tags %} | ||||
|  | ||||
| {% block content %} | ||||
| <div class="container"> | ||||
|     <nav aria-label="breadcrumb" class="fs-4"> | ||||
|         <ol class="breadcrumb"> | ||||
|             <li class="breadcrumb-item"></li> | ||||
|             {% for crumb in breadcrumbs %} | ||||
|             <li class="breadcrumb-item"><a href="{% url 'parts-componenttypes-detail' uuid=crumb.id %}">{{crumb.class_name}}</a></li> | ||||
|             {% endfor %} | ||||
|             <li class="breadcrumb-item active" aria-current="page">{{object.class_name}}</li> | ||||
|         </ol> | ||||
|     </nav> | ||||
|     <div class="row"> | ||||
|         <div class="col-md"> | ||||
|             <h2>Component Type: {{object.class_name}}</h2> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| {% endblock content %} | ||||
| {% block custom_scripts %} | ||||
|  | ||||
| <script type="text/javascript"> | ||||
| </script> | ||||
| {% endblock custom_scripts %} | ||||
							
								
								
									
										46
									
								
								shimatta_kenkyusho/templates/parts/component-types.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								shimatta_kenkyusho/templates/parts/component-types.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| {% extends 'base.html' %} | ||||
| {% load crispy_forms_tags %} | ||||
|  | ||||
| {% block content %} | ||||
| <div class="container"> | ||||
|     <div class="row"> | ||||
|         <div class="col-md"> | ||||
|             <h2>Component Types</h2> | ||||
|             <form action="" method="get"> | ||||
|                 <div class="input-group mb-3"> | ||||
|                     <input class="form-control" name="search" type="search" placeholder="Search Component Type..." {% if search_string %}value="{{search_string}}"{% endif %}> | ||||
|                     <button type="submit" class="btn btn-primary"> | ||||
|                         <i class="bi bi-search"></i> | ||||
|                     </button> | ||||
|                 </div> | ||||
|             </form> | ||||
|  | ||||
|             {% include 'paginator.html' with paginator=comptypes get_param='page' aria_label='Component Type Page Navigation' %} | ||||
|  | ||||
|             <div class="list-group mb-3"> | ||||
|                 {% for t in comptypes %} | ||||
|                     <a href="{% url 'parts-componenttypes-detail' uuid=t.id %}" class="text-decoration-none"> | ||||
|                     <li class="list-group-item list-group-item-action d-flex flex-row align-items-center justify-content-between"> | ||||
|                     <div class="p-2"> | ||||
|                         {{t}} | ||||
|                     </div> | ||||
|                     {% if t.passive %} | ||||
|                     <div class="p-2"> | ||||
|                         <span class="text-muted"> passive</span> | ||||
|                     </div> | ||||
|                     {% endif %} | ||||
|                     </li> | ||||
|                     </a> | ||||
|                 {% endfor %} | ||||
|             </div> | ||||
|             {% include 'paginator.html' with paginator=comptypes get_param='page' aria_label='Component Type Page Navigation' %} | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| {% endblock content %} | ||||
| {% block custom_scripts %} | ||||
|  | ||||
| <script type="text/javascript"> | ||||
| </script> | ||||
| {% endblock custom_scripts %} | ||||
| @@ -39,7 +39,7 @@ | ||||
|                 <tbody> | ||||
|                     <tr> | ||||
|                         <td class="align-middle" scope="row"> | ||||
|                             {{component.name}} | ||||
|                             {{component.name}}{% if key_parameter_string %}<br><span class="text-secondary">{{key_parameter_string}}</span>{% endif %} | ||||
|                         </td> | ||||
|                         <td class="align-middle" > | ||||
|                             {% if component.package %} | ||||
| @@ -119,6 +119,17 @@ | ||||
|                             <th scope="col"></th> | ||||
|                         </thead> | ||||
|                         <tbody> | ||||
|                             {% for param in package_parameters %} | ||||
|                             <td> | ||||
|                                 <h6 {% if param.parameter_type.parameter_description %} class="accordion-header" data-bs-toggle="collapse" data-bs-target="#collapse-pkg-parameter-desc-{{forloop.counter}}"{% endif %}> | ||||
|                                     {{param.parameter_type.parameter_name}} | ||||
|                                 </h6> | ||||
|                             </td> | ||||
|                             <td> | ||||
|                                 {{param.resolved_value_as_string}} | ||||
|                             </td> | ||||
|                             <td><span class="text-secondary">from Package</span></td> | ||||
|                             {% endfor %} | ||||
|                             {% for param in parameters %} | ||||
|                             <tr> | ||||
|                                 <td> | ||||
| @@ -148,6 +159,13 @@ | ||||
|                         </div> | ||||
|                         {% endif %} | ||||
|                         {% endfor %} | ||||
|                         {% for param in package_parameters %} | ||||
|                         {% if param.parameter_type.parameter_description %} | ||||
|                         <div class="collapse accordion-collapse" id="collapse-pkg-parameter-desc-{{forloop.counter}}" data-bs-parent="#accordion-param-desc"> | ||||
|                             {{param.parameter_type.parameter_description}} | ||||
|                         </div> | ||||
|                         {% endif %} | ||||
|                         {% endfor %} | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div class="col"> | ||||
|   | ||||
| @@ -17,7 +17,7 @@ | ||||
|                 </div> | ||||
|             </form> | ||||
|             <div class="collapse mb-3{% if advanced_search_shown %} show{% endif %}" id="advanced-search-collapse" aria-expanded="{% if advanced_search_shown %}true{% else %}false{% endif %}"> | ||||
|                 <form method="POST"> | ||||
|                 <form method="GET"> | ||||
|                     <div class="row"> | ||||
|                         <div class="col-sm"> | ||||
|                             {% crispy advanced_search_form %} | ||||
| @@ -48,7 +48,12 @@ | ||||
|                             {% endif %} | ||||
|                         </div> | ||||
|                         <div class="flex-grow-1 ms-3"> | ||||
|                             <h6 class="mt-0 text-primary">{{ comp.name }}</h6> | ||||
|                             <h6 class="mt-0 text-primary"> | ||||
|                                 {{ comp.name }} | ||||
|                                 {% for key_param in comp.get_key_parameters_as_text %} | ||||
|                                 {{key_param}} | ||||
|                                 {% endfor %} | ||||
|                             </h6> | ||||
|                             {% if comp.package %} | ||||
|                                 Package: {{comp.package}}<br> | ||||
|                             {% endif %} | ||||
|   | ||||
| @@ -69,7 +69,11 @@ | ||||
|                             {% endif %} | ||||
|                         </div> | ||||
|                         <div class="flex-grow-1 ms-3"> | ||||
|                             <h6 class="mt-0 text-primary"><a href="{% url 'parts-components-detail' uuid=stock.component.id %}" class="text-decoration-none">{{ stock.component.name }}</a></h6> | ||||
|                             <h6 class="mt-0 text-primary"><a href="{% url 'parts-components-detail' uuid=stock.component.id %}" class="text-decoration-none">{{ stock.component.name }}</a> | ||||
|                             {% for key_param in stock.component.get_key_parameters_as_text %} | ||||
|                                 {{key_param}}  | ||||
|                             {% endfor %} | ||||
|                              </h6> | ||||
|                             {% if stock.component.package %} | ||||
|                                 Package: {{stock.component.package}}<br> | ||||
|                             {% endif %} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user