217 Commits

Author SHA1 Message Date
6ff5450124 Minor fixes and improvements 2024-12-23 01:26:22 +01:00
f4af44c41c Merge pull request #40 from daniviga/catalogue
Introduce the concept of catalogs, improve books and code refactoring
2024-12-22 22:13:30 +01:00
e3ae18a4bd Update Python versions in GitHub workflows 2024-12-22 22:00:26 +01:00
2695358d9b Disable an old migration 2024-12-22 21:59:14 +01:00
3fbae0417e Update a migration 2024-12-22 21:56:16 +01:00
7a51ab9095 Bump version 2024-12-22 21:46:42 +01:00
dad40b3ee7 Implement documents inline for books and catalogs 2024-12-22 21:45:56 +01:00
d55bce6e78 More code refactoring, reduce template duplications 2024-12-22 21:32:22 +01:00
cbf6c942b9 Complete Catalogs with code refactoring 2024-12-22 18:53:47 +01:00
64f616d89f Merge branch 'master' into catalogue 2024-11-30 18:51:24 +01:00
f8246c31d3 Hotfix the manufacturer template 2024-11-30 14:56:26 +01:00
005ea11011 Minor improvements 2024-11-29 23:49:35 +01:00
83444266cb Add Catalogs views, but still need to fix templates (use books for now) 2024-11-29 23:43:36 +01:00
1a3b30ace3 Enable Catalogs in Admin 2024-11-29 23:30:33 +01:00
21c99f73c3 Implement Book data migration 2024-11-29 23:16:31 +01:00
b5b88f7714 Minor change to Image model Meta 2024-11-27 23:14:44 +01:00
119d25ede6 WIP: implement catalogue type of books 2024-11-27 23:07:43 +01:00
41d9338459 Allow books with no authors 2024-11-26 23:22:37 +01:00
32785f321a Fix the logout to use a POST (introduced with Django 4.1)
Also add an handy command to clear Django cache
2024-11-05 22:43:15 +01:00
5b975355a1 Add an help text to gauge 2024-11-04 22:33:26 +01:00
7d8c539e47 Update tracks related templates 2024-11-04 22:31:56 +01:00
9a832bca82 Improve sorting 2024-11-04 22:27:23 +01:00
54254bda7d Fix get_data method signatore in portal views.py 2024-11-04 15:06:55 +01:00
1c07c6a7a9 Add a custom manager to filter private and unpublished stuff (#39)
* Implement a customer manager for flatpages

* Implement public manager for private objects

* Add support for unpublished objects in roster and consist

* Add support for unpublished objects in bookshelf

* Update filtering on REST views

* Use uuid in urls.py

* Increment version
2024-11-04 15:00:34 +01:00
61b6d7a84e Update submodules 2024-11-04 11:33:44 +01:00
d0854a4cff Speedup inlines using autocomplete field and add more previews (#38) 2024-11-04 11:33:28 +01:00
456272b93a Add built-in decoder interface type 2024-04-30 11:08:16 +02:00
35905bafdf Improve rendering of pagination on mobile (#37) 2024-04-27 15:00:23 +02:00
6a9f37ca05 Add a 404 page and improve manufacturer lookup (#36)
* Add a custom 404 page
* Better manufacturer and item lookup
* Add migration to populate new field
* Version bump
2024-04-24 00:33:41 +02:00
54a68d9b1f Fix data retreival issue on GetData (#35) 2024-04-21 15:34:16 +02:00
aa02404dfe Fix an ordering issue on items in a set query 2024-04-21 09:56:10 +02:00
e4ad98fa38 Implement support for sets and other improvements (#34)
* Add a boolean to define item as part of a set
* Add contextual help in admin
* Introduce support to sets and to item code lookup
Also review the url path for pagination
2024-04-21 00:31:52 +02:00
b37f5420c5 Update to Bootstrap 5.3.3 (#33)
* Update to Bootstrap 5.3.3
* Remove support for python 3.9
2024-04-09 23:45:58 +02:00
4b74a69f3f Add the possbility to provide descriptions (#32)
to class, rolling stock, book
2024-03-02 15:45:42 +01:00
e7d34ce8e0 Remove unused args in upload_image 2024-02-17 23:06:41 +01:00
19eb70c492 Replace ckeditor with tinymce (#30)
* Replace ckeditor with tinymce due to deprecation
* Remove any ckeditor dependency from old migrations
   Disable alters, replace create with plain models.TextField
* Reformat files
* Add more hardening in image_upload
2024-02-17 23:05:18 +01:00
4428b8c11d Fix a RuntimeWarning introduced in Django 5 (#29) 2024-01-20 22:08:10 +01:00
8400a5acd3 Add a sample background to sample_data 2023-11-12 15:30:13 +01:00
7dadf23f5f Make pylibmc optional in requirements-prod.txt 2023-11-04 23:58:51 +01:00
4a12201d22 Make Document and Image files not nullable 2023-11-04 23:54:56 +01:00
830da80302 Keep media folder clean (#28)
* Reorg roster, portal and bookshelf media
* Extend media reorg to consists
* Delete roster and bookshelf images on delte.
   Do not delete others data that might be dedup! 
* Bump version
2023-10-31 11:16:55 +01:00
416ca5bbc6 eu.gif is part of dajngo-countries 2023-10-28 14:00:52 +02:00
03fc82c38d Enable csrf protection 2023-10-28 13:56:43 +02:00
ec8684dbc0 Add a "None" country and "Europe" with flags 2023-10-28 13:55:21 +02:00
7ec8baf733 Replace \t with spaces in base.html 2023-10-28 09:29:11 +02:00
86589ad718 More w3c minor fixes 2023-10-27 23:20:36 +02:00
98fed02a40 Fix a table in rollingstock.html 2023-10-27 23:16:23 +02:00
9602f67e0e Remove a spurious tag 2023-10-27 23:14:09 +02:00
5bb6279095 Extend UX improvements on other pages 2023-10-27 23:11:21 +02:00
84cdee42a6 Fix html syntax in rollingstock.html 2023-10-27 22:58:24 +02:00
168b424df7 Bump version 2023-10-27 22:46:19 +02:00
e1400fe720 Remove health page 2023-10-27 22:26:24 +02:00
26dea2fb35 Improve rollingstock page UX on mobile 2023-10-27 22:26:05 +02:00
ef767ec33d Fix a pretty-print on companies 2023-10-23 18:54:57 +02:00
b23801dbf0 Clear cache on save if active 2023-10-21 21:42:03 +02:00
c7fa54e90e Rename roster methods in portal view 2023-10-17 22:46:55 +02:00
9164ba494f Update examples to implement caching 2023-10-17 22:40:31 +02:00
97989c3384 Improve UX and filtering 2023-10-17 13:44:30 +02:00
7865bf04f0 Add consists view in rolling stock and them in company filter 2023-10-16 22:48:46 +02:00
e6f1480894 Change login menu icon on mobile 2023-10-12 22:33:55 +02:00
8d8ede4c06 Improve page layout on mobile 2023-10-11 22:39:29 +02:00
87e1107156 Bugfixing (#27)
* Enforce ordering on some metadata models
* Fix a 500 error while accessing flat pages
* Clean up HTML and fix cards (missing class)
* Make the "driver" app optional and disabled by default
2023-10-10 22:17:21 +02:00
448ecae070 Add Python 3.12 flow 2023-10-09 23:17:00 +02:00
2b0fdc4487 Workaround for python 3.12 on Fedora 39 2023-10-09 23:16:06 +02:00
764240d67a Fix bookshelf default sorting 2023-10-09 23:09:05 +02:00
424b17ae58 Bug fixing for consists 2023-10-08 09:52:38 +02:00
c73efb01e4 Introduce private docs and flatpages preview (#26)
* Add support for private documents
* Fix migrations after merge
* Rebase fixtures
* Filter private decoder docs
* Enable preview of unpublished pages
2023-10-07 22:38:20 +02:00
a21baac10c Fix a dependency on solo during bootstrap 2023-10-06 21:37:24 +02:00
4b0361acc1 Fix the consists search 2023-10-05 23:21:52 +02:00
425eed3d83 Bookshelf reloaded (#25)
* Navbar refactoring
* Fix coming soon SVG fonts
* Overhaul templating and extend search to consists and books
2023-10-05 23:13:42 +02:00
2d48463474 Change model default sort for Book 2023-10-03 23:08:11 +02:00
08226247c7 Extend ISBN to include dashes 2023-10-03 22:43:20 +02:00
4f52736d97 Fix a copy-paste issue in bookshelf model 2023-10-03 22:26:51 +02:00
bf8c2331c0 Add link to bookshelf in admin menu 2023-10-03 22:23:22 +02:00
1de4938ae7 Merge pull request #24 from daniviga/bookshelf
Implement a bookshelf
2023-10-03 22:18:11 +02:00
817d53d39a Anchor renaming 2023-10-03 22:13:39 +02:00
12ac33f4a2 Fix books default ordering 2023-10-03 21:58:45 +02:00
cbd76e4f66 Add a book details page in bookshelf 2023-10-03 21:54:47 +02:00
bcfed3534c Minor improvements 2023-10-02 23:27:57 +02:00
22bee7d95d Show booshelf menu 2023-10-02 23:16:54 +02:00
98c696b2d9 Add Books in the main menu 2023-10-02 23:01:43 +02:00
996ddd67ea Web bookshelf first draft 2023-10-02 22:58:15 +02:00
3f905877e7 Extend the bookshelf implementation 2023-10-02 22:19:04 +02:00
968ebeb0b6 First bookshelf implementation 2023-10-02 00:02:24 +02:00
b8572c1701 Rename SKU to 'Item number' 2023-10-01 21:35:14 +02:00
124f3c2a8b Do not show the coming soon image on mobile
To save some extra scrolling...
2023-10-01 19:26:04 +02:00
f4023f105f Add a default card image when no custom one exists (#23)
* Add a default card image when no custom one exists
* Add coming_soon.png source
* Use directly the svg source instead of the png raster
2023-10-01 16:36:30 +02:00
a189646aa5 More minor UX fixes 2023-10-01 11:14:17 +02:00
7a103cca56 Minor fix 2023-10-01 11:10:21 +02:00
2fe221d0f4 Add .table-group-divider to tbody with a custom color 2023-10-01 10:56:09 +02:00
6355460e01 Fix decoder document visualization 2023-10-01 10:39:09 +02:00
75074d5e90 Fix columns size 2023-10-01 10:22:03 +02:00
5d536ce568 Add documents to decoders (#22)
* Add decoder documents support
* Use abstract model for Documents
* Increase version
* Code cleanup
2023-10-01 00:03:41 +02:00
9483648a1f Update arduino/esp32 dependencies 2023-09-26 17:30:13 +02:00
8c15441fe5 More html cleanup to match W3c 2023-09-22 14:30:45 +02:00
5ebce9480e Cleanup html tags 2023-09-21 22:22:06 +02:00
64eefe43aa Update README.md 2023-09-18 22:02:13 +02:00
8e0a18d707 Update theme icon based on the selected one 2023-09-18 21:39:15 +02:00
46a2aa7011 Bump version 2023-09-18 20:23:03 +02:00
a176682615 Improve header html code 2023-09-18 20:21:41 +02:00
ad4591da04 Bootstrap 5.3 (#21)
* Migarte to Bootstrap 5.3
* Cleanup and version bump
2023-09-18 14:24:27 +02:00
2f2b96b2bb Add requirements-prod.txt 2023-05-07 14:52:49 +02:00
6cf3ad03cc Net-to-serial broadcast messages to all clients and other cleanups (#20)
* Cleanup and maintenance

* Net-to-serial broadcast messages to all clients

This will make all clients to stay in sync with any operation
occurring, like when having multiple JMRI instances

* Update README and python version in containers
2023-03-06 18:25:34 +01:00
2c5f0dcd6f Use slugs to filter 2023-01-09 22:54:15 +01:00
3545824016 Merge pull request #19 from daniviga/more-filters
Add more filters
2023-01-09 00:12:55 +01:00
78f9faee5e Switch back from pk filtering to safe name 2023-01-09 00:10:57 +01:00
6fbea294da Add more filters and search refactoring 2023-01-08 19:06:38 +01:00
35bdffdb3f Hotfix for 0.1.0 2023-01-08 01:26:14 +01:00
dccf467d38 Merge pull request #18 from daniviga/manufacturers
Add support for manufacturer filters
2023-01-08 00:44:01 +01:00
9dfa9172f4 Major templates and views refactoring 2023-01-08 00:40:13 +01:00
9279142a41 Add more manufacturers categories 2023-01-06 01:54:14 +01:00
aff1d20260 Add support for manufacturer filters 2023-01-06 01:47:07 +01:00
9b8ec6ba6b Add a favicon 2023-01-05 11:44:02 +01:00
c0b1b0b37b Hotfix some templates 2023-01-05 02:24:00 +01:00
169763e237 Merge pull request #17 from daniviga/ext-link
Support external links and replace font-awesome with bootstrap icons
2023-01-04 18:20:12 +01:00
bbe0758c6b Fix local copy of bootstrap icons 2023-01-04 18:18:47 +01:00
c73305fd85 Add support for external links 2023-01-04 18:17:20 +01:00
4a3fbda3dc Replace font-awesome with bootstrap icons 2023-01-04 18:15:04 +01:00
295965710f Merge pull request #16 from daniviga/cdn
Add cover to consist page and cdn option
2023-01-04 15:21:20 +01:00
c152f43aa6 Fix template indentation 2023-01-04 15:19:43 +01:00
8ed92dc5f0 Bump version 2023-01-04 15:16:02 +01:00
b70aa27a13 Add cover to consist page 2023-01-04 15:14:30 +01:00
3860ed70fd Allow the use of local copies of cdn files 2023-01-04 14:49:00 +01:00
68a18fcf58 Replace thumbnails with carousels in rolling stock pages (#15)
* Replace thumbnails with carousels in rolling stock pages

* Add consist data and notes in page
2023-01-03 01:32:16 +01:00
e45d11d4b1 Raise minimum python version to 3.9 2023-01-02 16:10:15 +01:00
32b5522a1e Change how images and consists are sorted (#14) 2023-01-02 16:08:25 +01:00
89b666dab2 Update README.md 2022-12-30 09:28:08 +01:00
ffad964373 Add possibility to inject js in head (analytics) 2022-12-28 23:54:49 +01:00
538dc0bd80 Add page title in html 2022-12-28 23:36:46 +01:00
8bd2635c28 Change image sort, thumbnails first 2022-12-28 22:14:10 +01:00
feda1f6cb4 [auto update] sync submodules 2022-11-28 18:22:05 +01:00
2c851b2822 Add migrations 2022-11-27 01:11:43 +01:00
e5ba2cfaec Merge pull request #13 from daniviga/dedup
Reuse existing file if content is the same
2022-11-27 01:09:50 +01:00
091f426242 Bump version 2022-11-27 01:09:34 +01:00
f603fd3e2d Reuse existing file if content is the same 2022-11-27 01:07:38 +01:00
a3b2112e03 Fix search query 2022-11-24 16:38:07 +01:00
055b0bab59 Enable "Save as" in roster and consist 2022-11-01 12:30:38 +01:00
3aea2ae340 Merge pull request #12 from daniviga/move-dcc-interface
Move decoder interface def into rolling stock
2022-11-01 00:07:18 +01:00
242fe6814d Move decoder interface def into rolling stock 2022-11-01 00:06:30 +01:00
90ffadb2ab Hotfix templates/companies.html pagination 2022-10-22 22:55:31 +02:00
21bf09687a Improve a CSS for journal 2022-08-28 11:32:19 +02:00
c1a45ad4c9 Bump version 2022-08-27 14:58:13 +02:00
29180572c1 Add a journal for rolling stock 2022-08-27 14:57:26 +02:00
d30d9fc9ed Various improvements for flatpages 2022-08-25 12:44:04 +02:00
4ed95d0edf Fix migrations 2022-08-25 00:49:10 +02:00
24bd2aa53c Add migrations md to html 2022-08-24 17:56:59 +02:00
5ef51cb9b7 cleanup 2022-08-24 14:55:26 +02:00
65493ba068 Merge pull request #10 from daniviga/flat-pages
Introduce support for Flatpages
2022-08-24 14:54:14 +02:00
ca459c467b Replace md editor with ckeditor 2022-08-23 17:54:58 +02:00
575c938205 Hotfix for document filename 2022-08-22 18:23:38 +02:00
7cc917d9f7 Use a markdown editor 2022-08-22 18:16:59 +02:00
0fe0644d1b Bump version 2022-08-22 17:14:32 +02:00
f7987f06d5 Merge branch 'master' into flat-pages 2022-08-22 17:13:39 +02:00
2af772a722 Black'ed 2022-08-22 17:13:10 +02:00
f580bcffc5 Documents section in admin 2022-08-22 17:12:22 +02:00
6accb66006 Enable search by sku 2022-08-21 16:53:18 +02:00
602c8359e9 Black'ed 2022-08-07 18:46:33 +02:00
46477c4576 Introduce support for Flatpages
Markdown support only
2022-08-07 18:43:58 +02:00
f56accb4ff Use lead unit thumbnail if not provided in consist 2022-07-23 23:45:11 +02:00
5a7b7fd79e Update README.md 2022-07-23 22:55:58 +02:00
dcdad71b1b Update README.md 2022-07-23 22:54:46 +02:00
321ae1065e Update README.md 2022-07-23 22:51:24 +02:00
e8efa5d87a Update README.md 2022-07-23 22:50:58 +02:00
97254b302c Fix a typo 2022-07-23 16:15:56 +02:00
b8aa34ce1d Add modal for pictures 2022-07-23 11:58:17 +02:00
e023edbeeb Add support for dark mode 2022-07-22 22:39:02 +02:00
c9c8976c60 UX improvements 2022-07-21 23:01:34 +02:00
5765472704 Fix to scale abbr 2022-07-21 22:11:17 +02:00
4fb9d1903f Reduce elided_page_range 2022-07-20 21:51:18 +02:00
63379c9673 Expose tracks 2022-07-18 23:45:13 +02:00
be6a685f55 Gauge vs track 2022-07-18 23:41:47 +02:00
ad33731913 Fix filtered pagination 2022-07-18 22:48:04 +02:00
503a214a4d Minor fixes 2022-07-18 17:07:01 +02:00
1528d1ba56 Make card title a stretched link 2022-07-17 20:46:00 +02:00
5b04abb262 Add countries and scales pages 2022-07-17 12:25:09 +02:00
9fa70ae656 Add filtering by scale 2022-07-16 21:24:36 +02:00
49b7aac807 Run black 2022-07-16 21:00:17 +02:00
24af738ad4 Fix page range 2022-07-16 20:57:29 +02:00
8136a180ab Try another fix for ellipsis 2022-07-16 19:56:08 +02:00
908790c3e0 Try a fix for ellipsis 2022-07-16 19:46:33 +02:00
44cdb8b09f Refactor paginator and add ellipsis 2022-07-16 18:57:46 +02:00
7d3f29e734 Use int sort for road numbers 2022-07-16 17:48:04 +02:00
65b615ae63 Validate search 2022-07-15 22:26:37 +02:00
66a85b4ed7 Add tag based filtering 2022-07-15 21:31:40 +02:00
70c12c69b2 Improve road number sorting and enforce company on consists 2022-07-15 18:10:18 +02:00
e55f953c8a Add sorting, enforce foreign keys 2022-07-15 17:28:44 +02:00
273225f919 Default if none in template 2022-07-13 21:43:50 +02:00
1296c4e663 Default if none in template 2022-07-13 21:42:33 +02:00
bbecdd54bb Remove a pdb leftover 2022-07-13 20:41:48 +02:00
899e33da61 Move properties under model and class sections 2022-07-13 20:40:42 +02:00
1f4966ad49 Add missing swagger template 2022-07-13 20:39:49 +02:00
c7dfb84632 Hide 'About' when no text is provided 2022-07-12 21:56:07 +02:00
4e3a13162a Fix navbar on mobile 2022-07-12 21:17:29 +02:00
74431c5d94 Remove thumbnail padding 2022-07-12 21:01:17 +02:00
cb2ff90d8a Review roster API 2022-07-12 19:00:50 +02:00
2dbe01d8bd Add private properties and consist thumbnail 2022-07-12 19:00:29 +02:00
47b4b2915b Add an option to completely disable driver 2022-07-11 20:56:31 +02:00
49d28b176e Rename a logo file 2022-07-11 18:14:24 +02:00
0e30740366 Fix sample data 2022-07-11 18:12:45 +02:00
7c3fee4127 Limit decoders selection to model manufacturers 2022-07-10 23:48:10 +02:00
1f96ff8833 Metadata pretty print 2022-07-10 23:44:18 +02:00
e76a1ea6b2 Add some sample metadata 2022-07-10 23:42:13 +02:00
565028de72 Add support for local_settings.py 2022-07-10 23:31:37 +02:00
7c88983cba More url generation fixes 2022-07-10 23:19:21 +02:00
51eb9ba2a2 Fix main redirection 2022-07-10 22:56:27 +02:00
b19ea23fa6 Update footer and version 2022-07-10 22:52:54 +02:00
353da224be Fix consists pagination and remove hardcoded urls 2022-07-10 22:48:45 +02:00
d39cd47280 Update arduino-cli to 0.24.0 2022-07-10 22:41:54 +02:00
a67ea83068 Update README.md 2022-06-25 22:48:04 +02:00
410fcd8626 Update README.md 2022-06-25 22:47:34 +02:00
05eb0909c8 Update README.md 2022-06-25 22:46:16 +02:00
8369ffce46 Update README.md 2022-05-20 21:16:11 +02:00
818db8c6e3 Update README.md 2022-05-20 21:15:55 +02:00
9267d78815 Update README.md 2022-05-11 22:21:23 +02:00
de9910936a Update README.md 2022-04-24 00:12:02 +02:00
ea78c81f79 Update README.md 2022-04-24 00:10:53 +02:00
cb7a86c977 Update README 2022-04-24 00:02:06 +02:00
188 changed files with 8944 additions and 974 deletions

View File

@@ -13,7 +13,7 @@ jobs:
strategy:
max-parallel: 2
matrix:
python-version: ['3.9', '3.10']
python-version: ['3.12', '3.13']
steps:
- uses: actions/checkout@v3

1
.gitignore vendored
View File

@@ -10,7 +10,6 @@ __pycache__/
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/

View File

@@ -2,27 +2,31 @@
[![Django CI](https://github.com/daniviga/django-rma/actions/workflows/django.yml/badge.svg)](https://github.com/daniviga/django-rma/actions/workflows/django.yml)
![Screenshot 2023-09-18 at 21-57-33 Company RGS - Railroad Assets Manager](https://github.com/daniviga/django-ram/assets/1818657/d20fbe27-1192-4ab1-a19f-8d2ae50cf781)
A `jff` (just for fun) project that aims to create a
model railroad assets manager that allows to:
- Create a database of assets (model trains) and consits with their metadata
- Create a database of assets (model trains) and consists with their metadata
- Manage the database via a simple but rationale backoffice
- Expose main data via an HTML interface to show how beautiful is your collection
to the outside world
- Act as a DCC++ EX REST API gateway to control assets remotely via DCC.
By anyone, if you'd like (really?).
By anyone, if you'd like (seriously?).
## Preface
Project is intended to have fun only and it has been developed with a
commitment of few minutes a day; it lacks any kind of documentation, code
review, architectural review, security assesment, pentest, ISO certification,
etc.
**This project is work in progress**. It is intended for fun only and
it has been developed with a commitment of few minutes a day;
it lacks any kind of documentation, code review, architectural review,
security assesment, pentest, ISO certification, etc.
This project probably doesn't match you needs nor expectations. Be aware.
This project probably doesn't match your needs nor expectations. Be aware.
Your model train may also catch fire while using this software.
Check out [my own instance](https://daniele.mynarrowgauge.org).
## Components
Project is based on the following technologies and components:
@@ -30,22 +34,22 @@ Project is based on the following technologies and components:
- [Django](https://www.djangoproject.com/): *the* web framework
- [Django REST](https://www.django-rest-framework.org/): API for the lazy
- [Bootstrap](https://getbootstrap.com/): for the web frontend
- [Arduino](https://arduino.cc): DCC hardware
- [Arduino](https://arduino.cc): DCC hardware; you must get one, really
- [DCC++ EX Command Station](https://dcc-ex.com/): DCC firmware; an amazing project
- [DCC++ EX WebThrottle](https://github.com/DCC-EX/WebThrottle-EX): a slighly modified version of the DCC++ EX web throttle
- [DCC++ EX WebThrottle](https://github.com/DCC-EX/WebThrottle-EX): the DCC++ EX web throttle, a slightly modified version
It has been developed with:
- [vim](https://www.vim.org/): because it rocks
- [arduino-cli](https://github.com/arduino/arduino-cli/): a mouse? What the hack?
- [arduino-cli](https://github.com/arduino/arduino-cli/): a mouse? What the heck?
- [vim-arduino](https://github.com/stevearc/vim-arduino): another IDE? No thanks
- [podman](https://podman.io/): because containers are fancy
- [QEMU (avr)](https://qemu-project.gitlab.io/qemu/system/target-avr.html): QEMU can even make toast!
## Requirments
## Requirements
- Python 3.8+
- Python 3.10+
- A USB port when running Arduino hardware (and adaptors if you have a Mac)
## Web portal installation
@@ -92,7 +96,8 @@ Browse to `http://localhost:8000`
The DCC++ EX connector exposes an Arduino board running DCC++ EX Command Station,
connected via serial port, to the network, allowing commands to be sent via a
TCP socket.
TCP socket. A response generated by the DCC++ EX board is sent to all connected clients,
providing synchronization between multiple clients (eg. multiple JMRI instances).
Its use is not needed when running DCC++ EX from a [WiFi](https://dcc-ex.com/get-started/wifi-setup.html) capable board (like when
using an ESP8266 module or a [Mega+WiFi board](https://dcc-ex.com/advanced-setup/supported-microcontrollers/wifi-mega.html)).
@@ -108,7 +113,7 @@ Settings may need to be customized based on your setup.
```bash
$ cd daemons
$ podman build -t dcc/net-to-serial .
$ podman run -d -p 2560:2560 dcc/net-to-serial
$ podman run --group-add keep-groups --device /dev/ttyACM0 -p 2560:2560 dcc/net-to-serial
```
### Manual setup
@@ -132,11 +137,35 @@ $ podman run --init --cpus 0.1 -d -p 2560:2560 dcc/net-to-serial:sim
To be continued ...
## Screenshots
### Frontend
![Screenshot 2023-09-18 at 22-00-39 RGS C-19 #40 - Railroad Assets Manager](https://github.com/daniviga/django-ram/assets/1818657/94834b89-5b17-46e7-9494-a1651d72c072)
---
![Screenshot 2023-09-18 at 21-59-30 RGS 1930s short train - Railroad Assets Manager](https://github.com/daniviga/django-ram/assets/1818657/77f9b7c9-27b3-4a65-bad0-26e9cf77e623)
#### Dark mode
![Screenshot 2023-09-18 at 21-58-22 Company RGS - Railroad Assets Manager](https://github.com/daniviga/django-ram/assets/1818657/c95697c9-0897-46f4-941c-6092271e4743)
---
### Backoffice
![image](https://user-images.githubusercontent.com/1818657/175789937-3e4970a2-b37d-44c3-8605-62dabe209c65.png)
---
![image](https://user-images.githubusercontent.com/1818657/175789946-d7ce882c-1ba6-49b2-8e0a-1144e5c6bc35.png)
---
![image](https://user-images.githubusercontent.com/1818657/175789954-0735a4ea-bcaf-4a45-adbc-64105091b051.png)
### Rest API
![image](https://user-images.githubusercontent.com/1818657/180622471-ade06c84-c73b-41d5-a2a7-02a95b2ffc02.png)

View File

@@ -1,4 +1,4 @@
FROM python:3.10-alpine
FROM python:3.11-alpine
RUN mkdir /opt/dcc && pip -q install pyserial
ADD net-to-serial.py config.ini /opt/dcc

3
daemons/README.md Normal file
View File

@@ -0,0 +1,3 @@
## DCC++ EX connector
See [README.md](../README.md)

View File

@@ -2,6 +2,7 @@
LogLevel = debug
ListeningIP = 0.0.0.0
ListeningPort = 2560
MaxClients = 10
[Serial]
# UNO

View File

@@ -1,6 +1,5 @@
#!/usr/bin/env python3
import re
import time
import logging
import serial
import asyncio
@@ -10,11 +9,15 @@ from pathlib import Path
class SerialDaemon:
connected_clients = set()
def __init__(self, config):
self.ser = serial.Serial(
config["Serial"]["Port"],
timeout=int(config["Serial"]["Timeout"])/1000)
timeout=int(config["Serial"]["Timeout"]) / 1000,
)
self.ser.baudrate = config["Serial"]["Baudrate"]
self.max_clients = int(config["Daemon"]["MaxClients"])
def __del__(self):
try:
@@ -43,19 +46,32 @@ class SerialDaemon:
async def handle_echo(self, reader, writer):
"""Process a request from socket and return the response"""
while 1: # keep connection to client open
data = await reader.read(100)
if not data: # client has disconnected
break
logging.info(
"Clients already connected: {} (max: {})".format(
len(self.connected_clients),
self.max_clients,
)
)
addr = writer.get_extra_info('peername')
logging.info("Received {} from {}".format(data, addr[0]))
self.__write_serial(data)
response = self.__read_serial()
writer.write(response)
await writer.drain()
logging.info("Sent: {}".format(response))
addr = writer.get_extra_info("peername")[0]
if len(self.connected_clients) < self.max_clients:
self.connected_clients.add(writer)
while True: # keep connection to client open
data = await reader.read(100)
if not data: # client has disconnected
break
logging.info("Received {} from {}".format(data, addr))
self.__write_serial(data)
response = self.__read_serial()
for client in self.connected_clients:
client.write(response)
await client.drain()
logging.info("Sent: {}".format(response))
self.connected_clients.remove(writer)
else:
logging.warning(
"TooManyClients: client {} disconnected".format(addr)
)
writer.close()
await writer.wait_closed()
@@ -68,33 +84,37 @@ class SerialDaemon:
while "DCC-EX" not in line:
line = self.__read_serial().decode()
board = re.findall(r"<iDCC-EX.*>", line)[0]
return(board)
return board
async def main():
config = configparser.ConfigParser()
config.read(
Path(__file__).resolve().parent / "config.ini") # mimick os.path.join
Path(__file__).resolve().parent / "config.ini"
) # mimick os.path.join
logging.basicConfig(level=config["Daemon"]["LogLevel"].upper())
sd = SerialDaemon(config)
server = await asyncio.start_server(
sd.handle_echo,
config["Daemon"]["ListeningIP"],
config["Daemon"]["ListeningPort"])
config["Daemon"]["ListeningPort"],
)
addr = server.sockets[0].getsockname()
logging.warning("Serving on {} port {}".format(addr[0], addr[1]))
logging.warning(
logging.info("Serving on {} port {}".format(addr[0], addr[1]))
logging.info(
"Proxying to {} (Baudrate: {}, Timeout: {})".format(
config["Serial"]["Port"],
config["Serial"]["Baudrate"],
config["Serial"]["Timeout"]))
logging.warning("Initializing board")
logging.warning("Board {} ready".format(
await sd.return_board()))
config["Serial"]["Timeout"],
)
)
logging.info("Initializing board")
logging.info("Board {} ready".format(await sd.return_board()))
async with server:
await server.serve_forever()
if __name__ == "__main__":
asyncio.run(main())

Binary file not shown.

View File

@@ -1,6 +1,6 @@
# AVR Simulator
`qemu-system-avr` tries to use all the CPU cicles (leaving a CPU core stuck at 100%. Limit CPU core usage to 10%. It may be adjusted on slower machines.
`qemu-system-avr` tries to use all the CPU cicles (leaving a CPU core stuck at 100%; limit CPU core usage to 10% via `--cpus 0.1`. It can be adjusted on slower machines.
```bash
$ podman build -t dcc/net-to-serial:sim .

View File

169
ram/bookshelf/admin.py Normal file
View File

@@ -0,0 +1,169 @@
from django.contrib import admin
from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
from bookshelf.models import (
BaseBookProperty,
BaseBookImage,
BaseBookDocument,
Book,
Author,
Publisher,
Catalog,
)
class BookImageInline(SortableInlineAdminMixin, admin.TabularInline):
model = BaseBookImage
min_num = 0
extra = 0
readonly_fields = ("image_thumbnail",)
classes = ["collapse"]
verbose_name = "Image"
class BookDocInline(admin.TabularInline):
model = BaseBookDocument
min_num = 0
extra = 0
classes = ["collapse"]
class BookPropertyInline(admin.TabularInline):
model = BaseBookProperty
min_num = 0
extra = 0
autocomplete_fields = ("property",)
verbose_name = "Property"
verbose_name_plural = "Properties"
@admin.register(Book)
class BookAdmin(SortableAdminBase, admin.ModelAdmin):
inlines = (
BookPropertyInline,
BookImageInline,
BookDocInline,
)
list_display = (
"title",
"get_authors",
"get_publisher",
"publication_year",
"number_of_pages",
"published",
)
autocomplete_fields = ("authors", "publisher")
readonly_fields = ("creation_time", "updated_time")
search_fields = ("title", "publisher__name", "authors__last_name")
list_filter = ("publisher__name", "authors")
fieldsets = (
(
None,
{
"fields": (
"published",
"title",
"authors",
"publisher",
"ISBN",
"language",
"number_of_pages",
"publication_year",
"description",
"purchase_date",
"notes",
"tags",
)
},
),
(
"Audit",
{
"classes": ("collapse",),
"fields": (
"creation_time",
"updated_time",
),
},
),
)
@admin.display(description="Publisher")
def get_publisher(self, obj):
return obj.publisher.name
@admin.display(description="Authors")
def get_authors(self, obj):
return ", ".join(a.short_name() for a in obj.authors.all())
@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
search_fields = (
"first_name",
"last_name",
)
list_filter = ("last_name",)
@admin.register(Publisher)
class PublisherAdmin(admin.ModelAdmin):
list_display = ("name", "country")
search_fields = ("name",)
@admin.register(Catalog)
class CatalogAdmin(SortableAdminBase, admin.ModelAdmin):
inlines = (
BookPropertyInline,
BookImageInline,
BookDocInline,
)
list_display = (
"__str__",
"manufacturer",
"years",
"get_scales",
"published",
)
autocomplete_fields = ("manufacturer",)
readonly_fields = ("creation_time", "updated_time")
search_fields = ("manufacturer__name", "years", "scales__scale")
list_filter = ("manufacturer__name", "publication_year", "scales__scale")
fieldsets = (
(
None,
{
"fields": (
"published",
"manufacturer",
"years",
"scales",
"ISBN",
"language",
"number_of_pages",
"publication_year",
"description",
"purchase_date",
"notes",
"tags",
)
},
),
(
"Audit",
{
"classes": ("collapse",),
"fields": (
"creation_time",
"updated_time",
),
},
),
)
@admin.display(description="Scales")
def get_scales(self, obj):
return "/".join(s.scale for s in obj.scales.all())

6
ram/bookshelf/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class BookshelfConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "bookshelf"

View File

@@ -0,0 +1,121 @@
# Generated by Django 4.2.5 on 2023-10-01 20:16
# ckeditor removal
# import ckeditor_uploader.fields
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
("metadata", "0012_alter_decoder_manufacturer_decoderdocument"),
]
operations = [
migrations.CreateModel(
name="Author",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("first_name", models.CharField(max_length=100)),
("last_name", models.CharField(max_length=100)),
],
),
migrations.CreateModel(
name="Book",
fields=[
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("title", models.CharField(max_length=200)),
("ISBN", models.CharField(max_length=13, unique=True)),
("publication_year", models.SmallIntegerField(blank=True, null=True)),
("purchase_date", models.DateField(blank=True, null=True)),
# ("notes", ckeditor_uploader.fields.RichTextUploadingField(blank=True)),
("notes", models.TextField(blank=True)),
("creation_time", models.DateTimeField(auto_now_add=True)),
("updated_time", models.DateTimeField(auto_now=True)),
("authors", models.ManyToManyField(to="bookshelf.author")),
],
),
migrations.CreateModel(
name="Publisher",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=200)),
("website", models.URLField()),
],
),
migrations.CreateModel(
name="BookProperty",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("value", models.CharField(max_length=256)),
(
"book",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="property",
to="bookshelf.book",
),
),
(
"property",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="metadata.property",
),
),
],
options={
"verbose_name_plural": "Properties",
},
),
migrations.AddField(
model_name="book",
name="publisher",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="bookshelf.publisher"
),
),
migrations.AddField(
model_name="book",
name="tags",
field=models.ManyToManyField(
blank=True, related_name="bookshelf", to="metadata.tag"
),
),
]

View File

@@ -0,0 +1,142 @@
# Generated by Django 4.2.5 on 2023-10-01 21:33
from django.db import migrations, models
import django_countries.fields
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="book",
name="language",
field=models.CharField(
choices=[
("af", "Afrikaans"),
("ar", "Arabic"),
("ar-dz", "Algerian Arabic"),
("ast", "Asturian"),
("az", "Azerbaijani"),
("bg", "Bulgarian"),
("be", "Belarusian"),
("bn", "Bengali"),
("br", "Breton"),
("bs", "Bosnian"),
("ca", "Catalan"),
("ckb", "Central Kurdish (Sorani)"),
("cs", "Czech"),
("cy", "Welsh"),
("da", "Danish"),
("de", "German"),
("dsb", "Lower Sorbian"),
("el", "Greek"),
("en", "English"),
("en-au", "Australian English"),
("en-gb", "British English"),
("eo", "Esperanto"),
("es", "Spanish"),
("es-ar", "Argentinian Spanish"),
("es-co", "Colombian Spanish"),
("es-mx", "Mexican Spanish"),
("es-ni", "Nicaraguan Spanish"),
("es-ve", "Venezuelan Spanish"),
("et", "Estonian"),
("eu", "Basque"),
("fa", "Persian"),
("fi", "Finnish"),
("fr", "French"),
("fy", "Frisian"),
("ga", "Irish"),
("gd", "Scottish Gaelic"),
("gl", "Galician"),
("he", "Hebrew"),
("hi", "Hindi"),
("hr", "Croatian"),
("hsb", "Upper Sorbian"),
("hu", "Hungarian"),
("hy", "Armenian"),
("ia", "Interlingua"),
("id", "Indonesian"),
("ig", "Igbo"),
("io", "Ido"),
("is", "Icelandic"),
("it", "Italian"),
("ja", "Japanese"),
("ka", "Georgian"),
("kab", "Kabyle"),
("kk", "Kazakh"),
("km", "Khmer"),
("kn", "Kannada"),
("ko", "Korean"),
("ky", "Kyrgyz"),
("lb", "Luxembourgish"),
("lt", "Lithuanian"),
("lv", "Latvian"),
("mk", "Macedonian"),
("ml", "Malayalam"),
("mn", "Mongolian"),
("mr", "Marathi"),
("ms", "Malay"),
("my", "Burmese"),
("nb", "Norwegian Bokmål"),
("ne", "Nepali"),
("nl", "Dutch"),
("nn", "Norwegian Nynorsk"),
("os", "Ossetic"),
("pa", "Punjabi"),
("pl", "Polish"),
("pt", "Portuguese"),
("pt-br", "Brazilian Portuguese"),
("ro", "Romanian"),
("ru", "Russian"),
("sk", "Slovak"),
("sl", "Slovenian"),
("sq", "Albanian"),
("sr", "Serbian"),
("sr-latn", "Serbian Latin"),
("sv", "Swedish"),
("sw", "Swahili"),
("ta", "Tamil"),
("te", "Telugu"),
("tg", "Tajik"),
("th", "Thai"),
("tk", "Turkmen"),
("tr", "Turkish"),
("tt", "Tatar"),
("udm", "Udmurt"),
("uk", "Ukrainian"),
("ur", "Urdu"),
("uz", "Uzbek"),
("vi", "Vietnamese"),
("zh-hans", "Simplified Chinese"),
("zh-hant", "Traditional Chinese"),
],
default="en",
max_length=7,
),
),
migrations.AddField(
model_name="book",
name="numbers_of_pages",
field=models.SmallIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name="publisher",
name="country",
field=django_countries.fields.CountryField(blank=True, max_length=2),
),
migrations.AlterField(
model_name="book",
name="ISBN",
field=models.CharField(blank=True, max_length=13),
),
migrations.AlterField(
model_name="publisher",
name="website",
field=models.URLField(blank=True),
),
]

View File

@@ -0,0 +1,51 @@
# Generated by Django 4.2.5 on 2023-10-02 10:36
from django.db import migrations, models
import django.db.models.deletion
import ram.utils
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0002_book_language_book_numbers_of_pages_and_more"),
]
operations = [
migrations.CreateModel(
name="BookImage",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("order", models.PositiveIntegerField(default=0)),
(
"image",
models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to="images/books/",
),
),
(
"book",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="image",
to="bookshelf.book",
),
),
],
options={
"ordering": ["order"],
"abstract": False,
},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.2.5 on 2023-10-02 20:38
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0003_bookimage"),
]
operations = [
migrations.RenameField(
model_name="book",
old_name="numbers_of_pages",
new_name="number_of_pages",
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 4.2.5 on 2023-10-03 19:57
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0004_rename_numbers_of_pages_book_number_of_pages"),
]
operations = [
migrations.AlterModelOptions(
name="book",
options={"ordering": ["authors__last_name", "title"]},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.2.5 on 2023-10-03 20:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0005_alter_book_options"),
]
operations = [
migrations.AlterField(
model_name="book",
name="ISBN",
field=models.CharField(blank=True, max_length=17),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 4.2.5 on 2023-10-03 21:07
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0006_alter_book_isbn"),
]
operations = [
migrations.AlterModelOptions(
name="book",
options={"ordering": ["title"]},
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 4.2.6 on 2023-10-09 21:08
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0007_alter_book_options"),
]
operations = [
migrations.AlterModelOptions(
name="author",
options={"ordering": ["last_name", "first_name"]},
),
migrations.AlterModelOptions(
name="publisher",
options={"ordering": ["name"]},
),
]

View File

@@ -0,0 +1,51 @@
# Generated by Django 4.2.6 on 2023-10-30 13:16
import os
import sys
import shutil
import ram.utils
import bookshelf.models
from django.db import migrations, models
from django.conf import settings
def move_images(apps, schema_editor):
sys.stdout.write("\n Processing files. Please await...")
for r in bookshelf.models.BaseBookImage.objects.all():
fname = os.path.basename(r.image.path)
new_image = bookshelf.models.book_image_upload(r, fname)
new_path = os.path.join(settings.MEDIA_ROOT, new_image)
os.makedirs(os.path.dirname(new_path), exist_ok=True)
try:
shutil.move(r.image.path, new_path)
except FileNotFoundError:
sys.stderr.write(" !! FileNotFoundError: {}\n".format(new_image))
pass
r.image.name = new_image
r.save()
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0008_alter_author_options_alter_publisher_options"),
]
# Migration is stale and shouldn't be used since model hes been heavily
# modified since then. Leaving it here for reference.
operations = [
# migrations.AlterField(
# model_name="bookimage",
# name="image",
# field=models.ImageField(
# blank=True,
# null=True,
# storage=ram.utils.DeduplicatedStorage,
# upload_to=bookshelf.models.book_image_upload,
# ),
# ),
# migrations.RunPython(
# move_images,
# reverse_code=migrations.RunPython.noop
# ),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 4.2.6 on 2023-11-04 22:53
import bookshelf.models
from django.db import migrations, models
import ram.utils
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0009_alter_bookimage_image"),
]
operations = [
migrations.AlterField(
model_name="bookimage",
name="image",
field=models.ImageField(
storage=ram.utils.DeduplicatedStorage,
upload_to=bookshelf.models.book_image_upload,
),
),
]

View File

@@ -0,0 +1,121 @@
# Generated by Django 5.0.1 on 2024-01-20 21:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0010_alter_bookimage_image"),
]
operations = [
migrations.AlterField(
model_name="book",
name="language",
field=models.CharField(
choices=[
("af", "Afrikaans"),
("ar", "Arabic"),
("ar-dz", "Algerian Arabic"),
("ast", "Asturian"),
("az", "Azerbaijani"),
("bg", "Bulgarian"),
("be", "Belarusian"),
("bn", "Bengali"),
("br", "Breton"),
("bs", "Bosnian"),
("ca", "Catalan"),
("ckb", "Central Kurdish (Sorani)"),
("cs", "Czech"),
("cy", "Welsh"),
("da", "Danish"),
("de", "German"),
("dsb", "Lower Sorbian"),
("el", "Greek"),
("en", "English"),
("en-au", "Australian English"),
("en-gb", "British English"),
("eo", "Esperanto"),
("es", "Spanish"),
("es-ar", "Argentinian Spanish"),
("es-co", "Colombian Spanish"),
("es-mx", "Mexican Spanish"),
("es-ni", "Nicaraguan Spanish"),
("es-ve", "Venezuelan Spanish"),
("et", "Estonian"),
("eu", "Basque"),
("fa", "Persian"),
("fi", "Finnish"),
("fr", "French"),
("fy", "Frisian"),
("ga", "Irish"),
("gd", "Scottish Gaelic"),
("gl", "Galician"),
("he", "Hebrew"),
("hi", "Hindi"),
("hr", "Croatian"),
("hsb", "Upper Sorbian"),
("hu", "Hungarian"),
("hy", "Armenian"),
("ia", "Interlingua"),
("id", "Indonesian"),
("ig", "Igbo"),
("io", "Ido"),
("is", "Icelandic"),
("it", "Italian"),
("ja", "Japanese"),
("ka", "Georgian"),
("kab", "Kabyle"),
("kk", "Kazakh"),
("km", "Khmer"),
("kn", "Kannada"),
("ko", "Korean"),
("ky", "Kyrgyz"),
("lb", "Luxembourgish"),
("lt", "Lithuanian"),
("lv", "Latvian"),
("mk", "Macedonian"),
("ml", "Malayalam"),
("mn", "Mongolian"),
("mr", "Marathi"),
("ms", "Malay"),
("my", "Burmese"),
("nb", "Norwegian Bokmål"),
("ne", "Nepali"),
("nl", "Dutch"),
("nn", "Norwegian Nynorsk"),
("os", "Ossetic"),
("pa", "Punjabi"),
("pl", "Polish"),
("pt", "Portuguese"),
("pt-br", "Brazilian Portuguese"),
("ro", "Romanian"),
("ru", "Russian"),
("sk", "Slovak"),
("sl", "Slovenian"),
("sq", "Albanian"),
("sr", "Serbian"),
("sr-latn", "Serbian Latin"),
("sv", "Swedish"),
("sw", "Swahili"),
("ta", "Tamil"),
("te", "Telugu"),
("tg", "Tajik"),
("th", "Thai"),
("tk", "Turkmen"),
("tr", "Turkish"),
("tt", "Tatar"),
("udm", "Udmurt"),
("ug", "Uyghur"),
("uk", "Ukrainian"),
("ur", "Urdu"),
("uz", "Uzbek"),
("vi", "Vietnamese"),
("zh-hans", "Simplified Chinese"),
("zh-hant", "Traditional Chinese"),
],
default="en",
max_length=7,
),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.0.2 on 2024-02-17 12:19
import tinymce.models
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0011_alter_book_language"),
]
operations = [
migrations.AlterField(
model_name="book",
name="notes",
field=tinymce.models.HTMLField(blank=True),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.0.2 on 2024-03-02 14:31
import tinymce.models
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0012_alter_book_notes"),
]
operations = [
migrations.AddField(
model_name="book",
name="description",
field=tinymce.models.HTMLField(blank=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2024-11-04 13:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0013_book_description"),
]
operations = [
migrations.AddField(
model_name="book",
name="published",
field=models.BooleanField(default=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2024-11-26 22:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0014_book_published"),
]
operations = [
migrations.AlterField(
model_name="book",
name="authors",
field=models.ManyToManyField(blank=True, to="bookshelf.author"),
),
]

View File

@@ -0,0 +1,141 @@
# Generated by Django 5.1.2 on 2024-11-27 16:35
import django.db.models.deletion
from django.db import migrations, models
def basebook_to_book(apps, schema_editor):
basebook = apps.get_model("bookshelf", "BaseBook")
book = apps.get_model("bookshelf", "Book")
for row in basebook.objects.all():
b = book.objects.create(
basebook_ptr=row,
title=row.old_title,
publisher=row.old_publisher,
)
b.authors.set(row.old_authors.all())
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0015_alter_book_authors"),
("metadata", "0019_alter_scale_gauge"),
]
operations = [
migrations.AlterModelOptions(
name="Book",
options={"ordering": ["creation_time"]},
),
migrations.RenameModel(
old_name="BookImage",
new_name="BaseBookImage",
),
migrations.RenameModel(
old_name="BookProperty",
new_name="BaseBookProperty",
),
migrations.RenameModel(
old_name="Book",
new_name="BaseBook",
),
migrations.RenameField(
model_name="basebook",
old_name="title",
new_name="old_title",
),
migrations.RenameField(
model_name="basebook",
old_name="authors",
new_name="old_authors",
),
migrations.RenameField(
model_name="basebook",
old_name="publisher",
new_name="old_publisher",
),
migrations.AlterModelOptions(
name="basebookimage",
options={"ordering": ["order"], "verbose_name_plural": "Images"},
),
migrations.CreateModel(
name="Book",
fields=[
(
"basebook_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="bookshelf.basebook",
),
),
("title", models.CharField(max_length=200)),
(
"authors",
models.ManyToManyField(
blank=True,
to="bookshelf.author"
),
),
(
"publisher",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="bookshelf.publisher"
),
),
],
options={
"ordering": ["title"],
},
),
migrations.RunPython(
basebook_to_book,
reverse_code=migrations.RunPython.noop
),
migrations.RemoveField(
model_name="basebook",
name="old_title",
),
migrations.RemoveField(
model_name="basebook",
name="old_authors",
),
migrations.RemoveField(
model_name="basebook",
name="old_publisher",
),
migrations.CreateModel(
name="Catalog",
fields=[
(
"basebook_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="bookshelf.basebook",
),
),
("years", models.CharField(max_length=12)),
(
"manufacturer",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="metadata.manufacturer",
),
),
("scales", models.ManyToManyField(to="metadata.scale")),
],
options={
"ordering": ["manufacturer", "publication_year"],
},
bases=("bookshelf.basebook",),
),
]

View File

@@ -0,0 +1,52 @@
# Generated by Django 5.1.2 on 2024-12-22 20:38
import django.db.models.deletion
import ram.utils
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0016_basebook_book_catalogue"),
]
operations = [
migrations.AlterModelOptions(
name="basebook",
options={},
),
migrations.CreateModel(
name="BaseBookDocument",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("description", models.CharField(blank=True, max_length=128)),
(
"file",
models.FileField(
storage=ram.utils.DeduplicatedStorage(), upload_to="files/"
),
),
("private", models.BooleanField(default=False)),
(
"book",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="document",
to="bookshelf.basebook",
),
),
],
options={
"unique_together": {("book", "file")},
},
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.1.4 on 2024-12-22 20:44
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0017_alter_basebook_options_basebookdocument"),
]
operations = [
migrations.AlterModelOptions(
name="basebookdocument",
options={"verbose_name_plural": "Documents"},
),
]

View File

150
ram/bookshelf/models.py Normal file
View File

@@ -0,0 +1,150 @@
import os
import shutil
from django.db import models
from django.conf import settings
from django.urls import reverse
from django_countries.fields import CountryField
from tinymce import models as tinymce
from metadata.models import Tag
from ram.utils import DeduplicatedStorage
from ram.models import BaseModel, Image, Document, PropertyInstance
from metadata.models import Scale, Manufacturer
class Publisher(models.Model):
name = models.CharField(max_length=200)
country = CountryField(blank=True)
website = models.URLField(blank=True)
class Meta:
ordering = ["name"]
def __str__(self):
return self.name
class Author(models.Model):
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
class Meta:
ordering = ["last_name", "first_name"]
def __str__(self):
return f"{self.last_name}, {self.first_name}"
def short_name(self):
return f"{self.last_name} {self.first_name[0]}."
class BaseBook(BaseModel):
ISBN = models.CharField(max_length=17, blank=True) # 13 + dashes
language = models.CharField(
max_length=7,
choices=settings.LANGUAGES,
default='en'
)
number_of_pages = models.SmallIntegerField(null=True, blank=True)
publication_year = models.SmallIntegerField(null=True, blank=True)
description = tinymce.HTMLField(blank=True)
purchase_date = models.DateField(null=True, blank=True)
tags = models.ManyToManyField(
Tag, related_name="bookshelf", blank=True
)
def delete(self, *args, **kwargs):
shutil.rmtree(
os.path.join(
settings.MEDIA_ROOT, "images", "books", str(self.uuid)
),
ignore_errors=True
)
super(BaseBook, self).delete(*args, **kwargs)
def book_image_upload(instance, filename):
return os.path.join(
"images",
"books",
str(instance.book.uuid),
filename
)
class BaseBookImage(Image):
book = models.ForeignKey(
BaseBook, on_delete=models.CASCADE, related_name="image"
)
image = models.ImageField(
upload_to=book_image_upload,
storage=DeduplicatedStorage,
)
class BaseBookDocument(Document):
book = models.ForeignKey(
BaseBook, on_delete=models.CASCADE, related_name="document"
)
class Meta:
verbose_name_plural = "Documents"
unique_together = ("book", "file")
class BaseBookProperty(PropertyInstance):
book = models.ForeignKey(
BaseBook,
on_delete=models.CASCADE,
null=False,
blank=False,
related_name="property",
)
class Book(BaseBook):
title = models.CharField(max_length=200)
authors = models.ManyToManyField(Author, blank=True)
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
class Meta:
ordering = ["title"]
def __str__(self):
return self.title
def publisher_name(self):
return self.publisher.name
def get_absolute_url(self):
return reverse(
"bookshelf_item",
kwargs={"selector": "book", "uuid": self.uuid}
)
class Catalog(BaseBook):
manufacturer = models.ForeignKey(
Manufacturer,
on_delete=models.CASCADE,
)
years = models.CharField(max_length=12)
scales = models.ManyToManyField(Scale)
class Meta:
ordering = ["manufacturer", "publication_year"]
def __str__(self):
scales = self.get_scales
return "%s %s %s" % (self.manufacturer.name, self.years, scales)
def get_absolute_url(self):
return reverse(
"bookshelf_item",
kwargs={"selector": "catalog", "uuid": self.uuid}
)
@property
def get_scales(self):
return "/".join([s.scale for s in self.scales.all()])

View File

@@ -0,0 +1,26 @@
from rest_framework import serializers
from bookshelf.models import Book, Author, Publisher
from metadata.serializers import TagSerializer
class AuthorSerializer(serializers.ModelSerializer):
class Meta:
model = Author
fields = "__all__"
class PublisherSerializer(serializers.ModelSerializer):
class Meta:
model = Publisher
fields = "__all__"
class BookSerializer(serializers.ModelSerializer):
authors = AuthorSerializer(many=True)
publisher = PublisherSerializer()
tags = TagSerializer(many=True)
class Meta:
model = Book
fields = "__all__"
read_only_fields = ("creation_time", "updated_time")

3
ram/bookshelf/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

7
ram/bookshelf/urls.py Normal file
View File

@@ -0,0 +1,7 @@
from django.urls import path
from bookshelf.views import BookList, BookGet
urlpatterns = [
path("book/list", BookList.as_view()),
path("book/get/<uuid:uuid>", BookGet.as_view()),
]

21
ram/bookshelf/views.py Normal file
View File

@@ -0,0 +1,21 @@
from rest_framework.generics import ListAPIView, RetrieveAPIView
from rest_framework.schemas.openapi import AutoSchema
from bookshelf.models import Book
from bookshelf.serializers import BookSerializer
class BookList(ListAPIView):
serializer_class = BookSerializer
def get_queryset(self):
return Book.objects.get_published(self.request.user)
class BookGet(RetrieveAPIView):
serializer_class = BookSerializer
lookup_field = "uuid"
schema = AutoSchema(operation_id_base="retrieveBookByUUID")
def get_queryset(self):
return Book.objects.get_published(self.request.user)

View File

@@ -8,7 +8,8 @@ class ConsistItemInline(SortableInlineAdminMixin, admin.TabularInline):
model = ConsistItem
min_num = 1
extra = 0
readonly_fields = ("address", "type", "company", "era")
autocomplete_fields = ("rolling_stock",)
readonly_fields = ("preview", "published", "address", "type", "company", "era")
@admin.register(Consist)
@@ -18,19 +19,22 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
"creation_time",
"updated_time",
)
list_display = ("identifier", "company", "era")
list_display = ("identifier", "published", "company", "era")
list_filter = list_display
search_fields = list_display
save_as = True
fieldsets = (
(
None,
{
"fields": (
"published",
"identifier",
"consist_address",
"company",
"era",
"image",
"notes",
"tags",
)

View File

@@ -0,0 +1,17 @@
# Generated by Django 4.0.6 on 2022-07-10 13:19
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('consist', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='consist',
options={'ordering': ['creation_time']},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.6 on 2022-07-12 12:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('consist', '0002_alter_consist_options'),
]
operations = [
migrations.AddField(
model_name='consist',
name='image',
field=models.ImageField(blank=True, null=True, upload_to='images/'),
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 4.0.6 on 2022-07-15 16:08
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('metadata', '0004_alter_rollingstocktype_options_and_more'),
('consist', '0003_consist_image'),
]
operations = [
migrations.AlterField(
model_name='consist',
name='company',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='metadata.company'),
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 4.1 on 2022-08-23 15:54
# ckeditor removal
# import ckeditor_uploader.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("consist", "0004_alter_consist_company"),
]
operations = [
# migrations.AlterField(
# model_name="consist",
# name="notes",
# field=ckeditor_uploader.fields.RichTextUploadingField(blank=True),
# ),
]

View File

@@ -0,0 +1,34 @@
# Generated by Django 4.1 on 2022-08-24 15:30
import markdown
from django.db import migrations
def md_to_html(apps, schema_editor):
fields = {
"Consist": ["notes"],
}
for m in fields.items():
model = apps.get_model("consist", m[0])
for row in model.objects.all():
for field in m[1]:
html = markdown.markdown(getattr(row, field))
row.__dict__[field] = html
row.save(update_fields=m[1])
class Migration(migrations.Migration):
dependencies = [
("consist", "0005_alter_consist_notes"),
]
operations = [
migrations.RunPython(
md_to_html,
reverse_code=migrations.RunPython.noop
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 4.1.2 on 2022-11-27 00:10
from django.db import migrations, models
import ram.utils
class Migration(migrations.Migration):
dependencies = [
("consist", "0006_md_to_html"),
]
operations = [
migrations.AlterField(
model_name="consist",
name="image",
field=models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to="images/",
),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 4.1.3 on 2023-01-02 15:03
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("consist", "0007_alter_consist_image"),
]
operations = [
migrations.AlterModelOptions(
name="consist",
options={"ordering": ["company", "-creation_time"]},
),
]

View File

@@ -0,0 +1,51 @@
# Generated by Django 4.2.6 on 2023-10-31 09:41
import os
import sys
import shutil
import ram.utils
from django.conf import settings
from django.db import migrations, models
def move_images(apps, schema_editor):
sys.stdout.write("\n Processing files. Please await...")
model = apps.get_model("consist", "Consist")
for r in model.objects.all():
if not r.image: # exit the loop if there's no image
continue
fname = os.path.basename(r.image.path)
new_image = os.path.join("images", "consists", fname)
new_path = os.path.join(settings.MEDIA_ROOT, new_image)
os.makedirs(os.path.dirname(new_path), exist_ok=True)
try:
shutil.move(r.image.path, new_path)
except FileNotFoundError:
sys.stderr.write(" !! FileNotFoundError: {}\n".format(new_image))
pass
r.image.name = new_image
r.save()
class Migration(migrations.Migration):
dependencies = [
("consist", "0008_alter_consist_options"),
]
operations = [
migrations.AlterField(
model_name="consist",
name="image",
field=models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to="images/consists",
),
),
migrations.RunPython(
move_images,
reverse_code=migrations.RunPython.noop
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.0.2 on 2024-02-17 12:19
import tinymce.models
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("consist", "0009_alter_consist_image"),
]
operations = [
migrations.AlterField(
model_name="consist",
name="notes",
field=tinymce.models.HTMLField(blank=True),
),
]

View File

@@ -0,0 +1,30 @@
# Generated by Django 5.0.4 on 2024-04-20 12:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("consist", "0010_alter_consist_notes"),
]
operations = [
migrations.AlterField(
model_name="consist",
name="consist_address",
field=models.SmallIntegerField(
blank=True,
default=None,
help_text="DCC consist address if enabled",
null=True,
),
),
migrations.AlterField(
model_name="consist",
name="era",
field=models.CharField(
blank=True, help_text="Era or epoch of the consist", max_length=32
),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2024-11-04 12:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("consist", "0011_alter_consist_consist_address_alter_consist_era"),
]
operations = [
migrations.AddField(
model_name="consist",
name="published",
field=models.BooleanField(default=True),
),
]

View File

@@ -1,32 +1,53 @@
from uuid import uuid4
import os
from django.db import models
from django.urls import reverse
from django.dispatch import receiver
from django.core.exceptions import ValidationError
from ram.models import BaseModel
from ram.utils import DeduplicatedStorage
from metadata.models import Company, Tag
from roster.models import RollingStock
class Consist(models.Model):
uuid = models.UUIDField(primary_key=True, default=uuid4, editable=False)
class Consist(BaseModel):
identifier = models.CharField(max_length=128, unique=False)
tags = models.ManyToManyField(Tag, related_name="consist", blank=True)
consist_address = models.SmallIntegerField(
default=None, null=True, blank=True
default=None,
null=True,
blank=True,
help_text="DCC consist address if enabled",
)
company = models.ForeignKey(
Company, on_delete=models.CASCADE, null=True, blank=True
company = models.ForeignKey(Company, on_delete=models.CASCADE)
era = models.CharField(
max_length=32,
blank=True,
help_text="Era or epoch of the consist",
)
image = models.ImageField(
upload_to=os.path.join("images", "consists"),
storage=DeduplicatedStorage,
null=True,
blank=True,
)
era = models.CharField(max_length=32, blank=True)
notes = models.TextField(blank=True)
creation_time = models.DateTimeField(auto_now_add=True)
updated_time = models.DateTimeField(auto_now=True)
def __str__(self):
return "{0}".format(self.identifier)
return "{0} {1}".format(self.company, self.identifier)
def get_absolute_url(self):
return reverse("consist", kwargs={"uuid": self.uuid})
def clean(self):
if self.consist_item.filter(rolling_stock__published=False).exists():
raise ValidationError(
"You must publish all items in the consist before publishing the consist." # noqa: E501
)
class Meta:
ordering = ["company", "-creation_time"]
class ConsistItem(models.Model):
consist = models.ForeignKey(
@@ -41,6 +62,13 @@ class ConsistItem(models.Model):
def __str__(self):
return "{0}".format(self.rolling_stock)
def published(self):
return self.rolling_stock.published
published.boolean = True
def preview(self):
return self.rolling_stock.image.first().image_thumbnail(100)
def type(self):
return self.rolling_stock.rolling_class.type
@@ -52,3 +80,14 @@ class ConsistItem(models.Model):
def era(self):
return self.rolling_stock.era
# Unpublish any consist that contains an unpublished rolling stock
# this signal is called after a rolling stock is saved
# it is hosted here to avoid circular imports
@receiver(models.signals.post_save, sender=RollingStock)
def post_save_unpublish_consist(sender, instance, *args, **kwargs):
consists = Consist.objects.filter(consist_item__rolling_stock=instance)
for consist in consists:
consist.published = False
consist.save()

View File

@@ -5,11 +5,15 @@ from consist.serializers import ConsistSerializer
class ConsistList(ListAPIView):
queryset = Consist.objects.all()
serializer_class = ConsistSerializer
def get_queryset(self):
return Consist.objects.get_published(self.request.user)
class ConsistGet(RetrieveAPIView):
queryset = Consist.objects.all()
serializer_class = ConsistSerializer
lookup_field = "uuid"
def get_queryset(self):
return Consist.objects.get_published(self.request.user)

View File

@@ -7,6 +7,10 @@ from driver.models import DriverConfiguration
@admin.register(DriverConfiguration)
class DriverConfigurationAdmin(SingletonModelAdmin):
fieldsets = (
(
"General configuration",
{"fields": ("enabled",)},
),
(
"Remote DCC-EX configuration",
{

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.6 on 2022-07-11 18:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('driver', '0004_alter_driverconfiguration_remote_host'),
]
operations = [
migrations.AddField(
model_name='driverconfiguration',
name='enabled',
field=models.BooleanField(default=False),
),
]

View File

@@ -1,10 +1,11 @@
from django.db import models
from django.core.exceptions import ValidationError
from ipaddress import IPv4Address, IPv4Network
from ipaddress import IPv4Network
from solo.models import SingletonModel
class DriverConfiguration(SingletonModel):
enabled = models.BooleanField(default=False)
remote_host = models.GenericIPAddressField(
protocol="IPv4", default="192.168.4.1"
)

View File

@@ -34,9 +34,22 @@ def addresschecker(f):
return addresslookup
class IsEnabled(BasePermission):
def has_permission(self, request, view):
config = DriverConfiguration.get_solo()
# if driver is disabled, block all connections
if not config.enabled:
raise Http404
return True
class Firewall(BasePermission):
def has_permission(self, request, view):
config = DriverConfiguration.get_solo()
# if network is not configured, accept only read ops
if not config.network:
return request.method in SAFE_METHODS
@@ -61,7 +74,7 @@ class Test(APIView):
"""
parser_classes = [PlainTextParser]
permission_classes = [IsAuthenticated | Firewall]
permission_classes = [IsEnabled & IsAuthenticated | Firewall]
def get(self, request):
response = Connector().passthrough("<s>")
@@ -76,7 +89,7 @@ class SendCommand(APIView):
"""
parser_classes = [PlainTextParser]
permission_classes = [IsAuthenticated | Firewall]
permission_classes = [IsEnabled & IsAuthenticated | Firewall]
def put(self, request):
data = request.data
@@ -101,7 +114,7 @@ class Function(APIView):
Send "Function" commands to a valid DCC address
"""
permission_classes = [IsAuthenticated | Firewall]
permission_classes = [IsEnabled & IsAuthenticated | Firewall]
def put(self, request, address):
serializer = FunctionSerializer(data=request.data)
@@ -118,7 +131,7 @@ class Cab(APIView):
Send "Cab" commands to a valid DCC address
"""
permission_classes = [IsAuthenticated | Firewall]
permission_classes = [IsEnabled & IsAuthenticated | Firewall]
def put(self, request, address):
serializer = CabSerializer(data=request.data)
@@ -134,7 +147,7 @@ class Infra(APIView):
Send "Infra" commands to a valid DCC address
"""
permission_classes = [IsAuthenticated | Firewall]
permission_classes = [IsEnabled & IsAuthenticated | Firewall]
def put(self, request):
serializer = InfraSerializer(data=request.data)
@@ -150,7 +163,7 @@ class Emergency(APIView):
Send an "Emergency" stop, no matter the HTTP method used
"""
permission_classes = [IsAuthenticated | Firewall]
permission_classes = [IsEnabled & IsAuthenticated | Firewall]
def put(self, request):
Connector().emergency()

View File

@@ -1,7 +1,10 @@
from django.contrib import admin
from adminsortable2.admin import SortableAdminMixin
from metadata.models import (
Property,
Decoder,
DecoderDocument,
Scale,
Manufacturer,
Company,
@@ -12,21 +15,30 @@ from metadata.models import (
@admin.register(Property)
class PropertyAdmin(admin.ModelAdmin):
list_display = ("name", "private")
search_fields = ("name",)
class DecoderDocInline(admin.TabularInline):
model = DecoderDocument
min_num = 0
extra = 0
classes = ["collapse"]
@admin.register(Decoder)
class DecoderAdmin(admin.ModelAdmin):
inlines = (DecoderDocInline,)
readonly_fields = ("image_thumbnail",)
list_display = ("__str__", "interface")
list_filter = ("manufacturer", "interface")
search_fields = ("__str__",)
list_display = ("__str__", "sound")
list_filter = ("manufacturer", "sound")
search_fields = ("name", "manufacturer__name")
@admin.register(Scale)
class ScaleAdmin(admin.ModelAdmin):
list_display = ("scale", "ratio", "gauge")
list_filter = ("ratio", "gauge")
list_display = ("scale", "ratio", "gauge", "tracks")
list_filter = ("ratio", "gauge", "tracks")
search_fields = list_display
@@ -54,7 +66,7 @@ class TagAdmin(admin.ModelAdmin):
@admin.register(RollingStockType)
class RollingStockTypeAdmin(admin.ModelAdmin):
class RollingStockTypeAdmin(SortableAdminMixin, admin.ModelAdmin):
list_display = ("__str__",)
list_filter = ("type", "category")
search_fields = list_display
search_fields = ("type", "category")

View File

@@ -0,0 +1,19 @@
# Generated by Django 4.0.6 on 2022-07-10 21:46
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('metadata', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='decoder',
name='manufacturer',
field=models.ForeignKey(limit_choices_to={'category': 'model'}, on_delete=django.db.models.deletion.CASCADE, to='metadata.manufacturer'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.6 on 2022-07-12 10:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('metadata', '0002_alter_decoder_manufacturer'),
]
operations = [
migrations.AddField(
model_name='property',
name='private',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 4.0.6 on 2022-07-14 14:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('metadata', '0003_property_private'),
]
operations = [
migrations.AlterModelOptions(
name='rollingstocktype',
options={'ordering': ['order']},
),
migrations.AddField(
model_name='rollingstocktype',
name='order',
field=models.PositiveSmallIntegerField(default=0),
preserve_default=False,
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.6 on 2022-07-18 21:40
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('metadata', '0004_alter_rollingstocktype_options_and_more'),
]
operations = [
migrations.RenameField(
model_name='scale',
old_name='gauge',
new_name='track',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.6 on 2022-07-18 21:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('metadata', '0005_rename_gauge_scale_track'),
]
operations = [
migrations.AddField(
model_name='scale',
name='gauge',
field=models.CharField(blank=True, max_length=16),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.6 on 2022-07-18 21:44
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('metadata', '0006_scale_gauge'),
]
operations = [
migrations.RenameField(
model_name='scale',
old_name='track',
new_name='tracks',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.1.2 on 2022-10-31 23:01
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("metadata", "0007_rename_track_scale_tracks"),
("roster", "0013_rollingstock_decoder_interface"),
]
operations = [
migrations.RemoveField(
model_name="decoder",
name="interface",
),
]

View File

@@ -0,0 +1,44 @@
# Generated by Django 4.1.2 on 2022-11-27 00:10
from django.db import migrations, models
import ram.utils
class Migration(migrations.Migration):
dependencies = [
("metadata", "0008_remove_decoder_interface"),
]
operations = [
migrations.AlterField(
model_name="company",
name="logo",
field=models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to="images/",
),
),
migrations.AlterField(
model_name="decoder",
name="image",
field=models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to="images/",
),
),
migrations.AlterField(
model_name="manufacturer",
name="logo",
field=models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to="images/",
),
),
]

View File

@@ -0,0 +1,26 @@
# Generated by Django 4.1.5 on 2023-01-06 00:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("metadata", "0009_alter_company_logo_alter_decoder_image_and_more"),
]
operations = [
migrations.AlterField(
model_name="manufacturer",
name="category",
field=models.CharField(
choices=[
("model", "Model"),
("real", "Real"),
("accessory", "Accessory"),
("other", "Other"),
],
max_length=64,
),
),
]

View File

@@ -0,0 +1,93 @@
# Generated by Django 4.1.5 on 2023-01-09 11:22
from django.db import migrations, models
from ram.utils import slugify
def create_slug(apps, schema_editor):
fields = ["Company", "Manufacturer", "RollingStockType", "Scale"]
for m in fields:
model = apps.get_model("metadata", m)
for row in model.objects.all():
if hasattr(row, "type"):
row.slug = slugify("{} {}".format(row.type, row.category))
elif hasattr(row, "scale"):
row.slug = slugify(row.scale)
else:
row.slug = slugify(row.name)
row.save(update_fields=["slug"])
class Migration(migrations.Migration):
dependencies = [
("metadata", "0010_alter_manufacturer_category"),
]
operations = [
migrations.AddField(
model_name="company",
name="slug",
field=models.CharField(
editable=False, max_length=64, blank=True
),
),
migrations.AddField(
model_name="manufacturer",
name="slug",
field=models.CharField(
editable=False, max_length=128, blank=True
),
),
migrations.AddField(
model_name="rollingstocktype",
name="slug",
field=models.CharField(
editable=False, max_length=128, blank=True
),
),
migrations.AddField(
model_name="scale",
name="slug",
field=models.CharField(
editable=False, max_length=32, blank=True
),
),
migrations.RunPython(
create_slug,
reverse_code=migrations.RunPython.noop
),
migrations.AlterField(
model_name="company",
name="slug",
field=models.CharField(
editable=False, max_length=64, unique=True
),
),
migrations.AlterField(
model_name="manufacturer",
name="slug",
field=models.CharField(
editable=False, max_length=128, unique=True
),
),
migrations.AlterField(
model_name="rollingstocktype",
name="slug",
field=models.CharField(
editable=False, max_length=128, unique=True
),
),
migrations.AlterField(
model_name="scale",
name="slug",
field=models.CharField(
editable=False, max_length=32, unique=True
),
),
]

View File

@@ -0,0 +1,59 @@
# Generated by Django 4.2 on 2023-09-30 21:54
from django.db import migrations, models
import django.db.models.deletion
import ram.utils
class Migration(migrations.Migration):
dependencies = [
("metadata", "0011_company_slug_and_more"),
]
operations = [
migrations.AlterField(
model_name="decoder",
name="manufacturer",
field=models.ForeignKey(
limit_choices_to={"category": "accessory"},
on_delete=django.db.models.deletion.CASCADE,
to="metadata.manufacturer",
),
),
migrations.CreateModel(
name="DecoderDocument",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("description", models.CharField(blank=True, max_length=128)),
(
"file",
models.FileField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage(),
upload_to="files/",
),
),
(
"decoder",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="document",
to="metadata.decoder",
),
),
],
options={
"unique_together": {("decoder", "file")},
},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.2.5 on 2023-10-06 19:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("metadata", "0012_alter_decoder_manufacturer_decoderdocument"),
]
operations = [
migrations.AddField(
model_name="decoderdocument",
name="private",
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 4.2.6 on 2023-10-10 12:44
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("metadata", "0013_decoderdocument_private"),
]
operations = [
migrations.AlterModelOptions(
name="decoder",
options={"ordering": ["manufacturer", "name"]},
),
migrations.AlterModelOptions(
name="tag",
options={"ordering": ["name"]},
),
]

View File

@@ -0,0 +1,80 @@
# Generated by Django 4.2.6 on 2023-10-30 13:16
import os
import sys
import shutil
import ram.utils
from django.conf import settings
from django.db import migrations, models
def move_images(apps, schema_editor):
fields = {
"Company": ["companies", "logo"],
"Decoder": ["decoders", "image"],
"Manufacturer": ["manufacturers", "logo"],
}
sys.stdout.write("\n Processing files. Please await...")
for m in fields.items():
model = apps.get_model("metadata", m[0])
for r in model.objects.all():
field = getattr(r, m[1][1])
if not field: # exit the loop if there's no image
continue
fname = os.path.basename(field.path)
new_image = os.path.join("images", m[1][0], fname)
new_path = os.path.join(settings.MEDIA_ROOT, new_image)
os.makedirs(os.path.dirname(new_path), exist_ok=True)
try:
shutil.move(field.path, new_path)
except FileNotFoundError:
sys.stderr.write(
" !! FileNotFoundError: {}\n".format(new_image)
)
pass
field.name = new_image
r.save()
class Migration(migrations.Migration):
dependencies = [
("metadata", "0014_alter_decoder_options_alter_tag_options"),
]
operations = [
migrations.AlterField(
model_name="company",
name="logo",
field=models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to="images/companies",
),
),
migrations.AlterField(
model_name="decoder",
name="image",
field=models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to="images/decoders",
),
),
migrations.AlterField(
model_name="manufacturer",
name="logo",
field=models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to="images/manufacturers",
),
),
migrations.RunPython(
move_images,
reverse_code=migrations.RunPython.noop
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 4.2.6 on 2023-11-04 22:53
from django.db import migrations, models
import ram.utils
class Migration(migrations.Migration):
dependencies = [
("metadata", "0015_alter_company_logo_alter_decoder_image_and_more"),
]
operations = [
migrations.AlterField(
model_name="decoderdocument",
name="file",
field=models.FileField(
storage=ram.utils.DeduplicatedStorage(), upload_to="files/"
),
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 5.0.4 on 2024-04-20 12:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("metadata", "0016_alter_decoderdocument_file"),
]
operations = [
migrations.AlterField(
model_name="property",
name="private",
field=models.BooleanField(
default=False, help_text="Property will be only visible to logged users"
),
),
]

View File

@@ -0,0 +1,69 @@
# Generated by Django 5.1.2 on 2024-11-04 21:17
import django.db.migrations.operations.special
import metadata.models
from django.db import migrations, models
def gen_ratio(apps, schema_editor):
Scale = apps.get_model('metadata', 'Scale')
for row in Scale.objects.all():
row.ratio_int = metadata.models.calculate_ratio(row.ratio)
row.save(update_fields=['ratio_int'])
def convert_tarcks(apps, schema_editor):
Scale = apps.get_model("metadata", "Scale")
for row in Scale.objects.all():
row.tracks = "".join(
filter(
lambda x: str.isdigit(x) or x == "." or x == ",",
row.tracks
)
)
row.save(update_fields=["tracks"])
class Migration(migrations.Migration):
dependencies = [
('metadata', '0017_alter_property_private'),
]
operations = [
migrations.AlterModelOptions(
name='decoder',
options={'ordering': ['manufacturer__name', 'name']},
),
migrations.AlterModelOptions(
name='scale',
options={'ordering': ['ratio_int', 'scale']},
),
migrations.AddField(
model_name='scale',
name='ratio_int',
field=models.SmallIntegerField(default=0, editable=False),
),
migrations.RunPython(
code=gen_ratio,
reverse_code=django.db.migrations.operations.special.RunPython.noop,
),
migrations.AlterField(
model_name='scale',
name='ratio',
field=models.CharField(max_length=16, validators=[metadata.models.calculate_ratio]),
),
migrations.AlterModelOptions(
name='scale',
options={'ordering': ['-ratio_int', '-tracks', 'scale']},
),
migrations.RunPython(
code=convert_tarcks,
reverse_code=django.db.migrations.operations.special.RunPython.noop,
),
migrations.AlterField(
model_name='scale',
name='tracks',
field=models.FloatField(help_text='Distance between model tracks in mm'),
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 5.1.2 on 2024-11-04 21:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("metadata", "0018_alter_decoder_options_alter_scale_options_and_more"),
]
operations = [
migrations.AlterField(
model_name="scale",
name="gauge",
field=models.CharField(
blank=True,
help_text="Distance between real tracks. Please specify the unit (mm, in, ...)",
max_length=16,
),
),
]

View File

@@ -1,13 +1,22 @@
import os
from django.db import models
from django.urls import reverse
from django.conf import settings
from django.dispatch.dispatcher import receiver
from django.core.exceptions import ValidationError
from django_countries.fields import CountryField
from ram.utils import get_image_preview, slugify
from ram.models import Document
from ram.utils import DeduplicatedStorage, get_image_preview, slugify
from ram.managers import PublicManager
class Property(models.Model):
name = models.CharField(max_length=128, unique=True)
private = models.BooleanField(
default=False,
help_text="Property will be only visible to logged users",
)
class Meta:
verbose_name_plural = "Properties"
@@ -16,14 +25,22 @@ class Property(models.Model):
def __str__(self):
return self.name
objects = PublicManager()
class Manufacturer(models.Model):
name = models.CharField(max_length=128, unique=True)
slug = models.CharField(max_length=128, unique=True, editable=False)
category = models.CharField(
max_length=64, choices=settings.MANUFACTURER_TYPES
)
website = models.URLField(blank=True)
logo = models.ImageField(upload_to="images/", null=True, blank=True)
logo = models.ImageField(
upload_to=os.path.join("images", "manufacturers"),
storage=DeduplicatedStorage,
null=True,
blank=True,
)
class Meta:
ordering = ["category", "name"]
@@ -31,6 +48,15 @@ class Manufacturer(models.Model):
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse(
"filtered",
kwargs={
"_filter": "manufacturer",
"search": self.slug,
},
)
def logo_thumbnail(self):
return get_image_preview(self.logo.url)
@@ -39,10 +65,16 @@ class Manufacturer(models.Model):
class Company(models.Model):
name = models.CharField(max_length=64, unique=True)
slug = models.CharField(max_length=64, unique=True, editable=False)
extended_name = models.CharField(max_length=128, blank=True)
country = CountryField()
freelance = models.BooleanField(default=False)
logo = models.ImageField(upload_to="images/", null=True, blank=True)
logo = models.ImageField(
upload_to=os.path.join("images", "companies"),
storage=DeduplicatedStorage,
null=True,
blank=True,
)
class Meta:
verbose_name_plural = "Companies"
@@ -51,6 +83,18 @@ class Company(models.Model):
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse(
"filtered",
kwargs={
"_filter": "company",
"search": self.slug,
},
)
def extended_name_pp(self):
return "({})".format(self.extended_name) if self.extended_name else ""
def logo_thumbnail(self):
return get_image_preview(self.logo.url)
@@ -59,13 +103,22 @@ class Company(models.Model):
class Decoder(models.Model):
name = models.CharField(max_length=128, unique=True)
manufacturer = models.ForeignKey(Manufacturer, on_delete=models.CASCADE)
version = models.CharField(max_length=64, blank=True)
interface = models.PositiveSmallIntegerField(
choices=settings.DECODER_INTERFACES, null=True, blank=True
manufacturer = models.ForeignKey(
Manufacturer,
on_delete=models.CASCADE,
limit_choices_to={"category": "accessory"},
)
version = models.CharField(max_length=64, blank=True)
sound = models.BooleanField(default=False)
image = models.ImageField(upload_to="images/", null=True, blank=True)
image = models.ImageField(
upload_to=os.path.join("images", "decoders"),
storage=DeduplicatedStorage,
null=True,
blank=True,
)
class Meta(object):
ordering = ["manufacturer__name", "name"]
def __str__(self):
return "{0} - {1}".format(self.manufacturer, self.name)
@@ -76,39 +129,107 @@ class Decoder(models.Model):
image_thumbnail.short_description = "Preview"
class Scale(models.Model):
scale = models.CharField(max_length=32, unique=True)
ratio = models.CharField(max_length=16, blank=True)
gauge = models.CharField(max_length=16, blank=True)
class DecoderDocument(Document):
decoder = models.ForeignKey(
Decoder, on_delete=models.CASCADE, related_name="document"
)
class Meta:
ordering = ["scale"]
unique_together = ("decoder", "file")
def calculate_ratio(ratio):
try:
num, den = ratio.split(":")
return int(num) / float(den) * 10000
except (ValueError, ZeroDivisionError):
raise ValidationError("Invalid ratio format")
class Scale(models.Model):
scale = models.CharField(max_length=32, unique=True)
slug = models.CharField(max_length=32, unique=True, editable=False)
ratio = models.CharField(max_length=16, validators=[calculate_ratio])
ratio_int = models.SmallIntegerField(editable=False, default=0)
tracks = models.FloatField(
help_text="Distance between model tracks in mm",
)
gauge = models.CharField(
max_length=16,
blank=True,
help_text="Distance between real tracks. Please specify the unit (mm, in, ...)", # noqa: E501
)
class Meta:
ordering = ["-ratio_int", "-tracks", "scale"]
def get_absolute_url(self):
return reverse(
"filtered",
kwargs={
"_filter": "scale",
"search": self.slug,
},
)
def __str__(self):
return str(self.scale)
@receiver(models.signals.pre_save, sender=Scale)
def scale_save(sender, instance, **kwargs):
instance.ratio_int = calculate_ratio(instance.ratio)
class RollingStockType(models.Model):
type = models.CharField(max_length=64)
order = models.PositiveSmallIntegerField()
category = models.CharField(
max_length=64, choices=settings.ROLLING_STOCK_TYPES
)
slug = models.CharField(max_length=128, unique=True, editable=False)
class Meta(object):
unique_together = ("category", "type")
ordering = ["order"]
def get_absolute_url(self):
return reverse(
"filtered",
kwargs={
"_filter": "type",
"search": self.slug,
},
)
def __str__(self):
return "{0} {1}".format(self.type, self.category)
class Tag(models.Model):
name = models.CharField(max_length=128, unique=True)
slug = models.CharField(max_length=128, unique=True)
class Meta(object):
ordering = ["name"]
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse(
"filtered",
kwargs={
"_filter": "tag",
"search": self.slug,
},
)
@receiver(models.signals.pre_save, sender=Manufacturer)
@receiver(models.signals.pre_save, sender=Company)
@receiver(models.signals.pre_save, sender=Scale)
@receiver(models.signals.pre_save, sender=RollingStockType)
@receiver(models.signals.pre_save, sender=Tag)
def tag_pre_save(sender, instance, **kwargs):
instance.slug = slugify(instance.name)
class RollingStockType(models.Model):
type = models.CharField(max_length=64)
category = models.CharField(
max_length=64, choices=settings.ROLLING_STOCK_TYPES
)
class Meta(object):
unique_together = ("category", "type")
def __str__(self):
return "{0} {1}".format(self.type, self.category)
def slug_pre_save(sender, instance, **kwargs):
instance.slug = slugify(instance.__str__())

View File

@@ -1,6 +1,68 @@
from django.contrib import admin
from solo.admin import SingletonModelAdmin
from portal.models import SiteConfiguration
from portal.models import SiteConfiguration, Flatpage
admin.site.register(SiteConfiguration, SingletonModelAdmin)
@admin.register(SiteConfiguration)
class SiteConfigurationAdmin(SingletonModelAdmin):
readonly_fields = ("site_name",)
fieldsets = (
(
None,
{
"fields": (
"site_name",
"site_author",
"about",
"items_per_page",
"items_ordering",
"footer",
"footer_extended",
)
},
),
(
"Advanced",
{
"classes": ("collapse",),
"fields": (
"show_version",
"use_cdn",
"extra_head",
),
},
),
)
@admin.register(Flatpage)
class FlatpageAdmin(admin.ModelAdmin):
readonly_fields = ("path", "creation_time", "updated_time")
list_display = ("name", "path", "published", "get_link")
list_filter = ("published",)
search_fields = ("name",)
fieldsets = (
(
None,
{
"fields": (
"name",
"path",
"content",
"published",
)
},
),
(
"Audit",
{
"classes": ("collapse",),
"fields": (
"creation_time",
"updated_time",
),
},
),
)

View File

@@ -0,0 +1,5 @@
from django.conf import settings
def default_card_image(request):
return {"DEFAULT_CARD_IMAGE": settings.DEFAULT_CARD_IMAGE}

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.6 on 2022-07-15 12:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portal', '0006_alter_siteconfiguration_site_name'),
]
operations = [
migrations.AddField(
model_name='siteconfiguration',
name='items_ordering',
field=models.CharField(choices=[('type', 'By rolling stock type'), ('company', 'By company name'), ('identifier', 'By rolling stock class')], default='type', max_length=10),
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 4.0.6 on 2022-08-07 15:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portal', '0007_siteconfiguration_items_ordering'),
]
operations = [
migrations.CreateModel(
name='Flatpage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=256, unique=True)),
('draft', models.BooleanField(default=True)),
('content', models.TextField(blank=True)),
],
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 4.0.6 on 2022-08-07 15:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portal', '0008_flatpage'),
]
operations = [
migrations.AddField(
model_name='flatpage',
name='path',
field=models.CharField(default='', max_length=256, unique=True),
preserve_default=False,
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 4.0.6 on 2022-08-07 15:46
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('portal', '0009_flatpage_path'),
]
operations = [
migrations.AddField(
model_name='flatpage',
name='creation_time',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='flatpage',
name='updated_time',
field=models.DateTimeField(auto_now=True),
),
]

View File

@@ -0,0 +1,36 @@
# Generated by Django 4.1 on 2022-08-23 15:54
# ckeditor dependency removal
# import ckeditor.fields
# import ckeditor_uploader.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("portal", "0010_flatpage_creation_time_flatpage_updated_time"),
]
operations = [
# migrations.AlterField(
# model_name="flatpage",
# name="content",
# field=ckeditor_uploader.fields.RichTextUploadingField(),
# ),
# migrations.AlterField(
# model_name="siteconfiguration",
# name="about",
# field=ckeditor.fields.RichTextField(blank=True),
# ),
# migrations.AlterField(
# model_name="siteconfiguration",
# name="footer",
# field=ckeditor.fields.RichTextField(blank=True),
# ),
# migrations.AlterField(
# model_name="siteconfiguration",
# name="footer_extended",
# field=ckeditor.fields.RichTextField(blank=True),
# ),
]

View File

@@ -0,0 +1,38 @@
# Generated by Django 4.1 on 2022-08-24 15:00
import markdown
from django.db import migrations
def md_to_html(apps, schema_editor):
fields = {
"SiteConfiguration": ["about", "footer", "footer_extended"],
"Flatpage": ["content"]
}
for m in fields.items():
model = apps.get_model("portal", m[0])
for row in model.objects.all():
for field in m[1]:
html = markdown.markdown(getattr(row, field))
row.__dict__[field] = html
row.save(update_fields=m[1])
class Migration(migrations.Migration):
dependencies = [
(
"portal",
"0011_alter_flatpage_content_alter_siteconfiguration_about_and_more",
),
]
operations = [
migrations.RunPython(
md_to_html,
reverse_code=migrations.RunPython.noop
),
]

View File

@@ -0,0 +1,42 @@
# Generated by Django 4.1 on 2022-08-25 10:18
from django.db import migrations, models
def reverse_bool(apps, schema_editor):
model = apps.get_model("portal", "Flatpage")
for row in model.objects.all():
row.published = not row.draft
row.save(update_fields=["published"])
def reverse_bool_back(apps, schema_editor):
model = apps.get_model("portal", "Flatpage")
for row in model.objects.all():
row.draft = not row.published
row.save(update_fields=["draft"])
class Migration(migrations.Migration):
dependencies = [
("portal", "0012_md_to_html"),
]
operations = [
migrations.AddField(
model_name="flatpage",
name="published",
field=models.BooleanField(default=False),
),
migrations.RunPython(
reverse_bool,
reverse_code=reverse_bool_back
),
migrations.RemoveField(
model_name="flatpage",
name="draft",
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.1.3 on 2022-12-28 22:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("portal", "0013_remove_flatpage_draft_flatpage_published"),
]
operations = [
migrations.AddField(
model_name="siteconfiguration",
name="extra_head",
field=models.TextField(blank=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.1.5 on 2023-01-03 15:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("portal", "0014_siteconfiguration_extra_head"),
]
operations = [
migrations.AddField(
model_name="siteconfiguration",
name="use_cdn",
field=models.BooleanField(default=True),
),
]

View File

@@ -0,0 +1,16 @@
# Generated by Django 5.0.1 on 2024-01-20 21:02
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("portal", "0015_siteconfiguration_use_cdn"),
]
operations = [
migrations.RemoveField(
model_name="siteconfiguration",
name="site_name",
),
]

View File

@@ -0,0 +1,34 @@
# Generated by Django 5.0.2 on 2024-02-17 12:19
import tinymce.models
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("portal", "0016_remove_siteconfiguration_site_name"),
]
operations = [
migrations.AlterField(
model_name="flatpage",
name="content",
field=tinymce.models.HTMLField(),
),
migrations.AlterField(
model_name="siteconfiguration",
name="about",
field=tinymce.models.HTMLField(blank=True),
),
migrations.AlterField(
model_name="siteconfiguration",
name="footer",
field=tinymce.models.HTMLField(blank=True),
),
migrations.AlterField(
model_name="siteconfiguration",
name="footer_extended",
field=tinymce.models.HTMLField(blank=True),
),
]

View File

@@ -1,24 +1,40 @@
import django
from django.db import models
from django.conf import settings
from django.urls import reverse
from django.dispatch.dispatcher import receiver
from django.utils.safestring import mark_safe
from solo.models import SingletonModel
from tinymce import models as tinymce
from ram import __version__ as app_version
from solo.models import SingletonModel
from ram.managers import PublicManager
from ram.utils import slugify
class SiteConfiguration(SingletonModel):
site_name = models.CharField(
max_length=256, default="Railroad Assets Manager"
)
site_author = models.CharField(max_length=256, blank=True)
about = models.TextField(blank=True)
about = tinymce.HTMLField(blank=True)
items_per_page = models.CharField(
max_length=2,
choices=[(str(x * 3), str(x * 3)) for x in range(2, 11)],
default="6",
)
footer = models.TextField(blank=True)
footer_extended = models.TextField(blank=True)
items_ordering = models.CharField(
max_length=10,
choices=[
("type", "By rolling stock type"),
("company", "By company name"),
("identifier", "By rolling stock class"),
],
default="type",
)
footer = tinymce.HTMLField(blank=True)
footer_extended = tinymce.HTMLField(blank=True)
show_version = models.BooleanField(default=True)
use_cdn = models.BooleanField(default=True)
extra_head = models.TextField(blank=True)
class Meta:
verbose_name = "Site Configuration"
@@ -26,8 +42,40 @@ class SiteConfiguration(SingletonModel):
def __str__(self):
return "Site Configuration"
def site_name(self):
return settings.SITE_NAME
def version(self):
return app_version
def django_version(self):
return django.get_version()
class Flatpage(models.Model):
name = models.CharField(max_length=256, unique=True)
path = models.CharField(max_length=256, unique=True)
published = models.BooleanField(default=False)
content = tinymce.HTMLField()
creation_time = models.DateTimeField(auto_now_add=True)
updated_time = models.DateTimeField(auto_now=True)
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse("flatpage", kwargs={"flatpage": self.path})
def get_link(self):
return mark_safe(
'<a href="{0}" target="_blank">Link</a>'.format(
self.get_absolute_url()
)
)
objects = PublicManager()
@receiver(models.signals.pre_save, sender=Flatpage)
def tag_pre_save(sender, instance, **kwargs):
instance.path = slugify(instance.name)

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More