From cfaf558821613c910b36d0b0526af62361cd7eef Mon Sep 17 00:00:00 2001 From: Jan Krupa Date: Wed, 21 Jan 2026 13:54:38 +0100 Subject: [PATCH 01/11] Add CHANGELOG.md for v6.0.0 release --- CHANGELOG.md | 619 +-------------------------------------------------- 1 file changed, 9 insertions(+), 610 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9191aa9..2ef288f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,621 +1,20 @@ # Changelog -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [6.0.0] - 2025-01-12 - -### BREAKING CHANGE - -- **NetBox 4.5.0 Required**: This version is ONLY compatible with NetBox 4.5.0 and later - - Filter system updated to use NetBox 4.5.0's new filter architecture - - NOT backward compatible with NetBox 4.4.x - - Users on NetBox 4.4.x must use plugin version 5.4.x or earlier - -### Removed (Phase 3 - Type-Specific Data Cleanup) - -- **Database**: Removed `type_specific_data` JSONField column from Segment table (migration 0035) -- **Models**: Removed legacy JSON validation functions from `segment_types.py`: - - `SEGMENT_TYPE_SCHEMAS` dictionary (174 lines of JSON schema definitions) - - `validate_segment_type_data()` - Runtime JSON validation - - `get_segment_type_schema()` - Schema retrieval - - `get_all_segment_types()` - Segment type listing -- **Segment Model**: Removed JSONField-related helper methods: - - `validate_type_specific_data()` - JSON validation method - - `get_type_specific_display()` - JSON formatting for templates - - `has_type_specific_data()` - JSON data check (model method; GraphQL method remains) -- **API**: `type_specific_data` field is now a computed field (no longer a JSONField) - - Returns structured data from relational models (DarkFiberSegmentData, OpticalSpectrumSegmentData, EthernetServiceSegmentData) - - Same field name as before, but underlying implementation is completely different - - Data comes from OneToOne related models instead of JSON blob -- **Filters**: Removed `has_type_specific_data` filter from segment filtersets -- **Forms**: Removed `has_type_specific_data` form field from segment filter forms -- **Total code reduction**: ~405 lines of legacy code removed - -### Changed - -- **API**: `type_specific_data` field completely reimplemented as computed field - - Now returns data from relational models instead of JSON blob - - Field name intentionally kept the same for API familiarity - - GraphQL field also renamed from `type_specific_technicals` to `type_specific_data` -- **Architecture**: Fully migrated to relational database schema for type-specific data - - All type-specific data stored in dedicated models (Phase 2) - - JSONField completely removed from database (Phase 3) - - Clean, normalized schema with proper database constraints - -### Technical Details - -- **Migration 0035**: One-way migration removes `type_specific_data` column -- **Data Safety**: All existing data preserved in relational models (migrated in Phase 2) -- **Performance**: Improved query performance with indexed relational fields -- **Validation**: Database-level constraints replace runtime JSON validation -- **Maintainability**: Django model fields replace 272 lines of JSON schema code - -### Non-Breaking Change Note - -**API field name remains the same**: `type_specific_data` -- Field name is unchanged from previous versions -- However, the underlying implementation is completely different: - - **Before 6.0.0**: Direct JSONField column in database - - **After 6.0.0**: Computed field reading from relational models -- **No API client changes required** - the field name is identical -- Data structure is also similar, making the transition seamless - -### Documentation - -- Added `PHASE_3_PLAN.md` - Detailed implementation plan -- Added `PHASE_3_COMPLETE.md` - Completion summary with metrics -- Added `RUN_MIGRATION_0035.md` - Migration instructions -- Added `PHASE_3_QUICK_VERIFICATION.md` - Post-migration verification - -## [5.4.0] - 2025-12-08 - +## [6.0.0] - 2026-01-21 ### Added - -- **Contract Information Management System**: Complete replacement of SegmentFinancialInfo with ContractInfo - - Versioned contract system with linear version chains (similar to Git commits) - - Support for contract amendments and renewals through NetBox clone functionality - - Many-to-many relationship between contracts and segments (one contract can cover multiple segments) - - Contract metadata tracking: contract number, type (new/amendment/renewal), effective dates - - Enhanced recurring charge tracking with configurable periods (monthly, quarterly, annually, etc.) - - Commitment end date calculation and tracking with visual indicators - - Contract version history visualization in UI - -- **Contract Versioning Features**: - - Linear version chain using linked list pattern (previous_version/superseded_by) - - Automatic version numbering (v1, v2, v3...) - - Version navigation: get first version, latest version, full version history - - Active/superseded contract status tracking - - Clone functionality for creating amendments and renewals - -- **Contract Financial Tracking**: - - Recurring charges with customizable periods (monthly, quarterly, semi-annually, annually, bi-annually) - - Number of recurring charge periods tracking - - Non-recurring charges for setup/installation fees - - Multi-currency support with immutable currency (set at contract creation) - - Automatic financial calculations: - - Total recurring cost (recurring charge × number of periods) - - Total contract value (recurring + non-recurring charges) - - Commitment end date (start date + recurring periods) - -- **Contract UI Components**: - - ContractInfo list view with advanced filtering - - Contract detail view with version history timeline - - Color-coded date badges for contract status (green/orange/red/gray) - - Interactive tooltips showing days remaining and contract status - - Version chain visualization showing contract evolution - - Financial summary panel with all calculations - - Navigation menu integration - -- **Contract API Enhancements**: - - New `/api/plugins/cesnet-service-path-plugin/contract-info/` endpoint - - Support for versioning fields in API: - - `previous_version`: Link to previous contract version - - `superseded_by`: Link to superseding contract version - - `is_active`: Boolean indicating if contract is current version - - `version`: Calculated version number - - Computed financial fields in API responses - - Advanced filtering: by active status, version status, contract type, currency, dates - -- **Segment View Enhancements**: - - M:N contract relationship support in segment detail view - - Display all contracts associated with a segment - - Color-coded contract status indicators - - Contract end date and commitment end date visualization - -- **GraphQL Support**: - - Updated GraphQL schema for ContractInfo model - - Support for querying contract versions and relationships - - Financial calculations available in GraphQL queries +- New features and improvements. ### Changed - -- **Breaking**: Replaced SegmentFinancialInfo model with ContractInfo model - - Changed from 1:1 segment-financial relationship to M:N segment-contract relationship - - Financial information now managed through contracts rather than directly on segments - - API endpoint changed from `/segment-financial-info/` to `/contract-info/` - -- **Database Schema**: - - Removed SegmentFinancialInfo table - - Added ContractInfo table with versioning support - - Added ContractSegmentMapping join table for M:N relationships - - Migration automatically converts existing financial data to contracts - -- **Financial Field Changes**: - - Renamed `monthly_charge` to `recurring_charge` with configurable period - - Added `recurring_charge_period` field (monthly, quarterly, annually, etc.) - - Renamed `commitment_period_months` to `number_of_recurring_charges` - - Renamed `recurring_charge_end_date` to `commitment_end_date` for clarity - - Made recurring charge fields nullable to support amendments without recurring charges - -- **Model Improvements**: - - Currency is now immutable after contract creation (cannot be changed in amendments) - - All contract attributes can be updated through versioning (except currency) - - Enhanced clone functionality for proper M2M relationship handling - - Improved date calculations and validations - -- **Color-Coded Date Visualization**: - - Contract end dates now show color-coded status badges - - Commitment end dates display with visual indicators: - - Green: Date has passed - - Orange: Within 30 days of expiration - - Red: More than 30 days remaining - - Gray: Date not set - - Interactive tooltips showing exact dates and days remaining - -### Removed - -- **Breaking**: SegmentFinancialInfo model and related components - - Removed `/api/plugins/cesnet-service-path-plugin/segment-financial-info/` endpoint - - Removed SegmentFinancialInfo views, forms, tables, and serializers - - Removed direct financial relationship from segments +- Enhancements to existing functionalities. ### Fixed +- Bugs addressed in this release. -- Improved decimal handling in financial calculations -- Enhanced date validation for contract periods -- Better error handling for version chain operations -- Fixed M2M relationship serialization in API responses - -### Migration Notes - -- **Database Migration Required**: Migration 0033 automatically converts SegmentFinancialInfo to ContractInfo -- **Data Preservation**: All existing financial data is preserved during migration: - - Monthly charges → recurring charges (monthly period) - - Commitment period months → number of recurring charges - - Segment install/termination dates → contract start/end dates - - Notes and tags are fully preserved - - Created/updated timestamps are maintained -- **API Breaking Change**: Update API clients to use `/contract-info/` endpoint instead of `/segment-financial-info/` -- **Permission Updates**: New permissions for ContractInfo (view, add, change, delete) -- **M:N Relationships**: Segments can now be associated with multiple contracts -- **Versioning Workflow**: Use NetBox clone functionality to create contract amendments - -### Upgrade Instructions - -1. **Backup your database** before upgrading (important for any major version) -2. Update the plugin: `pip install --upgrade cesnet_service_path_plugin` -3. Run migrations: `python manage.py migrate cesnet_service_path_plugin` -4. Update API integrations to use new `/contract-info/` endpoint -5. Review and update user permissions for ContractInfo model -6. Test contract creation and amendment workflow in UI - -## [5.3.0] - 2025-11-19 - -### Added - - - **Introduced ownership type support attribute** - - New **database migration** adding ownership_type to segments. - - New constants in custom_choices and model methods for ownership type labels and colors. - - Added backend and frontend color mappings for ownership type (badges + map line colors). - - Added new "Ownership Type" color scheme to the Segments Map, including: - - Optimized Segments map color scheme change for faster rendering - -### Changed - - - Improved segment map UI: - - Popups and detail panels now show both status badge and ownership type badge. - - Map legend updated to support ownership types. - - Optimized color scheme switching with a new updateSegmentColors() function to avoid full redraw. - - Status color mapping corrected (duplicate “Planned” entry resolved). - - Enhanced fall-back line logic to correctly show straight-line path only when path data is missing. - - Adjusted styling of badges and map-line colors for better consistency with Bootstrap and existing segment status colors. - -### Fixed - - - Several UI inconsistencies in map popups where status badges were duplicated or missing label formatting. - - Missing ownership fields in multiple API outputs and templates. +### Deprecated +- Features that will be removed in future releases. ### Removed +- Obsolete features from the previous versions. - - Deprecated static color entries in map_status_colors.html. - -## [5.2.1] - 2025-11-07 - -### Added -- **Topology Visualization**: Interactive network topology visualization using Cytoscape.js - - Visual representation of segment connections and circuit terminations - - Multi-topology support for service paths with multiple segments - - Automatic topology generation for both segments and service paths - - Clean NetBox Blue styled visualization with gradients and shadows - - Interactive topology viewer with hover tooltips showing node details - - Topology visualization integrated into segment and service path detail views - - Topology visualization added to circuit detail pages showing related segments/service paths - - Toggle between multiple topologies when segment belongs to multiple service paths - -- **Commitment End Date Tracking**: Enhanced financial commitment monitoring - - Automatic calculation of commitment end date based on install date and commitment period - - Color-coded commitment status indicators: - - Red: More than 30 days until end - - Orange: Within 30 days of end - - Green: Commitment period has ended - - Gray: No commitment period set - - Interactive tooltips showing days remaining until commitment end - - Visual feedback for commitment periods that have ended - - Commitment end date displayed in segment detail view with badge styling - - GraphQL API support for commitment end dates with ISO format - -### Changed -- **Circuit Extensions Refactoring**: Improved code organization - - Renamed `CircuitKomoraSegmentExtension` to `CircuitSegmentExtension` for better naming consistency - - Enhanced circuit detail view with topology visualization support - - Better separation of concerns in template content extensions - - Circuit pages now show topology visualizations for associated segments - -- **Currency Field Enhancement**: Made charge_currency field required - - Removed default currency value to ensure explicit currency selection - - Migration `0031` updates currency field constraints - - Currency must now be explicitly set when creating financial information - - Prevents accidental use of default currency when not intended - -- **Table Improvements**: Enhanced data presentation - - Circuit column in SegmentCircuitMappingTable now orders by CID instead of name - - Improved ordering logic for better data organization and searchability - -- **Version Update**: Updated to version 5.2.1b5 in pyproject.toml - -### Fixed -- Added missing `python-dateutil` dependency to pyproject.toml for date calculations -- Improved commitment end date calculation with proper timezone handling using `django.utils.timezone` -- Enhanced tooltip rendering with proper Bootstrap integration -- Fixed tooltip data attributes for proper display of commitment information - -### Technical Details -- New utility module `utils_topology.py` with `TopologyBuilder` class for generating network graphs -- Cytoscape.js (v3.28.1) integration for advanced graph visualization -- Reusable topology visualization templates: - - `topology_visualization.html` - Core Cytoscape includes and initialization - - `topology_segment_card.html` - Topology display card with multi-topology support - - `topology_styles.html` - Styling for topology containers and tooltips -- Support for multiple topologies on single page with tab switching functionality -- Topology data stored as JSON and rendered client-side for performance -- Color-coding system for commitment status based on time remaining (30-day threshold) -- New GraphQL field resolver for `commitment_end_date` with ISO format output -- Template extensions now check for service path membership to generate appropriate topologies - -### Migration Notes -- **Migration 0031**: Updates `charge_currency` field to remove default value - requires explicit currency selection -- **New Dependencies**: Added `python-dateutil` for relativedelta calculations in commitment period tracking -- **Template Updates**: New topology visualization templates require Cytoscape.js CDN (included automatically) -- **API Changes**: GraphQL API now includes `commitment_end_date` field in SegmentFinancialInfoType - -### Upgrade Instructions -1. Run migrations: `python manage.py migrate cesnet_service_path_plugin` -2. Install new dependency: `pip install python-dateutil` (or upgrade plugin package) -3. Update existing financial records to set currency explicitly if using default -4. Refresh browser cache to load new topology visualization assets - -## [5.2.0] - 2025-10-29 - -### Added -- **Financial Information Management**: New segment financial tracking system - - `SegmentFinancialInfo` model for tracking segment costs and commitments - - Monthly charge and non-recurring charge fields - - Multi-currency support with configurable currency list - - Commitment period tracking (months) - - Automatic cost calculations (total commitment cost, total with setup) - - Permission-based access control for financial data - - Integration with segment detail view - - REST API support with nested serialization in segment endpoints - - Financial info displayed only to users with view permissions - -- **Plugin Configuration**: Enhanced configuration options - - Configurable currency list in plugin settings - - Default currency selection - - Example configuration in README - -- **Plugin Metadata**: Added `netbox-plugin.yaml` - - Official plugin metadata file for NetBox plugin registry - - Compatibility matrix with NetBox versions - - Package information and versioning - -### Changed -- **API Enhancements**: - - Improved error handling in segment serializer with detailed logging - - Financial info included in segment API responses (permission-based) - - Cleaner error messages for path file upload failures - - Better separation of validation and processing errors - -- **Permission System**: - - Financial data visibility controlled by Django permissions - - View, add, change, and delete permissions for financial info - - Automatic permission checks in views and API - -- **Documentation Updates**: - - Updated plugin configuration examples with currency settings - - Corrected file path references (configuration.py → configuration/plugins.py) - - Updated compatibility badge to reflect NetBox 4.4 support - -- **Development Dependencies**: - - Unpinned development dependency versions for flexibility - - Updated Python version requirement to >= 3.10 - - Corrected license classifier to Apache 2.0 - -### Technical Details -- Financial info uses one-to-one relationship with Segment model -- Currency choices are dynamically loaded from plugin configuration -- Financial data is optional - segments can exist without financial info -- API serializer uses method field for conditional financial data inclusion -- Redirect-based views for better UX (financial detail redirects to segment detail) -- Custom return URL handling for create/edit/delete operations - -### Migration Notes -- **New Model**: `SegmentFinancialInfo` table will be created -- **Permissions**: Four new permissions added for financial info management -- **Configuration**: Optional currency configuration can be added to plugin settings -- **API Change**: Segment API responses now include `financial_info` field (null if no data or no permission) - -## [5.1.0] - 2025-09-23 - -### Added -- **Segment Type System**: Complete implementation of segment type classification - - `segment_type` field with Dark Fiber, Optical Spectrum, and Ethernet Service types - - Type-specific data fields stored as JSON with dynamic schemas - - Smart numeric filtering for type-specific fields with operators (>, <, >=, <=, ranges) - - Dynamic form generation based on selected segment type - - Type-specific field validation and conversion (Decimal, Integer) - - Enhanced GraphQL API with type-specific data filtering (`has_type_specific_data`) - -- **Enhanced Map Visualization**: Advanced mapping features - - Segment type-based coloring and legend in map views - - Color schemes: by status and by provider - - Improved overlapping segment detection and selection - - Multiple background map layers (OpenStreetMap, satellite, topographic, CartoDB) - -- **Smart Filtering System**: Advanced filtering capabilities - - Smart numeric filters for JSON fields with operator support - - Type-specific field filters (fiber_type, connector_type, modulation_format, etc.) - - Range filters for numeric fields (fiber_attenuation_max, wavelength, port_speed, etc.) - - Boolean value parsing improvements - - Enhanced search functionality including segment_type - -### Changed -- Updated segment form to preserve type-specific field values during type changes -- Enhanced JavaScript form handling to hide fields without clearing values -- Improved field initialization and population logic -- Updated segment table to include segment_type column -- Modified API serializers to handle path file uploads -- Removed unnecessary SegmentListSerializer, unified with SegmentSerializer - -### Fixed -- Fixed form rendering issues with type-specific fields -- Improved value preservation when switching segment types -- Enhanced JSON field conversion for Decimal types -- Fixed smart numeric filtering edge cases -- Resolved issues with dynamic field visibility - - -## [5.0.3] - 2025-08-29 - -### Fixed -- **Critical**: Added save_m2m() call to SegmentForm to properly save tags - - Fixed missing many-to-many relationship saving (tags were being lost) - - Ensured proper persistence of all many-to-many fields - -### Changed -- Updated documentation with sample map in README -- Added Apache 2.0 License (same as NetBox) -- Updated pyproject.toml with repository information - -## [5.0.2] - 2025-08-21 - -### Added -- **Documentation Improvements**: Enhanced README and licensing - - Apache 2.0 License badge and full license file - - Sample map visualization in README - - Updated repository information in pyproject.toml - -### Changed -- Repository metadata and documentation updates -- Warning about work-in-progress status - -## [5.0.1] - 2025-08-04 - -### Added -- **Comprehensive Segment Map**: Interactive map view for all segments - - Map utilizes list view filtering capabilities - - Multiple background layer options - - Improved navigation and user experience - -### Fixed -- Fixed button types to prevent form submission when changing map layers -- Enhanced map layer switching controls - -## [5.0.0] - 2025-08-01 - -### Added -- **Geographic Path Visualization**: Complete interactive map system with Leaflet - - Multiple tile layer support (OpenStreetMap, satellite, topographic, CartoDB variants) - - Individual segment map views with path geometry display - - Comprehensive segments map view with filtering support - - Overlapping segment detection and selection interface - - Status-based color coding for visual segment identification - -- **Path Data Management**: Full support for geographic path data - - KML, KMZ, and GeoJSON file format support - - Enhanced KMZ processing with multi-layer extraction - - Automatic 3D to 2D coordinate conversion - - Path geometry validation and error reporting - - Automatic path length calculation using projected coordinates - - Path data export as GeoJSON files - -- **Advanced Map Features**: - - Interactive controls (pan, zoom, fit-to-bounds) - - Fallback visualization with straight lines when path data unavailable - - Site markers for segment endpoints - - Detailed segment information panels - - Path data availability indicators - - Responsive map controls and layer switching - -- **Enhanced Data Model**: - - `path_geometry` field for storing MultiLineString geometries - - `path_length_km` field with automatic calculation - - `path_source_format` field tracking data origin - - `path_notes` field for additional metadata - - Geographic helper methods for coordinate handling - -- **UI/UX Improvements**: - - Template extensions for Circuits, Providers, Sites, Locations, and Tenants - - Custom table columns showing path data availability - - Date status indicators with visual progress bars - - Enhanced filtering including geographic data availability - - Improved navigation with map view integration - -- **API Enhancements**: - - Separate serializers for list and detail views (performance optimization) - - Geographic data endpoints for map visualization - - GeoJSON export capabilities - - Path bounds and coordinate data in API responses - - Enhanced filtering on geographic fields - -- **GraphQL Support**: - - Complete GraphQL schema with geographic field support - - Custom scalar types for path bounds and coordinates - - Lazy-loaded relationship fields for performance - - Geographic data queries and filtering - -### Changed -- **Breaking**: Upgraded to Django 5.2.3 with GeoDjango support -- **Breaking**: Added PostGIS dependency for geographic features -- **Breaking**: Modified database schema to include geographic fields -- Improved segment form with path data upload capability -- Enhanced segment detail view with geographic information -- Updated table layouts with new path-related columns -- Refactored status choices to use configurable ChoiceSet system -- Improved error handling for geographic data processing - -### Fixed -- Resolved migration conflicts during table renaming process -- Fixed segment validation to properly handle location-site relationships -- Improved date validation with better error messaging -- Enhanced KMZ file processing for complex archive structures -- Fixed coordinate system handling for accurate length calculations -- Fixed segment detail view table rendering and typos - -### Technical Details -- Added `geopandas`, `fiona`, and `shapely` as core dependencies -- Implemented comprehensive GIS utility functions -- Added extensive JavaScript map handling with modular design -- Created reusable template components for map functionality -- Enhanced error handling and logging for geographic operations -- Implemented proper geometric validation and sanitization -- Replaced setup.py with pyproject.toml for modern Python packaging - -### Migration Notes -- **Database Migration Required**: New geographic fields require PostGIS -- **Dependency Installation**: Geographic libraries (GDAL, GEOS, PROJ) required -- **Configuration Updates**: May need GeoDjango configuration updates -- **Data Migration**: Existing installations will have empty path geometry fields - -## [4.3.0] - 2025-05-16 - -### Added -- **NetBox 4.3 Compatibility**: Updated for NetBox 4.3 support - - New URL patterns for bulk operations on service paths, segment mappings, and circuit mappings - - Enhanced Meta classes for filter consistency - - Improved import structure for better maintainability - -### Changed -- Updated imports in filters.py for better readability -- Changed `model` to `models` in template_content.py for multi-model compatibility -- Removed unused imports and decorators for cleaner code -- Updated plugin version to 4.3.0 - -## [4.0.1] - 2025-02-24 - -### Fixed -- **Bookmark and Subscription Issues**: Resolved non-functional bookmark and subscription features -- Updated plugin configuration and version management -- Removed unused imports for cleaner codebase - -## [4.0.0] - 2025-02-19 - -### Added -- **Enhanced Service Management**: Comprehensive service path and segment management - - Ability to assign segments to circuits or service paths from segment detail view - - Improved ServicePath kind field with ChoiceSet system - - Enhanced date validation logic in SegmentForm - - Date status display in table and detail views with color-coded progress bars - -### Changed -- **Breaking**: Refactored from "Komora" to "CESNET" branding throughout - - Plugin renamed from `komora_service_path_plugin` to `cesnet_service_path_plugin` - - Database table names changed from `komora_*` to `cesnet_*` - - URL patterns and configuration updated - - All references and documentation updated - -- **Model Improvements**: - - Replaced `state` field with `status` field in ServicePath and Segment models - - Added StatusChoices for consistent status options - - Made provider field required in SegmentForm - - Enhanced date validation with install_date/termination_date constraints - -- **Data Cleanup**: - - Removed sync_status from all models - - Removed device_ and port_ fields from Segment model - - Merged segment notes (note_a, note_b) into unified comments field - - Removed imported_data and komora_id fields - - Removed unnecessary db_table options from models - -### Fixed -- Fixed link for adding segment to a circuit -- Fixed EditForm for SegmentCircuitMapping model -- Enabled ID linkify for mapping tables -- Fixed date status logic and display -- Enhanced form validation and error handling - -### Migration Notes -- **Breaking**: Database table renaming requires careful migration -- **Data Migration**: Existing installations need to migrate from old table names -- **Configuration**: Update plugin configuration from komora to cesnet references - -## [0.1.0] - 2024-04-23 - -### Added -- **Initial Release**: First version published on PyPI -- Basic segment and service path management -- Provider and circuit relationship tracking -- Simple filtering and table views -- REST API endpoints -- NetBox 3.7 compatibility - -### Features -- Segment model with provider, site, and date tracking -- Service path model with status and kind classification -- Mapping models for segment-circuit and service path-segment relationships -- Basic NetBox integration with standard views and forms -- Template extensions for related model pages - ---- - -## Development Guidelines - -When updating this changelog: -1. Add new entries at the top under "Unreleased" section -2. Move completed features to version sections when releasing -3. Follow [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format -4. Include breaking changes with **Breaking** prefix -5. Group changes by type: Added, Changed, Deprecated, Removed, Fixed, Security -6. Include migration notes for database or configuration changes \ No newline at end of file +### Security +- Security improvements implemented. \ No newline at end of file From 63d7f8d179c20ef7fd58b3f3d056be4d4e0ef529 Mon Sep 17 00:00:00 2001 From: Jiri Vrany Date: Thu, 12 Feb 2026 11:28:42 +0100 Subject: [PATCH 02/11] Fix missing id primary key on segment data tables for NetBox branching support v6.0.1 Remove primary_key=True from OneToOneField on DarkFiberSegmentData, OpticalSpectrumSegmentData, and EthernetServiceSegmentData models. Add custom migration to safely switch primary key from segment_id to auto-generated id column without breaking existing data. --- .../0037_darkfibersegmentdata_id_and_more.py | 133 ++++++++++++++++++ .../models/dark_fiber_data.py | 1 - .../models/ethernet_service_data.py | 1 - .../models/optical_spectrum_data.py | 1 - pyproject.toml | 2 +- 5 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 cesnet_service_path_plugin/migrations/0037_darkfibersegmentdata_id_and_more.py diff --git a/cesnet_service_path_plugin/migrations/0037_darkfibersegmentdata_id_and_more.py b/cesnet_service_path_plugin/migrations/0037_darkfibersegmentdata_id_and_more.py new file mode 100644 index 0000000..86c4235 --- /dev/null +++ b/cesnet_service_path_plugin/migrations/0037_darkfibersegmentdata_id_and_more.py @@ -0,0 +1,133 @@ +# Custom migration: switch primary key from segment_id to auto-generated id +# for DarkFiberSegmentData, EthernetServiceSegmentData, OpticalSpectrumSegmentData. +# +# Django's auto-generated migration fails because it tries to add a new PK +# column while the old PK constraint still exists. This migration uses raw SQL +# to do the steps in the correct order: +# 1. Drop the existing PK constraint on segment_id +# 2. Add a new "id" identity column as the primary key +# 3. Add a UNIQUE constraint on segment_id (required by OneToOneField) +# 4. Register the state changes so Django's ORM stays in sync + +import django.db.models.deletion +from django.db import migrations, models + + +# (table_name, old_pkey_constraint_name) +# Constraint names come from the \d output in Postgres. +TABLES = [ + ( + "cesnet_service_path_plugin_darkfibersegmentdata", + "cesnet_service_path_plugin_darkfibersegmentdata_pkey", + ), + ( + "cesnet_service_path_plugin_ethernetservicesegmentdata", + "cesnet_service_path_plugin_ethernetservicesegmentdata_pkey", + ), + ( + "cesnet_service_path_plugin_opticalspectrumsegmentdata", + "cesnet_service_path_plugin_opticalspectrumsegmentdata_pkey", + ), +] + + +def forwards(apps, schema_editor): + for table, pkey_name in TABLES: + # 1. Drop the existing primary key (segment_id) + schema_editor.execute( + f'ALTER TABLE "{table}" DROP CONSTRAINT "{pkey_name}";' + ) + # 2. Add new "id" column as identity primary key + schema_editor.execute( + f'ALTER TABLE "{table}" ADD COLUMN "id" bigint NOT NULL ' + f"GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY;" + ) + # 3. Add unique constraint on segment_id (OneToOneField needs it) + schema_editor.execute( + f'ALTER TABLE "{table}" ADD CONSTRAINT ' + f'"{table}_segment_id_key" UNIQUE ("segment_id");' + ) + + +def backwards(apps, schema_editor): + for table, pkey_name in TABLES: + # Reverse: drop the unique constraint, drop id column, re-add old PK + schema_editor.execute( + f'ALTER TABLE "{table}" DROP CONSTRAINT "{table}_segment_id_key";' + ) + schema_editor.execute( + f'ALTER TABLE "{table}" DROP COLUMN "id";' + ) + schema_editor.execute( + f'ALTER TABLE "{table}" ADD CONSTRAINT "{pkey_name}" ' + f'PRIMARY KEY ("segment_id");' + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "cesnet_service_path_plugin", + "0036_alter_opticalspectrumsegmentdata_chromatic_dispersion", + ), + ] + + operations = [ + # Raw SQL handles the actual schema change + migrations.RunPython(forwards, backwards), + # State-only operations so Django's ORM knows about the new fields + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.AddField( + model_name="darkfibersegmentdata", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False + ), + ), + migrations.AlterField( + model_name="darkfibersegmentdata", + name="segment", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="dark_fiber_data", + to="cesnet_service_path_plugin.segment", + ), + ), + migrations.AddField( + model_name="ethernetservicesegmentdata", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False + ), + ), + migrations.AlterField( + model_name="ethernetservicesegmentdata", + name="segment", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="ethernet_service_data", + to="cesnet_service_path_plugin.segment", + ), + ), + migrations.AddField( + model_name="opticalspectrumsegmentdata", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False + ), + ), + migrations.AlterField( + model_name="opticalspectrumsegmentdata", + name="segment", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="optical_spectrum_data", + to="cesnet_service_path_plugin.segment", + ), + ), + ], + database_operations=[], # Already handled by RunPython above + ), + ] diff --git a/cesnet_service_path_plugin/models/dark_fiber_data.py b/cesnet_service_path_plugin/models/dark_fiber_data.py index 0f9414e..ba02341 100644 --- a/cesnet_service_path_plugin/models/dark_fiber_data.py +++ b/cesnet_service_path_plugin/models/dark_fiber_data.py @@ -24,7 +24,6 @@ class DarkFiberSegmentData(NetBoxModel): "Segment", on_delete=models.CASCADE, related_name="dark_fiber_data", - primary_key=True, help_text="Associated segment (1:1 relationship)", ) diff --git a/cesnet_service_path_plugin/models/ethernet_service_data.py b/cesnet_service_path_plugin/models/ethernet_service_data.py index 49097bd..9efc0b8 100644 --- a/cesnet_service_path_plugin/models/ethernet_service_data.py +++ b/cesnet_service_path_plugin/models/ethernet_service_data.py @@ -19,7 +19,6 @@ class EthernetServiceSegmentData(NetBoxModel): "Segment", on_delete=models.CASCADE, related_name="ethernet_service_data", - primary_key=True, help_text="Associated segment (1:1 relationship)", ) diff --git a/cesnet_service_path_plugin/models/optical_spectrum_data.py b/cesnet_service_path_plugin/models/optical_spectrum_data.py index fa9c394..f34eb15 100644 --- a/cesnet_service_path_plugin/models/optical_spectrum_data.py +++ b/cesnet_service_path_plugin/models/optical_spectrum_data.py @@ -18,7 +18,6 @@ class OpticalSpectrumSegmentData(NetBoxModel): "Segment", on_delete=models.CASCADE, related_name="optical_spectrum_data", - primary_key=True, help_text="Associated segment (1:1 relationship)", ) diff --git a/pyproject.toml b/pyproject.toml index c4a2273..4e75f47 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "cesnet_service_path_plugin" -version = "6.0.0" +version = "6.0.1b1" description = "Adds ability to create, edit and view service paths in the network." authors = [ {name = "Jan Krupa", email = "jan.krupa@cesnet.cz"}, From 3cf8f78aeada7db38525d216787708993b8d8186 Mon Sep 17 00:00:00 2001 From: Jan Krupa Date: Mon, 16 Feb 2026 13:51:35 +0000 Subject: [PATCH 03/11] Release 6.0.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4e75f47..20f684c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "cesnet_service_path_plugin" -version = "6.0.1b1" +version = "6.0.1" description = "Adds ability to create, edit and view service paths in the network." authors = [ {name = "Jan Krupa", email = "jan.krupa@cesnet.cz"}, From 042a1a9a5a56e2f355c56eae427022aaaf96ee76 Mon Sep 17 00:00:00 2001 From: Jan Krupa <> Date: Mon, 9 Mar 2026 09:49:54 +0000 Subject: [PATCH 04/11] Chore: Bump version to 6.1.0 and add CHANGELOG entry - Add v6.1.0 CHANGELOG entry documenting breaking changes, fixes, and compatibility matrix - Bump version from 6.0.1 to 6.1.0 in pyproject.toml --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ pyproject.toml | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ef288f..e00c602 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## [6.1.0] - 2026-03-09 + +> ⚠️ Requires NetBox >= 4.5.4. NetBox 4.5.0–4.5.3 is not supported by this release — use v6.0.x for those versions. + +### Breaking Changes + +- **Minimum NetBox version raised to 4.5.4.** + This release uses `StrFilterLookup` from strawberry-graphql-django >= 0.79.0, which ships with + NetBox 4.5.4. Starting the plugin on NetBox 4.5.0–4.5.3 will raise an `ImportError`. + +- **GraphQL CharField filter types changed.** + All string filter fields migrated from `FilterLookup[str]` → `StrFilterLookup[str]` to eliminate + `DuplicatedTypeName` schema errors introduced in strawberry-graphql-django 0.79.0. + GraphQL clients relying on type introspection may need updating. + +### Fixed + +- GraphQL `@field` resolver methods now correctly declare `Info` type annotations, resolving + startup errors on NetBox 4.5.4+ with stricter strawberry-django introspection. + +### Compatibility + +| cesnet_service_path_plugin | NetBox | +|---|---| +| 6.1.0+ | 4.5.4+ | +| 6.0.x | 4.5.0 – 4.5.3 | + ## [6.0.0] - 2026-01-21 ### Added - New features and improvements. diff --git a/pyproject.toml b/pyproject.toml index 20f684c..72dbd5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "cesnet_service_path_plugin" -version = "6.0.1" +version = "6.1.0" description = "Adds ability to create, edit and view service paths in the network." authors = [ {name = "Jan Krupa", email = "jan.krupa@cesnet.cz"}, From 7b2ec336324475d49380a018cc3ae4c40ff65851 Mon Sep 17 00:00:00 2001 From: Jan Krupa <> Date: Mon, 9 Mar 2026 10:01:49 +0000 Subject: [PATCH 05/11] chore: remove CLAUDE.md --- CLAUDE.md | 519 ------------------------------------------------------ 1 file changed, 519 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index f9f878d..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,519 +0,0 @@ -# CLAUDE.md - AI Assistant Context - -## Project Overview - -**cesnet_service_path_plugin** is a NetBox plugin for managing network service paths and segments with advanced geographic visualization, interactive topology visualization, and financial tracking capabilities. - -- **Type**: NetBox Plugin (Django application) -- **Language**: Python 3.10+ -- **Framework**: Django with NetBox Plugin Framework -- **Current Version**: 6.0.0 -- **License**: Apache-2.0 - -## Core Purpose - -This plugin extends NetBox to provide comprehensive network service path management with: -- Network segment tracking between locations -- Service path definitions connecting multiple segments -- Geographic path visualization using actual route data (KML, KMZ, GeoJSON) -- Interactive topology visualization using Cytoscape.js -- Financial information tracking with multi-currency support -- Contract management with versioning - -## Architecture - -### Directory Structure - -``` -cesnet_service_path_plugin/ -├── cesnet_service_path_plugin/ # Main plugin package -│ ├── api/ # REST API (views, serializers, URLs) -│ │ ├── views/ # API ViewSets -│ │ └── serializers/ # DRF serializers -│ ├── filtersets/ # Django filters for list views -│ ├── forms/ # Django forms for CRUD operations -│ ├── graphql/ # GraphQL schema and types -│ ├── migrations/ # Django database migrations -│ ├── models/ # Core data models -│ │ ├── segment.py # Network segment model -│ │ ├── service_path.py # Service path model -│ │ ├── contract_info.py # Contract tracking with versioning -│ │ ├── segment_types.py # Segment type definitions -│ │ └── custom_choices.py # Status, currency, etc. choices -│ ├── tables/ # django-tables2 definitions -│ ├── templates/ # Django HTML templates -│ ├── templatetags/ # Custom template filters/tags -│ ├── utils/ # Utility functions -│ │ ├── utils_gis.py # Geographic data processing -│ │ └── utils_topology.py # Topology generation -│ └── views/ # Django views -├── tests/ # Test suite -└── docs/ # Documentation - -``` - -### Key Technologies - -- **Django**: Web framework (NetBox is built on Django) -- **Django GIS (GeoDjango)**: Geographic database features -- **PostGIS**: PostgreSQL extension for geographic data -- **GDAL/GEOS/PROJ**: Geographic data processing libraries -- **GeoPandas/Fiona/Shapely**: Python geographic libraries -- **Leaflet.js**: Interactive map visualization -- **Cytoscape.js**: Network topology graphs -- **Django REST Framework**: API endpoints -- **Strawberry GraphQL**: GraphQL API (via NetBox) - -### Database Requirements - -- PostgreSQL with PostGIS extension enabled -- Must use `django.contrib.gis.db.backends.postgis` engine -- Stores geographic data using MultiLineString geometries (SRID 4326 - WGS84) - -## Core Models - -### 1. Segment (`models/segment.py`) - -Represents a physical network segment between two locations. - -**Key Fields:** -- `name`: Segment identifier -- `network_label`: Optional network label -- `provider`: Foreign key to NetBox Provider -- `site_a`, `site_b`: Start and end sites (required) -- `location_a`, `location_b`: Specific locations within sites (optional) -- `install_date`, `termination_date`: Lifecycle dates -- `status`: Active, Planned, Offline, etc. (customizable) -- `ownership_type`: Leased, Owned, etc. -- `segment_type`: Dark fiber, optical spectrum, ethernet service -- `path_geometry`: PostGIS MultiLineString (geographic route) -- `path_length_km`: Calculated path length -- `circuits`: Many-to-Many through SegmentCircuitMapping - -**Type-Specific Data Models (OneToOne relationships):** -- `DarkFiberSegmentData`: Fiber mode, attenuation, connectors, etc. -- `OpticalSpectrumSegmentData`: Wavelength, dispersion, modulation, etc. -- `EthernetServiceSegmentData`: Port speed, VLAN, encapsulation, MTU, etc. - -**Geographic Features:** -- Stores actual geographic paths (not just A-to-B lines) -- Supports KML, KMZ, and GeoJSON upload -- Automatic path length calculation -- Fallback to straight lines if no path data - -### 2. ServicePath (`models/service_path.py`) - -Represents a logical service path composed of multiple segments. - -**Key Fields:** -- `name`: Service path identifier -- `status`: Active, Planned, Offline -- `kind`: Experimental, Core, Customer (customizable) -- `segments`: Many-to-Many through ServicePathSegmentMapping -- `comments`: Additional notes - -**Purpose:** -- Group segments into logical end-to-end paths -- Visualize complete service topology -- Track service-level status and classification - -### 3. ContractInfo (`models/contract_info.py`) - -Tracks legal agreements and financial information for segments. - -**Version Chain:** -- Contracts support version history using linked list pattern -- `previous_version` → points to older version -- `superseded_by` → points to newer version -- Contract types: New, Amendment, Renewal - -**Key Fields:** -- `contract_number`: Provider's reference -- `contract_type`: New, Amendment, Renewal (auto-set) -- `effective_date`: When contract version takes effect -- `charge_currency`: Currency (fixed, cannot change in amendments) -- `non_recurring_charge`: One-time setup fees (cumulative) -- `recurring_charge`: Regular periodic charge -- `recurring_period`: Monthly, Quarterly, Annual -- `commitment_months`: Contract commitment period -- `change_reason`: Required for amendments/renewals -- `segments`: Many-to-Many through ContractSegmentMapping - -**Financial Calculations:** -- Commitment end date based on effective date + commitment months -- Visual status badges (red >30 days, orange <30 days, green expired) -- Total costs calculated automatically - -### 4. Mapping Models - -**ServicePathSegmentMapping:** -- Links segments to service paths -- Tracks position/order in the path - -**SegmentCircuitMapping:** -- Links segments to NetBox circuits -- Many-to-many relationship - -**ContractSegmentMapping:** -- Links contract versions to segments -- Supports multiple segments per contract - -### 5. Type-Specific Data Models - -Three specialized models store technical parameters for different segment types using OneToOne relationships with Segment: - -**DarkFiberSegmentData** (`models/dark_fiber_data.py`): -- `fiber_mode`: Single-mode or Multimode -- `single_mode_subtype`: G.652D, G.655, G.657A1, etc. -- `multimode_subtype`: OM1, OM2, OM3, OM4, OM5 -- `jacket_type`: Indoor, Outdoor, Armored, etc. -- `fiber_attenuation_max`: Maximum attenuation (dB/km) -- `total_loss`: End-to-end optical loss (dB) -- `total_length`: Physical cable length (km) -- `number_of_fibers`: Fiber strand count -- `connector_type_side_a`, `connector_type_side_b`: LC/APC, SC/UPC, etc. - -**OpticalSpectrumSegmentData** (`models/optical_spectrum_data.py`): -- `wavelength`: Center wavelength (nm) - C-band/L-band -- `spectral_slot_width`: Optical channel bandwidth (GHz) -- `itu_grid_position`: ITU-T G.694.1 channel number -- `chromatic_dispersion`: Dispersion at wavelength (ps/nm) -- `pmd_tolerance`: Polarization mode dispersion (ps) -- `modulation_format`: NRZ, PAM4, QPSK, 16QAM, etc. - -**EthernetServiceSegmentData** (`models/ethernet_service_data.py`): -- `port_speed`: Bandwidth (Mbps) -- `vlan_id`: Primary VLAN tag (1-4094) -- `vlan_tags`: Additional VLANs for QinQ -- `encapsulation_type`: 802.1Q, 802.1ad, MPLS, MEF E-Line, etc. -- `interface_type`: RJ45, SFP, SFP+, QSFP+, QSFP28, etc. -- `mtu_size`: Maximum transmission unit (bytes) - -**Architecture Benefits:** -- **Database constraints**: Field validation at DB level (NOT NULL, CHECK, ranges) -- **Indexed fields**: Fast queries on individual parameters -- **Type safety**: Django ORM prevents invalid data types -- **Maintainability**: Add fields via migrations, not JSON schema updates -- **API clarity**: Structured objects, not JSON blobs - -**API Access:** -```python -# Computed field on Segment API -segment['type_specific_technicals'] # Returns appropriate model data -``` - -## Key Features - -### 1. Geographic Visualization - -**Map Features:** -- Interactive Leaflet maps with multiple tile layers -- OpenStreetMap, satellite, topographic views -- Status-based, provider-based, or segment-type color coding -- Click segments for detailed information -- Export GeoJSON data via API - -**Path Data Processing:** -- Upload KML, KMZ, or GeoJSON files -- Automatic 3D to 2D conversion -- Multi-segment path support -- Length calculation using projected coordinates -- Path validation with error reporting - -**Implementation:** `utils/utils_gis.py` - -### 2. Topology Visualization - -**Features:** -- Interactive network graphs using Cytoscape.js -- Automatic topology generation for segments and service paths -- Multi-topology support (toggle between different views) -- Node types: locations, circuits, circuit terminations -- Hover tooltips with detailed information -- NetBox Blue themed styling - -**Implementation:** `utils/utils_topology.py` - -### 3. Contract Management - -**Versioning System:** -- Linear version chain (linked list) -- Types: New contract, Amendment, Renewal -- Immutable currency across versions -- Change tracking with reason field - -**Financial Tracking:** -- Multi-currency support (configurable) -- Recurring and non-recurring charges -- Commitment period tracking -- Automatic end date calculation -- Visual status indicators - -### 4. API Support - -**REST API:** -- Full CRUD operations for all models -- Geographic data in GeoJSON format -- Financial data with permission checks -- Endpoints: `/api/plugins/cesnet-service-path-plugin/` - -**GraphQL API:** -- Query segments, service paths, contracts -- Geographic fields (geometry, bounds, coordinates) -- Advanced filtering -- Nested relationships -- Access: `/graphql/` - -## Configuration - -### Plugin Configuration (`configuration/plugins.py`) - -```python -PLUGINS = [ - 'cesnet_service_path_plugin', -] - -PLUGINS_CONFIG = { - "cesnet_service_path_plugin": { - # Currency configuration - 'currencies': [ - ('CZK', 'Czech Koruna'), - ('EUR', 'Euro'), - ('USD', 'US Dollar'), - ], - 'default_currency': 'EUR', - }, -} -``` - -### Custom Choices - -Extend status, kind, or other choices in `configuration.py`: - -```python -FIELD_CHOICES = { - 'cesnet_service_path_plugin.choices.status': ( - ('custom_status', 'Custom Status', 'blue'), - ), - 'cesnet_service_path_plugin.choices.kind': ( - ('custom_kind', 'Custom Kind', 'purple'), - ) -} -``` - -## Development Guidelines - -### Database Engine Requirement - -Must configure NetBox to use PostGIS engine: - -```python -DATABASE_ENGINE = "django.contrib.gis.db.backends.postgis" -``` - -### System Dependencies - -Required for geographic features: -- PostgreSQL with PostGIS extension -- GDAL runtime libraries (`gdal-bin`, `libgdal34`) -- GEOS libraries (`libgeos-c1t64`) -- PROJ libraries (`libproj25`) - -### Testing Geographic Features - -```python -from cesnet_service_path_plugin.utils import check_gis_environment -check_gis_environment() -``` - -### Code Style - -- Uses Black and autopep8 for formatting -- Ruff for linting (`.ruff.toml`) -- Django best practices -- NetBox plugin patterns - -## Important Patterns - -### 1. Model Registration - -Models inherit from `NetBoxModel` which provides: -- Automatic primary key -- Created/last_updated timestamps -- Custom fields support -- Tags support -- Change logging - -### 2. Geographic Data Handling - -**Upload Path Data:** -1. User uploads KML/KMZ/GeoJSON file via form -2. `utils_gis.py` processes file (validation, conversion) -3. Stored as PostGIS MultiLineString -4. Length calculated and stored - -**Display Path Data:** -1. API endpoint returns GeoJSON -2. Leaflet map renders paths -3. Click handlers show segment details - -### 3. Permission-Based Visibility - -Financial information respects Django permissions: -- View: `cesnet_service_path_plugin.view_contractinfo` -- Add: `cesnet_service_path_plugin.add_contractinfo` -- Change: `cesnet_service_path_plugin.change_contractinfo` -- Delete: `cesnet_service_path_plugin.delete_contractinfo` - -API includes financial data only if user has view permission. - -### 4. Template Extensions - -Plugin extends NetBox core pages: -- Circuit pages: Show related segments -- Provider pages: List provider segments -- Site/Location pages: Display connected segments -- Uses NetBox template extension points - -## Common Tasks - -### Adding a New Model - -1. Create model in `models/` -2. Add to `models/__init__.py` -3. Create migration: `python manage.py makemigrations cesnet_service_path_plugin` -4. Create serializer in `api/serializers/` -5. Create viewset in `api/views/` -6. Add URL route in `api/urls.py` -7. Create form in `forms/` -8. Create table in `tables/` -9. Create filterset in `filtersets/` -10. Create views in `views/` -11. Add URL routes in `urls.py` -12. Add to navigation in `navigation.py` -13. Update GraphQL schema if needed - -### Adding a Geographic Feature - -1. Use GeoDjango fields (`gis_models.MultiLineStringField`) -2. Process with GeoPandas/Fiona in `utils_gis.py` -3. Serialize as GeoJSON in API -4. Render with Leaflet in templates - -### Working with Contracts - -**Creating New Contract:** -- Use `ContractTypeChoices.NEW` -- No previous_version - -**Creating Amendment:** -- Clone existing contract data -- Set `previous_version` to current active -- Set `superseded_by` on old contract -- Use `ContractTypeChoices.AMENDMENT` -- Currency must match original - -**Creating Renewal:** -- Similar to amendment -- Use `ContractTypeChoices.RENEWAL` -- Typically extends commitment period - -## Testing - -Run tests: -```bash -pytest -``` - -Key test files: -- `tests/test_komora_service_path_plugin.py`: Main integration tests -- `tests/test_integration_segment_type_specific_data.py`: Type-specific data tests -- `tests/test_integration_segment_financial_info_api.py`: Financial API tests - -## Troubleshooting - -### PostGIS Not Available - -**Symptoms:** GeoDjango errors, cannot create geographic fields - -**Solution:** -1. Enable PostGIS: `CREATE EXTENSION IF NOT EXISTS postgis;` -2. Configure engine: `DATABASE_ENGINE = "django.contrib.gis.db.backends.postgis"` -3. Install system libraries: `gdal-bin`, `libgdal34`, `libgeos-c1t64`, `libproj25` - -### Path Upload Fails - -**Symptoms:** File upload rejected, validation errors - -**Solution:** -- Check file format (KML, KMZ, GeoJSON) -- Verify file contains LineString/MultiLineString geometries -- Check for 3D coordinates (will auto-convert to 2D) -- Review error messages in UI - -### Financial Data Not Visible - -**Symptoms:** Contract info not showing in UI or API - -**Solution:** -- Check user has `view_contractinfo` permission -- Verify contract exists and is linked to segment -- Check API response for null financial_info field - -### Map Not Loading - -**Symptoms:** Blank map, JavaScript errors - -**Solution:** -- Check browser console for tile layer errors -- Verify internet connectivity (tile layers from CDN) -- Check segment has valid path_geometry data - -## URLs and Endpoints - -### Web UI -- Segments List: `/plugins/cesnet-service-path-plugin/segments/` -- Segments Map: `/plugins/cesnet-service-path-plugin/segments-map/` -- Service Paths: `/plugins/cesnet-service-path-plugin/service-paths/` -- Contracts: `/plugins/cesnet-service-path-plugin/contracts/` - -### API -- Base: `/api/plugins/cesnet-service-path-plugin/` -- Segments: `/api/plugins/cesnet-service-path-plugin/segments/` -- Service Paths: `/api/plugins/cesnet-service-path-plugin/service-paths/` -- Contracts: `/api/plugins/cesnet-service-path-plugin/contracts/` -- GeoJSON Export: `/api/plugins/cesnet-service-path-plugin/segments/{id}/geojson-api/` - -### GraphQL -- Endpoint: `/graphql/` -- Queries: `segment_list`, `service_path_list`, `contract_info_list` - -## Resources - -- **Repository**: https://github.com/CESNET/cesnet_service_path_plugin -- **Issues**: https://github.com/CESNET/cesnet_service_path_plugin/issues -- **NetBox Docs**: https://docs.netbox.dev/ -- **NetBox Plugin Dev**: https://github.com/netbox-community/netbox-plugin-tutorial -- **GeoDjango**: https://docs.djangoproject.com/en/stable/ref/contrib/gis/ -- **PostGIS**: https://postgis.net/documentation/ -- **Leaflet**: https://leafletjs.com/reference.html -- **Cytoscape.js**: https://js.cytoscape.org/ - -## Recent Changes (v5.3.0) - -- Implemented versioned contract system with M:N segment relationships -- Contract versioning using linear chain (linked list) pattern -- Support for contract amendments and renewals -- Immutable attributes and cumulative charges across versions -- Removed direct provider relationship from contract model -- Enhanced contract filtering and display - -## Authors - -- Jan Krupa (jan.krupa@cesnet.cz) -- Jiri Vrany (jiri.vrany@cesnet.cz) - ---- - -**Last Updated**: Based on version 5.3.0 analysis -**For AI Assistants**: This document provides context for understanding and working with the cesnet_service_path_plugin codebase. Use it to answer questions, make changes, or debug issues. From 1e0be80d7095fc0c6223a24bbdac972a96391e3e Mon Sep 17 00:00:00 2001 From: Jan Krupa <> Date: Mon, 9 Mar 2026 10:02:44 +0000 Subject: [PATCH 06/11] fix: use StrFilterLookup and Info annotations for NetBox 4.5.4+ compatibility --- cesnet_service_path_plugin/graphql/filters.py | 46 +++++++++---------- cesnet_service_path_plugin/graphql/types.py | 13 +++--- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/cesnet_service_path_plugin/graphql/filters.py b/cesnet_service_path_plugin/graphql/filters.py index c1bc6e6..e7361ca 100644 --- a/cesnet_service_path_plugin/graphql/filters.py +++ b/cesnet_service_path_plugin/graphql/filters.py @@ -8,7 +8,7 @@ from netbox.graphql.filters import NetBoxModelFilter -from strawberry_django import FilterLookup +from strawberry_django import FilterLookup, StrFilterLookup if TYPE_CHECKING: from circuits.graphql.filters import CircuitFilter, ProviderFilter @@ -36,19 +36,19 @@ class ContractInfoFilter(NetBoxModelFilter): """GraphQL filter for ContractInfo model""" # Basic fields - contract_number: FilterLookup[str] | None = strawberry_django.filter_field() - contract_type: FilterLookup[str] | None = strawberry_django.filter_field() + contract_number: StrFilterLookup[str] | None = strawberry_django.filter_field() + contract_type: StrFilterLookup[str] | None = strawberry_django.filter_field() # Financial fields - charge_currency: FilterLookup[str] | None = strawberry_django.filter_field() - recurring_charge_period: FilterLookup[str] | None = strawberry_django.filter_field() + charge_currency: StrFilterLookup[str] | None = strawberry_django.filter_field() + recurring_charge_period: StrFilterLookup[str] | None = strawberry_django.filter_field() # Date fields - start_date: FilterLookup[str] | None = strawberry_django.filter_field() - end_date: FilterLookup[str] | None = strawberry_django.filter_field() + start_date: StrFilterLookup[str] | None = strawberry_django.filter_field() + end_date: StrFilterLookup[str] | None = strawberry_django.filter_field() # Notes - notes: FilterLookup[str] | None = strawberry_django.filter_field() + notes: StrFilterLookup[str] | None = strawberry_django.filter_field() # Related segments segments: Annotated["SegmentFilter", strawberry.lazy(".filters")] | None = strawberry_django.filter_field() @@ -79,22 +79,22 @@ class SegmentFilter(NetBoxModelFilter): """GraphQL filter for Segment model""" # Basic fields - name: FilterLookup[str] | None = strawberry_django.filter_field() - network_label: FilterLookup[str] | None = strawberry_django.filter_field() - install_date: FilterLookup[str] | None = strawberry_django.filter_field() # Date fields as string - termination_date: FilterLookup[str] | None = strawberry_django.filter_field() - status: FilterLookup[str] | None = strawberry_django.filter_field() - ownership_type: FilterLookup[str] | None = strawberry_django.filter_field() - provider_segment_id: FilterLookup[str] | None = strawberry_django.filter_field() - comments: FilterLookup[str] | None = strawberry_django.filter_field() + name: StrFilterLookup[str] | None = strawberry_django.filter_field() + network_label: StrFilterLookup[str] | None = strawberry_django.filter_field() + install_date: StrFilterLookup[str] | None = strawberry_django.filter_field() # Date fields as string + termination_date: StrFilterLookup[str] | None = strawberry_django.filter_field() + status: StrFilterLookup[str] | None = strawberry_django.filter_field() + ownership_type: StrFilterLookup[str] | None = strawberry_django.filter_field() + provider_segment_id: StrFilterLookup[str] | None = strawberry_django.filter_field() + comments: StrFilterLookup[str] | None = strawberry_django.filter_field() # Segment type field - segment_type: FilterLookup[str] | None = strawberry_django.filter_field() + segment_type: StrFilterLookup[str] | None = strawberry_django.filter_field() # Path geometry fields path_length_km: FilterLookup[float] | None = strawberry_django.filter_field() - path_source_format: FilterLookup[str] | None = strawberry_django.filter_field() - path_notes: FilterLookup[str] | None = strawberry_django.filter_field() + path_source_format: StrFilterLookup[str] | None = strawberry_django.filter_field() + path_notes: StrFilterLookup[str] | None = strawberry_django.filter_field() # Related fields - using lazy imports to avoid circular dependencies provider: Annotated["ProviderFilter", strawberry.lazy("circuits.graphql.filters")] | None = ( @@ -184,10 +184,10 @@ def has_type_specific_data(self, value: bool, prefix: str) -> Q: class ServicePathFilter(NetBoxModelFilter): """GraphQL filter for ServicePath model""" - name: FilterLookup[str] | None = strawberry_django.filter_field() - status: FilterLookup[str] | None = strawberry_django.filter_field() - kind: FilterLookup[str] | None = strawberry_django.filter_field() - comments: FilterLookup[str] | None = strawberry_django.filter_field() + name: StrFilterLookup[str] | None = strawberry_django.filter_field() + status: StrFilterLookup[str] | None = strawberry_django.filter_field() + kind: StrFilterLookup[str] | None = strawberry_django.filter_field() + comments: StrFilterLookup[str] | None = strawberry_django.filter_field() # Related segments segments: Annotated["SegmentFilter", strawberry.lazy(".filters")] | None = strawberry_django.filter_field() diff --git a/cesnet_service_path_plugin/graphql/types.py b/cesnet_service_path_plugin/graphql/types.py index 6d76ae3..bedb73a 100644 --- a/cesnet_service_path_plugin/graphql/types.py +++ b/cesnet_service_path_plugin/graphql/types.py @@ -5,6 +5,7 @@ from dcim.graphql.types import LocationType, SiteType from netbox.graphql.types import NetBoxObjectType from strawberry import auto, field, lazy +from strawberry.types import Info from strawberry_django import type as strawberry_django_type from decimal import Decimal @@ -70,28 +71,28 @@ class ContractInfoType(NetBoxObjectType): superseded_by: Optional[Annotated["ContractInfoType", lazy(".types")]] @field - def version(self, info) -> int: + def version(self, info: Info) -> int: """Calculate version number by counting predecessors""" return self.version @field - def is_active(self, info) -> bool: + def is_active(self, info: Info) -> bool: """Check if this is the active (not superseded) version""" return self.is_active @field - def total_recurring_cost(self, info) -> Optional[Decimal]: + def total_recurring_cost(self, info: Info) -> Optional[Decimal]: """Calculate total recurring cost - only if user has permission""" # Permission check happens at the query level, so if we're here, user has access return self.total_recurring_cost @field - def total_contract_value(self, info) -> Optional[Decimal]: + def total_contract_value(self, info: Info) -> Optional[Decimal]: """Total contract value including non-recurring charge - only if user has permission""" return self.total_contract_value @field - def commitment_end_date(self, info) -> Optional[str]: + def commitment_end_date(self, info: Info) -> Optional[str]: """Calculate commitment end date based on start date and recurring periods""" if hasattr(self, "commitment_end_date") and self.commitment_end_date: return self.commitment_end_date.isoformat() @@ -128,7 +129,7 @@ class SegmentType(NetBoxObjectType): circuits: List[Annotated["CircuitType", lazy("circuits.graphql.types")]] @field - def contracts(self, info) -> List[Annotated["ContractInfoType", lazy(".types")]]: + def contracts(self, info: Info) -> List[Annotated["ContractInfoType", lazy(".types")]]: """ Return contracts only if user has permission to view them. This mimics the REST API behavior for M:N relationships. From c034c9cacdb5b366a3f9a182e07668b4a30b09c3 Mon Sep 17 00:00:00 2001 From: Jan Krupa <> Date: Mon, 9 Mar 2026 10:04:00 +0000 Subject: [PATCH 07/11] chore: configure ruff (line-length=120, E/F/W/I) and djlint; replace black/autopep8 --- pyproject.toml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 72dbd5a..be7035a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,14 +38,20 @@ Issues = "https://github.com/CESNET/cesnet_service_path_plugin/issues" [project.optional-dependencies] dev = [ - "black", - "autopep8", + "ruff", + "djlint", "pip", "check-manifest", "pytest", "python-dotenv", ] +[tool.ruff] +line-length = 120 + +[tool.ruff.lint] +select = ["E", "F", "W", "I"] + [tool.setuptools.packages.find] # This replaces find_packages() where = ["."] From 21123c420e332c595886e3654d804b50ec75a157 Mon Sep 17 00:00:00 2001 From: Jan Krupa <> Date: Mon, 9 Mar 2026 10:04:11 +0000 Subject: [PATCH 08/11] chore: apply ruff lint fixes (remove unused imports, add noqa suppressions) --- ...actinfo_contractsegmentmapping_and_more.py | 87 ++++++++++++++++--- .../0034_add_type_specific_models.py | 12 +-- ...t_integration_segment_contract_info_api.py | 26 ++++-- tests/test_integration_segment_workflow.py | 5 +- tests/test_komora_service_path_plugin.py | 1 - 5 files changed, 98 insertions(+), 33 deletions(-) diff --git a/cesnet_service_path_plugin/migrations/0033_contractinfo_contractsegmentmapping_and_more.py b/cesnet_service_path_plugin/migrations/0033_contractinfo_contractsegmentmapping_and_more.py index 714ff8d..65d7e9c 100644 --- a/cesnet_service_path_plugin/migrations/0033_contractinfo_contractsegmentmapping_and_more.py +++ b/cesnet_service_path_plugin/migrations/0033_contractinfo_contractsegmentmapping_and_more.py @@ -16,7 +16,7 @@ def migrate_financial_info_to_contract(apps, schema_editor): SegmentFinancialInfo = apps.get_model("cesnet_service_path_plugin", "SegmentFinancialInfo") ContractInfo = apps.get_model("cesnet_service_path_plugin", "ContractInfo") ContractSegmentMapping = apps.get_model("cesnet_service_path_plugin", "ContractSegmentMapping") - Segment = apps.get_model("cesnet_service_path_plugin", "Segment") + Segment = apps.get_model("cesnet_service_path_plugin", "Segment") # noqa: F841 TaggedItem = apps.get_model("extras", "TaggedItem") ContentType = apps.get_model("contenttypes", "ContentType") @@ -36,7 +36,9 @@ def migrate_financial_info_to_contract(apps, schema_editor): end_date = segment.termination_date if segment.termination_date else None # Determine number_of_recurring_charges (convert commitment_period_months to number of charges) - number_of_recurring_charges = old_financial_info.commitment_period_months if old_financial_info.commitment_period_months else None + number_of_recurring_charges = ( + old_financial_info.commitment_period_months if old_financial_info.commitment_period_months else None + ) # Create new ContractInfo contract = ContractInfo.objects.create( @@ -95,7 +97,9 @@ def reverse_migrate_contract_to_financial_info(apps, schema_editor): segment=segment, monthly_charge=contract.recurring_charge or Decimal("0"), charge_currency=contract.charge_currency, - non_recurring_charge=contract.non_recurring_charge if contract.non_recurring_charge and contract.non_recurring_charge > 0 else None, + non_recurring_charge=contract.non_recurring_charge + if contract.non_recurring_charge and contract.non_recurring_charge > 0 + else None, commitment_period_months=commitment_period_months, notes=contract.notes, created=contract.created, @@ -110,7 +114,6 @@ def reverse_migrate_contract_to_financial_info(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ ("cesnet_service_path_plugin", "0032_segment_ownership_type"), ("circuits", "0052_extend_circuit_abs_distance_upper_limit"), @@ -129,18 +132,71 @@ class Migration(migrations.Migration): "custom_field_data", models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder), ), - ("contract_number", models.CharField(blank=True, help_text="Provider's contract reference number", max_length=100)), - ("contract_type", models.CharField(default="new", editable=False, help_text="Type of contract (set automatically)", max_length=20)), - ("charge_currency", models.CharField(blank=True, default="CZK", help_text="Currency for all charges (defaults to CZK if not specified, cannot be changed in amendments)", max_length=3)), + ( + "contract_number", + models.CharField(blank=True, help_text="Provider's contract reference number", max_length=100), + ), + ( + "contract_type", + models.CharField( + default="new", editable=False, help_text="Type of contract (set automatically)", max_length=20 + ), + ), + ( + "charge_currency", + models.CharField( + blank=True, + default="CZK", + help_text="Currency for all charges (defaults to CZK if not specified, cannot be changed in amendments)", + max_length=3, + ), + ), ( "non_recurring_charge", - models.DecimalField(blank=True, decimal_places=2, default=Decimal("0"), help_text="One-time fees for this version (setup, installation, etc.)", max_digits=10, null=True), + models.DecimalField( + blank=True, + decimal_places=2, + default=Decimal("0"), + help_text="One-time fees for this version (setup, installation, etc.)", + max_digits=10, + null=True, + ), + ), + ( + "recurring_charge", + models.DecimalField( + blank=True, + decimal_places=2, + help_text="Recurring fee amount (optional for amendments)", + max_digits=10, + null=True, + ), + ), + ( + "recurring_charge_period", + models.CharField( + blank=True, + help_text="Frequency of recurring charges (optional for amendments)", + max_length=20, + null=True, + ), + ), + ( + "number_of_recurring_charges", + models.PositiveIntegerField( + blank=True, + help_text="Number of recurring charge periods in this contract (optional for amendments)", + null=True, + ), + ), + ( + "start_date", + models.DateField(blank=True, help_text="When this contract version starts (optional)", null=True), + ), + ( + "end_date", + models.DateField(blank=True, help_text="When this contract version ends (optional)", null=True), ), - ("recurring_charge", models.DecimalField(blank=True, decimal_places=2, help_text="Recurring fee amount (optional for amendments)", max_digits=10, null=True)), - ("recurring_charge_period", models.CharField(blank=True, help_text="Frequency of recurring charges (optional for amendments)", max_length=20, null=True)), - ("number_of_recurring_charges", models.PositiveIntegerField(blank=True, help_text="Number of recurring charge periods in this contract (optional for amendments)", null=True)), - ("start_date", models.DateField(blank=True, help_text="When this contract version starts (optional)", null=True)), - ("end_date", models.DateField(blank=True, help_text="When this contract version ends (optional)", null=True)), ("notes", models.TextField(blank=True, help_text="Notes specific to this version")), ( "previous_version", @@ -179,7 +235,10 @@ class Migration(migrations.Migration): name="ContractSegmentMapping", fields=[ ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), - ("added_date", models.DateField(auto_now_add=True, help_text="When this segment was added to the contract")), + ( + "added_date", + models.DateField(auto_now_add=True, help_text="When this segment was added to the contract"), + ), ("notes", models.TextField(blank=True, help_text="Notes about this segment-contract relationship")), ( "contract", diff --git a/cesnet_service_path_plugin/migrations/0034_add_type_specific_models.py b/cesnet_service_path_plugin/migrations/0034_add_type_specific_models.py index e39881c..9c80234 100644 --- a/cesnet_service_path_plugin/migrations/0034_add_type_specific_models.py +++ b/cesnet_service_path_plugin/migrations/0034_add_type_specific_models.py @@ -13,7 +13,6 @@ import netbox.models.deletion import taggit.managers import utilities.json -from decimal import Decimal from django.db import migrations, models @@ -46,7 +45,7 @@ def migrate_json_to_models_forward(apps, schema_editor): json_data = segment.type_specific_data # Handle fiber_type - was multichoice (list) in JSON - fiber_type_raw = json_data.get("fiber_type") + fiber_type_raw = json_data.get("fiber_type") # noqa: F841 # Note: The new model doesn't have a direct fiber_type list field # Instead we have fiber_mode and subtypes # For now, we'll skip fiber_type as it's being replaced by the new structure @@ -104,7 +103,7 @@ def migrate_json_to_models_forward(apps, schema_editor): print(f" ⚠️ {error_msg}") # Print migration summary - print(f"\n✅ Migration completed:") + print("\n✅ Migration completed:") print(f" - Dark Fiber segments: {stats['dark_fiber']}") print(f" - Optical Spectrum segments: {stats['optical_spectrum']}") print(f" - Ethernet Service segments: {stats['ethernet_service']}") @@ -194,7 +193,7 @@ def migrate_json_to_models_reverse(apps, schema_editor): Reverse migration: Copy data from models back to JSON field. This allows rollback if needed. """ - Segment = apps.get_model("cesnet_service_path_plugin", "Segment") + Segment = apps.get_model("cesnet_service_path_plugin", "Segment") # noqa: F841 DarkFiberSegmentData = apps.get_model("cesnet_service_path_plugin", "DarkFiberSegmentData") OpticalSpectrumSegmentData = apps.get_model("cesnet_service_path_plugin", "OpticalSpectrumSegmentData") EthernetServiceSegmentData = apps.get_model("cesnet_service_path_plugin", "EthernetServiceSegmentData") @@ -203,9 +202,7 @@ def migrate_json_to_models_reverse(apps, schema_editor): for df_data in DarkFiberSegmentData.objects.all(): segment = df_data.segment segment.type_specific_data = { - "fiber_attenuation_max": float(df_data.fiber_attenuation_max) - if df_data.fiber_attenuation_max - else None, + "fiber_attenuation_max": float(df_data.fiber_attenuation_max) if df_data.fiber_attenuation_max else None, "total_loss": float(df_data.total_loss) if df_data.total_loss else None, "total_length": float(df_data.total_length) if df_data.total_length else None, "number_of_fibers": df_data.number_of_fibers, @@ -315,7 +312,6 @@ def _reverse_map_interface_type(new_value): class Migration(migrations.Migration): - dependencies = [ ("cesnet_service_path_plugin", "0033_contractinfo_contractsegmentmapping_and_more"), ("extras", "0133_make_cf_minmax_decimal"), diff --git a/tests/test_integration_segment_contract_info_api.py b/tests/test_integration_segment_contract_info_api.py index 12c93c2..af0f1c2 100644 --- a/tests/test_integration_segment_contract_info_api.py +++ b/tests/test_integration_segment_contract_info_api.py @@ -245,7 +245,9 @@ def test_contract_version_numbers(versioned_contract_chain): v3_id = versioned_contract_chain["v3_id"] # Check v1 - response = requests.get(f"{BASE_URL}/api/plugins/cesnet-service-path-plugin/contract-info/{v1_id}/", headers=HEADERS) + response = requests.get( + f"{BASE_URL}/api/plugins/cesnet-service-path-plugin/contract-info/{v1_id}/", headers=HEADERS + ) assert response.status_code == 200 data = response.json() assert data["version"] == 1 @@ -254,10 +256,14 @@ def test_contract_version_numbers(versioned_contract_chain): print(f"V1 is_active: {data['is_active']}, superseded_by: {data['superseded_by']}") assert data["is_active"] is False, f"V1 should be inactive (superseded), but is_active={data['is_active']}" assert data["previous_version"] is None - assert data["superseded_by"] == v2_id, f"V1 should be superseded by v2 ({v2_id}), but superseded_by={data['superseded_by']}" + assert data["superseded_by"] == v2_id, ( + f"V1 should be superseded by v2 ({v2_id}), but superseded_by={data['superseded_by']}" + ) # Check v2 - response = requests.get(f"{BASE_URL}/api/plugins/cesnet-service-path-plugin/contract-info/{v2_id}/", headers=HEADERS) + response = requests.get( + f"{BASE_URL}/api/plugins/cesnet-service-path-plugin/contract-info/{v2_id}/", headers=HEADERS + ) assert response.status_code == 200 data = response.json() assert data["version"] == 2 @@ -267,7 +273,9 @@ def test_contract_version_numbers(versioned_contract_chain): assert data["superseded_by"] == v3_id # Check v3 - response = requests.get(f"{BASE_URL}/api/plugins/cesnet-service-path-plugin/contract-info/{v3_id}/", headers=HEADERS) + response = requests.get( + f"{BASE_URL}/api/plugins/cesnet-service-path-plugin/contract-info/{v3_id}/", headers=HEADERS + ) assert response.status_code == 200 data = response.json() assert data["version"] == 3 @@ -283,7 +291,9 @@ def test_contract_notes(versioned_contract_chain): v3_id = versioned_contract_chain["v3_id"] - response = requests.get(f"{BASE_URL}/api/plugins/cesnet-service-path-plugin/contract-info/{v3_id}/", headers=HEADERS) + response = requests.get( + f"{BASE_URL}/api/plugins/cesnet-service-path-plugin/contract-info/{v3_id}/", headers=HEADERS + ) assert response.status_code == 200 data = response.json() @@ -299,7 +309,9 @@ def test_computed_financial_fields(versioned_contract_chain): v3_id = versioned_contract_chain["v3_id"] - response = requests.get(f"{BASE_URL}/api/plugins/cesnet-service-path-plugin/contract-info/{v3_id}/", headers=HEADERS) + response = requests.get( + f"{BASE_URL}/api/plugins/cesnet-service-path-plugin/contract-info/{v3_id}/", headers=HEADERS + ) assert response.status_code == 200 data = response.json() @@ -340,7 +352,7 @@ def test_contract_currency_immutability(versioned_contract_chain): print("\n=== Testing Currency Immutability ===") v3_id = versioned_contract_chain["v3_id"] - today = date.today() + today = date.today() # noqa: F841 # Attempt to change currency (should fail validation) response = requests.patch( diff --git a/tests/test_integration_segment_workflow.py b/tests/test_integration_segment_workflow.py index bcb46d3..89ea836 100644 --- a/tests/test_integration_segment_workflow.py +++ b/tests/test_integration_segment_workflow.py @@ -13,7 +13,6 @@ /home/albert/cesnet/netbox/venv/bin/python -m pytest tests/test_integration_segment_workflow.py -v """ -import pytest import requests import os from dotenv import load_dotenv @@ -285,7 +284,7 @@ def test_changing_segment_type(): # Verify dark fiber data is deleted (cascade or manual cleanup needed) # Note: This depends on whether changing segment_type triggers cascade delete # The segment should now have type_specific_data = None - updated_seg = requests.get( + updated_seg = requests.get( # noqa: F841 f"{BASE_URL}/api/plugins/cesnet-service-path-plugin/segments/{segment_id}/", headers=HEADERS, ) @@ -463,7 +462,7 @@ def test_list_view_includes_type_specific_data(): list_response = requests.get( f"{BASE_URL}/api/plugins/cesnet-service-path-plugin/segments/", headers=HEADERS, - params={"name": "List View Test Segment"} + params={"name": "List View Test Segment"}, ) assert list_response.status_code == 200 diff --git a/tests/test_komora_service_path_plugin.py b/tests/test_komora_service_path_plugin.py index b7fb541..33d5ca4 100644 --- a/tests/test_komora_service_path_plugin.py +++ b/tests/test_komora_service_path_plugin.py @@ -1,4 +1,3 @@ #!/usr/bin/env python """Tests for `cesnet_service_path_plugin` package.""" - From 7843fb0e020f3e56761f17fbed59ca32a34dd678 Mon Sep 17 00:00:00 2001 From: Jan Krupa <> Date: Mon, 9 Mar 2026 10:04:18 +0000 Subject: [PATCH 09/11] chore: apply ruff format (line-length=120) to all Python source files --- .../api/serializers/contract_info.py | 18 +- .../serializers/dark_fiber_data_serializer.py | 58 ++- .../ethernet_service_data_serializer.py | 50 +- .../optical_spectrum_data_serializer.py | 50 +- .../api/serializers/segment.py | 6 +- .../api/views/dark_fiber_data_view.py | 2 +- .../api/views/ethernet_service_data_view.py | 2 +- .../api/views/optical_spectrum_data_view.py | 2 +- .../filtersets/contract_info.py | 4 +- .../forms/contract_info.py | 7 +- ...er_segmentfinancialinfo_charge_currency.py | 7 +- .../migrations/0032_segment_ownership_type.py | 9 +- .../0035_remove_type_specific_data.py | 7 +- ...pectrumsegmentdata_chromatic_dispersion.py | 18 +- .../0037_darkfibersegmentdata_id_and_more.py | 38 +- .../models/dark_fiber_data.py | 4 +- .../templates/buttons/clone_custom.html | 6 +- .../circuit_segments_extension.html | 5 +- .../contractinfo.html | 238 +++++---- .../inc/cytoscape_includes.html | 2 +- .../inc/leaflet_includes.html | 5 +- .../inc/map_layer_dropdown.html | 85 ++-- .../inc/map_layers_config.html | 2 +- .../inc/map_layers_styles.html | 2 +- .../inc/path_data_badge.html | 5 +- .../inc/path_length.html | 2 +- .../inc/topology_segment_card.html | 64 ++- .../inc/topology_styles.html | 2 +- .../inc/topology_visualization.html | 4 +- .../provider_segments_extension.html | 8 +- .../cesnet_service_path_plugin/segment.html | 463 +++++++++--------- .../segment_edit.html | 2 +- .../segment_list.html | 6 +- .../segment_map.html | 139 +++--- .../segment_path_clear_confirm.html | 151 +++--- .../segmentcircuitmapping.html | 49 +- .../segments_map.html | 347 ++++++------- .../servicepath.html | 41 +- .../views/contract_info.py | 5 +- .../views/dark_fiber_data.py | 6 +- .../views/ethernet_service_data.py | 6 +- .../views/optical_spectrum_data.py | 6 +- cesnet_service_path_plugin/views/segment.py | 24 +- 43 files changed, 969 insertions(+), 988 deletions(-) diff --git a/cesnet_service_path_plugin/api/serializers/contract_info.py b/cesnet_service_path_plugin/api/serializers/contract_info.py index ea1e827..0e5843b 100644 --- a/cesnet_service_path_plugin/api/serializers/contract_info.py +++ b/cesnet_service_path_plugin/api/serializers/contract_info.py @@ -28,11 +28,11 @@ class ContractInfoSerializer(NetBoxModelSerializer): queryset=ContractInfo.objects.all(), required=False, allow_null=True, - help_text="Link to previous version for amendments/renewals" + help_text="Link to previous version for amendments/renewals", ) contract_type = serializers.CharField( required=False, - help_text="Type of contract (auto-set to 'amendment' if previous_version exists, can be set to 'renewal')" + help_text="Type of contract (auto-set to 'amendment' if previous_version exists, can be set to 'renewal')", ) # Read-only versioning fields superseded_by = serializers.PrimaryKeyRelatedField(read_only=True) @@ -122,11 +122,13 @@ def get_segments_detail(self, obj): "plugins-api:cesnet_service_path_plugin-api:segment-detail", kwargs={"pk": segment.id} ) - segments_list.append({ - "id": segment.id, - "url": segment_url, - "display": str(segment), - "name": segment.name, - }) + segments_list.append( + { + "id": segment.id, + "url": segment_url, + "display": str(segment), + "name": segment.name, + } + ) return segments_list diff --git a/cesnet_service_path_plugin/api/serializers/dark_fiber_data_serializer.py b/cesnet_service_path_plugin/api/serializers/dark_fiber_data_serializer.py index 0ec5d8f..ccc0506 100644 --- a/cesnet_service_path_plugin/api/serializers/dark_fiber_data_serializer.py +++ b/cesnet_service_path_plugin/api/serializers/dark_fiber_data_serializer.py @@ -18,46 +18,44 @@ class DarkFiberSegmentDataSerializer(NetBoxModelSerializer): # Write-only segment ID for POST/PUT/PATCH operations segment_id = serializers.PrimaryKeyRelatedField( queryset=Segment.objects.all(), - source='segment', + source="segment", write_only=True, required=True, - help_text="ID of the segment this technical data belongs to" + help_text="ID of the segment this technical data belongs to", ) class Meta: model = DarkFiberSegmentData fields = [ - 'segment', - 'segment_id', - 'fiber_mode', - 'single_mode_subtype', - 'multimode_subtype', - 'jacket_type', - 'fiber_attenuation_max', - 'total_loss', - 'total_length', - 'number_of_fibers', - 'connector_type_side_a', - 'connector_type_side_b', - 'created', - 'last_updated', + "segment", + "segment_id", + "fiber_mode", + "single_mode_subtype", + "multimode_subtype", + "jacket_type", + "fiber_attenuation_max", + "total_loss", + "total_length", + "number_of_fibers", + "connector_type_side_a", + "connector_type_side_b", + "created", + "last_updated", ] def get_segment(self, obj): """Return basic segment info for GET operations""" - request = self.context.get('request') + request = self.context.get("request") if not request: return { - 'id': obj.segment.id, - 'name': obj.segment.name, + "id": obj.segment.id, + "name": obj.segment.name, } return { - 'id': obj.segment.id, - 'name': obj.segment.name, - 'url': request.build_absolute_uri( - f'/api/plugins/cesnet-service-path-plugin/segments/{obj.segment.id}/' - ) + "id": obj.segment.id, + "name": obj.segment.name, + "url": request.build_absolute_uri(f"/api/plugins/cesnet-service-path-plugin/segments/{obj.segment.id}/"), } def validate(self, data): @@ -68,15 +66,15 @@ def validate(self, data): """ from django.core.exceptions import ValidationError as DjangoValidationError - segment = data.get('segment') + segment = data.get("segment") # Check if this is an update (instance exists) or create (new instance) if not self.instance and segment: # Creating new instance - check if segment already has data - if hasattr(segment, 'dark_fiber_data'): - raise serializers.ValidationError({ - 'segment': f'Segment "{segment.name}" already has dark fiber technical data.' - }) + if hasattr(segment, "dark_fiber_data"): + raise serializers.ValidationError( + {"segment": f'Segment "{segment.name}" already has dark fiber technical data.'} + ) # Trigger model-level validation by creating a temporary instance # This ensures model's clean() method is called @@ -93,6 +91,6 @@ def validate(self, data): instance.clean() except DjangoValidationError as e: # Convert Django ValidationError to DRF ValidationError - raise serializers.ValidationError(e.message_dict if hasattr(e, 'message_dict') else str(e)) + raise serializers.ValidationError(e.message_dict if hasattr(e, "message_dict") else str(e)) return data diff --git a/cesnet_service_path_plugin/api/serializers/ethernet_service_data_serializer.py b/cesnet_service_path_plugin/api/serializers/ethernet_service_data_serializer.py index b9b71be..680d19c 100644 --- a/cesnet_service_path_plugin/api/serializers/ethernet_service_data_serializer.py +++ b/cesnet_service_path_plugin/api/serializers/ethernet_service_data_serializer.py @@ -18,42 +18,40 @@ class EthernetServiceSegmentDataSerializer(NetBoxModelSerializer): # Write-only segment ID for POST/PUT/PATCH operations segment_id = serializers.PrimaryKeyRelatedField( queryset=Segment.objects.all(), - source='segment', + source="segment", write_only=True, required=True, - help_text="ID of the segment this technical data belongs to" + help_text="ID of the segment this technical data belongs to", ) class Meta: model = EthernetServiceSegmentData fields = [ - 'segment', - 'segment_id', - 'port_speed', - 'vlan_id', - 'vlan_tags', - 'encapsulation_type', - 'interface_type', - 'mtu_size', - 'created', - 'last_updated', + "segment", + "segment_id", + "port_speed", + "vlan_id", + "vlan_tags", + "encapsulation_type", + "interface_type", + "mtu_size", + "created", + "last_updated", ] def get_segment(self, obj): """Return basic segment info for GET operations""" - request = self.context.get('request') + request = self.context.get("request") if not request: return { - 'id': obj.segment.id, - 'name': obj.segment.name, + "id": obj.segment.id, + "name": obj.segment.name, } return { - 'id': obj.segment.id, - 'name': obj.segment.name, - 'url': request.build_absolute_uri( - f'/api/plugins/cesnet-service-path-plugin/segments/{obj.segment.id}/' - ) + "id": obj.segment.id, + "name": obj.segment.name, + "url": request.build_absolute_uri(f"/api/plugins/cesnet-service-path-plugin/segments/{obj.segment.id}/"), } def validate(self, data): @@ -64,15 +62,15 @@ def validate(self, data): """ from django.core.exceptions import ValidationError as DjangoValidationError - segment = data.get('segment') + segment = data.get("segment") # Check if this is an update (instance exists) or create (new instance) if not self.instance and segment: # Creating new instance - check if segment already has data - if hasattr(segment, 'ethernet_service_data'): - raise serializers.ValidationError({ - 'segment': f'Segment "{segment.name}" already has ethernet service technical data.' - }) + if hasattr(segment, "ethernet_service_data"): + raise serializers.ValidationError( + {"segment": f'Segment "{segment.name}" already has ethernet service technical data.'} + ) # Trigger model-level validation by creating a temporary instance # This ensures model's clean() method is called @@ -89,6 +87,6 @@ def validate(self, data): instance.clean() except DjangoValidationError as e: # Convert Django ValidationError to DRF ValidationError - raise serializers.ValidationError(e.message_dict if hasattr(e, 'message_dict') else str(e)) + raise serializers.ValidationError(e.message_dict if hasattr(e, "message_dict") else str(e)) return data diff --git a/cesnet_service_path_plugin/api/serializers/optical_spectrum_data_serializer.py b/cesnet_service_path_plugin/api/serializers/optical_spectrum_data_serializer.py index 2fcd18a..90dd358 100644 --- a/cesnet_service_path_plugin/api/serializers/optical_spectrum_data_serializer.py +++ b/cesnet_service_path_plugin/api/serializers/optical_spectrum_data_serializer.py @@ -18,42 +18,40 @@ class OpticalSpectrumSegmentDataSerializer(NetBoxModelSerializer): # Write-only segment ID for POST/PUT/PATCH operations segment_id = serializers.PrimaryKeyRelatedField( queryset=Segment.objects.all(), - source='segment', + source="segment", write_only=True, required=True, - help_text="ID of the segment this technical data belongs to" + help_text="ID of the segment this technical data belongs to", ) class Meta: model = OpticalSpectrumSegmentData fields = [ - 'segment', - 'segment_id', - 'wavelength', - 'spectral_slot_width', - 'itu_grid_position', - 'chromatic_dispersion', - 'pmd_tolerance', - 'modulation_format', - 'created', - 'last_updated', + "segment", + "segment_id", + "wavelength", + "spectral_slot_width", + "itu_grid_position", + "chromatic_dispersion", + "pmd_tolerance", + "modulation_format", + "created", + "last_updated", ] def get_segment(self, obj): """Return basic segment info for GET operations""" - request = self.context.get('request') + request = self.context.get("request") if not request: return { - 'id': obj.segment.id, - 'name': obj.segment.name, + "id": obj.segment.id, + "name": obj.segment.name, } return { - 'id': obj.segment.id, - 'name': obj.segment.name, - 'url': request.build_absolute_uri( - f'/api/plugins/cesnet-service-path-plugin/segments/{obj.segment.id}/' - ) + "id": obj.segment.id, + "name": obj.segment.name, + "url": request.build_absolute_uri(f"/api/plugins/cesnet-service-path-plugin/segments/{obj.segment.id}/"), } def validate(self, data): @@ -64,15 +62,15 @@ def validate(self, data): """ from django.core.exceptions import ValidationError as DjangoValidationError - segment = data.get('segment') + segment = data.get("segment") # Check if this is an update (instance exists) or create (new instance) if not self.instance and segment: # Creating new instance - check if segment already has data - if hasattr(segment, 'optical_spectrum_data'): - raise serializers.ValidationError({ - 'segment': f'Segment "{segment.name}" already has optical spectrum technical data.' - }) + if hasattr(segment, "optical_spectrum_data"): + raise serializers.ValidationError( + {"segment": f'Segment "{segment.name}" already has optical spectrum technical data.'} + ) # Trigger model-level validation by creating a temporary instance # This ensures model's clean() method is called @@ -89,6 +87,6 @@ def validate(self, data): instance.clean() except DjangoValidationError as e: # Convert Django ValidationError to DRF ValidationError - raise serializers.ValidationError(e.message_dict if hasattr(e, 'message_dict') else str(e)) + raise serializers.ValidationError(e.message_dict if hasattr(e, "message_dict") else str(e)) return data diff --git a/cesnet_service_path_plugin/api/serializers/segment.py b/cesnet_service_path_plugin/api/serializers/segment.py index 316ec4e..9b8b825 100644 --- a/cesnet_service_path_plugin/api/serializers/segment.py +++ b/cesnet_service_path_plugin/api/serializers/segment.py @@ -72,17 +72,17 @@ def get_type_specific_data(self, obj): or EthernetServiceSegmentData depending on the segment's type. """ try: - if obj.segment_type == 'dark_fiber': + if obj.segment_type == "dark_fiber": try: return DarkFiberSegmentDataSerializer(obj.dark_fiber_data, context=self.context).data except obj.dark_fiber_data.RelatedObjectDoesNotExist: return None - elif obj.segment_type == 'optical_spectrum': + elif obj.segment_type == "optical_spectrum": try: return OpticalSpectrumSegmentDataSerializer(obj.optical_spectrum_data, context=self.context).data except obj.optical_spectrum_data.RelatedObjectDoesNotExist: return None - elif obj.segment_type == 'ethernet_service': + elif obj.segment_type == "ethernet_service": try: return EthernetServiceSegmentDataSerializer(obj.ethernet_service_data, context=self.context).data except obj.ethernet_service_data.RelatedObjectDoesNotExist: diff --git a/cesnet_service_path_plugin/api/views/dark_fiber_data_view.py b/cesnet_service_path_plugin/api/views/dark_fiber_data_view.py index 116999c..b061a57 100644 --- a/cesnet_service_path_plugin/api/views/dark_fiber_data_view.py +++ b/cesnet_service_path_plugin/api/views/dark_fiber_data_view.py @@ -13,5 +13,5 @@ class DarkFiberSegmentDataViewSet(NetBoxModelViewSet): Provides CRUD operations for dark fiber segment technical specifications. """ - queryset = DarkFiberSegmentData.objects.select_related('segment').all() + queryset = DarkFiberSegmentData.objects.select_related("segment").all() serializer_class = DarkFiberSegmentDataSerializer diff --git a/cesnet_service_path_plugin/api/views/ethernet_service_data_view.py b/cesnet_service_path_plugin/api/views/ethernet_service_data_view.py index f5c1efb..f32f55a 100644 --- a/cesnet_service_path_plugin/api/views/ethernet_service_data_view.py +++ b/cesnet_service_path_plugin/api/views/ethernet_service_data_view.py @@ -13,5 +13,5 @@ class EthernetServiceSegmentDataViewSet(NetBoxModelViewSet): Provides CRUD operations for ethernet service segment technical specifications. """ - queryset = EthernetServiceSegmentData.objects.select_related('segment').all() + queryset = EthernetServiceSegmentData.objects.select_related("segment").all() serializer_class = EthernetServiceSegmentDataSerializer diff --git a/cesnet_service_path_plugin/api/views/optical_spectrum_data_view.py b/cesnet_service_path_plugin/api/views/optical_spectrum_data_view.py index 1ae0093..a278a69 100644 --- a/cesnet_service_path_plugin/api/views/optical_spectrum_data_view.py +++ b/cesnet_service_path_plugin/api/views/optical_spectrum_data_view.py @@ -13,5 +13,5 @@ class OpticalSpectrumSegmentDataViewSet(NetBoxModelViewSet): Provides CRUD operations for optical spectrum segment technical specifications. """ - queryset = OpticalSpectrumSegmentData.objects.select_related('segment').all() + queryset = OpticalSpectrumSegmentData.objects.select_related("segment").all() serializer_class = OpticalSpectrumSegmentDataSerializer diff --git a/cesnet_service_path_plugin/filtersets/contract_info.py b/cesnet_service_path_plugin/filtersets/contract_info.py index a07eaa7..46c5632 100644 --- a/cesnet_service_path_plugin/filtersets/contract_info.py +++ b/cesnet_service_path_plugin/filtersets/contract_info.py @@ -84,6 +84,4 @@ def search(self, queryset, name, value): contract_number = Q(contract_number__icontains=value) notes = Q(notes__icontains=value) - return queryset.filter( - contract_number | notes - ) + return queryset.filter(contract_number | notes) diff --git a/cesnet_service_path_plugin/forms/contract_info.py b/cesnet_service_path_plugin/forms/contract_info.py index de74180..ae94ca8 100644 --- a/cesnet_service_path_plugin/forms/contract_info.py +++ b/cesnet_service_path_plugin/forms/contract_info.py @@ -44,7 +44,6 @@ class ContractInfoForm(NetBoxModelForm): required=False, min_value=0, help_text="Number of recurring charge periods (0 for no recurring charges)" ) - notes = forms.CharField( required=False, widget=forms.Textarea(attrs={"rows": 3}), help_text="Notes specific to this version" ) @@ -89,9 +88,9 @@ def __init__(self, *args, **kwargs): # For amendments, make currency field disabled in the UI # Note: disabled fields don't submit values, but we handle this in clean_charge_currency() self.fields["charge_currency"].disabled = True - self.fields["charge_currency"].help_text = ( - "Currency cannot be changed in amendments (inherited from original contract)" - ) + self.fields[ + "charge_currency" + ].help_text = "Currency cannot be changed in amendments (inherited from original contract)" def clean_charge_currency(self): """ diff --git a/cesnet_service_path_plugin/migrations/0031_alter_segmentfinancialinfo_charge_currency.py b/cesnet_service_path_plugin/migrations/0031_alter_segmentfinancialinfo_charge_currency.py index adaf7a2..6e95689 100644 --- a/cesnet_service_path_plugin/migrations/0031_alter_segmentfinancialinfo_charge_currency.py +++ b/cesnet_service_path_plugin/migrations/0031_alter_segmentfinancialinfo_charge_currency.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('cesnet_service_path_plugin', '0030_alter_segment_location_a_alter_segment_location_b'), + ("cesnet_service_path_plugin", "0030_alter_segment_location_a_alter_segment_location_b"), ] operations = [ migrations.AlterField( - model_name='segmentfinancialinfo', - name='charge_currency', + model_name="segmentfinancialinfo", + name="charge_currency", field=models.CharField(max_length=3), ), ] diff --git a/cesnet_service_path_plugin/migrations/0032_segment_ownership_type.py b/cesnet_service_path_plugin/migrations/0032_segment_ownership_type.py index ccf6aa7..ea53977 100644 --- a/cesnet_service_path_plugin/migrations/0032_segment_ownership_type.py +++ b/cesnet_service_path_plugin/migrations/0032_segment_ownership_type.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('cesnet_service_path_plugin', '0031_alter_segmentfinancialinfo_charge_currency'), + ("cesnet_service_path_plugin", "0031_alter_segmentfinancialinfo_charge_currency"), ] operations = [ migrations.AddField( - model_name='segment', - name='ownership_type', - field=models.CharField(default='leased', max_length=30), + model_name="segment", + name="ownership_type", + field=models.CharField(default="leased", max_length=30), ), ] diff --git a/cesnet_service_path_plugin/migrations/0035_remove_type_specific_data.py b/cesnet_service_path_plugin/migrations/0035_remove_type_specific_data.py index ca6140d..6a171c0 100644 --- a/cesnet_service_path_plugin/migrations/0035_remove_type_specific_data.py +++ b/cesnet_service_path_plugin/migrations/0035_remove_type_specific_data.py @@ -4,14 +4,13 @@ class Migration(migrations.Migration): - dependencies = [ - ('cesnet_service_path_plugin', '0034_add_type_specific_models'), + ("cesnet_service_path_plugin", "0034_add_type_specific_models"), ] operations = [ migrations.RemoveField( - model_name='segment', - name='type_specific_data', + model_name="segment", + name="type_specific_data", ), ] diff --git a/cesnet_service_path_plugin/migrations/0036_alter_opticalspectrumsegmentdata_chromatic_dispersion.py b/cesnet_service_path_plugin/migrations/0036_alter_opticalspectrumsegmentdata_chromatic_dispersion.py index 6fa733e..aaa2de8 100644 --- a/cesnet_service_path_plugin/migrations/0036_alter_opticalspectrumsegmentdata_chromatic_dispersion.py +++ b/cesnet_service_path_plugin/migrations/0036_alter_opticalspectrumsegmentdata_chromatic_dispersion.py @@ -5,15 +5,23 @@ class Migration(migrations.Migration): - dependencies = [ - ('cesnet_service_path_plugin', '0035_remove_type_specific_data'), + ("cesnet_service_path_plugin", "0035_remove_type_specific_data"), ] operations = [ migrations.AlterField( - model_name='opticalspectrumsegmentdata', - name='chromatic_dispersion', - field=models.DecimalField(blank=True, decimal_places=3, max_digits=8, null=True, validators=[django.core.validators.MinValueValidator(-100000), django.core.validators.MaxValueValidator(100000)]), + model_name="opticalspectrumsegmentdata", + name="chromatic_dispersion", + field=models.DecimalField( + blank=True, + decimal_places=3, + max_digits=8, + null=True, + validators=[ + django.core.validators.MinValueValidator(-100000), + django.core.validators.MaxValueValidator(100000), + ], + ), ), ] diff --git a/cesnet_service_path_plugin/migrations/0037_darkfibersegmentdata_id_and_more.py b/cesnet_service_path_plugin/migrations/0037_darkfibersegmentdata_id_and_more.py index 86c4235..312b2c8 100644 --- a/cesnet_service_path_plugin/migrations/0037_darkfibersegmentdata_id_and_more.py +++ b/cesnet_service_path_plugin/migrations/0037_darkfibersegmentdata_id_and_more.py @@ -34,38 +34,24 @@ def forwards(apps, schema_editor): for table, pkey_name in TABLES: # 1. Drop the existing primary key (segment_id) - schema_editor.execute( - f'ALTER TABLE "{table}" DROP CONSTRAINT "{pkey_name}";' - ) + schema_editor.execute(f'ALTER TABLE "{table}" DROP CONSTRAINT "{pkey_name}";') # 2. Add new "id" column as identity primary key schema_editor.execute( - f'ALTER TABLE "{table}" ADD COLUMN "id" bigint NOT NULL ' - f"GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY;" + f'ALTER TABLE "{table}" ADD COLUMN "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY;' ) # 3. Add unique constraint on segment_id (OneToOneField needs it) - schema_editor.execute( - f'ALTER TABLE "{table}" ADD CONSTRAINT ' - f'"{table}_segment_id_key" UNIQUE ("segment_id");' - ) + schema_editor.execute(f'ALTER TABLE "{table}" ADD CONSTRAINT "{table}_segment_id_key" UNIQUE ("segment_id");') def backwards(apps, schema_editor): for table, pkey_name in TABLES: # Reverse: drop the unique constraint, drop id column, re-add old PK - schema_editor.execute( - f'ALTER TABLE "{table}" DROP CONSTRAINT "{table}_segment_id_key";' - ) - schema_editor.execute( - f'ALTER TABLE "{table}" DROP COLUMN "id";' - ) - schema_editor.execute( - f'ALTER TABLE "{table}" ADD CONSTRAINT "{pkey_name}" ' - f'PRIMARY KEY ("segment_id");' - ) + schema_editor.execute(f'ALTER TABLE "{table}" DROP CONSTRAINT "{table}_segment_id_key";') + schema_editor.execute(f'ALTER TABLE "{table}" DROP COLUMN "id";') + schema_editor.execute(f'ALTER TABLE "{table}" ADD CONSTRAINT "{pkey_name}" PRIMARY KEY ("segment_id");') class Migration(migrations.Migration): - dependencies = [ ( "cesnet_service_path_plugin", @@ -82,9 +68,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name="darkfibersegmentdata", name="id", - field=models.BigAutoField( - auto_created=True, primary_key=True, serialize=False - ), + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), ), migrations.AlterField( model_name="darkfibersegmentdata", @@ -98,9 +82,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name="ethernetservicesegmentdata", name="id", - field=models.BigAutoField( - auto_created=True, primary_key=True, serialize=False - ), + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), ), migrations.AlterField( model_name="ethernetservicesegmentdata", @@ -114,9 +96,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name="opticalspectrumsegmentdata", name="id", - field=models.BigAutoField( - auto_created=True, primary_key=True, serialize=False - ), + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), ), migrations.AlterField( model_name="opticalspectrumsegmentdata", diff --git a/cesnet_service_path_plugin/models/dark_fiber_data.py b/cesnet_service_path_plugin/models/dark_fiber_data.py index ba02341..f86267f 100644 --- a/cesnet_service_path_plugin/models/dark_fiber_data.py +++ b/cesnet_service_path_plugin/models/dark_fiber_data.py @@ -135,9 +135,7 @@ def clean(self): # Validate that single-mode subtype is only set for single-mode fibers if self.single_mode_subtype and self.fiber_mode != FiberModeChoices.SINGLE_MODE: - errors["single_mode_subtype"] = ( - "Single-mode subtype can only be set when fiber mode is Single-mode" - ) + errors["single_mode_subtype"] = "Single-mode subtype can only be set when fiber mode is Single-mode" # Validate that multimode subtype is only set for multimode fibers if self.multimode_subtype and self.fiber_mode != FiberModeChoices.MULTIMODE: diff --git a/cesnet_service_path_plugin/templates/buttons/clone_custom.html b/cesnet_service_path_plugin/templates/buttons/clone_custom.html index a4e2a81..0809230 100644 --- a/cesnet_service_path_plugin/templates/buttons/clone_custom.html +++ b/cesnet_service_path_plugin/templates/buttons/clone_custom.html @@ -1,6 +1,6 @@ {% load i18n %} {% if url %} - - {{ label }} - + + {{ label }} + {% endif %} diff --git a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/circuit_segments_extension.html b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/circuit_segments_extension.html index 5d3070a..22bf05d 100644 --- a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/circuit_segments_extension.html +++ b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/circuit_segments_extension.html @@ -1,5 +1,4 @@ {% load i18n %} -
@@ -15,10 +14,8 @@
{% htmx_table 'plugins:cesnet_service_path_plugin:segmentcircuitmapping_list' circuit_id=object.pk %}
- {% if topologies %} {% include 'cesnet_service_path_plugin/inc/topology_visualization.html' %} {% include 'cesnet_service_path_plugin/inc/topology_segment_card.html' %} - -{% endif %} \ No newline at end of file +{% endif %} diff --git a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/contractinfo.html b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/contractinfo.html index 3e18a34..862e756 100644 --- a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/contractinfo.html +++ b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/contractinfo.html @@ -3,9 +3,7 @@ {% load plugins %} {% load custom_filters %} {% load render_table from django_tables2 %} - {% block content %} -
@@ -20,9 +18,7 @@
Contract Information
Contract Type - - {{ object.get_contract_type_display }} - + {{ object.get_contract_type_display }} @@ -39,7 +35,6 @@
Contract Information
-
Financial Information
@@ -48,9 +43,7 @@
Financial Information
Currency - - {{ object.get_charge_currency_display }} - + {{ object.get_charge_currency_display }} @@ -71,28 +64,33 @@
Financial Information
Total Recurring Cost - {{ object.charge_currency }} {{ object.total_recurring_cost|floatformat:2|spacecomma }} + + {{ object.charge_currency }} {{ object.total_recurring_cost|floatformat:2|spacecomma }} + {% if object.non_recurring_charge %} - - Non-Recurring Charge - {{ object.charge_currency }} {{ object.non_recurring_charge|floatformat:2|spacecomma }} - + + Non-Recurring Charge + {{ object.charge_currency }} {{ object.non_recurring_charge|floatformat:2|spacecomma }} + {% endif %} {% if object.total_cumulative_non_recurring %} - - Cumulative Non-Recurring - {{ object.charge_currency }} {{ object.total_cumulative_non_recurring|floatformat:2|spacecomma }} - + + Cumulative Non-Recurring + + {{ object.charge_currency }} {{ object.total_cumulative_non_recurring|floatformat:2|spacecomma }} + + {% endif %} Total Contract Value - {{ object.charge_currency }} {{ object.total_contract_value|floatformat:2|spacecomma }} + + {{ object.charge_currency }} {{ object.total_contract_value|floatformat:2|spacecomma }} +
-
Contract Dates
@@ -107,142 +105,130 @@
Contract Dates
{% if object.end_date %} - {{ object.end_date }} - + title="{{ object.get_end_date_tooltip }}">{{ object.end_date }} {% else %} {{ ''|placeholder }} {% endif %} {% if object.commitment_end_date %} - - Commitment End Date - - - {{ object.commitment_end_date }} - - - + + Commitment End Date + + + {{ object.commitment_end_date }} + + + {% endif %}
{% plugin_left_page object %} -
{% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/tags.html' %} {% include 'inc/panels/comments.html' %} - {% if version_history %} -
-
Version History
-
- - - - - - - - - - - {% for version in version_history %} - - - - - - - {% endfor %} - -
VersionTypeStatusActions
- v{{ version.version }} - - - {{ version.get_contract_type_display }} - - - {% if version.is_active %} - Active - {% else %} - Superseded - {% endif %} - - {% if version.pk != object.pk %} - - View - - {% else %} - Current - {% endif %} -
+
+
Version History
+
+ + + + + + + + + + + {% for version in version_history %} + + + + + + + {% endfor %} + +
VersionTypeStatusActions
+ v{{ version.version }} + + {{ version.get_contract_type_display }} + + {% if version.is_active %} + Active + {% else %} + Superseded + {% endif %} + + {% if version.pk != object.pk %} + + View + + {% else %} + Current + {% endif %} +
+
-
{% endif %} - {% if segments %} -
-
Related Segments
-
- - - - - - - - - - - {% for segment in segments %} - - - - - - - {% endfor %} - -
NameSegment TypeProviderStatus
- {{ segment.name }} - - - {{ segment.get_segment_type_display }} - - - {% if segment.provider %} - {{ segment.provider }} - {% else %} - {{ ''|placeholder }} - {% endif %} - - - {{ segment.get_status_display }} - -
+
+
Related Segments
+
+ + + + + + + + + + + {% for segment in segments %} + + + + + + + {% endfor %} + +
NameSegment TypeProviderStatus
+ {{ segment.name }} + + {{ segment.get_segment_type_display }} + + {% if segment.provider %} + {{ segment.provider }} + {% else %} + {{ ''|placeholder }} + {% endif %} + + {{ segment.get_status_display }} +
+
-
{% endif %} - {% if object.notes %} -
-
Notes
-
-
{{ object.notes }}
+
+
Notes
+
+
{{ object.notes }}
+
-
{% endif %} {% plugin_right_page object %}
- {% endblock content %} diff --git a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/cytoscape_includes.html b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/cytoscape_includes.html index 80421aa..ae4e3be 100644 --- a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/cytoscape_includes.html +++ b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/cytoscape_includes.html @@ -1,2 +1,2 @@ - \ No newline at end of file + diff --git a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/leaflet_includes.html b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/leaflet_includes.html index 4b1846d..658e518 100644 --- a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/leaflet_includes.html +++ b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/leaflet_includes.html @@ -1,3 +1,4 @@ - - \ No newline at end of file + + diff --git a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/map_layer_dropdown.html b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/map_layer_dropdown.html index 27e340f..4ce8fa6 100644 --- a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/map_layer_dropdown.html +++ b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/map_layer_dropdown.html @@ -1,35 +1,62 @@ \ No newline at end of file +
diff --git a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/map_layers_config.html b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/map_layers_config.html index 2884bba..af7d1d2 100644 --- a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/map_layers_config.html +++ b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/map_layers_config.html @@ -126,4 +126,4 @@ } }); } - \ No newline at end of file + diff --git a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/map_layers_styles.html b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/map_layers_styles.html index 3fa78e9..a5de8e0 100644 --- a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/map_layers_styles.html +++ b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/map_layers_styles.html @@ -52,4 +52,4 @@ .segment-highlighted { filter: drop-shadow(0 0 3px rgba(0, 123, 255, 0.8)); } - \ No newline at end of file + diff --git a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/path_data_badge.html b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/path_data_badge.html index 4bbc593..3e8b28c 100644 --- a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/path_data_badge.html +++ b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/path_data_badge.html @@ -1,9 +1,10 @@ {% if record.has_path_data %} - + Yes {% else %} No -{% endif %} \ No newline at end of file +{% endif %} diff --git a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/path_length.html b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/path_length.html index 3889bb3..605b923 100644 --- a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/path_length.html +++ b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/path_length.html @@ -2,4 +2,4 @@ {{ record.path_length_km }} km {% else %} {{ ''|placeholder }} -{% endif %} \ No newline at end of file +{% endif %} diff --git a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/topology_segment_card.html b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/topology_segment_card.html index aa7e01f..e0fffa5 100644 --- a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/topology_segment_card.html +++ b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/topology_segment_card.html @@ -1,46 +1,42 @@ {% load i18n %} {% if not topologies %} -
- {% trans "No topologies available." %} -
+
{% trans "No topologies available." %}
{% else %} - -
-
-
-
+
+
+
+
+ {% if topologies|length > 1 %} + {% trans "Topologies" %} + {% else %} + {% trans "Topology of" %} + {% endif %} + {% for topology_title, topology_data in topologies.items %} {% if topologies|length > 1 %} - {% trans "Topologies" %} - {% else %} - {% trans "Topology of" %} - {% endif %} - {% for topology_title, topology_data in topologies.items %} - {% if topologies|length > 1 %} - - {% else %} - {{ topology_title }} - {% endif %} - {% endfor %} -
-
- -
- - {% for topology_title, topology_data in topologies.items %} - - {% endfor %} -
+ {% else %} + {{ topology_title }} + {% endif %} + {% endfor %} +
+
+ +
+ + {% for topology_title, topology_data in topologies.items %} + + {% endfor %}
- +
- - {% if topologies|length > 1 %} + {% endif %} -{% endif %} \ No newline at end of file +{% endif %} diff --git a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/topology_styles.html b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/topology_styles.html index af7fdbc..7cc8258 100644 --- a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/topology_styles.html +++ b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/topology_styles.html @@ -34,4 +34,4 @@ margin-bottom: 8px; font-weight: 700; } - \ No newline at end of file + diff --git a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/topology_visualization.html b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/topology_visualization.html index 0e00630..67e732e 100644 --- a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/topology_visualization.html +++ b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/inc/topology_visualization.html @@ -1,10 +1,8 @@ {% load i18n %} {# Load Cytoscape.js #} {% include './cytoscape_includes.html' %} - {# Load Topology Styles #} {% include './topology_styles.html' %} - \ No newline at end of file + diff --git a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/provider_segments_extension.html b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/provider_segments_extension.html index 8738fce..60df561 100644 --- a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/provider_segments_extension.html +++ b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/provider_segments_extension.html @@ -1,7 +1,7 @@ {% load i18n %}
-
-
{% trans "Segments" %}
- {% htmx_table 'plugins:cesnet_service_path_plugin:segment_list' provider_id=object.pk %} -
+
+
{% trans "Segments" %}
+ {% htmx_table 'plugins:cesnet_service_path_plugin:segment_list' provider_id=object.pk %} +
diff --git a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/segment.html b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/segment.html index 9cdb067..d66999e 100644 --- a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/segment.html +++ b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/segment.html @@ -8,18 +8,17 @@ {% load perms %} {% load custom_filters %} {% load render_table from django_tables2 %} - {% block extra_controls %} {# Add button to create Circuit from Segment data #} {% if perms.circuits.add_circuit %} - + {% trans "Generate Circuit" %} {% endif %} {% endblock extra_controls %} - {% block content %} -
@@ -34,9 +33,7 @@
Segment
Segment Type - - {{ object.get_segment_type_display }} - + {{ object.get_segment_type_display }} @@ -91,32 +88,32 @@
Segment
{% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/tags.html' %} {% include 'inc/panels/comments.html' %} - +
Path Information {% if object.has_path_data %} {% else %} +
{% endif %}
@@ -163,228 +160,227 @@
{% plugin_right_page object %}
- {% if has_contract_view_perm %} -
-
-
-
- Active Contract Information - {% if contract_info %} -
- - Detail View - - {% if has_contract_change_perm %} - - Edit - - {% endif %} - {% if has_contract_add_perm and contract_info.is_active %} - - New Version +
+
+
+
+ Active Contract Information + {% if contract_info %} +
+ + Detail View - {% endif %} -
- {% else %} -
- {% if has_contract_add_perm %} - - Add Contract Info - - {% endif %} -
- {% endif %} -
-
- {% if contract_info %} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {% if contract_info.non_recurring_charge %} + {% if has_contract_change_perm %} + + Edit + + {% endif %} + {% if has_contract_add_perm and contract_info.is_active %} + + New Version + + {% endif %} + + {% else %} +
+ {% if has_contract_add_perm %} + + Add Contract Info + + {% endif %} +
+ {% endif %} + +
+ {% if contract_info %} +
Contract Number - {{ contract_info.contract_number|placeholder }} -
Contract Type - - {{ contract_info.get_contract_type_display }} - -
Version - v{{ contract_info.version }} - {% if contract_info.is_active %} - Active - {% else %} - Superseded - {% endif %} -
Currency - - {{ contract_info.get_charge_currency_display }} - -
Recurring Charge - {{ contract_info.charge_currency }} {{ contract_info.recurring_charge|floatformat:2|spacecomma }} -
Recurring Period{{ contract_info.get_recurring_charge_period_display|placeholder }}
Number of Charges{{ contract_info.number_of_recurring_charges|placeholder }}
Total Recurring Cost - {{ contract_info.charge_currency }} {{ contract_info.total_recurring_cost|floatformat:2|spacecomma }} -
- - + + - {% endif %} - - - - - - - - - - - + + - - {% if contract_info.commitment_end_date %} + + - + + + + + - {% endif %} - {% if contract_info.notes %} - - + + - {% endif %} -
Non-Recurring Charge{{ contract_info.charge_currency }} {{ contract_info.non_recurring_charge|floatformat:2|spacecomma }}Contract Number + {{ contract_info.contract_number|placeholder }} +
Total Contract Value - {{ contract_info.charge_currency }} {{ contract_info.total_contract_value|floatformat:2|spacecomma }} -
Start Date{{ contract_info.start_date|placeholder }}
End Date - {% if contract_info.end_date %} - - {{ contract_info.end_date }} +
Contract Type + + {{ contract_info.get_contract_type_display }} - {% else %} - {{ ''|placeholder }} - {% endif %} -
Commitment End DateVersion - - {{ contract_info.commitment_end_date }} + v{{ contract_info.version }} + {% if contract_info.is_active %} + Active + {% else %} + Superseded + {% endif %} +
Currency + + {{ contract_info.get_charge_currency_display }}
Notes{{ contract_info.notes|linebreaks }}Recurring Charge + {{ contract_info.charge_currency }} {{ contract_info.recurring_charge|floatformat:2|spacecomma }} +
- {% else %} -
- - No contract information available for this segment. - {% if has_contract_add_perm %} - - Add contract information - - {% endif %} -
- {% endif %} + + Recurring Period + {{ contract_info.get_recurring_charge_period_display|placeholder }} + + + Number of Charges + {{ contract_info.number_of_recurring_charges|placeholder }} + + + Total Recurring Cost + + {{ contract_info.charge_currency }} {{ contract_info.total_recurring_cost|floatformat:2|spacecomma }} + + + {% if contract_info.non_recurring_charge %} + + Non-Recurring Charge + + {{ contract_info.charge_currency }} {{ contract_info.non_recurring_charge|floatformat:2|spacecomma }} + + + {% endif %} + + Total Contract Value + + {{ contract_info.charge_currency }} {{ contract_info.total_contract_value|floatformat:2|spacecomma }} + + + + Start Date + {{ contract_info.start_date|placeholder }} + + + End Date + + {% if contract_info.end_date %} + + {{ contract_info.end_date }} + + {% else %} + {{ ''|placeholder }} + {% endif %} + + + {% if contract_info.commitment_end_date %} + + Commitment End Date + + + {{ contract_info.commitment_end_date }} + + + + {% endif %} + {% if contract_info.notes %} + + Notes + {{ contract_info.notes|linebreaks }} + + {% endif %} + + {% else %} +
+ + No contract information available for this segment. + {% if has_contract_add_perm %} + + Add contract information + + {% endif %} +
+ {% endif %} +
-
-
-
-
Contract Version History
-
- {% if contract_chains %} - {% for chain in contract_chains %} - {% if chain.version_history %} -
- {{ chain.latest.contract_number|default:"Contract" }} - {{ chain.version_count }} version{{ chain.version_count|pluralize }} -
- - - - - - - - - - - {% for version in chain.version_history %} - - - - - - - {% endfor %} - -
VersionTypeStatusActions
- v{{ version.version }} - - - {{ version.get_contract_type_display }} - - - {% if version.is_active %} - Active - {% else %} - Superseded - {% endif %} - - {% if version.pk != selected_contract_id %} - - View - - {% else %} - Current - {% endif %} -
- {% endif %} - {% endfor %} - {% else %} -
- - No contract version history available. -
- {% endif %} +
+
+
Contract Version History
+
+ {% if contract_chains %} + {% for chain in contract_chains %} + {% if chain.version_history %} +
+ {{ chain.latest.contract_number|default:"Contract" }} + {{ chain.version_count }} version{{ chain.version_count|pluralize }} +
+ + + + + + + + + + + {% for version in chain.version_history %} + + + + + + + {% endfor %} + +
VersionTypeStatusActions
+ v{{ version.version }} + + {{ version.get_contract_type_display }} + + {% if version.is_active %} + Active + {% else %} + Superseded + {% endif %} + + {% if version.pk != selected_contract_id %} + + View + + {% else %} + Current + {% endif %} +
+ {% endif %} + {% endfor %} + {% else %} +
+ + No contract version history available. +
+ {% endif %} +
-
{% endif %} -
@@ -392,9 +388,7 @@
Technical Specifications - - {{ object.get_segment_type_display }} - + {{ object.get_segment_type_display }}
{% if object.segment_type == 'dark_fiber' %} @@ -483,10 +477,12 @@
{% endif %}
@@ -495,7 +491,8 @@
No dark fiber technical specifications configured. - + Add Dark Fiber Data
@@ -554,10 +551,12 @@
{% endif %}
@@ -566,7 +565,8 @@
No optical spectrum technical specifications configured. - + Add Optical Spectrum Data
@@ -625,10 +625,12 @@
{% endif %}
@@ -637,7 +639,8 @@
No ethernet service technical specifications configured. - + Add Ethernet Service Data
@@ -652,13 +655,9 @@
- {% include './inc/topology_visualization.html' %} - {% include './inc/topology_segment_card.html' %} - -
diff --git a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/segment_edit.html b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/segment_edit.html index cd4addf..b2c7b38 100644 --- a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/segment_edit.html +++ b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/segment_edit.html @@ -6,4 +6,4 @@ -{% endblock head %} \ No newline at end of file +{% endblock head %} diff --git a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/segment_list.html b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/segment_list.html index 2b4b87c..70a56c6 100644 --- a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/segment_list.html +++ b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/segment_list.html @@ -1,15 +1,13 @@ {% extends 'generic/object_list.html' %} {% load static %} {% load helpers %} - {% block title %}Segments{% endblock %} - {% block extra_controls %} {{ block.super }} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/segment_map.html b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/segment_map.html index 6642832..674d759 100644 --- a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/segment_map.html +++ b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/segment_map.html @@ -1,67 +1,72 @@ {% extends 'generic/object.html' %} {% load static %} {% load helpers %} - {% block title %}{{ object.name }} - Map View{% endblock %} - {% block content %} -
-
-
-
-
- - Map: {{ object.name }} -
- -
-
- {% if not object.has_path_data and not has_fallback_line %} - - {% else %} - -
- {{ object.provider }} | - {{ object.get_status_display }} | - {{ object.get_ownership_type_display }} | - {% if object.path_length_km %}{{ object.path_length_km }} km{% else %}Length unknown{% endif %} | - {{ object.site_a }} ↔ {{ object.site_b }} - {% if has_fallback_line %} -
- - No path data available - showing straight line between sites - - {% endif %} -
- - -
- - - {% include './inc/map_layer_dropdown.html' %} +
+
+
+
+
+ + Map: {{ object.name }} +
+ - - -
- - - {% include './inc/leaflet_includes.html' %} - - - - - - {% include './inc/map_layers_config.html' %} - - + + {% include './inc/map_layers_config.html' %} + - {% endif %} + + {% endif %} +
-
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/segment_path_clear_confirm.html b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/segment_path_clear_confirm.html index cb707dd..42b1371 100644 --- a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/segment_path_clear_confirm.html +++ b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/segment_path_clear_confirm.html @@ -1,88 +1,87 @@ {% extends 'generic/object_delete.html' %} {% load i18n %} - -{% block title %}{% trans "Clear Path Data" %}{% endblock %} - +{% block title %} + {% trans "Clear Path Data" %} +{% endblock %} {% block content %} -
-
-
- {% csrf_token %} -
-
- {% trans "Clear Path Data" %} -
-
-

- {% blocktrans with name=object.name %} +

+
+ + {% csrf_token %} +
+
+ {% trans "Clear Path Data" %} +
+
+

+ {% blocktrans with name=object.name %} Are you sure you want to clear the path data from segment "{{ name }}"? {% endblocktrans %} -

-

- - {% trans "This will permanently remove:" %} -

-
    -
  • {% trans "Path geometry data" %}
  • -
  • {% trans "Path length calculation" %}
  • -
  • {% trans "Source format information" %}
  • -
  • {% trans "Path notes" %}
  • -
-

- - {% trans "The segment itself and all other data will remain unchanged. You can upload new path data later if needed." %} - -

+

+

+ + {% trans "This will permanently remove:" %} +

+
    +
  • {% trans "Path geometry data" %}
  • +
  • {% trans "Path length calculation" %}
  • +
  • {% trans "Source format information" %}
  • +
  • {% trans "Path notes" %}
  • +
+

+ + {% trans "The segment itself and all other data will remain unchanged. You can upload new path data later if needed." %} + +

+
+
- +
+
+
+ {% trans "Current Path Information" %}
-
- -
-
-
-
- {% trans "Current Path Information" %} -
-
- - - - - - - - - - - - - - - - - - {% if object.path_notes %} +
+
{% trans "Path Length" %} - {% if object.path_length_km %} - {{ object.path_length_km }} km - {% else %} - {{ ''|default:"—" }} - {% endif %} -
{% trans "Source Format" %}{{ object.get_path_source_format_display|default:object.path_source_format|default:"—" }}
{% trans "Segments Count" %}{{ object.get_path_segment_count }}
{% trans "Total Points" %}{{ object.get_total_points }}
- - + + - {% endif %} -
{% trans "Path Notes" %}{{ object.path_notes|truncatewords:20 }}{% trans "Path Length" %} + {% if object.path_length_km %} + {{ object.path_length_km }} km + {% else %} + {{ ''|default:"—" }} + {% endif %} +
+ + {% trans "Source Format" %} + {{ object.get_path_source_format_display|default:object.path_source_format|default:"—" }} + + + {% trans "Segments Count" %} + {{ object.get_path_segment_count }} + + + {% trans "Total Points" %} + {{ object.get_total_points }} + + {% if object.path_notes %} + + {% trans "Path Notes" %} + {{ object.path_notes|truncatewords:20 }} + + {% endif %} + +
-
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/segmentcircuitmapping.html b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/segmentcircuitmapping.html index b9ac272..d3d35fe 100644 --- a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/segmentcircuitmapping.html +++ b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/segmentcircuitmapping.html @@ -1,33 +1,28 @@ {% extends 'generic/object.html' %} {% load helpers %} - {% block content %} -
-
-
-
Segment Circuit Mapping
-
- - - - - - - - - -
Segment - {{ object.segment|linkify }} -
Circuit - {{ object.circuit|linkify }} -
+
+
+
+
Segment Circuit Mapping
+
+ + + + + + + + + +
Segment{{ object.segment|linkify }}
Circuit{{ object.circuit|linkify }}
+
+
+ {% include 'inc/panels/custom_fields.html' %} +
+
+ {% include 'inc/panels/tags.html' %} + {% include 'inc/panels/comments.html' %}
-
- {% include 'inc/panels/custom_fields.html' %} -
-
- {% include 'inc/panels/tags.html' %} - {% include 'inc/panels/comments.html' %}
-
{% endblock content %} diff --git a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/segments_map.html b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/segments_map.html index 360bc1d..f071ace 100644 --- a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/segments_map.html +++ b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/segments_map.html @@ -6,162 +6,167 @@ {% load plugins %} {% load render_table from django_tables2 %} {% load i18n %} - {% block title %}Segments Map View{% endblock %} - {% block content %} - {# Object list tab #} -
- - {# Applied filters #} - {% if filter_form %} - {% applied_filters model filter_form request.GET %} - {% endif %} - - {# Object table controls #} - -
- {% csrf_token %} - {# "Select all" form #} - {% if table.paginator.num_pages > 1 %} -
-
-
-
- - -
-
- {% if 'bulk_edit' in actions %} - {% bulk_edit_button model query_params=request.GET %} - {% endif %} - {% if 'bulk_delete' in actions %} - {% bulk_delete_button model query_params=request.GET %} - {% endif %} -
-
-
-
+ {# Object list tab #} +
+ {# Applied filters #} + {% if filter_form %} + {% applied_filters model filter_form request.GET %} {% endif %} - -
- {% csrf_token %} - - - {# Warn of any missing prerequisite objects #} - {% if prerequisite_model %} - {% include 'inc/missing_prerequisites.html' %} - {% endif %} - - {# Objects table #} -
- -
-
-
-
- - Segments Map - {{ segments_count }} segments -
-
- - Table View - - - - - - -
- - + {# Object table controls #} + + {% csrf_token %} + {# "Select all" form #} + {% if table.paginator.num_pages > 1 %} +
+
+
+
+ + +
+
+ {% if 'bulk_edit' in actions %} + {% bulk_edit_button model query_params=request.GET %} + {% endif %} + {% if 'bulk_delete' in actions %} + {% bulk_delete_button model query_params=request.GET %} + {% endif %} +
+
- - - - - {% include './inc/map_layer_dropdown.html' %}
-
-
- {% if map_warning %} - + {% endif %} +
+ {% csrf_token %} + + {# Warn of any missing prerequisite objects #} + {% if prerequisite_model %} + {% include 'inc/missing_prerequisites.html' %} {% endif %} - - {% if segments_count == 0 %} - - {% else %} - -
- - - {% include './inc/leaflet_includes.html' %} - - - {% include './inc/map_layers_styles.html' %} - - - +
+
+ {# /Objects table #} + {# Form buttons #} +
+ {% block bulk_buttons %} +
+ {% if 'bulk_edit' in actions %} + {% bulk_edit_button model query_params=request.GET %} + {% endif %} + {% if 'bulk_delete' in actions %} + {% bulk_delete_button model query_params=request.GET %} + {% endif %} +
+ {% endblock %} +
+ {# /Form buttons #}
-
-
-
-{# /Objects table #} - - {# Form buttons #} -
- {% block bulk_buttons %} -
- {% if 'bulk_edit' in actions %} - {% bulk_edit_button model query_params=request.GET %} - {% endif %} - {% if 'bulk_delete' in actions %} - {% bulk_delete_button model query_params=request.GET %} - {% endif %} -
- {% endblock %} -
- {# /Form buttons #} - -
- +
{# /Object list tab #} - {# Filters tab #} {% if filter_form %} -
- {% include 'inc/filter_list.html' %} -
+
{% include 'inc/filter_list.html' %}
{% endif %} {# /Filters tab #} - -{% endblock content %} \ No newline at end of file +{% endblock content %} diff --git a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/servicepath.html b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/servicepath.html index 012dd39..a958059 100644 --- a/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/servicepath.html +++ b/cesnet_service_path_plugin/templates/cesnet_service_path_plugin/servicepath.html @@ -8,7 +8,6 @@ {% load perms %} {% load render_table from django_tables2 %} {% load static %} - {% block content %}
@@ -42,32 +41,30 @@
Service Path
{% plugin_right_page object %}
- - - {% include './inc/topology_visualization.html' %} - -
-
-
-
- {% trans "Topology of" %} - {{ object.name }} - {% trans "Service Path" %} -
-
-
-
+ + {% include './inc/topology_visualization.html' %} +
+
+
+
+ {% trans "Topology of" %} + {{ object.name }} + {% trans "Service Path" %} +
+
+
- - - +
@@ -89,6 +86,4 @@
{% plugin_full_width_page object %}
- - -{% endblock content %} \ No newline at end of file +{% endblock content %} diff --git a/cesnet_service_path_plugin/views/contract_info.py b/cesnet_service_path_plugin/views/contract_info.py index 9a7cd8d..aa1a57e 100644 --- a/cesnet_service_path_plugin/views/contract_info.py +++ b/cesnet_service_path_plugin/views/contract_info.py @@ -14,8 +14,9 @@ class CloneActiveContractOnly(BaseCloneObject): Custom clone action that only allows cloning of active (non-superseded) contract versions. This prevents database integrity errors from attempting to clone older versions. """ - label = 'New Version' - template_name = 'buttons/clone_custom.html' + + label = "New Version" + template_name = "buttons/clone_custom.html" @classmethod def get_url(cls, obj): diff --git a/cesnet_service_path_plugin/views/dark_fiber_data.py b/cesnet_service_path_plugin/views/dark_fiber_data.py index 391c95b..77fb4db 100644 --- a/cesnet_service_path_plugin/views/dark_fiber_data.py +++ b/cesnet_service_path_plugin/views/dark_fiber_data.py @@ -15,6 +15,7 @@ class DarkFiberSegmentDataEditView(generic.ObjectEditView): The segment is expected to be passed via URL parameter (segment_id) for add, or derived from the instance for edit operations. """ + queryset = DarkFiberSegmentData.objects.all() form = DarkFiberSegmentDataForm @@ -24,7 +25,7 @@ def alter_object(self, obj, request, url_args, url_kwargs): """ if not obj.segment_id: # Get segment_id from URL parameter for new objects - segment_id = request.GET.get('segment') + segment_id = request.GET.get("segment") if segment_id: try: obj.segment = Segment.objects.get(pk=segment_id) @@ -55,6 +56,7 @@ class DarkFiberSegmentDataDeleteView(generic.ObjectDeleteView): """ View for deleting dark fiber technical data. """ + queryset = DarkFiberSegmentData.objects.all() def get(self, request, *args, **kwargs): @@ -86,7 +88,7 @@ def get_return_url(self, request, obj=None): return return_url # Use the stored segment URL if available - if hasattr(self, '_segment_url') and self._segment_url: + if hasattr(self, "_segment_url") and self._segment_url: return self._segment_url # Fallback to default behavior diff --git a/cesnet_service_path_plugin/views/ethernet_service_data.py b/cesnet_service_path_plugin/views/ethernet_service_data.py index be9fcbe..f8d599d 100644 --- a/cesnet_service_path_plugin/views/ethernet_service_data.py +++ b/cesnet_service_path_plugin/views/ethernet_service_data.py @@ -15,6 +15,7 @@ class EthernetServiceSegmentDataEditView(generic.ObjectEditView): The segment is expected to be passed via URL parameter (segment_id) for add, or derived from the instance for edit operations. """ + queryset = EthernetServiceSegmentData.objects.all() form = EthernetServiceSegmentDataForm @@ -24,7 +25,7 @@ def alter_object(self, obj, request, url_args, url_kwargs): """ if not obj.segment_id: # Get segment_id from URL parameter for new objects - segment_id = request.GET.get('segment') + segment_id = request.GET.get("segment") if segment_id: try: obj.segment = Segment.objects.get(pk=segment_id) @@ -55,6 +56,7 @@ class EthernetServiceSegmentDataDeleteView(generic.ObjectDeleteView): """ View for deleting ethernet service technical data. """ + queryset = EthernetServiceSegmentData.objects.all() def get(self, request, *args, **kwargs): @@ -86,7 +88,7 @@ def get_return_url(self, request, obj=None): return return_url # Use the stored segment URL if available - if hasattr(self, '_segment_url') and self._segment_url: + if hasattr(self, "_segment_url") and self._segment_url: return self._segment_url # Fallback to default behavior diff --git a/cesnet_service_path_plugin/views/optical_spectrum_data.py b/cesnet_service_path_plugin/views/optical_spectrum_data.py index 5034fa9..3320903 100644 --- a/cesnet_service_path_plugin/views/optical_spectrum_data.py +++ b/cesnet_service_path_plugin/views/optical_spectrum_data.py @@ -15,6 +15,7 @@ class OpticalSpectrumSegmentDataEditView(generic.ObjectEditView): The segment is expected to be passed via URL parameter (segment_id) for add, or derived from the instance for edit operations. """ + queryset = OpticalSpectrumSegmentData.objects.all() form = OpticalSpectrumSegmentDataForm @@ -24,7 +25,7 @@ def alter_object(self, obj, request, url_args, url_kwargs): """ if not obj.segment_id: # Get segment_id from URL parameter for new objects - segment_id = request.GET.get('segment') + segment_id = request.GET.get("segment") if segment_id: try: obj.segment = Segment.objects.get(pk=segment_id) @@ -55,6 +56,7 @@ class OpticalSpectrumSegmentDataDeleteView(generic.ObjectDeleteView): """ View for deleting optical spectrum technical data. """ + queryset = OpticalSpectrumSegmentData.objects.all() def get(self, request, *args, **kwargs): @@ -86,7 +88,7 @@ def get_return_url(self, request, obj=None): return return_url # Use the stored segment URL if available - if hasattr(self, '_segment_url') and self._segment_url: + if hasattr(self, "_segment_url") and self._segment_url: return self._segment_url # Fallback to default behavior diff --git a/cesnet_service_path_plugin/views/segment.py b/cesnet_service_path_plugin/views/segment.py index 8a4306f..b972194 100644 --- a/cesnet_service_path_plugin/views/segment.py +++ b/cesnet_service_path_plugin/views/segment.py @@ -83,7 +83,7 @@ def generate_circuit_creation_url(segment): @register_model_view(Segment) class SegmentView(generic.ObjectView): - queryset = Segment.objects.prefetch_related('contracts') + queryset = Segment.objects.prefetch_related("contracts") def get_extra_context(self, request, instance): circuits = instance.circuits.all() @@ -108,7 +108,7 @@ def get_extra_context(self, request, instance): if has_contract_view_perm: # Fetch all contracts related to this segment - all_contracts = instance.contracts.all().order_by('contract_number', '-id') + all_contracts = instance.contracts.all().order_by("contract_number", "-id") logger.debug(f"=== Contract Info Debug for Segment {instance.pk} ({instance.name}) ===") logger.debug(f"Total contracts found: {all_contracts.count()}") @@ -140,11 +140,11 @@ def get_extra_context(self, request, instance): version_history = first_version.get_version_history() contract_chains_dict[first_version.pk] = { - 'first': first_version, - 'latest': latest, - 'version_count': latest.version, - 'is_active': latest.is_active, - 'version_history': version_history, + "first": first_version, + "latest": latest, + "version_count": latest.version, + "is_active": latest.is_active, + "version_history": version_history, } logger.debug(f" Contract Chain for '{first_version.contract_number}':") @@ -160,16 +160,20 @@ def get_extra_context(self, request, instance): logger.debug("=== End Contract Info Debug ===") # Check if a specific contract version is requested via URL parameter - contract_version_id = request.GET.get('contract_version') + contract_version_id = request.GET.get("contract_version") if contract_version_id: try: # Try to get the requested contract version requested_contract = all_contracts.get(pk=int(contract_version_id)) contract_info = requested_contract selected_contract_id = requested_contract.pk - logger.debug(f"Displaying requested contract version: {requested_contract.pk} (v{requested_contract.version})") + logger.debug( + f"Displaying requested contract version: {requested_contract.pk} (v{requested_contract.version})" + ) except (ValueError, all_contracts.model.DoesNotExist): - logger.warning(f"Requested contract version {contract_version_id} not found or not related to this segment") + logger.warning( + f"Requested contract version {contract_version_id} not found or not related to this segment" + ) # Fall back to default behavior if active_contracts.exists(): contract_info = active_contracts.first() From e336810fcbec8e27f779fac0fc3c772fab8b42c3 Mon Sep 17 00:00:00 2001 From: Jan Krupa <> Date: Mon, 9 Mar 2026 10:40:17 +0000 Subject: [PATCH 10/11] fix: raise min_version to 4.5.4 (requires strawberry-graphql-django >= 0.79.0) --- cesnet_service_path_plugin/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cesnet_service_path_plugin/__init__.py b/cesnet_service_path_plugin/__init__.py index eb30013..91a326b 100644 --- a/cesnet_service_path_plugin/__init__.py +++ b/cesnet_service_path_plugin/__init__.py @@ -21,7 +21,7 @@ class CesnetServicePathPluginConfig(PluginConfig): base_url = "cesnet-service-path-plugin" author = __email__ graphql_schema = "graphql.schema" - min_version = "4.5.0" + min_version = "4.5.4" max_version = "4.5.99" From 6ef37e2fef1f848ee0823f743644771e75fa833a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:18:11 +0000 Subject: [PATCH 11/11] Add renovate.json --- renovate.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 renovate.json diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..5db72dd --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended" + ] +}