From aec460306bf0b09515473b79039ec0e137a68ba1 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 14 Jul 2024 16:48:54 +0930 Subject: [PATCH 001/123] fix(config_management): don't exclude parent from field, only self !42 #74 --- app/config_management/forms/group/group.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/config_management/forms/group/group.py b/app/config_management/forms/group/group.py index c45b36a6..8199d4b3 100644 --- a/app/config_management/forms/group/group.py +++ b/app/config_management/forms/group/group.py @@ -26,9 +26,9 @@ class ConfigGroupForm(CommonModelForm): super().__init__(*args, **kwargs) - if 'parent' in kwargs['initial']: + if hasattr(kwargs['instance'], 'id'): self.fields['parent'].queryset = self.fields['parent'].queryset.filter( ).exclude( - id=int(kwargs['initial']['parent']) + id=int(kwargs['instance'].id) ) From fbe7e63cc9b0a95358bd9fa98dfd06067f40437b Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 14 Jul 2024 16:57:25 +0930 Subject: [PATCH 002/123] fix(access): Correct team form fields fixes missing name for team !42 #74 --- app/access/forms/team.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/access/forms/team.py b/app/access/forms/team.py index 180a4c37..0ec8e7fc 100644 --- a/app/access/forms/team.py +++ b/app/access/forms/team.py @@ -28,7 +28,8 @@ class TeamFormAdd(CommonModelForm): class Meta: model = Team fields = [ - 'name', + 'team_name', + 'model_notes', ] @@ -38,7 +39,7 @@ class TeamForm(CommonModelForm): class Meta: model = Team fields = [ - 'name', + 'team_name', 'permissions', 'model_notes', ] From af3e77076048438b61f327e99ced6372aab51f6b Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 14 Jul 2024 17:09:18 +0930 Subject: [PATCH 003/123] fix(settings): correct the permission to view manufacturers !42 #74 --- app/settings/views/manufacturer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/settings/views/manufacturer.py b/app/settings/views/manufacturer.py index bc2d9f5b..6ef2c189 100644 --- a/app/settings/views/manufacturer.py +++ b/app/settings/views/manufacturer.py @@ -22,7 +22,7 @@ class Index(IndexView): paginate_by = 10 permission_required = [ - 'core.view_devicetype' + 'core.view_manufacturer' ] template_name = 'settings/manufacturers.html.j2' From 6776612b66c1519c8f20941aeb9cf329a201e81c Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 14 Jul 2024 17:11:49 +0930 Subject: [PATCH 004/123] chore: move docker-compose to deploy directory intent of dir is that is the location for all avail deploy methods !42 #74 --- docker-compose.yaml => deploy/docker-compose.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) rename docker-compose.yaml => deploy/docker-compose.yaml (53%) diff --git a/docker-compose.yaml b/deploy/docker-compose.yaml similarity index 53% rename from docker-compose.yaml rename to deploy/docker-compose.yaml index 4c049f16..6aa7af8c 100644 --- a/docker-compose.yaml +++ b/deploy/docker-compose.yaml @@ -1,16 +1,16 @@ -# docker exec -ti django-itsm python manage.py migrate -# docker exec -ti django-itsm python manage.py createsuperuser +# docker exec -ti centurion-erp python manage.py migrate +# docker exec -ti centurion-erp python manage.py createsuperuser version: "3.2" services: - django-itsm: - image: django-itsm:dev + centurion-erp: + image: centurion-erp:dev build: - context: . + context: ../. dockerfile: dockerfile - container_name : django-itsm - hostname: django-itsm + container_name: centurion-erp + hostname: centurion-erp ports: - "8002:8000" volumes: From 9acc4fdfcb4b595e3a5f6def31ed4977ea871d54 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 14 Jul 2024 17:16:16 +0930 Subject: [PATCH 005/123] docs(gitlab): update MR template !42 #74 --- .gitlab/merge_request_templates/default.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab/merge_request_templates/default.md b/.gitlab/merge_request_templates/default.md index 27d3c9aa..5fa898eb 100644 --- a/.gitlab/merge_request_templates/default.md +++ b/.gitlab/merge_request_templates/default.md @@ -20,7 +20,7 @@ -- [ ] ~"breaking-change" Any Breaking change(s) +- [ ] Contains ~"breaking-change" Any Breaking change(s)? _Breaking Change must also be notated in the commit that introduces it and in [Conventional Commit Format](https://www.conventionalcommits.org/en/v1.0.0/)._ @@ -30,6 +30,6 @@ - [ ] Milestone assigned -- [ ] [Unit Test(s) Written](https://nofusscomputing.com/projects/django-template/development/testing/) +- [ ] [Unit Test(s) Written](https://nofusscomputing.com/projects/centurion_erp/development/testing/) _ensure test coverage delta is not less than zero_ From f1201e89338880830339f21ec1fe73a05d50027b Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 15 Jul 2024 13:38:24 +0930 Subject: [PATCH 006/123] feat(access): Add multi-tennant manager manager filters results to that of data from the organizations the users is part of. !42 #124 --- app/access/mixin.py | 4 +- app/access/models.py | 93 ++++++++++++++++++- app/core/forms/common.py | 83 +---------------- app/core/tests/abstract/models.py | 62 +++++++++++++ app/core/views/common.py | 16 ---- .../development/api/models/index.md | 6 +- .../development/api/models/tenancy_object.md | 25 +++++ .../centurion_erp/development/forms.md | 6 +- .../centurion_erp/development/index.md | 4 +- .../centurion_erp/development/models.md | 41 +++----- mkdocs.yml | 2 + 11 files changed, 208 insertions(+), 134 deletions(-) create mode 100644 app/core/tests/abstract/models.py create mode 100644 docs/projects/centurion_erp/development/api/models/tenancy_object.md diff --git a/app/access/mixin.py b/app/access/mixin.py index f5680275..fd2dc4da 100644 --- a/app/access/mixin.py +++ b/app/access/mixin.py @@ -80,7 +80,9 @@ class OrganizationMixin(): id = int(self.request.POST.get(field)) - + except: + + pass return id diff --git a/app/access/models.py b/app/access/models.py index 82db1a06..bc3e0cda 100644 --- a/app/access/models.py +++ b/app/access/models.py @@ -63,13 +63,103 @@ class Organization(SaveHistory): return self -class TenancyObject(models.Model): + +class TenancyManager(models.Manager): + """Multi-Tennant Object Manager + + This manager specifically caters for the multi-tenancy features of Centurion ERP. + """ + + + def get_queryset(self): + """ Fetch the data + + This function filters the data fetched from the database to that which is from the organizations + the user is a part of. + + !!! danger "Requirement" + This method may be overridden however must still be called from the overriding function. i.e. `super().get_queryset()` + + ## Workflow + + This functions workflow is as follows: + + - Fetch the user from the request + + - Check if the user is authenticated + + - Iterate over the users teams + + - Store unique organizations from users teams + + - return results + + Returns: + (queryset): **super user**: return unfiltered data. + (queryset): **not super user**: return data from the stored unique organizations. + """ + + request = get_request() + + user_organizations: list(str()) = [] + + + if request: + + user = request.user._wrapped if hasattr(request.user,'_wrapped') else request.user + + + if user.is_authenticated: + + for team_user in TeamUsers.objects.filter(user=user): + + + if team_user.team.organization.name not in user_organizations: + + + if not user_organizations: + + self.user_organizations = [] + + user_organizations += [ team_user.team.organization.id ] + + + if len(user_organizations) > 0 and not user.is_superuser: + + return super().get_queryset().filter( + models.Q(organization__in=user_organizations) + | + models.Q(is_global = True) + ) + + return super().get_queryset() + + + +class TenancyObject(SaveHistory): + """ Tenancy Model Abstrct class. + + This class is for inclusion wihtin **every** model within Centurion ERP. + Provides the required fields, functions and methods for multi tennant objects. + Unless otherwise stated, **no** object within this class may be overridden. + + Raises: + ValidationError: User failed to supply organization + """ + + objects = TenancyManager() + """ Multi-Tenanant Objects """ class Meta: abstract = True def validatate_organization_exists(self): + """Ensure that the user did provide an organization + + Raises: + ValidationError: User failed to supply organization. + """ if not self: raise ValidationError('You must provide an organization') @@ -99,6 +189,7 @@ class TenancyObject(models.Model): return self.organization + class Team(Group, TenancyObject, SaveHistory): class Meta: # proxy = True diff --git a/app/core/forms/common.py b/app/core/forms/common.py index 211e2d87..6c32b2d2 100644 --- a/app/core/forms/common.py +++ b/app/core/forms/common.py @@ -1,9 +1,5 @@ from django import forms -from django.db.models import Q - -from access.models import Organization, TeamUsers - class CommonModelForm(forms.ModelForm): @@ -12,14 +8,6 @@ class CommonModelForm(forms.ModelForm): This class exists so that common functions can be conducted against forms as they are loaded. """ - organization_field: str = 'organization' - """ Organization Field - - Name of the field that contains Organizations. - - This field will be filtered to those that the user is part of. - """ - def __init__(self, *args, **kwargs): """Form initialization. @@ -28,73 +16,8 @@ class CommonModelForm(forms.ModelForm): contained within this method. - ## Tenancy Objects - - Fields that contain an attribute called `organization` will have the objects filtered to - the organizations the user is part of. If the object has `is_global=True`, that object will not be - filtered out. + !!! danger "Requirement" + This method may be overridden however must still be called from the overriding function. i.e. `super().__init__(*args, **kwargs)` """ - user = kwargs.pop('user', None) - - user_organizations: list([str]) = [] - user_organizations_id: list(int()) = [] - - for team_user in TeamUsers.objects.filter(user=user): - - if team_user.team.organization.name not in user_organizations: - - if not user_organizations: - - self.user_organizations = [] - - user_organizations += [ team_user.team.organization.name ] - user_organizations_id += [ team_user.team.organization.id ] - - new_kwargs: dict = {} - - for key, value in kwargs.items(): - - if key != 'user': - - new_kwargs.update({key: value}) - - super().__init__(*args, **new_kwargs) - - - if len(user_organizations_id) > 0: - - for field_name in self.fields: - - field = self.fields[field_name] - - if hasattr(field, 'queryset'): - - if hasattr(field.queryset.model, 'organization'): - - if hasattr(field.queryset.model, 'is_global'): - - self.fields[field_name].queryset = field.queryset.filter( - Q(organization__in=user_organizations_id) - | - Q(is_global = True) - ) - - else: - - self.fields[field_name].queryset = field.queryset.filter( - Q(organization__in=user_organizations_id) - ) - - - if self.Meta.fields: - - if self.organization_field in self.Meta.fields: - - self.fields[self.organization_field].queryset = self.fields[self.organization_field].queryset.filter( - Q(name__in=user_organizations) - | - Q(manager=user) - ) - - + super().__init__(*args, **kwargs) diff --git a/app/core/tests/abstract/models.py b/app/core/tests/abstract/models.py new file mode 100644 index 00000000..53eb8949 --- /dev/null +++ b/app/core/tests/abstract/models.py @@ -0,0 +1,62 @@ +import pytest +import unittest + +from django.test import TestCase + + + +class Models: + """ Test cases for Model Abstract Classes """ + + + + @pytest.mark.skip(reason="write test") + def test_model_class_tenancy_manager_function_get_queryset(self): + """ Function Check + + function `get_queryset()` must exist + """ + + pass + + + @pytest.mark.skip(reason="write test") + def test_model_class_tenancy_manager_results_get_queryset(self): + """ Function Results Check + + function `get_queryset()` must not return data from any organization the user is + not part of. + """ + + pass + + + @pytest.mark.skip(reason="write test") + def test_model_class_tenancy_manager_results_get_queryset_super_user(self): + """ Function Results Check + + function `get_queryset()` must return un-filtered data for super-user. + """ + + pass + + + @pytest.mark.skip(reason="write test") + def test_model_class_tenancy_object_attribute_objects(self): + """ Attribute Check + + attribute `objects` must be set to `access.models.TenancyManager()` + """ + + pass + + + @pytest.mark.skip(reason="write test") + def test_model_class_inheritence_tenancy_object_save_history(self): + """ Class inheritence Check + + Class inherits from `core.mixin.history_save.SaveHistory` + """ + + pass + diff --git a/app/core/views/common.py b/app/core/views/common.py index a63e5e0a..635524be 100644 --- a/app/core/views/common.py +++ b/app/core/views/common.py @@ -15,22 +15,6 @@ class View(OrganizationPermission): template_name:str = 'form.html.j2' - def get_form_kwargs(self) -> dict: - """ Fetch kwargs for form - - Returns: - dict: kwargs used in fetching form - """ - - kwargs = super().get_form_kwargs() - - if self.form_class: - - kwargs.update({'user': self.request.user}) - - return kwargs - - class AddView(View, generic.CreateView): diff --git a/docs/projects/centurion_erp/development/api/models/index.md b/docs/projects/centurion_erp/development/api/models/index.md index 2d2cdf0a..d744dc32 100644 --- a/docs/projects/centurion_erp/development/api/models/index.md +++ b/docs/projects/centurion_erp/development/api/models/index.md @@ -1,6 +1,6 @@ --- -title: django ITSM Models -description: No Fuss Computings Centurion ERP device model +title: Centurion ERP Models +description: No Fuss Computings Centurion ERP Models development documentation. date: 2024-06-16 template: project.html about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/centurion_erp @@ -18,3 +18,5 @@ Models within Centurion ERP: - [History Save](./core_history_save.md) - [Organization Permission Checking](./access_organization_permission_checking.md) + +- [Tenancy Object](./tenancy_object.md) diff --git a/docs/projects/centurion_erp/development/api/models/tenancy_object.md b/docs/projects/centurion_erp/development/api/models/tenancy_object.md new file mode 100644 index 00000000..0258c41b --- /dev/null +++ b/docs/projects/centurion_erp/development/api/models/tenancy_object.md @@ -0,0 +1,25 @@ +--- +title: Tenancy Object +description: No Fuss Computings Centurion ERP tenancy object API Documentation +date: 2024-07-15 +template: project.html +about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/centurion_erp +--- + +This page contains the Tenancy Object API Documentation. + + +## Tenancy Object Model Abstract class + +::: app.access.models.TenancyObject + options: + inherited_members: true + heading_level: 3 + + +## Tenancy Object Manager + +::: app.access.models.TenancyManager + options: + inherited_members: true + heading_level: 3 diff --git a/docs/projects/centurion_erp/development/forms.md b/docs/projects/centurion_erp/development/forms.md index 651fa017..e305f09d 100644 --- a/docs/projects/centurion_erp/development/forms.md +++ b/docs/projects/centurion_erp/development/forms.md @@ -15,7 +15,7 @@ All forms must meet the following requirements: - is defined as a class -- inherits from `core.forms.common.CommonModelForm` _[docs](./api/form.md)_ +- inherits from [`core.forms.common.CommonModelForm`](./api/form.md) - contains a `Meta` sub-class with following parameters: @@ -23,6 +23,6 @@ All forms must meet the following requirements: - `model` -- Any additional filtering is done as part of an `__init__` method that also calls the super-class `__inti__` first +- Any additional filtering is done as part of an `__init__` method that also calls the super-class [`__init__`](./api/form.md) first - - Any filtering of a field `queryset` is to filter the existing `queryset` not redefine it. i.e. `field[].queryset.filter()` + - Any filtering of a fields `queryset` is to filter the existing `queryset` not redefine it. i.e. `field[].queryset = field[].queryset.filter()` diff --git a/docs/projects/centurion_erp/development/index.md b/docs/projects/centurion_erp/development/index.md index 7aa6dbcf..d2c46638 100644 --- a/docs/projects/centurion_erp/development/index.md +++ b/docs/projects/centurion_erp/development/index.md @@ -8,8 +8,10 @@ about: https://gitlab.com/nofusscomputing/infrastructure/configuration-managemen This section of the documentation contains different items related to the development of this application. The target audience is anyone whom wishes to develop any part of the application. +Centurion ERP is a Django Application. We have added a lot of little tid bits that aid in the development process. i.e. abstract classes, tests etc. This allows for decreased development times as items that are common are what could easily be considered templated with the only additional requirement is to add that objests differences. -## Handy links + +## Areas of the code - [Application API Documentation](./api/index.md) diff --git a/docs/projects/centurion_erp/development/models.md b/docs/projects/centurion_erp/development/models.md index 24a23036..63d64c97 100644 --- a/docs/projects/centurion_erp/development/models.md +++ b/docs/projects/centurion_erp/development/models.md @@ -8,12 +8,20 @@ about: https://gitlab.com/nofusscomputing/infrastructure/configuration-managemen Models within Centurion ERP are how the data is structured within the database. This page contains documentation pertinent to the development of the models for use with Centurion ERP. +All models within Centurion ERP are what we call a "Tenancy Object." A tenancy object is a model that takes advantage of the multi-tenancy features of Centurion ERP. + ## Requirements All models must meet the following requirements: -- inherits from `app.access.models.TenancyObject` and `django.db.models.Model` +- inherits from [`app.access.models.TenancyObject`](./api/models/tenancy_object.md) + + !!! tip + There is no requirement to include class [`app.core.mixin.history_save.SaveHistory`](./api/models/core_history_save.md) for inheritence as this class is already included within class [`app.access.models.TenancyObject`](./api/models/tenancy_object.md). + + !!! note + If there is a specific use case for the object not to be a tenancy object, this will need to be discussed with a maintainer. - class has `__str__` method defined to return that is used to return a default value if no field is specified. @@ -23,41 +31,14 @@ All models must meet the following requirements: - `help_text` +- No `queryset` is to return data that the user has not got access to. _see [queryset()](./api/models/tenancy_object.md#tenancy-object-manager)_ + ## Docs to clean up !!! note The below documentation is still to be developed. As such what is written below may be incorrect. - -## Model Setup - -Any item you wish to be multi-tenant, ensure within your model you include the tenancy model abstract class. The class includes a field called `organization` which links directly to the organization model and is used by the tenancy permission check. - -``` python title="/models.py" - -from access.models import TenancyObject - -class YourObject(TenancyObject): - ... - -``` - - -### Add history to model - -The tracking of changes can be added to a model by including the `SaveHistory` mixin from `core.mixin.history_save` to the model. - -``` python - -from core.mixin.history_save import SaveHistory - -class MyModel(SaveHistory): - - ..... - -``` - for items that have a parent item, modification will need to be made to the mixin by adding the relevant check and setting the relevant keys. ``` python diff --git a/mkdocs.yml b/mkdocs.yml index 51b82451..0d033db8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -78,6 +78,8 @@ nav: - projects/centurion_erp/development/api/models/access_organization_permission_checking.md + - projects/centurion_erp/development/api/models/tenancy_object.md + - projects/centurion_erp/development/api/common_views.md - Serializers: From 4ee62aa3998f34a31736b7b5201aa5acfa971998 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 15 Jul 2024 23:00:36 +0930 Subject: [PATCH 007/123] test: refactor to single abstract model for inclusion. !42 #15 --- app/access/tests/unit/team/test_team.py | 12 +++- .../team/test_team_access_tenancy_object.py | 17 ----- app/app/tests/abstract/models.py | 22 ++++++ .../unit/config_groups/test_config_groups.py | 7 +- ...est_config_groups_access_tenancy_object.py | 18 ----- .../test_config_groups_software.py | 7 +- ...g_groups_software_access_tenancy_object.py | 18 ----- .../unit/manufacturer/test_manufacturer.py | 17 +++++ app/core/tests/unit/test_notes/test_notes.py | 32 ++++++--- app/itam/tests/unit/device/test_device.py | 10 ++- .../test_device_access_tenancy_object.py | 18 ----- .../unit/device_model/test_device_model.py | 60 ++++++++-------- ...test_device_model_access_tenancy_object.py | 18 ----- .../test_device_operating_system.py | 7 +- ..._operating_system_access_tenancy_object.py | 18 ----- .../device_software/test_device_software.py | 8 ++- ...t_device_software_access_tenancy_object.py | 18 ----- .../unit/device_type/test_device_type.py | 17 +++++ .../test_device_type_access_tenancy_object.py | 18 ----- .../operating_system/test_operating_system.py | 68 +++++++++++-------- ..._operating_system_access_tenancy_object.py | 18 ----- .../test_operating_system_version.py | 7 +- ...ng_system_version_access_tenancy_object.py | 18 ----- app/itam/tests/unit/software/test_software.py | 53 ++++++++------- .../test_software_access_tenancy_object.py | 18 ----- .../test_software_category.py | 18 +++++ ...software_category_access_tenancy_object.py | 18 ----- 27 files changed, 238 insertions(+), 322 deletions(-) delete mode 100644 app/access/tests/unit/team/test_team_access_tenancy_object.py delete mode 100644 app/config_management/tests/unit/config_groups/test_config_groups_access_tenancy_object.py delete mode 100644 app/config_management/tests/unit/config_groups_software/test_config_groups_software_access_tenancy_object.py create mode 100644 app/core/tests/unit/manufacturer/test_manufacturer.py delete mode 100644 app/itam/tests/unit/device/test_device_access_tenancy_object.py delete mode 100644 app/itam/tests/unit/device_model/test_device_model_access_tenancy_object.py delete mode 100644 app/itam/tests/unit/device_operating_system/test_device_operating_system_access_tenancy_object.py delete mode 100644 app/itam/tests/unit/device_software/test_device_software_access_tenancy_object.py create mode 100644 app/itam/tests/unit/device_type/test_device_type.py delete mode 100644 app/itam/tests/unit/device_type/test_device_type_access_tenancy_object.py delete mode 100644 app/itam/tests/unit/operating_system/test_operating_system_access_tenancy_object.py delete mode 100644 app/itam/tests/unit/operating_system_version/test_operating_system_version_access_tenancy_object.py delete mode 100644 app/itam/tests/unit/software/test_software_access_tenancy_object.py create mode 100644 app/itam/tests/unit/software_category/test_software_category.py delete mode 100644 app/itam/tests/unit/software_category/test_software_category_access_tenancy_object.py diff --git a/app/access/tests/unit/team/test_team.py b/app/access/tests/unit/team/test_team.py index 8d270a20..052ea6d8 100644 --- a/app/access/tests/unit/team/test_team.py +++ b/app/access/tests/unit/team/test_team.py @@ -5,9 +5,14 @@ from django.test import TestCase, Client from access.models import Organization, Team, TeamUsers, Permission +from app.tests.abstract.models import TenancyModel -class TeamModel(TestCase): + +class TeamModel( + TestCase, + TenancyModel +): model = Team @@ -53,4 +58,9 @@ class TeamModel(TestCase): the save method is overridden. the function attributes must match default django method """ + pass + + + @pytest.mark.skip(reason="uses Django group manager") + def test_attribute_is_type_objects(self): pass \ No newline at end of file diff --git a/app/access/tests/unit/team/test_team_access_tenancy_object.py b/app/access/tests/unit/team/test_team_access_tenancy_object.py deleted file mode 100644 index 2de17f3d..00000000 --- a/app/access/tests/unit/team/test_team_access_tenancy_object.py +++ /dev/null @@ -1,17 +0,0 @@ -import pytest -import unittest -import requests - -from django.test import TestCase, Client - -from access.models import Team -from access.tests.abstract.tenancy_object import TenancyObject - - - -class TeamTenancyObject( - TestCase, - TenancyObject -): - - model = Team diff --git a/app/app/tests/abstract/models.py b/app/app/tests/abstract/models.py index 6b4ad48b..bccde312 100644 --- a/app/app/tests/abstract/models.py +++ b/app/app/tests/abstract/models.py @@ -3,6 +3,28 @@ import unittest from app.tests.abstract.views import AddView, ChangeView, DeleteView, DisplayView, IndexView + + +class BaseModel: + """ Test cases for all models """ + + model = None + """ Model to test """ + + + + +class TenancyModel( + BaseModel, + TenancyObjectTestCases +): + """ Test cases for tenancy models""" + + model = None + """ Model to test """ + + + class ModelAdd( AddView ): diff --git a/app/config_management/tests/unit/config_groups/test_config_groups.py b/app/config_management/tests/unit/config_groups/test_config_groups.py index 5d387fff..697e6335 100644 --- a/app/config_management/tests/unit/config_groups/test_config_groups.py +++ b/app/config_management/tests/unit/config_groups/test_config_groups.py @@ -5,12 +5,17 @@ from django.test import TestCase from access.models import Organization +from app.tests.abstract.models import TenancyModel + from config_management.models.groups import ConfigGroups @pytest.mark.django_db -class ConfigGroupsModel(TestCase): +class ConfigGroupsModel( + TestCase, + TenancyModel +): model = ConfigGroups diff --git a/app/config_management/tests/unit/config_groups/test_config_groups_access_tenancy_object.py b/app/config_management/tests/unit/config_groups/test_config_groups_access_tenancy_object.py deleted file mode 100644 index 72930958..00000000 --- a/app/config_management/tests/unit/config_groups/test_config_groups_access_tenancy_object.py +++ /dev/null @@ -1,18 +0,0 @@ -import pytest -import unittest -import requests - -from django.test import TestCase, Client - -from access.tests.abstract.tenancy_object import TenancyObject - -from config_management.models.groups import ConfigGroups - - - -class ConfigGroupsTenancyObject( - TestCase, - TenancyObject -): - - model = ConfigGroups diff --git a/app/config_management/tests/unit/config_groups_software/test_config_groups_software.py b/app/config_management/tests/unit/config_groups_software/test_config_groups_software.py index b52278ea..ceb1028e 100644 --- a/app/config_management/tests/unit/config_groups_software/test_config_groups_software.py +++ b/app/config_management/tests/unit/config_groups_software/test_config_groups_software.py @@ -5,6 +5,8 @@ from django.test import TestCase from access.models import Organization +from app.tests.abstract.models import TenancyModel + from config_management.models.groups import ConfigGroups, ConfigGroupSoftware from itam.models.device import DeviceSoftware @@ -12,7 +14,10 @@ from itam.models.software import Software -class ConfigGroupSoftwareModel(TestCase): +class ConfigGroupSoftwareModel( + TestCase, + TenancyModel +): model = ConfigGroupSoftware diff --git a/app/config_management/tests/unit/config_groups_software/test_config_groups_software_access_tenancy_object.py b/app/config_management/tests/unit/config_groups_software/test_config_groups_software_access_tenancy_object.py deleted file mode 100644 index ec4cb23d..00000000 --- a/app/config_management/tests/unit/config_groups_software/test_config_groups_software_access_tenancy_object.py +++ /dev/null @@ -1,18 +0,0 @@ -import pytest -import unittest -import requests - -from django.test import TestCase, Client - -from access.tests.abstract.tenancy_object import TenancyObject - -from config_management.models.groups import ConfigGroupSoftware - - - -class ConfigGroupSoftwareTenancyObject( - TestCase, - TenancyObject -): - - model = ConfigGroupSoftware diff --git a/app/core/tests/unit/manufacturer/test_manufacturer.py b/app/core/tests/unit/manufacturer/test_manufacturer.py new file mode 100644 index 00000000..5a91925e --- /dev/null +++ b/app/core/tests/unit/manufacturer/test_manufacturer.py @@ -0,0 +1,17 @@ +import pytest +import unittest +import requests + +from django.test import TestCase + +from app.tests.abstract.models import TenancyModel + +from core.models.manufacturer import Manufacturer + + +class ManufacturerModel( + TestCase, + TenancyModel +): + + model = Manufacturer diff --git a/app/core/tests/unit/test_notes/test_notes.py b/app/core/tests/unit/test_notes/test_notes.py index 9c2885a8..a5d690f2 100644 --- a/app/core/tests/unit/test_notes/test_notes.py +++ b/app/core/tests/unit/test_notes/test_notes.py @@ -1,21 +1,31 @@ - -from django.test import TestCase, Client - import pytest import unittest import requests +from django.test import TestCase + +from app.tests.abstract.models import TenancyModel + +from core.models.notes import Notes -@pytest.mark.skip(reason="to be written") -def test_note_new_correct_usercreated(): - """ The user who added the note must be added to the note """ - pass +class NotesModel( + TestCase, + TenancyModel +): + + model = Notes -@pytest.mark.skip(reason="to be written") -def test_note_new_correct_usermodified(): - """ The user who edited the note must be added to the note """ - pass + @pytest.mark.skip(reason="to be written") + def test_note_new_correct_usercreated(): + """ The user who added the note must be added to the note """ + pass + + + @pytest.mark.skip(reason="to be written") + def test_note_new_correct_usermodified(): + """ The user who edited the note must be added to the note """ + pass diff --git a/app/itam/tests/unit/device/test_device.py b/app/itam/tests/unit/device/test_device.py index e33b9a7e..ba29202d 100644 --- a/app/itam/tests/unit/device/test_device.py +++ b/app/itam/tests/unit/device/test_device.py @@ -6,16 +6,14 @@ import pytest import unittest import requests -# from django.contrib.auth import get_user_model -# from django.core.exceptions import ValidationError -# from access.models import Organization - -# from access.models import Organization +from app.tests.abstract.models import TenancyModel from itam.models.device import Device + class Device( - TestCase + TestCase, + TenancyModel, ): model = Device diff --git a/app/itam/tests/unit/device/test_device_access_tenancy_object.py b/app/itam/tests/unit/device/test_device_access_tenancy_object.py deleted file mode 100644 index 1ff479bb..00000000 --- a/app/itam/tests/unit/device/test_device_access_tenancy_object.py +++ /dev/null @@ -1,18 +0,0 @@ -import pytest -import unittest - -from django.test import TestCase, Client - -from access.tests.abstract.tenancy_object import TenancyObject - -from itam.models.device import Device - - - -class DeviceTenancyObject( - TestCase, - TenancyObject -): - - model = Device - diff --git a/app/itam/tests/unit/device_model/test_device_model.py b/app/itam/tests/unit/device_model/test_device_model.py index ee5c8b04..14f2df9e 100644 --- a/app/itam/tests/unit/device_model/test_device_model.py +++ b/app/itam/tests/unit/device_model/test_device_model.py @@ -1,44 +1,46 @@ -# from django.conf import settings -# from django.shortcuts import reverse -from django.test import TestCase, Client - import pytest import unittest import requests -# from django.contrib.auth import get_user_model -# from django.core.exceptions import ValidationError -# from access.models import Organization +from django.test import TestCase, Client -# class Test_app_structure_auth(unittest.TestCase): -# User = get_user_model() +from app.tests.abstract.models import TenancyModel + +from itam.models.device_models import DeviceModel -@pytest.mark.skip(reason="to be written") -def test_device_model_software_action(user): - """Ensure only software that is from the same organization or is global can be added to the device - """ - pass +class DeviceModelModel( + TestCase, + TenancyModel +): + + model = DeviceModel + + @pytest.mark.skip(reason="to be written") + def test_device_model_software_action(user): + """Ensure only software that is from the same organization or is global can be added to the device + """ + pass -@pytest.mark.skip(reason="to be written") -def test_device_model_must_have_organization(user): - """ Device Model must have organization set """ - pass + @pytest.mark.skip(reason="to be written") + def test_device_model_must_have_organization(user): + """ Device Model must have organization set """ + pass -@pytest.mark.skip(reason="to be written") -def test_device_model_not_global(user): - """Devices are not global items. + @pytest.mark.skip(reason="to be written") + def test_device_model_not_global(user): + """Devices are not global items. - Ensure that a device can't be set to be global. - """ - pass + Ensure that a device can't be set to be global. + """ + pass -@pytest.mark.skip(reason="to be written") -def test_device_model_operating_system_version_only_one(user): - """model deviceoperatingsystem must only contain one value per device - """ - pass + @pytest.mark.skip(reason="to be written") + def test_device_model_operating_system_version_only_one(user): + """model deviceoperatingsystem must only contain one value per device + """ + pass diff --git a/app/itam/tests/unit/device_model/test_device_model_access_tenancy_object.py b/app/itam/tests/unit/device_model/test_device_model_access_tenancy_object.py deleted file mode 100644 index eee81c88..00000000 --- a/app/itam/tests/unit/device_model/test_device_model_access_tenancy_object.py +++ /dev/null @@ -1,18 +0,0 @@ -import pytest -import unittest -import requests - -from django.test import TestCase, Client - -from access.tests.abstract.tenancy_object import TenancyObject - -from itam.models.device import DeviceModel - - - -class DeviceModelTenancyObject( - TestCase, - TenancyObject -): - - model = DeviceModel diff --git a/app/itam/tests/unit/device_operating_system/test_device_operating_system.py b/app/itam/tests/unit/device_operating_system/test_device_operating_system.py index 5a968fee..4e5cd664 100644 --- a/app/itam/tests/unit/device_operating_system/test_device_operating_system.py +++ b/app/itam/tests/unit/device_operating_system/test_device_operating_system.py @@ -5,6 +5,8 @@ from django.test import TestCase from access.models import Organization +from app.tests.abstract.models import TenancyModel + from config_management.models.groups import ConfigGroups, ConfigGroupSoftware from itam.models.device import Device, DeviceOperatingSystem @@ -12,7 +14,10 @@ from itam.models.operating_system import OperatingSystem, OperatingSystemVersion -class DeviceOperatingSystemModel(TestCase): +class DeviceOperatingSystemModel( + TestCase, + TenancyModel, +): model = DeviceOperatingSystem diff --git a/app/itam/tests/unit/device_operating_system/test_device_operating_system_access_tenancy_object.py b/app/itam/tests/unit/device_operating_system/test_device_operating_system_access_tenancy_object.py deleted file mode 100644 index ee24ca80..00000000 --- a/app/itam/tests/unit/device_operating_system/test_device_operating_system_access_tenancy_object.py +++ /dev/null @@ -1,18 +0,0 @@ -import pytest -import unittest -import requests - -from django.test import TestCase, Client - -from access.tests.abstract.tenancy_object import TenancyObject - -from itam.models.device import DeviceOperatingSystem - - - -class DeviceOperatingSystemTenancyObject( - TestCase, - TenancyObject -): - - model = DeviceOperatingSystem diff --git a/app/itam/tests/unit/device_software/test_device_software.py b/app/itam/tests/unit/device_software/test_device_software.py index 3a1aa053..776f9c73 100644 --- a/app/itam/tests/unit/device_software/test_device_software.py +++ b/app/itam/tests/unit/device_software/test_device_software.py @@ -5,6 +5,8 @@ from django.test import TestCase from access.models import Organization +from app.tests.abstract.models import TenancyModel + from config_management.models.groups import ConfigGroups, ConfigGroupSoftware from itam.models.device import Device, DeviceSoftware @@ -12,12 +14,14 @@ from itam.models.software import Software -class DeviceSoftwareModel(TestCase): +class DeviceSoftwareModel( + TestCase, + TenancyModel, +): model = DeviceSoftware - @classmethod def setUpTestData(self): """ Setup Test diff --git a/app/itam/tests/unit/device_software/test_device_software_access_tenancy_object.py b/app/itam/tests/unit/device_software/test_device_software_access_tenancy_object.py deleted file mode 100644 index 1520df89..00000000 --- a/app/itam/tests/unit/device_software/test_device_software_access_tenancy_object.py +++ /dev/null @@ -1,18 +0,0 @@ -import pytest -import unittest -import requests - -from django.test import TestCase, Client - -from access.tests.abstract.tenancy_object import TenancyObject - -from itam.models.device import DeviceSoftware - - - -class DeviceSoftwareTenancyObject( - TestCase, - TenancyObject -): - - model = DeviceSoftware diff --git a/app/itam/tests/unit/device_type/test_device_type.py b/app/itam/tests/unit/device_type/test_device_type.py new file mode 100644 index 00000000..d52a89ec --- /dev/null +++ b/app/itam/tests/unit/device_type/test_device_type.py @@ -0,0 +1,17 @@ +import pytest +import unittest +import requests + +from django.test import TestCase + +from app.tests.abstract.models import TenancyModel + +from itam.models.device import DeviceType + + +class DeviceTypeModel( + TestCase, + TenancyModel +): + + model = DeviceType diff --git a/app/itam/tests/unit/device_type/test_device_type_access_tenancy_object.py b/app/itam/tests/unit/device_type/test_device_type_access_tenancy_object.py deleted file mode 100644 index d1c0eac0..00000000 --- a/app/itam/tests/unit/device_type/test_device_type_access_tenancy_object.py +++ /dev/null @@ -1,18 +0,0 @@ -import pytest -import unittest -import requests - -from django.test import TestCase, Client - -from access.tests.abstract.tenancy_object import TenancyObject - -from itam.models.device import DeviceType - - - -class DeviceTypeTenancyObject( - TestCase, - TenancyObject -): - - model = DeviceType diff --git a/app/itam/tests/unit/operating_system/test_operating_system.py b/app/itam/tests/unit/operating_system/test_operating_system.py index de64efb3..ad2d770e 100644 --- a/app/itam/tests/unit/operating_system/test_operating_system.py +++ b/app/itam/tests/unit/operating_system/test_operating_system.py @@ -6,42 +6,50 @@ import pytest import unittest import requests -# from django.contrib.auth import get_user_model -# from django.core.exceptions import ValidationError -# from access.models import Organization -# class Test_app_structure_auth(unittest.TestCase): +from app.tests.abstract.models import TenancyModel + +from itam.models.operating_system import OperatingSystem -@pytest.mark.skip(reason="to be written") -def test_operating_system_must_have_organization(user): - """ Operating_system must have organization set """ - pass -@pytest.mark.skip(reason="to be written") -def test_operating_system_update_is_global_no_change(user): - """Once operating_system is set to global it can't be changed. +class OperatingSystemModel( + TestCase, + TenancyModel +): - global status can't be changed as non-global items may reference the item. - """ - - pass - -@pytest.mark.skip(reason="to be written") -def test_operating_system_prevent_delete_if_used(user): - """Any operating_system in use by a operating_system must not be deleted. - - i.e. A global os can't be deleted - """ - - pass + model = OperatingSystem -@pytest.mark.skip(reason="to be written") -def test_operating_system_version_installs_by_os_count(user): - """Operating System Versions has a count field that must be accurate + @pytest.mark.skip(reason="to be written") + def test_operating_system_must_have_organization(user): + """ Operating_system must have organization set """ + pass - The count is of model OperatingSystemVersion linked to model operating_systemOperatingSystem - """ + @pytest.mark.skip(reason="to be written") + def test_operating_system_update_is_global_no_change(user): + """Once operating_system is set to global it can't be changed. - pass + global status can't be changed as non-global items may reference the item. + """ + + pass + + @pytest.mark.skip(reason="to be written") + def test_operating_system_prevent_delete_if_used(user): + """Any operating_system in use by a operating_system must not be deleted. + + i.e. A global os can't be deleted + """ + + pass + + + @pytest.mark.skip(reason="to be written") + def test_operating_system_version_installs_by_os_count(user): + """Operating System Versions has a count field that must be accurate + + The count is of model OperatingSystemVersion linked to model operating_systemOperatingSystem + """ + + pass diff --git a/app/itam/tests/unit/operating_system/test_operating_system_access_tenancy_object.py b/app/itam/tests/unit/operating_system/test_operating_system_access_tenancy_object.py deleted file mode 100644 index a15fa253..00000000 --- a/app/itam/tests/unit/operating_system/test_operating_system_access_tenancy_object.py +++ /dev/null @@ -1,18 +0,0 @@ -import pytest -import unittest -import requests - -from django.test import TestCase, Client - -from access.tests.abstract.tenancy_object import TenancyObject - -from itam.models.operating_system import OperatingSystem - - - -class OperatingSystemTenancyObject( - TestCase, - TenancyObject -): - - model = OperatingSystem diff --git a/app/itam/tests/unit/operating_system_version/test_operating_system_version.py b/app/itam/tests/unit/operating_system_version/test_operating_system_version.py index f192a20c..a510e3be 100644 --- a/app/itam/tests/unit/operating_system_version/test_operating_system_version.py +++ b/app/itam/tests/unit/operating_system_version/test_operating_system_version.py @@ -5,13 +5,18 @@ from django.test import TestCase from access.models import Organization +from app.tests.abstract.models import TenancyModel + from config_management.models.groups import ConfigGroups, ConfigGroupSoftware from itam.models.operating_system import OperatingSystem, OperatingSystemVersion -class OperatingSystemVersionModel(TestCase): +class OperatingSystemVersionModel( + TestCase, + TenancyModel, +): model = OperatingSystemVersion diff --git a/app/itam/tests/unit/operating_system_version/test_operating_system_version_access_tenancy_object.py b/app/itam/tests/unit/operating_system_version/test_operating_system_version_access_tenancy_object.py deleted file mode 100644 index d3a5657d..00000000 --- a/app/itam/tests/unit/operating_system_version/test_operating_system_version_access_tenancy_object.py +++ /dev/null @@ -1,18 +0,0 @@ -import pytest -import unittest -import requests - -from django.test import TestCase, Client - -from access.tests.abstract.tenancy_object import TenancyObject - -from itam.models.operating_system import OperatingSystemVersion - - - -class OperatingSystemVersionTenancyObject( - TestCase, - TenancyObject -): - - model = OperatingSystemVersion diff --git a/app/itam/tests/unit/software/test_software.py b/app/itam/tests/unit/software/test_software.py index 5762dcef..14fe3893 100644 --- a/app/itam/tests/unit/software/test_software.py +++ b/app/itam/tests/unit/software/test_software.py @@ -1,37 +1,42 @@ -# from django.conf import settings -# from django.shortcuts import reverse -from django.test import TestCase, Client - import pytest import unittest import requests -# from django.contrib.auth import get_user_model -# from django.core.exceptions import ValidationError -# from access.models import Organization +from django.test import TestCase, Client -# class Test_app_structure_auth(unittest.TestCase): +from app.tests.abstract.models import TenancyModel + +from itam.models.software import Software -@pytest.mark.skip(reason="to be written") -def test_software_must_have_organization(user): - """ Software must have organization set """ - pass -@pytest.mark.skip(reason="to be written") -def test_software_update_is_global_no_change(user): - """Once software is set to global it can't be changed. +class SoftwareModel( + TestCase, + TenancyModel +): - global status can't be changed as non-global items may reference the item. - """ + model = Software - pass -@pytest.mark.skip(reason="to be written") -def test_software_prevent_delete_if_used(user): - """Any software in use by a software must not be deleted. + @pytest.mark.skip(reason="to be written") + def test_software_must_have_organization(self): + """ Software must have organization set """ + pass - i.e. A software has an action set for the software. - """ + @pytest.mark.skip(reason="to be written") + def test_software_update_is_global_no_change(self): + """Once software is set to global it can't be changed. - pass + global status can't be changed as non-global items may reference the item. + """ + + pass + + @pytest.mark.skip(reason="to be written") + def test_software_prevent_delete_if_used(self): + """Any software in use by a software must not be deleted. + + i.e. A software has an action set for the software. + """ + + pass diff --git a/app/itam/tests/unit/software/test_software_access_tenancy_object.py b/app/itam/tests/unit/software/test_software_access_tenancy_object.py deleted file mode 100644 index 1bca835b..00000000 --- a/app/itam/tests/unit/software/test_software_access_tenancy_object.py +++ /dev/null @@ -1,18 +0,0 @@ -import pytest -import unittest -import requests - -from django.test import TestCase, Client - -from access.tests.abstract.tenancy_object import TenancyObject - -from itam.models.software import Software - - - -class SoftwareTenancyObject( - TestCase, - TenancyObject -): - - model = Software diff --git a/app/itam/tests/unit/software_category/test_software_category.py b/app/itam/tests/unit/software_category/test_software_category.py new file mode 100644 index 00000000..0cc70312 --- /dev/null +++ b/app/itam/tests/unit/software_category/test_software_category.py @@ -0,0 +1,18 @@ +import pytest +import unittest +import requests + +from django.test import TestCase + +from app.tests.abstract.models import TenancyModel + +from itam.models.software import SoftwareCategory + + + +class SoftwareCategoryModel( + TestCase, + TenancyModel +): + + model = SoftwareCategory diff --git a/app/itam/tests/unit/software_category/test_software_category_access_tenancy_object.py b/app/itam/tests/unit/software_category/test_software_category_access_tenancy_object.py deleted file mode 100644 index 021e2e15..00000000 --- a/app/itam/tests/unit/software_category/test_software_category_access_tenancy_object.py +++ /dev/null @@ -1,18 +0,0 @@ -import pytest -import unittest -import requests - -from django.test import TestCase, Client - -from access.tests.abstract.tenancy_object import TenancyObject - -from itam.models.software import SoftwareCategory - - - -class SoftwareCategoryTenancyObject( - TestCase, - TenancyObject -): - - model = SoftwareCategory From d8e89bee104149186beaec173d65f56f0407b65c Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 15 Jul 2024 23:22:15 +0930 Subject: [PATCH 008/123] test: tenancy objects !42 #15 closes #124 --- app/access/tests/abstract/tenancy_object.py | 20 +++++++ .../tenancy_object/test_tenancy_object.py | 52 ++++++++++++++++++- app/app/tests/abstract/models.py | 14 +++++ app/core/tests/abstract/models.py | 19 ------- .../development/api/tests/index.md | 2 + .../development/api/tests/models.md | 27 ++++++++++ .../centurion_erp/development/models.md | 12 +++++ .../centurion_erp/development/views.md | 20 +++++++ mkdocs.yml | 2 + 9 files changed, 147 insertions(+), 21 deletions(-) create mode 100644 docs/projects/centurion_erp/development/api/tests/models.md diff --git a/app/access/tests/abstract/tenancy_object.py b/app/access/tests/abstract/tenancy_object.py index 60368ab5..e1ad9b7e 100644 --- a/app/access/tests/abstract/tenancy_object.py +++ b/app/access/tests/abstract/tenancy_object.py @@ -1,6 +1,8 @@ import pytest import unittest +from access.models import TenancyManager + class TenancyObject: @@ -66,3 +68,21 @@ class TenancyObject: Must not be able to edit an item without an organization """ pass + + + def test_has_attr_organization(self): + """ TenancyObject attribute check + + TenancyObject has function objects + """ + + assert hasattr(self.model, 'objects') + + + def test_attribute_is_type_objects(self): + """ Attribute Check + + attribute `objects` must be set to `access.models.TenancyManager()` + """ + + assert type(self.model.objects) is TenancyManager diff --git a/app/access/tests/unit/tenancy_object/test_tenancy_object.py b/app/access/tests/unit/tenancy_object/test_tenancy_object.py index 25b97db8..c2107ccf 100644 --- a/app/access/tests/unit/tenancy_object/test_tenancy_object.py +++ b/app/access/tests/unit/tenancy_object/test_tenancy_object.py @@ -3,17 +3,45 @@ import unittest from django.test import TestCase -from access.models import TenancyObject +from access.models import TenancyObject, TenancyManager + +from core.mixin.history_save import SaveHistory from unittest.mock import patch -class TenancyObject(TestCase): +class TenancyManagerTests(TestCase): + + item = TenancyManager + + + def test_has_attribute_get_queryset(self): + """ Field organization exists """ + + assert hasattr(self.item, 'get_queryset') + + + def test_is_function_get_queryset(self): + """ Attribute 'get_organization' is a function """ + + assert callable(self.item.get_queryset) + + + +class TenancyObjectTests(TestCase): item = TenancyObject + def test_class_inherits_save_history(self): + """ Confirm class inheritence + + TenancyObject must inherit SaveHistory + """ + + assert issubclass(TenancyObject, SaveHistory) + def test_has_attribute_organization(self): """ Field organization exists """ @@ -43,3 +71,23 @@ class TenancyObject(TestCase): """ Attribute 'get_organization' is a function """ assert callable(self.item.get_organization) + + + @pytest.mark.skip(reason="figure out how to test abstract class") + def test_has_attribute_objects(self): + """ Attribute Check + + attribute `objects` must be set to `access.models.TenancyManager()` + """ + + assert 'objects' in self.item + + + @pytest.mark.skip(reason="figure out how to test abstract class") + def test_attribute_not_none_objects(self): + """ Attribute Check + + attribute `objects` must be set to `access.models.TenancyManager()` + """ + + assert self.item.objects is not None diff --git a/app/app/tests/abstract/models.py b/app/app/tests/abstract/models.py index bccde312..89caaf77 100644 --- a/app/app/tests/abstract/models.py +++ b/app/app/tests/abstract/models.py @@ -1,8 +1,13 @@ import pytest import unittest +from access.models import TenancyObject +from access.tests.abstract.tenancy_object import TenancyObject as TenancyObjectTestCases + from app.tests.abstract.views import AddView, ChangeView, DeleteView, DisplayView, IndexView +from core.mixin.history_save import SaveHistory + class BaseModel: @@ -12,6 +17,15 @@ class BaseModel: """ Model to test """ + @pytest.mark.skip(reason="figure out how to test sub-sub-class") + def test_class_inherits_save_history(self): + """ Confirm class inheritence + + TenancyObject must inherit SaveHistory + """ + + assert issubclass(self.model, TenancyObject) + class TenancyModel( diff --git a/app/core/tests/abstract/models.py b/app/core/tests/abstract/models.py index 53eb8949..5ad90843 100644 --- a/app/core/tests/abstract/models.py +++ b/app/core/tests/abstract/models.py @@ -41,22 +41,3 @@ class Models: pass - @pytest.mark.skip(reason="write test") - def test_model_class_tenancy_object_attribute_objects(self): - """ Attribute Check - - attribute `objects` must be set to `access.models.TenancyManager()` - """ - - pass - - - @pytest.mark.skip(reason="write test") - def test_model_class_inheritence_tenancy_object_save_history(self): - """ Class inheritence Check - - Class inherits from `core.mixin.history_save.SaveHistory` - """ - - pass - diff --git a/docs/projects/centurion_erp/development/api/tests/index.md b/docs/projects/centurion_erp/development/api/tests/index.md index 5682dfaa..485d8b83 100644 --- a/docs/projects/centurion_erp/development/api/tests/index.md +++ b/docs/projects/centurion_erp/development/api/tests/index.md @@ -8,6 +8,8 @@ about: https://gitlab.com/nofusscomputing/infrastructure/configuration-managemen Models are tested using the following test cases: +- [Models](./models.md) + - [ALL Model Permission](./model_permissions.md) - [ALL Model Permission (organization Manager)](./model_permissions_organization_manager.md) diff --git a/docs/projects/centurion_erp/development/api/tests/models.md b/docs/projects/centurion_erp/development/api/tests/models.md new file mode 100644 index 00000000..a640c10d --- /dev/null +++ b/docs/projects/centurion_erp/development/api/tests/models.md @@ -0,0 +1,27 @@ +--- +title: Model Test Cases +description: No Fuss Computings model nit test cases +date: 2024-07-15 +template: project.html +about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/centurion_erp +--- + + +## Base Unit Tests for all models + +Abstract test class containing test cases for all models. + +::: app.app.tests.abstract.models.BaseModel + options: + inherited_members: false + heading_level: 3 + + +## Tenancy model Unit Tests + +Abstract test class containing test cases for Tenancy Object models + +::: app.app.tests.abstract.models.TenancyModel + options: + inherited_members: True + heading_level: 3 diff --git a/docs/projects/centurion_erp/development/models.md b/docs/projects/centurion_erp/development/models.md index 63d64c97..a7d8158c 100644 --- a/docs/projects/centurion_erp/development/models.md +++ b/docs/projects/centurion_erp/development/models.md @@ -34,6 +34,18 @@ All models must meet the following requirements: - No `queryset` is to return data that the user has not got access to. _see [queryset()](./api/models/tenancy_object.md#tenancy-object-manager)_ +## Tests + +The following Unit test cases exists for models: + +- [BaseModel](./api/tests/models.md#base-unit-tests-for-all-models) + +- [TenancyObject](./api/tests/models.md#tenancy-model-unit-tests) + +!!! info + If you add a feature you will have to write the test cases for that feature if they are not covered by existing test cases. + + ## Docs to clean up !!! note diff --git a/docs/projects/centurion_erp/development/views.md b/docs/projects/centurion_erp/development/views.md index 8de9d3aa..09bb5b78 100644 --- a/docs/projects/centurion_erp/development/views.md +++ b/docs/projects/centurion_erp/development/views.md @@ -50,6 +50,26 @@ All views are to meet the following requirements: - Add and change views to use a form class +## Tests + +The following unit test cases exist for views: + +- [AddView](./api/tests/model_views.md#add-view) + +- [ChangeView](./api/tests/model_views.md#change-view) + +- [DeleteView](./api/tests/model_views.md#delete-view) + +- [Display View](./api/tests/model_views.md#display-view) + +- [IndexView](./api/tests/model_views.md#index-view) + +- [AllViews](./api/tests/model_views.md#all-views) + +!!! tip + The `AllViews` test class is an aggregation of all views. This class is the recommended test class to include if the model uses all available views. + + ## Docs to clean up !!! note diff --git a/mkdocs.yml b/mkdocs.yml index 0d033db8..62a7b683 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -92,6 +92,8 @@ nav: - projects/centurion_erp/development/api/tests/index.md + - projects/centurion_erp/development/api/tests/models.md + - projects/centurion_erp/development/api/tests/model_history.md - projects/centurion_erp/development/api/tests/model_history_child_item.md From 621cbd2d7151e5e1a8515ee9751b5232b6d41359 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 16 Jul 2024 00:02:45 +0930 Subject: [PATCH 009/123] revert: return organization filtering back to forms !42 #124 --- app/core/forms/common.py | 79 +++++++++++++++++++++++++++++++++++++++- app/core/views/common.py | 16 ++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/app/core/forms/common.py b/app/core/forms/common.py index 6c32b2d2..d59714d1 100644 --- a/app/core/forms/common.py +++ b/app/core/forms/common.py @@ -1,4 +1,7 @@ from django import forms +from django.db.models import Q + +from access.models import Organization, TeamUsers @@ -8,6 +11,14 @@ class CommonModelForm(forms.ModelForm): This class exists so that common functions can be conducted against forms as they are loaded. """ + organization_field: str = 'organization' + """ Organization Field + + Name of the field that contains Organizations. + + This field will be filtered to those that the user is part of. + """ + def __init__(self, *args, **kwargs): """Form initialization. @@ -15,9 +26,75 @@ class CommonModelForm(forms.ModelForm): Initialize the form using the super classes first then continue to initialize the form using logic contained within this method. + ## Tenancy Objects + + Fields that contain an attribute called `organization` will have the objects filtered to + the organizations the user is part of. If the object has `is_global=True`, that object will not be + filtered out. + !!! danger "Requirement" This method may be overridden however must still be called from the overriding function. i.e. `super().__init__(*args, **kwargs)` """ - super().__init__(*args, **kwargs) + user = kwargs.pop('user', None) + + user_organizations: list([str]) = [] + user_organizations_id: list(int()) = [] + + for team_user in TeamUsers.objects.filter(user=user): + + if team_user.team.organization.name not in user_organizations: + + if not user_organizations: + + self.user_organizations = [] + + user_organizations += [ team_user.team.organization.name ] + user_organizations_id += [ team_user.team.organization.id ] + + new_kwargs: dict = {} + + for key, value in kwargs.items(): + + if key != 'user': + + new_kwargs.update({key: value}) + + super().__init__(*args, **new_kwargs) + + + if len(user_organizations_id) > 0: + + for field_name in self.fields: + + field = self.fields[field_name] + + if hasattr(field, 'queryset'): + + if hasattr(field.queryset.model, 'organization'): + + if hasattr(field.queryset.model, 'is_global'): + + self.fields[field_name].queryset = field.queryset.filter( + Q(organization__in=user_organizations_id) + | + Q(is_global = True) + ) + + else: + + self.fields[field_name].queryset = field.queryset.filter( + Q(organization__in=user_organizations_id) + ) + + + if self.Meta.fields: + + if self.organization_field in self.Meta.fields: + + self.fields[self.organization_field].queryset = self.fields[self.organization_field].queryset.filter( + Q(id__in=user_organizations_id) + | + Q(manager=user) + ) diff --git a/app/core/views/common.py b/app/core/views/common.py index 635524be..a63e5e0a 100644 --- a/app/core/views/common.py +++ b/app/core/views/common.py @@ -15,6 +15,22 @@ class View(OrganizationPermission): template_name:str = 'form.html.j2' + def get_form_kwargs(self) -> dict: + """ Fetch kwargs for form + + Returns: + dict: kwargs used in fetching form + """ + + kwargs = super().get_form_kwargs() + + if self.form_class: + + kwargs.update({'user': self.request.user}) + + return kwargs + + class AddView(View, generic.CreateView): From 7c62309a31fe2d7b91d584dc06b2a590d3074d95 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 16 Jul 2024 11:55:25 +0930 Subject: [PATCH 010/123] fix(core): migrate manufacturer to use new form/view logic !42 fixes #127 --- app/core/forms/manufacturer.py | 3 ++- app/settings/views/manufacturer.py | 17 +++-------------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/app/core/forms/manufacturer.py b/app/core/forms/manufacturer.py index 164222de..f65ee3dc 100644 --- a/app/core/forms/manufacturer.py +++ b/app/core/forms/manufacturer.py @@ -1,10 +1,11 @@ from django import forms +from core.forms.common import CommonModelForm from core.models.manufacturer import Manufacturer -class ManufacturerForm(forms.ModelForm): +class ManufacturerForm(CommonModelForm): class Meta: diff --git a/app/settings/views/manufacturer.py b/app/settings/views/manufacturer.py index 6ef2c189..b5c96370 100644 --- a/app/settings/views/manufacturer.py +++ b/app/settings/views/manufacturer.py @@ -28,17 +28,6 @@ class Index(IndexView): template_name = 'settings/manufacturers.html.j2' - def get_queryset(self): - - if self.request.user.is_superuser: - - return self.model.objects.filter().order_by('name') - - else: - - return self.model.objects.filter(Q(organization__in=self.user_organizations()) | Q(is_global = True)).order_by('name') - - def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -49,7 +38,7 @@ class Index(IndexView): -class View(OrganizationPermission, generic.UpdateView): +class View(ChangeView): context_object_name = "manufacturer" @@ -89,7 +78,7 @@ class View(OrganizationPermission, generic.UpdateView): -class Add(OrganizationPermission, generic.CreateView): +class Add(AddView): form_class = ManufacturerForm @@ -116,7 +105,7 @@ class Add(OrganizationPermission, generic.CreateView): return context -class Delete(OrganizationPermission, generic.DeleteView): +class Delete(DeleteView): model = Manufacturer From 7b26fac73d38d9644e20db0b3a85b4def73c9bf1 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 16 Jul 2024 13:12:37 +0930 Subject: [PATCH 011/123] feat: Administratively set global items org/is_global field now read-only !42 fixes #126 --- app/core/forms/manufacturer.py | 7 ++++- app/core/views/common.py | 8 +++++ app/itam/forms/device_model.py | 8 ++++- app/itam/forms/device_type.py | 8 ++++- app/itam/forms/software_category.py | 7 ++++- app/itam/views/device_type.py | 8 ----- app/settings/forms/__init__.py | 0 app/settings/forms/admin_settings_global.py | 31 +++++++++++++++++++ .../development/api/admin_model_form.md | 19 ++++++++++++ .../api/{form.md => model_form.md} | 0 .../centurion_erp/development/forms.md | 13 ++++++-- .../centurion_erp/development/index.md | 2 +- mkdocs.yml | 4 ++- 13 files changed, 99 insertions(+), 16 deletions(-) create mode 100644 app/settings/forms/__init__.py create mode 100644 app/settings/forms/admin_settings_global.py create mode 100644 docs/projects/centurion_erp/development/api/admin_model_form.md rename docs/projects/centurion_erp/development/api/{form.md => model_form.md} (100%) diff --git a/app/core/forms/manufacturer.py b/app/core/forms/manufacturer.py index f65ee3dc..ffd3c72f 100644 --- a/app/core/forms/manufacturer.py +++ b/app/core/forms/manufacturer.py @@ -3,9 +3,14 @@ from django import forms from core.forms.common import CommonModelForm from core.models.manufacturer import Manufacturer +from settings.forms.admin_settings_global import AdminGlobalModels -class ManufacturerForm(CommonModelForm): + +class ManufacturerForm( + AdminGlobalModels, + CommonModelForm +): class Meta: diff --git a/app/core/views/common.py b/app/core/views/common.py index a63e5e0a..fef95df0 100644 --- a/app/core/views/common.py +++ b/app/core/views/common.py @@ -4,6 +4,9 @@ from access.mixin import OrganizationPermission from core.exceptions import MissingAttribute +from settings.models.user_settings import UserSettings + + class View(OrganizationPermission): """ Abstract class common to all views @@ -36,6 +39,11 @@ class AddView(View, generic.CreateView): template_name:str = 'form.html.j2' + def get_initial(self): + + return { + 'organization': UserSettings.objects.get(user = self.request.user).default_organization + } class ChangeView(View, generic.UpdateView): diff --git a/app/itam/forms/device_model.py b/app/itam/forms/device_model.py index 1dc01bb9..c2841f77 100644 --- a/app/itam/forms/device_model.py +++ b/app/itam/forms/device_model.py @@ -4,9 +4,15 @@ from core.forms.common import CommonModelForm from itam.models.device_models import DeviceModel +from settings.forms.admin_settings_global import AdminGlobalModels -class DeviceModelForm(CommonModelForm): + +class DeviceModelForm( + AdminGlobalModels, + CommonModelForm +): + class Meta: diff --git a/app/itam/forms/device_type.py b/app/itam/forms/device_type.py index 7ce3cbc1..9d0adfc3 100644 --- a/app/itam/forms/device_type.py +++ b/app/itam/forms/device_type.py @@ -4,9 +4,15 @@ from core.forms.common import CommonModelForm from itam.models.device import DeviceType +from settings.forms.admin_settings_global import AdminGlobalModels -class DeviceTypeForm(CommonModelForm): + +class DeviceTypeForm( + AdminGlobalModels, + CommonModelForm +): + class Meta: diff --git a/app/itam/forms/software_category.py b/app/itam/forms/software_category.py index 23a5daff..d0218cd1 100644 --- a/app/itam/forms/software_category.py +++ b/app/itam/forms/software_category.py @@ -4,9 +4,14 @@ from core.forms.common import CommonModelForm from itam.models.software import SoftwareCategory +from settings.forms.admin_settings_global import AdminGlobalModels -class SoftwareCategoryForm(CommonModelForm): + +class SoftwareCategoryForm( + AdminGlobalModels, + CommonModelForm +): class Meta: diff --git a/app/itam/views/device_type.py b/app/itam/views/device_type.py index 84b93229..77e92513 100644 --- a/app/itam/views/device_type.py +++ b/app/itam/views/device_type.py @@ -7,7 +7,6 @@ from core.views.common import AddView, ChangeView, DeleteView, IndexView from itam.models.device import DeviceType from itam.forms.device_type import DeviceTypeForm -from settings.models.user_settings import UserSettings @@ -61,13 +60,6 @@ class Add(AddView): template_name = 'form.html.j2' - def get_initial(self): - - return { - 'organization': UserSettings.objects.get(user = self.request.user).default_organization - } - - def get_success_url(self, **kwargs): return reverse('Settings:_device_types') diff --git a/app/settings/forms/__init__.py b/app/settings/forms/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/settings/forms/admin_settings_global.py b/app/settings/forms/admin_settings_global.py new file mode 100644 index 00000000..c443296f --- /dev/null +++ b/app/settings/forms/admin_settings_global.py @@ -0,0 +1,31 @@ +from django import forms +from django.db.models import Q + +from django.contrib.auth.models import User + +from access.models import Organization, TeamUsers + +from core.forms.common import CommonModelForm + +from settings.models.app_settings import AppSettings + + +class AdminGlobalModels: + """Administratively set Global Models + + Use this class on models that can be set within the application settings as a global + application. + """ + + + def __init__(self, *args, **kwargs): + """ Init Form + + As these forms are for administratively set global organization, set the `organization` and `is_global` fields + to be read only. + """ + + super().__init__(*args, **kwargs) + + self.fields['organization'].widget.attrs['readonly'] = True + self.fields['is_global'].widget.attrs['readonly'] = True diff --git a/docs/projects/centurion_erp/development/api/admin_model_form.md b/docs/projects/centurion_erp/development/api/admin_model_form.md new file mode 100644 index 00000000..a64c3529 --- /dev/null +++ b/docs/projects/centurion_erp/development/api/admin_model_form.md @@ -0,0 +1,19 @@ +--- +title: Common forms +description: Centurion ERP Common Forms API Documentation +date: 2024-07-12 +template: project.html +about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/centurion_erp +--- + +Below you will find the API documentation for the forms that are used throughout the Centurion ERP application. + + +## Admin Global Model Form + +This class in intended to be inherited by any form that has an administrative setting to set the model as global and to be placed within a dedicated "Global organization" + +::: app.settings.forms.admin_settings_global.AdminGlobalModels + options: + inherited_members: false + heading_level: 3 diff --git a/docs/projects/centurion_erp/development/api/form.md b/docs/projects/centurion_erp/development/api/model_form.md similarity index 100% rename from docs/projects/centurion_erp/development/api/form.md rename to docs/projects/centurion_erp/development/api/model_form.md diff --git a/docs/projects/centurion_erp/development/forms.md b/docs/projects/centurion_erp/development/forms.md index e305f09d..c5905f08 100644 --- a/docs/projects/centurion_erp/development/forms.md +++ b/docs/projects/centurion_erp/development/forms.md @@ -15,7 +15,7 @@ All forms must meet the following requirements: - is defined as a class -- inherits from [`core.forms.common.CommonModelForm`](./api/form.md) +- inherits from [`core.forms.common.CommonModelForm`](./api/model_form.md) - contains a `Meta` sub-class with following parameters: @@ -23,6 +23,15 @@ All forms must meet the following requirements: - `model` -- Any additional filtering is done as part of an `__init__` method that also calls the super-class [`__init__`](./api/form.md) first +- Any additional filtering is done as part of an `__init__` method that also calls the super-class [`__init__`](./api/model_form.md) first - Any filtering of a fields `queryset` is to filter the existing `queryset` not redefine it. i.e. `field[].queryset = field[].queryset.filter()` + + +## Abstract Classes + +The following abstract classes exist for a forms inheritance: + +- [AdminGlobalModels](./api/admin_model_form.md#model-form) + +- [CommonModelForm](./api/model_form.md#model-form) diff --git a/docs/projects/centurion_erp/development/index.md b/docs/projects/centurion_erp/development/index.md index d2c46638..382ba600 100644 --- a/docs/projects/centurion_erp/development/index.md +++ b/docs/projects/centurion_erp/development/index.md @@ -15,7 +15,7 @@ Centurion ERP is a Django Application. We have added a lot of little tid bits th - [Application API Documentation](./api/index.md) -- [Forms](./api/form.md) +- [Forms](./forms.md) - [Models](./models.md) diff --git a/mkdocs.yml b/mkdocs.yml index 62a7b683..239d583d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -64,7 +64,9 @@ nav: - projects/centurion_erp/development/api/index.md - - projects/centurion_erp/development/api/form.md + - projects/centurion_erp/development/api/admin_model_form.md + + - projects/centurion_erp/development/api/model_form.md - projects/centurion_erp/development/api/token_authentication.md From 5a201ef548274ea6ea12a90f4b641fad814611a7 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 10 Jul 2024 03:14:39 +0930 Subject: [PATCH 012/123] refactor!: Squash database migrations BREAKING CHANGE: squashed DB migrations in preparation for v1.0 release. !40 !35 #74 --- app/access/migrations/0001_initial.py | 8 +- .../0002_alter_team_organization.py | 19 --- .../0003_alter_team_organization.py | 20 --- .../migrations/0004_team_model_notes.py | 18 --- ...zation_manager_organization_model_notes.py | 26 ---- ...alter_organization_model_notes_and_more.py | 23 ---- app/api/migrations/0001_initial.py | 2 +- .../migrations/0001_initial.py | 13 +- ...ions_alter_configgroups_config_and_more.py | 43 ------ ...2_configgrouphosts_configgroupsoftware.py} | 26 +++- ..._configgrouphosts_organization_and_more.py | 26 ---- ...5_configgrouphosts_model_notes_and_more.py | 28 ---- ...r_configgrouphosts_model_notes_and_more.py | 28 ---- app/core/migrations/0001_initial.py | 41 ++++-- app/core/migrations/0002_notes.py | 43 ++++++ ...rial_number_alter_notes_device_and_more.py | 34 ----- .../0003_alter_notes_note_history.py | 41 ------ app/core/migrations/0004_notes_is_null.py | 20 --- app/core/migrations/0005_manufacturer.py | 32 ----- .../migrations/0006_alter_history_user.py | 21 --- .../migrations/0007_notes_config_group.py | 20 --- ...lter_manufacturer_organization_and_more.py | 26 ---- ...ufacturer_model_notes_notes_model_notes.py | 23 ---- ...alter_manufacturer_model_notes_and_more.py | 23 ---- app/itam/migrations/0001_initial.py | 124 ++++++++++++++---- .../0002_alter_softwareversion_name.py | 18 --- .../migrations/0003_devicesoftware_version.py | 19 --- ..._operatingsystem_operatingsystemversion.py | 47 ------- ...er_operatingsystemversion_name_and_more.py | 38 ------ ...6_alter_devicesoftware_options_and_more.py | 38 ------ .../migrations/0007_device_inventorydate.py | 18 --- ...0008_alter_device_organization_and_more.py | 60 --------- .../0009_devicemodel_device_device_model.py | 39 ------ ...tingsystem_publisher_software_publisher.py | 20 --- .../migrations/0011_software_publisher.py | 20 --- ..._device_serial_number_alter_device_uuid.py | 23 ---- ...0013_alter_device_organization_and_more.py | 66 ---------- ..._notes_devicemodel_model_notes_and_more.py | 63 --------- ...model_alter_device_device_type_and_more.py | 34 ----- .../0016_alter_device_model_notes_and_more.py | 63 --------- app/settings/migrations/0001_initial.py | 69 +++++++++- app/settings/migrations/0002_usersettings.py | 32 ----- .../0003_create_settings_for_all_users.py | 37 ------ app/settings/migrations/0004_appsettings.py | 31 ----- .../0005_create_settings_for_app.py | 35 ----- ..._software_categories_is_global_and_more.py | 25 ---- ...0007_appsettings_device_model_is_global.py | 18 --- .../0008_appsettings_device_type_is_global.py | 18 --- ...0009_appsettings_manufacturer_is_global.py | 18 --- .../migrations/0010_delete_settings.py | 16 --- 50 files changed, 274 insertions(+), 1319 deletions(-) delete mode 100644 app/access/migrations/0002_alter_team_organization.py delete mode 100644 app/access/migrations/0003_alter_team_organization.py delete mode 100644 app/access/migrations/0004_team_model_notes.py delete mode 100644 app/access/migrations/0005_organization_manager_organization_model_notes.py delete mode 100644 app/access/migrations/0006_alter_organization_model_notes_and_more.py delete mode 100644 app/config_management/migrations/0002_alter_configgroups_options_alter_configgroups_config_and_more.py rename app/config_management/migrations/{0004_configgroupsoftware.py => 0002_configgrouphosts_configgroupsoftware.py} (51%) delete mode 100644 app/config_management/migrations/0003_alter_configgrouphosts_organization_and_more.py delete mode 100644 app/config_management/migrations/0005_configgrouphosts_model_notes_and_more.py delete mode 100644 app/config_management/migrations/0006_alter_configgrouphosts_model_notes_and_more.py create mode 100644 app/core/migrations/0002_notes.py delete mode 100644 app/core/migrations/0002_remove_notes_serial_number_alter_notes_device_and_more.py delete mode 100644 app/core/migrations/0003_alter_notes_note_history.py delete mode 100644 app/core/migrations/0004_notes_is_null.py delete mode 100644 app/core/migrations/0005_manufacturer.py delete mode 100644 app/core/migrations/0006_alter_history_user.py delete mode 100644 app/core/migrations/0007_notes_config_group.py delete mode 100644 app/core/migrations/0008_alter_manufacturer_organization_and_more.py delete mode 100644 app/core/migrations/0009_manufacturer_model_notes_notes_model_notes.py delete mode 100644 app/core/migrations/0010_alter_manufacturer_model_notes_and_more.py delete mode 100644 app/itam/migrations/0002_alter_softwareversion_name.py delete mode 100644 app/itam/migrations/0003_devicesoftware_version.py delete mode 100644 app/itam/migrations/0004_operatingsystem_operatingsystemversion.py delete mode 100644 app/itam/migrations/0005_alter_operatingsystemversion_name_and_more.py delete mode 100644 app/itam/migrations/0006_alter_devicesoftware_options_and_more.py delete mode 100644 app/itam/migrations/0007_device_inventorydate.py delete mode 100644 app/itam/migrations/0008_alter_device_organization_and_more.py delete mode 100644 app/itam/migrations/0009_devicemodel_device_device_model.py delete mode 100644 app/itam/migrations/0010_operatingsystem_publisher_software_publisher.py delete mode 100644 app/itam/migrations/0011_software_publisher.py delete mode 100644 app/itam/migrations/0012_alter_device_serial_number_alter_device_uuid.py delete mode 100644 app/itam/migrations/0013_alter_device_organization_and_more.py delete mode 100644 app/itam/migrations/0014_device_model_notes_devicemodel_model_notes_and_more.py delete mode 100644 app/itam/migrations/0015_alter_device_device_model_alter_device_device_type_and_more.py delete mode 100644 app/itam/migrations/0016_alter_device_model_notes_and_more.py delete mode 100644 app/settings/migrations/0002_usersettings.py delete mode 100644 app/settings/migrations/0003_create_settings_for_all_users.py delete mode 100644 app/settings/migrations/0004_appsettings.py delete mode 100644 app/settings/migrations/0005_create_settings_for_app.py delete mode 100644 app/settings/migrations/0006_appsettings_software_categories_is_global_and_more.py delete mode 100644 app/settings/migrations/0007_appsettings_device_model_is_global.py delete mode 100644 app/settings/migrations/0008_appsettings_device_type_is_global.py delete mode 100644 app/settings/migrations/0009_appsettings_manufacturer_is_global.py delete mode 100644 app/settings/migrations/0010_delete_settings.py diff --git a/app/access/migrations/0001_initial.py b/app/access/migrations/0001_initial.py index 47ae10a7..6a836369 100644 --- a/app/access/migrations/0001_initial.py +++ b/app/access/migrations/0001_initial.py @@ -1,6 +1,7 @@ -# Generated by Django 5.0.4 on 2024-05-13 16:08 +# Generated by Django 5.0.7 on 2024-07-12 03:54 import access.fields +import access.models import django.contrib.auth.models import django.db.models.deletion import django.utils.timezone @@ -23,9 +24,11 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(primary_key=True, serialize=False, unique=True)), ('name', models.CharField(max_length=50, unique=True)), + ('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')), ('slug', access.fields.AutoSlugField()), ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), ('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), + ('manager', models.ForeignKey(help_text='Organization Manager', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), ], options={ 'verbose_name_plural': 'Organizations', @@ -37,10 +40,11 @@ class Migration(migrations.Migration): fields=[ ('group_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='auth.group')), ('is_global', models.BooleanField(default=False)), + ('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')), ('team_name', models.CharField(default='', max_length=50, verbose_name='Name')), ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), ('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), - ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='access.organization')), + ('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])), ], options={ 'verbose_name_plural': 'Teams', diff --git a/app/access/migrations/0002_alter_team_organization.py b/app/access/migrations/0002_alter_team_organization.py deleted file mode 100644 index fc091978..00000000 --- a/app/access/migrations/0002_alter_team_organization.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.0.6 on 2024-05-23 10:37 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('access', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='team', - name='organization', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization'), - ), - ] diff --git a/app/access/migrations/0003_alter_team_organization.py b/app/access/migrations/0003_alter_team_organization.py deleted file mode 100644 index 624955b4..00000000 --- a/app/access/migrations/0003_alter_team_organization.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 5.0.6 on 2024-06-05 09:16 - -import access.models -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('access', '0002_alter_team_organization'), - ] - - operations = [ - migrations.AlterField( - model_name='team', - name='organization', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists]), - ), - ] diff --git a/app/access/migrations/0004_team_model_notes.py b/app/access/migrations/0004_team_model_notes.py deleted file mode 100644 index cbcf9e7c..00000000 --- a/app/access/migrations/0004_team_model_notes.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.0.6 on 2024-06-11 20:14 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('access', '0003_alter_team_organization'), - ] - - operations = [ - migrations.AddField( - model_name='team', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True), - ), - ] diff --git a/app/access/migrations/0005_organization_manager_organization_model_notes.py b/app/access/migrations/0005_organization_manager_organization_model_notes.py deleted file mode 100644 index 1a5d1d9a..00000000 --- a/app/access/migrations/0005_organization_manager_organization_model_notes.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 5.0.6 on 2024-06-17 10:03 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('access', '0004_team_model_notes'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AddField( - model_name='organization', - name='manager', - field=models.ForeignKey(help_text='Organization Manager', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='organization', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True), - ), - ] diff --git a/app/access/migrations/0006_alter_organization_model_notes_and_more.py b/app/access/migrations/0006_alter_organization_model_notes_and_more.py deleted file mode 100644 index f7c4dd95..00000000 --- a/app/access/migrations/0006_alter_organization_model_notes_and_more.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.0.6 on 2024-07-11 04:26 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('access', '0005_organization_manager_organization_model_notes'), - ] - - operations = [ - migrations.AlterField( - model_name='organization', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'), - ), - migrations.AlterField( - model_name='team', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'), - ), - ] diff --git a/app/api/migrations/0001_initial.py b/app/api/migrations/0001_initial.py index 9789f115..66d2b7a2 100644 --- a/app/api/migrations/0001_initial.py +++ b/app/api/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.6 on 2024-06-27 18:25 +# Generated by Django 5.0.7 on 2024-07-12 03:54 import access.fields import django.db.models.deletion diff --git a/app/config_management/migrations/0001_initial.py b/app/config_management/migrations/0001_initial.py index 71bbd5a9..810f64b3 100644 --- a/app/config_management/migrations/0001_initial.py +++ b/app/config_management/migrations/0001_initial.py @@ -1,6 +1,8 @@ -# Generated by Django 5.0.6 on 2024-06-02 14:48 +# Generated by Django 5.0.7 on 2024-07-12 03:54 import access.fields +import access.models +import config_management.models.groups import django.db.models.deletion import django.utils.timezone from django.db import migrations, models @@ -11,7 +13,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('access', '0002_alter_team_organization'), + ('access', '0001_initial'), ] operations = [ @@ -19,16 +21,17 @@ class Migration(migrations.Migration): name='ConfigGroups', fields=[ ('is_global', models.BooleanField(default=False)), + ('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')), ('id', models.AutoField(primary_key=True, serialize=False, unique=True)), ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), ('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), ('name', models.CharField(max_length=50)), - ('config', models.JSONField(blank=True, default=None, null=True)), - ('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization')), + ('config', models.JSONField(blank=True, default=None, null=True, validators=[config_management.models.groups.ConfigGroups.validate_config_keys_not_reserved])), + ('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])), ('parent', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='config_management.configgroups')), ], options={ - 'verbose_name': 'Config Groups', + 'abstract': False, }, ), ] diff --git a/app/config_management/migrations/0002_alter_configgroups_options_alter_configgroups_config_and_more.py b/app/config_management/migrations/0002_alter_configgroups_options_alter_configgroups_config_and_more.py deleted file mode 100644 index c1b9741f..00000000 --- a/app/config_management/migrations/0002_alter_configgroups_options_alter_configgroups_config_and_more.py +++ /dev/null @@ -1,43 +0,0 @@ -# Generated by Django 5.0.6 on 2024-06-02 20:51 - -import access.fields -import config_management.models.groups -import django.db.models.deletion -import django.utils.timezone -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('access', '0002_alter_team_organization'), - ('config_management', '0001_initial'), - ('itam', '0012_alter_device_serial_number_alter_device_uuid'), - ] - - operations = [ - migrations.AlterModelOptions( - name='configgroups', - options={}, - ), - migrations.AlterField( - model_name='configgroups', - name='config', - field=models.JSONField(blank=True, default=None, null=True, validators=[config_management.models.groups.ConfigGroups.validate_config_keys_not_reserved]), - ), - migrations.CreateModel( - name='ConfigGroupHosts', - fields=[ - ('is_global', models.BooleanField(default=False)), - ('id', models.AutoField(primary_key=True, serialize=False, unique=True)), - ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), - ('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), - ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='config_management.configgroups')), - ('host', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='itam.device', validators=[config_management.models.groups.ConfigGroupHosts.validate_host_no_parent_group])), - ('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization')), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/app/config_management/migrations/0004_configgroupsoftware.py b/app/config_management/migrations/0002_configgrouphosts_configgroupsoftware.py similarity index 51% rename from app/config_management/migrations/0004_configgroupsoftware.py rename to app/config_management/migrations/0002_configgrouphosts_configgroupsoftware.py index 611b6be2..19318d52 100644 --- a/app/config_management/migrations/0004_configgroupsoftware.py +++ b/app/config_management/migrations/0002_configgrouphosts_configgroupsoftware.py @@ -1,7 +1,8 @@ -# Generated by Django 5.0.6 on 2024-06-07 21:43 +# Generated by Django 5.0.7 on 2024-07-12 03:58 import access.fields import access.models +import config_management.models.groups import django.db.models.deletion import django.utils.timezone from django.db import migrations, models @@ -10,16 +11,33 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('access', '0003_alter_team_organization'), - ('config_management', '0003_alter_configgrouphosts_organization_and_more'), - ('itam', '0013_alter_device_organization_and_more'), + ('access', '0001_initial'), + ('config_management', '0001_initial'), + ('itam', '0001_initial'), ] operations = [ + migrations.CreateModel( + name='ConfigGroupHosts', + fields=[ + ('is_global', models.BooleanField(default=False)), + ('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')), + ('id', models.AutoField(primary_key=True, serialize=False, unique=True)), + ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), + ('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), + ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='config_management.configgroups')), + ('host', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='itam.device', validators=[config_management.models.groups.ConfigGroupHosts.validate_host_no_parent_group])), + ('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])), + ], + options={ + 'abstract': False, + }, + ), migrations.CreateModel( name='ConfigGroupSoftware', fields=[ ('is_global', models.BooleanField(default=False)), + ('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')), ('id', models.AutoField(primary_key=True, serialize=False, unique=True)), ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), ('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), diff --git a/app/config_management/migrations/0003_alter_configgrouphosts_organization_and_more.py b/app/config_management/migrations/0003_alter_configgrouphosts_organization_and_more.py deleted file mode 100644 index 2bc04b9e..00000000 --- a/app/config_management/migrations/0003_alter_configgrouphosts_organization_and_more.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 5.0.6 on 2024-06-05 09:16 - -import access.models -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('access', '0003_alter_team_organization'), - ('config_management', '0002_alter_configgroups_options_alter_configgroups_config_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='configgrouphosts', - name='organization', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists]), - ), - migrations.AlterField( - model_name='configgroups', - name='organization', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists]), - ), - ] diff --git a/app/config_management/migrations/0005_configgrouphosts_model_notes_and_more.py b/app/config_management/migrations/0005_configgrouphosts_model_notes_and_more.py deleted file mode 100644 index b1a52715..00000000 --- a/app/config_management/migrations/0005_configgrouphosts_model_notes_and_more.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 5.0.6 on 2024-06-11 20:14 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('config_management', '0004_configgroupsoftware'), - ] - - operations = [ - migrations.AddField( - model_name='configgrouphosts', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True), - ), - migrations.AddField( - model_name='configgroups', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True), - ), - migrations.AddField( - model_name='configgroupsoftware', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True), - ), - ] diff --git a/app/config_management/migrations/0006_alter_configgrouphosts_model_notes_and_more.py b/app/config_management/migrations/0006_alter_configgrouphosts_model_notes_and_more.py deleted file mode 100644 index d1b8f187..00000000 --- a/app/config_management/migrations/0006_alter_configgrouphosts_model_notes_and_more.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 5.0.6 on 2024-07-11 04:26 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('config_management', '0005_configgrouphosts_model_notes_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='configgrouphosts', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'), - ), - migrations.AlterField( - model_name='configgroups', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'), - ), - migrations.AlterField( - model_name='configgroupsoftware', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'), - ), - ] diff --git a/app/core/migrations/0001_initial.py b/app/core/migrations/0001_initial.py index 300d1c9e..17fd0a89 100644 --- a/app/core/migrations/0001_initial.py +++ b/app/core/migrations/0001_initial.py @@ -1,6 +1,7 @@ -# Generated by Django 5.0.6 on 2024-05-20 15:44 +# Generated by Django 5.0.7 on 2024-07-12 03:54 import access.fields +import access.models import django.db.models.deletion import django.utils.timezone from django.conf import settings @@ -13,29 +14,43 @@ class Migration(migrations.Migration): dependencies = [ ('access', '0001_initial'), - ('itam', '0007_device_inventorydate'), + ('config_management', '0001_initial'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='Notes', + name='History', fields=[ - ('is_global', models.BooleanField(default=False)), ('id', models.AutoField(primary_key=True, serialize=False, unique=True)), ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), - ('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), - ('serial_number', models.CharField(blank=True, default=None, max_length=50, null=True, verbose_name='Serial Number')), - ('note', models.TextField(default=None, null=True, verbose_name='Note')), - ('device', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='itam.device')), - ('operatingsystem', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='itam.operatingsystem')), - ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='access.organization')), - ('software', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='itam.software')), - ('usercreated', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='usercreated', to=settings.AUTH_USER_MODEL, verbose_name='Added By')), - ('usermodified', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='usermodified', to=settings.AUTH_USER_MODEL, verbose_name='Edited By')), + ('before', models.TextField(blank=True, default=None, help_text='JSON Object before Change', null=True)), + ('after', models.TextField(blank=True, default=None, help_text='JSON Object After Change', null=True)), + ('action', models.IntegerField(choices=[('1', 'Create'), ('2', 'Update'), ('3', 'Delete')], default=None, null=True)), + ('item_pk', models.IntegerField(default=None, null=True)), + ('item_class', models.CharField(default=None, max_length=50, null=True)), + ('item_parent_pk', models.IntegerField(default=None, null=True)), + ('item_parent_class', models.CharField(default=None, max_length=50, null=True)), + ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL)), ], options={ 'ordering': ['-created'], }, ), + migrations.CreateModel( + name='Manufacturer', + fields=[ + ('is_global', models.BooleanField(default=False)), + ('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')), + ('id', models.AutoField(primary_key=True, serialize=False, unique=True)), + ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), + ('modified', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), + ('name', models.CharField(max_length=50, unique=True)), + ('slug', access.fields.AutoSlugField()), + ('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])), + ], + options={ + 'ordering': ['name'], + }, + ), ] diff --git a/app/core/migrations/0002_notes.py b/app/core/migrations/0002_notes.py new file mode 100644 index 00000000..13c0a237 --- /dev/null +++ b/app/core/migrations/0002_notes.py @@ -0,0 +1,43 @@ +# Generated by Django 5.0.7 on 2024-07-12 03:58 + +import access.fields +import access.models +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('access', '0001_initial'), + ('config_management', '0002_configgrouphosts_configgroupsoftware'), + ('core', '0001_initial'), + ('itam', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Notes', + fields=[ + ('is_global', models.BooleanField(default=False)), + ('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')), + ('id', models.AutoField(primary_key=True, serialize=False, unique=True)), + ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), + ('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), + ('note', models.TextField(blank=True, default=None, null=True, verbose_name='Note')), + ('config_group', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='config_management.configgroups')), + ('device', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.device')), + ('operatingsystem', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.operatingsystem')), + ('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])), + ('software', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.software')), + ('usercreated', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='usercreated', to=settings.AUTH_USER_MODEL, verbose_name='Added By')), + ('usermodified', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='usermodified', to=settings.AUTH_USER_MODEL, verbose_name='Edited By')), + ], + options={ + 'ordering': ['-created'], + }, + ), + ] diff --git a/app/core/migrations/0002_remove_notes_serial_number_alter_notes_device_and_more.py b/app/core/migrations/0002_remove_notes_serial_number_alter_notes_device_and_more.py deleted file mode 100644 index 0b9e71c5..00000000 --- a/app/core/migrations/0002_remove_notes_serial_number_alter_notes_device_and_more.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 5.0.6 on 2024-05-20 16:06 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0001_initial'), - ('itam', '0007_device_inventorydate'), - ] - - operations = [ - migrations.RemoveField( - model_name='notes', - name='serial_number', - ), - migrations.AlterField( - model_name='notes', - name='device', - field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.device'), - ), - migrations.AlterField( - model_name='notes', - name='operatingsystem', - field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.operatingsystem'), - ), - migrations.AlterField( - model_name='notes', - name='software', - field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.software'), - ), - ] diff --git a/app/core/migrations/0003_alter_notes_note_history.py b/app/core/migrations/0003_alter_notes_note_history.py deleted file mode 100644 index 786b3add..00000000 --- a/app/core/migrations/0003_alter_notes_note_history.py +++ /dev/null @@ -1,41 +0,0 @@ -# Generated by Django 5.0.6 on 2024-05-23 03:59 - -import access.fields -import django.db.models.deletion -import django.utils.timezone -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0002_remove_notes_serial_number_alter_notes_device_and_more'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AlterField( - model_name='notes', - name='note', - field=models.TextField(blank=True, default=None, null=True, verbose_name='Note'), - ), - migrations.CreateModel( - name='History', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False, unique=True)), - ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), - ('before', models.TextField(blank=True, default=None, help_text='JSON Object before Change', null=True)), - ('after', models.TextField(blank=True, default=None, help_text='JSON Object After Change', null=True)), - ('action', models.IntegerField(choices=[('1', 'Create'), ('2', 'Update'), ('3', 'Delete')], default=None, null=True)), - ('item_pk', models.IntegerField(default=None, null=True)), - ('item_class', models.CharField(default=None, max_length=50, null=True)), - ('item_parent_pk', models.IntegerField(default=None, null=True)), - ('item_parent_class', models.CharField(default=None, max_length=50, null=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL)), - ], - options={ - 'ordering': ['-created'], - }, - ), - ] diff --git a/app/core/migrations/0004_notes_is_null.py b/app/core/migrations/0004_notes_is_null.py deleted file mode 100644 index b9ee8ffb..00000000 --- a/app/core/migrations/0004_notes_is_null.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 5.0.6 on 2024-05-23 10:37 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('access', '0002_alter_team_organization'), - ('core', '0003_alter_notes_note_history'), - ] - - operations = [ - migrations.AlterField( - model_name='notes', - name='organization', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization'), - ), - ] diff --git a/app/core/migrations/0005_manufacturer.py b/app/core/migrations/0005_manufacturer.py deleted file mode 100644 index 64a5ed12..00000000 --- a/app/core/migrations/0005_manufacturer.py +++ /dev/null @@ -1,32 +0,0 @@ -# Generated by Django 5.0.6 on 2024-05-23 10:58 - -import access.fields -import django.db.models.deletion -import django.utils.timezone -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('access', '0002_alter_team_organization'), - ('core', '0004_notes_is_null'), - ] - - operations = [ - migrations.CreateModel( - name='Manufacturer', - fields=[ - ('is_global', models.BooleanField(default=False)), - ('id', models.AutoField(primary_key=True, serialize=False, unique=True)), - ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), - ('modified', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), - ('name', models.CharField(max_length=50, unique=True)), - ('slug', access.fields.AutoSlugField()), - ('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization')), - ], - options={ - 'ordering': ['name'], - }, - ), - ] diff --git a/app/core/migrations/0006_alter_history_user.py b/app/core/migrations/0006_alter_history_user.py deleted file mode 100644 index 4a234b82..00000000 --- a/app/core/migrations/0006_alter_history_user.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 5.0.6 on 2024-05-25 05:21 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0005_manufacturer'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AlterField( - model_name='history', - name='user', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL), - ), - ] diff --git a/app/core/migrations/0007_notes_config_group.py b/app/core/migrations/0007_notes_config_group.py deleted file mode 100644 index f02152c2..00000000 --- a/app/core/migrations/0007_notes_config_group.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 5.0.6 on 2024-06-02 14:48 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('config_management', '0001_initial'), - ('core', '0006_alter_history_user'), - ] - - operations = [ - migrations.AddField( - model_name='notes', - name='config_group', - field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='config_management.configgroups'), - ), - ] diff --git a/app/core/migrations/0008_alter_manufacturer_organization_and_more.py b/app/core/migrations/0008_alter_manufacturer_organization_and_more.py deleted file mode 100644 index 60a5ebe7..00000000 --- a/app/core/migrations/0008_alter_manufacturer_organization_and_more.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 5.0.6 on 2024-06-05 09:16 - -import access.models -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('access', '0003_alter_team_organization'), - ('core', '0007_notes_config_group'), - ] - - operations = [ - migrations.AlterField( - model_name='manufacturer', - name='organization', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists]), - ), - migrations.AlterField( - model_name='notes', - name='organization', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists]), - ), - ] diff --git a/app/core/migrations/0009_manufacturer_model_notes_notes_model_notes.py b/app/core/migrations/0009_manufacturer_model_notes_notes_model_notes.py deleted file mode 100644 index 160f6317..00000000 --- a/app/core/migrations/0009_manufacturer_model_notes_notes_model_notes.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.0.6 on 2024-06-11 20:14 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0008_alter_manufacturer_organization_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='manufacturer', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True), - ), - migrations.AddField( - model_name='notes', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True), - ), - ] diff --git a/app/core/migrations/0010_alter_manufacturer_model_notes_and_more.py b/app/core/migrations/0010_alter_manufacturer_model_notes_and_more.py deleted file mode 100644 index 1912414c..00000000 --- a/app/core/migrations/0010_alter_manufacturer_model_notes_and_more.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.0.6 on 2024-07-11 04:26 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0009_manufacturer_model_notes_notes_model_notes'), - ] - - operations = [ - migrations.AlterField( - model_name='manufacturer', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'), - ), - migrations.AlterField( - model_name='notes', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'), - ), - ] diff --git a/app/itam/migrations/0001_initial.py b/app/itam/migrations/0001_initial.py index 5f6fc7e4..636adbe6 100644 --- a/app/itam/migrations/0001_initial.py +++ b/app/itam/migrations/0001_initial.py @@ -1,6 +1,7 @@ -# Generated by Django 5.0.6 on 2024-05-15 06:10 +# Generated by Django 5.0.7 on 2024-07-12 03:55 import access.fields +import access.models import django.db.models.deletion import django.utils.timezone from django.db import migrations, models @@ -12,19 +13,38 @@ class Migration(migrations.Migration): dependencies = [ ('access', '0001_initial'), + ('core', '0001_initial'), ] operations = [ migrations.CreateModel( - name='DeviceType', + name='DeviceModel', fields=[ ('is_global', models.BooleanField(default=False)), + ('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')), ('id', models.AutoField(primary_key=True, serialize=False, unique=True)), ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), ('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), ('name', models.CharField(max_length=50, unique=True)), ('slug', access.fields.AutoSlugField()), - ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='access.organization')), + ('manufacturer', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.manufacturer')), + ('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])), + ], + options={ + 'ordering': ['manufacturer', 'name'], + }, + ), + migrations.CreateModel( + name='DeviceType', + fields=[ + ('is_global', models.BooleanField(default=False)), + ('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')), + ('id', models.AutoField(primary_key=True, serialize=False, unique=True)), + ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), + ('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), + ('name', models.CharField(max_length=50, unique=True)), + ('slug', access.fields.AutoSlugField()), + ('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])), ], options={ 'abstract': False, @@ -34,46 +54,69 @@ class Migration(migrations.Migration): name='Device', fields=[ ('is_global', models.BooleanField(default=False)), + ('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')), ('id', models.AutoField(primary_key=True, serialize=False, unique=True)), ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), ('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), ('name', models.CharField(max_length=50, unique=True)), ('slug', access.fields.AutoSlugField()), - ('serial_number', models.CharField(blank=True, default=None, max_length=50, null=True, verbose_name='Serial Number')), - ('uuid', models.CharField(blank=True, default=None, max_length=50, null=True, verbose_name='UUID')), - ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='access.organization')), - ('device_type', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.devicetype')), + ('serial_number', models.CharField(blank=True, default=None, help_text='Serial number of the device.', max_length=50, null=True, unique=True, verbose_name='Serial Number')), + ('uuid', models.CharField(blank=True, default=None, help_text='System GUID/UUID.', max_length=50, null=True, unique=True, verbose_name='UUID')), + ('inventorydate', models.DateTimeField(blank=True, null=True, verbose_name='Last Inventory Date')), + ('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])), + ('device_model', models.ForeignKey(blank=True, default=None, help_text='Model of the device.', null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.devicemodel')), + ('device_type', models.ForeignKey(blank=True, default=None, help_text='Type of device.', null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.devicetype')), ], options={ 'abstract': False, }, ), migrations.CreateModel( - name='Software', + name='OperatingSystem', fields=[ ('is_global', models.BooleanField(default=False)), + ('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')), ('id', models.AutoField(primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(max_length=50, unique=True)), - ('slug', access.fields.AutoSlugField()), ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), ('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), - ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='access.organization')), + ('name', models.CharField(max_length=50, unique=True)), + ('slug', access.fields.AutoSlugField()), + ('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])), + ('publisher', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.manufacturer')), ], options={ 'abstract': False, }, ), migrations.CreateModel( - name='DeviceSoftware', + name='OperatingSystemVersion', fields=[ ('is_global', models.BooleanField(default=False)), + ('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')), ('id', models.AutoField(primary_key=True, serialize=False, unique=True)), ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), ('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), - ('action', models.CharField(choices=[('1', 'Install'), ('0', 'Remove')], default=None, max_length=1)), + ('name', models.CharField(max_length=50, verbose_name='Major Version')), + ('operating_system', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='itam.operatingsystem')), + ('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='DeviceOperatingSystem', + fields=[ + ('is_global', models.BooleanField(default=False)), + ('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')), + ('id', models.AutoField(primary_key=True, serialize=False, unique=True)), + ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), + ('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), + ('version', models.CharField(max_length=15, verbose_name='Installed Version')), + ('installdate', models.DateTimeField(blank=True, default=None, null=True, verbose_name='Install Date')), ('device', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='itam.device')), - ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='access.organization')), - ('software', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='itam.software')), + ('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])), + ('operating_system_version', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='itam.operatingsystemversion', verbose_name='Operating System/Version')), ], options={ 'abstract': False, @@ -83,36 +126,71 @@ class Migration(migrations.Migration): name='SoftwareCategory', fields=[ ('is_global', models.BooleanField(default=False)), + ('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')), ('id', models.AutoField(primary_key=True, serialize=False, unique=True)), ('name', models.CharField(max_length=50, unique=True)), ('slug', access.fields.AutoSlugField()), ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), ('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), - ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='access.organization')), + ('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])), ], options={ 'abstract': False, }, ), - migrations.AddField( - model_name='software', - name='category', - field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.softwarecategory'), - ), migrations.CreateModel( - name='SoftwareVersion', + name='Software', fields=[ ('is_global', models.BooleanField(default=False)), + ('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')), ('id', models.AutoField(primary_key=True, serialize=False, unique=True)), ('name', models.CharField(max_length=50, unique=True)), ('slug', access.fields.AutoSlugField()), ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), ('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), - ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='access.organization')), + ('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])), + ('publisher', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.manufacturer')), + ('category', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.softwarecategory')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='SoftwareVersion', + fields=[ + ('is_global', models.BooleanField(default=False)), + ('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')), + ('id', models.AutoField(primary_key=True, serialize=False, unique=True)), + ('slug', access.fields.AutoSlugField()), + ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), + ('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), + ('name', models.CharField(max_length=50)), + ('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])), ('software', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='itam.software')), ], options={ 'abstract': False, }, ), + migrations.CreateModel( + name='DeviceSoftware', + fields=[ + ('is_global', models.BooleanField(default=False)), + ('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')), + ('id', models.AutoField(primary_key=True, serialize=False, unique=True)), + ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), + ('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), + ('action', models.CharField(blank=True, choices=[('1', 'Install'), ('0', 'Remove')], default=None, max_length=1, null=True)), + ('installed', models.DateTimeField(blank=True, null=True, verbose_name='Install Date')), + ('device', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='itam.device')), + ('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])), + ('software', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='itam.software')), + ('installedversion', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='installedversion', to='itam.softwareversion')), + ('version', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.softwareversion')), + ], + options={ + 'ordering': ['-action', 'software'], + }, + ), ] diff --git a/app/itam/migrations/0002_alter_softwareversion_name.py b/app/itam/migrations/0002_alter_softwareversion_name.py deleted file mode 100644 index 8ddd6d20..00000000 --- a/app/itam/migrations/0002_alter_softwareversion_name.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.0.6 on 2024-05-17 10:10 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('itam', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='softwareversion', - name='name', - field=models.CharField(max_length=50), - ), - ] diff --git a/app/itam/migrations/0003_devicesoftware_version.py b/app/itam/migrations/0003_devicesoftware_version.py deleted file mode 100644 index 029e150e..00000000 --- a/app/itam/migrations/0003_devicesoftware_version.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.0.6 on 2024-05-17 10:18 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('itam', '0002_alter_softwareversion_name'), - ] - - operations = [ - migrations.AddField( - model_name='devicesoftware', - name='version', - field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.softwareversion'), - ), - ] diff --git a/app/itam/migrations/0004_operatingsystem_operatingsystemversion.py b/app/itam/migrations/0004_operatingsystem_operatingsystemversion.py deleted file mode 100644 index 5601aa97..00000000 --- a/app/itam/migrations/0004_operatingsystem_operatingsystemversion.py +++ /dev/null @@ -1,47 +0,0 @@ -# Generated by Django 5.0.6 on 2024-05-18 08:51 - -import access.fields -import django.db.models.deletion -import django.utils.timezone -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('access', '0001_initial'), - ('itam', '0003_devicesoftware_version'), - ] - - operations = [ - migrations.CreateModel( - name='OperatingSystem', - fields=[ - ('is_global', models.BooleanField(default=False)), - ('id', models.AutoField(primary_key=True, serialize=False, unique=True)), - ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), - ('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), - ('name', models.CharField(max_length=50, unique=True)), - ('slug', access.fields.AutoSlugField()), - ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='access.organization')), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='OperatingSystemVersion', - fields=[ - ('is_global', models.BooleanField(default=False)), - ('id', models.AutoField(primary_key=True, serialize=False, unique=True)), - ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), - ('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), - ('name', models.CharField(max_length=50)), - ('operating_system', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='itam.operatingsystem')), - ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='access.organization')), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/app/itam/migrations/0005_alter_operatingsystemversion_name_and_more.py b/app/itam/migrations/0005_alter_operatingsystemversion_name_and_more.py deleted file mode 100644 index 51d9c97e..00000000 --- a/app/itam/migrations/0005_alter_operatingsystemversion_name_and_more.py +++ /dev/null @@ -1,38 +0,0 @@ -# Generated by Django 5.0.6 on 2024-05-18 15:20 - -import access.fields -import django.db.models.deletion -import django.utils.timezone -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('access', '0001_initial'), - ('itam', '0004_operatingsystem_operatingsystemversion'), - ] - - operations = [ - migrations.AlterField( - model_name='operatingsystemversion', - name='name', - field=models.CharField(max_length=50, verbose_name='Major Version'), - ), - migrations.CreateModel( - name='DeviceOperatingSystem', - fields=[ - ('is_global', models.BooleanField(default=False)), - ('id', models.AutoField(primary_key=True, serialize=False, unique=True)), - ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), - ('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), - ('version', models.CharField(max_length=15, verbose_name='Installed Version')), - ('device', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='itam.device')), - ('operating_system_version', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='itam.operatingsystemversion', verbose_name='Operating System/Version')), - ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='access.organization')), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/app/itam/migrations/0006_alter_devicesoftware_options_and_more.py b/app/itam/migrations/0006_alter_devicesoftware_options_and_more.py deleted file mode 100644 index 93bb65a2..00000000 --- a/app/itam/migrations/0006_alter_devicesoftware_options_and_more.py +++ /dev/null @@ -1,38 +0,0 @@ -# Generated by Django 5.0.6 on 2024-05-20 06:36 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('itam', '0005_alter_operatingsystemversion_name_and_more'), - ] - - operations = [ - migrations.AlterModelOptions( - name='devicesoftware', - options={'ordering': ['-action', 'software']}, - ), - migrations.AddField( - model_name='deviceoperatingsystem', - name='installdate', - field=models.DateTimeField(blank=True, default=None, null=True, verbose_name='Install Date'), - ), - migrations.AddField( - model_name='devicesoftware', - name='installed', - field=models.DateTimeField(blank=True, null=True, verbose_name='Install Date'), - ), - migrations.AddField( - model_name='devicesoftware', - name='installedversion', - field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='installedversion', to='itam.softwareversion'), - ), - migrations.AlterField( - model_name='devicesoftware', - name='action', - field=models.CharField(blank=True, choices=[('1', 'Install'), ('0', 'Remove')], default=None, max_length=1, null=True), - ), - ] diff --git a/app/itam/migrations/0007_device_inventorydate.py b/app/itam/migrations/0007_device_inventorydate.py deleted file mode 100644 index 0cb81014..00000000 --- a/app/itam/migrations/0007_device_inventorydate.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.0.6 on 2024-05-20 09:47 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('itam', '0006_alter_devicesoftware_options_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='device', - name='inventorydate', - field=models.DateTimeField(blank=True, null=True, verbose_name='Last Inventory Date'), - ), - ] diff --git a/app/itam/migrations/0008_alter_device_organization_and_more.py b/app/itam/migrations/0008_alter_device_organization_and_more.py deleted file mode 100644 index 6ea99488..00000000 --- a/app/itam/migrations/0008_alter_device_organization_and_more.py +++ /dev/null @@ -1,60 +0,0 @@ -# Generated by Django 5.0.6 on 2024-05-23 10:37 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('access', '0002_alter_team_organization'), - ('itam', '0007_device_inventorydate'), - ] - - operations = [ - migrations.AlterField( - model_name='device', - name='organization', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization'), - ), - migrations.AlterField( - model_name='deviceoperatingsystem', - name='organization', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization'), - ), - migrations.AlterField( - model_name='devicesoftware', - name='organization', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization'), - ), - migrations.AlterField( - model_name='devicetype', - name='organization', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization'), - ), - migrations.AlterField( - model_name='operatingsystem', - name='organization', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization'), - ), - migrations.AlterField( - model_name='operatingsystemversion', - name='organization', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization'), - ), - migrations.AlterField( - model_name='software', - name='organization', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization'), - ), - migrations.AlterField( - model_name='softwarecategory', - name='organization', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization'), - ), - migrations.AlterField( - model_name='softwareversion', - name='organization', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization'), - ), - ] diff --git a/app/itam/migrations/0009_devicemodel_device_device_model.py b/app/itam/migrations/0009_devicemodel_device_device_model.py deleted file mode 100644 index 7da6665f..00000000 --- a/app/itam/migrations/0009_devicemodel_device_device_model.py +++ /dev/null @@ -1,39 +0,0 @@ -# Generated by Django 5.0.6 on 2024-05-23 12:05 - -import access.fields -import django.db.models.deletion -import django.utils.timezone -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('access', '0002_alter_team_organization'), - ('core', '0005_manufacturer'), - ('itam', '0008_alter_device_organization_and_more'), - ] - - operations = [ - migrations.CreateModel( - name='DeviceModel', - fields=[ - ('is_global', models.BooleanField(default=False)), - ('id', models.AutoField(primary_key=True, serialize=False, unique=True)), - ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), - ('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), - ('name', models.CharField(max_length=50, unique=True)), - ('slug', access.fields.AutoSlugField()), - ('manufacturer', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.manufacturer')), - ('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization')), - ], - options={ - 'ordering': ['manufacturer', 'name'], - }, - ), - migrations.AddField( - model_name='device', - name='device_model', - field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.devicemodel'), - ), - ] diff --git a/app/itam/migrations/0010_operatingsystem_publisher_software_publisher.py b/app/itam/migrations/0010_operatingsystem_publisher_software_publisher.py deleted file mode 100644 index 1f5df1d6..00000000 --- a/app/itam/migrations/0010_operatingsystem_publisher_software_publisher.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 5.0.6 on 2024-05-23 12:48 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0005_manufacturer'), - ('itam', '0009_devicemodel_device_device_model'), - ] - - operations = [ - migrations.AddField( - model_name='operatingsystem', - name='publisher', - field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.manufacturer'), - ), - ] diff --git a/app/itam/migrations/0011_software_publisher.py b/app/itam/migrations/0011_software_publisher.py deleted file mode 100644 index 7f4cf073..00000000 --- a/app/itam/migrations/0011_software_publisher.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 5.0.6 on 2024-05-23 12:49 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0005_manufacturer'), - ('itam', '0010_operatingsystem_publisher_software_publisher'), - ] - - operations = [ - migrations.AddField( - model_name='software', - name='publisher', - field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.manufacturer'), - ), - ] diff --git a/app/itam/migrations/0012_alter_device_serial_number_alter_device_uuid.py b/app/itam/migrations/0012_alter_device_serial_number_alter_device_uuid.py deleted file mode 100644 index d05767a3..00000000 --- a/app/itam/migrations/0012_alter_device_serial_number_alter_device_uuid.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.0.6 on 2024-05-28 06:33 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('itam', '0011_software_publisher'), - ] - - operations = [ - migrations.AlterField( - model_name='device', - name='serial_number', - field=models.CharField(blank=True, default=None, max_length=50, null=True, unique=True, verbose_name='Serial Number'), - ), - migrations.AlterField( - model_name='device', - name='uuid', - field=models.CharField(blank=True, default=None, max_length=50, null=True, unique=True, verbose_name='UUID'), - ), - ] diff --git a/app/itam/migrations/0013_alter_device_organization_and_more.py b/app/itam/migrations/0013_alter_device_organization_and_more.py deleted file mode 100644 index 6bbd6cb6..00000000 --- a/app/itam/migrations/0013_alter_device_organization_and_more.py +++ /dev/null @@ -1,66 +0,0 @@ -# Generated by Django 5.0.6 on 2024-06-05 09:16 - -import access.models -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('access', '0003_alter_team_organization'), - ('itam', '0012_alter_device_serial_number_alter_device_uuid'), - ] - - operations = [ - migrations.AlterField( - model_name='device', - name='organization', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists]), - ), - migrations.AlterField( - model_name='devicemodel', - name='organization', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists]), - ), - migrations.AlterField( - model_name='deviceoperatingsystem', - name='organization', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists]), - ), - migrations.AlterField( - model_name='devicesoftware', - name='organization', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists]), - ), - migrations.AlterField( - model_name='devicetype', - name='organization', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists]), - ), - migrations.AlterField( - model_name='operatingsystem', - name='organization', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists]), - ), - migrations.AlterField( - model_name='operatingsystemversion', - name='organization', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists]), - ), - migrations.AlterField( - model_name='software', - name='organization', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists]), - ), - migrations.AlterField( - model_name='softwarecategory', - name='organization', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists]), - ), - migrations.AlterField( - model_name='softwareversion', - name='organization', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists]), - ), - ] diff --git a/app/itam/migrations/0014_device_model_notes_devicemodel_model_notes_and_more.py b/app/itam/migrations/0014_device_model_notes_devicemodel_model_notes_and_more.py deleted file mode 100644 index fedebefa..00000000 --- a/app/itam/migrations/0014_device_model_notes_devicemodel_model_notes_and_more.py +++ /dev/null @@ -1,63 +0,0 @@ -# Generated by Django 5.0.6 on 2024-06-11 20:14 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('itam', '0013_alter_device_organization_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='device', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True), - ), - migrations.AddField( - model_name='devicemodel', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True), - ), - migrations.AddField( - model_name='deviceoperatingsystem', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True), - ), - migrations.AddField( - model_name='devicesoftware', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True), - ), - migrations.AddField( - model_name='devicetype', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True), - ), - migrations.AddField( - model_name='operatingsystem', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True), - ), - migrations.AddField( - model_name='operatingsystemversion', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True), - ), - migrations.AddField( - model_name='software', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True), - ), - migrations.AddField( - model_name='softwarecategory', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True), - ), - migrations.AddField( - model_name='softwareversion', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True), - ), - ] diff --git a/app/itam/migrations/0015_alter_device_device_model_alter_device_device_type_and_more.py b/app/itam/migrations/0015_alter_device_device_model_alter_device_device_type_and_more.py deleted file mode 100644 index 8eebe36a..00000000 --- a/app/itam/migrations/0015_alter_device_device_model_alter_device_device_type_and_more.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 5.0.6 on 2024-06-17 08:56 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('itam', '0014_device_model_notes_devicemodel_model_notes_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='device', - name='device_model', - field=models.ForeignKey(blank=True, default=None, help_text='Model of the device.', null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.devicemodel'), - ), - migrations.AlterField( - model_name='device', - name='device_type', - field=models.ForeignKey(blank=True, default=None, help_text='Type of device.', null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.devicetype'), - ), - migrations.AlterField( - model_name='device', - name='serial_number', - field=models.CharField(blank=True, default=None, help_text='Serial number of the device.', max_length=50, null=True, unique=True, verbose_name='Serial Number'), - ), - migrations.AlterField( - model_name='device', - name='uuid', - field=models.CharField(blank=True, default=None, help_text='System GUID/UUID.', max_length=50, null=True, unique=True, verbose_name='UUID'), - ), - ] diff --git a/app/itam/migrations/0016_alter_device_model_notes_and_more.py b/app/itam/migrations/0016_alter_device_model_notes_and_more.py deleted file mode 100644 index 4e596d4d..00000000 --- a/app/itam/migrations/0016_alter_device_model_notes_and_more.py +++ /dev/null @@ -1,63 +0,0 @@ -# Generated by Django 5.0.6 on 2024-07-11 04:26 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('itam', '0015_alter_device_device_model_alter_device_device_type_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='device', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'), - ), - migrations.AlterField( - model_name='devicemodel', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'), - ), - migrations.AlterField( - model_name='deviceoperatingsystem', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'), - ), - migrations.AlterField( - model_name='devicesoftware', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'), - ), - migrations.AlterField( - model_name='devicetype', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'), - ), - migrations.AlterField( - model_name='operatingsystem', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'), - ), - migrations.AlterField( - model_name='operatingsystemversion', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'), - ), - migrations.AlterField( - model_name='software', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'), - ), - migrations.AlterField( - model_name='softwarecategory', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'), - ), - migrations.AlterField( - model_name='softwareversion', - name='model_notes', - field=models.TextField(blank=True, default=None, null=True, verbose_name='Notes'), - ), - ] diff --git a/app/settings/migrations/0001_initial.py b/app/settings/migrations/0001_initial.py index a2baef56..01fc87e4 100644 --- a/app/settings/migrations/0001_initial.py +++ b/app/settings/migrations/0001_initial.py @@ -1,23 +1,84 @@ -# Generated by Django 5.0.6 on 2024-05-23 10:13 +# Generated by Django 5.0.7 on 2024-07-12 03:55 +import access.fields +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings from django.db import migrations, models +from django.contrib.auth.models import User + +from settings.models.user_settings import UserSettings + + +def add_user_settings(apps, schema_editor): + + for user in User.objects.all(): + + if not UserSettings.objects.filter(pk=user.id).exists(): + + user_setting = UserSettings.objects.create( + user=user + ) + + user_setting.save() + + + +def add_app_settings(apps, schema_editor): + + app = apps.get_model('settings', 'appsettings') + + if not app.objects.filter(owner_organization=None).exists(): + + setting = app.objects.create( + owner_organization=None + ) + + setting.save() + class Migration(migrations.Migration): initial = True dependencies = [ + ('access', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='Settings', + name='AppSettings', fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('id', models.AutoField(primary_key=True, serialize=False, unique=True)), + ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), + ('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), + ('device_model_is_global', models.BooleanField(default=False, verbose_name='All Device Models are global')), + ('device_type_is_global', models.BooleanField(default=False, verbose_name='All Device Types is global')), + ('manufacturer_is_global', models.BooleanField(default=False, verbose_name='All Manufacturer / Publishers are global')), + ('software_is_global', models.BooleanField(default=False, verbose_name='All Software is global')), + ('software_categories_is_global', models.BooleanField(default=False, verbose_name='All Software Categories are global')), + ('global_organization', models.ForeignKey(blank=True, default=None, help_text='Organization global items will be created in', null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='global_organization', to='access.organization')), + ('owner_organization', models.ForeignKey(blank=True, default=None, help_text='Organization the settings belong to', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='owner_organization', to='access.organization')), ], options={ - 'managed': False, + 'abstract': False, }, ), + migrations.CreateModel( + name='UserSettings', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False, unique=True)), + ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), + ('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), + ('default_organization', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='access.organization')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.RunPython(add_user_settings), + migrations.RunPython(add_app_settings), ] diff --git a/app/settings/migrations/0002_usersettings.py b/app/settings/migrations/0002_usersettings.py deleted file mode 100644 index d85cf365..00000000 --- a/app/settings/migrations/0002_usersettings.py +++ /dev/null @@ -1,32 +0,0 @@ -# Generated by Django 5.0.6 on 2024-05-24 23:50 - -import access.fields -import django.db.models.deletion -import django.utils.timezone -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('access', '0002_alter_team_organization'), - ('settings', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='UserSettings', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False, unique=True)), - ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), - ('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), - ('default_organization', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='access.organization')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/app/settings/migrations/0003_create_settings_for_all_users.py b/app/settings/migrations/0003_create_settings_for_all_users.py deleted file mode 100644 index c1458d23..00000000 --- a/app/settings/migrations/0003_create_settings_for_all_users.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 5.0.6 on 2024-05-24 23:19 - -import access.fields -import django.db.models.deletion -import django.utils.timezone -from django.conf import settings -from django.db import migrations, models - -from django.contrib.auth.models import User - -from settings.models.user_settings import UserSettings - -def add_user_settings(apps, schema_editor): - - for user in User.objects.all(): - - if not UserSettings.objects.filter(pk=user.id).exists(): - - user_setting = UserSettings.objects.create( - user=user - ) - - user_setting.save() - - - -class Migration(migrations.Migration): - - dependencies = [ - ('access', '0002_alter_team_organization'), - ('settings', '0002_usersettings'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.RunPython(add_user_settings), - ] diff --git a/app/settings/migrations/0004_appsettings.py b/app/settings/migrations/0004_appsettings.py deleted file mode 100644 index d1bffbd3..00000000 --- a/app/settings/migrations/0004_appsettings.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 5.0.6 on 2024-05-25 02:42 - -import access.fields -import django.db.models.deletion -import django.utils.timezone -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('access', '0002_alter_team_organization'), - ('settings', '0003_create_settings_for_all_users'), - ] - - operations = [ - migrations.CreateModel( - name='AppSettings', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False, unique=True)), - ('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), - ('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), - ('software_is_global', models.BooleanField(default=False, verbose_name='All Software is global')), - ('global_organization', models.ForeignKey(blank=True, default=None, help_text='Organization global items will be created in', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='global_organization', to='access.organization')), - ('owner_organization', models.ForeignKey(blank=True, default=None, help_text='Organization the settings belong to', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='owner_organization', to='access.organization')), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/app/settings/migrations/0005_create_settings_for_app.py b/app/settings/migrations/0005_create_settings_for_app.py deleted file mode 100644 index 7b6efd5b..00000000 --- a/app/settings/migrations/0005_create_settings_for_app.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 5.0.6 on 2024-05-24 23:19 - -import access.fields -import django.db.models.deletion -import django.utils.timezone -from django.conf import settings -from django.db import migrations, models - -from django.contrib.auth.models import User - -def add_app_settings(apps, schema_editor): - - app = apps.get_model('settings', 'appsettings') - - if not app.objects.filter(owner_organization=None).exists(): - - setting = app.objects.create( - owner_organization=None - ) - - setting.save() - - - -class Migration(migrations.Migration): - - dependencies = [ - ('access', '0002_alter_team_organization'), - ('settings', '0004_appsettings'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.RunPython(add_app_settings), - ] diff --git a/app/settings/migrations/0006_appsettings_software_categories_is_global_and_more.py b/app/settings/migrations/0006_appsettings_software_categories_is_global_and_more.py deleted file mode 100644 index 1fb32867..00000000 --- a/app/settings/migrations/0006_appsettings_software_categories_is_global_and_more.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 5.0.6 on 2024-05-27 04:16 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('access', '0002_alter_team_organization'), - ('settings', '0005_create_settings_for_app'), - ] - - operations = [ - migrations.AddField( - model_name='appsettings', - name='software_categories_is_global', - field=models.BooleanField(default=False, verbose_name='All Software Categories are global'), - ), - migrations.AlterField( - model_name='appsettings', - name='global_organization', - field=models.ForeignKey(blank=True, default=None, help_text='Organization global items will be created in', null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='global_organization', to='access.organization'), - ), - ] diff --git a/app/settings/migrations/0007_appsettings_device_model_is_global.py b/app/settings/migrations/0007_appsettings_device_model_is_global.py deleted file mode 100644 index aa08c75b..00000000 --- a/app/settings/migrations/0007_appsettings_device_model_is_global.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.0.6 on 2024-05-27 04:38 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('settings', '0006_appsettings_software_categories_is_global_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='appsettings', - name='device_model_is_global', - field=models.BooleanField(default=False, verbose_name='All Device Models are global'), - ), - ] diff --git a/app/settings/migrations/0008_appsettings_device_type_is_global.py b/app/settings/migrations/0008_appsettings_device_type_is_global.py deleted file mode 100644 index 0771af70..00000000 --- a/app/settings/migrations/0008_appsettings_device_type_is_global.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.0.6 on 2024-05-27 04:52 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('settings', '0007_appsettings_device_model_is_global'), - ] - - operations = [ - migrations.AddField( - model_name='appsettings', - name='device_type_is_global', - field=models.BooleanField(default=False, verbose_name='All Device Types is global'), - ), - ] diff --git a/app/settings/migrations/0009_appsettings_manufacturer_is_global.py b/app/settings/migrations/0009_appsettings_manufacturer_is_global.py deleted file mode 100644 index 6fb06b0f..00000000 --- a/app/settings/migrations/0009_appsettings_manufacturer_is_global.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.0.6 on 2024-05-27 05:26 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('settings', '0008_appsettings_device_type_is_global'), - ] - - operations = [ - migrations.AddField( - model_name='appsettings', - name='manufacturer_is_global', - field=models.BooleanField(default=False, verbose_name='All Manufacturer / Publishers are global'), - ), - ] diff --git a/app/settings/migrations/0010_delete_settings.py b/app/settings/migrations/0010_delete_settings.py deleted file mode 100644 index c4210515..00000000 --- a/app/settings/migrations/0010_delete_settings.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 5.0.6 on 2024-07-11 04:26 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('settings', '0009_appsettings_manufacturer_is_global'), - ] - - operations = [ - migrations.DeleteModel( - name='Settings', - ), - ] From 30e0342f5266fe8bcb6af819138eb1dbfc278eca Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 12 Jul 2024 13:36:48 +0930 Subject: [PATCH 013/123] ci: temp change to release, on dev to be alpha release !40 !35 #74 --- .gitlab-ci.yml | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0b1161e2..3218795e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -115,6 +115,95 @@ Docker Container: - when: never + + + + + +.gitlab_release: + stage: release + image: registry.gitlab.com/gitlab-org/release-cli:latest + before_script: + - if [ "0$JOB_ROOT_DIR" == "0" ]; then ROOT_DIR=gitlab-ci; else ROOT_DIR=$JOB_ROOT_DIR ; fi + - echo "[DEBUG] ROOT_DIR[$ROOT_DIR]" + - mkdir -p "$CI_PROJECT_DIR/artifacts/$CI_JOB_STAGE/$CI_JOB_NAME" + - mkdir -p "$CI_PROJECT_DIR/artifacts/$CI_JOB_STAGE/tests" + - apk update + - apk add git + - apk add --update --no-cache python3 && ln -sf python3 /usr/bin/python + - python -m ensurepip && ln -sf pip3 /usr/bin/pip + - pip install --upgrade pip + - pip install -r $ROOT_DIR/gitlab_release/requirements.txt + - pip install $ROOT_DIR/gitlab_release/python-module/cz_nfc/. + - 'CLONE_URL="https://gitlab-ci-token:$GIT_COMMIT_TOKEN@gitlab.com/$CI_PROJECT_PATH.git"' + - echo "[DEBUG] CLONE_URL[$CLONE_URL]" + - git clone -b development $CLONE_URL repo + - cd repo + - git branch + - git config --global user.email "helpdesk@nofusscomputing.com" + - git config --global user.name "nfc_bot" + - git push --set-upstream origin development + - RELEASE_VERSION_CURRENT=$(cz -n cz_nfc version --project) + script: + - "$MY_COMMAND" + - if [ "$CI_COMMIT_BRANCH" == "development" ] ; then RELEASE_CHANGELOG=$(cz -n cz_nfc bump --changelog --changelog-to-stdout --prerelease alpha); else RELEASE_CHANGELOG=$(cz -n cz_nfc bump --changelog --changelog-to-stdout); fi + - RELEASE_VERSION_NEW=$(cz -n cz_nfc version --project) + - RELEASE_TAG=$RELEASE_VERSION_NEW + - echo "[DEBUG] RELEASE_VERSION_CURRENT[$RELEASE_VERSION_CURRENT]" + - echo "[DEBUG] RELEASE_CHANGELOG[$RELEASE_CHANGELOG]" + - echo "[DEBUG] RELEASE_VERSION_NEW[$RELEASE_VERSION_NEW]" + - echo "[DEBUG] RELEASE_TAG[$RELEASE_TAG]" + - RELEASE_TAG_SHA1=$(git log -n1 --format=format:"%H") + - echo "[DEBUG] RELEASE_TAG_SHA1[$RELEASE_TAG_SHA1]" + - if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then echo "[DEBUG] No tag to delete, version was not bumped"; else git tag -d $RELEASE_TAG; fi + + - if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then echo "[DEBUG] No push will be conducted, version was not bumped"; else git push; fi + - if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then echo "[DEBUG] No release will be created, version was not bumped"; else release-cli create --name "Release $RELEASE_TAG" --tag-name "$RELEASE_TAG" --tag-message "$RELEASE_CHANGELOG" --ref "$RELEASE_TAG_SHA1" --description "$RELEASE_CHANGELOG"; fi + - if [ "$CI_COMMIT_BRANCH" == "master" ] ; then git checkout master; fi + - if [ "$CI_COMMIT_BRANCH" == "master" ] ; then git push --set-upstream origin master; fi + - if [ "$CI_COMMIT_BRANCH" == "master" ] ; then git merge --no-ff development; fi + - if [ "$CI_COMMIT_BRANCH" == "master" ] ; then git push origin master; fi + after_script: + - rm -Rf repo + rules: + - if: '$JOB_STOP_GITLAB_RELEASE' + when: never + + - if: "$CI_COMMIT_AUTHOR =='nfc_bot '" + when: never + + - if: # condition_master_branch_push + $CI_COMMIT_BRANCH == "master" && + $CI_PIPELINE_SOURCE == "push" + allow_failure: false + when: on_success + + - if: # condition_dev_branch_push + $CI_COMMIT_BRANCH == "development" && + $CI_PIPELINE_SOURCE == "push" + when: manual + allow_failure: true + + # for testing + # - if: '$CI_COMMIT_BRANCH != "master"' + # when: always + # allow_failure: true + - when: never + +# +# Release +# +Gitlab Release: + extends: + - .gitlab_release + + + + + + + + Docker.Hub.Branch.Publish: extends: .publish-docker-hub needs: [ "Docker Container" ] From a1759ecaaf7df03846bded9c673f654f3da5fe58 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 16 Jul 2024 13:39:52 +0930 Subject: [PATCH 014/123] ci: add alpha branch to docker builds and publish !40 !42 #74 --- .gitlab-ci.yml | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3218795e..07b62129 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -103,9 +103,8 @@ Docker Container: ( $CI_COMMIT_BRANCH == "development" || - $CI_COMMIT_BRANCH == "master" - ) - && + $CI_COMMIT_BRANCH == "v-1-0-0-alpha" + ) && $CI_PIPELINE_SOURCE == "push" exists: - '{dockerfile,dockerfile.j2}' @@ -181,7 +180,7 @@ Docker Container: - if: # condition_dev_branch_push $CI_COMMIT_BRANCH == "development" && $CI_PIPELINE_SOURCE == "push" - when: manual + when: always allow_failure: true # for testing @@ -229,7 +228,11 @@ Docker.Hub.Branch.Publish: when: always - if: # condition_dev_branch_push - $CI_COMMIT_BRANCH == "development" && + ( + $CI_COMMIT_BRANCH == "development" + || + $CI_COMMIT_BRANCH == "v-1-0-0-alpha" + ) && $CI_PIPELINE_SOURCE == "push" exists: - '{dockerfile,dockerfile.j2}' @@ -251,13 +254,8 @@ Github (Push --mirror): when: never - if: # condition_master_or_dev_push - ( - $CI_COMMIT_BRANCH == "master" - || - $CI_COMMIT_BRANCH == "development" - || - $CI_COMMIT_BRANCH == "14-feat-project-management" - ) && + $CI_COMMIT_BRANCH + && $CI_PIPELINE_SOURCE == "push" when: always @@ -273,7 +271,11 @@ Website.Submodule.Deploy: name: Documentation rules: - if: # condition_dev_branch_push - $CI_COMMIT_BRANCH == "development" && + ( + $CI_COMMIT_BRANCH == "development" + || + $CI_COMMIT_BRANCH == "v-1-0-0-alpha" + ) && $CI_PIPELINE_SOURCE == "push" exists: - '{docs/**,pages/**}/*.md' From 1f8244ae40abc5dd34596142a46ba02417b24357 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 16 Jul 2024 14:55:29 +0930 Subject: [PATCH 015/123] ci: use updated commitizen !42 !40 #74 --- .gitlab-ci.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 07b62129..ec016717 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -133,7 +133,8 @@ Docker Container: - python -m ensurepip && ln -sf pip3 /usr/bin/pip - pip install --upgrade pip - pip install -r $ROOT_DIR/gitlab_release/requirements.txt - - pip install $ROOT_DIR/gitlab_release/python-module/cz_nfc/. + # - pip install $ROOT_DIR/gitlab_release/python-module/cz_nfc/. + - pip install commitizen --force - 'CLONE_URL="https://gitlab-ci-token:$GIT_COMMIT_TOKEN@gitlab.com/$CI_PROJECT_PATH.git"' - echo "[DEBUG] CLONE_URL[$CLONE_URL]" - git clone -b development $CLONE_URL repo @@ -142,11 +143,11 @@ Docker Container: - git config --global user.email "helpdesk@nofusscomputing.com" - git config --global user.name "nfc_bot" - git push --set-upstream origin development - - RELEASE_VERSION_CURRENT=$(cz -n cz_nfc version --project) + - RELEASE_VERSION_CURRENT=$(cz version --project) script: - "$MY_COMMAND" - - if [ "$CI_COMMIT_BRANCH" == "development" ] ; then RELEASE_CHANGELOG=$(cz -n cz_nfc bump --changelog --changelog-to-stdout --prerelease alpha); else RELEASE_CHANGELOG=$(cz -n cz_nfc bump --changelog --changelog-to-stdout); fi - - RELEASE_VERSION_NEW=$(cz -n cz_nfc version --project) + - if [ "$CI_COMMIT_BRANCH" == "development" ] ; then RELEASE_CHANGELOG=$(cz bump --changelog --changelog-to-stdout --prerelease alpha); else RELEASE_CHANGELOG=$(cz bump --changelog --changelog-to-stdout); fi + - RELEASE_VERSION_NEW=$(cz version --project) - RELEASE_TAG=$RELEASE_VERSION_NEW - echo "[DEBUG] RELEASE_VERSION_CURRENT[$RELEASE_VERSION_CURRENT]" - echo "[DEBUG] RELEASE_CHANGELOG[$RELEASE_CHANGELOG]" From fe64c11927b592db9aa15173e66affa37844f1e7 Mon Sep 17 00:00:00 2001 From: nfc_bot Date: Tue, 16 Jul 2024 05:56:44 +0000 Subject: [PATCH 016/123] =?UTF-8?q?bump:=20version=200.7.0=20=E2=86=92=201?= =?UTF-8?q?.0.0-a1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cz.yaml | 3 ++- CHANGELOG.md | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/.cz.yaml b/.cz.yaml index 711d0d1c..b24a69ff 100644 --- a/.cz.yaml +++ b/.cz.yaml @@ -1,7 +1,8 @@ +--- commitizen: name: cz_conventional_commits prerelease_offset: 1 tag_format: $version update_changelog_on_bump: false - version: 0.7.0 + version: 1.0.0-a1 version_scheme: semver diff --git a/CHANGELOG.md b/CHANGELOG.md index 78e4e91b..f3bb41de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +## 1.0.0-a1 (2024-07-16) + +### BREAKING CHANGE + +- squashed DB migrations in preparation for v1.0 release. + +### Feat + +- Administratively set global items org/is_global field now read-only +- **access**: Add multi-tennant manager + +### Fix + +- **core**: migrate manufacturer to use new form/view logic +- **settings**: correct the permission to view manufacturers +- **access**: Correct team form fields +- **config_management**: don't exclude parent from field, only self + +### Refactor + +- Squash database migrations + ## 0.7.0 (2024-07-14) ### Bug Fixes From 416e029c23bed315de0525b7d216f89b20537a4f Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 17 Jul 2024 11:00:41 +0930 Subject: [PATCH 017/123] revert: return ci build settings to not include branch alpha partial revert of a1759ecaaf7df03846bded9c673f654f3da5fe58 !42 --- .gitlab-ci.yml | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ec016717..7973500c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -103,8 +103,9 @@ Docker Container: ( $CI_COMMIT_BRANCH == "development" || - $CI_COMMIT_BRANCH == "v-1-0-0-alpha" - ) && + $CI_COMMIT_BRANCH == "master" + ) + && $CI_PIPELINE_SOURCE == "push" exists: - '{dockerfile,dockerfile.j2}' @@ -181,7 +182,7 @@ Docker Container: - if: # condition_dev_branch_push $CI_COMMIT_BRANCH == "development" && $CI_PIPELINE_SOURCE == "push" - when: always + when: manual allow_failure: true # for testing @@ -229,11 +230,7 @@ Docker.Hub.Branch.Publish: when: always - if: # condition_dev_branch_push - ( - $CI_COMMIT_BRANCH == "development" - || - $CI_COMMIT_BRANCH == "v-1-0-0-alpha" - ) && + $CI_COMMIT_BRANCH == "development" && $CI_PIPELINE_SOURCE == "push" exists: - '{dockerfile,dockerfile.j2}' @@ -272,11 +269,7 @@ Website.Submodule.Deploy: name: Documentation rules: - if: # condition_dev_branch_push - ( - $CI_COMMIT_BRANCH == "development" - || - $CI_COMMIT_BRANCH == "v-1-0-0-alpha" - ) && + $CI_COMMIT_BRANCH == "development" && $CI_PIPELINE_SOURCE == "push" exists: - '{docs/**,pages/**}/*.md' From 60538e1cec78bf40cd966fd7d612dbf78a636452 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 17 Jul 2024 23:51:25 +0930 Subject: [PATCH 018/123] feat(base): show warning bar if the user has not set a default organization !42 fixes #133 --- app/app/context_processors/base.py | 18 ++++++++++++++++++ app/templates/base.html.j2 | 12 ++++++++++++ .../projects/centurion_erp/user/itam/device.md | 3 +++ 3 files changed, 33 insertions(+) diff --git a/app/app/context_processors/base.py b/app/app/context_processors/base.py index 57eb5521..e5da94bd 100644 --- a/app/app/context_processors/base.py +++ b/app/app/context_processors/base.py @@ -74,6 +74,23 @@ def user_settings(context) -> int: return None +def user_default_organization(context) -> int: + """ Provides the users default organization. + + Returns: + int: Users Default Organization + """ + if context.user.is_authenticated: + + settings = UserSettings.objects.filter(user=context.user) + + if settings[0].default_organization: + + return settings[0].default_organization.id + + return None + + def nav_items(context) -> list(dict()): """ Fetch All Project URLs @@ -203,4 +220,5 @@ def common(context): 'nav_items': nav_items(context), 'social_backends': social_backends(context), 'user_settings': user_settings(context), + 'user_default_organization': user_default_organization(context) } diff --git a/app/templates/base.html.j2 b/app/templates/base.html.j2 index ad20c46a..c3dd316e 100644 --- a/app/templates/base.html.j2 +++ b/app/templates/base.html.j2 @@ -76,6 +76,15 @@ section h2 span svg { fill: #177ee6; } +.warning-bar { + background-color: #f1d599; + border: 1px solid #ecb785; + height: 30px; + line-height: 30px; + width: 100%; + padding: 0px 20px 0px 20px +} +
@@ -100,6 +109,9 @@ section h2 span svg { {% endblock content_header_icon %} {% endif %} + {% if not user_default_organization %} +
You do not have a default organization set, go to user settings to set one
+ {% endif %} {% block article %}
{% block content %}{% endblock %} diff --git a/docs/projects/centurion_erp/user/itam/device.md b/docs/projects/centurion_erp/user/itam/device.md index 48cf6bee..dd8876b5 100644 --- a/docs/projects/centurion_erp/user/itam/device.md +++ b/docs/projects/centurion_erp/user/itam/device.md @@ -69,6 +69,9 @@ This configuration can also be obtained from API endpoint `/api/config/ Date: Thu, 18 Jul 2024 01:14:33 +0930 Subject: [PATCH 019/123] fix(api): correct inventory device search to be case insensitive !42 fixes #134 --- app/api/tasks.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/app/api/tasks.py b/app/api/tasks.py index 90a0d29c..8b8a54f1 100644 --- a/app/api/tasks.py +++ b/app/api/tasks.py @@ -37,13 +37,6 @@ def process_inventory(self, data, organization: int): organization = Organization.objects.get(id=organization) - if Device.objects.filter(slug=str(data.details.name).lower()).exists(): - - device = Device.objects.get(slug=str(data.details.name).lower()) - - # device = self.obj - - app_settings = AppSettings.objects.get(owner_organization = None) device_serial_number = None @@ -57,6 +50,23 @@ def process_inventory(self, data, organization: int): device_uuid = str(data.details.uuid) + + if not device: # Search for device by Name. + + device = Device.objects.filter( + name__iexact=str(data.details.name).lower() + ) + + if device.exists(): + + device = Device.objects.get( + name__iexact=str(data.details.name).lower() + ) + + else: + + device = None + if not device: # Create the device From 55197e7dcc45ce2ff823465804f11cb9a4056326 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 18 Jul 2024 01:16:34 +0930 Subject: [PATCH 020/123] fix(api): correct inventory operating system and it's linking to device wasn't updating existing device os !42 #134 --- app/api/tasks.py | 96 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 79 insertions(+), 17 deletions(-) diff --git a/app/api/tasks.py b/app/api/tasks.py index 8b8a54f1..c036d591 100644 --- a/app/api/tasks.py +++ b/app/api/tasks.py @@ -97,30 +97,72 @@ def process_inventory(self, data, organization: int): device.save() + operating_system = OperatingSystem.objects.filter( + slug=data.operating_system.name, + is_global = True + ) - if OperatingSystem.objects.filter( slug=data.operating_system.name ).exists(): + if operating_system.exists(): - operating_system = OperatingSystem.objects.get( slug=data.operating_system.name ) + operating_system = OperatingSystem.objects.get( + slug=data.operating_system.name, + is_global = True + ) - else: # Create Operating System + + else: + + operating_system = None + + + + if not operating_system: + + operating_system = OperatingSystem.objects.filter( + slug=data.operating_system.name, + organization = organization + ) + + + if operating_system.exists(): + + operating_system = OperatingSystem.objects.get( + slug=data.operating_system.name, + organization = organization + ) + + else: + + operating_system = None + + + if not operating_system: operating_system = OperatingSystem.objects.create( - name = data.operating_system.name, + slug = data.operating_system.name, organization = organization, is_global = True ) - if OperatingSystemVersion.objects.filter( name=data.operating_system.version_major, operating_system=operating_system ).exists(): + operating_system_version = OperatingSystemVersion.objects.filter( + name=data.operating_system.version_major, + operating_system=operating_system + ) + + if operating_system_version.exists(): operating_system_version = OperatingSystemVersion.objects.get( - organization = organization, - is_global = True, - name = data.operating_system.version_major, - operating_system = operating_system + name=data.operating_system.version_major, + operating_system=operating_system ) - else: # Create Operating System Version + else: + + operating_system_version = None + + + if not operating_system_version: operating_system_version = OperatingSystemVersion.objects.create( organization = organization, @@ -129,22 +171,22 @@ def process_inventory(self, data, organization: int): operating_system = operating_system, ) + device_operating_system = DeviceOperatingSystem.objects.filter( + device=device, + ) - if DeviceOperatingSystem.objects.filter( version=data.operating_system.version, device=device, operating_system_version=operating_system_version ).exists(): + if device_operating_system.exists(): device_operating_system = DeviceOperatingSystem.objects.get( device=device, - version = data.operating_system.version, - operating_system_version = operating_system_version, ) - if not device_operating_system.installdate: # Only update install date if empty + else: - device_operating_system.installdate = timezone.now() + device_operating_system = None - device_operating_system.save() - else: # Create Operating System Version + if not device_operating_system: device_operating_system = DeviceOperatingSystem.objects.create( organization = organization, @@ -154,6 +196,26 @@ def process_inventory(self, data, organization: int): installdate = timezone.now() ) + if not device_operating_system.installdate: # Only update install date if empty + + device_operating_system.installdate = timezone.now() + + device_operating_system.save() + + + if device_operating_system.operating_system_version != operating_system_version: + + device_operating_system.operating_system_version = operating_system_version + + device_operating_system.save() + + + if device_operating_system.version != data.operating_system.version: + + device_operating_system.version = data.operating_system.version + + device_operating_system.save() + if app_settings.software_is_global: From 9a94ba31e42621004a13c66fc0a5fe554432187e Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 18 Jul 2024 01:17:29 +0930 Subject: [PATCH 021/123] feat(api): Inventory matching of device first by serial number !42 #134 --- app/api/tasks.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/api/tasks.py b/app/api/tasks.py index c036d591..a82ce871 100644 --- a/app/api/tasks.py +++ b/app/api/tasks.py @@ -51,6 +51,22 @@ def process_inventory(self, data, organization: int): device_uuid = str(data.details.uuid) + if device_serial_number: # Search for device by serial number. + + device = Device.objects.filter( + serial_number__iexact=device_serial_number + ) + + if device.exists(): + + device = Device.objects.get( + serial_number__iexact=device_serial_number + ) + + else: + + device = None + if not device: # Search for device by Name. device = Device.objects.filter( From 40350d166e54283e09c2d1f404c22f5e12424c32 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 18 Jul 2024 01:17:51 +0930 Subject: [PATCH 022/123] feat(api): Inventory matching of device second by uuid !42 #134 --- app/api/tasks.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/app/api/tasks.py b/app/api/tasks.py index a82ce871..58ec00f1 100644 --- a/app/api/tasks.py +++ b/app/api/tasks.py @@ -67,6 +67,24 @@ def process_inventory(self, data, organization: int): device = None + + if device_uuid and not device: # Search for device by UUID. + + device = Device.objects.filter( + uuid__iexact=device_uuid + ) + + if device.exists(): + + device = Device.objects.get( + uuid__iexact=device_uuid + ) + + else: + + device = None + + if not device: # Search for device by Name. device = Device.objects.filter( @@ -83,10 +101,11 @@ def process_inventory(self, data, organization: int): device = None + + + if not device: # Create the device - - device = Device.objects.create( name = data.details.name, device_type = None, From 5bc5a4b065f3ab8f4334c7526871133665c5e4e5 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 18 Jul 2024 01:28:22 +0930 Subject: [PATCH 023/123] docs(worker): add worker and task logs !42 fixes #135 --- docs/projects/centurion_erp/user/core/index.md | 5 +++++ docs/projects/centurion_erp/user/itam/device.md | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/docs/projects/centurion_erp/user/core/index.md b/docs/projects/centurion_erp/user/core/index.md index a197ccad..2126b873 100644 --- a/docs/projects/centurion_erp/user/core/index.md +++ b/docs/projects/centurion_erp/user/core/index.md @@ -17,3 +17,8 @@ The core module contains items that are relevant across multiple modules. ## Manufacturers A manufacturer is an entity that creates an item. Within the IT world a manufacturer can also be known as a publisher, this is in the case of software. To add a new manufacturer navigate to `settings -> Common -> Manufacturers / Publishers` + + +## Background worker + +Centurion ERP has a background worker. This worker relies upon RabbitMQ as the broker for storing and routing tasks to workers. Task logs for the jobs ran can be found by navigating to `Settings -> Application -> Task Logs`. diff --git a/docs/projects/centurion_erp/user/itam/device.md b/docs/projects/centurion_erp/user/itam/device.md index dd8876b5..6b55f236 100644 --- a/docs/projects/centurion_erp/user/itam/device.md +++ b/docs/projects/centurion_erp/user/itam/device.md @@ -74,6 +74,11 @@ This configuration can also be obtained from API endpoint `/api/config/ Application -> Task Logs` + The report can contain the following information: - device: From 8457f15eca94659052c0a5277691a091f26554b7 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 18 Jul 2024 01:48:21 +0930 Subject: [PATCH 024/123] fix(api): correct inventory operating system selection by name !42 #134 --- app/api/tasks.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/api/tasks.py b/app/api/tasks.py index 58ec00f1..1adfbb6b 100644 --- a/app/api/tasks.py +++ b/app/api/tasks.py @@ -133,14 +133,14 @@ def process_inventory(self, data, organization: int): device.save() operating_system = OperatingSystem.objects.filter( - slug=data.operating_system.name, + name=data.operating_system.name, is_global = True ) if operating_system.exists(): operating_system = OperatingSystem.objects.get( - slug=data.operating_system.name, + name=data.operating_system.name, is_global = True ) @@ -154,7 +154,7 @@ def process_inventory(self, data, organization: int): if not operating_system: operating_system = OperatingSystem.objects.filter( - slug=data.operating_system.name, + name=data.operating_system.name, organization = organization ) @@ -162,7 +162,7 @@ def process_inventory(self, data, organization: int): if operating_system.exists(): operating_system = OperatingSystem.objects.get( - slug=data.operating_system.name, + name=data.operating_system.name, organization = organization ) @@ -174,7 +174,7 @@ def process_inventory(self, data, organization: int): if not operating_system: operating_system = OperatingSystem.objects.create( - slug = data.operating_system.name, + name = data.operating_system.name, organization = organization, is_global = True ) From 5c7436084283f897d4aba6665214d7781b799460 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 18 Jul 2024 01:48:57 +0930 Subject: [PATCH 025/123] fix(base): dont show user warning bar for non-authenticated user !42 --- app/templates/base.html.j2 | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/templates/base.html.j2 b/app/templates/base.html.j2 index c3dd316e..9ed7c9ec 100644 --- a/app/templates/base.html.j2 +++ b/app/templates/base.html.j2 @@ -109,8 +109,10 @@ section h2 span svg { {% endblock content_header_icon %} {% endif %} - {% if not user_default_organization %} -
You do not have a default organization set, go to user settings to set one
+ {% if user.is_authenticated %} + {% if not user_default_organization %} +
You do not have a default organization set, go to user settings to set one
+ {% endif %} {% endif %} {% block article %}
From fa2b90ee7b271098ceb80cf46b2c233173412bea Mon Sep 17 00:00:00 2001 From: nfc_bot Date: Wed, 17 Jul 2024 16:53:14 +0000 Subject: [PATCH 026/123] =?UTF-8?q?bump:=20version=201.0.0-a1=20=E2=86=92?= =?UTF-8?q?=201.0.0-a2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cz.yaml | 2 +- CHANGELOG.md | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.cz.yaml b/.cz.yaml index b24a69ff..e905d417 100644 --- a/.cz.yaml +++ b/.cz.yaml @@ -4,5 +4,5 @@ commitizen: prerelease_offset: 1 tag_format: $version update_changelog_on_bump: false - version: 1.0.0-a1 + version: 1.0.0-a2 version_scheme: semver diff --git a/CHANGELOG.md b/CHANGELOG.md index f3bb41de..e0bedb16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +## 1.0.0-a2 (2024-07-17) + +### Feat + +- **api**: Inventory matching of device second by uuid +- **api**: Inventory matching of device first by serial number +- **base**: show warning bar if the user has not set a default organization + +### Fix + +- **base**: dont show user warning bar for non-authenticated user +- **api**: correct inventory operating system selection by name +- **api**: correct inventory operating system and it's linking to device +- **api**: correct inventory device search to be case insensitive + ## 1.0.0-a1 (2024-07-16) ### BREAKING CHANGE From a5a58742110c8c61d9d90e6de04cb765cfba17d7 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 18 Jul 2024 14:30:57 +0930 Subject: [PATCH 027/123] fix(itam): When changing device organization move related items too. !42 fixes #137 --- app/itam/models/device.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/app/itam/models/device.py b/app/itam/models/device.py index 4632d9b3..da17c1b3 100644 --- a/app/itam/models/device.py +++ b/app/itam/models/device.py @@ -76,7 +76,6 @@ class Device(DeviceCommonFieldsName, SaveHistory): null = True, blank= True, help_text = 'Type of device.', - ) @@ -86,6 +85,36 @@ class Device(DeviceCommonFieldsName, SaveHistory): blank = True, ) + def save( + self, force_insert=False, force_update=False, using=None, update_fields=None + ): + """ Save Device Model + + After saving the device update the related items so that they are a part + of the same organization as the device. + """ + + super().save( + force_insert=False, force_update=False, using=None, update_fields=None + ) + + models_to_update =[ + DeviceSoftware, + DeviceOperatingSystem + ] + + for update_model in models_to_update: + + obj = update_model.objects.filter( + device = self.id, + ) + + if obj.exists(): + + obj.update( + organization = self.organization, + ) + def __str__(self): From 519277e18bb1cde9194730af84887f64e0170feb Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 18 Jul 2024 14:32:03 +0930 Subject: [PATCH 028/123] fix(itam): Device related items should not be global. !42 --- app/itam/models/device.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/app/itam/models/device.py b/app/itam/models/device.py index da17c1b3..f4504c9b 100644 --- a/app/itam/models/device.py +++ b/app/itam/models/device.py @@ -112,6 +112,7 @@ class Device(DeviceCommonFieldsName, SaveHistory): if obj.exists(): obj.update( + is_global = False, organization = self.organization, ) @@ -287,6 +288,16 @@ class DeviceSoftware(DeviceCommonFields, SaveHistory): return self.device + def save( + self, force_insert=False, force_update=False, using=None, update_fields=None + ): + + self.is_global = False + + super().save( + force_insert=False, force_update=False, using=None, update_fields=None + ) + class DeviceOperatingSystem(DeviceCommonFields, SaveHistory): @@ -329,3 +340,14 @@ class DeviceOperatingSystem(DeviceCommonFields, SaveHistory): """ Fetch the parent object """ return self.device + + + def save( + self, force_insert=False, force_update=False, using=None, update_fields=None + ): + + self.is_global = False + + super().save( + force_insert=False, force_update=False, using=None, update_fields=None + ) From ebc266010afdc3b68b235fe32c6d11ea9f0331f8 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 18 Jul 2024 15:02:26 +0930 Subject: [PATCH 029/123] feat(itam): On device organization change remove config groups !42 --- app/itam/models/device.py | 6 ++++++ docs/projects/centurion_erp/user/itam/device.md | 3 +++ 2 files changed, 9 insertions(+) diff --git a/app/itam/models/device.py b/app/itam/models/device.py index f4504c9b..31407bec 100644 --- a/app/itam/models/device.py +++ b/app/itam/models/device.py @@ -116,6 +116,12 @@ class Device(DeviceCommonFieldsName, SaveHistory): organization = self.organization, ) + from config_management.models.groups import ConfigGroupHosts + + ConfigGroupHosts.objects.filter( + host = self.id, + ).delete() + def __str__(self): diff --git a/docs/projects/centurion_erp/user/itam/device.md b/docs/projects/centurion_erp/user/itam/device.md index 6b55f236..e814137b 100644 --- a/docs/projects/centurion_erp/user/itam/device.md +++ b/docs/projects/centurion_erp/user/itam/device.md @@ -41,6 +41,9 @@ To add a new model navigate to `settings -> ITAM -> Device Models` Operating System is also visible on this tab with the version `name` as intended to be full [semver](https://semver.org/). +!!! note + If you change the devices organization the config groups the device is a part of will be removed. + ### Software From e84e80cd8f25b30a7b50e42a3abb0290393b560c Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 18 Jul 2024 15:06:18 +0930 Subject: [PATCH 030/123] feat(config_management): Prevent a config group from being able to change organization !42 --- app/config_management/models/groups.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/config_management/models/groups.py b/app/config_management/models/groups.py index 597516a1..b237b090 100644 --- a/app/config_management/models/groups.py +++ b/app/config_management/models/groups.py @@ -186,6 +186,14 @@ class ConfigGroups(GroupsCommonFields, SaveHistory): if self.parent: self.organization = ConfigGroups.objects.get(id=self.parent.id).organization + + obj = ConfigGroups.objects.get( + id = self.id, + ) + + # Prevent organization change. ToDo: add feature so that config can change organizations + self.organization = obj.organization + super().save(*args, **kwargs) From 9a1ca7a1044784b7461881ce321b5a54e144d016 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 18 Jul 2024 15:12:19 +0930 Subject: [PATCH 031/123] fix(itam): remove org filter for software so that user can see installations not required as org filtering is done as part of the initial queryset within the model. !42 --- app/itam/views/software.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/itam/views/software.py b/app/itam/views/software.py index 4ed997bf..a80767e3 100644 --- a/app/itam/views/software.py +++ b/app/itam/views/software.py @@ -1,5 +1,5 @@ from django.contrib.auth import decorators as auth_decorator -from django.db.models import Count, Q +from django.db.models import Count from django.urls import reverse from django.utils.decorators import method_decorator @@ -47,7 +47,7 @@ class IndexView(IndexView): else: - return Software.objects.filter(Q(organization__in=self.user_organizations()) | Q(is_global = True)).order_by('name') + return Software.objects.filter().order_by('name') @@ -98,9 +98,9 @@ class View(ChangeView): ) elif not self.request.user.is_superuser: + context['device_software'] = DeviceSoftware.objects.filter( - Q(device__in=self.user_organizations(), - software=self.kwargs['pk']) + software=self.kwargs['pk'] ).order_by( 'device', 'organization' From adefbf39605a7e49cb71348d9b6d94ad01912614 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 18 Jul 2024 15:16:54 +0930 Subject: [PATCH 032/123] fix(itam): remove org filter for operating systems so that user can see installations not required as org filtering is done as part of the initial queryset within the model. !42 --- app/itam/views/operating_system.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/itam/views/operating_system.py b/app/itam/views/operating_system.py index 4859c07c..5d60f550 100644 --- a/app/itam/views/operating_system.py +++ b/app/itam/views/operating_system.py @@ -1,5 +1,5 @@ from django.contrib.auth import decorators as auth_decorator -from django.db.models import Q, Count +from django.db.models import Count from django.urls import reverse from django.utils.decorators import method_decorator @@ -40,7 +40,7 @@ class IndexView(IndexView): else: - return OperatingSystem.objects.filter(Q(organization__in=self.user_organizations()) | Q(is_global = True)).order_by('name') + return OperatingSystem.objects.filter().order_by('name') From a3be95013cb9bc06114fe2d390f9fe4fb22a866f Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 18 Jul 2024 15:18:26 +0930 Subject: [PATCH 033/123] fix(itam): remove org filter for device so that user can see installations not required as org filtering is done as part of the initial queryset within the model. !42 --- app/itam/views/device.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/itam/views/device.py b/app/itam/views/device.py index 5d6661c5..3b54a5a1 100644 --- a/app/itam/views/device.py +++ b/app/itam/views/device.py @@ -3,7 +3,6 @@ import markdown from django.contrib.auth import decorators as auth_decorator from django.core.paginator import Paginator -from django.db.models import Q from django.http import HttpResponseRedirect from django.urls import reverse from django.utils.decorators import method_decorator @@ -61,7 +60,7 @@ class IndexView(IndexView): else: - return Device.objects.filter(Q(organization__in=self.user_organizations()) | Q(is_global = True)).order_by('name') + return Device.objects.filter().order_by('name') From 7f225784c20886f12df0cb7e0743ae037d0cd9e8 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 18 Jul 2024 15:22:47 +0930 Subject: [PATCH 034/123] chore(settings): remove org filter as its not required not required as org filtering is done as part of the initial queryset within the model. !42 --- app/settings/views/device_models.py | 9 +-------- app/settings/views/device_types.py | 10 +--------- app/settings/views/manufacturer.py | 1 - app/settings/views/software_categories.py | 8 +------- 4 files changed, 3 insertions(+), 25 deletions(-) diff --git a/app/settings/views/device_models.py b/app/settings/views/device_models.py index dad3b639..9dd7c5c8 100644 --- a/app/settings/views/device_models.py +++ b/app/settings/views/device_models.py @@ -1,5 +1,3 @@ -from django.db.models import Q - from core.views.common import IndexView from itam.models.device_models import DeviceModel @@ -23,13 +21,8 @@ class Index(IndexView): def get_queryset(self): - if self.request.user.is_superuser: + return self.model.objects.filter().order_by('name') - return self.model.objects.filter().order_by('name') - - else: - - return self.model.objects.filter(Q(organization__in=self.user_organizations()) | Q(is_global = True)).order_by('name') def get_context_data(self, **kwargs): diff --git a/app/settings/views/device_types.py b/app/settings/views/device_types.py index 4644d97c..aabd9d12 100644 --- a/app/settings/views/device_types.py +++ b/app/settings/views/device_types.py @@ -1,5 +1,3 @@ -from django.db.models import Q - from core.views.common import IndexView from itam.models.device import DeviceType @@ -23,13 +21,7 @@ class Index(IndexView): def get_queryset(self): - if self.request.user.is_superuser: - - return self.model.objects.filter().order_by('name') - - else: - - return self.model.objects.filter(Q(organization__in=self.user_organizations()) | Q(is_global = True)).order_by('name') + return self.model.objects.filter().order_by('name') def get_context_data(self, **kwargs): diff --git a/app/settings/views/manufacturer.py b/app/settings/views/manufacturer.py index b5c96370..90c5ab16 100644 --- a/app/settings/views/manufacturer.py +++ b/app/settings/views/manufacturer.py @@ -1,5 +1,4 @@ from django.contrib.auth import decorators as auth_decorator -from django.db.models import Q from django.urls import reverse from django.utils.decorators import method_decorator from django.views import generic diff --git a/app/settings/views/software_categories.py b/app/settings/views/software_categories.py index 9649fd94..a59c48ec 100644 --- a/app/settings/views/software_categories.py +++ b/app/settings/views/software_categories.py @@ -22,13 +22,7 @@ class Index(IndexView): def get_queryset(self): - if self.request.user.is_superuser: - - return self.model.objects.filter().order_by('name') - - else: - - return self.model.objects.filter(organization__in=self.user_organizations()).order_by('name') + return self.model.objects.filter().order_by('name') def get_context_data(self, **kwargs): From 974a208869bde962f4e20466f59af5d0514a5d34 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 18 Jul 2024 15:24:55 +0930 Subject: [PATCH 035/123] chore(config_management): remove org filter as its not required not required as org filtering is done as part of the initial queryset within the model. !42 --- app/config_management/views/groups/groups.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/app/config_management/views/groups/groups.py b/app/config_management/views/groups/groups.py index e6e950fd..5ff4e655 100644 --- a/app/config_management/views/groups/groups.py +++ b/app/config_management/views/groups/groups.py @@ -1,7 +1,6 @@ import json from django.contrib.auth import decorators as auth_decorator -from django.db.models import Count, Q from django.urls import reverse from django.utils.decorators import method_decorator @@ -47,13 +46,7 @@ class GroupIndexView(IndexView): def get_queryset(self): - if self.request.user.is_superuser: - - return self.model.objects.filter(parent=None).order_by('name') - - else: - - return self.model.objects.filter(Q(parent=None, organization__in=self.user_organizations()) | Q(parent=None, is_global = True)).order_by('name') + return self.model.objects.filter(parent=None).order_by('name') From 2d0c3a660a1dbe47eefcb97f046c0e2435e694a9 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 18 Jul 2024 15:34:02 +0930 Subject: [PATCH 036/123] fix(config_management): dont attempt to do action during save if group being created !42 --- app/config_management/models/groups.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/config_management/models/groups.py b/app/config_management/models/groups.py index b237b090..9a91d60f 100644 --- a/app/config_management/models/groups.py +++ b/app/config_management/models/groups.py @@ -186,13 +186,14 @@ class ConfigGroups(GroupsCommonFields, SaveHistory): if self.parent: self.organization = ConfigGroups.objects.get(id=self.parent.id).organization + if self.pk: - obj = ConfigGroups.objects.get( - id = self.id, - ) + obj = ConfigGroups.objects.get( + id = self.id, + ) - # Prevent organization change. ToDo: add feature so that config can change organizations - self.organization = obj.organization + # Prevent organization change. ToDo: add feature so that config can change organizations + self.organization = obj.organization super().save(*args, **kwargs) From 8d6d1d0d56567d126065b605b34372a15579e208 Mon Sep 17 00:00:00 2001 From: nfc_bot Date: Thu, 18 Jul 2024 06:25:34 +0000 Subject: [PATCH 037/123] =?UTF-8?q?bump:=20version=201.0.0-a2=20=E2=86=92?= =?UTF-8?q?=201.0.0-a3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cz.yaml | 2 +- CHANGELOG.md | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/.cz.yaml b/.cz.yaml index e905d417..c63994e1 100644 --- a/.cz.yaml +++ b/.cz.yaml @@ -4,5 +4,5 @@ commitizen: prerelease_offset: 1 tag_format: $version update_changelog_on_bump: false - version: 1.0.0-a2 + version: 1.0.0-a3 version_scheme: semver diff --git a/CHANGELOG.md b/CHANGELOG.md index e0bedb16..73485144 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +## 1.0.0-a3 (2024-07-18) + +### Feat + +- **config_management**: Prevent a config group from being able to change organization +- **itam**: On device organization change remove config groups + +### Fix + +- **config_management**: dont attempt to do action during save if group being created +- **itam**: remove org filter for device so that user can see installations +- **itam**: remove org filter for operating systems so that user can see installations +- **itam**: remove org filter for software so that user can see installations +- **itam**: Device related items should not be global. +- **itam**: When changing device organization move related items too. + ## 1.0.0-a2 (2024-07-17) ### Feat From 4f89255c4f889518795386d987bb2090ae6776b0 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 18 Jul 2024 16:51:22 +0930 Subject: [PATCH 038/123] feat(config_management): Group name to be entire breadcrumb !43 --- app/config_management/models/groups.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/config_management/models/groups.py b/app/config_management/models/groups.py index 9a91d60f..cfb80a7d 100644 --- a/app/config_management/models/groups.py +++ b/app/config_management/models/groups.py @@ -202,7 +202,7 @@ class ConfigGroups(GroupsCommonFields, SaveHistory): if self.parent: - return f'{self.parent.name} > {self.name}' + return f'{self.parent} > {self.name}' return self.name From 72ab9253d79ea44d07e5f5455d90c81d698558aa Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 18 Jul 2024 17:08:52 +0930 Subject: [PATCH 039/123] feat(api): When processing uploaded inventory and name does not match, update name to one within inventory file !43 --- app/api/tasks.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/app/api/tasks.py b/app/api/tasks.py index 1adfbb6b..db39b78b 100644 --- a/app/api/tasks.py +++ b/app/api/tasks.py @@ -119,19 +119,35 @@ def process_inventory(self, data, organization: int): logger.info(f"Device: {device.name}, Serial: {device.serial_number}, UUID: {device.uuid}") + device_edited = False + + if not device.uuid and device_uuid: device.uuid = device_uuid - device.save() + device_edited = True if not device.serial_number and device_serial_number: device.serial_number = data.details.serial_number + device_edited = True + + + if str(device.name).lower() != str(data.details.name).lower(): # Update device Name + + device.name = data.details.name + + device_edited = True + + + if device_edited: + device.save() + operating_system = OperatingSystem.objects.filter( name=data.operating_system.name, is_global = True From ec1e7cca85dad5db7f01ddc546d066bf66ec2289 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 18 Jul 2024 22:04:27 +0930 Subject: [PATCH 040/123] test: placeholder for moving organization !42 #15 --- app/itam/tests/unit/device/test_device.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/itam/tests/unit/device/test_device.py b/app/itam/tests/unit/device/test_device.py index ba29202d..a9d9813d 100644 --- a/app/itam/tests/unit/device/test_device.py +++ b/app/itam/tests/unit/device/test_device.py @@ -38,6 +38,16 @@ class Device( # name = 'deviceone' # ) + @pytest.mark.skip(reason="to be written") + def test_device_move_organization(user): + """Move Organization test + + When a device moves organization, devicesoftware and devicesoftware table data + must also move organizations + """ + pass + + @pytest.mark.skip(reason="to be written") def test_device_software_action(user): """Ensure only software that is from the same organization or is global can be added to the device From 8244676530c7348fea336eefce914b0346274547 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 18 Jul 2024 22:05:12 +0930 Subject: [PATCH 041/123] test: ensure inventory upload matches by both serial number and uuid if device name different !42 #15 --- .../unit/inventory/test_api_inventory.py | 563 ++++++++++++++++++ 1 file changed, 563 insertions(+) diff --git a/app/api/tests/unit/inventory/test_api_inventory.py b/app/api/tests/unit/inventory/test_api_inventory.py index 0b714133..0ddf27e3 100644 --- a/app/api/tests/unit/inventory/test_api_inventory.py +++ b/app/api/tests/unit/inventory/test_api_inventory.py @@ -255,6 +255,20 @@ class InventoryAPI(TestCase): + def test_api_inventory_device_uuid_match(self): + """ Device uuid match """ + + assert self.device.uuid == self.inventory['details']['uuid'] + + + + def test_api_inventory_device_serial_number_match(self): + """ Device SN match """ + + assert self.device.serial_number == self.inventory['details']['serial_number'] + + + def test_api_inventory_operating_system_added(self): """ Operating System is created """ @@ -424,3 +438,552 @@ class InventoryAPI(TestCase): """ pass + + + + +class InventoryAPIDifferentNameSerialNumberMatch(TestCase): + """ Test inventory upload with different name + + should match by serial number + """ + + model = Device + + model_name = 'device' + app_label = 'itam' + + inventory = { + "details": { + "name": "device_name", + "serial_number": "serial_number_123", + "uuid": "string" + }, + "os": { + "name": "os_name", + "version_major": "12", + "version": "12.1" + }, + "software": [ + { + "name": "software_name", + "category": "category_name", + "version": "1.2.3" + }, + { + "name": "software_name_not_semver", + "category": "category_name", + "version": "2024.4" + }, + { + "name": "software_name_semver_contained", + "category": "category_name", + "version": "1.2.3-rc1" + }, + ] + } + + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user + 2. Create a team for user with correct permissions + 3. add user to the teeam + 4. upload the inventory + 5. conduct queries for tests + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + Device.objects.create( + name='random device name', + serial_number='serial_number_123' + ) + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model_name, + content_type = ContentType.objects.get( + app_label = self.app_label, + model = self.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + + add_user_settings = UserSettings.objects.get(user=self.add_user) + + add_user_settings.default_organization = organization + + add_user_settings.save() + + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + # upload the inventory + process_inventory(json.dumps(self.inventory), organization.id) + + + self.device = Device.objects.get(name=self.inventory['details']['name']) + + self.operating_system = OperatingSystem.objects.get(name=self.inventory['os']['name']) + + self.operating_system_version = OperatingSystemVersion.objects.get(name=self.inventory['os']['version_major']) + + self.device_operating_system = DeviceOperatingSystem.objects.get(version=self.inventory['os']['version']) + + self.software = Software.objects.get(name=self.inventory['software'][0]['name']) + + self.software_category = SoftwareCategory.objects.get(name=self.inventory['software'][0]['category']) + + self.software_version = SoftwareVersion.objects.get( + name = self.inventory['software'][0]['version'], + software = self.software, + ) + + self.software_not_semver = Software.objects.get(name=self.inventory['software'][1]['name']) + + self.software_version_not_semver = SoftwareVersion.objects.get( + name = self.inventory['software'][1]['version'], + software = self.software_not_semver + ) + + self.software_is_semver = Software.objects.get(name=self.inventory['software'][2]['name']) + + self.software_version_is_semver = SoftwareVersion.objects.get( + software = self.software_is_semver + ) + + self.device_software = DeviceSoftware.objects.get(device=self.device,software=self.software) + + + + def test_api_inventory_device_added(self): + """ Device is created """ + + assert self.device.name == self.inventory['details']['name'] + + + + def test_api_inventory_device_uuid_match(self): + """ Device uuid match """ + + assert self.device.uuid == self.inventory['details']['uuid'] + + + + def test_api_inventory_device_serial_number_match(self): + """ Device SN match """ + + assert self.device.serial_number == self.inventory['details']['serial_number'] + + + + def test_api_inventory_operating_system_added(self): + """ Operating System is created """ + + assert self.operating_system.name == self.inventory['os']['name'] + + + + def test_api_inventory_operating_system_version_added(self): + """ Operating System version is created """ + + assert self.operating_system_version.name == self.inventory['os']['version_major'] + + + + def test_api_inventory_device_has_operating_system_added(self): + """ Operating System version linked to device """ + + assert self.device_operating_system.version == self.inventory['os']['version'] + + + + @pytest.mark.skip(reason="to be written") + def test_api_inventory_device_operating_system_version_is_semver(self): + """ Operating System version is full semver + + Operating system versions name is the major version number of semver. + The device version is to be full semver + """ + pass + + + + @pytest.mark.skip(reason="to be written") + def test_api_inventory_software_no_version_cleaned(self): + """ Check softare cleaned up + + As part of the inventory upload the software versions of software found on the device is set to null + and before the processing is completed, the version=null software is supposed to be cleaned up. + """ + pass + + + + def test_api_inventory_software_category_added(self): + """ Software category exists """ + + assert self.software_category.name == self.inventory['software'][0]['category'] + + + + def test_api_inventory_software_added(self): + """ Test software exists """ + + assert self.software.name == self.inventory['software'][0]['name'] + + + + def test_api_inventory_software_category_linked_to_software(self): + """ Software category linked to software """ + + assert self.software.category == self.software_category + + + + def test_api_inventory_software_version_added(self): + """ Test software version exists """ + + assert self.software_version.name == self.inventory['software'][0]['version'] + + + + def test_api_inventory_software_version_returns_semver(self): + """ Software Version from inventory returns semver if within version string """ + + assert self.software_version_is_semver.name == str(self.inventory['software'][2]['version']).split('-')[0] + + + + def test_api_inventory_software_version_returns_original_version(self): + """ Software Version from inventory returns inventoried version if no semver found """ + + assert self.software_version_not_semver.name == self.inventory['software'][1]['version'] + + + + + def test_api_inventory_software_version_linked_to_software(self): + """ Test software version linked to software it belongs too """ + + assert self.software_version.software == self.software + + + + def test_api_inventory_device_has_software_version(self): + """ Inventoried software is linked to device and it's the corret one""" + + assert self.software_version.name == self.inventory['software'][0]['version'] + + + + def test_api_inventory_device_software_has_installed_date(self): + """ Inventoried software version has install date """ + + assert self.device_software.installed is not None + + + + def test_api_inventory_device_software_installed_date_type(self): + """ Inventoried software version has install date """ + + assert type(self.device_software.installed) is datetime.datetime + + + + @pytest.mark.skip(reason="to be written") + def test_api_inventory_device_software_blank_installed_date_is_updated(self): + """ A blank installed date of software is updated if the software was already attached to the device """ + pass + + + + + + + + +class InventoryAPIDifferentNameUUIDMatch(TestCase): + """ Test inventory upload with different name + + should match by uuid + """ + + model = Device + + model_name = 'device' + app_label = 'itam' + + inventory = { + "details": { + "name": "device_name", + "serial_number": "serial_number_123", + "uuid": "123-456-789" + }, + "os": { + "name": "os_name", + "version_major": "12", + "version": "12.1" + }, + "software": [ + { + "name": "software_name", + "category": "category_name", + "version": "1.2.3" + }, + { + "name": "software_name_not_semver", + "category": "category_name", + "version": "2024.4" + }, + { + "name": "software_name_semver_contained", + "category": "category_name", + "version": "1.2.3-rc1" + }, + ] + } + + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user + 2. Create a team for user with correct permissions + 3. add user to the teeam + 4. upload the inventory + 5. conduct queries for tests + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + Device.objects.create( + name='random device name', + uuid='123-456-789' + ) + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model_name, + content_type = ContentType.objects.get( + app_label = self.app_label, + model = self.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + + add_user_settings = UserSettings.objects.get(user=self.add_user) + + add_user_settings.default_organization = organization + + add_user_settings.save() + + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + # upload the inventory + process_inventory(json.dumps(self.inventory), organization.id) + + + self.device = Device.objects.get(name=self.inventory['details']['name']) + + self.operating_system = OperatingSystem.objects.get(name=self.inventory['os']['name']) + + self.operating_system_version = OperatingSystemVersion.objects.get(name=self.inventory['os']['version_major']) + + self.device_operating_system = DeviceOperatingSystem.objects.get(version=self.inventory['os']['version']) + + self.software = Software.objects.get(name=self.inventory['software'][0]['name']) + + self.software_category = SoftwareCategory.objects.get(name=self.inventory['software'][0]['category']) + + self.software_version = SoftwareVersion.objects.get( + name = self.inventory['software'][0]['version'], + software = self.software, + ) + + self.software_not_semver = Software.objects.get(name=self.inventory['software'][1]['name']) + + self.software_version_not_semver = SoftwareVersion.objects.get( + name = self.inventory['software'][1]['version'], + software = self.software_not_semver + ) + + self.software_is_semver = Software.objects.get(name=self.inventory['software'][2]['name']) + + self.software_version_is_semver = SoftwareVersion.objects.get( + software = self.software_is_semver + ) + + self.device_software = DeviceSoftware.objects.get(device=self.device,software=self.software) + + + + def test_api_inventory_device_added(self): + """ Device is created """ + + assert self.device.name == self.inventory['details']['name'] + + + + def test_api_inventory_device_uuid_match(self): + """ Device uuid match """ + + assert self.device.uuid == self.inventory['details']['uuid'] + + + + def test_api_inventory_device_serial_number_match(self): + """ Device SN match """ + + assert self.device.serial_number == self.inventory['details']['serial_number'] + + + + def test_api_inventory_operating_system_added(self): + """ Operating System is created """ + + assert self.operating_system.name == self.inventory['os']['name'] + + + + def test_api_inventory_operating_system_version_added(self): + """ Operating System version is created """ + + assert self.operating_system_version.name == self.inventory['os']['version_major'] + + + + def test_api_inventory_device_has_operating_system_added(self): + """ Operating System version linked to device """ + + assert self.device_operating_system.version == self.inventory['os']['version'] + + + + @pytest.mark.skip(reason="to be written") + def test_api_inventory_device_operating_system_version_is_semver(self): + """ Operating System version is full semver + + Operating system versions name is the major version number of semver. + The device version is to be full semver + """ + pass + + + + @pytest.mark.skip(reason="to be written") + def test_api_inventory_software_no_version_cleaned(self): + """ Check softare cleaned up + + As part of the inventory upload the software versions of software found on the device is set to null + and before the processing is completed, the version=null software is supposed to be cleaned up. + """ + pass + + + + def test_api_inventory_software_category_added(self): + """ Software category exists """ + + assert self.software_category.name == self.inventory['software'][0]['category'] + + + + def test_api_inventory_software_added(self): + """ Test software exists """ + + assert self.software.name == self.inventory['software'][0]['name'] + + + + def test_api_inventory_software_category_linked_to_software(self): + """ Software category linked to software """ + + assert self.software.category == self.software_category + + + + def test_api_inventory_software_version_added(self): + """ Test software version exists """ + + assert self.software_version.name == self.inventory['software'][0]['version'] + + + + def test_api_inventory_software_version_returns_semver(self): + """ Software Version from inventory returns semver if within version string """ + + assert self.software_version_is_semver.name == str(self.inventory['software'][2]['version']).split('-')[0] + + + + def test_api_inventory_software_version_returns_original_version(self): + """ Software Version from inventory returns inventoried version if no semver found """ + + assert self.software_version_not_semver.name == self.inventory['software'][1]['version'] + + + + + def test_api_inventory_software_version_linked_to_software(self): + """ Test software version linked to software it belongs too """ + + assert self.software_version.software == self.software + + + + def test_api_inventory_device_has_software_version(self): + """ Inventoried software is linked to device and it's the corret one""" + + assert self.software_version.name == self.inventory['software'][0]['version'] + + + + def test_api_inventory_device_software_has_installed_date(self): + """ Inventoried software version has install date """ + + assert self.device_software.installed is not None + + + + def test_api_inventory_device_software_installed_date_type(self): + """ Inventoried software version has install date """ + + assert type(self.device_software.installed) is datetime.datetime + + + + @pytest.mark.skip(reason="to be written") + def test_api_inventory_device_software_blank_installed_date_is_updated(self): + """ A blank installed date of software is updated if the software was already attached to the device """ + pass + From 61b9435d1f829bbd0651236aa530edb27792993c Mon Sep 17 00:00:00 2001 From: nfc_bot Date: Thu, 18 Jul 2024 12:59:03 +0000 Subject: [PATCH 042/123] =?UTF-8?q?bump:=20version=201.0.0-a3=20=E2=86=92?= =?UTF-8?q?=201.0.0-a4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cz.yaml | 2 +- CHANGELOG.md | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.cz.yaml b/.cz.yaml index c63994e1..55d17e46 100644 --- a/.cz.yaml +++ b/.cz.yaml @@ -4,5 +4,5 @@ commitizen: prerelease_offset: 1 tag_format: $version update_changelog_on_bump: false - version: 1.0.0-a3 + version: 1.0.0-a4 version_scheme: semver diff --git a/CHANGELOG.md b/CHANGELOG.md index 73485144..07ca1209 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.0.0-a4 (2024-07-18) + +### Feat + +- **api**: When processing uploaded inventory and name does not match, update name to one within inventory file +- **config_management**: Group name to be entire breadcrumb + ## 1.0.0-a3 (2024-07-18) ### Feat From 5188b3d52e2c4a4b7e872bc06ab05c1ed9c294ba Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 18 Jul 2024 23:48:03 +0930 Subject: [PATCH 043/123] fix(itam): ensure installed software count is limited to users organizations !42 --- app/itam/views/software.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/itam/views/software.py b/app/itam/views/software.py index a80767e3..1ab04f40 100644 --- a/app/itam/views/software.py +++ b/app/itam/views/software.py @@ -1,5 +1,5 @@ from django.contrib.auth import decorators as auth_decorator -from django.db.models import Count +from django.db.models import Count, Q from django.urls import reverse from django.utils.decorators import method_decorator @@ -71,9 +71,12 @@ class View(ChangeView): context = super().get_context_data(**kwargs) software_versions = SoftwareVersion.objects.filter( - software=self.kwargs['pk'] + software=self.kwargs['pk'], ).annotate( - installs=Count("installedversion") + installs=Count( + "installedversion", + filter=Q(installedversion__organization__in = self.user_organizations()) + ) ) context['software_versions'] = software_versions From e5ce86a9bbcf5a06ad5dd482d14ae0234fc4aa61 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 19 Jul 2024 11:02:56 +0930 Subject: [PATCH 044/123] fix(itam): ensure installed operating system count is limited to users organizations !42 --- app/itam/views/operating_system.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/itam/views/operating_system.py b/app/itam/views/operating_system.py index 5d60f550..d00e9670 100644 --- a/app/itam/views/operating_system.py +++ b/app/itam/views/operating_system.py @@ -1,5 +1,5 @@ from django.contrib.auth import decorators as auth_decorator -from django.db.models import Count +from django.db.models import Count, Q from django.urls import reverse from django.utils.decorators import method_decorator @@ -62,7 +62,15 @@ class View(ChangeView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - operating_system_versions = OperatingSystemVersion.objects.filter(operating_system=self.kwargs['pk']).order_by('name').annotate(installs=Count("deviceoperatingsystem")) + operating_system_versions = OperatingSystemVersion.objects.filter( + operating_system=self.kwargs['pk'] + ).order_by( + 'name' + ).annotate( + installs=Count("deviceoperatingsystem"), + filter=Q(deviceoperatingsystem__organization__in = self.user_organizations()) + ) + context['operating_system_versions'] = operating_system_versions installs = DeviceOperatingSystem.objects.filter(operating_system_version__operating_system_id=self.kwargs['pk']) From 034857d0880acb809ee72384732a0ea5c3d89491 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 19 Jul 2024 15:35:32 +0930 Subject: [PATCH 045/123] ci: dev branch releases now beta preperation for RC. all dev releases are now beta. !42 #74 --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7973500c..e77cb2ae 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -147,7 +147,7 @@ Docker Container: - RELEASE_VERSION_CURRENT=$(cz version --project) script: - "$MY_COMMAND" - - if [ "$CI_COMMIT_BRANCH" == "development" ] ; then RELEASE_CHANGELOG=$(cz bump --changelog --changelog-to-stdout --prerelease alpha); else RELEASE_CHANGELOG=$(cz bump --changelog --changelog-to-stdout); fi + - if [ "$CI_COMMIT_BRANCH" == "development" ] ; then RELEASE_CHANGELOG=$(cz bump --changelog --changelog-to-stdout --prerelease beta); else RELEASE_CHANGELOG=$(cz bump --changelog --changelog-to-stdout); fi - RELEASE_VERSION_NEW=$(cz version --project) - RELEASE_TAG=$RELEASE_VERSION_NEW - echo "[DEBUG] RELEASE_VERSION_CURRENT[$RELEASE_VERSION_CURRENT]" From 92a411baece782e2405ad76ce7e5fc96cbc61d0d Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 19 Jul 2024 16:27:33 +0930 Subject: [PATCH 046/123] docs(administration): explain the magic !42 #74 --- .gitlab/merge_request_templates/default.md | 2 + Release-Notes.md | 7 ++ .../centurion_erp/administration/index.md | 65 +++++++++++++++++-- 3 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 Release-Notes.md diff --git a/.gitlab/merge_request_templates/default.md b/.gitlab/merge_request_templates/default.md index 5fa898eb..52ee7ff4 100644 --- a/.gitlab/merge_request_templates/default.md +++ b/.gitlab/merge_request_templates/default.md @@ -24,6 +24,8 @@ _Breaking Change must also be notated in the commit that introduces it and in [Conventional Commit Format](https://www.conventionalcommits.org/en/v1.0.0/)._ + - [ ] Release notes updated + - [ ] ~Documentation Documentation written _All features to be documented within the correct section(s). Administration, Development and/or User_ diff --git a/Release-Notes.md b/Release-Notes.md new file mode 100644 index 00000000..dc2bda58 --- /dev/null +++ b/Release-Notes.md @@ -0,0 +1,7 @@ +# Version 1.0.0 + +Initial Release of Centurion ERP. + +## Breaking changes + +- Nil diff --git a/docs/projects/centurion_erp/administration/index.md b/docs/projects/centurion_erp/administration/index.md index 72a28f4d..95dc8037 100644 --- a/docs/projects/centurion_erp/administration/index.md +++ b/docs/projects/centurion_erp/administration/index.md @@ -8,25 +8,80 @@ about: https://gitlab.com/nofusscomputing/infrastructure/configuration-managemen This documentation is targeted towards those whom administer the applications deployment. +Centurion ERP is a simple application to deploy with the only additional requirements being that you have already deployed a database server and a RabbitMQ server. Centurion ERP is container based and is deployable via Docker or upon Kubernetes. Our images are available on [Docker Hub](https://hub.docker.com/r/nofusscomputing/centurion-erp). + +!!! note "TL;DR" + `docker pull nofusscomputing/centurion-erp:latest`. + ## Installation -To install this application you must have a container engine installed, both docker and kubernetes are supported. The container image is available on [Docker Hub](https://hub.docker.com/r/nofusscomputing/centurion-erp) and can be pulled with `docker pull nofusscomputing/centurion-erp:latest`. +Basic installation steps are as follows: -Settings for the application are stored within a docker volume at path `/etc/itsm/`, with the settings living in `.py` files. A database is also required for the application to store it's settings. SQLLite and MariaDB/MySQL are supported. +1. Deploy a Database Server + +1. Deploy a RabbitMQ Server + +1. Deploy a Web container for Centurion ERP + +1. Deploy a Worker container for Centurion ERP + +1. Add settings file to path `/etc/itsm/settings.py` for both Centurion ERP containers. + +1. Run migrations + + - Docker `docker exec -ti -- python manage.py migrate` + + - Kubernetes `kubectl exec -ti -n deploy/ -- python manage.py migrate` -### Background workers +### Database Server -This application requires that you deploy at least one background worker. The background worker requires access to a RabbitMQ message broker for the queueing and routing of messages (background jobs). If you are using our docker container to deploy this application, launch an additional container with `celery -A app worker -l INFO` as the entrypoint/command. Configuration for the worker resides in directory `/etc/itsm/` within the container. see below for the `CELERY_` configuration. +As Centurion ERP is uses the Django Framework, Theoretically Every Django supported database is available. The reality is however, that we have only used PostgreSQL Server with Centurion ERP. By default if no database is configured a SQLite database will be used. This allows [tests](../development/testing.md) to function and to quickly spin up a deployment for testing. + + +### RabbitMQ Server + +Centurion ERP uses RabbitMQ as for its worker queue. As tasks are created when using Centurion ERP, they are added to the RabbitMQ server for the background worker to pickup. When the background worker picks up the task, it does it's business, clears the task from the RabbitMQ server and saves the [results](../user/core/index.md#background-worker) within the Database. + + +### Web Container + +The [web container](https://hub.docker.com/r/nofusscomputing/centurion-erp) is the guts of Centurion ERP. It provides the interface and endpoints for interacting with Centurion ERP. This container is scalable with the only additional requirement being that a load-balancer be placed in front of all web containers for traffic routing. If deploying to Kubernetes the service load-balancer is sufficient and setting the deployment `replicas` to the number of desired containers is the simplest method to scale. + + +### Background Worker Container + +The [Background Worker container](https://hub.docker.com/r/nofusscomputing/centurion-erp) is a worker that waits for tasks sent to the RabbitMQ server. The worker is based upon [Celery](https://docs.celeryq.dev/en/stable/index.html). On the worker not being busy, it'll pickup and run the task. This container is scalable with nil additional requirements for launching additional workers. If deploying to Kubernetes the setting the deployment `replicas` to the number of desired containers is the simplest method to scale. The container start command will need to be set to `celery -A app worker -l INFO` so that the worker is started on container startup. + +Configuration for the worker resides in directory `/etc/itsm/` within the container. see below for the `CELERY_` configuration. ### Settings file -The settings file is a python file `.py` and must remain a valid python file for the application to work. +The settings file is a python file `.py` and must remain a valid python file for the application to work. Settings for the application are stored within a docker volume at path `/etc/itsm/`, with the settings living in `.py` files. A database is also required for the application to store it's settings. SQLLite and MariaDB/MySQL are supported. ``` py title="settings.py" --8<-- "includes/etc/itsm/settings.py" ``` + + +### Migrations + +Migrations serve the purpose of setting up the database. On initial deployment of Centurion ERP migrations must be run as must they be on any upgrade. + + +## Backup / Restoration + +Most Data within Centurion ERP resides within the database. This simplifies the backup/restoration process as only the database the application uses needs to be backed up. + +Tasks that have been sent to the RabbitMQ server will remain within the task queue, if Centurion ERP has not processed them. Should you wish not to loose tasks you should also backup the rabbitMQ server. + + +## Updating + +We use [semver](https://semver.org/) versioning for Centurion ERP. Using this method of versioning enables us to clearly show what versions will have breaking changes. You can rest assured that every version whose `Major` version number remains the same will not break your deployment. [Release notes](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/blob/master/Release-Notes.md) are available within the repository root and are a running document for the current `Major` release. To locate the release notes for your particular version please select the release tag from the branches drop-down. We will use the release notes to denote **Any** Breaking changes. + +Updating to a newer version of Centurion ERP is as simple as backing up your database and RabbitMQ server, then updating the deployed image to the desired version and running the database migrations. From f4e68529baea00b08580dfacdcbd15b2cac6dbf7 Mon Sep 17 00:00:00 2001 From: nfc_bot Date: Fri, 19 Jul 2024 07:15:09 +0000 Subject: [PATCH 047/123] =?UTF-8?q?bump:=20version=201.0.0-a4=20=E2=86=92?= =?UTF-8?q?=201.0.0-b1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cz.yaml | 2 +- CHANGELOG.md | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.cz.yaml b/.cz.yaml index 55d17e46..3ee59528 100644 --- a/.cz.yaml +++ b/.cz.yaml @@ -4,5 +4,5 @@ commitizen: prerelease_offset: 1 tag_format: $version update_changelog_on_bump: false - version: 1.0.0-a4 + version: 1.0.0-b1 version_scheme: semver diff --git a/CHANGELOG.md b/CHANGELOG.md index 07ca1209..cf4c88eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.0.0-b1 (2024-07-19) + +### Fix + +- **itam**: ensure installed operating system count is limited to users organizations +- **itam**: ensure installed software count is limited to users organizations + ## 1.0.0-a4 (2024-07-18) ### Feat From 0798a672c25b0def73f87ff9122a9e4614dd067c Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 19 Jul 2024 16:57:10 +0930 Subject: [PATCH 048/123] docs(administration): spread the love out !42 --- .../administration/authentication.md | 9 +++ .../centurion_erp/administration/backup.md | 11 +++ .../centurion_erp/administration/index.md | 77 +----------------- .../administration/installation.md | 78 +++++++++++++++++++ mkdocs.yml | 6 ++ 5 files changed, 108 insertions(+), 73 deletions(-) create mode 100644 docs/projects/centurion_erp/administration/authentication.md create mode 100644 docs/projects/centurion_erp/administration/backup.md create mode 100644 docs/projects/centurion_erp/administration/installation.md diff --git a/docs/projects/centurion_erp/administration/authentication.md b/docs/projects/centurion_erp/administration/authentication.md new file mode 100644 index 00000000..4fe490a5 --- /dev/null +++ b/docs/projects/centurion_erp/administration/authentication.md @@ -0,0 +1,9 @@ +--- +title: Authentication +description: Authentication administration documentation for Centurion ERP by No Fuss Computing +date: 2024-07-19 +template: project.html +about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/centurion_erp +--- + +Centurion ERP requires that a user be authenticated to access the ERP features of the application. The built in authentication system as well as Single Sign on (SSO) via an identity broker is available. diff --git a/docs/projects/centurion_erp/administration/backup.md b/docs/projects/centurion_erp/administration/backup.md new file mode 100644 index 00000000..5b1bbffe --- /dev/null +++ b/docs/projects/centurion_erp/administration/backup.md @@ -0,0 +1,11 @@ +--- +title: Backup +description: Backup documentation for Centurion ERP by No Fuss Computing +date: 2024-07-19 +template: project.html +about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/centurion_erp +--- + +Most Data within Centurion ERP resides within the database. This simplifies the backup/restoration process as only the database the application uses needs to be backed up. + +Tasks that have been sent to the RabbitMQ server will remain within the task queue, if Centurion ERP has not processed them. Should you wish not to loose tasks you should also backup the rabbitMQ server. diff --git a/docs/projects/centurion_erp/administration/index.md b/docs/projects/centurion_erp/administration/index.md index 95dc8037..e25d7cf4 100644 --- a/docs/projects/centurion_erp/administration/index.md +++ b/docs/projects/centurion_erp/administration/index.md @@ -8,80 +8,11 @@ about: https://gitlab.com/nofusscomputing/infrastructure/configuration-managemen This documentation is targeted towards those whom administer the applications deployment. -Centurion ERP is a simple application to deploy with the only additional requirements being that you have already deployed a database server and a RabbitMQ server. Centurion ERP is container based and is deployable via Docker or upon Kubernetes. Our images are available on [Docker Hub](https://hub.docker.com/r/nofusscomputing/centurion-erp). -!!! note "TL;DR" - `docker pull nofusscomputing/centurion-erp:latest`. +## Contents +- [Authentication](./authentication.md) -## Installation +- [Backup](./backup.md) -Basic installation steps are as follows: - -1. Deploy a Database Server - -1. Deploy a RabbitMQ Server - -1. Deploy a Web container for Centurion ERP - -1. Deploy a Worker container for Centurion ERP - -1. Add settings file to path `/etc/itsm/settings.py` for both Centurion ERP containers. - -1. Run migrations - - - Docker `docker exec -ti -- python manage.py migrate` - - - Kubernetes `kubectl exec -ti -n deploy/ -- python manage.py migrate` - - -### Database Server - -As Centurion ERP is uses the Django Framework, Theoretically Every Django supported database is available. The reality is however, that we have only used PostgreSQL Server with Centurion ERP. By default if no database is configured a SQLite database will be used. This allows [tests](../development/testing.md) to function and to quickly spin up a deployment for testing. - - -### RabbitMQ Server - -Centurion ERP uses RabbitMQ as for its worker queue. As tasks are created when using Centurion ERP, they are added to the RabbitMQ server for the background worker to pickup. When the background worker picks up the task, it does it's business, clears the task from the RabbitMQ server and saves the [results](../user/core/index.md#background-worker) within the Database. - - -### Web Container - -The [web container](https://hub.docker.com/r/nofusscomputing/centurion-erp) is the guts of Centurion ERP. It provides the interface and endpoints for interacting with Centurion ERP. This container is scalable with the only additional requirement being that a load-balancer be placed in front of all web containers for traffic routing. If deploying to Kubernetes the service load-balancer is sufficient and setting the deployment `replicas` to the number of desired containers is the simplest method to scale. - - -### Background Worker Container - -The [Background Worker container](https://hub.docker.com/r/nofusscomputing/centurion-erp) is a worker that waits for tasks sent to the RabbitMQ server. The worker is based upon [Celery](https://docs.celeryq.dev/en/stable/index.html). On the worker not being busy, it'll pickup and run the task. This container is scalable with nil additional requirements for launching additional workers. If deploying to Kubernetes the setting the deployment `replicas` to the number of desired containers is the simplest method to scale. The container start command will need to be set to `celery -A app worker -l INFO` so that the worker is started on container startup. - -Configuration for the worker resides in directory `/etc/itsm/` within the container. see below for the `CELERY_` configuration. - - -### Settings file - -The settings file is a python file `.py` and must remain a valid python file for the application to work. Settings for the application are stored within a docker volume at path `/etc/itsm/`, with the settings living in `.py` files. A database is also required for the application to store it's settings. SQLLite and MariaDB/MySQL are supported. - -``` py title="settings.py" - ---8<-- "includes/etc/itsm/settings.py" - -``` - - -### Migrations - -Migrations serve the purpose of setting up the database. On initial deployment of Centurion ERP migrations must be run as must they be on any upgrade. - - -## Backup / Restoration - -Most Data within Centurion ERP resides within the database. This simplifies the backup/restoration process as only the database the application uses needs to be backed up. - -Tasks that have been sent to the RabbitMQ server will remain within the task queue, if Centurion ERP has not processed them. Should you wish not to loose tasks you should also backup the rabbitMQ server. - - -## Updating - -We use [semver](https://semver.org/) versioning for Centurion ERP. Using this method of versioning enables us to clearly show what versions will have breaking changes. You can rest assured that every version whose `Major` version number remains the same will not break your deployment. [Release notes](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/blob/master/Release-Notes.md) are available within the repository root and are a running document for the current `Major` release. To locate the release notes for your particular version please select the release tag from the branches drop-down. We will use the release notes to denote **Any** Breaking changes. - -Updating to a newer version of Centurion ERP is as simple as backing up your database and RabbitMQ server, then updating the deployed image to the desired version and running the database migrations. +- [Installation](./installation.md) diff --git a/docs/projects/centurion_erp/administration/installation.md b/docs/projects/centurion_erp/administration/installation.md new file mode 100644 index 00000000..b1ddffd3 --- /dev/null +++ b/docs/projects/centurion_erp/administration/installation.md @@ -0,0 +1,78 @@ +--- +title: Installation +description: Installation documentation for Centurion ERP by No Fuss Computing +date: 2024-07-19 +template: project.html +about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/centurion_erp +--- + +Centurion ERP is a simple application to deploy with the only additional requirements being that you have already deployed a database server and a RabbitMQ server. Centurion ERP is container based and is deployable via Docker or upon Kubernetes. Our images are available on [Docker Hub](https://hub.docker.com/r/nofusscomputing/centurion-erp). + +!!! note "TL;DR" + `docker pull nofusscomputing/centurion-erp:latest`. + + +## Installation + +Basic installation steps are as follows: + +1. Deploy a Database Server + +1. Deploy a RabbitMQ Server + +1. Deploy a Web container for Centurion ERP + +1. Deploy a Worker container for Centurion ERP + +1. Add settings file to path `/etc/itsm/settings.py` for both Centurion ERP containers. + +1. Run migrations + + - Docker `docker exec -ti -- python manage.py migrate` + + - Kubernetes `kubectl exec -ti -n deploy/ -- python manage.py migrate` + + +### Database Server + +As Centurion ERP is uses the Django Framework, Theoretically Every Django supported database is available. The reality is however, that we have only used PostgreSQL Server with Centurion ERP. By default if no database is configured a SQLite database will be used. This allows [tests](../development/testing.md) to function and to quickly spin up a deployment for testing. + + +### RabbitMQ Server + +Centurion ERP uses RabbitMQ as for its worker queue. As tasks are created when using Centurion ERP, they are added to the RabbitMQ server for the background worker to pickup. When the background worker picks up the task, it does it's business, clears the task from the RabbitMQ server and saves the [results](../user/core/index.md#background-worker) within the Database. + + +### Web Container + +The [web container](https://hub.docker.com/r/nofusscomputing/centurion-erp) is the guts of Centurion ERP. It provides the interface and endpoints for interacting with Centurion ERP. This container is scalable with the only additional requirement being that a load-balancer be placed in front of all web containers for traffic routing. If deploying to Kubernetes the service load-balancer is sufficient and setting the deployment `replicas` to the number of desired containers is the simplest method to scale. + + +### Background Worker Container + +The [Background Worker container](https://hub.docker.com/r/nofusscomputing/centurion-erp) is a worker that waits for tasks sent to the RabbitMQ server. The worker is based upon [Celery](https://docs.celeryq.dev/en/stable/index.html). On the worker not being busy, it'll pickup and run the task. This container is scalable with nil additional requirements for launching additional workers. If deploying to Kubernetes the setting the deployment `replicas` to the number of desired containers is the simplest method to scale. The container start command will need to be set to `celery -A app worker -l INFO` so that the worker is started on container startup. + +Configuration for the worker resides in directory `/etc/itsm/` within the container. see below for the `CELERY_` configuration. + + +### Settings file + +The settings file is a python file `.py` and must remain a valid python file for the application to work. Settings for the application are stored within a docker volume at path `/etc/itsm/`, with the settings living in `.py` files. A database is also required for the application to store it's settings. SQLLite and MariaDB/MySQL are supported. + +``` py title="settings.py" + +--8<-- "includes/etc/itsm/settings.py" + +``` + + +### Migrations + +Migrations serve the purpose of setting up the database. On initial deployment of Centurion ERP migrations must be run as must they be on any upgrade. + + +## Updating + +We use [semver](https://semver.org/) versioning for Centurion ERP. Using this method of versioning enables us to clearly show what versions will have breaking changes. You can rest assured that every version whose `Major` version number remains the same will not break your deployment. [Release notes](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/blob/master/Release-Notes.md) are available within the repository root and are a running document for the current `Major` release. To locate the release notes for your particular version please select the release tag from the branches drop-down. We will use the release notes to denote **Any** Breaking changes. + +Updating to a newer version of Centurion ERP is as simple as [backing up your database](./backup.md) and RabbitMQ server, then updating the deployed image to the desired version and running the database migrations. diff --git a/mkdocs.yml b/mkdocs.yml index 239d583d..bfc62947 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -56,6 +56,12 @@ nav: - projects/centurion_erp/administration/index.md + - projects/centurion_erp/administration/authentication.md + + - projects/centurion_erp/administration/backup.md + + - projects/centurion_erp/administration/installation.md + - Development: - projects/centurion_erp/development/index.md From 9ea4fe1adc28e8ee597e930521d06b5414f968de Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 19 Jul 2024 17:41:18 +0930 Subject: [PATCH 049/123] ci: Create Version labels within repo on release !42 --- .gitlab-ci.yml | 9 +++++++-- .gitlab/additional_actions_bump.sh | 7 +++++++ 2 files changed, 14 insertions(+), 2 deletions(-) create mode 100755 .gitlab/additional_actions_bump.sh diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e77cb2ae..5eabb84c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -15,6 +15,9 @@ variables: DOCKER_IMAGE_PUBLISH_REGISTRY: docker.io/nofusscomputing DOCKER_IMAGE_PUBLISH_URL: https://hub.docker.com/r/nofusscomputing/$DOCKER_IMAGE_PUBLISH_NAME + # Extra release commands + MY_COMMAND: './.gitlab/additional_actions_bump.sh' + # Docs NFC PAGES_ENVIRONMENT_PATH: projects/centurion_erp/ @@ -146,7 +149,6 @@ Docker Container: - git push --set-upstream origin development - RELEASE_VERSION_CURRENT=$(cz version --project) script: - - "$MY_COMMAND" - if [ "$CI_COMMIT_BRANCH" == "development" ] ; then RELEASE_CHANGELOG=$(cz bump --changelog --changelog-to-stdout --prerelease beta); else RELEASE_CHANGELOG=$(cz bump --changelog --changelog-to-stdout); fi - RELEASE_VERSION_NEW=$(cz version --project) - RELEASE_TAG=$RELEASE_VERSION_NEW @@ -156,8 +158,11 @@ Docker Container: - echo "[DEBUG] RELEASE_TAG[$RELEASE_TAG]" - RELEASE_TAG_SHA1=$(git log -n1 --format=format:"%H") - echo "[DEBUG] RELEASE_TAG_SHA1[$RELEASE_TAG_SHA1]" + + - if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then echo "[DEBUG] not running extra actions, no new version"; else "$MY_COMMAND"; fi + - if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then echo "[DEBUG] No tag to delete, version was not bumped"; else git tag -d $RELEASE_TAG; fi - + - if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then echo "[DEBUG] No push will be conducted, version was not bumped"; else git push; fi - if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then echo "[DEBUG] No release will be created, version was not bumped"; else release-cli create --name "Release $RELEASE_TAG" --tag-name "$RELEASE_TAG" --tag-message "$RELEASE_CHANGELOG" --ref "$RELEASE_TAG_SHA1" --description "$RELEASE_CHANGELOG"; fi - if [ "$CI_COMMIT_BRANCH" == "master" ] ; then git checkout master; fi diff --git a/.gitlab/additional_actions_bump.sh b/.gitlab/additional_actions_bump.sh new file mode 100755 index 00000000..2cd38606 --- /dev/null +++ b/.gitlab/additional_actions_bump.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +# Create Version label wtihn repo +curl \ + --data "name=v${$RELEASE_TAG}&color=#eee600&description=Version%20that%20is%20affected" \ + --header "PRIVATE-TOKEN: $GIT_COMMIT_TOKEN" \ + "https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/labels" From 3a32c621199885040853811ee2dfd21cf6fa320e Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 19 Jul 2024 18:00:42 +0930 Subject: [PATCH 050/123] fix(itam): only show os version once !42 fixes #139 --- app/itam/views/operating_system.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/itam/views/operating_system.py b/app/itam/views/operating_system.py index d00e9670..f7342e29 100644 --- a/app/itam/views/operating_system.py +++ b/app/itam/views/operating_system.py @@ -68,7 +68,7 @@ class View(ChangeView): 'name' ).annotate( installs=Count("deviceoperatingsystem"), - filter=Q(deviceoperatingsystem__organization__in = self.user_organizations()) + filter=Q(deviceoperatingsystem__operating_system_version__organization__in = self.user_organizations()) ) context['operating_system_versions'] = operating_system_versions From 94576cc7338ddd55de7ef3fa3428c377b1b210a0 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 19 Jul 2024 18:21:43 +0930 Subject: [PATCH 051/123] ci: fix additional command as part of release !42 --- .gitlab-ci.yml | 25 ++++++++++++++++++++++--- .gitlab/additional_actions_bump.sh | 2 +- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5eabb84c..50c01340 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,7 +16,7 @@ variables: DOCKER_IMAGE_PUBLISH_URL: https://hub.docker.com/r/nofusscomputing/$DOCKER_IMAGE_PUBLISH_NAME # Extra release commands - MY_COMMAND: './.gitlab/additional_actions_bump.sh' + MY_COMMAND: ./.gitlab/additional_actions_bump.sh # Docs NFC PAGES_ENVIRONMENT_PATH: projects/centurion_erp/ @@ -132,7 +132,7 @@ Docker Container: - mkdir -p "$CI_PROJECT_DIR/artifacts/$CI_JOB_STAGE/$CI_JOB_NAME" - mkdir -p "$CI_PROJECT_DIR/artifacts/$CI_JOB_STAGE/tests" - apk update - - apk add git + - apk add git curl - apk add --update --no-cache python3 && ln -sf python3 /usr/bin/python - python -m ensurepip && ln -sf pip3 /usr/bin/pip - pip install --upgrade pip @@ -159,7 +159,26 @@ Docker Container: - RELEASE_TAG_SHA1=$(git log -n1 --format=format:"%H") - echo "[DEBUG] RELEASE_TAG_SHA1[$RELEASE_TAG_SHA1]" - - if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then echo "[DEBUG] not running extra actions, no new version"; else "$MY_COMMAND"; fi + - | + if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then + + echo "[DEBUG] not running extra actions, no new version"; + + else + + echo "[DEBUG] Creating new Version Label"; + + curl \ + --data "name=v${RELEASE_TAG}&color=#eee600&description=Version%20that%20is%20affected" \ + --header "PRIVATE-TOKEN: $GIT_COMMIT_TOKEN" \ + "https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/labels"; + + echo curl \ + --data "name=v${RELEASE_TAG}&color=#eee600&description=Version%20that%20is%20affected" \ + --header "PRIVATE-TOKEN: $GIT_COMMIT_TOKEN" \ + "https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/labels"; + + fi - if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then echo "[DEBUG] No tag to delete, version was not bumped"; else git tag -d $RELEASE_TAG; fi diff --git a/.gitlab/additional_actions_bump.sh b/.gitlab/additional_actions_bump.sh index 2cd38606..f22e00d3 100755 --- a/.gitlab/additional_actions_bump.sh +++ b/.gitlab/additional_actions_bump.sh @@ -2,6 +2,6 @@ # Create Version label wtihn repo curl \ - --data "name=v${$RELEASE_TAG}&color=#eee600&description=Version%20that%20is%20affected" \ + --data "name=v${RELEASE_TAG}&color=#eee600&description=Version%20that%20is%20affected" \ --header "PRIVATE-TOKEN: $GIT_COMMIT_TOKEN" \ "https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/labels" From 61fe05951310676ce0d8e1aea313f3547eb2ec5f Mon Sep 17 00:00:00 2001 From: nfc_bot Date: Fri, 19 Jul 2024 10:41:41 +0000 Subject: [PATCH 052/123] =?UTF-8?q?bump:=20version=201.0.0-b1=20=E2=86=92?= =?UTF-8?q?=201.0.0-b2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cz.yaml | 2 +- CHANGELOG.md | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.cz.yaml b/.cz.yaml index 3ee59528..115ced9d 100644 --- a/.cz.yaml +++ b/.cz.yaml @@ -4,5 +4,5 @@ commitizen: prerelease_offset: 1 tag_format: $version update_changelog_on_bump: false - version: 1.0.0-b1 + version: 1.0.0-b2 version_scheme: semver diff --git a/CHANGELOG.md b/CHANGELOG.md index cf4c88eb..65880825 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 1.0.0-b2 (2024-07-19) + +### Fix + +- **itam**: only show os version once + ## 1.0.0-b1 (2024-07-19) ### Fix From 8a48902b64c2e14f9efe0399e83c18aff91e5ad7 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 20 Jul 2024 13:02:04 +0930 Subject: [PATCH 053/123] ci: return command to release !42 --- .gitlab-ci.yml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 50c01340..fd8bea44 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -168,15 +168,7 @@ Docker Container: echo "[DEBUG] Creating new Version Label"; - curl \ - --data "name=v${RELEASE_TAG}&color=#eee600&description=Version%20that%20is%20affected" \ - --header "PRIVATE-TOKEN: $GIT_COMMIT_TOKEN" \ - "https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/labels"; - - echo curl \ - --data "name=v${RELEASE_TAG}&color=#eee600&description=Version%20that%20is%20affected" \ - --header "PRIVATE-TOKEN: $GIT_COMMIT_TOKEN" \ - "https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/labels"; + ${MY_COMMAND} fi From 5704560beba2af9127701d2c5d26e048e70dbbaa Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 21 Jul 2024 10:07:36 +0930 Subject: [PATCH 054/123] fix(itam): Limit os version count to devices user has access to !42 --- app/itam/views/operating_system.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/itam/views/operating_system.py b/app/itam/views/operating_system.py index f7342e29..0053ac80 100644 --- a/app/itam/views/operating_system.py +++ b/app/itam/views/operating_system.py @@ -67,8 +67,14 @@ class View(ChangeView): ).order_by( 'name' ).annotate( - installs=Count("deviceoperatingsystem"), - filter=Q(deviceoperatingsystem__operating_system_version__organization__in = self.user_organizations()) + installs=Count( + "deviceoperatingsystem", + filter=Q(deviceoperatingsystem__device__organization__in = self.user_organizations()) + ), + # filter=Q(deviceoperatingsystem__operating_system_version__organization__in = self.user_organizations()) + # filter=Q(deviceoperatingsystem__operating_system_version__deviceoperatingsystem__device__organization__in = self.user_organizations()), + filter=Q(deviceoperatingsystem__operating_system_version__organization__in = self.user_organizations()), + ) context['operating_system_versions'] = operating_system_versions From 41414438d1471b316aee154e042fe82a830db978 Mon Sep 17 00:00:00 2001 From: nfc_bot Date: Sun, 21 Jul 2024 01:47:05 +0000 Subject: [PATCH 055/123] =?UTF-8?q?bump:=20version=201.0.0-b2=20=E2=86=92?= =?UTF-8?q?=201.0.0-b3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cz.yaml | 2 +- CHANGELOG.md | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.cz.yaml b/.cz.yaml index 115ced9d..bf4e26a4 100644 --- a/.cz.yaml +++ b/.cz.yaml @@ -4,5 +4,5 @@ commitizen: prerelease_offset: 1 tag_format: $version update_changelog_on_bump: false - version: 1.0.0-b2 + version: 1.0.0-b3 version_scheme: semver diff --git a/CHANGELOG.md b/CHANGELOG.md index 65880825..67c880b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 1.0.0-b3 (2024-07-21) + +### Fix + +- **itam**: Limit os version count to devices user has access to + ## 1.0.0-b2 (2024-07-19) ### Fix From 823ebc0eb5f9dc17f307a8a71ddf04946cdfc954 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 21 Jul 2024 13:27:36 +0930 Subject: [PATCH 056/123] fix(access): Team model class inheritance order corrected !42 --- app/access/tests/unit/team/test_team.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/access/tests/unit/team/test_team.py b/app/access/tests/unit/team/test_team.py index 052ea6d8..f0d1c1b0 100644 --- a/app/access/tests/unit/team/test_team.py +++ b/app/access/tests/unit/team/test_team.py @@ -63,4 +63,8 @@ class TeamModel( @pytest.mark.skip(reason="uses Django group manager") def test_attribute_is_type_objects(self): + pass + + @pytest.mark.skip(reason="uses Django group manager") + def test_model_class_tenancy_manager_function_get_queryset_called(self): pass \ No newline at end of file From de53948cea8f1a603d2fe6beb965c7ed3bcab459 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 21 Jul 2024 13:27:45 +0930 Subject: [PATCH 057/123] test: confirm that the tenancymanager is called !43 --- app/access/models.py | 2 +- app/app/tests/abstract/models.py | 4 +++- app/core/tests/abstract/models.py | 23 ++++++++++++++++++++--- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/app/access/models.py b/app/access/models.py index bc3e0cda..761f8b31 100644 --- a/app/access/models.py +++ b/app/access/models.py @@ -190,7 +190,7 @@ class TenancyObject(SaveHistory): -class Team(Group, TenancyObject, SaveHistory): +class Team(Group, TenancyObject): class Meta: # proxy = True verbose_name_plural = "Teams" diff --git a/app/app/tests/abstract/models.py b/app/app/tests/abstract/models.py index 89caaf77..172a6c2e 100644 --- a/app/app/tests/abstract/models.py +++ b/app/app/tests/abstract/models.py @@ -7,6 +7,7 @@ from access.tests.abstract.tenancy_object import TenancyObject as TenancyObjectT from app.tests.abstract.views import AddView, ChangeView, DeleteView, DisplayView, IndexView from core.mixin.history_save import SaveHistory +from core.tests.abstract.models import Models @@ -30,7 +31,8 @@ class BaseModel: class TenancyModel( BaseModel, - TenancyObjectTestCases + TenancyObjectTestCases, + Models ): """ Test cases for tenancy models""" diff --git a/app/core/tests/abstract/models.py b/app/core/tests/abstract/models.py index 5ad90843..a88eadcc 100644 --- a/app/core/tests/abstract/models.py +++ b/app/core/tests/abstract/models.py @@ -3,21 +3,38 @@ import unittest from django.test import TestCase +from unittest.mock import patch + +from access.models import TenancyManager class Models: """ Test cases for Model Abstract Classes """ - - @pytest.mark.skip(reason="write test") + def test_model_class_tenancy_manager_function_get_queryset(self): """ Function Check function `get_queryset()` must exist """ - pass + assert hasattr(self.model.objects, 'get_queryset') + + assert callable(self.model.objects.get_queryset) + + + @patch.object(TenancyManager, 'get_queryset') + def test_model_class_tenancy_manager_function_get_queryset_called(self, get_queryset): + """ Function Check + + function `access.models.TenancyManager.get_queryset()` within the Tenancy manager must + be called as this function limits queries to the current users organizations. + """ + + self.model.objects.filter() + + assert get_queryset.called @pytest.mark.skip(reason="write test") From fc3f0b39e28b9c54a2b3516445e49c0ed299816f Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 29 Jul 2024 16:49:24 +0930 Subject: [PATCH 058/123] ci: add debug out to extra command !44 --- .gitlab-ci.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index fd8bea44..78efb120 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -168,8 +168,19 @@ Docker Container: echo "[DEBUG] Creating new Version Label"; - ${MY_COMMAND} + echo "----------------------------"; + echo ${MY_COMMAND}; + + echo "----------------------------"; + + cat ${MY_COMMAND}; + + echo "----------------------------"; + + ${MY_COMMAND}; + + echo "----------------------------"; fi - if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then echo "[DEBUG] No tag to delete, version was not bumped"; else git tag -d $RELEASE_TAG; fi From 098e41e6a1f517355678193eb32bd1c10f3355b7 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 29 Jul 2024 16:49:51 +0930 Subject: [PATCH 059/123] feat(swagger): remove `{format}` suffixed doc entries !44 --- app/app/settings.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/app/settings.py b/app/app/settings.py index a3d1398d..b2c9b17e 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -307,6 +307,9 @@ curl: 'SWAGGER_UI_DIST': 'SIDECAR', 'SWAGGER_UI_FAVICON_HREF': 'SIDECAR', 'REDOC_DIST': 'SIDECAR', + 'PREPROCESSING_HOOKS': [ + 'drf_spectacular.hooks.preprocess_exclude_path_format' + ], } DATETIME_FORMAT = 'j N Y H:i:s' From 8d59462561d995112efff2d26cebabf7e29def41 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 29 Jul 2024 16:53:03 +0930 Subject: [PATCH 060/123] fix(api): Ensure that organizations can't be created via the API !44 fixes #155 --- .../test_organizaiton_permission_api.py | 70 ++++++++++++++++++- app/api/views/access.py | 26 +++++-- 2 files changed, 90 insertions(+), 6 deletions(-) diff --git a/app/access/tests/unit/organization/test_organizaiton_permission_api.py b/app/access/tests/unit/organization/test_organizaiton_permission_api.py index 94842b3e..754a7999 100644 --- a/app/access/tests/unit/organization/test_organizaiton_permission_api.py +++ b/app/access/tests/unit/organization/test_organizaiton_permission_api.py @@ -5,7 +5,7 @@ from django.contrib.auth import get_user_model from django.contrib.auth.models import AnonymousUser, User from django.contrib.contenttypes.models import ContentType from django.shortcuts import reverse -from django.test import TestCase +from django.test import Client, TestCase from access.models import Organization, Team, TeamUsers, Permission @@ -24,7 +24,7 @@ class OrganizationPermissionsAPI(TestCase, APIPermissionChange, APIPermissionVie url_name = '_api_organization' - url_list = 'device-list' + url_list = '_api_orgs' change_data = {'name': 'device'} @@ -124,6 +124,8 @@ class OrganizationPermissionsAPI(TestCase, APIPermissionChange, APIPermissionVie delete_team.permissions.set([delete_permissions]) + self.super_user = User.objects.create_user(username="super_user", password="password", is_superuser=True) + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") @@ -171,3 +173,67 @@ class OrganizationPermissionsAPI(TestCase, APIPermissionChange, APIPermissionVie team = different_organization_team, user = self.different_organization_user ) + + + def test_add_is_prohibited_anon_user(self): + """ Ensure Organization cant be created + + Attempt to create organization as anon user + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_list) + + + # client.force_login(self.add_user) + response = client.post(url, data={'name': 'should not create'}, content_type='application/json') + + assert response.status_code == 401 + + + def test_add_is_prohibited_diff_org_user(self): + """ Ensure Organization cant be created + + Attempt to create organization as user with different org permissions. + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_list) + + + client.force_login(self.different_organization_user) + response = client.post(url, data={'name': 'should not create'}, content_type='application/json') + + assert response.status_code == 405 + + + def test_add_is_prohibited_super_user(self): + """ Ensure Organization cant be created + + Attempt to create organization as user who is super user + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_list) + + + client.force_login(self.super_user) + response = client.post(url, data={'name': 'should not create'}, content_type='application/json') + + assert response.status_code == 405 + + + def test_add_is_prohibited_user_same_org(self): + """ Ensure Organization cant be created + + Attempt to create organization as user with permission + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_list) + + + client.force_login(self.add_user) + response = client.post(url, data={'name': 'should not create'}, content_type='application/json') + + assert response.status_code == 405 diff --git a/app/api/views/access.py b/app/api/views/access.py index 524f5825..e428863d 100644 --- a/app/api/views/access.py +++ b/app/api/views/access.py @@ -1,5 +1,7 @@ from django.contrib.auth.models import Permission +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + from rest_framework import generics, routers, serializers, views from rest_framework.permissions import DjangoObjectPermissions from rest_framework.response import Response @@ -7,12 +9,17 @@ from rest_framework.response import Response from access.mixin import OrganizationMixin from access.models import Organization, Team -from api.serializers.access import OrganizationSerializer, OrganizationListSerializer, TeamSerializer +from api.serializers.access import OrganizationSerializer, OrganizationListSerializer, TeamSerializer, TeamPermissionSerializer from api.views.mixin import OrganizationPermissionAPI - -class OrganizationList(generics.ListCreateAPIView): +@extend_schema_view( + get=extend_schema( + summary = "Fetch Organizations", + description="Returns a list of organizations." + ), +) +class OrganizationList(generics.ListAPIView): permission_classes = [ OrganizationPermissionAPI @@ -28,7 +35,18 @@ class OrganizationList(generics.ListCreateAPIView): -class OrganizationDetail(generics.RetrieveUpdateDestroyAPIView): +@extend_schema_view( + get=extend_schema( + summary = "Get An Organization", + ), + patch=extend_schema( + summary = "Update an organization", + ), + put=extend_schema( + summary = "Update an organization", + ), +) +class OrganizationDetail(generics.RetrieveUpdateAPIView): permission_classes = [ OrganizationPermissionAPI From 3a9e4b29b3586531969c7e2677e72fde5f261983 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 29 Jul 2024 17:02:52 +0930 Subject: [PATCH 061/123] fix(api): confirm HTTP method is allowed before permission check return HTTP/405 for logged in user ONLY!! !44 #159 --- app/api/views/mixin.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/api/views/mixin.py b/app/api/views/mixin.py index 5f205db4..e2d77bc8 100644 --- a/app/api/views/mixin.py +++ b/app/api/views/mixin.py @@ -1,6 +1,7 @@ from django.core.exceptions import PermissionDenied from django.forms import ValidationError +from rest_framework import exceptions from rest_framework.permissions import DjangoObjectPermissions from access.mixin import OrganizationMixin @@ -28,12 +29,16 @@ class OrganizationPermissionAPI(DjangoObjectPermissions, OrganizationMixin): self.request = request + method = self.request._request.method.lower() + + if method.upper() not in view.allowed_methods: + + view.http_method_not_allowed(request._request) + if hasattr(view, 'queryset'): if view.queryset.model._meta: self.obj = view.queryset.model - method = self.request._request.method.lower() - object_organization = None if method == 'get': From 9b673f4a0778b774be64ca361e3ebfd8fce628e8 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 29 Jul 2024 17:03:25 +0930 Subject: [PATCH 062/123] fix(api): cleanup team post/get !44 #159 --- app/api/serializers/access.py | 26 +++++--- app/api/views/access.py | 117 ++++++++++++++++++++++++++++++++-- 2 files changed, 130 insertions(+), 13 deletions(-) diff --git a/app/api/serializers/access.py b/app/api/serializers/access.py index 45262537..623352be 100644 --- a/app/api/serializers/access.py +++ b/app/api/serializers/access.py @@ -14,9 +14,8 @@ class TeamSerializerBase(serializers.ModelSerializer): class Meta: model = Team fields = ( - "id", - "team_name", - 'organization', + 'team_name', + 'permissions', 'url', ) @@ -29,9 +28,18 @@ class TeamSerializerBase(serializers.ModelSerializer): +class TeamPermissionSerializer(serializers.ModelSerializer): + + + class Meta: + model = Permission + depth = 1 + fields = '__all__' + + class TeamSerializer(TeamSerializerBase): - permissions = serializers.SerializerMethodField('get_url') + permissions_url = serializers.SerializerMethodField('get_url') def get_url(self, obj): @@ -63,16 +71,18 @@ class TeamSerializer(TeamSerializerBase): class Meta: model = Team - depth = 1 + depth = 2 fields = ( "id", "team_name", - 'organization', 'permissions', + 'permissions_url', 'url', ) read_only_fields = [ - 'permissions', + 'id', + 'organization', + 'permissions_url', 'url' ] @@ -111,7 +121,7 @@ class OrganizationSerializer(serializers.ModelSerializer): return request.build_absolute_uri(reverse('API:_api_organization_teams', args=[obj.id])) - teams = TeamSerializerBase(source='team_set', many=True, read_only=False) + teams = TeamSerializer(source='team_set', many=True, read_only=False) view_name="API:_api_organization" diff --git a/app/api/views/access.py b/app/api/views/access.py index e428863d..ee892eca 100644 --- a/app/api/views/access.py +++ b/app/api/views/access.py @@ -62,6 +62,20 @@ class OrganizationDetail(generics.RetrieveUpdateAPIView): +@extend_schema_view( + post=extend_schema( + summary = "Create a Team", + description = """Create a team within the defined organization.""", + tags = ['team',], + request = TeamSerializer, + responses = { + 200: OpenApiResponse(description='Team has been updated with the supplied permissions'), + 401: OpenApiResponse(description='User Not logged in'), + 403: OpenApiResponse(description='User is missing permission or in different organization'), + } + ), + create=extend_schema(exclude=True), +) class TeamList(generics.ListCreateAPIView): permission_classes = [ @@ -84,6 +98,45 @@ class TeamList(generics.ListCreateAPIView): +@extend_schema_view( + get=extend_schema( + summary = "Fetch a Team", + description = """Fetch a team within the defined organization. + """, + methods=["GET"], + tags = ['team',], + request = TeamSerializer, + responses = { + 200: OpenApiResponse(description='Team has been updated with the supplied permissions'), + 401: OpenApiResponse(description='User Not logged in'), + 403: OpenApiResponse(description='User is missing permission or in different organization'), + } + ), + patch=extend_schema( + summary = "Update a Team", + description = """Update a team within the defined organization. + """, + methods=["Patch"], + tags = ['team',], + request = TeamSerializer, + responses = { + 200: OpenApiResponse(description='Team has been updated with the supplied permissions'), + 401: OpenApiResponse(description='User Not logged in'), + 403: OpenApiResponse(description='User is missing permission or in different organization'), + } + ), + put = extend_schema( + summary = "Amend a team", + tags = ['team',], + ), + delete=extend_schema( + summary = "Delete a Team", + tags = ['team',], + ), + post = extend_schema( + exclude = True, + ) +) class TeamDetail(generics.RetrieveUpdateDestroyAPIView): permission_classes = [ @@ -97,12 +150,66 @@ class TeamDetail(generics.RetrieveUpdateDestroyAPIView): -class TeamPermissionDetail(routers.APIRootView): +@extend_schema_view( + get=extend_schema( + summary = "Fetch a teams permissions", + tags = ['team',], + ), + post=extend_schema( + summary = "Replace team Permissions", + description = """Replace the teams permissions with the permissions supplied. - # temp disabled until permission checker updated - # permission_classes = [ - # OrganizationPermissionAPI - # ] +Teams Permissions will be replaced with the permissions supplied. **ALL** existing permissions will be +removed. + +permissions are required to be in format `__` + """, + + methods=["POST"], + tags = ['team',], + request = TeamPermissionSerializer, + responses = { + 200: OpenApiResponse(description='Team has been updated with the supplied permissions'), + 401: OpenApiResponse(description='User Not logged in'), + 403: OpenApiResponse(description='User is missing permission or in different organization'), + } + ), + delete=extend_schema( + summary = "Delete permissions", + tags = ['team',], + ), + patch = extend_schema( + summary = "Amend team Permissions", + description = """Amend the teams permissions with the permissions supplied. + +Teams permissions will include the existing permissions along with the ones supplied. +permissions are required to be in format `__
` + """, + + methods=["PATCH"], + parameters = None, + tags = ['team',], + request = TeamPermissionSerializer, + responses = { + 200: OpenApiResponse(description='Team has been updated with the supplied permissions'), + 401: OpenApiResponse(description='User Not logged in'), + 403: OpenApiResponse(description='User is missing permission or in different organization'), + } + ), + put = extend_schema( + summary = "Amend team Permissions", + tags = ['team',], + ) +) +class TeamPermissionDetail(views.APIView): + + permission_classes = [ + OrganizationPermissionAPI + ] + + queryset = Team.objects.all() + + serializer_class = TeamPermissionSerializer def get(self, request, *args, **kwargs): From 289668bb7f0fd5b947a8e8612ae652f4bebcb5fa Mon Sep 17 00:00:00 2001 From: nfc_bot Date: Mon, 29 Jul 2024 07:54:33 +0000 Subject: [PATCH 063/123] =?UTF-8?q?bump:=20version=201.0.0-b3=20=E2=86=92?= =?UTF-8?q?=201.0.0-b4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cz.yaml | 2 +- CHANGELOG.md | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/.cz.yaml b/.cz.yaml index bf4e26a4..d695183d 100644 --- a/.cz.yaml +++ b/.cz.yaml @@ -4,5 +4,5 @@ commitizen: prerelease_offset: 1 tag_format: $version update_changelog_on_bump: false - version: 1.0.0-b3 + version: 1.0.0-b4 version_scheme: semver diff --git a/CHANGELOG.md b/CHANGELOG.md index 67c880b7..64cb906f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +## 1.0.0-b4 (2024-07-29) + +### Feat + +- **swagger**: remove `{format}` suffixed doc entries + +### Fix + +- **api**: cleanup team post/get +- **api**: confirm HTTP method is allowed before permission check +- **api**: Ensure that organizations can't be created via the API +- **access**: Team model class inheritance order corrected + ## 1.0.0-b3 (2024-07-21) ### Fix From f5ba608ed1e7d2bebde67ce1df36b09f279175ac Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 31 Jul 2024 21:19:55 +0930 Subject: [PATCH 064/123] ci: var to export for use in script !45 --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 78efb120..514c07a4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -151,7 +151,7 @@ Docker Container: script: - if [ "$CI_COMMIT_BRANCH" == "development" ] ; then RELEASE_CHANGELOG=$(cz bump --changelog --changelog-to-stdout --prerelease beta); else RELEASE_CHANGELOG=$(cz bump --changelog --changelog-to-stdout); fi - RELEASE_VERSION_NEW=$(cz version --project) - - RELEASE_TAG=$RELEASE_VERSION_NEW + - export RELEASE_TAG=$RELEASE_VERSION_NEW - echo "[DEBUG] RELEASE_VERSION_CURRENT[$RELEASE_VERSION_CURRENT]" - echo "[DEBUG] RELEASE_CHANGELOG[$RELEASE_CHANGELOG]" - echo "[DEBUG] RELEASE_VERSION_NEW[$RELEASE_VERSION_NEW]" From 968b3a0f929b67b5a3b1d55e04419c2da78a725b Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 31 Jul 2024 22:24:43 +0930 Subject: [PATCH 065/123] feat(api): Ability to fetch configgroups from api along with config !45 #160 nofusscomputing/projects/ansible/collections/centurion_erp_collection#4 --- app/api/serializers/config.py | 86 +++++++++++++++++++++++++++++++++++ app/api/urls.py | 5 +- app/api/views/config.py | 54 ++++++++++++++++++++++ app/api/views/index.py | 1 + 4 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 app/api/serializers/config.py create mode 100644 app/api/views/config.py diff --git a/app/api/serializers/config.py b/app/api/serializers/config.py new file mode 100644 index 00000000..f190a1bb --- /dev/null +++ b/app/api/serializers/config.py @@ -0,0 +1,86 @@ +from rest_framework import serializers +from rest_framework.reverse import reverse + +from config_management.models.groups import ConfigGroups + + + +class ParentGroupSerializer(serializers.ModelSerializer): + + url = serializers.SerializerMethodField('get_url') + + + class Meta: + model = ConfigGroups + fields = [ + 'id', + 'name', + 'url', + ] + read_only_fields = [ + 'id', + 'name', + 'url', + ] + + + def get_url(self, obj): + + request = self.context.get('request') + + return request.build_absolute_uri(reverse("API:_api_config_group", args=[obj.pk])) + + + +class ConfigGroupsSerializerBase(serializers.ModelSerializer): + + parent = ParentGroupSerializer(read_only=True) + url = serializers.SerializerMethodField('get_url') + + + class Meta: + model = ConfigGroups + fields = [ + 'id', + 'parent', + 'name', + 'config', + 'url', + ] + read_only_fields = [ + 'id', + 'name', + 'config', + 'url', + ] + + + def get_url(self, obj): + + request = self.context.get('request') + + return request.build_absolute_uri(reverse("API:_api_config_group", args=[obj.pk])) + + + +class ConfigGroupsSerializer(ConfigGroupsSerializerBase): + + + class Meta: + model = ConfigGroups + depth = 1 + fields = [ + 'id', + 'parent', + 'name', + 'config', + 'url', + ] + read_only_fields = [ + 'id', + 'parent', + 'name', + 'config', + 'url', + ] + diff --git a/app/api/urls.py b/app/api/urls.py index a2f760d0..d5d51ecc 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -3,7 +3,7 @@ from django.urls import path from rest_framework.routers import DefaultRouter from rest_framework.urlpatterns import format_suffix_patterns -from .views import access, index +from .views import access, config, index from .views.itam import software, config as itam_config from .views.itam.device import DeviceViewSet @@ -24,6 +24,9 @@ router.register('software', software.SoftwareViewSet, basename='software') urlpatterns = [ path("config//", itam_config.View.as_view(), name="_api_device_config"), + path("configuration/", config.ConfigGroupsList.as_view(), name='_api_config_groups'), + path("configuration/", config.ConfigGroupsDetail.as_view(), name='_api_config_group'), + path("device/inventory", inventory.Collect.as_view(), name="_api_device_inventory"), path("organization/", access.OrganizationList.as_view(), name='_api_orgs'), diff --git a/app/api/views/config.py b/app/api/views/config.py new file mode 100644 index 00000000..728fbe87 --- /dev/null +++ b/app/api/views/config.py @@ -0,0 +1,54 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view + +from rest_framework import generics + +from api.serializers.config import ConfigGroupsSerializer +from api.views.mixin import OrganizationPermissionAPI + +from config_management.models.groups import ConfigGroups + + + +@extend_schema_view( + get=extend_schema( + summary = "Fetch Config groups", + description="Returns a list of Config Groups." + ), +) +class ConfigGroupsList(generics.ListAPIView): + + permission_classes = [ + OrganizationPermissionAPI + ] + + queryset = ConfigGroups.objects.all() + lookup_field = 'pk' + serializer_class = ConfigGroupsSerializer + + + def get_view_name(self): + return "Config Groups" + + + +@extend_schema_view( + get=extend_schema( + summary = "Get A Config Group", + # responses = {} + ), +) +class ConfigGroupsDetail(generics.RetrieveAPIView): + + permission_classes = [ + OrganizationPermissionAPI + ] + + queryset = ConfigGroups.objects.all() + lookup_field = 'pk' + serializer_class = ConfigGroupsSerializer + + + def get_view_name(self): + return "Config Group" + + diff --git a/app/api/views/index.py b/app/api/views/index.py index 4dd6b710..f15b79f6 100644 --- a/app/api/views/index.py +++ b/app/api/views/index.py @@ -27,6 +27,7 @@ class Index(viewsets.ViewSet): { # "teams": reverse("_api_teams", request=request), "devices": reverse("API:device-list", request=request), + "config_groups": reverse("API:_api_config_groups", request=request), "organizations": reverse("API:_api_orgs", request=request), "software": reverse("API:software-list", request=request), } From 4fd157a785d354b2a97baaa1196d95bec3b5bdc2 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 31 Jul 2024 22:27:45 +0930 Subject: [PATCH 066/123] test(api): test configgroups API fields tests for type and existence !45 closes #160 --- .../config_groups/test_config_groups_api.py | 224 ++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 app/config_management/tests/unit/config_groups/test_config_groups_api.py diff --git a/app/config_management/tests/unit/config_groups/test_config_groups_api.py b/app/config_management/tests/unit/config_groups/test_config_groups_api.py new file mode 100644 index 00000000..838e1bd2 --- /dev/null +++ b/app/config_management/tests/unit/config_groups/test_config_groups_api.py @@ -0,0 +1,224 @@ +import pytest +import unittest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from app.tests.abstract.models import TenancyModel + +from config_management.models.groups import ConfigGroups + + + +@pytest.mark.django_db +class ConfigGroupsAPI( + TestCase, +): + + model = ConfigGroups + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + + self.item = self.model.objects.create( + organization = self.organization, + name = 'one', + config = dict({"key": "one", "existing": "dont_over_write"}) + ) + + self.second_item = self.model.objects.create( + organization = self.organization, + name = 'one_two', + config = dict({"key": "two"}), + parent = self.item + ) + + self.url_view_kwargs = {'pk': self.second_item.id} + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + client = Client() + url = reverse('API:_api_config_group', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + def test_api_field_exists_id(self): + """ Test for existance of API Field + + id field must exist + """ + + assert 'id' in self.api_data + + + def test_api_field_type_id(self): + """ Test for type for API Field + + id field must be int + """ + + assert type(self.api_data['id']) is int + + + def test_api_field_exists_parent(self): + """ Test for existance of API Field + + parent field must exist + """ + + assert 'parent' in self.api_data + + + def test_api_field_type_parent(self): + """ Test for type for API Field + + parent field must be dict + """ + + assert type(self.api_data['parent']) is dict + + + def test_api_field_exists_name(self): + """ Test for existance of API Field + + name field must exist + """ + + assert 'name' in self.api_data + + + def test_api_field_type_name(self): + """ Test for type for API Field + + name field must be str + """ + + assert type(self.api_data['name']) is str + + + def test_api_field_exists_config(self): + """ Test for existance of API Field + + config field must exist + """ + + assert 'config' in self.api_data + + + def test_api_field_type_config(self): + """ Test for type for API Field + + config field must be dict + """ + + assert type(self.api_data['config']) is dict + + + def test_api_field_exists_url(self): + """ Test for existance of API Field + + url field must exist + """ + + assert 'url' in self.api_data + + + def test_api_field_type_url(self): + """ Test for type for API Field + + url field must be str + """ + + assert type(self.api_data['url']) is str + + + + def test_api_field_exists_parent_id(self): + """ Test for existance of API Field + + parent.id field must exist + """ + + assert 'id' in self.api_data['parent'] + + + def test_api_field_type_parent_id(self): + """ Test for type for API Field + + parent.id field must be int + """ + + assert type(self.api_data['parent']['id']) is int + + + def test_api_field_exists_parent_name(self): + """ Test for existance of API Field + + parent.name field must exist + """ + + assert 'name' in self.api_data['parent'] + + + def test_api_field_type_parent_name(self): + """ Test for type for API Field + + parent.name field must be str + """ + + assert type(self.api_data['parent']['name']) is str + + + def test_api_field_exists_parent_url(self): + """ Test for existance of API Field + + parent.url field must exist + """ + + assert 'url' in self.api_data['parent'] + + + def test_api_field_type_parent_url(self): + """ Test for type for API Field + + parent.url field must be str + """ + + assert type(self.api_data['parent']['url']) is str From 281d839801154a9a440cd503afbc04586d648d21 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 1 Aug 2024 00:32:16 +0930 Subject: [PATCH 067/123] feat(api): Add device config groups to devices !45 #160 nofusscomputing/projects/ansible/collections/centurion_erp_collection!7 nofusscomputing/projects/ansible/collections/centurion_erp_collection#4 --- app/api/serializers/itam/device.py | 54 ++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/app/api/serializers/itam/device.py b/app/api/serializers/itam/device.py index cc9eeacc..a63e5d44 100644 --- a/app/api/serializers/itam/device.py +++ b/app/api/serializers/itam/device.py @@ -1,9 +1,38 @@ from django.urls import reverse -from itam.models.device import Device from rest_framework import serializers +from api.serializers.config import ParentGroupSerializer +from config_management.models.groups import ConfigGroupHosts + +from itam.models.device import Device + + + +class DeviceConfigGroupsSerializer(serializers.ModelSerializer): + + name = serializers.CharField(source='group.name', read_only=True) + + url = serializers.HyperlinkedIdentityField( + view_name="API:_api_config_group", format="html" + ) + + class Meta: + + model = ConfigGroupHosts + + fields = [ + 'id', + 'name', + 'url', + + ] + read_only_fields = [ + 'id', + 'name', + 'url', + ] class DeviceSerializer(serializers.ModelSerializer): @@ -13,7 +42,9 @@ class DeviceSerializer(serializers.ModelSerializer): ) config = serializers.SerializerMethodField('get_device_config') - + + groups = DeviceConfigGroupsSerializer(source='configgrouphosts_set', many=True, read_only=False) + def get_device_config(self, device): request = self.context.get('request') @@ -22,11 +53,28 @@ class DeviceSerializer(serializers.ModelSerializer): class Meta: model = Device - fields = '__all__' + depth = 1 + fields = [ + 'id', + 'is_global', + 'name', + 'config', + 'serial_number', + 'uuid', + 'inventorydate', + 'created', + 'modified', + 'groups', + 'organization', + 'url', + ] read_only_fields = [ + 'created', + 'modified', 'inventorydate', 'is_global', 'slug', + 'groups', ] From 213644a51a11169fcb71251f967e074274918633 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 1 Aug 2024 01:12:33 +0930 Subject: [PATCH 068/123] test(api): Field existence and type checks for device checks for required fields !45 #160 #162 --- app/itam/tests/unit/device/test_device_api.py | 522 ++++++++++++++++++ 1 file changed, 522 insertions(+) create mode 100644 app/itam/tests/unit/device/test_device_api.py diff --git a/app/itam/tests/unit/device/test_device_api.py b/app/itam/tests/unit/device/test_device_api.py new file mode 100644 index 00000000..2b4aa259 --- /dev/null +++ b/app/itam/tests/unit/device/test_device_api.py @@ -0,0 +1,522 @@ +import pytest +import unittest + +from django.contrib.auth.models import AnonymousUser, User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions import APIPermissions + +from config_management.models.groups import ConfigGroups, ConfigGroupHosts + +from itam.models.device import Device + + +class DeviceAPI(TestCase): + + + model = Device + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a device + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + self.item = self.model.objects.create( + organization=organization, + name = 'deviceone', + uuid = 'val', + serial_number = 'another val' + ) + + config_group = ConfigGroups.objects.create( + organization = self.organization, + name = 'one', + config = dict({"key": "one", "existing": "dont_over_write"}) + ) + + config_group_second_item = ConfigGroups.objects.create( + organization = self.organization, + name = 'one_two', + config = dict({"key": "two"}), + parent = config_group + ) + + config_group_hosts = ConfigGroupHosts.objects.create( + organization = organization, + host = self.item, + group = config_group, + ) + + + config_group_hosts_two = ConfigGroupHosts.objects.create( + organization = organization, + host = self.item, + group = config_group_second_item, + ) + + + # self.url_kwargs = {'pk': self.item.id} + + self.url_view_kwargs = {'pk': self.item.id} + + # self.add_data = {'name': 'device', 'organization': self.organization.id} + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + # add_permissions = Permission.objects.get( + # codename = 'add_' + self.model._meta.model_name, + # content_type = ContentType.objects.get( + # app_label = self.model._meta.app_label, + # model = self.model._meta.model_name, + # ) + # ) + + # add_team = Team.objects.create( + # team_name = 'add_team', + # organization = organization, + # ) + + # add_team.permissions.set([add_permissions]) + + + + # change_permissions = Permission.objects.get( + # codename = 'change_' + self.model._meta.model_name, + # content_type = ContentType.objects.get( + # app_label = self.model._meta.app_label, + # model = self.model._meta.model_name, + # ) + # ) + + # change_team = Team.objects.create( + # team_name = 'change_team', + # organization = organization, + # ) + + # change_team.permissions.set([change_permissions]) + + + + # delete_permissions = Permission.objects.get( + # codename = 'delete_' + self.model._meta.model_name, + # content_type = ContentType.objects.get( + # app_label = self.model._meta.app_label, + # model = self.model._meta.model_name, + # ) + # ) + + # delete_team = Team.objects.create( + # team_name = 'delete_team', + # organization = organization, + # ) + + # delete_team.permissions.set([delete_permissions]) + + + # self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + # self.add_user = User.objects.create_user(username="test_user_add", password="password") + # teamuser = TeamUsers.objects.create( + # team = add_team, + # user = self.add_user + # ) + + # self.change_user = User.objects.create_user(username="test_user_change", password="password") + # teamuser = TeamUsers.objects.create( + # team = change_team, + # user = self.change_user + # ) + + # self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + # teamuser = TeamUsers.objects.create( + # team = delete_team, + # user = self.delete_user + # ) + + + # self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + # different_organization_team = Team.objects.create( + # team_name = 'different_organization_team', + # organization = different_organization, + # ) + + # different_organization_team.permissions.set([ + # view_permissions, + # add_permissions, + # change_permissions, + # delete_permissions, + # ]) + + # TeamUsers.objects.create( + # team = different_organization_team, + # user = self.different_organization_user + # ) + + + client = Client() + url = reverse('API:device-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + def test_api_field_exists_id(self): + """ Test for existance of API Field + + id field must exist + """ + + assert 'id' in self.api_data + + + def test_api_field_type_id(self): + """ Test for type for API Field + + id field must be int + """ + + assert type(self.api_data['id']) is int + + + def test_api_field_exists_is_global(self): + """ Test for existance of API Field + + is_global field must exist + """ + + assert 'is_global' in self.api_data + + + def test_api_field_type_is_global(self): + """ Test for type for API Field + + is_global field must be boolean + """ + + assert type(self.api_data['is_global']) is bool + + + def test_api_field_exists_name(self): + """ Test for existance of API Field + + name field must exist + """ + + assert 'name' in self.api_data + + + def test_api_field_type_name(self): + """ Test for type for API Field + + name field must be str + """ + + assert type(self.api_data['name']) is str + + + def test_api_field_exists_config(self): + """ Test for existance of API Field + + config field must exist + """ + + assert 'config' in self.api_data + + + def test_api_field_type_config(self): + """ Test for type for API Field + + config field must be dict + """ + + assert type(self.api_data['config']) is str + + + def test_api_field_exists_serial_number(self): + """ Test for existance of API Field + + serial_number field must exist + """ + + assert 'serial_number' in self.api_data + + + def test_api_field_type_serial_number(self): + """ Test for type for API Field + + serial_number field must be str + """ + + assert type(self.api_data['serial_number']) is str + + + def test_api_field_exists_uuid(self): + """ Test for existance of API Field + + uuid field must exist + """ + + assert 'uuid' in self.api_data + + + def test_api_field_type_uuid(self): + """ Test for type for API Field + + uuid field must be str + """ + + assert type(self.api_data['uuid']) is str + + + def test_api_field_exists_inventorydate(self): + """ Test for existance of API Field + + inventorydate field must exist + """ + + assert 'inventorydate' in self.api_data + + + def test_api_field_type_inventorydate(self): + """ Test for type for API Field + + inventorydate field must be str + """ + + assert ( + type(self.api_data['inventorydate']) is str + or + self.api_data['inventorydate'] is None + ) + + + def test_api_field_exists_created(self): + """ Test for existance of API Field + + created field must exist + """ + + assert 'created' in self.api_data + + + def test_api_field_type_created(self): + """ Test for type for API Field + + created field must be str + """ + + assert type(self.api_data['created']) is str + + + def test_api_field_exists_modified(self): + """ Test for existance of API Field + + modified field must exist + """ + + assert 'modified' in self.api_data + + + def test_api_field_type_modified(self): + """ Test for type for API Field + + modified field must be str + """ + + assert type(self.api_data['modified']) is str + + + def test_api_field_exists_groups(self): + """ Test for existance of API Field + + groups field must exist + """ + + assert 'groups' in self.api_data + + + def test_api_field_type_groups(self): + """ Test for type for API Field + + groups field must be list + """ + + assert type(self.api_data['groups']) is list + + + def test_api_field_exists_organization(self): + """ Test for existance of API Field + + organization field must exist + """ + + assert 'organization' in self.api_data + + + def test_api_field_type_organization(self): + """ Test for type for API Field + + organization field must be dict + """ + + assert type(self.api_data['organization']) is dict + + + def test_api_field_exists_url(self): + """ Test for existance of API Field + + url field must exist + """ + + assert 'url' in self.api_data + + + def test_api_field_type_url(self): + """ Test for type for API Field + + url field must be str + """ + + assert type(self.api_data['url']) is Hyperlink + + + + + def test_api_field_exists_organization_id(self): + """ Test for existance of API Field + + organization.id field must exist + """ + + assert 'id' in self.api_data['organization'] + + + def test_api_field_type_organization_id(self): + """ Test for type for API Field + + organization.id field must be int + """ + + assert type(self.api_data['organization']['id']) is int + + + def test_api_field_exists_organization_name(self): + """ Test for existance of API Field + + organization.name field must exist + """ + + assert 'name' in self.api_data['organization'] + + + def test_api_field_type_organization_name(self): + """ Test for type for API Field + + organization.name field must be str + """ + + assert type(self.api_data['organization']['name']) is str + + + + + def test_api_field_exists_groups_id(self): + """ Test for existance of API Field + + groups.id field must exist + """ + + assert 'id' in self.api_data['groups'][0] + + + def test_api_field_type_groups_id(self): + """ Test for type for API Field + + groups.id field must be int + """ + + assert type(self.api_data['groups'][0]['id']) is int + + + def test_api_field_exists_groups_name(self): + """ Test for existance of API Field + + groups.name field must exist + """ + + assert 'name' in self.api_data['groups'][0] + + + def test_api_field_type_groups_name(self): + """ Test for type for API Field + + groups.name field must be str + """ + + assert type(self.api_data['groups'][0]['name']) is str + + + def test_api_field_exists_groups_url(self): + """ Test for existance of API Field + + groups.url field must exist + """ + + assert 'url' in self.api_data['groups'][0] + + + def test_api_field_type_groups_url(self): + """ Test for type for API Field + + groups.url field must be str + """ + + assert type(self.api_data['groups'][0]['url']) is Hyperlink From 2f55024f0b34013ce944ec524094446705dcddc4 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 1 Aug 2024 01:54:24 +0930 Subject: [PATCH 069/123] fix(api): Ensure device groups is read only checks for required fields !45 #160 #162 --- app/api/serializers/itam/device.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/api/serializers/itam/device.py b/app/api/serializers/itam/device.py index a63e5d44..3517c867 100644 --- a/app/api/serializers/itam/device.py +++ b/app/api/serializers/itam/device.py @@ -43,7 +43,7 @@ class DeviceSerializer(serializers.ModelSerializer): config = serializers.SerializerMethodField('get_device_config') - groups = DeviceConfigGroupsSerializer(source='configgrouphosts_set', many=True, read_only=False) + groups = DeviceConfigGroupsSerializer(source='configgrouphosts_set', many=True, read_only=True) def get_device_config(self, device): @@ -70,11 +70,12 @@ class DeviceSerializer(serializers.ModelSerializer): ] read_only_fields = [ + 'id', + 'config', + 'inventorydate', 'created', 'modified', - 'inventorydate', - 'is_global', - 'slug', 'groups', + 'url', ] From a4b37b34a91e0fb7416d889d508d1851c3bf108d Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 1 Aug 2024 02:12:42 +0930 Subject: [PATCH 070/123] docs: Add dets of collection !45 closes #161 --- docs/projects/ansible/collections/centurion/index.md | 0 docs/projects/centurion_erp/administration/index.md | 5 +++++ docs/projects/centurion_erp/index.md | 2 ++ 3 files changed, 7 insertions(+) create mode 100644 docs/projects/ansible/collections/centurion/index.md diff --git a/docs/projects/ansible/collections/centurion/index.md b/docs/projects/ansible/collections/centurion/index.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/projects/centurion_erp/administration/index.md b/docs/projects/centurion_erp/administration/index.md index e25d7cf4..70e4f23d 100644 --- a/docs/projects/centurion_erp/administration/index.md +++ b/docs/projects/centurion_erp/administration/index.md @@ -16,3 +16,8 @@ This documentation is targeted towards those whom administer the applications de - [Backup](./backup.md) - [Installation](./installation.md) + + +## Ansible Automation Platform / AWX + +We have built an [Ansible Collection](../../ansible/collections/centurion/index.md) for Centurion ERP that you could consider the bridge between the config within Centurion and the end device. This collection can be directly added to AAP / AWX as a project which enables accessing the features the collection has to offer. Please refer to the [collections documentation](../../ansible/collections/centurion/index.md) for further information. diff --git a/docs/projects/centurion_erp/index.md b/docs/projects/centurion_erp/index.md index c0c828df..8579fb33 100644 --- a/docs/projects/centurion_erp/index.md +++ b/docs/projects/centurion_erp/index.md @@ -27,6 +27,8 @@ Whilst there are many Enterprise Rescource Planning (ERP) applications, Centurio Centurion ERP contains the following modules: +- [Companion Ansible Collection](../ansible/collections/centurion/index.md) + - [Configuration Management](./user/config_management/index.md) - [IT Asset Management (ITAM)](./user/itam/index.md) From c496d10c1a72c4132b3822b3e36535ae48a90aeb Mon Sep 17 00:00:00 2001 From: nfc_bot Date: Wed, 31 Jul 2024 17:02:31 +0000 Subject: [PATCH 071/123] =?UTF-8?q?bump:=20version=201.0.0-b4=20=E2=86=92?= =?UTF-8?q?=201.0.0-b5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cz.yaml | 2 +- CHANGELOG.md | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.cz.yaml b/.cz.yaml index d695183d..d84f9ba8 100644 --- a/.cz.yaml +++ b/.cz.yaml @@ -4,5 +4,5 @@ commitizen: prerelease_offset: 1 tag_format: $version update_changelog_on_bump: false - version: 1.0.0-b4 + version: 1.0.0-b5 version_scheme: semver diff --git a/CHANGELOG.md b/CHANGELOG.md index 64cb906f..a855ce88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## 1.0.0-b5 (2024-07-31) + +### Feat + +- **api**: Add device config groups to devices +- **api**: Ability to fetch configgroups from api along with config + +### Fix + +- **api**: Ensure device groups is read only + ## 1.0.0-b4 (2024-07-29) ### Feat From fed0c5c3e577e000c8713264bf6fb5d84795dee9 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 1 Aug 2024 17:29:32 +0930 Subject: [PATCH 072/123] chore: artifacthub preperation !43 --- .gitlab-ci.yml | 4 +++- README.md | 2 +- artifacthub-repo.yml | 6 ++++++ dockerfile | 8 ++++++++ gitlab-ci | 2 +- 5 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 artifacthub-repo.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 514c07a4..d3c50c16 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -49,10 +49,12 @@ Docker Container: echo "[DEBUG] building multiarch/specified arch image"; docker buildx build --platform=$DOCKER_IMAGE_BUILD_TARGET_PLATFORMS . \ - --label org.opencontainers.image.created="$(date '+%Y-%m-%d %H:%M:%S%:z')" \ + --label org.opencontainers.image.created="$(date '+%Y-%m-%dT%H:%M:%S%:z')" \ --label org.opencontainers.image.documentation="$CI_PROJECT_URL" \ --label org.opencontainers.image.source="$CI_PROJECT_URL" \ --label org.opencontainers.image.revision="$CI_COMMIT_SHA" \ + --label io.artifacthub.package.readme-url="$CI_PROJECT_URL/-/raw/development/README.md?ref_type=heads" \ + --label io.artifacthub.package.maintainers='[{"name":"No Fuss Computing","email":"helpdesk@nofusscomputing.com"}, {"name":"jon_nfc","email":"jonathon.lockwood@networkedweb.com"}]' \ --push \ --build-arg CI_PROJECT_URL=$CI_PROJECT_URL \ --build-arg CI_COMMIT_SHA=$CI_COMMIT_SHA \ diff --git a/README.md b/README.md index e731bf5c..4ee1cf33 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ![Project Status - Active](https://img.shields.io/badge/Project%20Status-Active-green?logo=gitlab&style=plastic) -[![Docker Pulls](https://img.shields.io/docker/pulls/nofusscomputing/centurion-erp?style=plastic&logo=docker&color=0db7ed)](https://hub.docker.com/r/nofusscomputing/centurion-erp) +[![Docker Pulls](https://img.shields.io/docker/pulls/nofusscomputing/centurion-erp?style=plastic&logo=docker&color=0db7ed)](https://hub.docker.com/r/nofusscomputing/centurion-erp) [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/centurion-erp)](https://artifacthub.io/packages/search?repo=centurion-erp) diff --git a/artifacthub-repo.yml b/artifacthub-repo.yml new file mode 100644 index 00000000..7676ed31 --- /dev/null +++ b/artifacthub-repo.yml @@ -0,0 +1,6 @@ +repositoryID: 17eaf871-a980-41ba-b841-2a78734535ca +owners: + - name: no-fuss-computing + email: helpdesk@nofusscomputing.com + - name: jon_nfc + email: jonathon.lockwood@networkedweb.com diff --git a/dockerfile b/dockerfile index ee4f00ee..f9785ce0 100644 --- a/dockerfile +++ b/dockerfile @@ -57,6 +57,14 @@ RUN cd /tmp/python_modules \ FROM python:3.11-alpine3.19 +LABEL \ + org.opencontainers.image.vendor="No Fuss Computing" \ + org.opencontainers.image.title="Centurion ERP" \ + org.opencontainers.image.description="An ERP with a focus on ITSM and automation" \ + org.opencontainers.image.vendor="No Fuss Computing" \ + io.artifacthub.package.license="MIT" + + ARG CI_PROJECT_URL ARG CI_COMMIT_SHA ARG CI_COMMIT_TAG diff --git a/gitlab-ci b/gitlab-ci index 673441f8..58ffcabb 160000 --- a/gitlab-ci +++ b/gitlab-ci @@ -1 +1 @@ -Subproject commit 673441f83a7d943434252ee23899e3572cdfb141 +Subproject commit 58ffcabbfb503af3e57d9cb3ab43931b23dc4cd8 From 366579c12bca9ef2ae9d7e0886b98dbcd158b481 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 2 Aug 2024 04:43:14 +0930 Subject: [PATCH 073/123] ci(github): add unit tests !43 --- .github/workflows/unit-test-report.yaml | 34 ++++++++++++ .github/workflows/unit-test.yaml | 69 +++++++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 .github/workflows/unit-test-report.yaml create mode 100644 .github/workflows/unit-test.yaml diff --git a/.github/workflows/unit-test-report.yaml b/.github/workflows/unit-test-report.yaml new file mode 100644 index 00000000..a3e09045 --- /dev/null +++ b/.github/workflows/unit-test-report.yaml @@ -0,0 +1,34 @@ +--- + +name: 'Process Unit Test Artifact' + +on: + workflow_run: + workflows: + - 'Unit Test' + types: + - completed + + +permissions: + contents: read + actions: read + checks: write + + +jobs: + report: + runs-on: ubuntu-latest + strategy: + max-parallel: 4 + matrix: + python-version: ['3.10', '3.11', '3.12'] + steps: + + - name: Test Report + uses: dorny/test-reporter@v1 + with: + artifact: unit-test-results-${{ matrix.python-version }} + name: Unit Test Report [Python ${{ matrix.python-version }}] + path: '*.xml' + reporter: java-junit diff --git a/.github/workflows/unit-test.yaml b/.github/workflows/unit-test.yaml new file mode 100644 index 00000000..2264558a --- /dev/null +++ b/.github/workflows/unit-test.yaml @@ -0,0 +1,69 @@ +name: 'Unit Test' + +on: + push: + branches: + - "development" + tags: + - '*' + pull_request: + branches: + - "development" + + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + max-parallel: 4 + matrix: + python-version: ['3.10', '3.11', '3.12'] + + steps: + + + - uses: actions/checkout@v4 + + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install -r requirements_test.txt + + + - name: Run Tests + run: | + cd app; + pytest --cov --cov-report term --cov-report xml:../coverage.xml --cov-report html:../coverage/ --junit-xml=../unit.JUnit.xml **/tests/unit; + + + - name: Upload Test Report + uses: actions/upload-artifact@v4 + if: success() || failure() + with: + name: unit-test-results-${{ matrix.python-version }} + path: unit.JUnit.xml + + + - name: Upload Coverage Report + uses: actions/upload-artifact@v4 + if: success() || failure() + with: + name: coverage-report-${{ matrix.python-version }} + path: coverage.xml + + + - name: Upload Coverage + uses: actions/upload-artifact@v4 + if: success() || failure() + with: + name: coverage-${{ matrix.python-version }} + path: coverage/* From 5fa88a520951a2b05d30749d2e232c7dc15437f5 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 2 Aug 2024 05:39:46 +0930 Subject: [PATCH 074/123] ci(github): add coverage !43 --- .github/workflows/code-coverage-report.yaml | 93 +++++++++++++++++++++ .github/workflows/unit-test-report.yaml | 15 ++-- .github/workflows/unit-test.yaml | 67 ++++++++++++++- 3 files changed, 164 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/code-coverage-report.yaml diff --git a/.github/workflows/code-coverage-report.yaml b/.github/workflows/code-coverage-report.yaml new file mode 100644 index 00000000..8fc875ce --- /dev/null +++ b/.github/workflows/code-coverage-report.yaml @@ -0,0 +1,93 @@ +--- + +name: 'Process Coverage Artifact' + +on: + workflow_run: + workflows: + - 'Unit Test' + types: + - completed + + +permissions: + contents: read + actions: read + checks: write + + +jobs: + report: + runs-on: ubuntu-latest + # strategy: + # max-parallel: 4 + # matrix: + # python-version: ['3.12'] + name: Coverage + steps: + + - name: Run Tests + run: | + ls -l; + + - name: Download Coverage Artifact + uses: actions/download-artifact@v4 + with: + name: coverage-report-3.12 + # path: coverage.xml + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + + - name: ls + if: success() || failure() + run: | + ls -l; + + - name: Code Coverage Report + uses: irongut/CodeCoverageSummary@v1.3.0 + with: + filename: coverage.xml + badge: true + fail_below_min: true + format: markdown + hide_branch_rate: false + hide_complexity: false + indicators: true + output: both + thresholds: '60 85' + + + # - name: Add Coverage PR Comment + # uses: marocchino/sticky-pull-request-comment@v2 + # if: github.event_name == 'pull_request' + # with: + # recreate: true + # path: code-coverage-results.md + + + - name: ls + if: success() || failure() + run: | + ls -l; + + # - name: Adding markdown + # run: | + # cat $(ls *.md | tail -1) >> $GITHUB_STEP_SUMMARY + + - name: create status check/comment for code coverage results + id: jest_coverage_check + uses: im-open/process-code-coverage-summary@v2.3.0 + with: + # github-token: ${{ secrets.GITHUB_TOKEN }} + github-token: ${{ github.token }} + summary-file: code-coverage-results.md + create-pr-comment: true + update-comment-if-one-exists: true + update-comment-key: "${{ env.GITHUB-JOB }}_${{ env.GITHUB-ACTION }}" + + - name: Upload Coverage Summary + uses: actions/upload-artifact@v4 + if: success() || failure() + with: + name: code-coverage-results-3.12 + path: code-coverage-results.md diff --git a/.github/workflows/unit-test-report.yaml b/.github/workflows/unit-test-report.yaml index a3e09045..61662729 100644 --- a/.github/workflows/unit-test-report.yaml +++ b/.github/workflows/unit-test-report.yaml @@ -25,10 +25,11 @@ jobs: python-version: ['3.10', '3.11', '3.12'] steps: - - name: Test Report - uses: dorny/test-reporter@v1 - with: - artifact: unit-test-results-${{ matrix.python-version }} - name: Unit Test Report [Python ${{ matrix.python-version }}] - path: '*.xml' - reporter: java-junit + - name: Test Report + uses: dorny/test-reporter@v1 + with: + artifact: unit-test-results-${{ matrix.python-version }} + badge-title: 'Unit Tests [Python ${{ matrix.python-version }}]' + name: Unit Test Report [Python ${{ matrix.python-version }}] + path: '*.xml' + reporter: java-junit diff --git a/.github/workflows/unit-test.yaml b/.github/workflows/unit-test.yaml index 2264558a..40cfd331 100644 --- a/.github/workflows/unit-test.yaml +++ b/.github/workflows/unit-test.yaml @@ -12,7 +12,7 @@ on: jobs: - build: + test: runs-on: ubuntu-latest strategy: @@ -46,7 +46,7 @@ jobs: - name: Upload Test Report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v4 if: success() || failure() with: name: unit-test-results-${{ matrix.python-version }} @@ -54,7 +54,7 @@ jobs: - name: Upload Coverage Report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v4 if: success() || failure() with: name: coverage-report-${{ matrix.python-version }} @@ -62,8 +62,67 @@ jobs: - name: Upload Coverage - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v4 if: success() || failure() with: name: coverage-${{ matrix.python-version }} path: coverage/* + + # coverage: + # needs: + # - test + # runs-on: ubuntu-latest + # # strategy: + # # max-parallel: 4 + # # matrix: + # # python-version: ['3.12'] + # name: Coverage + # steps: + + # # - name: Run Tests + # # run: | + # # ls -l; + + # - name: Download Coverage Artifact + # uses: actions/download-artifact@v4 + # with: + # name: coverage-report-3.12 + # # path: coverage.xml + # # run-id: ${{ github.event.workflow_run.id }} + # # github-token: ${{ github.token }} + + + # - name: Add Coverage PR Comment + # uses: marocchino/sticky-pull-request-comment@v2 + # if: github.event_name == 'pull_request' + # with: + # recreate: true + # path: code-coverage-results.md + + + # - name: ls + # if: success() || failure() + # run: | + # ls -l; + + # - name: Code Coverage Report + # uses: irongut/CodeCoverageSummary@v1.3.0 + # with: + # filename: coverage.xml + # badge: true + # fail_below_min: true + # format: markdown + # hide_branch_rate: false + # hide_complexity: false + # indicators: true + # output: both + # thresholds: '60 85' + + # - name: ls + # if: success() || failure() + # run: | + # ls -l; + + # - name: Summary + # run: | + # cat $(ls *.md | tail -1) >> $GITHUB_STEP_SUMMARY From 81a72773cbf97908e8131df06fc9828fbee9aa7a Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 9 Aug 2024 15:15:54 +0930 Subject: [PATCH 075/123] ci: remove gitlab pipelines #216 --- .gitlab-ci.yml | 434 +++++++++++++++++++++++++------------------------ 1 file changed, 218 insertions(+), 216 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d3c50c16..2600e235 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,21 +2,21 @@ variables: MY_PROJECT_ID: "57560288" - GIT_SYNC_URL: "https://$GITHUB_USERNAME_ROBOT:$GITHUB_TOKEN_ROBOT@github.com/NoFussComputing/centurion_erp.git" + # GIT_SYNC_URL: "https://$GITHUB_USERNAME_ROBOT:$GITHUB_TOKEN_ROBOT@github.com/NoFussComputing/centurion_erp.git" - # Docker Build / Publish - DOCKER_IMAGE_BUILD_TARGET_PLATFORMS: "linux/amd64,linux/arm64" - DOCKER_IMAGE_BUILD_NAME: centurion-erp - DOCKER_IMAGE_BUILD_REGISTRY: $CI_REGISTRY_IMAGE - DOCKER_IMAGE_BUILD_TAG: $CI_COMMIT_SHA + # # Docker Build / Publish + # DOCKER_IMAGE_BUILD_TARGET_PLATFORMS: "linux/amd64,linux/arm64" + # DOCKER_IMAGE_BUILD_NAME: centurion-erp + # DOCKER_IMAGE_BUILD_REGISTRY: $CI_REGISTRY_IMAGE + # DOCKER_IMAGE_BUILD_TAG: $CI_COMMIT_SHA - # Docker Publish - DOCKER_IMAGE_PUBLISH_NAME: centurion-erp - DOCKER_IMAGE_PUBLISH_REGISTRY: docker.io/nofusscomputing - DOCKER_IMAGE_PUBLISH_URL: https://hub.docker.com/r/nofusscomputing/$DOCKER_IMAGE_PUBLISH_NAME + # # Docker Publish + # DOCKER_IMAGE_PUBLISH_NAME: centurion-erp + # DOCKER_IMAGE_PUBLISH_REGISTRY: docker.io/nofusscomputing + # DOCKER_IMAGE_PUBLISH_URL: https://hub.docker.com/r/nofusscomputing/$DOCKER_IMAGE_PUBLISH_NAME - # Extra release commands - MY_COMMAND: ./.gitlab/additional_actions_bump.sh + # # Extra release commands + # MY_COMMAND: ./.gitlab/additional_actions_bump.sh # Docs NFC PAGES_ENVIRONMENT_PATH: projects/centurion_erp/ @@ -25,99 +25,101 @@ variables: include: - - local: .gitlab/pytest.gitlab-ci.yml + # - local: .gitlab/pytest.gitlab-ci.yml # - local: .gitlab/unit-test.gitlab-ci.yml - project: nofusscomputing/projects/gitlab-ci ref: development file: - .gitlab-ci_common.yaml - - template/automagic.gitlab-ci.yaml - - -Update Git Submodules: - extends: .ansible_playbook_git_submodule - - -Docker Container: - extends: .build_docker_container - resource_group: build - needs: [] - script: - - update-binfmts --display - - | - - echo "[DEBUG] building multiarch/specified arch image"; - - docker buildx build --platform=$DOCKER_IMAGE_BUILD_TARGET_PLATFORMS . \ - --label org.opencontainers.image.created="$(date '+%Y-%m-%dT%H:%M:%S%:z')" \ - --label org.opencontainers.image.documentation="$CI_PROJECT_URL" \ - --label org.opencontainers.image.source="$CI_PROJECT_URL" \ - --label org.opencontainers.image.revision="$CI_COMMIT_SHA" \ - --label io.artifacthub.package.readme-url="$CI_PROJECT_URL/-/raw/development/README.md?ref_type=heads" \ - --label io.artifacthub.package.maintainers='[{"name":"No Fuss Computing","email":"helpdesk@nofusscomputing.com"}, {"name":"jon_nfc","email":"jonathon.lockwood@networkedweb.com"}]' \ - --push \ - --build-arg CI_PROJECT_URL=$CI_PROJECT_URL \ - --build-arg CI_COMMIT_SHA=$CI_COMMIT_SHA \ - --build-arg CI_COMMIT_TAG=$CI_COMMIT_TAG \ - --file $DOCKER_DOCKERFILE \ - --tag $DOCKER_IMAGE_BUILD_REGISTRY/$DOCKER_IMAGE_BUILD_NAME:$DOCKER_IMAGE_BUILD_TAG; - - docker buildx imagetools inspect $DOCKER_IMAGE_BUILD_REGISTRY/$DOCKER_IMAGE_BUILD_NAME:$DOCKER_IMAGE_BUILD_TAG; - - # during docker multi platform build there are >=3 additional unknown images added to gitlab container registry. cleanup + # - template/automagic.gitlab-ci.yaml + - local: gitlab-ci/automation/.gitlab-ci-ansible.yaml + - local: gitlab-ci/template/mkdocs-documentation.gitlab-ci.yaml + - local: gitlab-ci/lint/ansible.gitlab-ci.yaml - DOCKER_MULTI_ARCH_IMAGES=$(docker buildx imagetools inspect "$DOCKER_IMAGE_BUILD_REGISTRY/$DOCKER_IMAGE_BUILD_NAME:$DOCKER_IMAGE_BUILD_TAG" --format "{{ range .Manifest.Manifests }}{{ if ne (print .Platform) \"&{unknown unknown [] }\" }}$DOCKER_IMAGE_BUILD_REGISTRY/$DOCKER_IMAGE_BUILD_NAME:$DOCKER_IMAGE_BUILD_TAG@{{ println .Digest }}{{end}} {{end}}"); - docker buildx imagetools create $DOCKER_MULTI_ARCH_IMAGES --tag $DOCKER_IMAGE_BUILD_REGISTRY/$DOCKER_IMAGE_BUILD_NAME:$DOCKER_IMAGE_BUILD_TAG; - docker buildx imagetools inspect $DOCKER_IMAGE_BUILD_REGISTRY/$DOCKER_IMAGE_BUILD_NAME:$DOCKER_IMAGE_BUILD_TAG; +# Update Git Submodules: +# extends: .ansible_playbook_git_submodule - rules: # rules manually synced from docker/publish.gitlab-ci.yaml removing git tag - # - if: # condition_master_branch_push - # $CI_COMMIT_BRANCH == "master" && - # $CI_PIPELINE_SOURCE == "push" - # exists: - # - '{dockerfile,dockerfile.j2}' - # when: always +# Docker Container: +# extends: .build_docker_container +# resource_group: build +# needs: [] +# script: +# - update-binfmts --display +# - | - - if: - $CI_COMMIT_AUTHOR =='nfc_bot ' - && - $CI_COMMIT_BRANCH == "development" - when: never +# echo "[DEBUG] building multiarch/specified arch image"; - - if: # condition_not_master_or_dev_push - $CI_COMMIT_BRANCH != "master" && - $CI_COMMIT_BRANCH != "development" && - $CI_PIPELINE_SOURCE == "push" - exists: - - '{dockerfile,dockerfile.j2}' - changes: - paths: - - '{dockerfile,dockerfile.j2,includes/**/*}' - compare_to: 'development' - when: always +# docker buildx build --platform=$DOCKER_IMAGE_BUILD_TARGET_PLATFORMS . \ +# --label org.opencontainers.image.created="$(date '+%Y-%m-%d %H:%M:%S%:z')" \ +# --label org.opencontainers.image.documentation="$CI_PROJECT_URL" \ +# --label org.opencontainers.image.source="$CI_PROJECT_URL" \ +# --label org.opencontainers.image.revision="$CI_COMMIT_SHA" \ +# --push \ +# --build-arg CI_PROJECT_URL=$CI_PROJECT_URL \ +# --build-arg CI_COMMIT_SHA=$CI_COMMIT_SHA \ +# --build-arg CI_COMMIT_TAG=$CI_COMMIT_TAG \ +# --file $DOCKER_DOCKERFILE \ +# --tag $DOCKER_IMAGE_BUILD_REGISTRY/$DOCKER_IMAGE_BUILD_NAME:$DOCKER_IMAGE_BUILD_TAG; - - if: $CI_COMMIT_TAG - exists: - - '{dockerfile,dockerfile.j2}' - when: always +# docker buildx imagetools inspect $DOCKER_IMAGE_BUILD_REGISTRY/$DOCKER_IMAGE_BUILD_NAME:$DOCKER_IMAGE_BUILD_TAG; - - if: # condition_dev_branch_push - ( - $CI_COMMIT_BRANCH == "development" - || - $CI_COMMIT_BRANCH == "master" - ) - && - $CI_PIPELINE_SOURCE == "push" - exists: - - '{dockerfile,dockerfile.j2}' - allow_failure: true - when: on_success +# # during docker multi platform build there are >=3 additional unknown images added to gitlab container registry. cleanup + +# DOCKER_MULTI_ARCH_IMAGES=$(docker buildx imagetools inspect "$DOCKER_IMAGE_BUILD_REGISTRY/$DOCKER_IMAGE_BUILD_NAME:$DOCKER_IMAGE_BUILD_TAG" --format "{{ range .Manifest.Manifests }}{{ if ne (print .Platform) \"&{unknown unknown [] }\" }}$DOCKER_IMAGE_BUILD_REGISTRY/$DOCKER_IMAGE_BUILD_NAME:$DOCKER_IMAGE_BUILD_TAG@{{ println .Digest }}{{end}} {{end}}"); - - when: never +# docker buildx imagetools create $DOCKER_MULTI_ARCH_IMAGES --tag $DOCKER_IMAGE_BUILD_REGISTRY/$DOCKER_IMAGE_BUILD_NAME:$DOCKER_IMAGE_BUILD_TAG; + +# docker buildx imagetools inspect $DOCKER_IMAGE_BUILD_REGISTRY/$DOCKER_IMAGE_BUILD_NAME:$DOCKER_IMAGE_BUILD_TAG; + +# rules: # rules manually synced from docker/publish.gitlab-ci.yaml removing git tag + +# # - if: # condition_master_branch_push +# # $CI_COMMIT_BRANCH == "master" && +# # $CI_PIPELINE_SOURCE == "push" +# # exists: +# # - '{dockerfile,dockerfile.j2}' +# # when: always + +# - if: +# $CI_COMMIT_AUTHOR =='nfc_bot ' +# && +# $CI_COMMIT_BRANCH == "development" +# when: never + +# - if: # condition_not_master_or_dev_push +# $CI_COMMIT_BRANCH != "master" && +# $CI_COMMIT_BRANCH != "development" && +# $CI_PIPELINE_SOURCE == "push" +# exists: +# - '{dockerfile,dockerfile.j2}' +# changes: +# paths: +# - '{dockerfile,dockerfile.j2,includes/**/*}' +# compare_to: 'development' +# when: always + +# - if: $CI_COMMIT_TAG +# exists: +# - '{dockerfile,dockerfile.j2}' +# when: always + +# - if: # condition_dev_branch_push +# ( +# $CI_COMMIT_BRANCH == "development" +# || +# $CI_COMMIT_BRANCH == "master" +# ) +# && +# $CI_PIPELINE_SOURCE == "push" +# exists: +# - '{dockerfile,dockerfile.j2}' +# allow_failure: true +# when: on_success + +# - when: never @@ -125,107 +127,107 @@ Docker Container: -.gitlab_release: - stage: release - image: registry.gitlab.com/gitlab-org/release-cli:latest - before_script: - - if [ "0$JOB_ROOT_DIR" == "0" ]; then ROOT_DIR=gitlab-ci; else ROOT_DIR=$JOB_ROOT_DIR ; fi - - echo "[DEBUG] ROOT_DIR[$ROOT_DIR]" - - mkdir -p "$CI_PROJECT_DIR/artifacts/$CI_JOB_STAGE/$CI_JOB_NAME" - - mkdir -p "$CI_PROJECT_DIR/artifacts/$CI_JOB_STAGE/tests" - - apk update - - apk add git curl - - apk add --update --no-cache python3 && ln -sf python3 /usr/bin/python - - python -m ensurepip && ln -sf pip3 /usr/bin/pip - - pip install --upgrade pip - - pip install -r $ROOT_DIR/gitlab_release/requirements.txt - # - pip install $ROOT_DIR/gitlab_release/python-module/cz_nfc/. - - pip install commitizen --force - - 'CLONE_URL="https://gitlab-ci-token:$GIT_COMMIT_TOKEN@gitlab.com/$CI_PROJECT_PATH.git"' - - echo "[DEBUG] CLONE_URL[$CLONE_URL]" - - git clone -b development $CLONE_URL repo - - cd repo - - git branch - - git config --global user.email "helpdesk@nofusscomputing.com" - - git config --global user.name "nfc_bot" - - git push --set-upstream origin development - - RELEASE_VERSION_CURRENT=$(cz version --project) - script: - - if [ "$CI_COMMIT_BRANCH" == "development" ] ; then RELEASE_CHANGELOG=$(cz bump --changelog --changelog-to-stdout --prerelease beta); else RELEASE_CHANGELOG=$(cz bump --changelog --changelog-to-stdout); fi - - RELEASE_VERSION_NEW=$(cz version --project) - - export RELEASE_TAG=$RELEASE_VERSION_NEW - - echo "[DEBUG] RELEASE_VERSION_CURRENT[$RELEASE_VERSION_CURRENT]" - - echo "[DEBUG] RELEASE_CHANGELOG[$RELEASE_CHANGELOG]" - - echo "[DEBUG] RELEASE_VERSION_NEW[$RELEASE_VERSION_NEW]" - - echo "[DEBUG] RELEASE_TAG[$RELEASE_TAG]" - - RELEASE_TAG_SHA1=$(git log -n1 --format=format:"%H") - - echo "[DEBUG] RELEASE_TAG_SHA1[$RELEASE_TAG_SHA1]" +# .gitlab_release: +# stage: release +# image: registry.gitlab.com/gitlab-org/release-cli:latest +# before_script: +# - if [ "0$JOB_ROOT_DIR" == "0" ]; then ROOT_DIR=gitlab-ci; else ROOT_DIR=$JOB_ROOT_DIR ; fi +# - echo "[DEBUG] ROOT_DIR[$ROOT_DIR]" +# - mkdir -p "$CI_PROJECT_DIR/artifacts/$CI_JOB_STAGE/$CI_JOB_NAME" +# - mkdir -p "$CI_PROJECT_DIR/artifacts/$CI_JOB_STAGE/tests" +# - apk update +# - apk add git curl +# - apk add --update --no-cache python3 && ln -sf python3 /usr/bin/python +# - python -m ensurepip && ln -sf pip3 /usr/bin/pip +# - pip install --upgrade pip +# - pip install -r $ROOT_DIR/gitlab_release/requirements.txt +# # - pip install $ROOT_DIR/gitlab_release/python-module/cz_nfc/. +# - pip install commitizen --force +# - 'CLONE_URL="https://gitlab-ci-token:$GIT_COMMIT_TOKEN@gitlab.com/$CI_PROJECT_PATH.git"' +# - echo "[DEBUG] CLONE_URL[$CLONE_URL]" +# - git clone -b development $CLONE_URL repo +# - cd repo +# - git branch +# - git config --global user.email "helpdesk@nofusscomputing.com" +# - git config --global user.name "nfc_bot" +# - git push --set-upstream origin development +# - RELEASE_VERSION_CURRENT=$(cz version --project) +# script: +# - if [ "$CI_COMMIT_BRANCH" == "development" ] ; then RELEASE_CHANGELOG=$(cz bump --changelog --changelog-to-stdout --prerelease beta); else RELEASE_CHANGELOG=$(cz bump --changelog --changelog-to-stdout); fi +# - RELEASE_VERSION_NEW=$(cz version --project) +# - RELEASE_TAG=$RELEASE_VERSION_NEW +# - echo "[DEBUG] RELEASE_VERSION_CURRENT[$RELEASE_VERSION_CURRENT]" +# - echo "[DEBUG] RELEASE_CHANGELOG[$RELEASE_CHANGELOG]" +# - echo "[DEBUG] RELEASE_VERSION_NEW[$RELEASE_VERSION_NEW]" +# - echo "[DEBUG] RELEASE_TAG[$RELEASE_TAG]" +# - RELEASE_TAG_SHA1=$(git log -n1 --format=format:"%H") +# - echo "[DEBUG] RELEASE_TAG_SHA1[$RELEASE_TAG_SHA1]" - - | - if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then +# - | +# if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then - echo "[DEBUG] not running extra actions, no new version"; +# echo "[DEBUG] not running extra actions, no new version"; - else +# else - echo "[DEBUG] Creating new Version Label"; +# echo "[DEBUG] Creating new Version Label"; - echo "----------------------------"; +# echo "----------------------------"; - echo ${MY_COMMAND}; +# echo ${MY_COMMAND}; - echo "----------------------------"; +# echo "----------------------------"; - cat ${MY_COMMAND}; +# cat ${MY_COMMAND}; - echo "----------------------------"; +# echo "----------------------------"; - ${MY_COMMAND}; +# ${MY_COMMAND}; - echo "----------------------------"; - fi +# echo "----------------------------"; +# fi - - if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then echo "[DEBUG] No tag to delete, version was not bumped"; else git tag -d $RELEASE_TAG; fi +# - if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then echo "[DEBUG] No tag to delete, version was not bumped"; else git tag -d $RELEASE_TAG; fi - - if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then echo "[DEBUG] No push will be conducted, version was not bumped"; else git push; fi - - if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then echo "[DEBUG] No release will be created, version was not bumped"; else release-cli create --name "Release $RELEASE_TAG" --tag-name "$RELEASE_TAG" --tag-message "$RELEASE_CHANGELOG" --ref "$RELEASE_TAG_SHA1" --description "$RELEASE_CHANGELOG"; fi - - if [ "$CI_COMMIT_BRANCH" == "master" ] ; then git checkout master; fi - - if [ "$CI_COMMIT_BRANCH" == "master" ] ; then git push --set-upstream origin master; fi - - if [ "$CI_COMMIT_BRANCH" == "master" ] ; then git merge --no-ff development; fi - - if [ "$CI_COMMIT_BRANCH" == "master" ] ; then git push origin master; fi - after_script: - - rm -Rf repo - rules: - - if: '$JOB_STOP_GITLAB_RELEASE' - when: never +# - if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then echo "[DEBUG] No push will be conducted, version was not bumped"; else git push; fi +# - if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then echo "[DEBUG] No release will be created, version was not bumped"; else release-cli create --name "Release $RELEASE_TAG" --tag-name "$RELEASE_TAG" --tag-message "$RELEASE_CHANGELOG" --ref "$RELEASE_TAG_SHA1" --description "$RELEASE_CHANGELOG"; fi +# - if [ "$CI_COMMIT_BRANCH" == "master" ] ; then git checkout master; fi +# - if [ "$CI_COMMIT_BRANCH" == "master" ] ; then git push --set-upstream origin master; fi +# - if [ "$CI_COMMIT_BRANCH" == "master" ] ; then git merge --no-ff development; fi +# - if [ "$CI_COMMIT_BRANCH" == "master" ] ; then git push origin master; fi +# after_script: +# - rm -Rf repo +# rules: +# - if: '$JOB_STOP_GITLAB_RELEASE' +# when: never - - if: "$CI_COMMIT_AUTHOR =='nfc_bot '" - when: never +# - if: "$CI_COMMIT_AUTHOR =='nfc_bot '" +# when: never - - if: # condition_master_branch_push - $CI_COMMIT_BRANCH == "master" && - $CI_PIPELINE_SOURCE == "push" - allow_failure: false - when: on_success +# - if: # condition_master_branch_push +# $CI_COMMIT_BRANCH == "master" && +# $CI_PIPELINE_SOURCE == "push" +# allow_failure: false +# when: on_success - - if: # condition_dev_branch_push - $CI_COMMIT_BRANCH == "development" && - $CI_PIPELINE_SOURCE == "push" - when: manual - allow_failure: true +# - if: # condition_dev_branch_push +# $CI_COMMIT_BRANCH == "development" && +# $CI_PIPELINE_SOURCE == "push" +# when: manual +# allow_failure: true - # for testing - # - if: '$CI_COMMIT_BRANCH != "master"' - # when: always - # allow_failure: true - - when: never +# # for testing +# # - if: '$CI_COMMIT_BRANCH != "master"' +# # when: always +# # allow_failure: true +# - when: never -# -# Release -# -Gitlab Release: - extends: - - .gitlab_release +# # +# # Release +# # +# Gitlab Release: +# extends: +# - .gitlab_release @@ -234,59 +236,59 @@ Gitlab Release: -Docker.Hub.Branch.Publish: - extends: .publish-docker-hub - needs: [ "Docker Container" ] - resource_group: build - rules: # rules manually synced from docker/publish.gitlab-ci.yaml removing git tag +# Docker.Hub.Branch.Publish: +# extends: .publish-docker-hub +# needs: [ "Docker Container" ] +# resource_group: build +# rules: # rules manually synced from docker/publish.gitlab-ci.yaml removing git tag - # - if: # condition_master_branch_push - # $CI_COMMIT_BRANCH == "master" && - # $CI_PIPELINE_SOURCE == "push" - # exists: - # - '{dockerfile,dockerfile.j2}' - # when: always +# # - if: # condition_master_branch_push +# # $CI_COMMIT_BRANCH == "master" && +# # $CI_PIPELINE_SOURCE == "push" +# # exists: +# # - '{dockerfile,dockerfile.j2}' +# # when: always - - if: - $CI_COMMIT_AUTHOR =='nfc_bot ' - && - $CI_COMMIT_BRANCH == "development" - when: never +# - if: +# $CI_COMMIT_AUTHOR =='nfc_bot ' +# && +# $CI_COMMIT_BRANCH == "development" +# when: never - - if: $CI_COMMIT_TAG - exists: - - '{dockerfile,dockerfile.j2}' - when: always +# - if: $CI_COMMIT_TAG +# exists: +# - '{dockerfile,dockerfile.j2}' +# when: always - - if: # condition_dev_branch_push - $CI_COMMIT_BRANCH == "development" && - $CI_PIPELINE_SOURCE == "push" - exists: - - '{dockerfile,dockerfile.j2}' - allow_failure: true - when: on_success +# - if: # condition_dev_branch_push +# $CI_COMMIT_BRANCH == "development" && +# $CI_PIPELINE_SOURCE == "push" +# exists: +# - '{dockerfile,dockerfile.j2}' +# allow_failure: true +# when: on_success - - when: never +# - when: never -Github (Push --mirror): - extends: - - .git_push_mirror - needs: [] - rules: - - if: '$JOB_STOP_GIT_PUSH_MIRROR' - when: never +# Github (Push --mirror): +# extends: +# - .git_push_mirror +# needs: [] +# rules: +# - if: '$JOB_STOP_GIT_PUSH_MIRROR' +# when: never - - if: $GIT_SYNC_URL == null - when: never +# - if: $GIT_SYNC_URL == null +# when: never - - if: # condition_master_or_dev_push - $CI_COMMIT_BRANCH - && - $CI_PIPELINE_SOURCE == "push" - when: always +# - if: # condition_master_or_dev_push +# $CI_COMMIT_BRANCH +# && +# $CI_PIPELINE_SOURCE == "push" +# when: always - - when: never +# - when: never Website.Submodule.Deploy: From d99f2d3c6fb5924852eecc06ce5a78607fc7de74 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 9 Aug 2024 15:16:21 +0930 Subject: [PATCH 076/123] docs: update readme to reflect Github as project home . #216 --- README.md | 30 +++++++++++++++++++----------- artifacthub-repo.yml | 2 -- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 4ee1cf33..6c9cd4c1 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ![Project Status - Active](https://img.shields.io/badge/Project%20Status-Active-green?logo=gitlab&style=plastic) -[![Docker Pulls](https://img.shields.io/docker/pulls/nofusscomputing/centurion-erp?style=plastic&logo=docker&color=0db7ed)](https://hub.docker.com/r/nofusscomputing/centurion-erp) [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/centurion-erp)](https://artifacthub.io/packages/search?repo=centurion-erp) +[![Docker Pulls](https://img.shields.io/docker/pulls/nofusscomputing/centurion-erp?style=plastic&logo=docker&color=0db7ed)](https://hub.docker.com/r/nofusscomputing/centurion-erp) [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/centurion-erp)](https://artifacthub.io/packages/container/centurion-erp/centurion-erp) @@ -15,27 +15,35 @@
-![Gitlab forks count](https://img.shields.io/badge/dynamic/json?label=Forks&query=%24.forks_count&url=https%3A%2F%2Fgitlab.com%2Fapi%2Fv4%2Fprojects%2F57560288%2F&color=ff782e&logo=gitlab&style=plastic) ![Gitlab stars](https://img.shields.io/badge/dynamic/json?label=Stars&query=%24.star_count&url=https%3A%2F%2Fgitlab.com%2Fapi%2Fv4%2Fprojects%2F57560288%2F&color=ff782e&logo=gitlab&style=plastic) [![Open Issues](https://img.shields.io/badge/dynamic/json?color=ff782e&logo=gitlab&style=plastic&label=Open%20Issues&query=%24.statistics.counts.opened&url=https%3A%2F%2Fgitlab.com%2Fapi%2Fv4%2Fprojects%2F57560288%2Fissues_statistics)](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues) [![GitLab Bugs](https://img.shields.io/gitlab/issues/open/nofusscomputing%2Fprojects%2Fcenturion_erp?labels=type%3A%3Abug&style=plastic&logo=gitlab&label=Bug%20Fixes%20Required&color=fc6d26)](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/?sort=created_date&state=opened&label_name%5B%5D=type%3A%3Abug) - - - ![GitHub forks](https://img.shields.io/github/forks/NofussComputing/centurion_erp?logo=github&style=plastic&color=000000&labell=Forks) ![GitHub stars](https://img.shields.io/github/stars/NofussComputing/centurion_erp?color=000000&logo=github&style=plastic) ![Github Watchers](https://img.shields.io/github/watchers/NofussComputing/centurion_erp?color=000000&label=Watchers&logo=github&style=plastic) + + + +![Gitlab forks count](https://img.shields.io/badge/dynamic/json?label=Forks&query=%24.forks_count&url=https%3A%2F%2Fgitlab.com%2Fapi%2Fv4%2Fprojects%2F57560288%2F&color=ff782e&logo=gitlab&style=plastic) ![Gitlab stars](https://img.shields.io/badge/dynamic/json?label=Stars&query=%24.star_count&url=https%3A%2F%2Fgitlab.com%2Fapi%2Fv4%2Fprojects%2F57560288%2F&color=ff782e&logo=gitlab&style=plastic) +
-This project is hosted on [gitlab](https://gitlab.com/nofusscomputing/projects/centurion_erp) and has a read-only copy hosted on [Github](https://github.com/NofussComputing/centurion_erp). + ![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/nofusscomputing/centurion_erp?style=plastic&logo=github&label=Open%20Issues&color=000) ![GitHub Issues or Pull Requests by label](https://img.shields.io/github/issues/nofusscomputing/centurion_erp/type%3A%3Abug?style=plastic&logo=github&label=Bug%20Fixes%20Required&color=000) + + +This project is hosted on [Github](https://github.com/NofussComputing/centurion_erp) and has a read-only copy hosted on [gitlab](https://gitlab.com/nofusscomputing/projects/centurion_erp). ---- **Stable Branch** -![Gitlab build status - stable](https://img.shields.io/badge/dynamic/json?color=ff782e&label=Build&query=0.status&url=https%3A%2F%2Fgitlab.com%2Fapi%2Fv4%2Fprojects%2F57560288%2Fpipelines%3Fref%3Dmaster&logo=gitlab&style=plastic) ![branch release version](https://img.shields.io/badge/dynamic/yaml?color=ff782e&logo=gitlab&style=plastic&label=Release&query=%24.commitizen.version&url=https%3A//gitlab.com/nofusscomputing/projects/centurion_erp%2F-%2Fraw%2Fmaster%2F.cz.yaml) [![Gitlab Code Coverage](https://img.shields.io/gitlab/pipeline-coverage/nofusscomputing%2Fprojects%2Fcenturion_erp?branch=master&style=plastic&logo=gitlab&label=Test%20Coverage)](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/jobs/artifacts/master/browse/artifacts/coverage/?job=Unit) +![GitHub branch status](https://img.shields.io/github/check-runs/nofusscomputing/centurion_erp/master?style=plastic&logo=github&label=Build&color=000) ![GitHub Release](https://img.shields.io/github/v/release/nofusscomputing/centurion_erp?sort=semver&display_name=release&style=plastic&logo=github&label=Build&color=000) ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fmaster%2Fbadge_endpoint_coverage.json&style=plastic) + ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fmaster%2Fbadge_endpoint_unit_test.json) ---- **Development Branch** -![Gitlab build status - development](https://img.shields.io/badge/dynamic/json?color=ff782e&label=Build&query=0.status&url=https%3A%2F%2Fgitlab.com%2Fapi%2Fv4%2Fprojects%2F57560288%2Fpipelines%3Fref%3Ddevelopment&logo=gitlab&style=plastic) ![branch release version](https://img.shields.io/badge/dynamic/yaml?color=ff782e&logo=gitlab&style=plastic&label=Release&query=%24.commitizen.version&url=https%3A//gitlab.com/nofusscomputing/projects/centurion_erp%2F-%2Fraw%2Fdevelopment%2F.cz.yaml) [![Gitlab Code Coverage](https://img.shields.io/gitlab/pipeline-coverage/nofusscomputing%2Fprojects%2Fcenturion_erp?branch=development&style=plastic&logo=gitlab&label=Test%20Coverage)](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/jobs/artifacts/development/browse/artifacts/coverage/?job=Unit) + + +![GitHub branch status](https://img.shields.io/github/check-runs/nofusscomputing/centurion_erp/development?style=plastic&logo=github&label=Build&color=000) ![GitHub Release](https://img.shields.io/github/v/release/nofusscomputing/centurion_erp?include_prereleases&sort=semver&display_name=release&style=plastic&logo=github&label=Build&color=000) ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fdevelopment%2Fbadge_endpoint_coverage.json&style=plastic) + ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fdevelopment%2Fbadge_endpoint_unit_test.json) ---- @@ -45,9 +53,9 @@ This project is hosted on [gitlab](https://gitlab.com/nofusscomputing/projects/c links: -- [Issues](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues) +- [Issues](https://github.com/nofusscomputing/centurion_erp/issues) -- [Merge Requests (Pull Requests)](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests) +- [Merge Requests (Pull Requests)](https://github.com/nofusscomputing/centurion_erp/pulls) An ERP with a large emphasis on the IT Service Management (ITSM) and Automation. @@ -55,7 +63,7 @@ An ERP with a large emphasis on the IT Service Management (ITSM) and Automation. ## Contributing -All contributions for this project must conducted from [Gitlab](https://gitlab.com/nofusscomputing/projects/centurion_erp). +All contributions for this project must conducted from [GitHub](https://github.com/nofusscomputing/centurion_erp). For further details on contributing please refer to the [contribution guide](CONTRIBUTING.md). diff --git a/artifacthub-repo.yml b/artifacthub-repo.yml index 7676ed31..0608736a 100644 --- a/artifacthub-repo.yml +++ b/artifacthub-repo.yml @@ -2,5 +2,3 @@ repositoryID: 17eaf871-a980-41ba-b841-2a78734535ca owners: - name: no-fuss-computing email: helpdesk@nofusscomputing.com - - name: jon_nfc - email: jonathon.lockwood@networkedweb.com From bb388a196940d0a5182f7eab4f80a05afc1b5867 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 9 Aug 2024 21:03:58 +0930 Subject: [PATCH 077/123] ci: remove temp workflows #216 #214 --- .github/workflows/code-coverage-report.yaml | 93 -------------- .github/workflows/unit-test-report.yaml | 35 ------ .github/workflows/unit-test.yaml | 128 -------------------- 3 files changed, 256 deletions(-) delete mode 100644 .github/workflows/code-coverage-report.yaml delete mode 100644 .github/workflows/unit-test-report.yaml delete mode 100644 .github/workflows/unit-test.yaml diff --git a/.github/workflows/code-coverage-report.yaml b/.github/workflows/code-coverage-report.yaml deleted file mode 100644 index 8fc875ce..00000000 --- a/.github/workflows/code-coverage-report.yaml +++ /dev/null @@ -1,93 +0,0 @@ ---- - -name: 'Process Coverage Artifact' - -on: - workflow_run: - workflows: - - 'Unit Test' - types: - - completed - - -permissions: - contents: read - actions: read - checks: write - - -jobs: - report: - runs-on: ubuntu-latest - # strategy: - # max-parallel: 4 - # matrix: - # python-version: ['3.12'] - name: Coverage - steps: - - - name: Run Tests - run: | - ls -l; - - - name: Download Coverage Artifact - uses: actions/download-artifact@v4 - with: - name: coverage-report-3.12 - # path: coverage.xml - run-id: ${{ github.event.workflow_run.id }} - github-token: ${{ github.token }} - - - name: ls - if: success() || failure() - run: | - ls -l; - - - name: Code Coverage Report - uses: irongut/CodeCoverageSummary@v1.3.0 - with: - filename: coverage.xml - badge: true - fail_below_min: true - format: markdown - hide_branch_rate: false - hide_complexity: false - indicators: true - output: both - thresholds: '60 85' - - - # - name: Add Coverage PR Comment - # uses: marocchino/sticky-pull-request-comment@v2 - # if: github.event_name == 'pull_request' - # with: - # recreate: true - # path: code-coverage-results.md - - - - name: ls - if: success() || failure() - run: | - ls -l; - - # - name: Adding markdown - # run: | - # cat $(ls *.md | tail -1) >> $GITHUB_STEP_SUMMARY - - - name: create status check/comment for code coverage results - id: jest_coverage_check - uses: im-open/process-code-coverage-summary@v2.3.0 - with: - # github-token: ${{ secrets.GITHUB_TOKEN }} - github-token: ${{ github.token }} - summary-file: code-coverage-results.md - create-pr-comment: true - update-comment-if-one-exists: true - update-comment-key: "${{ env.GITHUB-JOB }}_${{ env.GITHUB-ACTION }}" - - - name: Upload Coverage Summary - uses: actions/upload-artifact@v4 - if: success() || failure() - with: - name: code-coverage-results-3.12 - path: code-coverage-results.md diff --git a/.github/workflows/unit-test-report.yaml b/.github/workflows/unit-test-report.yaml deleted file mode 100644 index 61662729..00000000 --- a/.github/workflows/unit-test-report.yaml +++ /dev/null @@ -1,35 +0,0 @@ ---- - -name: 'Process Unit Test Artifact' - -on: - workflow_run: - workflows: - - 'Unit Test' - types: - - completed - - -permissions: - contents: read - actions: read - checks: write - - -jobs: - report: - runs-on: ubuntu-latest - strategy: - max-parallel: 4 - matrix: - python-version: ['3.10', '3.11', '3.12'] - steps: - - - name: Test Report - uses: dorny/test-reporter@v1 - with: - artifact: unit-test-results-${{ matrix.python-version }} - badge-title: 'Unit Tests [Python ${{ matrix.python-version }}]' - name: Unit Test Report [Python ${{ matrix.python-version }}] - path: '*.xml' - reporter: java-junit diff --git a/.github/workflows/unit-test.yaml b/.github/workflows/unit-test.yaml deleted file mode 100644 index 40cfd331..00000000 --- a/.github/workflows/unit-test.yaml +++ /dev/null @@ -1,128 +0,0 @@ -name: 'Unit Test' - -on: - push: - branches: - - "development" - tags: - - '*' - pull_request: - branches: - - "development" - - -jobs: - test: - - runs-on: ubuntu-latest - strategy: - max-parallel: 4 - matrix: - python-version: ['3.10', '3.11', '3.12'] - - steps: - - - - uses: actions/checkout@v4 - - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - - name: Install Dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - pip install -r requirements_test.txt - - - - name: Run Tests - run: | - cd app; - pytest --cov --cov-report term --cov-report xml:../coverage.xml --cov-report html:../coverage/ --junit-xml=../unit.JUnit.xml **/tests/unit; - - - - name: Upload Test Report - uses: actions/upload-artifact@v4 - if: success() || failure() - with: - name: unit-test-results-${{ matrix.python-version }} - path: unit.JUnit.xml - - - - name: Upload Coverage Report - uses: actions/upload-artifact@v4 - if: success() || failure() - with: - name: coverage-report-${{ matrix.python-version }} - path: coverage.xml - - - - name: Upload Coverage - uses: actions/upload-artifact@v4 - if: success() || failure() - with: - name: coverage-${{ matrix.python-version }} - path: coverage/* - - # coverage: - # needs: - # - test - # runs-on: ubuntu-latest - # # strategy: - # # max-parallel: 4 - # # matrix: - # # python-version: ['3.12'] - # name: Coverage - # steps: - - # # - name: Run Tests - # # run: | - # # ls -l; - - # - name: Download Coverage Artifact - # uses: actions/download-artifact@v4 - # with: - # name: coverage-report-3.12 - # # path: coverage.xml - # # run-id: ${{ github.event.workflow_run.id }} - # # github-token: ${{ github.token }} - - - # - name: Add Coverage PR Comment - # uses: marocchino/sticky-pull-request-comment@v2 - # if: github.event_name == 'pull_request' - # with: - # recreate: true - # path: code-coverage-results.md - - - # - name: ls - # if: success() || failure() - # run: | - # ls -l; - - # - name: Code Coverage Report - # uses: irongut/CodeCoverageSummary@v1.3.0 - # with: - # filename: coverage.xml - # badge: true - # fail_below_min: true - # format: markdown - # hide_branch_rate: false - # hide_complexity: false - # indicators: true - # output: both - # thresholds: '60 85' - - # - name: ls - # if: success() || failure() - # run: | - # ls -l; - - # - name: Summary - # run: | - # cat $(ls *.md | tail -1) >> $GITHUB_STEP_SUMMARY From e8684c5206979182a5aac2d86052d17d88715e61 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 9 Aug 2024 21:04:25 +0930 Subject: [PATCH 078/123] ci: Add PR checks workflow #216 #214 --- .github/workflows/pull-requests.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/workflows/pull-requests.yaml diff --git a/.github/workflows/pull-requests.yaml b/.github/workflows/pull-requests.yaml new file mode 100644 index 00000000..c7d09222 --- /dev/null +++ b/.github/workflows/pull-requests.yaml @@ -0,0 +1,14 @@ +--- + +name: Pull Requests + + +on: + pull_request: {} + + +jobs: + + pull-request: + name: pull-request + uses: nofusscomputing/action_pull_requests/.github/workflows/pull-requests.yaml@development From cf00ab62341ccaabc5909602c03f2a6bc385a7ac Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 9 Aug 2024 21:05:05 +0930 Subject: [PATCH 079/123] ci: Add Docker workflow #216 #214 --- .github/workflows/ci.yaml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/ci.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 00000000..85c5f0b0 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,26 @@ +--- + +name: 'CI' + + +on: + push: + branches: + - '**' + tags: + - '*' + + +jobs: + + + docker: + name: 'Docker' + uses: nofusscomputing/action_docker/.github/workflows/docker.yaml@development + with: + DOCKER_BUILD_IMAGE_NAME: "nofusscomputing/centurion-erp" + DOCKER_PUBLISH_REGISTRY: "ghcr.io" + DOCKER_PUBLISH_IMAGE_NAME: "nofusscomputing/centurion-erp" + secrets: + DOCKER_PUBLISH_USERNAME: ${{ secrets.NFC_DOCKERHUB_TOKEN }} + DOCKER_PUBLISH_PASSWORD: ${{ secrets.NFC_DOCKERHUB_USERNAME }} From c624a3617cae4a457b6a1985894f1e073785e060 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 9 Aug 2024 21:05:16 +0930 Subject: [PATCH 080/123] ci: Add Python workflow #216 #214 --- .github/workflows/ci.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 85c5f0b0..dc4f52d7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -24,3 +24,10 @@ jobs: secrets: DOCKER_PUBLISH_USERNAME: ${{ secrets.NFC_DOCKERHUB_TOKEN }} DOCKER_PUBLISH_PASSWORD: ${{ secrets.NFC_DOCKERHUB_USERNAME }} + + + python: + name: 'Python' + uses: nofusscomputing/action_python/.github/workflows/python.yaml@development + secrets: + WORKFLOW_TOKEN: ${{ secrets.WORKFLOW_TOKEN }} From 71726035dc66b8f37e536830cb3d266c05a1fe90 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 9 Aug 2024 21:05:38 +0930 Subject: [PATCH 081/123] ci: Add Bump workflow #216 #214 --- .github/workflows/bump.yaml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/bump.yaml diff --git a/.github/workflows/bump.yaml b/.github/workflows/bump.yaml new file mode 100644 index 00000000..266fa04d --- /dev/null +++ b/.github/workflows/bump.yaml @@ -0,0 +1,31 @@ +--- + +name: 'Bump' + + +on: + workflow_dispatch: + inputs: + CZ_PRE_RELEASE: + default: none + required: false + description: Create Pre-Release {alpha,beta,rc,none} + CZ_INCREMENT: + default: none + required: false + description: Type of bump to conduct {MAJOR,MINOR,PATCH,none} + push: + branches: + - 'master' + + +jobs: + + bump: + name: 'Bump' + uses: nofusscomputing/action_bump/.github/workflows/bump.yaml@development + with: + CZ_PRE_RELEASE: ${{ inputs.CZ_PRE_RELEASE }} + CZ_INCREMENT: ${{ inputs.CZ_INCREMENT }} + secrets: + WORKFLOW_TOKEN: ${{ secrets.WORKFLOW_TOKEN }} From c6ed5c8279f62df72457170fe3eaa2b357379682 Mon Sep 17 00:00:00 2001 From: nfc-bot Date: Fri, 9 Aug 2024 11:58:50 +0000 Subject: [PATCH 082/123] build: bump version 1.0.0-b5 -> 1.0.0-b6 --- .cz.yaml | 2 +- CHANGELOG.md | 798 ++++++++++++++++++++++----------------------------- 2 files changed, 352 insertions(+), 448 deletions(-) diff --git a/.cz.yaml b/.cz.yaml index d84f9ba8..64868c6c 100644 --- a/.cz.yaml +++ b/.cz.yaml @@ -4,5 +4,5 @@ commitizen: prerelease_offset: 1 tag_format: $version update_changelog_on_bump: false - version: 1.0.0-b5 + version: 1.0.0-b6 version_scheme: semver diff --git a/CHANGELOG.md b/CHANGELOG.md index a855ce88..272dc4bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.0.0-b6 (2024-08-09) + ## 1.0.0-b5 (2024-07-31) ### Feat @@ -103,503 +105,405 @@ ## 0.7.0 (2024-07-14) -### Bug Fixes +### Feat -- **config_management**: [5ae487cd](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/5ae487cd3eac2f5273d3b2a9e7642e714bdbde68) - Don't allow a config group to assign itself as its parent [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#122](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/122) ] -- **config_management**: [3aab7b57](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/3aab7b57e80b48e1f4671413034c1c71dfad4c66) - correct permission for deleting a host from config group [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- **config_management**: [931c9864](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/931c9864db8e14072a0a7e331d525aeedd19eb2a) - use parent group details to work out permissions when adding a host [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#120](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/120) ] -- **config_management**: [65bf9946](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/65bf994619949c4d42bfa92e06e8c63f67acabca) - use parent group details to work out permissions [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#121](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/121) ] -- **itam**: [77ff580f](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/77ff580f19e41a430cfa7d3bf1ba3870d7993cf9) - Add missing permissions to software categories index view [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#74](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/74) ] -- **itam**: [423ff11d](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/423ff11d4c30670cb8c7832b452af78cf54a5fd3) - Add missing permissions to device types index view [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#74](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/74) ] -- **itam**: [9e4b5185](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/9e4b5185b144ade67b98aeb6e909e1af39b62545) - Add missing permissions to device model index view [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#74](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/74) ] -- **settings**: [020441c4](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/020441c41aae4ccc3453becdf451918bc3a432f7) - Add missing permissions to app settings view [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#74](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/74) ] -- **itam**: [d0a3b7b4](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/d0a3b7b49dfa8c0468106652af723b824c7ecf89) - Add missing permissions to software index view [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#74](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/74) ] -- **itam**: [960fa548](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/960fa5485d3198c1e4868957e6d57f3c8bd65cf8) - Add missing permissions to operating system index view [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#74](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/74) ] -- **itam**: [26db4630](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/26db4630445ceff412a6d12f97f661e95c165a3b) - Add missing permissions to device index view [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#74](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/74) ] -- **config_management**: [1193f1d8](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/1193f1d86d247e5df77dc44bae36a5534c0f0c88) - Add missing permissions to group views [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#74](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/74) ] -- **navigation**: [ee8920a4](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/ee8920a464017e2ec7ef714530a105197eaae75b) - always show settings menu entry [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- **itam**: [a62a36ba](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/a62a36ba82252257646325679180c68632971c52) - cater for fields that are prefixed [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#112](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/112) ] -- **itam**: [c00cf16b](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/c00cf16bc8ac85f5c5bf19d30cf78ae9a838d00f) - Ability to view software category [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- **itam**: [7784dfed](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/7784dfede98f94bd1a5e4df5155130e91876fe67) - correct view permission [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- **access**: [03d350e3](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/03d350e302c443ff53fc71e32bc32f489af1409a) - When adding a new team to org ensure parent model is fetched [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- **access**: [1d5c86f1](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/1d5c86f13b2df36e0562e1fe3b6aca7dfa04a7ec) - enable org manager to view orgs [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#105](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/105) ] -- **settings**: [9e336d36](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/9e336d368d51381701b358287853f9ceab61b49f) - restrict user visible organizations to ones they are part of [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#99](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/99) ] -- **access**: [937e9359](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/937e9359498da9388fd730c69e591679458c331e) - enable org manager to view orgs [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#105](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/105) ] -- **access**: [860eaa67](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/860eaa674937dc9ff1690615bafcddaab38d890d) - fetch object if method exists [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#105](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/105) ] -- **docs**: [aab94431](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/aab94431a9d3864ace91af70e29b5dc59b61fd6f) - update docs link to new path [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#103](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/103) ] -- **access**: [524a70ba](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/524a70ba184c0af1125636f5923492ba65765f1e) - correctly set team user parent model to team [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#109](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/109) ] -- **access**: [29c4b4a0](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/29c4b4a0caaa37866774235d56312f2b9ede8148) - fallback to django permissions if org permissions check is false [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#109](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/109) [#101](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/101) ] -- **access**: [f5ae01b0](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/f5ae01b08d0ce91e4af86a042effff79db489d6a) - Correct logic so that org managers can see orgs they manage [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#100](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/100) ] -- **base**: [ee3dd68c](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/ee3dd68cfe78c71a4431e88f6d1f6429dd2af8c0) - add missing content_title to context [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#74](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/74) ] -- **access**: [25efa314](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/25efa31493f71eca5d553d46136d3a261a9d3612) - Enable Organization Manager to view organisations they are assigned to [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#100](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/100) ] -- **api**: [4a6ce353](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/4a6ce353325148f66e3216e61feee7ad96b45cbc) - correct logic for adding inventory UUID and serial number to device [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- **ui**: [2d80f026](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/2d80f026341910eecd23560ef970323ac275b112) - navigation alignment and software icon [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- **ui**: [abe1ce69](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/abe1ce69480de5c831d5183845987bc9a3264fc3) - display organization manager name instead of ID [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- **access**: [86ed7318](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/86ed7318ecd43c15cfccdb344af4ea2ac05c8a87) - ensure name param exists before attempting to access [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- **itam**: [90a01911](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/90a01911dacd698d0b7832a05e24eec6fe8310eb) - dont show none/nil for device fields containing no value [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- **itam**: [de3ed3a8](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/de3ed3a881bd53e533b9b35f37ce17419c6d75f2) - show device model name instead of ID [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- **api**: [f64be2ea](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/f64be2ea33ebf0ae1ba735a7259caf880bcddad5) - Ensure if serial number from inventory is `null` that it's not used [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#78](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/78) ] -- **api**: [ef9c596e](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/ef9c596ec79decec268ffb700e62d5bc35e49019) - ensure checked uuid and serial number is used for updating [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- **itam**: [67f20ecb](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/67f20ecb661b039092ad34b491c3a8a7296534db) - only remove device software when not found during inventory upload [ [!38](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/38) [#75](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/75) ] -- **itam**: [3bceb666](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/3bceb66600404919ac32498ffa5cbb4f24fcced4) - only update software version if different [ [!38](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/38) [#75](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/75) ] -- **itam**: [241ba47c](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/241ba47c80805dbd648392ef0b6b26793d3f55ff) - correct device software pagination [ [!36](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/36) [#67](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/67) ] +- **core**: Filter every form field if associated with an organization to users organizations only +- **core**: add var `template_name` to common view template for all views that require it +- **core**: add Display view to common forms abstract class +- **navigation**: always show every menu for super admin +- **core**: only display navigation menu item if use can view model +- **django**: update 5.0.6 -> 5.0.7 +- **core**: add common forms abstract class +- **core**: add common views abstract class +- add postgreSQL database support +- **ui**: add config groups navigation icon +- **ui**: add some navigation icons +- **itam**: update inventory status icon +- **itam**: ensure device software pagination links keep interface on software tab +- **access**: enable non-organization django permission checks +- **settings**: Add celery task results index and view page +- **base**: Add background worker +- **itam**: Update Serial Number from inventory if present and Serial Number not set +- **itam**: Update UUID from inventory if present and UUID not set -### Code Refactor +### Fix -- [367c4beb](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/367c4bebb67c24ffcc2ade19abfeac6089a1e702) - adjust views missing add/change form to now use forms [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#15](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/15) [#46](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/46) [#74](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/74) [#120](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/120) [#121](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/121) [#118](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/118) ] -- [0276f945](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/0276f9454b3999ec147314327b25945a69213250) - add navigation menu expand arrows [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#21](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/21) ] -- [7d172fb4](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/7d172fb4afa284e67231d3d24c7f1bdc533f922a) - migrate views to use new abstract model view classes [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#111](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/111) ] -- [f848d01b](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/f848d01b347af5615613e8bfdb0d9d7324e1daec) - migrate forms to use new abstract model form class [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- **access**: [7cfede45](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/7cfede45b89dd5d5ce5b2d2fa8e4ef0c64d31ab3) - Rename Team Button "new user" -> "Assign User" [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#110](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/110) ] -- **access**: [65de9371](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/65de93715d505d5c71b150175e31471b5a07bb8f) - model pk and name not required context for adding a device [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- [fea7ea31](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/fea7ea31198190bf115dc89595b00d7e034aa991) - rename field "model notes" -> "Notes" [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#102](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/102) [#104](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/104) ] -- [f0bbd22c](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/f0bbd22cf441cb3c3f5b01c8108a7a0fd8357938) - remove settings model [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- **ui**: [fb907283](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/fb907283b036454aa2afd41bbc658c8feb1a44d8) - increase indentation to sub-menu items [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- **itam**: [c1a8ee65](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/c1a8ee65f2bc892c2ca8bfacc5f95094a0130b24) - rename old inventory status icon for use with security [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- **api**: [7aeba347](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/7aeba347875cbefb6d5eae112804ae6f0097d264) - migrate inventory processing to background worker [ [!39](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/39) [#76](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/76) ] -- **itam**: [f47b97e2](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/f47b97e2a084e1acbfba733c910f3b8f4f764a36) - only perform actions on device inventory if DB matches inventory item [ [!38](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/38) [#75](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/75) ] +- **config_management**: Don't allow a config group to assign itself as its parent +- **config_management**: correct permission for deleting a host from config group +- **config_management**: use parent group details to work out permissions when adding a host +- **config_management**: use parent group details to work out permissions +- **itam**: Add missing permissions to software categories index view +- **itam**: Add missing permissions to device types index view +- **itam**: Add missing permissions to device model index view +- **settings**: Add missing permissions to app settings view +- **itam**: Add missing permissions to software index view +- **itam**: Add missing permissions to operating system index view +- **itam**: Add missing permissions to device index view +- **config_management**: Add missing permissions to group views +- **navigation**: always show settings menu entry +- **itam**: cater for fields that are prefixed +- **itam**: Ability to view software category +- **itam**: correct view permission +- **access**: When adding a new team to org ensure parent model is fetched +- **access**: enable org manager to view orgs +- **settings**: restrict user visible organizations to ones they are part of +- **access**: enable org manager to view orgs +- **access**: fetch object if method exists +- **docs**: update docs link to new path +- **access**: correctly set team user parent model to team +- **access**: fallback to django permissions if org permissions check is false +- **access**: Correct logic so that org managers can see orgs they manage +- **base**: add missing content_title to context +- **access**: Enable Organization Manager to view organisations they are assigned to +- **api**: correct logic for adding inventory UUID and serial number to device +- **ui**: navigation alignment and software icon +- **ui**: display organization manager name instead of ID +- **access**: ensure name param exists before attempting to access +- **itam**: dont show none/nil for device fields containing no value +- **itam**: show device model name instead of ID +- **api**: Ensure if serial number from inventory is `null` that it's not used +- **api**: ensure checked uuid and serial number is used for updating +- **itam**: only remove device software when not found during inventory upload +- **itam**: only update software version if different +- **itam**: correct device software pagination -### Continious Integration +### Refactor -- [e25ec12c](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/e25ec12cb02f8f48853806d9cc97959d3e757115) - correct test report path [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- [a235aa7e](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/a235aa7ec37a6dda69bc96ab854df41eacba255b) - add submodule update job [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] - -### Documentaton / Guides - -- **development**: [935e10dc](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/935e10dc24d10c2d99b52a185730d676b4978908) - add initial forms [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- **development**: [d4aaea4d](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/d4aaea4dbb250f23850d02d8c5cb9ecbc147a9da) - update views, models and index [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- [329049e8](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/329049e81dd50ca01512f75323669c7953be2199) - roadmap update [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- [c41c7ed1](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/c41c7ed1f09a7a13ac93043bee4e3f3ce0613245) - update mkdocs [ [!41](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/41) ] -- [c9190e9a](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/c9190e9a7dd12725df323811816ea603315331e9) - Update index [ [!41](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/41) ] -- **centurion**: [0294f5ed](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/0294f5ed65868fadd9f27a8a9c22ca861418061b) - replace Django ITSM -> Centurion ERP [ [!41](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/41) ] -- [7329a65a](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/7329a65ae7f30b6e89d0c284609aac2063c76ea6) - update roadmap [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- [9a529a64](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/9a529a64e2e5deaeebec872e3cbe91a6eccebcbe) - add bug count badge [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- [9b79c9d7](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/9b79c9d7ffaacc6ecd9a8d379bc26a303d2951c4) - update readme [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- [9dd2f6a3](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/9dd2f6a341c4e4c78de4e80769552914a4b23bc9) - fix mkdocs navigation [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- [23c640a4](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/23c640a4602948d1b6ec0d3a88473aa26d359997) - add roadmap [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- **api**: [27eb54cc](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/27eb54cc3729bcf3ac4b74c84b2def8086685b34) - update swagger docs with inventory changes [ [!39](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/39) [#76](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/76) ] -- **administration**: [a8e2c687](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/a8e2c687b11186f922abd57bb46e98de9f9bf985) - notate rabbitMQ setup [ [!39](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/39) [#76](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/76) ] - -### Features - -- **core**: [4c42f776](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/4c42f776924dc2f8c52c237b9facd35f10e85e28) - Filter every form field if associated with an organization to users organizations only [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#119](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/119) ] -- **core**: [1cf15f73](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/1cf15f7339a50d9aa1a7940feb84a3128a573ea6) - add var `template_name` to common view template for all views that require it [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- **core**: [c057ffdc](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/c057ffdc9c57b399206579f13c51bf4d28120e88) - add Display view to common forms abstract class [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- **navigation**: [6837c383](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/6837c383034b962973248cbdd6ae6a5a8a758a41) - always show every menu for super admin [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- **core**: [45cc3428](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/45cc34284a7b89aa3d61e61cfa3a12c73165127b) - only display navigation menu item if use can view model [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#114](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/114) ] -- **django**: [f2640df0](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/f2640df0d3737a5fc7a416cd692c37690786e7d1) - update 5.0.6 -> 5.0.7 [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- **core**: [44f20b28](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/44f20b28be8c54978b53f00fac9664a5c403ed50) - add common forms abstract class [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- **core**: [2e22a484](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/2e22a484a0f4416f41dffb49c68db703c177fe0d) - add common views abstract class [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- [332810ff](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/332810ffd6cf6024a0a917024eafed21ec8d2139) - add postgreSQL database support [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- **ui**: [cb66b930](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/cb66b9303aa752ee131e3fd6edb9d670e37c3b0e) - add config groups navigation icon [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- **ui**: [a2a8e120](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/a2a8e1204649a27481862e85f8292a229e85fa97) - add some navigation icons [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#21](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/21) [#22](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/22) [#23](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/23) ] -- **itam**: [6a14f78b](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/6a14f78bf7fcda8509d1b0c6b477711b4da59180) - update inventory status icon [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) ] -- **itam**: [656807e4](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/656807e410c4dcc0d049cc61ce67c07114313925) - ensure device software pagination links keep interface on software tab [ [!35](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/35) [#81](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/81) ] -- **access**: [b42bb3a3](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/b42bb3a30e613ed9701c95c6ad10fa1890d17dac) - enable non-organization django permission checks [ [!39](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/39) [#76](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/76) ] -- **settings**: [090c4a54](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/090c4a542544cb61356bc00ce2258463d5647f67) - Add celery task results index and view page [ [!39](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/39) [#76](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/76) ] -- **base**: [87a1f2aa](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/87a1f2aa20fdcbbff1863a751a0c5e7d91b269bb) - Add background worker [ [!39](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/39) [#76](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/76) ] -- **itam**: [7b4ed7b1](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/7b4ed7b13537de064680f3c19a703b37c9d2bb83) - Update Serial Number from inventory if present and Serial Number not set [ [!37](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/37) ] -- **itam**: [b801c9a4](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/commit/b801c9a49e70ca5640c65bafdd05c29daed40798) - Update UUID from inventory if present and UUID not set [ [!37](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/merge_requests/37) [#66](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/66) ] +- adjust views missing add/change form to now use forms +- add navigation menu expand arrows +- migrate views to use new abstract model view classes +- migrate forms to use new abstract model form class +- **access**: Rename Team Button "new user" -> "Assign User" +- **access**: model pk and name not required context for adding a device +- rename field "model notes" -> "Notes" +- remove settings model +- **ui**: increase indentation to sub-menu items +- **itam**: rename old inventory status icon for use with security +- **api**: migrate inventory processing to background worker +- **itam**: only perform actions on device inventory if DB matches inventory item ## 0.6.0 (2024-06-30) -### Bug Fixes +### Feat -- **user_token**: [6cfcf158](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/6cfcf1580c669c046e4dd6d547b99c8b9814a078) - conduct user check on token view access [ [!34](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/34) [#63](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/63) ] -- **itam**: [f6866912](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/f6866912329fd2ea5f1bce6014db53605e1fee55) - use same form for edit and add [ [!34](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/34) [#65](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/65) ] -- **itam**: [802f2c41](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/802f2c410da1d4810005991f6da27963621adc25) - dont add field inventorydate if adding new item [ [!34](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/34) ] -- **api**: [4e428560](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/4e428560274fc2a82d927338c66b4641a1c93986) - inventory upload requires sanitization [ [!33](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/33) ] +- **api**: API token authentication +- **api**: abilty for user to create/delete api token +- **api**: create token model -### Code Refactor +### Fix -- **settings**: [66b8d936](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/66b8d9362d815e7f54ae402e4689c0a38f65c14d) - use seperate change/view views [ [!34](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/34) ] -- **settings**: [37d277e1](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/37d277e1493ab708b8861fa8d0de3191da24d2f2) - use form for user settings [ [!34](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/34) ] -- **tests**: [58b134ae](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/58b134ae30866b2ca207cef2cf17158d54517044) - move unit tests to unit test sub-directory [ [!33](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/33) [#15](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/15) ] +- **user_token**: conduct user check on token view access +- **itam**: use same form for edit and add +- **itam**: dont add field inventorydate if adding new item +- **api**: inventory upload requires sanitization -### Continious Integration +### Refactor -- **git_sync**: [a0874356](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/a0874356fd59978864664d4c25217dca527ee667) - sync on push ro feature branch 14-feat-project-management [ [!29](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/29) [!31](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/31) ] -- [5d8f5e3a](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/5d8f5e3a518bea520a4b6159623c60a3eaade051) - remove dockerhub publish on bot push [ [!29](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/29) ] - -### Documentaton / Guides - -- [4d3a2385](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/4d3a2385831c4db99bb9f3e70411b3d2d4d624f0) - Add user settings documentation [ [!34](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/34) [#63](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/63) ] -- **api**: [47d6a3be](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/47d6a3beffa7bb3d5b822c54440fe8b31ad18e02) - API Token authentication [ [!34](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/34) [#63](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/63) ] - -### Features - -- **api**: [11179143](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/111791438a45a8eb0cf4c175e4a1439cd56c84da) - API token authentication [ [!34](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/34) [#63](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/63) ] -- **api**: [ce2c6f3b](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/ce2c6f3b135ec9110682db3b77c80d6dde26a3c2) - abilty for user to create/delete api token [ [!34](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/34) [#63](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/63) ] -- **api**: [e655f22f](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/e655f22fac4d7de2ef42f16f33c8427528b63481) - create token model [ [!34](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/34) [#63](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/63) ] +- **settings**: use seperate change/view views +- **settings**: use form for user settings +- **tests**: move unit tests to unit test sub-directory ## 0.5.0 (2024-06-17) -### Bug Fixes +### Feat -- **itam**: [78216116](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/78216116dfdfca8542daa95e530bcb8855ec8cb6) - remove requirement that user needs change device to add notes [ [!27](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/27) [#52](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/52) ] -- **core**: [54c34a95](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/54c34a95f59e32ca393c71ca2e579d9ee69f5eff) - dont attempt to access parent_object if 'None' during history save [ [!27](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/27) ] -- **config_management**: [3b3ee9fc](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/3b3ee9fc3ddf1327207309cd860dc4ee9e2bf013) - Add missing parent item getter to model [ [!27](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/27) ] -- **core**: [0a1aba7c](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/0a1aba7ca8e5c639fa56bede846c980ea0a5fb4e) - overridden save within SaveHistory to use default attributes [ [!27](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/27) ] -- **access**: [eb8dca98](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/eb8dca980684cf6b7a1851a86c3eec71e71d07a3) - overridden save to use default attributes [ [!27](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/27) ] -- **core**: [7239f572](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/7239f572a35f576735b0a77832caebbe7a9df227) - on object delete remove history entries [ [!25](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/25) [#54](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/54) ] -- **api**: [505f4cfd](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/505f4cfdd9d6738d0f634508983412a1b736d3e3) - ensure proper permission checking [ [!24](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/24) [#55](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/55) ] -- [dc4968ee](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/dc4968ee7bc691b1537a7cc04a1da8b896f1c231) - dont throw an exception during settings load for an item django already checks [ [!23](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/23) ] -- **core**: [8d6826f7](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/8d6826f7c0dd12e0f179d76dfac4e45215e7a4a6) - Add overrides for delete so delete history saved for items with parent model [ [!22](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/22) [#53](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/53) ] -- **config_management**: [23c43ed8](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/23c43ed8dc67d75301a9f27cbb901d596b0074d7) - correct delete success url [ [!22](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/22) [#43](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/43) ] -- **base**: [07e93243](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/07e93243a02602b4e3881e1aa4e13b8e6815720f) - remove social auth from nav menu [ [!21](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/21) ] -- **access**: [579e44f8](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/579e44f8344120b76f8c1f7580428da1e20a9419) - add a team user permissions to use team organization [ [!21](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/21) [#51](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/51) ] +- **access**: add notes field to organization +- **access**: add organization manger +- **config_management**: Use breadcrumbs for child group name display +- **config_management**: ability to add host to global group +- **itam**: add a status of "bad" for devices +- **itam**: paginate device software tab +- **itam**: status of device visible on device index page +- **core**: add skeleton http browser +- **core**: Add a notes field to manufacturer/ publisher +- **itam**: Add a notes field to software category +- **itam**: Add a notes field to device types +- **itam**: Add a notes field to device models +- **itam**: Add a notes field to software +- **itam**: Add a notes field to operating system +- **itam**: Add a notes field to devices +- **access**: Add a notes field to teams +- **base**: Add a notes field to `TenancyObjetcs` class +- **settings**: add docs icon to application settings page +- **itam**: add docs icon to software page +- **itam**: add docs icon to operating system page +- **itam**: add docs icon to devices page +- **config_management**: add docs icon to config groups page +- **base**: add dynamic docs icon +- **models**: add property parent_object to models that have a parent +- **config_management**: add config group software to group history +- **itam**: render group software config within device rendered config +- **config_management**: assign software action to config group +- add configuration value 'SESSION_COOKIE_AGE' +- remove development SECRET_KEY and enforce checking for user configured one +- **base**: build CSRF trusted origins from configuration +- **base**: Enforceable SSO ONLY +- **base**: configurable SSO -### Code Refactor +### Fix -- **access**: [991ddc3d](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/991ddc3d7f67f40e3d210025b6c38726e80ba460) - relocate permission check to own function [ [!28](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/28) [#39](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/39) ] -- **itam**: [e517c5fd](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/e517c5fd761e9f84dc589468da96b26979f6b33f) - move device os tab to details tab [ [!27](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/27) [#22](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/22) ] -- **itam**: [4a104095](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/4a10409551e459d6a29a1c844d848759e05a25ad) - add device change form and adjust view to be non-form [ [!21](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/21) ] -- **itam**: [904234c5](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/904234c5818d893d63fc152e5f7b86d37226e122) - migrate device vie to use manual entered fields in two columns [ [!21](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/21) [#13](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/13) [#22](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/22) ] -- **access**: [4016d4c2](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/4016d4c20044053da7a9165723d0706fe8dabb7c) - migrate team users view to use forms [ [!21](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/21) ] -- **access**: [f36662ca](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/f36662ca82b697ff2e6db286214ae1cac296d55b) - migrate teams view to use forms [ [!21](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/21) ] -- **access**: [3e340a47](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/3e340a47b844e955d9ba9d51bcb2fabbcfac6287) - migrate organization view to use form [ [!21](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/21) ] -- **base**: [3fb27063](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/3fb270632141d1b70a38e1edafec1e43dca9c18b) - cleanup form and prettyfy [ [!23](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/23) [#24](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/24) ] -- **config_management**: [ae81ee88](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/ae81ee886334795d29298bbfdeb561f689b1409e) - relocate groups views to own directory [ [!22](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/22) ] -- [3b743a84](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/3b743a847c0b610de348718e392d417dd008e7b5) - login to use base template [ [!20](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/20) ] -- [95a08b2d](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/95a08b2d2ca5915eb62abbcfe2acf45e85b578d3) - adjust template block names [ [!20](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/20) ] +- **itam**: remove requirement that user needs change device to add notes +- **core**: dont attempt to access parent_object if 'None' during history save +- **config_management**: Add missing parent item getter to model +- **core**: overridden save within SaveHistory to use default attributes +- **access**: overridden save to use default attributes +- **core**: on object delete remove history entries +- **api**: ensure proper permission checking +- dont throw an exception during settings load for an item django already checks +- **core**: Add overrides for delete so delete history saved for items with parent model +- **config_management**: correct delete success url +- **base**: remove social auth from nav menu +- **access**: add a team user permissions to use team organization -### Continious Integration +### Refactor -- [fa28fd43](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/fa28fd436ef03b66c270ad460cadfd877434fd0d) - dont rebuild on dev on git tag [ [!19](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/19) ] - -### Documentaton / Guides - -- [a9485687](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/a94856879e740a261354f2fce8b1f213c5afcf86) - correct testing link [ [!28](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/28) ] -- [108398da](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/108398da4b2cd9280b65c6fef3bdc6cc0e9758e3) - rejig [ [!28](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/28) ] -- **access**: [8abbf2ff](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/8abbf2ff9e33e1607be11be2c4757038334c0d11) - correct doc warnings [ [!28](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/28) ] -- **access**: [27b62d10](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/27b62d10180e0a299554fc72d2bd0faacf2e3b75) - add link to docs on team page [ [!28](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/28) [#39](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/39) ] -- **access**: [aef276b7](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/aef276b76c4636290d76735e7a390178e3bef12b) - add link to docs on organization page [ [!28](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/28) [#39](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/39) ] -- [afb5a709](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/afb5a709d7f19a042dcf451d227e29c26f3e04dc) - add badges to index [ [!27](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/27) ] -- [ddead8eb](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/ddead8eb56a06570926bc9aeeef877162a7d06fd) - restructure to sections administration, user and devlopment [ [!27](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/27) [!62](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/62) ] -- **development**: [f861295b](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/f861295b1c48ea843cdaeef107e3652468328814) - add device model to api docs [ [!27](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/27) ] -- [dbcb2825](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/dbcb2825487cd425fac6e60a3aeb9f02d18de96c) - docstrings show category headings [ [!27](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/27) ] -- **development**: [5eec41fe](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/5eec41fe57f5cd26943cddae53623a74ad1ddc58) - Add test case documentation [ [!27](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/27) [#15](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/15) [!16](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/16) [#57](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/57) [!83](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/83) ] -- **api**: [2eb50311](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/2eb50311b485094304f99cdd7d5fc1513896fabd) - document the inventory endpoint [ [!24](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/24) [#55](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/55) ] -- **api**: [36fa364d](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/36fa364d04473c7a9de71890fed4eebf843954ed) - notate inventory permission [ [!24](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/24) [#55](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/55) ] -- [05bb6f8a](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/05bb6f8a516edf9b61f6a3d3bdbb902b675b77b9) - update contributing with further test info [ [!22](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/22) ] -- **config_management**: [e62a570b](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/e62a570be3ca429e946b0d15b4865c9e71e9d4d1) - notate software group actions [ [!22](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/22) [#43](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/43) ] - -### Features - -- **access**: [84866185](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/848661856a44de31ee2116c1b5c4445fe6daf654) - add notes field to organization [ [!28](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/28) [#39](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/39) [#13](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/13) ] -- **access**: [14acea31](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/14acea31f286541ef75358ee37062ee042ea8ca8) - add organization manger [ [!28](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/28) [#39](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/39) ] -- **config_management**: [8af59754](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/8af5975428bb9d3aa18fe2126be702d894d7682e) - Use breadcrumbs for child group name display [ [!21](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/21) ] -- **config_management**: [ac707157](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/ac70715752eeeeb9f01526989e145a79b3b3a92f) - ability to add host to global group [ [!21](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/21) ] -- **itam**: [8ccdf9a8](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/8ccdf9a8f3815ae51a5005024021709b56601627) - add a status of "bad" for devices [ [!21](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/21) ] -- **itam**: [1200a879](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/1200a879136c52bcf63b8629a1ab8ca1d7edc0e5) - paginate device software tab [ [!21](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/21) ] -- **itam**: [e8cb685d](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/e8cb685da1bcfdec98da06fba77d076b0f9c096b) - status of device visible on device index page [ [!21](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/21) ] -- **core**: [8b47d956](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/8b47d95614d81685edd277c300890e58fa7153ff) - add skeleton http browser [ [!26](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/26) [#58](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/58) ] -- **core**: [c570fb11](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/c570fb114f70a2a1785fffe3ba627eee86c0c8d5) - Add a notes field to manufacturer/ publisher [ [!21](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/21) [#13](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/13) ] -- **itam**: [ea1727f2](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/ea1727f2c796bd305b0e01c87f1db16c217b1b1e) - Add a notes field to software category [ [!21](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/21) [#13](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/13) ] -- **itam**: [36d7e545](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/36d7e54547bd5afc1270c02f6160b95eda5cc557) - Add a notes field to device types [ [!21](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/21) [#13](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/13) ] -- **itam**: [a02fda84](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/a02fda84137810d840168d214af471dac451887f) - Add a notes field to device models [ [!21](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/21) [#13](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/13) ] -- **itam**: [b5bc76b0](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/b5bc76b0ab79dcec38d5c91a95c1a737b89c9f8b) - Add a notes field to software [ [!21](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/21) [#13](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/13) ] -- **itam**: [36c13e18](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/36c13e18c7f8f6e3354a2fb79c6a2240fdec431f) - Add a notes field to operating system [ [!21](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/21) [#13](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/13) ] -- **itam**: [6969b611](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/6969b611644f025426cdb1f77c92f7a79d361859) - Add a notes field to devices [ [!21](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/21) [#13](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/13) ] -- **access**: [85bf1b99](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/85bf1b9907788f1e190c00827c096b00e25a8048) - Add a notes field to teams [ [!21](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/21) [#13](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/13) ] -- **base**: [ca8e0c07](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/ca8e0c07ea6aff681e03ed966412403ba94c2d7c) - Add a notes field to `TenancyObjetcs` class [ [!21](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/21) [#13](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/13) ] -- **settings**: [da93425c](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/da93425c0b9eacb9d46233173c1cb99b79487103) - add docs icon to application settings page [ [!21](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/21) ] -- **itam**: [8a9899cf](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/8a9899cf66bee3ac1f752ca38e4360aafa92be51) - add docs icon to software page [ [!21](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/21) ] -- **itam**: [38db558b](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/38db558be2e330c7f9ebf6a1c70c70dc59ee1bd0) - add docs icon to operating system page [ [!21](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/21) ] -- **itam**: [67b204e4](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/67b204e40cfbb1e078dac6aecc9835b08132c13c) - add docs icon to devices page [ [!21](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/21) ] -- **config_management**: [456fed80](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/456fed80a9fd6adc09cf8cb7e1be6ea0a4492c5e) - add docs icon to config groups page [ [!21](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/21) ] -- **base**: [87282ce4](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/87282ce41cca9a750c7bc99ec8c1a78aa2b03dd7) - add dynamic docs icon [ [!21](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/21) ] -- **models**: [fe0696fe](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/fe0696fee6211236ac7f69e77dd0ec9c1516b25c) - add property parent_object to models that have a parent [ [!22](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/22) ] -- **config_management**: [1069211d](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/1069211d1bde9362fdb6fd2f2f5d080bffd81a6e) - add config group software to group history [ [!22](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/22) [#43](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/43) ] -- **itam**: [460eff1f](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/460eff1f71af6d75958c0eeb3dacb9b0169c6ca9) - render group software config within device rendered config [ [!22](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/22) [#43](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/43) ] -- **config_management**: [0c382a73](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/0c382a73e5e0462b4bd598734159626f14f3a96e) - assign software action to config group [ [!22](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/22) [#43](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/43) ] -- [8b887575](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/8b887575c921cdd6f5656e7e37ca8dab747081bf) - add configuration value 'SESSION_COOKIE_AGE' [ [!20](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/20) ] -- [d0e8e9a6](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/d0e8e9a674c24aff675ac73ba34dd20acca26b0c) - remove development SECRET_KEY and enforce checking for user configured one [ [!20](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/20) ] -- **base**: [d8d75c7d](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/d8d75c7db09f7532665408299c9bf878e079f99a) - build CSRF trusted origins from configuration [ [!20](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/20) ] -- **base**: [b38984fc](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/b38984fcb95ab121a610710b9df049ee7caa17cd) - Enforceable SSO ONLY [ [!20](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/20) [#1](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/1) ] -- **base**: [3040d4af](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/3040d4afe74c65ebf656329ea3cfe13e59409a61) - configurable SSO [ [!20](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/20) [#1](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/1) ] +- **access**: relocate permission check to own function +- **itam**: move device os tab to details tab +- **itam**: add device change form and adjust view to be non-form +- **itam**: migrate device vie to use manual entered fields in two columns +- **access**: migrate team users view to use forms +- **access**: migrate teams view to use forms +- **access**: migrate organization view to use form +- **base**: cleanup form and prettyfy +- **config_management**: relocate groups views to own directory +- login to use base template +- adjust template block names ## 0.4.0 (2024-06-05) -### Bug Fixes +### Feat -- **itam**: [dd0c13a6](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/dd0c13a65f4cc55e2047f1b654dd228147eac183) - ensure device type saves history [ [!18](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/18) ] -- **core**: [4cafa34d](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/4cafa34d69332995307faf29eff42efd81e569d6) - correct history view permissions [ [!18](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/18) [#48](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/48) [#15](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/15) ] -- **config_management**: [2c1bbbfc](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/2c1bbbfc15babdc22d67285dae1c18a4b6f3cc96) - set config dict keys to be valid ansible variables [ [!18](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/18) [#47](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/47) ] -- **itam**: [dd30a57a](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/dd30a57a9db7f5d6aa318651d9e252dae7f73b58) - correct logic for device add dynamic success url [ [!18](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/18) ] -- **itam**: [18e84db6](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/18e84db63c992c142f31f44fcc650561a16045fe) - correct config group link for device [ [!18](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/18) ] -- **config_management**: [c9098f5d](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/c9098f5d2fe1817d7d33b7ffd30aeffa4077cdd2) - correct model permissions [ [!17](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/17) [#42](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/42) ] -- **config_management**: [d422f2fe](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/d422f2feee4a5013ced153c43e0858098890d90b) - add config management to navigation [ [!17](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/17) [#42](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/42) ] -- **ui**: [8061b7c8](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/8061b7c8e29b0f1ad12969a3d4e6a3e27cd85b2d) - remove api entries from navigation [ [!17](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/17) ] -- **api**: [f41282d0](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/f41282d08b6a417a12d22268a67b27425bed2361) - check for org must by by type None [ [!16](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/16) ] -- **api**: [8dfb996b](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/8dfb996b24c26f3697a8d3c787faab4f190953eb) - correct software permissions [ [!16](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/16) ] -- **api**: [95dc9794](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/95dc979419c7cb0f6bfeff71169201149c9341fb) - corrct device permissions [ [!16](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/16) ] -- **api**: [09cc1db6](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/09cc1db665af30c32d043644806f65e56f80c510) - permissions for teams [ [!16](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/16) ] -- **api**: [e7c535c4](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/e7c535c48d73f7f3dbde6a0c191afb134ba2dd72) - correct reverse url lookup to use NS API [ [!16](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/16) ] -- **api**: [e9cd111a](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/e9cd111af6299dee24b7c917726c54f7e7be8fe2) - permissions for organization [ [!16](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/16) ] +- **database**: add mysql support +- **api**: move invneotry api endpoint to '/api/device/inventory' +- **core**: support more history types +- **core**: function to fetch history entry item +- **config_management**: Add button to groups ui for adding child group +- **access**: throw error if no organization added +- **itam**: add delete button to config group within ui +- **itam**: Config groups rendered configuration now part of devices rendered configuration +- **config_management**: Ability to delete a host from a config group +- **config_management**: Ability to add a host to a config group +- **config_management**: ensure config doesn't use reserved config keys +- **config_management**: Config groups rendered config +- **config_management**: add configuration groups +- **api**: add swagger ui for documentation +- **api**: filter software to users organizations +- **api**: filter devices to users organizations +- **api**: add org team view page +- **api**: configure team permissions -### Code Refactor +### Fix -- **access**: [6650434c](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/6650434c63a7fc620f98ed79b32fe4bbd52b1ada) - cache object so it doesnt have to be called multiple times [ [!18](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/18) ] -- **config_management**: [58738971](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/5873897184e906ae1fd3419a018441de78c5741d) - move groups to nav menu [ [!17](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/17) [#42](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/42) ] -- **api**: [e257c114](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/e257c1148808d6159bd6c8396a22168aa88c3b2f) - migrate devices and software to viewsets [ [!16](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/16) ] -- **api**: [33b1a6c9](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/33b1a6c91dc6d7f47738b96f7ce08b616e0749bb) - move permission check to mixin [ [!16](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/16) ] -- **access**: [5f3b48ea](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/5f3b48ea982588e39137a3e695a2bbe65fd4c0a2) - add team option to org permission check [ [!16](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/16) ] +- **itam**: ensure device type saves history +- **core**: correct history view permissions +- **config_management**: set config dict keys to be valid ansible variables +- **itam**: correct logic for device add dynamic success url +- **itam**: correct config group link for device +- **config_management**: correct model permissions +- **config_management**: add config management to navigation +- **ui**: remove api entries from navigation +- **api**: check for org must by by type None +- **api**: correct software permissions +- **api**: corrct device permissions +- **api**: permissions for teams +- **api**: correct reverse url lookup to use NS API +- **api**: permissions for organization -### Continious Integration +### Refactor -- [8e338c7c](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/8e338c7ca02057728089a8dabceae4348d3cb04a) - add pytest coverage report as environment [ [!15](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/15) [#37](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/37) ] -- [9b811ede](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/9b811ede266631a297bf84851b68f8b11a5d9f39) - run container build/publish on git tag [ [!15](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/15) ] - -### Documentaton / Guides - -- **config_management**: [0a17329a](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/0a17329a710e7f94ea3054857975467236130d1c) - notate future feature [ [!17](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/17) [#42](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/42) ] -- [0d18e974](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/0d18e974dda10490f0b2b95f416fd8af8351a58a) - correct liniting errors [ [!17](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/17) ] -- **config_management**: [62e605d4](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/62e605d4172c114d9d14a6aebf2bc122cee21866) - document module [ [!17](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/17) [#42](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/42) ] -- **api**: [fbdbede4](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/fbdbede4295005ab861b1ef3c0fe552c516b8738) - add team/org paths [ [!16](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/16) [#41](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/41) ] - -### Features - -- **database**: [adeffff4](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/adeffff42c666243ce7e3b84ca2de3140bb350ca) - add mysql support [ [!19](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/19) [#16](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/16) ] -- **api**: [c0173d6f](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/c0173d6feb22e1dae42d596c8e916d3083e63c4d) - move invneotry api endpoint to '/api/device/inventory' [ [!18](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/18) ] -- **core**: [eb6ae13c](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/eb6ae13c58d45240c0ad99fcde2bc1c3fbaef035) - support more history types [ [!18](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/18) ] -- **core**: [46bdd488](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/46bdd488ecedde9aeac97947caf96a5efb8c437f) - function to fetch history entry item [ [!18](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/18) [#48](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/48) [#15](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/15) ] -- **config_management**: [55f0db22](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/55f0db2217e247d7f76edd1c2e81bfd9b7570698) - Add button to groups ui for adding child group [ [!17](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/17) [#42](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/42) ] -- **access**: [7fe12603](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/7fe12603080d649bedb5d29f7084083271c9c982) - throw error if no organization added [ [!17](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/17) ] -- **itam**: [df27a7df](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/df27a7dfd365faed6ee1194433e0d8da9499600b) - add delete button to config group within ui [ [!17](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/17) [#42](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/42) ] -- **itam**: [5cb155e0](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/5cb155e01f72680c8249690be885f386679d458a) - Config groups rendered configuration now part of devices rendered configuration [ [!17](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/17) [#42](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/42) ] -- **config_management**: [39bfbd25](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/39bfbd25cbc2f936f54600d508cfd8c67a4e023b) - Ability to delete a host from a config group [ [!17](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/17) [#42](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/42) ] -- **config_management**: [fff51e38](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/fff51e38d2503bf4741b3734bfffad6d537fd862) - Ability to add a host to a config group [ [!17](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/17) [#42](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/42) ] -- **config_management**: [746b7ac7](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/746b7ac747fbb39657912225135dc1d4d4178c8c) - ensure config doesn't use reserved config keys [ [!17](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/17) [#42](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/42) ] -- **config_management**: [a7d195df](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/a7d195dfcbd38d14e04b1f45faeba09baca21696) - Config groups rendered config [ [!17](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/17) [#42](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/42) ] -- **config_management**: [fdeae217](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/fdeae217fa8883031b67df12c1f0f8b06ff92bbd) - add configuration groups [ [!17](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/17) [#42](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/42) ] -- **api**: [3f68d67b](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/3f68d67ba581e11cfe8ec88d2a1cdb7c6ba63e46) - add swagger ui for documentation [ [!17](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/17) ] -- **api**: [4151e0af](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/4151e0afdc6cbc9a253f41441ab0074fe947db01) - filter software to users organizations [ [!17](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/17) [#45](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/45) ] -- **api**: [89a5e0f4](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/89a5e0f4cc1336e042f242dfeef9a88c37b1d9f4) - filter devices to users organizations [ [!17](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/17) [#45](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/45) ] -- **api**: [3fef74e7](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/3fef74e7000bcd7e90a15d40e68f667c4a882114) - add org team view page [ [!16](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/16) [#41](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/41) ] -- **api**: [c0a09d5d](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/c0a09d5d505dedc5562be08844ccd3e7fc5b589a) - configure team permissions [ [!5](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/5) [#36](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/36) ] +- **access**: cache object so it doesnt have to be called multiple times +- **config_management**: move groups to nav menu +- **api**: migrate devices and software to viewsets +- **api**: move permission check to mixin +- **access**: add team option to org permission check ## 0.3.0 (2024-05-29) -### Bug Fixes +### Feat -- **settings**: [d379205b](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/d379205bffcc808031e8227d08220ef5d6c4e130) - Add correct permissions for team user delete [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **settings**: [ebf4cb7a](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/ebf4cb7a5daf6fcf2f39f912203ac9ed31d7fca6) - Add correct permissions for team user view/change [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **settings**: [b5669c83](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/b5669c83869b38463f7c99008eb9e2b29b59faf2) - Add correct permissions for team view/change [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **settings**: [58e688e0](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/58e688e0a5f44a63d1526b1a73f6ce63d67d3e07) - Add correct permissions for team add [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **settings**: [e3c2f712](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/e3c2f712c19bd1040c311891bd766311e302be6f) - Add correct permissions for team delete [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **access**: [0abcb462](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/0abcb4628e5c5fac3d7997b457df7589772b929f) - correct back link within team view [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **access**: [b9a2d2ac](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/b9a2d2ac59d8e31c99a268375b51d866186dc8bf) - correct url name to be within naming conventions [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **settings**: [8bfc952f](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/8bfc952f2eaac67bb1c40a40fdfd8046b8580eed) - Add correct permissions for manufacturer / publisher delete [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **settings**: [6e6bd107](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/6e6bd1070e5c0b63f8b97c6617098b21823f609c) - Add correct permissions for manufacturer / publisher add [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **settings**: [42fd648e](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/42fd648e4c6817af88248343312a1232bbfa22d3) - Add correct permissions for manufacturer / publisher view/update [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **settings**: [9893e5f9](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/9893e5f95270ebc3476a5c7c070080399304afab) - Add correct permissions for software category delete [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **settings**: [e35a2300](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/e35a2300e261586be5aa209e5cc70ad190d8d00c) - Add correct permissions for software category add [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **settings**: [0aa78a4c](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/0aa78a4c514faaddeb7501c1824ccdedc896c39c) - Add correct permissions for software category view/update [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **settings**: [84d895c2](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/84d895c214c8109b70b3bb764c26bcc488e0a85d) - Add correct permissions for device type delete [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **settings**: [cba28108](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/cba28108e04f3e9007f904c8038fa07edbf5d0ea) - Add correct permissions for device type add [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **settings**: [18339547](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/18339547ba8450d7ba25872085a7efda39049a87) - Add correct permissions for device type view/update [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **settings**: [d2e9e107](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/d2e9e1070e72e0aaf9dacb2cfff5d4d5c0bfb679) - Add correct permissions for device model delete [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **settings**: [6880c5e9](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/6880c5e90b8dbd8969b00b6571bb38f004f2db13) - Add correct permissions for device model add [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **settings**: [608a3838](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/608a38384db6415162d155b951abad743e03a10d) - Add correct permissions for device model view/update [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **access**: [cb7987f8](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/cb7987f841626687c8ec5b1ad17df3fcf2698257) - Add correct permissions for organization view/update [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **access**: [98885a32](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/98885a32e71463403f1bb9c535cb6cab39d09733) - use established view naming [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **itam**: [6b37c952](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/6b37c952f82367e5178ed926757e47c07436ebd5) - Add correct permissions for operating system delete [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **itam**: [d81d1ba3](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/d81d1ba32a51a592193f013b0a2eb45178e9fa49) - Add correct permissions for operating system add [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **itam**: [01c6cd4b](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/01c6cd4bdf3a167179be4a7e07ed59751ccf44ed) - Add correct permissions for operating system view/update [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **itam**: [88058234](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/880582340561060a8466c619b191d01cff261f65) - Add correct permissions for software delete [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **itam**: [7dd2634f](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/7dd2634facf6708b5aef5c22413b2fb7f5b5da44) - Add correct permissions for software add [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **itam**: [b1cfb9fa](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/b1cfb9fa59d009a0dfba6a64184264166ace5a11) - for non-admin user use correct order by fields for software view/update [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **itam**: [550e6f40](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/550e6f40801071b8c5222e809d9e922de0cb0c74) - Add correct permissions for software view/update [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **itam**: [94116fa1](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/94116fa173c8ed05d84d84aa09467d10fe02cd4c) - ensure permission_required parameter for view is a list [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **core**: [0e726684](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/0e7266845402d07d0cd289f268ae22e4a977362a) - dont save history when no user information available [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **access**: [37ceffcb](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/37ceffcb3bd196557d3fe0cc90b7c6722113e092) - during organization permission check, check the entire list of permissions [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **core**: [c656f5bc](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/c656f5bce597fc333a6549a2159b847a7338de29) - dont save history for anonymous user [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **access**: [6cb69c62](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/6cb69c627ff54eb1b5bfa11da2b60d4bb0b45b19) - during permission check use post request params for an add action [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **user**: [80c3af32](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/80c3af32d533995679854d7a20984c8dd4904fd0) - on new-user signal create settings row if not exist [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **itam**: [9d6bd6db](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/9d6bd6db83c56b8904d86829ef1134d689c5fb3e) - ensure only user with change permission can change a device [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **user**: [2750750a](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/2750750a0c3f384ab384d409d90f00afc44cc619) - if user settings row doesn't exist on access create [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **access**: [664ad0ec](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/664ad0ec7d220fd20aea1ad405b27546ac62b57f) - adding/deleting team group actions moved to model save/delete method override [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **api**: [1c9d8b1c](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/1c9d8b1c7e72a15e6186aa6d95a30e4ba3fbfac4) - add teams and permissions to org and teams respectively [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **ui**: [a3716b01](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/a3716b01584cb7842b782b0e9dd986c5542d8b6c) - correct repo url used [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **api**: [752770ec](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/752770ec32b6330ddd6060dc71dcbf3e60aacd83) - device inventory date set to read only [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **software**: [46af675f](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/46af675f3c87d975a8dab3da70090fa3ab3f7033) - ensure management command query correct for migration [ [!12](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/12) [#32](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/32) ] -- **device**: [7f4a036a](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/7f4a036a32630599ef95bbe801d24e6204c61fcf) - OS form trying to add last inventory date when empty [ [!11](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/11) ] -- [249b9cba](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/249b9cbab9e8f917e0e1ecd97fbcc5300c1c832f) - add static files path to urls [ [!11](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/11) ] -- **inventory**: [f5d5529c](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/f5d5529c173d64ef839e9db638600e963ec6a0aa) - Dont select device_type, use 'null' [ [!10](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/10) [#17](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/17) ] -- **base**: [d2dba2f7](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/d2dba2f7b8b38370da0dae18cab752789ac2e5e8) - show "content_title - SITE_TITLE" as site title [ [!10](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/10) [#18](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/18) ] -- **device**: [2689c35d](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/2689c35db36455982e57b08fd418ab2244280af0) - Read Only field set as required=false [ [!9](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/9) ] -- [7ae7ffae](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/7ae7ffaef46cc27a2bae9acdcfa70dff80a05f7a) - correct typo in notes templates [ [!8](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/8) [#7](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/7) ] -- **ui**: [5273b58a](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/5273b58afb383843de6da3faca552fe143fff8eb) - Ensure navigation menu entry highlighted for sub items [ [!8](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/8) ] +- **access**: during organization permission check, check to ensure user is logged on +- **history**: always create an entry even if user=none +- **itam**: device uuid must be unique +- **itam**: device serial number must be unique +- **setting**: Enable super admin to set ALL manufacturer/publishers as global +- **setting**: Enable super admin to set ALL device types as global +- **setting**: Enable super admin to set ALL device models as global +- **setting**: Enable super admin to set ALL software categories as global +- **UI**: show build details with page footer +- **software**: Add output to stdout to show what is and has occurred +- **base**: Add delete icon to content header +- **itam**: Populate initial organization value from user default organization for software category creation +- **itam**: Populate initial organization value from user default organization for device type creation +- **itam**: Populate initial organization value from user default organization for device model creation +- **api**: Populate initial organization value from user default organization inventory +- **itam**: Populate initial organization value from user default organization for Software creation +- **itam**: Populate initial organization value from user default organization for operating system creation +- **device**: Populate initial organization value from user default organization +- Add management command software +- **setting**: Enable super admin to set ALL software as global +- **user**: Add user settings panel +- **itam**: Add publisher to software +- **itam**: Add publisher to operating system +- **itam**: Add device model +- **core**: Add manufacturers +- **settings**: add dummy model for permissions +- **settings**: new module for whole of application settings/globals +- **access**: Save changes to history for organization and teams +- **software**: Save changes to history +- **operating_system**: Save changes to history +- **device**: Save changes to history +- **core**: history model for saving model history +- **itam**: Ability to add notes to software +- **itam**: Ability to add notes to operating systems +- **itam**: Ability to add notes on devices +- **core**: notes model added to core +- **device**: Record inventory date and show as part of details +- **ui**: Show inventory details if they exist +- **api**: API accept computer inventory -### Code Refactor +### Fix -- **access**: [dd0eaae6](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/dd0eaae6b3c112bf9746b7ab37b996ba693650fc) - add to models a get_organization function [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **access**: [e34d2998](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/e34d29987e128190cefd1af9cd1c504123c59170) - remove change view [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **itam**: [668e871e](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/668e871e4fd52abb20426b8dafa988c423d3d3a7) - relocation item delete from list to inside device [ [!11](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/11) [#23](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/23) ] -- **context_processor**: [900412b3](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/900412b31706fbba0040139dd5a04b76aeb32af2) - relocate as base [ [!11](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/11) ] -- **itam**: [23e661ce](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/23e661cef04627912363492955b004920748edb6) - software index does not require created and modified date [ [!10](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/10) ] -- **organizations**: [a6a0da72](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/a6a0da72b223ca64b6e7361db6e250ebbceedddb) - set org field to null if not set [ [!10](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/10) ] -- **itam**: [66e8b290](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/66e8b290146cb241455792cb70d5de184b59819a) - move software categories to settings app [ [!10](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/10) ] -- **itam**: [c83b8836](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/c83b8836730babd497b185c3ccf28176cd298ba7) - move device types to settings app [ [!10](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/10) ] -- **template**: [191244ed](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/191244ed40f8feacf8885f0eb9fff5b32ef91252) - content_title can be rendered in base [ [!8](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/8) ] +- **settings**: Add correct permissions for team user delete +- **settings**: Add correct permissions for team user view/change +- **settings**: Add correct permissions for team view/change +- **settings**: Add correct permissions for team add +- **settings**: Add correct permissions for team delete +- **access**: correct back link within team view +- **access**: correct url name to be within naming conventions +- **settings**: Add correct permissions for manufacturer / publisher delete +- **settings**: Add correct permissions for manufacturer / publisher add +- **settings**: Add correct permissions for manufacturer / publisher view/update +- **settings**: Add correct permissions for software category delete +- **settings**: Add correct permissions for software category add +- **settings**: Add correct permissions for software category view/update +- **settings**: Add correct permissions for device type delete +- **settings**: Add correct permissions for device type add +- **settings**: Add correct permissions for device type view/update +- **settings**: Add correct permissions for device model delete +- **settings**: Add correct permissions for device model add +- **settings**: Add correct permissions for device model view/update +- **access**: Add correct permissions for organization view/update +- **access**: use established view naming +- **itam**: Add correct permissions for operating system delete +- **itam**: Add correct permissions for operating system add +- **itam**: Add correct permissions for operating system view/update +- **itam**: Add correct permissions for software delete +- **itam**: Add correct permissions for software add +- **itam**: for non-admin user use correct order by fields for software view/update +- **itam**: Add correct permissions for software view/update +- **itam**: ensure permission_required parameter for view is a list +- **core**: dont save history when no user information available +- **access**: during organization permission check, check the entire list of permissions +- **core**: dont save history for anonymous user +- **access**: during permission check use post request params for an add action +- **user**: on new-user signal create settings row if not exist +- **itam**: ensure only user with change permission can change a device +- **user**: if user settings row doesn't exist on access create +- **access**: adding/deleting team group actions moved to model save/delete method override +- **api**: add teams and permissions to org and teams respectively +- **ui**: correct repo url used +- **api**: device inventory date set to read only +- **software**: ensure management command query correct for migration +- **device**: OS form trying to add last inventory date when empty +- add static files path to urls +- **inventory**: Dont select device_type, use 'null' +- **base**: show "content_title - SITE_TITLE" as site title +- **device**: Read Only field set as required=false +- correct typo in notes templates +- **ui**: Ensure navigation menu entry highlighted for sub items -### Continious Integration +### Refactor -- **docker**: [19d24b54](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/19d24b54a2cb9f4f81692b863260089caed12772) - build on any change [ [!12](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/12) ] -- **docker**: [2c81007c](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/2c81007c0ae73586fdaabe9387cc625486dee8f4) - always build on dev branch [ [!8](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/8) ] - -### Documentaton / Guides - -- [3af254d9](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/3af254d9e83093b91200a2b6ef9b6f92975a6ad8) - update software and os [ [!10](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/10) [#12](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/12) ] -- **core**: [f7444892](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/f7444892d06bb25c6a7de92ecf0410a2109ae0f5) - Add history docs [ [!9](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/9) [#5](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/5) ] -- **core**: [5dadc3fe](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/5dadc3fe98e3317c83305b73c1f9763617f486a8) - Add details about model notes [ [!8](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/8) [#7](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/7) ] -- [6b5acc0d](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/6b5acc0d575706d30a41d26f51dfe7d6f7bdf945) - add inventory details [ [!8](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/8) [#2](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/2) ] - -### Features - -- **access**: [7f7f7197](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/7f7f719731ce1214582f69f55544a1059b874a80) - during organization permission check, check to ensure user is logged on [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **history**: [8d786d4d](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/8d786d4dea2ed3b290a83f90c863bbe46e53cefd) - always create an entry even if user=none [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **itam**: [353117aa](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/353117aa74f0e0aac3ad687628cc7a21af690f0e) - device uuid must be unique [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **itam**: [c4fe2185](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/c4fe218592fe25ebad842e8bb24ce1f2062debaa) - device serial number must be unique [ [!13](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/13) ] -- **setting**: [bf69a301](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/bf69a30163e6bafd0f951dde5fd058a31c327c09) - Enable super admin to set ALL manufacturer/publishers as global [ [!12](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/12) ] -- **setting**: [ece6b9e3](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/ece6b9e354a149ff1aa73d147bedcb3c406603c0) - Enable super admin to set ALL device types as global [ [!12](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/12) [#31](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/31) ] -- **setting**: [abbda7b4](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/abbda7b400d7c06fee5165157d2bf545c00d4bbe) - Enable super admin to set ALL device models as global [ [!12](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/12) [#29](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/29) ] -- **setting**: [935e119e](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/935e119e6418d561739fff4e8b1fdbb399588a18) - Enable super admin to set ALL software categories as global [ [!12](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/12) [#30](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/30) ] -- **UI**: [da0d3a81](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/da0d3a816d398d45d729ad9bcd5c0f2c625a3469) - show build details with page footer [ [!12](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/12) [#25](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/25) ] -- **software**: [51e52e69](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/51e52e69a48fefc792b8556aa20d84652a53afe7) - Add output to stdout to show what is and has occurred [ [!12](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/12) [#32](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/32) ] -- **base**: [b2f7c831](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/b2f7c831551469445d091cd176ab6f210ec862e1) - Add delete icon to content header [ [!11](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/11) [#23](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/23) ] -- **itam**: [e66e9b8d](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/e66e9b8dca740ffe411b74c8f50c53c732acef2f) - Populate initial organization value from user default organization for software category creation [ [!11](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/11) [#28](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/28) ] -- **itam**: [4c002bc2](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/4c002bc259062fc4926deda8a943d165496c8a25) - Populate initial organization value from user default organization for device type creation [ [!11](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/11) [#28](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/28) ] -- **itam**: [90f95672](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/90f95672aa71c0a5358238f491949fb356503ff3) - Populate initial organization value from user default organization for device model creation [ [!11](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/11) [#28](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/28) ] -- **api**: [7f3bf95b](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/7f3bf95b4627be434732b1045120e59fcae21cf3) - Populate initial organization value from user default organization inventory [ [!11](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/11) [#28](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/28) ] -- **itam**: [9f5e5d25](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/9f5e5d25ec574caa0aeec91c5763f7f64c8b11c2) - Populate initial organization value from user default organization for Software creation [ [!11](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/11) [#28](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/28) ] -- **itam**: [62c0bb77](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/62c0bb77fe5134669e77b198acc02c28ea073696) - Populate initial organization value from user default organization for operating system creation [ [!11](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/11) [#28](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/28) ] -- **device**: [abbd6a49](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/abbd6a49d64fc24eb2c27191b594e8cefdb9042b) - Populate initial organization value from user default organization [ [!11](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/11) [#28](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/28) ] -- [395f24f2](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/395f24f22c5418eed99d59659b7c60726c2ade53) - Add management command software [ [!11](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/11) [#27](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/27) ] -- **setting**: [f36400db](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/f36400dbb98d305e1ec54f62493f7f2cff0359b1) - Enable super admin to set ALL software as global [ [!11](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/11) [#27](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/27) ] -- **user**: [ee7977fe](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/ee7977fe4a5e78844e8652c313af49eda901bdad) - Add user settings panel [ [!11](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/11) [#28](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/28) ] -- **itam**: [2fcbb1ea](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/2fcbb1ead72cfa294ad26f63688b4344df56b0db) - Add publisher to software [ [!10](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/10) [#12](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/12) ] -- **itam**: [53baeb59](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/53baeb59c9e0d0eff681ba36996e47c22bd7afe7) - Add publisher to operating system [ [!10](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/10) [#12](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/12) ] -- **itam**: [99a559fe](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/99a559fe6dd8b234cf860c7a44fd65dc178c1bc7) - Add device model [ [!10](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/10) [#12](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/12) ] -- **core**: [ef463b84](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/ef463b845d1738a6067c786bf410f5109d832a5c) - Add manufacturers [ [!10](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/10) [#12](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/12) ] -- **settings**: [bf0fa3f4](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/bf0fa3f41dda11a02d6ae2c9b58a9e21900d2a9f) - add dummy model for permissions [ [!10](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/10) ] -- **settings**: [ac233e43](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/ac233e432f7222c59a0bb62a1cd85a6a7770c13c) - new module for whole of application settings/globals [ [!10](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/10) ] -- **access**: [724c52b7](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/724c52b777896600fc8ed5a71bb3e3f6429f9e56) - Save changes to history for organization and teams [ [!9](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/9) [#5](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/5) ] -- **software**: [b5470f2c](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/b5470f2cefeddac2dd154ef37975902fe511e9f6) - Save changes to history [ [!9](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/9) [#5](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/5) ] -- **operating_system**: [e16a4212](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/e16a4212ccd8c7ed04d34a87bbef78a4f5565166) - Save changes to history [ [!9](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/9) [#5](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/5) ] -- **device**: [6cbcd4aa](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/6cbcd4aa56441efce29c8dfe2489794716a72e8b) - Save changes to history [ [!9](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/9) [#5](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/5) ] -- **core**: [9b2abeca](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/9b2abecac37f9b2ea12a56ad2023afedc6dd78fc) - history model for saving model history [ [!9](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/9) [#5](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/5) ] -- **itam**: [dec29429](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/dec2942996073746f78463b69156e67c8d879b72) - Ability to add notes to software [ [!8](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/8) [#7](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/7) ] -- **itam**: [4d5f229f](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/4d5f229fc737606bf272b0ccea6390ad2737dae1) - Ability to add notes to operating systems [ [!8](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/8) [#7](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/7) ] -- **itam**: [725e6b8c](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/725e6b8c922e0c1a6fd9d96a0f2581a8aad4a737) - Ability to add notes on devices [ [!8](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/8) [#7](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/7) ] -- **core**: [8e0df948](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/8e0df948d5006976981eb2ac3918c38fe74aff14) - notes model added to core [ [!8](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/8) [#7](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/7) ] -- **device**: [fb041f77](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/fb041f77ebb9b9b44b0ebbed955252e22d3ee4dc) - Record inventory date and show as part of details [ [!8](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/8) [#2](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/2) ] -- **ui**: [e93ce07d](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/e93ce07d887b3d3151cb6714df27fd5cf9fdcd67) - Show inventory details if they exist [ [!8](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/8) [#2](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/2) ] -- **api**: [c52fd080](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/c52fd0802ed2395fa3a62ec41ec0297b0bca373e) - API accept computer inventory [ [!8](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/8) [#2](https://gitlab.com/nofusscomputing/projects/django_template/-/issues/2) ] +- **access**: add to models a get_organization function +- **access**: remove change view +- **itam**: relocation item delete from list to inside device +- **context_processor**: relocate as base +- **itam**: software index does not require created and modified date +- **organizations**: set org field to null if not set +- **itam**: move software categories to settings app +- **itam**: move device types to settings app +- **template**: content_title can be rendered in base ## 0.2.0 (2024-05-18) -### Bug Fixes +### Feat -- **device**: [9e801fa9](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/9e801fa9eb0244d413d1555bff8e206b2ff6acd7) - correct software link [ [!5](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/5) ] +- **itam**: Add Operating System to ITAM models +- **api**: force content type to be JSON for req/resp +- **software**: view software +- **device**: Prevent devices from being set global +- **software**: if no installations found, denote +- **device**: configurable software version +- **software_version**: name does not need to be unique +- **software_version**: set is_global to match software +- **software**: prettify device software action +- **software**: ability to add software versions +- **base**: add stylised action button/text +- **software**: add pagination for index +- **device**: add pagination for index -### Continious Integration +### Fix -- [ce18edaa](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/ce18edaa398bfca5f38ae9320a6a98d6a6338318) - correct junit collection to use wildcard name [ [!6](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/6) ] -- [8b746bb9](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/8b746bb9ff607950a73850d3cb0432f3d5538c63) - correct junit report name [ [!5](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/5) ] - -### Documentaton / Guides - -- [fa97286d](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/fa97286dc885dacbf2e56bab02cb42c67c70f9ab) - start to document features [ [!6](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/6) ] -- [7d007f72](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/7d007f721af5e3a192c9a713069bec8c7a602d12) - update [ [!5](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/5) ] - -### Features - -- **itam**: [a0b5a08f](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/a0b5a08f0d27f8676998eaf818c449961ccc42dd) - Add Operating System to ITAM models [ [!6](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/6) ] -- **api**: [377c78d6](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/377c78d6b84398e2bbae01a91478a8ab8f94a0a2) - force content type to be JSON for req/resp [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **software**: [95405283](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/95405283b98ec6b39faedd509619dcdc39b82fc0) - view software [ [!6](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/6) ] -- **device**: [aade1e80](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/aade1e80d7d0b5bf5d45c7fe202a360d325bc396) - Prevent devices from being set global [ [!5](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/5) ] -- **software**: [0e69a0ac](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/0e69a0accc32ea1513394da38e78066b0e09a5ed) - if no installations found, denote [ [!5](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/5) ] -- **device**: [b811eedb](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/b811eedb338712e1e8ddfba3b032dbdd3513dda5) - configurable software version [ [!5](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/5) ] -- **software_version**: [b0e69ee6](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/b0e69ee64b929466a41d69b523641e17928188e7) - name does not need to be unique [ [!5](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/5) ] -- **software_version**: [b1c4e570](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/b1c4e570cfb92ce6c72bd6df28f4c9d6d9eb30e6) - set is_global to match software [ [!5](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/5) ] -- **software**: [b2e1a460](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/b2e1a460c853f57397c615707575f9b87b174e9c) - prettify device software action [ [!5](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/5) ] -- **software**: [7f35292f](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/7f35292f64656830208b097388516b13e8b91613) - ability to add software versions [ [!5](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/5) ] -- **base**: [7302f997](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/7302f997530c9caba8e534877eba65dfa3659f9c) - add stylised action button/text [ [!5](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/5) ] -- **software**: [6f6031fb](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/6f6031fb1eb789e86afb7c9cbb8c12e7f1563f56) - add pagination for index [ [!5](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/5) ] -- **device**: [789b4a55](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/789b4a55d657c6c7a23af4c5d499b2be0a20481b) - add pagination for index [ [!5](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/5) ] +- **device**: correct software link ## 0.1.0 (2024-05-17) -### Bug Fixes +### Feat -- **itam**: [d3cafe08](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/d3cafe08aa7e817d5511d8f455cbd8efe5294be2) - device software to come from device org or global not users orgs [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **access**: [5a3450f3](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/5a3450f3c0f84fc32781338ef0c644356072366e) - correct team required permissions [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **fields**: [2fe15778](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/2fe15778cb638eb420e5f3312ca24e33bfc601c5) - correct autoslug field so it works [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **docker**: [69aec7ba](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/69aec7ba6a5ea43fb3ec7f359744e30ac4a945ed) - build wheels then install [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] +- **api**: initial token authentication implementation +- **docker**: add settings to store data in separate volume +- **django**: add split settings for specifying additional settings paths +- **api**: Add device config to device +- **itam**: add organization to device installs +- **itam**: migrate app from own repo +- Enable API by default +- **admin**: remove team management +- **admin**: remove group management +- **access**: adjustable team permissions +- **api**: initial work on API +- **template**: add header content icon block +- **tenancy**: Add is_ global field +- **access**: when modifying a team ad/remove user from linked group +- **auth**: include python social auth django application +- Build docker container for release +- **access**: add permissions to team and user +- **style**: format check boxes +- **access**: delete team user form +- **view**: new user +- user who is 'is_superuser' to view everything and not be denied access +- **access**: add org mixin to current views +- **access**: add views for each action for teams +- **access**: add mixin to check organization permissions against user and object +- **account**: show admin site link if user is staff +- **development**: added the debug django app +- **access**: rename structure to access and remove organization app in favour of own implementation +- **account**: Add user password change form +- **urls**: provide option to exclude navigation items +- **structure**: unregister admin pages from organization app not required +- **auth**: Custom Login Page +- **auth**: Add User Account Menu +- **auth**: Setup Login required +- Dyno-magic build navigation from application urls.py +- **structure**: Select and View an individual Organization +- **structure**: View Organizations +- **app**: Add new app structure for organizations and teams +- **template**: add base template +- **django**: add organizations app -### Code Refactor +### Fix -- [761afb6f](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/761afb6f2bc592f29870a8eaac86c70a32086af3) - button to use same selection colour [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **access**: [30e7c8de](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/30e7c8de42eafeacba02c221ad855ca0fb68f50d) - remove inline form for org teams [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- [0edfba60](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/0edfba604aba7f7810dbb038b836770b888f9d15) - rename app from itsm -> app [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **access**: [86046d6e](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/86046d6e923a32145869c1cb6cc0661eec9bd1d6) - dont use inline formset [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **views**: [c7986328](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/c7986328f7c36ae6a817c4e7321d41daaa9423bd) - move views to own directory [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **access**: [c9f147d8](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/c9f147d805d7d2e94cb9177b61fcb608efd5deb8) - addjust org and teams to use different view per action [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] +- **itam**: device software to come from device org or global not users orgs +- **access**: correct team required permissions +- **fields**: correct autoslug field so it works +- **docker**: build wheels then install -### Continious Integration +### Refactor -- [de83d749](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/de83d7490b9e6118beaf2eec303d38ac49332d16) - sync project to github [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- [8e2542f9](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/8e2542f9a50e64a2bc966d85a96d561cc1de8e67) - correct test path [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **coverage**: [eb9eeff4](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/eb9eeff4ed63e09a4670c3b3ac07f98f2575694b) - add test coverage to ci [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] - -### Documentaton / Guides - -- [f59ffa58](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/f59ffa581c5711fcb414aa4aa3ae9a3695ce4786) - add base itam pages [ [!2](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/2) ] -- [c43f41d9](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/c43f41d9587e060ea7f3c4e51a72f5c928c9384b) - notate global object [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- [db5d7e18](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/db5d7e18ad77c7402a8d73495ddaa9bbe626754b) - update and include permissions [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] - -### Features - -- **api**: [962ae2b8](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/962ae2b8dfaf7cccdfd449e2a7db087f9b3542c9) - initial token authentication implementation [ [!3](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/3) ] -- **docker**: [4b77e2e6](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/4b77e2e63dcc57534e821386584c3b6896d44173) - add settings to store data in separate volume [ [!2](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/2) ] -- **django**: [a96fc062](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/a96fc062f209e86bf4a8f40dd4a738ad8d889cf2) - add split settings for specifying additional settings paths [ [!2](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/2) ] -- **api**: [0c38155c](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/0c38155c4453d89c552eaf16aaf7d7e2092b2431) - Add device config to device [ [!2](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/2) ] -- **itam**: [2d67f93d](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/2d67f93d882d1ebe7782d9425c915e31f3a16453) - add organization to device installs [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **itam**: [195bb5e4](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/195bb5e4ab29540647cf30d22fcbb6e6c06e6db6) - migrate app from own repo [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- [f98e3bc9](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/f98e3bc9c2ff5f3627dc2f49df6eb7e6afdc974c) - Enable API by default [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **admin**: [4b214d0b](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/4b214d0b8cc10f43c708ef45ce5e20225f9b6c21) - remove team management [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **admin**: [736d3930](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/736d3930dff9705c3d27f853ed9a5f0000108164) - remove group management [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **access**: [50371267](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/50371267c1fb02e066d9a4ac066f54128ce957ea) - adjustable team permissions [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **api**: [102aa981](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/102aa981ce0a72fa263016139076e87778255226) - initial work on API [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **template**: [50cc050a](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/50cc050adf4cfcf43303350850caa56bf649874b) - add header content icon block [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **tenancy**: [857aa7af](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/857aa7af72f9e92be04d9cc258fc5875e4223ffd) - Add is_ global field [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **access**: [070ba47d](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/070ba47de284d912fc86aabb323a1639e4328d4a) - when modifying a team ad/remove user from linked group [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **auth**: [a0f4940a](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/a0f4940a09fb00486ed8280eb17ec35811839947) - include python social auth django application [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- [b3b12638](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/b3b12638ad85fe1b3744561a2220d255cf9e105c) - Build docker container for release [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **access**: [ca68c258](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/ca68c2589a8cabdc11fbe7e95b0a5d58f5fd8a0e) - add permissions to team and user [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **style**: [9d507d82](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/9d507d82df745a057f7903d76bf439b142e71494) - format check boxes [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **access**: [7445d880](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/7445d8807ce7e995fdb2f7443e59b407e1cf92dd) - delete team user form [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **view**: [fa5703cb](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/fa5703cb794b010d46dfdce1bd03243ca260cde1) - new user [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- [8a62c3f6](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/8a62c3f6ee061add16ae165857735a44cb0bb085) - user who is 'is_superuser' to view everything and not be denied access [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **access**: [af858dcc](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/af858dcc43c414f6c523a757345537149eb4178e) - add org mixin to current views [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **access**: [2b5047db](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/2b5047db2db18bfb10ccaadbf9adce12802b9c11) - add views for each action for teams [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **access**: [d715038a](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/d715038a884cad87fba0a55f0d30ea66fda322b0) - add mixin to check organization permissions against user and object [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **account**: [0446d391](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/0446d39190406fb54baf85bc031708a03473e020) - show admin site link if user is staff [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **development**: [c0212178](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/c0212178111f000c20b0b60426d3542dd704e8ce) - added the debug django app [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **access**: [af5175c4](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/af5175c4e198f5431d3f7b0d5b94f78818366053) - rename structure to access and remove organization app in favour of own implementation [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **account**: [f7bbb122](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/f7bbb122e6651635a4cb8e74246a8e155be6dcd1) - Add user password change form [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **urls**: [789777a2](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/789777a270bbd46e0a3126026e7236222f38da35) - provide option to exclude navigation items [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **structure**: [dae7f3c4](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/dae7f3c47a3c511ac30decb647461902e0dc248f) - unregister admin pages from organization app not required [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **auth**: [96a99c9d](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/96a99c9df181367498e3f8d8031a0c1e4304a312) - Custom Login Page [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **auth**: [65bd32df](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/65bd32dfad3d7f8f867aea84dd4ad31133aa8fc1) - Add User Account Menu [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **auth**: [283ef9a7](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/283ef9a7145d424bca2c898935e08cdc83038fff) - Setup Login required [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- [71bcd192](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/71bcd192b3e9d6616ff8dca5d8b5745ad371de92) - Dyno-magic build navigation from application urls.py [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **structure**: [7cdfdab1](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/7cdfdab1fc966f3c92e69baf1162033c7d2e9bc4) - Select and View an individual Organization [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **structure**: [dd54eae8](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/dd54eae8d747ab0a17e71797d675a45eeaaae813) - View Organizations [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **app**: [9092445d](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/9092445d0bcbe3215f675eb5e4794cfefb710913) - Add new app structure for organizations and teams [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **template**: [1a886184](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/1a8861846bb16255c204729438057e42b3c81d7a) - add base template [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] -- **django**: [81b170ca](https://gitlab.com/nofusscomputing/projects/django_template/-/commit/81b170cabf2398304861fa5b68fadb962630d4cb) - add organizations app [ [!1](https://gitlab.com/nofusscomputing/projects/django_template/-/merge_requests/1) ] +- button to use same selection colour +- **access**: remove inline form for org teams +- rename app from itsm -> app +- **access**: dont use inline formset +- **views**: move views to own directory +- **access**: addjust org and teams to use different view per action ## 0.0.1 (2024-05-06) From 83328be22e779800865d5e611485fa39ad0b697a Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 9 Aug 2024 21:39:53 +0930 Subject: [PATCH 083/123] ci: fix publish registry #209 --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index dc4f52d7..f7716cdb 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -19,7 +19,7 @@ jobs: uses: nofusscomputing/action_docker/.github/workflows/docker.yaml@development with: DOCKER_BUILD_IMAGE_NAME: "nofusscomputing/centurion-erp" - DOCKER_PUBLISH_REGISTRY: "ghcr.io" + DOCKER_PUBLISH_REGISTRY: "docker.io" DOCKER_PUBLISH_IMAGE_NAME: "nofusscomputing/centurion-erp" secrets: DOCKER_PUBLISH_USERNAME: ${{ secrets.NFC_DOCKERHUB_TOKEN }} From a6c0785de0b82be9a3f0a77caa1b95e5efa27a90 Mon Sep 17 00:00:00 2001 From: nfc-bot Date: Fri, 9 Aug 2024 12:14:12 +0000 Subject: [PATCH 084/123] build: bump version 1.0.0-b6 -> 1.0.0-b7 --- .cz.yaml | 2 +- CHANGELOG.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.cz.yaml b/.cz.yaml index 64868c6c..a43fe66b 100644 --- a/.cz.yaml +++ b/.cz.yaml @@ -4,5 +4,5 @@ commitizen: prerelease_offset: 1 tag_format: $version update_changelog_on_bump: false - version: 1.0.0-b6 + version: 1.0.0-b7 version_scheme: semver diff --git a/CHANGELOG.md b/CHANGELOG.md index 272dc4bb..6a729a60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.0.0-b7 (2024-08-09) + ## 1.0.0-b6 (2024-08-09) ## 1.0.0-b5 (2024-07-31) From 27e73e21d1aa03a62ff5abab0eb9f13768dd6941 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 9 Aug 2024 21:55:28 +0930 Subject: [PATCH 085/123] ci: add org to docker publish registry #209 --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f7716cdb..14cb647a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -19,7 +19,7 @@ jobs: uses: nofusscomputing/action_docker/.github/workflows/docker.yaml@development with: DOCKER_BUILD_IMAGE_NAME: "nofusscomputing/centurion-erp" - DOCKER_PUBLISH_REGISTRY: "docker.io" + DOCKER_PUBLISH_REGISTRY: "docker.io/nofusscomputing" DOCKER_PUBLISH_IMAGE_NAME: "nofusscomputing/centurion-erp" secrets: DOCKER_PUBLISH_USERNAME: ${{ secrets.NFC_DOCKERHUB_TOKEN }} From 40ba645a352a6dcb458143884936c1dec9141944 Mon Sep 17 00:00:00 2001 From: nfc-bot Date: Fri, 9 Aug 2024 12:26:45 +0000 Subject: [PATCH 086/123] build: bump version 1.0.0-b7 -> 1.0.0-b8 --- .cz.yaml | 2 +- CHANGELOG.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.cz.yaml b/.cz.yaml index a43fe66b..1cdf356c 100644 --- a/.cz.yaml +++ b/.cz.yaml @@ -4,5 +4,5 @@ commitizen: prerelease_offset: 1 tag_format: $version update_changelog_on_bump: false - version: 1.0.0-b7 + version: 1.0.0-b8 version_scheme: semver diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a729a60..478eaaa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.0.0-b8 (2024-08-09) + ## 1.0.0-b7 (2024-08-09) ## 1.0.0-b6 (2024-08-09) From 4e11ad67d05cb017db39d182bb72b9c10bb10c05 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 9 Aug 2024 22:10:23 +0930 Subject: [PATCH 087/123] ci: use docker.io as publish registry #209 --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 14cb647a..f7716cdb 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -19,7 +19,7 @@ jobs: uses: nofusscomputing/action_docker/.github/workflows/docker.yaml@development with: DOCKER_BUILD_IMAGE_NAME: "nofusscomputing/centurion-erp" - DOCKER_PUBLISH_REGISTRY: "docker.io/nofusscomputing" + DOCKER_PUBLISH_REGISTRY: "docker.io" DOCKER_PUBLISH_IMAGE_NAME: "nofusscomputing/centurion-erp" secrets: DOCKER_PUBLISH_USERNAME: ${{ secrets.NFC_DOCKERHUB_TOKEN }} From f437eeccb88d3d6a8a347fa864111c160ab1bdfe Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 9 Aug 2024 22:11:24 +0930 Subject: [PATCH 088/123] ci: use full docker.io as publish image name #209 --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f7716cdb..949876c7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,7 +20,7 @@ jobs: with: DOCKER_BUILD_IMAGE_NAME: "nofusscomputing/centurion-erp" DOCKER_PUBLISH_REGISTRY: "docker.io" - DOCKER_PUBLISH_IMAGE_NAME: "nofusscomputing/centurion-erp" + DOCKER_PUBLISH_IMAGE_NAME: "docker.io/nofusscomputing/centurion-erp" secrets: DOCKER_PUBLISH_USERNAME: ${{ secrets.NFC_DOCKERHUB_TOKEN }} DOCKER_PUBLISH_PASSWORD: ${{ secrets.NFC_DOCKERHUB_USERNAME }} From 57bc972b0fa59c0c8fe1564ac20e3c1a9bac42f3 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 9 Aug 2024 22:11:47 +0930 Subject: [PATCH 089/123] ci: use correct docker.io credentials #209 --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 949876c7..6e614a82 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,8 +22,8 @@ jobs: DOCKER_PUBLISH_REGISTRY: "docker.io" DOCKER_PUBLISH_IMAGE_NAME: "docker.io/nofusscomputing/centurion-erp" secrets: - DOCKER_PUBLISH_USERNAME: ${{ secrets.NFC_DOCKERHUB_TOKEN }} - DOCKER_PUBLISH_PASSWORD: ${{ secrets.NFC_DOCKERHUB_USERNAME }} + DOCKER_PUBLISH_USERNAME: ${{ secrets.NFC_DOCKERHUB_USERNAME }} + DOCKER_PUBLISH_PASSWORD: ${{ secrets.NFC_DOCKERHUB_TOKEN }} python: From 33687791eccb388d0984951bbac3f92ece27e80e Mon Sep 17 00:00:00 2001 From: nfc-bot Date: Fri, 9 Aug 2024 12:43:24 +0000 Subject: [PATCH 090/123] build: bump version 1.0.0-b8 -> 1.0.0-b9 --- .cz.yaml | 2 +- CHANGELOG.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.cz.yaml b/.cz.yaml index 1cdf356c..97f208cc 100644 --- a/.cz.yaml +++ b/.cz.yaml @@ -4,5 +4,5 @@ commitizen: prerelease_offset: 1 tag_format: $version update_changelog_on_bump: false - version: 1.0.0-b8 + version: 1.0.0-b9 version_scheme: semver diff --git a/CHANGELOG.md b/CHANGELOG.md index 478eaaa7..77f2a687 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.0.0-b9 (2024-08-09) + ## 1.0.0-b8 (2024-08-09) ## 1.0.0-b7 (2024-08-09) From bfe9a950381580ab622b9059c0f1f6f00adb39b0 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 9 Aug 2024 22:22:52 +0930 Subject: [PATCH 091/123] ci: remove docker.io from image publish name #209 --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6e614a82..6d07c129 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,7 +20,7 @@ jobs: with: DOCKER_BUILD_IMAGE_NAME: "nofusscomputing/centurion-erp" DOCKER_PUBLISH_REGISTRY: "docker.io" - DOCKER_PUBLISH_IMAGE_NAME: "docker.io/nofusscomputing/centurion-erp" + DOCKER_PUBLISH_IMAGE_NAME: "nofusscomputing/centurion-erp" secrets: DOCKER_PUBLISH_USERNAME: ${{ secrets.NFC_DOCKERHUB_USERNAME }} DOCKER_PUBLISH_PASSWORD: ${{ secrets.NFC_DOCKERHUB_TOKEN }} From 8e6fd58107c3aceb484537023c92ddf2c995fc0b Mon Sep 17 00:00:00 2001 From: nfc-bot Date: Fri, 9 Aug 2024 12:53:39 +0000 Subject: [PATCH 092/123] build: bump version 1.0.0-b9 -> 1.0.0-b10 --- .cz.yaml | 2 +- CHANGELOG.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.cz.yaml b/.cz.yaml index 97f208cc..8a1c3998 100644 --- a/.cz.yaml +++ b/.cz.yaml @@ -4,5 +4,5 @@ commitizen: prerelease_offset: 1 tag_format: $version update_changelog_on_bump: false - version: 1.0.0-b9 + version: 1.0.0-b10 version_scheme: semver diff --git a/CHANGELOG.md b/CHANGELOG.md index 77f2a687..797c103c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.0.0-b10 (2024-08-09) + ## 1.0.0-b9 (2024-08-09) ## 1.0.0-b8 (2024-08-09) From 04dc00d79d25ed88392cd8ed26ba4297d89c116b Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 10 Aug 2024 13:35:37 +0930 Subject: [PATCH 093/123] ci(gitlab): fix includes https://github.com/nofusscomputing/centurion_erp/issues/214 --- .gitlab-ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2600e235..03df28a0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -32,9 +32,9 @@ include: file: - .gitlab-ci_common.yaml # - template/automagic.gitlab-ci.yaml - - local: gitlab-ci/automation/.gitlab-ci-ansible.yaml - - local: gitlab-ci/template/mkdocs-documentation.gitlab-ci.yaml - - local: gitlab-ci/lint/ansible.gitlab-ci.yaml + - automation/.gitlab-ci-ansible.yaml + - template/mkdocs-documentation.gitlab-ci.yaml + - lint/ansible.gitlab-ci.yaml From 43b7e413a6aa6184b9f9fb114930c7a2d4a2d0b6 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 10 Aug 2024 13:40:06 +0930 Subject: [PATCH 094/123] ci(project): add issue/pr project triage https://github.com/nofusscomputing/action_project/pull/1 https://github.com/nofusscomputing/centurion_erp/issues/214 #217 --- .github/workflows/triage.yaml | 37 +++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/triage.yaml diff --git a/.github/workflows/triage.yaml b/.github/workflows/triage.yaml new file mode 100644 index 00000000..1896deea --- /dev/null +++ b/.github/workflows/triage.yaml @@ -0,0 +1,37 @@ + +--- + +name: Triage + + +on: + issues: + types: + - opened + - reopened + - transferred + - milestoned + - demilestoned + - closed + - assigned + pull_request: + types: + - opened + - edited + - assigned + - reopened + - closed + + + +jobs: + + + project: + name: Project + uses: nofusscomputing/action_project/.github/workflows/project.yaml@development + with: + PROJECT_URL: https://github.com/orgs/nofusscomputing/projects/3 + secrets: + WORKFLOW_TOKEN: ${{ secrets.WORKFLOW_TOKEN }} + From f0ae185fc55ec88795bcc3af6e3cfcaad8cee0d4 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 10 Aug 2024 14:02:27 +0930 Subject: [PATCH 095/123] docs(readme): fix version badges #217 #214 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6c9cd4c1..7b5d6bb2 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ This project is hosted on [Github](https://github.com/NofussComputing/centurion_ **Stable Branch** -![GitHub branch status](https://img.shields.io/github/check-runs/nofusscomputing/centurion_erp/master?style=plastic&logo=github&label=Build&color=000) ![GitHub Release](https://img.shields.io/github/v/release/nofusscomputing/centurion_erp?sort=semver&display_name=release&style=plastic&logo=github&label=Build&color=000) ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fmaster%2Fbadge_endpoint_coverage.json&style=plastic) +![GitHub branch status](https://img.shields.io/github/check-runs/nofusscomputing/centurion_erp/master?style=plastic&logo=github&label=Build&color=000) ![GitHub Release](https://img.shields.io/github/v/release/nofusscomputing/centurion_erp?sort=date&style=plastic&logo=github&label=Release&color=000) ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fmaster%2Fbadge_endpoint_coverage.json&style=plastic) ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fmaster%2Fbadge_endpoint_unit_test.json) @@ -42,7 +42,7 @@ This project is hosted on [Github](https://github.com/NofussComputing/centurion_ -![GitHub branch status](https://img.shields.io/github/check-runs/nofusscomputing/centurion_erp/development?style=plastic&logo=github&label=Build&color=000) ![GitHub Release](https://img.shields.io/github/v/release/nofusscomputing/centurion_erp?include_prereleases&sort=semver&display_name=release&style=plastic&logo=github&label=Build&color=000) ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fdevelopment%2Fbadge_endpoint_coverage.json&style=plastic) +![GitHub branch status](https://img.shields.io/github/check-runs/nofusscomputing/centurion_erp/development?style=plastic&logo=github&label=Build&color=000) ![GitHub Release](https://img.shields.io/github/v/release/nofusscomputing/centurion_erp?include_prereleases&sort=date&style=plastic&logo=github&label=Release&color=000) ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fdevelopment%2Fbadge_endpoint_coverage.json&style=plastic) ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fdevelopment%2Fbadge_endpoint_unit_test.json) From 5e8bebbeb1c0bfdb57b76ff61f5c2b6b034c3e06 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 10 Aug 2024 17:22:07 +0930 Subject: [PATCH 096/123] build(python): update installed packages as part of the build #209 --- dockerfile | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/dockerfile b/dockerfile index f9785ce0..45d9d6b1 100644 --- a/dockerfile +++ b/dockerfile @@ -5,6 +5,11 @@ ARG CI_COMMIT_TAG='' FROM python:3.11-alpine3.19 as build +RUN pip --disable-pip-version-check list --outdated --format=json | \ + python -c "import json, sys; print('\n'.join([x['name'] for x in json.load(sys.stdin)]))" | \ + xargs -n1 pip install --no-cache-dir -U; + + RUN apk add --update \ bash \ git \ @@ -83,7 +88,10 @@ COPY --from=build /tmp/python_builds /tmp/python_builds COPY includes/ / -RUN apk update --no-cache; \ +RUN pip --disable-pip-version-check list --outdated --format=json | \ + python -c "import json, sys; print('\n'.join([x['name'] for x in json.load(sys.stdin)]))" | \ + xargs -n1 pip install --no-cache-dir -U; \ + apk update --no-cache; \ apk add --no-cache \ mariadb-client \ mariadb-dev \ From 84d4f48c63c7aae1d4b11b000e7b32f1d5c691fb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Aug 2024 05:03:00 +0000 Subject: [PATCH 097/123] chore(deps): bump django from 5.0.7 to 5.0.8 Bumps [django](https://github.com/django/django) from 5.0.7 to 5.0.8. - [Commits](https://github.com/django/django/compare/5.0.7...5.0.8) --- updated-dependencies: - dependency-name: django dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index be85e8e2..89b35d00 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Django==5.0.7 +Django==5.0.8 django-debug-toolbar==4.3.0 social-auth-app-django==5.4.1 From 3ba6bb5b4b1b175dcffa268cb81bd285c53aecd9 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 10 Aug 2024 17:35:53 +0930 Subject: [PATCH 098/123] docs(readme): correct build badge #209 #214 --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7b5d6bb2..ef2e247d 100644 --- a/README.md +++ b/README.md @@ -32,17 +32,18 @@ This project is hosted on [Github](https://github.com/NofussComputing/centurion_ **Stable Branch** -![GitHub branch status](https://img.shields.io/github/check-runs/nofusscomputing/centurion_erp/master?style=plastic&logo=github&label=Build&color=000) ![GitHub Release](https://img.shields.io/github/v/release/nofusscomputing/centurion_erp?sort=date&style=plastic&logo=github&label=Release&color=000) ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fmaster%2Fbadge_endpoint_coverage.json&style=plastic) +![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/nofusscomputing/centurion_erp/ci.yaml?branch=master&style=plastic&logo=github&label=Build&color=%23000) ![GitHub Release](https://img.shields.io/github/v/release/nofusscomputing/centurion_erp?sort=date&style=plastic&logo=github&label=Release&color=000) ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fmaster%2Fbadge_endpoint_coverage.json&style=plastic) ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fmaster%2Fbadge_endpoint_unit_test.json) + ---- **Development Branch** -![GitHub branch status](https://img.shields.io/github/check-runs/nofusscomputing/centurion_erp/development?style=plastic&logo=github&label=Build&color=000) ![GitHub Release](https://img.shields.io/github/v/release/nofusscomputing/centurion_erp?include_prereleases&sort=date&style=plastic&logo=github&label=Release&color=000) ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fdevelopment%2Fbadge_endpoint_coverage.json&style=plastic) +![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/nofusscomputing/centurion_erp/ci.yaml?branch=development&style=plastic&logo=github&label=Build&color=%23000) ![GitHub Release](https://img.shields.io/github/v/release/nofusscomputing/centurion_erp?include_prereleases&sort=date&style=plastic&logo=github&label=Release&color=000) ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fdevelopment%2Fbadge_endpoint_coverage.json&style=plastic) ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fdevelopment%2Fbadge_endpoint_unit_test.json) From cde25620481c62c2ca28835c76d32a75f41e80b6 Mon Sep 17 00:00:00 2001 From: nfc-bot Date: Sat, 10 Aug 2024 08:30:02 +0000 Subject: [PATCH 099/123] build: bump version 1.0.0-b10 -> 1.0.0-b11 --- .cz.yaml | 2 +- CHANGELOG.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.cz.yaml b/.cz.yaml index 8a1c3998..db04183a 100644 --- a/.cz.yaml +++ b/.cz.yaml @@ -4,5 +4,5 @@ commitizen: prerelease_offset: 1 tag_format: $version update_changelog_on_bump: false - version: 1.0.0-b10 + version: 1.0.0-b11 version_scheme: semver diff --git a/CHANGELOG.md b/CHANGELOG.md index 797c103c..82d50bab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.0.0-b11 (2024-08-10) + ## 1.0.0-b10 (2024-08-09) ## 1.0.0-b9 (2024-08-09) From 262e4318340bce7f94b40e5e42c38f2161a817f0 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 10 Aug 2024 19:12:26 +0930 Subject: [PATCH 100/123] test(organization): api field checks . #162 --- .../organization/test_organizaiton_api.py | 371 ++++++++++++++++++ 1 file changed, 371 insertions(+) create mode 100644 app/access/tests/unit/organization/test_organizaiton_api.py diff --git a/app/access/tests/unit/organization/test_organizaiton_api.py b/app/access/tests/unit/organization/test_organizaiton_api.py new file mode 100644 index 00000000..904ba2bc --- /dev/null +++ b/app/access/tests/unit/organization/test_organizaiton_api.py @@ -0,0 +1,371 @@ +import pytest +import unittest + +from django.contrib.auth import get_user_model +from django.contrib.auth.models import AnonymousUser, User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + + + +class OrganizationAPI(TestCase): + + model = Organization + + app_namespace = 'API' + + url_name = '_api_organization' + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a device + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + self.item = organization + + self.url_view_kwargs = {'pk': self.item.id} + + self.url_kwargs = {'pk': self.item.id} + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + def test_api_field_exists_id(self): + """ Test for existance of API Field + + id field must exist + """ + + assert 'id' in self.api_data + + + def test_api_field_type_id(self): + """ Test for type for API Field + + id field must be int + """ + + assert type(self.api_data['id']) is int + + + def test_api_field_exists_name(self): + """ Test for existance of API Field + + name field must exist + """ + + assert 'name' in self.api_data + + + def test_api_field_type_name(self): + """ Test for type for API Field + + name field must be str + """ + + assert type(self.api_data['name']) is str + + + def test_api_field_exists_teams(self): + """ Test for existance of API Field + + teams field must exist + """ + + assert 'teams' in self.api_data + + + def test_api_field_type_teams(self): + """ Test for type for API Field + + teams field must be list + """ + + assert type(self.api_data['teams']) is list + + + def test_api_field_exists_url(self): + """ Test for existance of API Field + + url field must exist + """ + + assert 'url' in self.api_data + + + def test_api_field_type_url(self): + """ Test for type for API Field + + url field must be str + """ + + assert type(self.api_data['url']) is Hyperlink + + + + + def test_api_field_exists_teams_id(self): + """ Test for existance of API Field + + teams.id field must exist + """ + + assert 'id' in self.api_data['teams'][0] + + + def test_api_field_type_teams_id(self): + """ Test for type for API Field + + teams.id field must be int + """ + + assert type(self.api_data['teams'][0]['id']) is int + + + def test_api_field_exists_teams_team_name(self): + """ Test for existance of API Field + + teams.team_name field must exist + """ + + assert 'team_name' in self.api_data['teams'][0] + + + def test_api_field_type_teams_team_name(self): + """ Test for type for API Field + + teams.team_name field must be string + """ + + assert type(self.api_data['teams'][0]['team_name']) is str + + + def test_api_field_exists_teams_permissions(self): + """ Test for existance of API Field + + teams.permissions field must exist + """ + + assert 'permissions' in self.api_data['teams'][0] + + + def test_api_field_type_teams_permissions(self): + """ Test for type for API Field + + teams.permissions field must be list + """ + + assert type(self.api_data['teams'][0]['permissions']) is list + + + def test_api_field_exists_teams_permissions_url(self): + """ Test for existance of API Field + + teams.permissions_url field must exist + """ + + assert 'permissions_url' in self.api_data['teams'][0] + + + def test_api_field_type_teams_permissions_url(self): + """ Test for type for API Field + + teams.permissions_url field must be url + """ + + assert type(self.api_data['teams'][0]['permissions_url']) is str + + + def test_api_field_exists_teams_url(self): + """ Test for existance of API Field + + teams.url field must exist + """ + + assert 'url' in self.api_data['teams'][0] + + + def test_api_field_type_teams_url(self): + """ Test for type for API Field + + teams.url field must be url + """ + + assert type(self.api_data['teams'][0]['url']) is str + + + + def test_api_field_exists_teams_permissions_id(self): + """ Test for existance of API Field + + teams.permissions.id field must exist + """ + + assert 'id' in self.api_data['teams'][0]['permissions'][0] + + + def test_api_field_type_teams_permissions_id(self): + """ Test for type for API Field + + teams.permissions.id field must be int + """ + + assert type(self.api_data['teams'][0]['permissions'][0]['id']) is int + + + def test_api_field_exists_teams_permissions_name(self): + """ Test for existance of API Field + + teams.permissions.name field must exist + """ + + assert 'name' in self.api_data['teams'][0]['permissions'][0] + + + def test_api_field_type_teams_permissions_name(self): + """ Test for type for API Field + + teams.permissions.name field must be str + """ + + assert type(self.api_data['teams'][0]['permissions'][0]['name']) is str + + + def test_api_field_exists_teams_permissions_codename(self): + """ Test for existance of API Field + + teams.permissions.codename field must exist + """ + + assert 'codename' in self.api_data['teams'][0]['permissions'][0] + + + def test_api_field_type_teams_permissions_codename(self): + """ Test for type for API Field + + teams.permissions.codename field must be str + """ + + assert type(self.api_data['teams'][0]['permissions'][0]['codename']) is str + + + def test_api_field_exists_teams_permissions_content_type(self): + """ Test for existance of API Field + + teams.permissions.content_type field must exist + """ + + assert 'content_type' in self.api_data['teams'][0]['permissions'][0] + + + def test_api_field_type_teams_permissions_content_type(self): + """ Test for type for API Field + + teams.permissions.content_type field must be dict + """ + + assert type(self.api_data['teams'][0]['permissions'][0]['content_type']) is dict + + + + def test_api_field_exists_teams_permissions_content_type_id(self): + """ Test for existance of API Field + + teams.permissions.content_type.id field must exist + """ + + assert 'id' in self.api_data['teams'][0]['permissions'][0]['content_type'] + + + def test_api_field_type_teams_permissions_content_type_id(self): + """ Test for type for API Field + + teams.permissions.content_type.id field must be int + """ + + assert type(self.api_data['teams'][0]['permissions'][0]['content_type']['id']) is int + + + def test_api_field_exists_teams_permissions_content_type_app_label(self): + """ Test for existance of API Field + + teams.permissions.content_type.app_label field must exist + """ + + assert 'app_label' in self.api_data['teams'][0]['permissions'][0]['content_type'] + + + def test_api_field_type_teams_permissions_content_type_app_label(self): + """ Test for type for API Field + + teams.permissions.content_type.app_label field must be str + """ + + assert type(self.api_data['teams'][0]['permissions'][0]['content_type']['app_label']) is str + + + def test_api_field_exists_teams_permissions_content_type_model(self): + """ Test for existance of API Field + + teams.permissions.content_type.model field must exist + """ + + assert 'model' in self.api_data['teams'][0]['permissions'][0]['content_type'] + + + def test_api_field_type_teams_permissions_content_type_model(self): + """ Test for type for API Field + + teams.permissions.content_type.model field must be str + """ + + assert type(self.api_data['teams'][0]['permissions'][0]['content_type']['model']) is str From 84d21f4af8f148282b6439b1db1f5a903320fa0f Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 10 Aug 2024 19:58:04 +0930 Subject: [PATCH 101/123] test(teams): api field checks . #162 #218 --- app/access/tests/unit/team/test_team_api.py | 294 ++++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 app/access/tests/unit/team/test_team_api.py diff --git a/app/access/tests/unit/team/test_team_api.py b/app/access/tests/unit/team/test_team_api.py new file mode 100644 index 00000000..cb4b73be --- /dev/null +++ b/app/access/tests/unit/team/test_team_api.py @@ -0,0 +1,294 @@ +import pytest +import unittest +import requests + + +from django.contrib.auth import get_user_model +from django.contrib.auth.models import AnonymousUser, User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +# from api.tests.abstract.api_permissions import APIPermissions + + + +class TeamAPI(TestCase): + + model = Team + + app_namespace = 'API' + + url_name = '_api_team' + + # url_list = '_api_organization_teams' + + # change_data = {'name': 'device'} + + # delete_data = {'device': 'device'} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + self.item = self.model.objects.create( + organization=organization, + team_name = 'teamone' + ) + + + self.url_kwargs = {'organization_id': self.organization.id} + + self.url_view_kwargs = {'organization_id': self.organization.id, 'group_ptr_id': self.item.id} + + self.add_data = {'team_name': 'team_post'} + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + # view_team = Team.objects.create( + # team_name = 'view_team', + # organization = organization, + # ) + + self.item.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = self.item, + user = self.view_user + ) + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + def test_api_field_exists_id(self): + """ Test for existance of API Field + + id field must exist + """ + + assert 'id' in self.api_data + + + def test_api_field_type_id(self): + """ Test for type for API Field + + id field must be int + """ + + assert type(self.api_data['id']) is int + + + def test_api_field_exists_team_name(self): + """ Test for existance of API Field + + team_name field must exist + """ + + assert 'team_name' in self.api_data + + + def test_api_field_type_name(self): + """ Test for type for API Field + + team_name field must be str + """ + + assert type(self.api_data['team_name']) is str + + + def test_api_field_exists_url(self): + """ Test for existance of API Field + + url field must exist + """ + + assert 'url' in self.api_data + + + def test_api_field_type_url(self): + """ Test for type for API Field + + url field must be str + """ + + assert type(self.api_data['url']) is str + + + def test_api_field_exists_permissions(self): + """ Test for existance of API Field + + permissions field must exist + """ + + assert 'permissions' in self.api_data + + + def test_api_field_type_permissions(self): + """ Test for type for API Field + + url field must be list + """ + + assert type(self.api_data['permissions']) is list + + + + def test_api_field_exists_permissions_id(self): + """ Test for existance of API Field + + permissions.id field must exist + """ + + assert 'id' in self.api_data['permissions'][0] + + + def test_api_field_type_permissions_id(self): + """ Test for type for API Field + + permissions.id field must be int + """ + + assert type(self.api_data['permissions'][0]['id']) is int + + + def test_api_field_exists_permissions_name(self): + """ Test for existance of API Field + + permissions.name field must exist + """ + + assert 'name' in self.api_data['permissions'][0] + + + def test_api_field_type_permissions_name(self): + """ Test for type for API Field + + permissions.name field must be str + """ + + assert type(self.api_data['permissions'][0]['name']) is str + + + def test_api_field_exists_permissions_codename(self): + """ Test for existance of API Field + + permissions.codename field must exist + """ + + assert 'codename' in self.api_data['permissions'][0] + + + def test_api_field_type_permissions_codename(self): + """ Test for type for API Field + + permissions.codename field must be str + """ + + assert type(self.api_data['permissions'][0]['codename']) is str + + + def test_api_field_exists_permissions_content_type(self): + """ Test for existance of API Field + + permissions.content_type field must exist + """ + + assert 'content_type' in self.api_data['permissions'][0] + + + def test_api_field_type_permissions_content_type(self): + """ Test for type for API Field + + permissions.content_type field must be dict + """ + + assert type(self.api_data['permissions'][0]['content_type']) is dict + + + + def test_api_field_exists_permissions_content_type_id(self): + """ Test for existance of API Field + + permissions.content_type.id field must exist + """ + + assert 'id' in self.api_data['permissions'][0]['content_type'] + + + def test_api_field_type_permissions_content_type_id(self): + """ Test for type for API Field + + permissions.content_type.id field must be int + """ + + assert type(self.api_data['permissions'][0]['content_type']['id']) is int + + + def test_api_field_exists_permissions_content_type_app_label(self): + """ Test for existance of API Field + + permissions.content_type.app_label field must exist + """ + + assert 'app_label' in self.api_data['permissions'][0]['content_type'] + + + def test_api_field_type_permissions_content_type_app_label(self): + """ Test for type for API Field + + permissions.content_type.app_label field must be str + """ + + assert type(self.api_data['permissions'][0]['content_type']['app_label']) is str + + + def test_api_field_exists_permissions_content_type_model(self): + """ Test for existance of API Field + + permissions.content_type.model field must exist + """ + + assert 'model' in self.api_data['permissions'][0]['content_type'] + + + def test_api_field_type_permissions_content_type_model(self): + """ Test for type for API Field + + permissions.content_type.model field must be str + """ + + assert type(self.api_data['permissions'][0]['content_type']['model']) is str From c3b585d416a8f0094ac7f171a6ac1f6f68efaad8 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 10 Aug 2024 20:24:43 +0930 Subject: [PATCH 102/123] fix(base): correct project links to github . #218 --- app/templates/base.html.j2 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/templates/base.html.j2 b/app/templates/base.html.j2 index 9ed7c9ec..b86f81bd 100644 --- a/app/templates/base.html.j2 +++ b/app/templates/base.html.j2 @@ -133,7 +133,7 @@ section h2 span svg { {% include 'icons/swagger_docs.svg' %} - + {% include 'icons/git.svg' %} @@ -147,7 +147,7 @@ section h2 span svg { {% else %} development {% endif %} - ( {% if build_details.project_url %}{% endif %} + ( {% if build_details.project_url %}{% endif %} {{ build_details.sha }} {% if build_details.project_url %}{% endif %} ) From b5c31d81d338ef7060391d77ead383f70b5199e3 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 10 Aug 2024 20:28:53 +0930 Subject: [PATCH 103/123] fix(api): ensure org mixin is inherited by software view . #218 fixes #219 --- app/api/views/itam/software.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/api/views/itam/software.py b/app/api/views/itam/software.py index 2937cd4d..7a13e331 100644 --- a/app/api/views/itam/software.py +++ b/app/api/views/itam/software.py @@ -3,6 +3,8 @@ from django.shortcuts import get_object_or_404 from rest_framework import generics, viewsets +from access.mixin import OrganizationMixin + from api.serializers.itam.software import SoftwareSerializer from api.views.mixin import OrganizationPermissionAPI @@ -10,7 +12,7 @@ from itam.models.software import Software -class SoftwareViewSet(viewsets.ModelViewSet): +class SoftwareViewSet(OrganizationMixin, viewsets.ModelViewSet): permission_classes = [ OrganizationPermissionAPI From 40e3078a5838277d54a75c7211cd0700ec467108 Mon Sep 17 00:00:00 2001 From: nfc-bot Date: Sat, 10 Aug 2024 11:23:21 +0000 Subject: [PATCH 104/123] build: bump version 1.0.0-b11 -> 1.0.0-b12 --- .cz.yaml | 2 +- CHANGELOG.md | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.cz.yaml b/.cz.yaml index db04183a..4fca755e 100644 --- a/.cz.yaml +++ b/.cz.yaml @@ -4,5 +4,5 @@ commitizen: prerelease_offset: 1 tag_format: $version update_changelog_on_bump: false - version: 1.0.0-b11 + version: 1.0.0-b12 version_scheme: semver diff --git a/CHANGELOG.md b/CHANGELOG.md index 82d50bab..ef96df18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.0.0-b12 (2024-08-10) + +### Fix + +- **api**: ensure org mixin is inherited by software view +- **base**: correct project links to github + ## 1.0.0-b11 (2024-08-10) ## 1.0.0-b10 (2024-08-09) From eb6b03f731554a9769f6e0c5a4644102f7844bc3 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 11 Aug 2024 12:05:46 +0930 Subject: [PATCH 105/123] docs(development): added api field test note . #220 --- docs/projects/centurion_erp/development/testing.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/projects/centurion_erp/development/testing.md b/docs/projects/centurion_erp/development/testing.md index 9df63a41..0ef64bde 100644 --- a/docs/projects/centurion_erp/development/testing.md +++ b/docs/projects/centurion_erp/development/testing.md @@ -105,6 +105,10 @@ Items to test include, and are not limited to: _applicable if notes are able to be added to an item._ +- API Fields + + _Field exists, Type is checked_ + ## Running Tests From 3fe09fb8f9b18f82cd467537a65a2141b6428253 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 11 Aug 2024 12:27:57 +0930 Subject: [PATCH 106/123] test(software): api field checks . #162 #220 --- .../tests/unit/software/test_software_api.py | 294 ++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 app/itam/tests/unit/software/test_software_api.py diff --git a/app/itam/tests/unit/software/test_software_api.py b/app/itam/tests/unit/software/test_software_api.py new file mode 100644 index 00000000..03130466 --- /dev/null +++ b/app/itam/tests/unit/software/test_software_api.py @@ -0,0 +1,294 @@ +import pytest +import unittest +import requests + +from django.contrib.auth.models import AnonymousUser, User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from core.models.manufacturer import Manufacturer + +from itam.models.software import Software, SoftwareCategory + + +class SoftwareAPI(TestCase): + + + model = Software + + app_namespace = 'API' + + url_name = 'software-detail' + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a software + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + category = SoftwareCategory.objects.create( + name='a category' + ) + + publisher = Manufacturer.objects.create( + name='a manufacturer' + ) + + + self.item = self.model.objects.create( + organization=organization, + name = 'softwareone', + model_notes = 'random str', + category = category, + publisher = publisher + ) + + self.url_view_kwargs = {'pk': self.item.id} + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + def test_api_field_exists_id(self): + """ Test for existance of API Field + + id field must exist + """ + + assert 'id' in self.api_data + + + def test_api_field_type_id(self): + """ Test for type for API Field + + id field must be int + """ + + assert type(self.api_data['id']) is int + + + def test_api_field_exists_url(self): + """ Test for existance of API Field + + url field must exist + """ + + assert 'url' in self.api_data + + + def test_api_field_type_url(self): + """ Test for type for API Field + + url field must be str + """ + + assert type(self.api_data['url']) is Hyperlink + + + def test_api_field_exists_is_global(self): + """ Test for existance of API Field + + is_global field must exist + """ + + assert 'is_global' in self.api_data + + + def test_api_field_type_is_global(self): + """ Test for type for API Field + + is_global field must be boolean + """ + + assert type(self.api_data['is_global']) is bool + + + def test_api_field_exists_model_notes(self): + """ Test for existance of API Field + + model_notes field must exist + """ + + assert 'model_notes' in self.api_data + + + def test_api_field_type_model_notes(self): + """ Test for type for API Field + + model_notes field must be str + """ + + assert type(self.api_data['model_notes']) is str + + + def test_api_field_exists_name(self): + """ Test for existance of API Field + + name field must exist + """ + + assert 'name' in self.api_data + + + def test_api_field_type_name(self): + """ Test for type for API Field + + name field must be str + """ + + assert type(self.api_data['name']) is str + + + def test_api_field_exists_slug(self): + """ Test for existance of API Field + + slug field must exist + """ + + assert 'slug' in self.api_data + + + def test_api_field_type_slug(self): + """ Test for type for API Field + + slug field must be str + """ + + assert type(self.api_data['slug']) is str + + + def test_api_field_exists_created(self): + """ Test for existance of API Field + + created field must exist + """ + + assert 'created' in self.api_data + + + def test_api_field_type_created(self): + """ Test for type for API Field + + created field must be str + """ + + assert type(self.api_data['created']) is str + + + def test_api_field_exists_modified(self): + """ Test for existance of API Field + + modified field must exist + """ + + assert 'modified' in self.api_data + + + def test_api_field_type_modified(self): + """ Test for type for API Field + + modified field must be str + """ + + assert type(self.api_data['modified']) is str + + + def test_api_field_exists_organization(self): + """ Test for existance of API Field + + organization field must exist + """ + + assert 'organization' in self.api_data + + + def test_api_field_type_organization(self): + """ Test for type for API Field + + organization field must be intt + """ + + assert type(self.api_data['organization']) is int + + + def test_api_field_exists_publisher(self): + """ Test for existance of API Field + + publisher field must exist + """ + + assert 'publisher' in self.api_data + + + def test_api_field_type_publisher(self): + """ Test for type for API Field + + publisher field must be int + """ + + assert type(self.api_data['publisher']) is int + + + def test_api_field_exists_category(self): + """ Test for existance of API Field + + category field must exist + """ + + assert 'category' in self.api_data + + + def test_api_field_type_category(self): + """ Test for type for API Field + + category field must be int + """ + + assert type(self.api_data['category']) is int + From 7de5ab12bf117d6b03dd5619e0a1ff07b506f005 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 11 Aug 2024 12:37:52 +0930 Subject: [PATCH 107/123] docs(readme): correct status badge icon to github . #220 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ef2e247d..a2adb644 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@
-![Project Status - Active](https://img.shields.io/badge/Project%20Status-Active-green?logo=gitlab&style=plastic) +![Project Status - Active](https://img.shields.io/badge/Project%20Status-Active-green?logo=github&style=plastic) [![Docker Pulls](https://img.shields.io/docker/pulls/nofusscomputing/centurion-erp?style=plastic&logo=docker&color=0db7ed)](https://hub.docker.com/r/nofusscomputing/centurion-erp) [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/centurion-erp)](https://artifacthub.io/packages/container/centurion-erp/centurion-erp) From d6bd99c5de44dc250234da997f55d8ec14c6f4d7 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 11 Aug 2024 12:38:20 +0930 Subject: [PATCH 108/123] docs: update project badges to reflect github hosted project . #220 --- docs/projects/centurion_erp/index.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/projects/centurion_erp/index.md b/docs/projects/centurion_erp/index.md index 8579fb33..5b634401 100644 --- a/docs/projects/centurion_erp/index.md +++ b/docs/projects/centurion_erp/index.md @@ -8,15 +8,16 @@ about: https://gitlab.com/nofusscomputing/infrastructure/configuration-managemen -![Project Status - Active](https://img.shields.io/badge/Project%20Status-Active-green?logo=gitlab&style=plastic) +![Project Status - Active](https://img.shields.io/badge/Project%20Status-Active-green?logo=github&style=plastic) -![Gitlab build status - stable](https://img.shields.io/badge/dynamic/json?color=ff782e&label=Stable%20Build&query=0.status&url=https%3A%2F%2Fgitlab.com%2Fapi%2Fv4%2Fprojects%2F57560288%2Fpipelines%3Fref%3Dmaster&logo=gitlab&style=plastic) ![Gitlab build status - development](https://img.shields.io/badge/dynamic/json?color=ff782e&label=Dev%20Build&query=0.status&url=https%3A%2F%2Fgitlab.com%2Fapi%2Fv4%2Fprojects%2F57560288%2Fpipelines%3Fref%3Ddevelopment&logo=gitlab&style=plastic) +![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/nofusscomputing/centurion_erp/ci.yaml?branch=master&style=plastic&logo=github&label=Stable%20Build&color=%23000) ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/nofusscomputing/centurion_erp/ci.yaml?branch=development&style=plastic&logo=github&label=Dev%20Build&color=%23000) -![GitLab Issues](https://img.shields.io/gitlab/issues/open/nofusscomputing%2Fprojects%2Fcenturion_erp?style=plastic&logo=gitlab&label=Issues&color=fc6d26) [![GitLab Bugs](https://img.shields.io/gitlab/issues/open/nofusscomputing%2Fprojects%2Fcenturion_erp?labels=type%3A%3Abug&style=plastic&logo=gitlab&label=Bug%20Fixes%20Required&color=fc6d26)](https://gitlab.com/nofusscomputing/projects/centurion_erp/-/issues/?sort=created_date&state=opened&label_name%5B%5D=type%3A%3Abug) +![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/nofusscomputing/centurion_erp?style=plastic&logo=github&label=Open%20Issues&color=000) ![GitHub Issues or Pull Requests by label](https://img.shields.io/github/issues/nofusscomputing/centurion_erp/type%3A%3Abug?style=plastic&logo=github&label=Bug%20Fixes%20Required&color=000) -![Gitlab Code Coverage](https://img.shields.io/gitlab/pipeline-coverage/nofusscomputing%2Fprojects%2Fcenturion_erp?branch=master&style=plastic&logo=gitlab&label=Test%20Coverage) +![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fdevelopment%2Fbadge_endpoint_coverage.json&style=plastic) ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fnofusscomputing%2F.github%2Fmaster%2Frepositories%2Fnofusscomputing%2Fcenturion_erp%2Fdevelopment%2Fbadge_endpoint_unit_test.json) -![Docker Pulls](https://img.shields.io/docker/pulls/nofusscomputing/centurion-erp?style=plastic&logo=docker&color=0db7ed) + +![Docker Pulls](https://img.shields.io/docker/pulls/nofusscomputing/centurion-erp?style=plastic&logo=docker&color=0db7ed) [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/centurion-erp)](https://artifacthub.io/packages/container/centurion-erp/centurion-erp) From b9d32a2c1630f572033d057438c8ee865414175f Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 11 Aug 2024 12:57:33 +0930 Subject: [PATCH 109/123] docs(tests): update testing docs explaining test types #220 closes #162 --- .../centurion_erp/development/testing.md | 46 ++++++++++++++----- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/docs/projects/centurion_erp/development/testing.md b/docs/projects/centurion_erp/development/testing.md index 0ef64bde..4df71704 100644 --- a/docs/projects/centurion_erp/development/testing.md +++ b/docs/projects/centurion_erp/development/testing.md @@ -8,14 +8,18 @@ about: https://gitlab.com/nofusscomputing/infrastructure/configuration-managemen Unit tests are written to aid in application stability and to assist in preventing regression bugs. As part of development the developer working on a Merge/Pull request is to ensure that tests are written. Failing to do so will more likely than not ensure that your Merge/Pull request is not merged. -User Interface (UI) test are written to test the user interface to ensure that it functions as it should. Changes to the UI will need to be tested. +User Interface (UI) test are written _if applicable_ to test the user interface to ensure that it functions as it should. Changes to the UI will need to be tested. + +In most cases functional tests will not need to be written, however you should confirm this with a maintainer. + +Integration tests **will** be required if the development introduces code that interacts with an independent third-party application. ## Writing Tests We use class based tests. Each class will require a `setUpTestData` method for test setup. To furhter assist in the writing of tests, we have written the test cases for common items as an abstract class. You are advised to review the [test cases](./api/tests/index.md) and if it's applicable to the item you have added, than add the test case class to be inherited by your test class. -naming of test classes is in `CamelCase` in format `` for example the class name for device model history entry tests would be `DeviceHistory`. +Naming of test classes is in `CamelCase` in format `` for example the class name for device model history entry tests would be `DeviceHistory`. Test setup is written in a method called `setUpTestData` and is to contain the setup for all tests within the test class. @@ -51,22 +55,40 @@ class DeviceHistory(TestCase, HistoryEntry, HistoryEntryParentItem): Each module is to contain a tests directory of the model being tested with a single file for grouping of what is being tested. for items that depend upon a parent model, the test file is to be within the child-models test directory named with format `test___` -_example file system structure for the device model that relies upon access app model organization, core app model history and model notes._ +_example file system structure showing the layout of the tests directory for a module_ ``` text - +. ├── tests -│   ├── -│   │   ├── test__access_organization.py -│   │   ├── test__api_permission.py -│   │   ├── test__core_history.py -│   │   ├── test__core_notes.py -│   │   ├── test__permission.py -│   │   └── test_device.py - +│   ├── functional +│   │ ├── __init__.py +│   │   └── +│   │   └── test__a_tast_name.py +│   ├── __init__.py +│   ├── integration +│   │ ├── __init__.py +│   │   └── +│   │   └── test__a_tast_name.py +│   ├── ui +│   │ ├── __init__.py +│   │   └── +│   │   └── test__a_tast_name.py +│   └── unit +│   ├── __init__.py +│   └── +│      ├── test__api.py +│      ├── test__permission_api.py +│      ├── test__permission.py +│      ├── test__core_history.py +│      ├── test__history_permission.py +│      ├── test_.py +│      └── test__views.py ``` +Tests are broken up into the type the test is (sub-directory to test), and they are `unit`, `functional`, `UI` and `integration`. These sub-directories each contain a sub-directory for each model they are testing. + + Items to test include, and are not limited to: - CRUD permissions admin site From e9fe4896df154963a0fb8731296935fcbd7fcf28 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 11 Aug 2024 13:17:44 +0930 Subject: [PATCH 110/123] ci: mirror repo to gitlab . #220 closes #214 --- .github/workflows/ci.yaml | 76 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6d07c129..f18cb2fc 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -10,6 +10,8 @@ on: tags: - '*' +env: + GIT_SYNC_URL: "https://${{ secrets.GITLAB_USERNAME_ROBOT }}:${{ secrets.GITLAB_TOKEN_ROBOT }}@gitlab.com/nofusscomputing/projects/centurion_erp.git" jobs: @@ -31,3 +33,77 @@ jobs: uses: nofusscomputing/action_python/.github/workflows/python.yaml@development secrets: WORKFLOW_TOKEN: ${{ secrets.WORKFLOW_TOKEN }} + + + gitlab-mirror: + if: ${{ github.repository == 'nofusscomputing/centurion_erp' }} + runs-on: ubuntu-latest + steps: + + + - name: Checks + shell: bash + run: | + if [ "0${{ env.GIT_SYNC_URL }}" == "0" ]; then + + echo "[ERROR] you must define variable GIT_SYNC_URL for mirroring this repository."; + + exit 1; + + fi + + + - name: clone + shell: bash + run: | + + git clone --mirror https://github.com/${{ github.repository }} repo; + + ls -la repo/ + + + - name: add remote + shell: bash + run: | + + cd repo; + + echo "**************************************** - git remote -v"; + + git remote -v; + + echo "****************************************"; + + git remote add destination $GIT_SYNC_URL; + + + - name: push branches + shell: bash + run: | + + cd repo; + + echo "**************************************** - git branch"; + + git branch; + + echo "****************************************"; + + # git push destination --all --force; + + git push destination --mirror || true; + + + # - name: push tags + # shell: bash + # run: | + + # cd repo; + + # echo "**************************************** - git tag"; + + # git tag; + + # echo "****************************************"; + + # git push destination --tags --force; From 0fc5f413910ac37d4cd8cdb01ea599d9889f244c Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 11 Aug 2024 16:26:19 +0930 Subject: [PATCH 111/123] fix(settings): ensure that the api token cant be saved to notes field #153 --- app/api/models/tokens.py | 33 +++++++++++++++++++++++++++++ app/settings/views/user_settings.py | 2 ++ 2 files changed, 35 insertions(+) diff --git a/app/api/models/tokens.py b/app/api/models/tokens.py index d5471c34..f4caf411 100644 --- a/app/api/models/tokens.py +++ b/app/api/models/tokens.py @@ -5,6 +5,8 @@ import string from django.conf import settings from django.contrib.auth.models import User from django.db import models +from django.db.models import Field +from django.forms import ValidationError from access.fields import * from access.models import TenancyObject @@ -14,6 +16,37 @@ from access.models import TenancyObject class AuthToken(models.Model): + def validate_note_no_token(self, note, token): + """ Ensure plaintext token cant be saved to notes field. + + called from app.settings.views.user_settings.TokenAdd.form_valid() + + Args: + note (Field): _Note field_ + token (Field): _Token field_ + + Raises: + ValidationError: _Validation failed_ + """ + + validation: bool = True + + + if str(note) == str(token): + + validation = False + + + if str(token)[:9] in str(note): # Allow user to use up to 8 chars so they can reference it. + + validation = False + + if not validation: + + raise ValidationError('Token can not be placed in the notes field.') + + + id = models.AutoField( primary_key=True, unique=True, diff --git a/app/settings/views/user_settings.py b/app/settings/views/user_settings.py index e3105c52..fe16b92c 100644 --- a/app/settings/views/user_settings.py +++ b/app/settings/views/user_settings.py @@ -101,6 +101,8 @@ class TokenAdd(AddView): form.instance.user = self.request.user form.instance.token = form.instance.token_hash(form.fields['gen_token'].initial) + self.model.validate_note_no_token(self, note=form.instance.note, token=form.fields['gen_token'].initial) + return super().form_valid(form) From e29d8e1ec18d78023009983a1580e86db86c8300 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 11 Aug 2024 16:34:25 +0930 Subject: [PATCH 112/123] fix(config_management): Ensure that config group can't set self as parent interface already filters self out, however check still to be done. . #153 #222 --- app/config_management/models/groups.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/config_management/models/groups.py b/app/config_management/models/groups.py index cfb80a7d..1574ecc8 100644 --- a/app/config_management/models/groups.py +++ b/app/config_management/models/groups.py @@ -195,6 +195,12 @@ class ConfigGroups(GroupsCommonFields, SaveHistory): # Prevent organization change. ToDo: add feature so that config can change organizations self.organization = obj.organization + if self.parent is not None: + + if self.pk == self.parent.pk: + + raise ValidationError('Can not set self as parent') + super().save(*args, **kwargs) From f86b2d5216c00b45697a4a4d856e802b309520fe Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 11 Aug 2024 17:08:56 +0930 Subject: [PATCH 113/123] fix(itam): Ensure device UUID is correctly formatted #153 #222 --- app/itam/models/device.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/itam/models/device.py b/app/itam/models/device.py index 31407bec..d0f2a7fb 100644 --- a/app/itam/models/device.py +++ b/app/itam/models/device.py @@ -1,8 +1,10 @@ import json +import re from datetime import timedelta from django.db import models +from django.forms import ValidationError from access.fields import * from access.models import TenancyObject @@ -18,6 +20,8 @@ from itam.models.operating_system import OperatingSystemVersion from settings.models.app_settings import AppSettings + + class DeviceType(DeviceCommonFieldsName, SaveHistory): @@ -39,6 +43,17 @@ class DeviceType(DeviceCommonFieldsName, SaveHistory): class Device(DeviceCommonFieldsName, SaveHistory): + + def validate_uuid_format(self): + + pattern = r'[0-9|a-f]{8}\-[0-9|a-f]{4}\-[0-9|a-f]{4}\-[0-9|a-f]{4}\-[0-9|a-f]{12}' + + if not re.match(pattern, str(self)): + + raise ValidationError(f'UUID Must be in {str(pattern)}') + + + serial_number = models.CharField( verbose_name = 'Serial Number', max_length = 50, @@ -58,6 +73,7 @@ class Device(DeviceCommonFieldsName, SaveHistory): blank = True, unique = True, help_text = 'System GUID/UUID.', + validators = [ validate_uuid_format ] ) device_model = models.ForeignKey( From 467f6fca6bca0b39d85c029a7e7d6f4ec3ea0e9b Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 11 Aug 2024 17:09:06 +0930 Subject: [PATCH 114/123] fix(itam): Ensure device name is formatted according to RFC1035 2.3.1 see https://datatracker.ietf.org/doc/html/rfc1035#autoid-6 #222 closes #153 --- app/itam/models/device.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/app/itam/models/device.py b/app/itam/models/device.py index d0f2a7fb..4bbc292b 100644 --- a/app/itam/models/device.py +++ b/app/itam/models/device.py @@ -53,6 +53,24 @@ class Device(DeviceCommonFieldsName, SaveHistory): raise ValidationError(f'UUID Must be in {str(pattern)}') + def validate_hostname_format(self): + + pattern = r'^[a-z]{1}[a-z|0-9|\-]+[a-z|0-9]{1}$' + + if not re.match(pattern, str(self).lower()): + + raise ValidationError( + '''[RFC1035 2.3.1] A hostname must start with a letter, end with a letter or digit, + and have as interior characters only letters, digits, and hyphen.''' + ) + + + name = models.CharField( + blank = False, + max_length = 50, + unique = True, + validators = [ validate_hostname_format ] + ) serial_number = models.CharField( verbose_name = 'Serial Number', From c0f186db891515f55a4d1a8e6d9309747d9cf2ce Mon Sep 17 00:00:00 2001 From: nfc-bot Date: Sun, 11 Aug 2024 08:01:20 +0000 Subject: [PATCH 115/123] build: bump version 1.0.0-b12 -> 1.0.0-b13 --- .cz.yaml | 2 +- CHANGELOG.md | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.cz.yaml b/.cz.yaml index 4fca755e..e18ee83f 100644 --- a/.cz.yaml +++ b/.cz.yaml @@ -4,5 +4,5 @@ commitizen: prerelease_offset: 1 tag_format: $version update_changelog_on_bump: false - version: 1.0.0-b12 + version: 1.0.0-b13 version_scheme: semver diff --git a/CHANGELOG.md b/CHANGELOG.md index ef96df18..486c38e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## 1.0.0-b13 (2024-08-11) + +### Fix + +- **itam**: Ensure device name is formatted according to RFC1035 2.3.1 +- **itam**: Ensure device UUID is correctly formatted +- **config_management**: Ensure that config group can't set self as parent +- **settings**: ensure that the api token cant be saved to notes field + ## 1.0.0-b12 (2024-08-10) ### Fix From aa40d68c88495bf4bb5d55a4c000b71048dbc1bc Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 11 Aug 2024 19:30:45 +0930 Subject: [PATCH 116/123] build: add test to changelog #209 --- .cz.yaml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.cz.yaml b/.cz.yaml index e18ee83f..54e79e71 100644 --- a/.cz.yaml +++ b/.cz.yaml @@ -1,8 +1,22 @@ --- commitizen: - name: cz_conventional_commits + # name: cz_conventional_commits + name: cz_customize prerelease_offset: 1 tag_format: $version update_changelog_on_bump: false version: 1.0.0-b13 version_scheme: semver + customize: + change_type_order: + - "BREAKING CHANGE" + - "feat" + - "fix" + - "test" + - "refactor" + change_type_map: + feature: Features + fix: Fixes + refactor: Refactoring + test: Tests + commit_parser: '^(?Pfeat|fix|test|refactor|perf|BREAKING CHANGE)(?:\((?P[^()\r\n]*)\)|\()?(?P!)?:\s(?P.*)?' From 3cace8943eeeca8cd67aa8f9d05632da133d78cc Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 12 Aug 2024 15:14:58 +0930 Subject: [PATCH 117/123] fix(api): ensure model_notes is an available field #223 --- app/api/serializers/access.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/api/serializers/access.py b/app/api/serializers/access.py index 623352be..44eadf19 100644 --- a/app/api/serializers/access.py +++ b/app/api/serializers/access.py @@ -15,6 +15,7 @@ class TeamSerializerBase(serializers.ModelSerializer): model = Team fields = ( 'team_name', + 'model_notes', 'permissions', 'url', ) @@ -75,6 +76,7 @@ class TeamSerializer(TeamSerializerBase): fields = ( "id", "team_name", + 'model_notes', 'permissions', 'permissions_url', 'url', From f298ce94bf6fb2713f74f30a539b60a384ab033f Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 12 Aug 2024 15:15:28 +0930 Subject: [PATCH 118/123] test(access): test field model_notes closes #223 --- app/access/tests/unit/team/test_team_api.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/app/access/tests/unit/team/test_team_api.py b/app/access/tests/unit/team/test_team_api.py index cb4b73be..ec8039c5 100644 --- a/app/access/tests/unit/team/test_team_api.py +++ b/app/access/tests/unit/team/test_team_api.py @@ -51,7 +51,8 @@ class TeamAPI(TestCase): self.item = self.model.objects.create( organization=organization, - team_name = 'teamone' + team_name = 'teamone', + model_notes = 'random note' ) @@ -130,6 +131,24 @@ class TeamAPI(TestCase): assert type(self.api_data['team_name']) is str + def test_api_field_exists_model_notes(self): + """ Test for existance of API Field + + model_notes field must exist + """ + + assert 'model_notes' in self.api_data + + + def test_api_field_type_model_notes(self): + """ Test for type for API Field + + model_notes field must be str + """ + + assert type(self.api_data['model_notes']) is str + + def test_api_field_exists_url(self): """ Test for existance of API Field From 9d5464b5a989f5095a099fcb081a9422b576cefa Mon Sep 17 00:00:00 2001 From: nfc-bot Date: Mon, 12 Aug 2024 06:02:07 +0000 Subject: [PATCH 119/123] build: bump version 1.0.0-b13 -> 1.0.0-b14 --- .cz.yaml | 29 +++--- CHANGELOG.md | 260 +++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 237 insertions(+), 52 deletions(-) diff --git a/.cz.yaml b/.cz.yaml index 54e79e71..be5a78ba 100644 --- a/.cz.yaml +++ b/.cz.yaml @@ -1,22 +1,21 @@ --- commitizen: - # name: cz_conventional_commits - name: cz_customize - prerelease_offset: 1 - tag_format: $version - update_changelog_on_bump: false - version: 1.0.0-b13 - version_scheme: semver - customize: - change_type_order: - - "BREAKING CHANGE" - - "feat" - - "fix" - - "test" - - "refactor" + customize: change_type_map: feature: Features fix: Fixes refactor: Refactoring test: Tests - commit_parser: '^(?Pfeat|fix|test|refactor|perf|BREAKING CHANGE)(?:\((?P[^()\r\n]*)\)|\()?(?P!)?:\s(?P.*)?' + change_type_order: + - BREAKING CHANGE + - feat + - fix + - test + - refactor + commit_parser: ^(?Pfeat|fix|test|refactor|perf|BREAKING CHANGE)(?:\((?P[^()\r\n]*)\)|\()?(?P!)?:\s(?P.*)? + name: cz_customize + prerelease_offset: 1 + tag_format: $version + update_changelog_on_bump: false + version: 1.0.0-b14 + version_scheme: semver diff --git a/CHANGELOG.md b/CHANGELOG.md index 486c38e3..0933558e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,43 @@ +## 1.0.0-b14 (2024-08-12) + +### Fixes + +- **api**: ensure model_notes is an available field + +### Tests + +- **access**: test field model_notes + ## 1.0.0-b13 (2024-08-11) -### Fix +### Fixes +- Audit models for validations - **itam**: Ensure device name is formatted according to RFC1035 2.3.1 - **itam**: Ensure device UUID is correctly formatted - **config_management**: Ensure that config group can't set self as parent - **settings**: ensure that the api token cant be saved to notes field +### Tests + +- api field checks +- **software**: api field checks + ## 1.0.0-b12 (2024-08-10) -### Fix +### Fixes - **api**: ensure org mixin is inherited by software view - **base**: correct project links to github +### Tests + +- api field checks + +#128 #162 +- **teams**: api field checks +- **organization**: api field checks + ## 1.0.0-b11 (2024-08-10) ## 1.0.0-b10 (2024-08-09) @@ -28,62 +52,78 @@ ## 1.0.0-b5 (2024-07-31) -### Feat +### feat +- add Config groups to API - **api**: Add device config groups to devices - **api**: Ability to fetch configgroups from api along with config -### Fix +### Fixes - **api**: Ensure device groups is read only +### Tests + +- **api**: Field existence and type checks for device +- **api**: test configgroups API fields + ## 1.0.0-b4 (2024-07-29) -### Feat +### feat - **swagger**: remove `{format}` suffixed doc entries -### Fix +### Fixes +- release-b3 fixes - **api**: cleanup team post/get - **api**: confirm HTTP method is allowed before permission check - **api**: Ensure that organizations can't be created via the API - **access**: Team model class inheritance order corrected +### Tests + +- confirm that the tenancymanager is called + ## 1.0.0-b3 (2024-07-21) -### Fix +### Fixes - **itam**: Limit os version count to devices user has access to ## 1.0.0-b2 (2024-07-19) -### Fix +### Fixes - **itam**: only show os version once ## 1.0.0-b1 (2024-07-19) -### Fix +### Fixes - **itam**: ensure installed operating system count is limited to users organizations - **itam**: ensure installed software count is limited to users organizations ## 1.0.0-a4 (2024-07-18) -### Feat +### feat - **api**: When processing uploaded inventory and name does not match, update name to one within inventory file - **config_management**: Group name to be entire breadcrumb +### Tests + +- ensure inventory upload matches by both serial number and uuid if device name different +- placeholder for moving organization + ## 1.0.0-a3 (2024-07-18) -### Feat +### feat - **config_management**: Prevent a config group from being able to change organization - **itam**: On device organization change remove config groups -### Fix +### Fixes - **config_management**: dont attempt to do action during save if group being created - **itam**: remove org filter for device so that user can see installations @@ -94,13 +134,13 @@ ## 1.0.0-a2 (2024-07-17) -### Feat +### feat - **api**: Inventory matching of device second by uuid - **api**: Inventory matching of device first by serial number - **base**: show warning bar if the user has not set a default organization -### Fix +### Fixes - **base**: dont show user warning bar for non-authenticated user - **api**: correct inventory operating system selection by name @@ -113,25 +153,31 @@ - squashed DB migrations in preparation for v1.0 release. -### Feat +### feat - Administratively set global items org/is_global field now read-only - **access**: Add multi-tennant manager -### Fix +### Fixes - **core**: migrate manufacturer to use new form/view logic - **settings**: correct the permission to view manufacturers - **access**: Correct team form fields - **config_management**: don't exclude parent from field, only self -### Refactor +### Refactoring +- repo preperation for v1.0.0-Alpha-1 - Squash database migrations +### Tests + +- tenancy objects +- refactor to single abstract model for inclusion. + ## 0.7.0 (2024-07-14) -### Feat +### feat - **core**: Filter every form field if associated with an organization to users organizations only - **core**: add var `template_name` to common view template for all views that require it @@ -146,13 +192,14 @@ - **ui**: add some navigation icons - **itam**: update inventory status icon - **itam**: ensure device software pagination links keep interface on software tab +- "Migrate inventory processing to background worker" - **access**: enable non-organization django permission checks - **settings**: Add celery task results index and view page - **base**: Add background worker - **itam**: Update Serial Number from inventory if present and Serial Number not set - **itam**: Update UUID from inventory if present and UUID not set -### Fix +### Fixes - **config_management**: Don't allow a config group to assign itself as its parent - **config_management**: correct permission for deleting a host from config group @@ -189,11 +236,14 @@ - **itam**: show device model name instead of ID - **api**: Ensure if serial number from inventory is `null` that it's not used - **api**: ensure checked uuid and serial number is used for updating +- inventory - **itam**: only remove device software when not found during inventory upload - **itam**: only update software version if different +- existing device without uuid not updated when uploading an inventory +- Device Software tab pagination does not work - **itam**: correct device software pagination -### Refactor +### Refactoring - adjust views missing add/change form to now use forms - add navigation menu expand arrows @@ -208,31 +258,57 @@ - **api**: migrate inventory processing to background worker - **itam**: only perform actions on device inventory if DB matches inventory item +### Tests + +- add test test_view_*_attribute_not_exists_fields for add and change views +- fix test_view_change_attribute_type_form_class to test if type class +- **views**: add test cases for model views +- Add Test case abstract classes to models +- **inventory**: add mocks?? for calling background worker +- **view**: view permission checks +- **inventory**: update tests for background worker changes + ## 0.6.0 (2024-06-30) -### Feat +### feat +- user api token - **api**: API token authentication - **api**: abilty for user to create/delete api token - **api**: create token model -### Fix +### Fixes - **user_token**: conduct user check on token view access - **itam**: use same form for edit and add - **itam**: dont add field inventorydate if adding new item - **api**: inventory upload requires sanitization -### Refactor +### Refactoring - **settings**: use seperate change/view views - **settings**: use form for user settings - **tests**: move unit tests to unit test sub-directory +### Tests + +- **token_auth**: test authentication method token +- more tests +- add .coveragerc to remove non-code files from coverage report +- Unit Tests TenancyObjects +- Test Cases for TenancyObjects +- tests for checking links from rendered templetes +- **core**: test cases for notes permissions +- **config_management**: config groups history permissions +- **api**: Majority of Inventory upload tests +- **access**: TenancyObject field tests +- **access**: remove skipped api tests for team users + ## 0.5.0 (2024-06-17) -### Feat +### feat +- Setup Organization Managers - **access**: add notes field to organization - **access**: add organization manger - **config_management**: Use breadcrumbs for child group name display @@ -240,6 +316,7 @@ - **itam**: add a status of "bad" for devices - **itam**: paginate device software tab - **itam**: status of device visible on device index page +- API Browser - **core**: add skeleton http browser - **core**: Add a notes field to manufacturer/ publisher - **itam**: Add a notes field to software category @@ -256,24 +333,28 @@ - **itam**: add docs icon to devices page - **config_management**: add docs icon to config groups page - **base**: add dynamic docs icon +- config group software - **models**: add property parent_object to models that have a parent - **config_management**: add config group software to group history - **itam**: render group software config within device rendered config - **config_management**: assign software action to config group +- sso - add configuration value 'SESSION_COOKIE_AGE' - remove development SECRET_KEY and enforce checking for user configured one - **base**: build CSRF trusted origins from configuration - **base**: Enforceable SSO ONLY - **base**: configurable SSO -### Fix +### Fixes - **itam**: remove requirement that user needs change device to add notes - **core**: dont attempt to access parent_object if 'None' during history save - **config_management**: Add missing parent item getter to model - **core**: overridden save within SaveHistory to use default attributes - **access**: overridden save to use default attributes +- History does not delete when item deleted - **core**: on object delete remove history entries +- inventory upload cant determin object organization - **api**: ensure proper permission checking - dont throw an exception during settings load for an item django already checks - **core**: Add overrides for delete so delete history saved for items with parent model @@ -281,7 +362,7 @@ - **base**: remove social auth from nav menu - **access**: add a team user permissions to use team organization -### Refactor +### Refactoring - **access**: relocate permission check to own function - **itam**: move device os tab to details tab @@ -295,14 +376,58 @@ - login to use base template - adjust template block names +### Tests + +- **access**: team user model permission check for organization manager +- **access**: team model permission check for organization manager +- **access**: organization model permission check for organization manager +- **access**: add test cases for model delete as organization manager +- **access**: add test cases for model addd as organization manager +- **access**: add test cases for model change as organization manager +- **access**: add test cases for model view as organization manager +- write some more +- **core**: skip invalid tests +- **itam**: tests for device type history entries +- **core**: tests for manufacturer history entries +- move manufacturer to it's parent +- refactor api model permission tests to use an abstract class of test cases +- move tests to the module they belong to +- refactor history permission tests to use an abstract class of test cases +- refactor model permission tests to use an abstract class of test cases +- refactor history entry to have test cases in abstract classes +- **itam**: history entry tests for software category +- **itam**: history entry tests for device operating system version +- **itam**: history entry tests for device operating system +- **itam**: history entry tests for device software +- **itam**: ensure child history is removed on config group software delete +- add placeholder tests +- **itam**: ensure history is removed on software delete +- **itam**: ensure history is removed on operating system delete +- **itam**: ensure history is removed on device model delete +- **config_management**: test history on delete for config groups +- **itam**: ensure history is removed on device delete +- **access**: test team history +- **access**: ensure team user history is created and removed as required +- **access**: ensure history is removed on team delete +- **access**: ensure history is removed on item delete +- **api**: Inventory upload permission checks +- **config_management**: testing of config_groups rendered config +- **config_management**: history save tests for config groups software +- **config_management**: config group software permission for add, change and delete +- **base**: placeholder tests for config groups software +- **base**: basic test for merge_software helper +- during unit tests add SECRET_KEY + ## 0.4.0 (2024-06-05) -### Feat +### feat +- 2024 06 05 - **database**: add mysql support - **api**: move invneotry api endpoint to '/api/device/inventory' - **core**: support more history types - **core**: function to fetch history entry item +- 2024 06 02 - **config_management**: Add button to groups ui for adding child group - **access**: throw error if no organization added - **itam**: add delete button to config group within ui @@ -315,10 +440,12 @@ - **api**: add swagger ui for documentation - **api**: filter software to users organizations - **api**: filter devices to users organizations +- randomz - **api**: add org team view page +- API configuration of permissions - **api**: configure team permissions -### Fix +### Fixes - **itam**: ensure device type saves history - **core**: correct history view permissions @@ -335,7 +462,7 @@ - **api**: correct reverse url lookup to use NS API - **api**: permissions for organization -### Refactor +### Refactoring - **access**: cache object so it doesnt have to be called multiple times - **config_management**: move groups to nav menu @@ -343,20 +470,49 @@ - **api**: move permission check to mixin - **access**: add team option to org permission check +### Tests + +- **api**: placeholder test for inventory +- **settings**: access permission check for app settings +- **settings**: history view permission check for software category +- **settings**: history view permission check for manufacturer +- **settings**: history view permission check for device type +- **settings**: user settings +- **settings**: view permission check for user settings +- refactor core test layout +- **itam**: view permission check for software +- **itam**: view permission check for operating system +- **itam**: view permission check for device model +- **itam**: view permission check for device +- **config_management**: view permission check for config_groups +- **access**: view permission check for team +- **access**: view permission check for organization +- add history entry creation tests for most models +- **config_management**: when adding a host to config group filter out host that are already members of the group +- **config_management**: unit test for config groups model to ensure permissions are working +- **api**: remove tests for os and manufacturer as they are not used in api +- **api**: check model permissions for software +- **api**: check model permissions for devices +- **api**: check model permissions for teams +- **api**: check model permissions for organizations + ## 0.3.0 (2024-05-29) -### Feat +### feat +- Randomz - **access**: during organization permission check, check to ensure user is logged on - **history**: always create an entry even if user=none - **itam**: device uuid must be unique - **itam**: device serial number must be unique +- 2024 05 26 - **setting**: Enable super admin to set ALL manufacturer/publishers as global - **setting**: Enable super admin to set ALL device types as global - **setting**: Enable super admin to set ALL device models as global - **setting**: Enable super admin to set ALL software categories as global - **UI**: show build details with page footer - **software**: Add output to stdout to show what is and has occurred +- 2024 05 25 - **base**: Add delete icon to content header - **itam**: Populate initial organization value from user default organization for software category creation - **itam**: Populate initial organization value from user default organization for device type creation @@ -368,17 +524,20 @@ - Add management command software - **setting**: Enable super admin to set ALL software as global - **user**: Add user settings panel +- Manufacturer and Model Information - **itam**: Add publisher to software - **itam**: Add publisher to operating system - **itam**: Add device model - **core**: Add manufacturers - **settings**: add dummy model for permissions - **settings**: new module for whole of application settings/globals +- 2024 05 21-23 - **access**: Save changes to history for organization and teams - **software**: Save changes to history - **operating_system**: Save changes to history - **device**: Save changes to history - **core**: history model for saving model history +- 2024 05 19/20 - **itam**: Ability to add notes to software - **itam**: Ability to add notes to operating systems - **itam**: Ability to add notes on devices @@ -387,7 +546,7 @@ - **ui**: Show inventory details if they exist - **api**: API accept computer inventory -### Fix +### Fixes - **settings**: Add correct permissions for team user delete - **settings**: Add correct permissions for team user view/change @@ -438,7 +597,7 @@ - correct typo in notes templates - **ui**: Ensure navigation menu entry highlighted for sub items -### Refactor +### Refactoring - **access**: add to models a get_organization function - **access**: remove change view @@ -450,13 +609,33 @@ - **itam**: move device types to settings app - **template**: content_title can be rendered in base +### Tests + +- cleanup duplicate tests and minor reshuffle +- **access**: unit testing team user permissions +- **access**: unit testing team permissions +- **settings**: unit testing manufacturer permissions +- **settings**: unit testing software category permissions +- **device_model**: unit testing device type permissions +- **device_model**: unit testing device model permissions +- **organization**: unit testing organization permissions +- **operating_system**: unit testing operating system permissions +- **software**: unit testing software permissions +- **device**: unit testing device permissions +- adjust test layout and update contributing +- **core**: placeholder tests for history component +- **core**: place holder tests for notes model +- **api**: add placeholder tests for inventory + ## 0.2.0 (2024-05-18) -### Feat +### feat +- 2024 05 18 - **itam**: Add Operating System to ITAM models - **api**: force content type to be JSON for req/resp - **software**: view software +- 2024 05 17 - **device**: Prevent devices from being set global - **software**: if no installations found, denote - **device**: configurable software version @@ -468,21 +647,24 @@ - **software**: add pagination for index - **device**: add pagination for index -### Fix +### Fixes - **device**: correct software link ## 0.1.0 (2024-05-17) -### Feat +### feat +- API token auth - **api**: initial token authentication implementation +- itam and API setup - **docker**: add settings to store data in separate volume - **django**: add split settings for specifying additional settings paths - **api**: Add device config to device - **itam**: add organization to device installs - **itam**: migrate app from own repo - Enable API by default +- Genesis - **admin**: remove team management - **admin**: remove group management - **access**: adjustable team permissions @@ -516,14 +698,14 @@ - **template**: add base template - **django**: add organizations app -### Fix +### Fixes - **itam**: device software to come from device org or global not users orgs - **access**: correct team required permissions - **fields**: correct autoslug field so it works - **docker**: build wheels then install -### Refactor +### Refactoring - button to use same selection colour - **access**: remove inline form for org teams @@ -532,4 +714,8 @@ - **views**: move views to own directory - **access**: addjust org and teams to use different view per action +### Tests + +- interim unit tests + ## 0.0.1 (2024-05-06) From 18b788844a2e590cfb75885ed4eda5aae9e60b42 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 19 Aug 2024 16:53:31 +0930 Subject: [PATCH 120/123] ci: update gitlab-ci to current head ref: #209 --- gitlab-ci | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitlab-ci b/gitlab-ci index 58ffcabb..6f8dfcba 160000 --- a/gitlab-ci +++ b/gitlab-ci @@ -1 +1 @@ -Subproject commit 58ffcabbfb503af3e57d9cb3ab43931b23dc4cd8 +Subproject commit 6f8dfcba0b25313b59bc17b4c99d674fcedd207a From b6ba3d38dc39d84089e28aa89aca5d2a820f561a Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 20 Aug 2024 12:08:59 +0930 Subject: [PATCH 121/123] docs: migrate project links to github ref: #213 --- app/templates/icons/issue_link.html.j2 | 2 +- mkdocs.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/templates/icons/issue_link.html.j2 b/app/templates/icons/issue_link.html.j2 index 16df22c7..76fa9c26 100644 --- a/app/templates/icons/issue_link.html.j2 +++ b/app/templates/icons/issue_link.html.j2 @@ -2,5 +2,5 @@ - see #{{ issue }} + see #{{ issue }} \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index bfc62947..ae45edc6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,8 +3,8 @@ INHERIT: website-template/mkdocs.yml docs_dir: 'docs' repo_name: Centurion ERP -repo_url: https://gitlab.com/nofusscomputing/projects/centurion_erp -edit_uri: '/-/ide/project/nofusscomputing/projects/centurion_erp/edit/development/-/docs/' +repo_url: https://github.com/nofusscomputing/centurion_erp +edit_uri: '/edit/development/docs/' plugins: mkdocstrings: From cc97128e259df8104a288fb415ecbeafc8690c12 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 20 Aug 2024 12:28:43 +0930 Subject: [PATCH 122/123] ci: add mkdocs workflow ref: #213 --- .github/workflows/ci.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f18cb2fc..d8ac6fad 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,6 +16,17 @@ env: jobs: + mkdocs: + name: 'MKDocs' + permissions: + pull-requests: write + contents: write + statuses: write + checks: write + actions: write + uses: nofusscomputing/action_mkdocs/.github/workflows/reusable_mkdocs.yaml@development + + docker: name: 'Docker' uses: nofusscomputing/action_docker/.github/workflows/docker.yaml@development From 6f7b3ffad6153e523a4c81e5cb23677b8db2a5cd Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 20 Aug 2024 14:21:00 +0930 Subject: [PATCH 123/123] chore: add github pr template ref: #213 --- .../ticket-issue-ui-wireframe.drawio.png | Bin 0 -> 153009 bytes .../media/ticket-issue-ux-wireframe.drawio | 94 ++++++++++++++++++ docs/pull_request_template.md | 39 ++++++++ 3 files changed, 133 insertions(+) create mode 100644 docs/projects/centurion_erp/development/media/ticket-issue-ui-wireframe.drawio.png create mode 100644 docs/projects/centurion_erp/development/media/ticket-issue-ux-wireframe.drawio create mode 100644 docs/pull_request_template.md diff --git a/docs/projects/centurion_erp/development/media/ticket-issue-ui-wireframe.drawio.png b/docs/projects/centurion_erp/development/media/ticket-issue-ui-wireframe.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..b50ac4d588719888418b349c1b68edb63eaddf09 GIT binary patch literal 153009 zcmeFZ2_Tef`v;7s8l{Y-kZq7CJK5J6vWG%Z!Z4P}ZV=gJt1KyHk4Q=-M3H?7m1vPQ zgd*AZo$r3eI7iF7{onup{d&Le=o~Z8{oK#JT-$wJzw3Hj)X`GewPW875)zVKYN|@d zNl0LU;D0!L8))%LA-N7dNb$#2&?H$6OoQM8hDWRE!NHFg92-hPqQ>EJ#K6VQ-4bVO zLBc7bxc-e(SkT%5@4_jf#3?L{adhOz;xJau7&|<_y@d;C0{1ai7WTvrM;*@NY;7@| z!pfrjg5VanfshcV2pW9T#M!&LgHLw}F%z-F2f;U#qob|GNeeR#92iGgR79L#L=1XF zLsjpnCa16>_->1{u>c<`7Unh%&?Ac0&JK2v3PTgt;Kq?(0YrRgEQ8`dA$j&S(sDg2&W(li~;>8qDb5?xUk27 zxe0X80bAxHK90i@A9WORkW`WoJ!d7!r*i&?+A#wiFv$0#ZH#H>uHuYwv_9&9wXg*P zW8K$>5EeWvxxOZ>$9jv9pyc887AxnCiHP$;4Z67V8+#$FTv*V`6^FIJ6CYlG(#65S z)&=MIeWSU9y}gAwk!!>eG0x5o=f3Y|>0nFT9tX=GE!4;&1K(TEV9c&%e zWnJWmAoP!*okWdsu^gMieK~XG5?L6R*$xDJ7DU z#5U{A07iM$^U^gr|Y7udiogXvj$XxisN_$zWn^aaNEEVGfKu z_FJ$JIhEnXe6B(jlZ?cm|EnF~Ij0^A&!MOvDUFF|jfrSKxh=koZxi)lj zvn}0d+iXh3em{-?|C%7c|63l#e?%WP6yWdBUjlLjpq*`|zt9F#exI|9=<9CM2u%lP z3p=oI;GerfpICx_222(d6bXWW1AIV{5b)RWz+bm;adn2;u{cK@9%qiTw*prdwoqrN z7aq8R&=^o7&J}Ox0A0FR*f|2956oe2j>F=xt^{-K3XN}z0l^OxVL{WZ&kbGKVXW*S z9R>E!$rZy7zUf2vE$ng75;4%AcDVHmH_(Uyt|%p>IN%rvI$Jn8TUc8F_t=?0OG2ZYgRQG0SP=A-+dAC@E%12g(j0ilf3g5- zv2X>yEM2W|7-;wQ(DK2jBM3GzfDOUP7oGm?rQ4@jSgrCemhuN0_H;3AeggoUhik;U`t@MAZDHP zAdUfR1fe;Rp00LaN$W?l8DRJN0jNum2m*N>K}hTf4eLUnJQO%UQ`y-QE^&AQQS8Ci zASPOXSP+t#bvgWlYy|$Bo4evM_Aa%26X;JIh$zBfF@z@}HG{ZK5L*zmgT**Nzt?5JnV>d= z-E0Ugv_QPY`fYm)yg3exw7!*fS^_n9hV}${-~oZ4?gaTDDkr#Hp9;FMVQ`2`+fbdq zYj>b=)=iH&v~HpT5kz_OHth+rZtn_&Vtoz36oKwQkD;g5%@IUbf<0R241sKa&l*8X zUN=Y3H!!dTpwAyJpuHI$@*jWlCO|+1Ihc455(87I=Kwk>@ri<#e-J5eIDWPkmK%;N z5o!?vfDXa2RmRxiY@s7b)xy@z!Uc#GbYoM54X6OZa5S)m4mK7WVK=eyKXPtv1kM}h z=0*g*$#0Pm{MEVn`}r*rM1uY4w;Z>y1y&qbKw%&boBVtHIh>s><`PzE@*Xke0Tv6>hU2O!oW zN_ZEHxy{BE+SEPwK`isk(3UG2slA_&gQ10 z7zk+jv!f=4fY}mO1I`VMBOYf)0F9s)JB+gx&K~XH;^JUOeCi)41hn%F4G;$-{GcHJ z5qbW>4;0z(%Qwlh_=b#s=Lc>G`S0Tg62Z_-exMpKi$Iczz}qH){@#QUx%Eq<@&ix6 zkqKEPTNE*fwZmbtkOXX4LrsEhQ-Zijw2#M$uq+^~2-*QWZ)r;aV6DNE79b8rJ30U# zhXBKgqB%v;z;-AK@{4kcDnb5?C>kt&;~F$U-JOBD*n`DlAm2zt$pQ%FIp{}P6Ijr4Os;qjgr!lBY$`_n>?N0&$wmSg<8TFhBpljJAHx3K0eLuW0Mng-n0TgZ-}&*8k#g>t`sI zD6D^lTmPD{{%6Cjf7iVJ7sp#agRMV#vVVbCKh}r-*O=1(-hgYvr``kvxI-247=X4z zseORn+Yst94A#M6kl(g-Ft;JpeEblJZD9SH7&8l79S4xaU9WvNBc^!Je-u+g&^{J} zw+1t;r;C2b8f+>T-WYTvf#3qAPl=Jt^p z;LP#-c$^(5^j8DP?>`>j1TFm;D6S`I!9aWvr2b=4Vp9__a{&&~jg&96)c?EMW#Nqk z5)s%J{YCBa1~WG&o;KQkpBP{GXR!1;kyEm8!-3*)LM{TT+W2*jbwe=zk~ZNDf`X!; zKVAeN(D;84$Pg>#f0DM1yyvFadqc8*b7>>SFPjY9PbcsHf-L<}o%_FMyX^m{Fz3gc zJA?c=8UvO8{%w@Y4C4&UF0fsIGn+C%pjZUtMgbUx#adc^KLB)#c*tUL&Y*y4-D2B= zoGEx|=08$tEV&_B8wtw|Q1Ty0n)rtJ{XT|GxrCR&H$(YRy_+^@Yo%jC(gZp1d z_Kli?O(xX9s>C+agMGh|Mfy1>|Ce+0mvi)& zbM$}l9?`FzqyMR~Av#Br{6rQ0JKm9?AU`n(+I(>QC;TH3@J^cY-}tLAgnQrp0iMmL zBjN7GpZrM_c0F zi*(A$zSn*zy3=x$Twi-6vwi90QsUG_M5XWQl6jOLY(J8eTvdUL`pTbQztWH;D>i4D_CO2_W8@Bb<>J!q_0zVCKMIo6=2l4Ik?x%&D$Nggplq%yN-#5?f zt*n5$+bH$1U;X<9t#{taX7{UR@@WRrt+(Wc920ncUWgxzHZbp3&HLY2fm@3Ys_e3U zMFJhfyKTRq%wHS(K^*?#*muJ9mneRxOn>RvcjoUetN8sv^Oy1X%Xs*~YT)S9UwQ-7 zUs=hL=N)v{rn=wB^F2wI)h0|X02_Swmv_59I=NQnVGW{x`;#0Y`e4sZM6G^M_xFaSEZ zMm#*pLI#`~w(=>xpSqNjK#xY<+@FGMy4Goy{^J~`(Fia*C$nUG{4M0+_VZ1%-)kM1 zL;&*6(z~+x&B1IqiXZoZ^lyMVFr9uR0|Pr&^c%SMa~D$RQFd81;IVNPJy%!z!8I%A zW=laHR+n!k70>~1TQZ3NJKjZlGh@R!9Ty^7TWItpTMarw6gL00WWRtb zH{M(W>ph=)`av{7gz^+VF?P=@7pyK0*(B#oztad|bSTc5ok&|KmtXFo#Ju2)vAuV( zY;9$%O8?}UJ-a18UvaD?=L+O|(rQ+}Fp%u)Q?NYws_I_GO{so|(+(xB607rlRfbkc zdz!)3jnOX^>)i2ws{l58c)FL{_CmlL?qR)z{g7NY!XG?tkm~7AaA3LrVQqOpzDjzm znC3vDW97K~+LXNcN{88HzPc!sfZqPhFsO`IGUMsWsHLd3&*G)LSLDDMLG9K4khJWob zko1%z01>BonKxH;{xg?+o2YV^>N<1Y@B>jEVIOpDR|oujC{YqiV2(>HN$-odDy+<` z`loQ|*DD7zlI420va#fxvdjNqcBes0dW80dp=Dfzj*`o>&sQ}nsZ^M#L^4@ChjRp{ z>P0Bk5ob?SXzf7nacriXNXt?g2mzn_M5%XpAd3QOKQ-3z{G3f zIVN1R8J=MO3@7m=S{MCN?AgeZ4zfrCHdWcB;m19fO_t5PxON+=9!xt>)><;x#kgDQ zYl`FhqvAm=Ccd(k?;JZzX4`o*r3P=C?_QZoUSn!LW%E#>dDoZwCM#!YgT-1+FWhO9 zeAwIHl{a;=Y*F9++r5n5!JT}0It7z2c@lQ;xC#f(wwg!gurTz=qQiM{G-I@x=R@9X zr87bdZN2Ziw$zPz(q_3c!S0D+t7U>+&C6}6Wf3xyb?)^r8cl@QN5`r<>j=qy>pda0 z8tWG0nHFHjP+bXz8SuxI+arv?7K-hp0c(J@T+5R&WfXHC@!Ah6haWex97#DU*7n(a zY}?BrgPA(|v07C9LcNt7&FbvavLpU%0ybHNmb5qBCk_jwXtgZ1x0LR6za_kuhm>i7LgcZ@0kzTfY#ds3zw;c7DrmjR$aYNGx$bXy8DegpHl_b?Pat|bUJ1E znVhKg_P_*w+h#HLRv0MVjVORhwUKSXc36Y$^Q%r%#3Jwig7!Pc3 z*5je9qHV&F+}iRhF0@&zGmm_OL>gs8SQJs8!64q1{@YpTO93H*0Uz$*PS^EH_Q#r= z)$E!<38G9<^T@{;nMmF(SBvC6;)dJ9&tU09Up4Q$`+BvAuwT^_$mmL` zwb)R`gY!G(tw-uQ9YRS`Yr)?3+=(^2#BGMO)RZ2{#6FS7jxD6GHDpcsW*CCxTV#cFRt`;no?C)ip*0^~c*} zjeoj8xvli&^R<|lo?|6+TRFc)3*ScEvrKfV>6HxS!M0?VPUQ=96+V7>=)TgihxUWP zNF`-NWcNfJJ%gbK+foRfv9|Dw{X!S+ADRlO9YF-U$Iu$;FxurEt3RGgJc-U<1cpOk zCfJohQ30iDzJ9D3b6nYp?q2Ge>s`X0^2VM;qC$CT0-0E}&-Vn;_4cXL2JWclE=>`w zq7bPrOShT$x+hXW^hWxPQ-^?M;_Hd@T{f4IzsQU&99PvfSBq3<+sn4^`VIzVtl&gz zOXs<6W=fkQ2=<*9hn_A<=x1zY%W76d9yl{tmKU@|ll`xg{ z9lyX@O(!CL100QVPm*fiA8)3L3>(RP#eaQlRjsAx*x{}smk;_UpR3f=akJ}&##sA~ z;*%M(jg$5Ux_ivVlr4WD9jYM3$~-LSy=2a;abwSzA(~s!C9Pig6)&ci`CM3J*ing| z!2GNc)P{~h{?K0B*n4V7#~eMf*JUe;Wb7--y2o3>E9jE&Sn-rH#(HI@<yZ$aI$9a8JD+_RG&1=izXeuDc1&xpS#H=xw_qtM$E; z?!ON}7|Ir#U*u(D>9~JrdS-d5k(IK?2FI1`0mSQ@$l&d3HIIr*z+so)<&Za4x)cm7DDRHb zeUb7$pMwz5#>%qv{5Oft+t6ONK{K^mk;ph|HaZO<$7Voi7M z07SCjK>7!`pwT&Cdv|3dI=?iC(F{P0QTEEBJv?UHton9d@gNn8JfD+w^+Uk?RbLNY z`H%-lIeFjlitV)&baE|Pr*D|}EX*6U0MeS9TIH71-ANr6wljvm-dXZu^u^9p$~Xg=E@f|zviD>=c@YbfatD3hNheE@ zy}V3xBHJ~=>GxWp#K;pkm-!)SCkbA%|0p%`@n>L6xl48L+6N;3pQWMznz(&(pVB z$uG6}#a8;Nez^ThqJm0Ebf4=Lny7+k-fyOYcRl zV7{jxg?(fr9tZ^Pf4MYP#=0=xYP`yy!DwH=#+y@3jVYd}u~C^S2`1X#`@sG_yVQ~J zfinfE7ZY7ehzQD6*>z4LQjda%FcN)XB4&3eO(xEI(H(uZ*Mze#>i+Y+r+F`_ig$bA zy#_^0CZ5grUhd->&_&Io;3#Y_m7vnqGH}WqtL~Lhy0Yhl!7Z++-6s(2kF9cF9N0Nl zGH2|yp8^%q_)?rE&{|>)*Sn%zeAugED;ZTOG<)BH*k2?on0seGk@@+1fL4xl^d$v(_jskK?>2P^&_bYawKL*9vyq9Ac zV;>4pd9NOTsO6}h@JU-5EfOeI^;{gxh>W~akytyvp_jjO;T6dEsOA@2%2-(U^u2v( z$#75&!9KK|w2u9cSmtl+NDpYMJn zgcz}G1}bzy$|*$Z5wwBdu1v;eLBP-K+Hp7}`>qkynl z@1H%z+GL2jODWZMU;|{>Ks?H`flBNXo9-&O1R#c4wjZaU{>lW5SwqguOGPA5OPKr3 zLqBz)2YPqqz`O6!iA@v;qXzPocAG=xv*FhEW}=W;XW95+q*Du5fV+~oLF-HJbB7WCct{$ezOqc{;Affk~l=J*sY zA>?*BZ|-~WDfqIVQ=?iQX%qey5cC$vcr?&Rs9N zzBdE(T4Xh{RchUyJaF6=S+(+uIX}Z;cKb~~zE_Sl&a?pJVzh@Ahjw@&ckX)#6`-A9hXCya23tX! z8qD?+V5z_eXqrcLC+2J%h%jE&$=qUuCDIq6+B`Rj1o&E|R$gEwG3e<4@~freJtMxgQMQDV&j z$SGDieVh+KE@v6yxLhs(MvHTMQhq1arV@Y-6kOh850STb0pyCS=1=5mW6r!NKPPQQ z%=vpafEmvZtEYK}0*q#iF8Llu7EA%GQe4E)B^m!En+Tb&Ab2< zR2fq`oj$(0G!~-mGkLAkTK%Eta^u>vw)=cfNY&x@nz}>t_AJw7YfEK4W4>#i^Irsf z4U0!uSI0xLXPQrk$OE|4?ooTA@9I=te>IIZMU3%z+&lyf&VqR2Nq5G#fGXE+H_=@C zl_8dvtn=P|jNNb$QF!MqfyV`w0o?Ggc=~DS%tQB&!Ho8;(*6PLDn$hZ#Yhrk+mon<)%F|(kXPAx7u;{SSMGr_Gxq4yLO1sm+DVZ&R}G`0pKHW0u!|71OawS+!FjaPdKu< zC$W*Usx8a7=v;$^M1K+#m_lJ5>y#&isbp7EYXo+>b?{MGGU+*uo{*jxG)NdL8Y@M) zdM4$^?xf7SL;0$zx%Nf9dl}zg zVM(-ss}qsFfe^yhv(scA?NTZ?|LKAw2vk}`Zi_LC8qL}1&e-6G+@!A|m5_6lI|ZtR zbo6g}juwu$HTsCH9_9h=a+hDri;;V0yb{iEQJ$DQPs3dvCw49u5aweUAk0v^4c_qJ zr_mGgO<$VbKkAI+HBox_o(#{BLLbrZ*jDxQ>?(s=fDX@;HFLub^lLu80b!;oEoPLp zU|-5vBiH*$ihBaTJ$RmnPZ9Obo*Gb?vf2hgxB<26-)@T8yvzZDZU;*fHwE5&6+=P+HTKi+1e^S_v1eN zE54_JRyREkcmdh&nS3R%*4nTD0p|eTOmJKpd0N|P-~(^ES8MmQbFW*cYs>g-heL?A z_cz@hF8KvD1CKA)75U-%PW$0Eu~*e9A@C6bPU{X_Kfd%%YhmwcTPlVsi;N_<9rE)P zlr_C|S|1_s-%)8^aJm>Dy~G-X(=2xYitK~XtNC*g-d}Hp^tzTVb&e^sfEdC7 zLS_402NiRr9m_LE6HfMmbU|eC3zv%EuN=Y|rj+lwZo0EKCsWY1N0vCstQ=)61rOwO zVXs8HM}Lz9L3gm_Tr${YO{VLTZ`Ns3#Cq%B4*SS)wX=KI#w(MMArFZQp+0o*u28la{ zJaMmlmO8p?Tsh|UZ11xA5g2NByOFS+rWE`kDg!<~YWp`*L2;GekBCd``j{Vwl3!kA}j}((z zE5@k;((652d#F|HZRf5u0$5iJBHt49V4&`y*f%fMwMiR};u39li8 zqm6V6AhB}zW@EksV5%P?fE#s4rUKE>^j=$@`7wnfb!H+{mWJtN{DlkEg|ISg$CYNy z@*@e80aHP`ns1anfs*FTyYD3Jh9E;nkSuuacfl=ZDabL<@p?e;p$Lg#2?&c<#!3U` zMHY%ah|d?G3{#hWB%}NUU}80LpW-D_VVq6jQ{-Z%Qayn*%A}^RMO(sFUUZu(_+3Qc z=Q_{Tt{#vmzqg-WY9#OdS*upZhg*yf z3PKKVwPGN>mRmKW1Hlb*q%ElX~?HRJAVWIosZ+%;g zJ?&`q=^J4ojJDY*T17kx?kNNCdUK>;ixK`gGR%*{vlML7%&%&VW1IL6v_$F zpot3)g~Thu<&My{driHzAxDS$PqX6p+j)GBQmGs*8e>_C5jx)>IiM6AroZZaUH`;I ze__-+074r+7|6VzwU3p&6r>?jLnB_BGQ#x3Bg4L|2rOwsj65KM-nXx+{GRZ<3=`_NDZIx4~JiQflw&j}@_& zXZluO!){5<1)Vu{IqsGE$cwGFq+0BzlT>tRKin&@IxLjSw<+r`B`S1+9F4U=L?gU( z;P0GrofN179qZvbj?W-iPNnw19wu40lOX$gCmU#7wI*W6?omiA`&Xo43b2E`ULG&# zJoZ{72I(0j8s5G~SxGk6A3_on=I;+<`}R6Z8b!TZ{nV%TMW^cw>UK)c$S=N)H1kub zAji~(neLQp;@tC%gd?D7KR8FLF(H(@wahCD&o-Az1t413-(-Ztjp2tRk_7v_=#kSp z?;fwf%|bmwsfMcoaOo`6=U;v^KrvFp4anmM>cG0?3atX)Reo;R&H$qTrQD<3p}S$q z<3>u}rQC=f_K2(wd~|oo>7L!>=z!Enzrd71;{6ITZHat!QB2 zxhUk@ZC^AZcK{wW7!BjBA$t1u6-pk)mnz}Uhk`xr?dVl&`}?`2OlI>9vI8MFZk0rtY)->`71+Z;QtoeACbrVL&< zUYqq*oIYFRW;IiF*)&VeaoIr?`r8ZFo(TuXRcQnu+=K~gk;hnc^vUaGwW90`&>_Xz zoTFQfG!XTiJMnFMjq(frckb-hQma9Z>~7mUigqL#Ssi~eeyHanQbkN4NFli}S~4Zi z?KSX*-u4$9NI$DtC4D`!5AKsdj;;^lYzSfXac3F}>(i^dgiSKNwgVMfYI{yPR%B=q z97Umz#tlGGLG1&F2c&8UbQ218$28`_&QRm@p>Wdiwbcw4BM}sw^rk4?P1=4lTeB1j zHnFm=tX?EF(!G0ENxgLr!6UI$Z%d1_Pe6H0A^fAmyEBJF7H-(&7NRlyer-9mEYdfS zdQZD2pIvXixOT}jy7`P3cXPjGfKJj-3V)x5t!#l?7xz#Fqpw_!Rr%(VbPoOI zzT9@bTtd=HkB9K~B%pES9ev zR5`G%!rd8NID4*iDrp`7!>Kw?Wfb9EgPeIr$=y`6u@~MIWoC1%K0Jn41ZVobrG%vn z^f>yHL^K6kR*>x5S&&1KYxV$^GqO!Oy>NSxy_h|arq>Z^zOgp(ZG%B{#u9hDu~NA< z4T*syT9^o>;)>F12!F)!T-FA*aWnqDV={q^i$W6_^1x^t^vA0qpkj?nGg$jub{+`D z1jJc@y$3WYjNB6mIj%A+*16q3EayC^vY~Ccp6S%Y4Htc^E5@DeU~s20zP!Mk*L0x% zEoV{P)?3u#WRk-C$KPC*ooqcAHnrNjx-gcm5L@`nr9_GWjrEJ}Kkt6kwX!t>nN=Pf ze)(N{kiwvfd;#Y{XurFJ5=j zkbaJH6svmeMVxRR!lJ;QI`G~6t)nlvP@lNZID`qU&G}|F&sqi`80G6#a9td_p}V+; z^YiXZz(wUK;v!o4+K$4zLYMRfgj7;$wR;PS9r=X)sBAR)QeOIGcYL&Z;T`KWm91|= z!=LWfJHU3E)86)ypcWvl>u3iz{CfMfooLsr`$$?dUO!*cCdjRRDbz-sYD4`O z9)WzRN83@)qqGeb)evSMk%`or6L5N4gecYGHtN7TH_v}_H|gIJARM6C#e74!@Oas^ ztw(~%4Y646w*uH6C!_QAX%kU7(*;O>*w|?_?<L$Kv+U)=?lX@Lm*IWR zhak*v9d4l2_0%&c))wRo4=hd31fP`B0nqDX?;LFuSn zuo|hU{p>FB8U^-69mh@r%3Uv*vSt+XXJ$Ve#azYlroM6EjvhZ;=v;JGOozsY_AX5X zTEbk`v_2=U>Z3P^`c*vn9^&es*GtR1Q*P$4k;9X>qzZi9lJ#MJp5BggDL$MVhTIX^ zBtmI6@W5u>1A3l>2pqRlAGs}n$Tsk#AF%zJJ<9{n=X<~~j!wGdxve;7yXvPh0O%gR zaC85gdl!W);5jylbOCN;9!!G5IJh#x1CAz#<58hg2K7$MqmMxUqQ?u~;pxmg) z5B3IDKP=YwanhP%H=VQ?%oJ7tdm1XxM2fi-#5e#$UF<9dNk0imI6a&aE8A1vOW`D& zo2F{0GM~EpcG0QjWCtCQCL?m_m-75` zI_IrPyc_r>q@UFr!YIZq=$=fsc|;tMKx%}gP~}8Syf>gfz05e${|u&s^@;Akndy{Z zQuM9hYSNt;&y1lrzQy;qMGU6DgFRf(NT=GbK1Ge9p>XGov@wqi8w8dWc1M3^|JRDu zyE~s$mC&C9p|*!S@|!*+($#6|t?(x*Vq|Votqa*&!3eiYvVx6Bm&sOUx99t=1WF2T z*O>CS>2&gWc$j+N=>YS7ezJal*sVkX_T$FtEj+Ks3X#?Ay1=2)mKS-H&Cfo~20sR5 zdD$6t_pGLP#M^hRkIt&>v4ZXN+TFZ6zM5l)?Ck}38CTX_Pa9+uF38;l}heux&T5G~RnmM)F)@$Dq-sm+o zILGs-SlH)@XLfpvF+5K0ef>b*P~M{`#nrfh3gLVBF7eWda0mdeZTvE6BA3yzlk>w6 z1ZL()d6gzMHciFN+=_M!e=XH#Ex7WmQEn*^D%A3tO6Y#jS)bUg(;?n8WpnbGHsx~n z+DiBMT-lmzsnJuDSLrdv`%LOf2X1C*9=68gtT>iZ9hSQYKq{=5AtQEoqgN%FAm=>mVLNrt5`Cp)4+PrsZpo> zYhekp|6rIa8bQx5K5u&*|h5Tc4eQfb037sB_)k=k&H#amKqKvAd3LVhN|0|fwDw9|=s z6w6TW&Rk#oCQHmk1JrY^-Etf}g5n;4;M?cLbU6 zFEO>xv_(JY^?=0nN^69vzO+Np$dkHZQ2iOvGXFx`hx&xE$CvVfLZ}io_AM%zuXMf_ zs$#t~BDwJSS``F@`pgw|Z!Lhz6MI0e1rF++RY18Mk6DTC9k@R~s^Z)IM=XR=b*NOR z(R&n+0ehwZ#C9kEh@@OiMw1DIu0I$k9JtANE@J^UdQ`lV))JKBW`p}R4>$pYKY6`? zLkueYN|AZgEj9Fj=1Ke6`vpTXP+EZ5t(C1=d_-(zb*_x{zIUVinpZ0T_bMR_-WXIu zw~j&pjxomo36I-jaZp(7_)d-M2qOf@r5-J}12Fylyr)d)G@6+0DFE^SJLPB&DP_m| zGh^r*0H&RD#_QC|3$Em_852qVh1r$4N=}>20F4_Fwz0QJsST9 z-0dpz;x~-maEH?c)B+77qz0UdO}B*UE@pg_3wtiwY`|dzid%a?DdJ@YBTD&YM|Tj? z^l?Q|sW5a*bBet)$QEx`NWduhnqOOxsJsJJXIs;RUEqk2V4rn8NRuMOai~pl#E%jW zUuo#pz4tA{k^$X12I}P*Tb=sX76MlSX|(9=?z{Nb7{(W6lh$y$)lmkm-n7kqQG-_U zJ{G|HnX@hxuFQ7ywf1h03h=orEw`(YlQo*FC;Dam0Be6TvtYg_I+jS2R7itN?e+5unx{k ztn2CFxhCdE^IMx1K#+cU#t5pg7o)K;HJX`buwDdDt=-RCLxrdCG8_;9GI^&| z&Adq6h6}#{0GGW3`dP1xBF^LrMa|jZj+$xJJT325T~&E@?&jd}FN&XC?TT4ePMk{U zdGg{wg3{gxM_Lz^8SQAjGoK~irL(izWg4CUy(d5)>(-7-^UZn1Ula%m&qQWf+iY9; zM$Ecmy=U9Z<)M<_w;3GwAD?{?lhtC-4YF_?i&~U&^QN&NQDW9He(&x{<99Mgnbl`r zL_V5y(pX7YqD~hg5m_W<2^$0%&4cZ4jPS2#au&rIO&-*SkMO=$bnVcUDl-h$No%Zm z_TDC4*Gcy8KjVP%-XKt|Ewl|84aK@lj^(66^OzhC9R&*ZSi^Ij zu4z+_t(cG?P!B1$3?k?qFnNjk&_jtadu;+FvM#p@0t8y6A_x!P?Wn98qSA!a;ZQ{~ z-yE704{#t0KF*f%g(*O6uU^(sh07B9DhPnH`OrdDsO4JZ4Xl?rcS9L2`DVU*R*&6! z>u9hy!CN5EdnsWkv7+?6U0hU71E8ohvY{D*hzqY=kG6V)EI>$ng7qZ!&L5@;JP>c4 z>K+afoYE}X$>k--5S4?6X6PCqG?xxh0gCODk8F=rDR%5H;3wOqHE}D1W#2*)V8b!Q z%b}2Lg)5iGj|A62uc0ujGwB3S`b^ID+~ouC&Rfe*exWGj1)6Bq7%~_cvl7j)Eyy*6 z-Ru3ha{$40AM4-|l79$J^|@-2u5A8Das8bH`+Xzy{uj<;65wgAGP5;(EcYMesz0C% z_zK`+2D^L)I+#MBcx<16z~g#h45*`x*zI6^_3=gelluf1US(livx1 zarUdg4CX(`mL2xlH*mM2m38H%i~^GU^e{=!1<6Y%@OoP|Y5{yc#kXoE_0e~y7hZWn zQ%v)bz2*&NE6<9K#Z$?&iYJfaI*&(M;zj)JaHIQM)58T*`Rdc)9r^(j>?d%%lwyk9 zLKv1iyJ4}Ms6%uBSQN3UKY+Sds!w)S&e*VQ@IH9!h`o4ib?KOb0&V5&)YYBn@TvD8 zop+KpFq^rikN0ssbxYuJ-!EiMGc6`2M$(4YsP?z225{I23<=s`dS5b~Jauh+m(R+O zNv%}Q&7Qv7@3`R~42xp>riPrfaCVkP!jD8$viCL$)iRMD+lxx&=W0+X9*|K=0mI2> zIX&0~-&g;|i0&(vXV*E%%zE=vfw?SX6wk;HH)Ay)}WYW^^~zmRzhHX zW__Fy;?M`Kr-fG|FRxr*j9FEU4AUn^&qvLhWt&CRuD&p>oIM__`(gfNhvAp1FK)8* z$;_VGBWmW^Z>@%&Bku!s6nOGMYK8?;#~~Vl7v3V9yz$uEUIBc6Fl z4|*MnKAUF$<2ou$9mfcX5Cx$|gjsFe)=1wf95NRBcECMRDp6oZ1ZR<}M4JS1xcG{! z-XZLED(h~LYn(u|D~v~k>Zu^uTdUr387R${N1XvVY@j-s^Sv@}Bd3oVt2b;ds-Bizc`b{mb95)CjweS; z9Gl)AarRK?RW(zdw$sKjC$_RRq*IbpSTkw~_#f)sIg+{pm$*0PXiD|MdS@?5obl@d zq#q_yVhDboHcRa^y|l-RrIN$Oj#@8{SNdnXk%!%b@#@drvJZPa{Z(~e0*n`aqLoX& z)&B9Eg`>^pvmK+KF5=UOpw6h5k-2mZO{PYUrkDQkBkVJy0y9tTCz`vwe_r;B&7iqhtLok=KWH z%jYl`0@2cOEr@{F?eXER-*lg`45=G;C!Riox~rF5tgSA`KgNA%MgWZg@bhcx<`BJc z%JGAy(ejfg_L+D{54F#jdbu?tx891}rN4&sSBL6H!TYOUqmfGHPRa;f_-BJR2Ccq} z?~`kiB4qG>M|L^+rzD-Jo4(qy>@iL2a`jlb+0k!8gP(`_b4WN^56hK-3RKDb9yl*JZhl`uz)wb;* zS>&3laF@<(Z=1$)d88(_QYt``-LAdkFoV5sVzi_cj=U=#fFLzK9|Ma!$kf6O*Po$# zqWA>njS40wgBRsUoV~4^Q9I?>OCIlT1oYwq<%wyY*()&gVscnOG;>oyN1#HvtTkoC zp-9e(V1>*$WT5WCBi|LC*<*)p3r@|m$Ek99zFNBMM_Wspl;sHxY5=DpJW2bs9=N=h*UYHJaAI z6${Ey#lUYLlt{$7Pt;NR8w|?9(bx*k94 zfPt^dc;6|g!odr+t4M$x%_;qeIxutb9$0T^jBWTLv(J2wlf7!(!Phm0eH`YF8s_{K zDwC~7#+fygwNb7LpXxqdvCSP_NT0Y?=2d){e|v6Ou?e|qtmk{9W^Q=;ssM9spKy91 zLK+o&fp6=Qg%`Eai8ed4rij@uZm(J$#Kc8v#ZC{=U&}`CH$8J3;m*;U;Sn~g36cO# z5vHZ)iMg}UHIq6pwtkiBGl62TE&XpT=Nd(zv*M|u7=!xELlI-i;Me~v4;&?k-lJ#r|1AC5bZCAV-iD=l6? zOTQn3qhHrZl^!2FZ~OJj3^PcPWHuVm%=r4^`ulnZZY;)7+}ocLT|-hSIip=_&>o9r zdNZJg$lPZFiYZ#MaHsFf6)~X0_x6lY)5iiVr94c=j2`ZC9AVbOF>@mTUtdIx*ySko zLfQ0bC^M(q)wYFl4|-Ju`vtOEQnrAgtF<($_c+HN@xXiPsp@j3V(Hvm@6xm=Ynq9N zC|oR$FDBO{X)S9w)>uzO0un#5@ zk=X5QJ~=ys4E!H!_$imM>wPMinx~_9vDfASt3Sf7`o*nXpC2y1X+irDZq$qm8$Tt4}5u2n10D716kdA@O4mqq~PbW`Y)lq^)}Or*E^p)_BP_lrz^4+6W7gn zcPb<{WJEB+a4B}KZpSSFLoVpmm{{i8kBa?>DXtNo@Gk%_kT0#`xfovW03Tl{O23PL z9HahtHFtM;^tsd5N0VV}&F$~z>se8^w9*%wnIB=>at*5wP>mU~ZwVPDkNupkjyRN` zaSV}pYH0GDZI45AvsQpZ^FCV|wo4vyrRK8W)gymz+|f$$ueXtUK;|1|YF-9YdDkyj ze!LudZ71);c}F!wr9io*!D)l{Ih-sP`<>jEv#ydTdIQrh5tCIbb&NU?yjxNItgVQ8 zN~Pu-D8lnH659(R5P9Jf`?kQ*^ZMBp;*|pz7*{Qg*D@!rvz~0pV$goLIPBp$`6%p5 zX{%tM=$5YBYfg=R;&XnbT%f{s?!cP_5-C}s-g#=79FeO##`-Z_7d`v3S@88j6DwOe zZ^5ht`I==W6~-g!mD5O-M1clTBC_fwpFJ<g-~OINylner1gc#$Vqb-{ z^c|Vh=ia88De9(?rdO=sIrqb+UM`rdd^>4w;>b)!Np{SqE}(Kkq3&!yx9E^SR*>~` zEoRcKS^{J6Pi?*dDbvn&?S6H^*4K;=CACB*S;II2*mD&S1$RwGKMjoZr6=blBps1J zeCjP==l)XL({!N6FXfV(vv;*X?%8)5^+UtC?8Tp|E4TTRTkVqOwScpc^}0|`o11^U zOcH*k=vjcy#hne3z0Ae%;2cG!s_Cn}SSz(R3ZaL>=~#*s>dh$xZ`QdeWSc(FhZF#_ z1^WU~aEz7g6w_Rd@D*g>!*LeA0McBJg1xg!Yd3@gGlvrkgKgf9!3*!LhX-=Sjw}^d zg%sD!I$BX5RzR`MQjjRaWwrgb^={>s@!_ofuxe4`&*8g-jTvfTnhP8;=~E28*tgL& z+FjZga?0FxpzHHNJWv?tvAe^91zEIp;sO-92e+3RA-}6FFE2?1Y8PtZ$W@@9Ht-Fi*%=*obn`7dPM&_f{UgKHaZjN_<_qBR#xOCzqm3B-2R|ppG_cx@IpDjx5*^PDfD3O3)^nd{2Lgr(t z_TVd!xP2G;Yokhzl+JN_YQvFFKz+Z2s`992#~>_u_>7~5Z2PlX!e3Lfoh1FY#vgb) zp0IcLcrvx$dTJ5)b^VlM|KH=Ve-`zCEcU5Q=XTIB(tfsA$T#V>4(|s5kI-%pOIHsg z3fw)R8`>Y4Q8zUHxK#l}#Ruj`*-EBF%tU|MvCR6C@`DYdbdbuoGT|9|0CaGx*0rwx zJ4$cI2Xyeuzw77z(+Sf0|7*nm$j*M&yN9TR!GVTGYC{9^f@dsY6F`w}2C>|Qgnhm2 z&-ABLbYY8+LMTQjL2UeZ3IrK1t$g}sP2^lyH0Y1h6{MU4SOE$g~F|6!EY48RCJVO*n z@=4hN&6(?;Jcai(F-z`3Y&6)z;z7JgG4314wy`V=ME>G75or7L#^nI zQzmx3*=!UD?#-ZF_b}l6`)+BFr4FZ3d+fvE38O|!MJ~${{GARSikPw`*JgRYg*0}3 zr5$y9r-K|5sa|0trpW3?Q!~OuP3R_Uo(RJDy6bsz$}*h`C$$r&k}ogmN!33i29>8nKFXpv{CIb=zy3$l z24Ut)`x9Yd0MNiXO?v>6WehpsNl?9Bvhig5hfDymiH(Dex-)t-p6+b|Xou<E+<$$u61SI%Fc>ir2s?Klqb7>Fu!+`_cw7q{yF_vCwst_Ve~ef(V7 zL+coyuvKiEef>iKpxB(@J%|4V+X&$(k`{3TGkmqITA&la7^a9qSgGGg_>482$ueyB z&qF$5uo_3)Jkq_|jIFBGi|=A_AS=!Mgnzf0luj86gt(#$A6adJWh5$pYfgd6{mW8f z{PsZUG-F?8VFJ9#DX=K0iS_N-eWGa*Ow3>Jdz$hRiuB9ta`pB-FFzfw$s#7*i6swz z!=vfnT6FQ|3aeg3V*{&t!UeLNc7UQ0H%-Zf-2pLmHHq(w@2-6d0)~M2_LItRAbVCX z1se%Nh(rN9Y%2>jk%)uH^#DD9BvE$T0q(4KcL}Uj-$1S>3eXn56@TiL0=mDJL-Lm- z15lrLaU%%OGKa*ErQ?ys=jBBud&Ks`)Y})dFK19kyvP>%Uf*r*hj`JMB-J)rEag1! z$SFXI;J4Z=(^&C07KkE0_RN7}4T#fQuNZRsCV=N~;zEqNhuU&IwZgmpsoMi8Xd8AD zWRc~o;uVtv(^p?_-8yBX?hM$}FSsscUlf41RNZ(m9FkMx)y^N?(y7PR1Dv4VTkXWX z0YdU$mXwYD#U6=Tc~CzL(=V&ZC;bSF8Nd(1zHj^MlL6O5$Dn@hhXi|FTd_ ztbE+onHDEMnETKsL_Xo_;Df~sfc5X zOlA>yClH{bd0+^-N{&z4*u#kGZy79HrA^^jsr!t`$%rJuATf$8_5B7)PbAh)PJ#Uv zZy=p853oASM4FD+_+?Y5y^Pi*pV*J^_ucR%2TuCZvdX?YGS!{b2I2|SO;;lrQ{F+Re0DA+%)RxpNWHgoaOh0CXjBJiL`6JJD z>K-$pyfSX*wTVE?kXImu{+6Yw{3xwkd`$==qJ=~N!ad?G>1R{&yuMQqQ9Z;~PkrbW zX}Qw!Ywop$OeiC`TOjx?;F$aw7=(%>g3BUdyeD{G zuD6@&J(cZFHlYNmOxPpJ>&72iLfI%2mA7vbz>T-qLU@>~J8`&b0i6 zo8uHwZ<#GTVIJk9eF;IX)-ALpWO+`$iPEo09+)+!!uUL-!w{rogAZn|es&(4d3t;k zs`+01nSj47x9w^#WkIOcro?a)aOdEI2b1u5vt^2)c~ksF@9U@PoX&3?UweTR^3A+g zHMGVrbm*2ogPW^#84cMw@x1P?P~uGva;NFGS7cvS?D!eXwf(!kodZrPpM zjcGwX&-n3~y&epkuXgf{2s6pt7@q7HQaH&k33&fN$tl}e#g50$T|mq@>WwLGLZDu(W34z67w91=53PYcPToc~0_>{o6 z!<(g68Vw9JC(J{CA+E*`!+{-jql=%*J3wGBLv0Z;=skbyL}cC?X2>Rbz<*Gxbq&O~ zq8>^0Y|sG_^R%}|uq+Lz^yaBvwgjxf++4a%LbqHut&%d?`3c|Oa*H3dk184~2(O+z zFW1)K(t!Tp@1btPUXX84aCqwnp_wI{o-_nhh~8pnoh$W%q)7C>%lppo`>e7|q5txE zZx*FT%;0Fbm7>T#R;n?NB<$d(b1-(k-xU+yKG&RVmLU_8C>x*@jfrFktvT2^TKTL) z$(bk2jJnKqmBg5g>=G?`t;Yic|9qJtdX!nY`DRMRoiSIPe7+VEI>WQ<21iziD5KMZ z2xvDYjw>?sCI*pS@<8Lmv#=vUDYy%nSFse>OSJPY)5~s zOPX^y96}|$ZOFdgqU5gCP=06I_K!W}4@lg-b*ST`;CpEV!4G$c_7}Gl4NoYt8nfXE zilghy@Rt%VeuPT00hznW0WCziBg*4*_cRG5gFuLyV-K$f;6I zPuR+$APIVIH$vdA_*!mQXDV#R2?bNMIL?#JGtA$Ze_xk+LOWvq=o~xd*6TO2q9JOt{PS zfj$sO|FFA#at4b01H7lo(~pLWV?eU`)K&kgu$KY3Gfi4#4e>)rB~4K`ykM?Z~DAZKG!t1soopT~C zL0}=tN8`NE5C5v;rVc}yx3}Dqkm{)mBytw)|3XW2F*sqQ?ZXuPH z4NM;_>$Sj0gtP^g4erR9KO#^|i51pYW~<(j`rJ*P_kB(BYjof&Ou8^7>!e3L0?O{> zkV=nX_}k9|?kIHKHdo^Au9D|{ee($^xpp_f;wQ!X9w4l2eYu_x-Dkc0(-PM$e^Ei> zQT~e@*-@8!Ul2#YFd$>)vibrCLO%fQY~kT&SzG*@BDS8hw_x0F*a^(xG>Hj(odOBz z^It)9M0qgjP>yQ3_zctU{`dWgLQu4ktM%;GkG{HxUxH+(KwkRCja{XxX3USj#_|nM zHS*VyH34#A_OVNIg-iROX0-B+TB^>gv_|5-UJC0&rgMK+w`U#h?i>b__QG_af* z?{|gl8G&jii~)6U9<1701-%Yg)N>OO2>dBBH4@;f3$}!}|3DJ99n;N$z7T_MJ-O3} zaE9m*7-u*+-SEuaKHwI9sAw~&g{NYF$%Lm>HapgHGX}Oo*Ab;g?RC+GeM57<4iEZ+ z4u*G$6x-w+HiZf2CXf2fdqB9k-PZ+{y9pPH5Zc#LFFc1@94fK@K2VG2)lr8&p7-NB zN(J|cZ%)3NRXQEt)NOwbeY@gRGdB2jCfI6$Yc)F{-EA(yC@uH8nFbp9Tx!t0!D;>K zA#cHMVOdWGC}JE6u%aloPcsjD34Y>&uq}b(%7M_4gu8M*MVe9EJN!5&*YPL+9xivQ z4KPnhb$!01`CvTf9F`1NN%8N;m-D~--htXH#AdsG@o%@aD|&9l?SEHy7tWD<{_oMDhB%1N58WS0eC-ihftUzxfiD?eog5Qg1B+twjp@t% z`p|ba`JH9=|ChnJj7+&xRf2zHihg+; z9D(7xor3L+qf0bwDoWxE=VHI0ToPp^=iF=B@_0PUWV-2Z>)+@1^m+MjFhFDtGUGW{ zVJd^KTYj~aT$f4~H_6i6UuOjK%gvS&C(?=hej zdrTVTo!n-9f9pf-X>}$IHbx>8?Oh}xW8mm&Oy*D~_zSHg)^4T+xtkbg7fq%IIt25- zCkVnjio4f7C1GqBP}`ySL3+wL@A{C)Yie{k{pRlfGCw#hf3p1HJh4JKkKhiWdbm zZfHn0LDj&ig<3e`5-opXRFb@gX&0z=_{IlRlJUp7bCZy;NR_p9(~8fdsVgI`oU~sl zTjlJyQr7CNPEJ?STlSCRThpFr|A>UW;=U+K(v8O@NSnjGZcC-%>5DuGAIC~d==(BI>z zBOvP+qZxAXjdBzG9KY#0!^slK-a%xfuWi}Z2?TqG;pK-MgC7jjd0&%QlsbZ^_0_d= z-Ot>=pe%f2^UsP)i>|J7d-R*y6Gck)CbYMT-j%4-iO)pUn*JTf!}mg^w_{DzJuCc zB8$D#Gfo$!w(9$zaimO3^>bPp#A4uz1dv{bY7qgb8Q*yn8LeBe1j=g*PN zmz2KXiRvli*3vTWg`Ec=$MDmT<2Ll?P(f-jh2FvIc{l%OkDxf7lKI%WM!nN}+xZ{y zUem2JSa684o<8S$_C*ADDUW@s{67!)E8WE9=nV@s{Q1sPp0MojxBsz~a>F^eLG0S( zCZbuCWD?LG95QJR&K%Cvl&^70vR;-1cHWqR-aPJ@CyURrIk^uECVE!~Ib(j|Zv&E5 zmM^j0Wg=Cr>r%Cg@rl0Cqw&gc zM-c7)*U>RGV){LQ+=gUK7`(dG`1lid@O1M?`~RqsPSuONZ8C!B+x6a4Rq)6;q?2M{bDK6VDJ;|x05rUghs%}o{7Z*;%|?EsY+;x%0^GZc` zJ3ok>M>uGqkD8FqCpVt1d2Y!~yb$+Ui2B|}Q%Ebgg~8}3EN~xX9p?5_rr$Yxvy{|c zWu6@{yah~AVxTP9R)Y>9JIbO09;3=lS`61E%H#jeH-AqE_*^7LsDFoH|RM&u{8h#VYdP&(D zsLRp-j>z(H2M|95PmU+fX^8;Cmucu#+N$( z6y}j~;S=A#^L+{dcFpSlTap)s)B|J@k`_=?4Tz*@98k)6DlQ!Gm%DdDA6r%?8_BGcRz!h7yF{70a@IK}B0bg!F8T4AIsB-_3Lx1>PMog$ZJmH##f`y~!2gh)iIgHD z6q*=47l{3z68R+}1)Ay4LG}(YMBncH^^a-n`8n`xT6^k=(dDho_yVM8_5|or9RHA% z8j7M;(yH8O7fg&ZH+^v0gv6W!B+1Rtff;~YMwV(>Lz``Z0BxwfPIRMYA?X_-8$n6( zA>m_yPAoYEbZ0uEY-tAa{xi-iie>j3gZFp9a-gB)erCi5kRgJj#1#3LpTE@G5Q)G* z`i4+hdI3raUtL(^hw1@qnsj;sL=0Ir66+DsKF8ibcaJe(-!FQ9#vl8^;M{bVva05P zx)3sQ;^*1=WbRO`?@RGQ^qv-s_Z{0X`H9lZYy`C=TkHbRl^<|XR|WxRA^{P8E<1m( z$=Jpn+xpJv2KYd39>4MUzJ&7`vSM9$;k1XV0Y_x_Za*3Cyksjw{0h+}Ag~g8iAiAc zWK|fu>ntDzu5t|M(Sx3%N;ix zTEQM@$(M8nfG#E)31>6s3jp8Z1t3m%`I_ZGu3v;1Hh0t^B&+cA_E$>P(uYeKsU4}# z0)OVro)QV`8Q9f?-QwZ8-~N`amd%f)XTBm&+tzF4c~gTgWu&csKz3UL6lmK4OX;W5 zaQ8l}^^X*N!gn`h+K&00R@(Zn-Ha^udk2pWB;$~)m9sN4>sZJ64q{yXh_jOaQ&s@? zy(p;L&5r$MUyJOkJ3*6zGT(l@g8fIs1|p~(CbtAHYW}1%x-;oy(pyHy6xq39A)hXq z)vAym3i7R{7;4Utif~!fod%0E?te#(K3CS4c6}`x$BCi`=c(;A4#=~o_ALQIW*FDP zH};eJUq6RjlK`e$FTb;lGg8D$;Gh;Pb`=xx1Bj1<^fn zclzuTy#0X7rOh8X1F99jK#ZSKJ5N*!98#KCH7-nFej);y#j0Umn}n4D{*{bb1i#M@ zek~?}+drvKbg#V)ZY~4LxcP zcLStu!9-;S;*bFBvdO{$=-Z2aa$r4-tdZ;_xMtZ#PzjK>XddNxguOnZG|vCe78j7H zikXQG37eP8W#3Vnz8b+e^UC&RTdfxlFSymPDMM5HCEY#2R^jb;$6uW(zRt3!7kYF>nQ2k49@%cxUcAaxlJXuO)ufiR=5cMiG!H8PCAf=i&(OBVvqF23Q-3v)e} zV`7y3No^&?r!PORV7tNgGO$efptM63<{YANKqOwzsAS~Zb#f_!%1dhl05w!wFEs>| zzoyoF;QMGg@yU;b^S+|LDOJr4%Yg*Bs-_wOjj~-^b-nn|c>abH@R+L` zWO)Eh&AmG9j_W67P?CDN@8~Y8HV%U1#(y2h^l^^5&>H|vHS@P6qoqxA(h3|RW}JBT z9)ZGp1FyI3M{mbax?rJTnrZ=Bxt__7KxX?-hYH!ojmT!VaKAFG_9MTn$5=kSXw{!h zCSC3!f2D$|ChQf+rp@1atp2B?+WEaKc6^LDghgd=yqKIMJBJOK1GR&=5S5kcd*KkR zc?tv3VaDP*FrP$*?$$X6g<@F2+H)%4*yNbL;D3FK0)EuK)-2fX_ z43bnIUV)qFz}QVKY>>goo-$!HSdO02jFc~ze8hKx2o=!r2n?8cVb&{9Eyzo#%LhhH zI$E9~HxbCxq7kL+4KHm-9S67I`U7FemJ4swrASr5iWt7=%UMpRNC0eWdrK9~5FW01 z{-ycmd0~WlD-z;Bw#3ABpM-hona{!&NSff)ng5pOw>OiFJjAXRtva9AI5etbZtR)!k{UTwnz ze|;3GD8{XG{t)Td*(~a)kkqRA`>yV=p9AMy!802a`+OAMFP{Ih{ISxI1Qm&LiH<4d z&PBCK@p`|QLs}&Hu@IkKsk8_cEt@l0CR3<0kNQ8hI3Qt?ab8+zrof^u&|)*Kw(5*n z=QFRXA^JO;9Oz_UzmbAEigm`vv7TLv=k#4rjM^*EtUcTZR~&^AHk?xA+b@+ASQamd z&i;H8v`Hy5=-=x)uo^4)F7*r6c7~AcgKOEaM8UDZQ32?NfSs@$Hlo9qfl?c^{gPhZ zRkC8&(}_T@Fom`mWYAFvLTFl&F2|H4uO`qWmjvXkSMzlGcMY9~&kr|QhP^#=nZIYd zFRWxw)48x3+=WPqN8&T_GLVcoox!skZZhwTi|>C*1OHj!jNqi9(dI>VEg)wPq$pn7 zM?%}7xQvh6$RoW%E#I(>g5Vgdu)&p(-0!g0`+VuyS zzp&=;WODs*xiibm#CxY7$?|4rWBw4qWO(gu6u-nrX@D;-?{_; zWy=g|Ad)~&d-2VUB^inqnieV^A6w*HU21Y?09>*%W$ammS;S)u-523LK>;!dTnxeq z4oK=p-F^*>VPfxP@13dbb(6T4O|MS$MC&DyF$mo*XWF0Cpw>96W|iU(X3Ctt__9bn zd|L9RUbsLLe>Ib6Qy%oMJQTuU(jK;ZoBH$-Tq=* z7ovIQOqOSI8t$*2O0|k65yT?jhFTv?`PJQL!^7{N?PH!#l_z_eNornRvnAELM4o4M z`qCz&<5v2sojk4R?~71>lz7)}9v@kAh85F7tf1k2vY4kQJ+8-cgKGbRYR+1VZ!goG zcZ*A1LaEK5`VDiGcH3$S6TNuKmk87%DR@mq4Kz$ z>+-O(;aOShQ!X4uc9(QfdEN}$3M;;*F+o*#`e#N_ZVbfUdxJL;Wv35)nHKw@rS3V+ zG3v#0vuNpiCqV+Mi#qU4wB$7T4#mtXMF@X3R$W^l@lrBtgs?Zw^o4yvyi89oI74C% z@6B#5Wbn24OFdA1rwiw)=7r6^5rW8w9vre$5J+fcI{hV3$GY0JwsMt!s5K#5XIrd2 zbV$rfMq4@6xl8{mel1G}mtuKHZ-@701k{Z>WR0A^+E{_hTWw+n`qX7|Vadc2I&MFQ z7eFPiUdQulKy*ewXKBmJ>^H;28-M56ErvObiwQ&MZ)!K7Zwn1-4>J9vS`)>~eB#qa zx%Sn)^0j;@XWhKp_*dED?%CW?{yW@4I4oF#oWrdjcbcZXs=B1(% z46r<##p#M@Fg!ERFI2Yoe&k#xW+{j<4=qPtUEODL(FhuT5+>T~)Gz~{efq8D!X7o%rVcjR3crIB`|ci0$?+l+ zELfYyI8mD49h%m9D}sjAQt_@n1Iop)RE^+1`TL&pc&^4QQ;`=&KQe}f)_}8{gOeAw zFD<06)FqNi;|SLBZ7$fia^X_$(9O0%O`Tt!2`OboT^P8%x2`XJl?>DKgZgv!#0gX@Bq6caHiOExBB1AvtKKyB z3f`hS%5HS;9*RMlU{w@Yk5on!R5qiwSagTOjWvNg>Dgc28C(WBjkDqHunt7V1MCG; z+Koy9T#U@ij29Q$A9ZkKq}l-8-x@N+v*A+VA>+%wklH+>P+F7*(fp@$%6RMYv7$g_ z?fU@!wd%5GbtZ|^qkxo!+v+JkSPw5|f{qTBxqF2xi;TO2#1*1>+pcDaFL+pevwl2{f z@_S5OM@gm4@54tStnOq`EWv2AM+bt$wR!JX=H6xH^lzaNc<2Qk4(2NjSV)ldx0A@C zDigJjK5o9v&1NKpAtSqPK)J-auE&5%K6=6E#TB@`W^<*B`dX}wWxEq&G%8+5fS1#X z>lv!>de-mjV57IzYAyO1P%UX4F=m)#2da{))Rq!WPMfJ5 zGSBWVCkknxeajE=OClwg+7hrYUg(91+=K|nlf&;xMqH+{%ZJl1X(++^9gnW z(cCH8sntmesgo{P)ArySz|k~8bS`aigeRnX$gwu@OeHQBg3IS@)87S~E8JI!tN%>n zRZ(ase`o(}PhC^pEu~`788S*}kYgwPv?SOiv^Y^2VkxuOI0wE>zBZ&)j+>0TJ%1_%mx$rgmpdJbCq- ze>W|i2J}o(!mdGG`6gP!%sI!Pf<+K=s3Y1;=WCQp;CUHxyVR0)8^l|m`rU2~*2yNc z*{bOVDdXjidCTm&?Bj3!y<(8Z$25A(Z)J6Bxd3r`V zffPqG>Lm3N-o}yc)RS~Q{dnJ+zY+(bPr1QT^mGGy=TWR^N9oN_W5dRxo6rxL|KnZ& z*uzu#M!V&)(_PP0ej4Qn@R4+oy^N&UVhFzl-LxT>NYH#ar z=WV|TKG5^hc6Gq70o&9!kje`esWh+s)_r1jlS=cC&y~akMswq3Ey$laf~wyf(y%GW z?WNkwBxWR)rg_qRRT+}(`Tcf=?VIWmMh!<=Uk{}&-i?zbr-xqrAAiDqMfnDA_}#cK|MPXg-~YdT zsXuP^vu@TPoSscoacL08fIm25ti$aXd{OXoj8qg+lCH9@4;%)N4g?Szqwj)xYVUFa zuUhE4m4iSAOSe;;4zi(Zau-Bn|6r@Z7#}$))xEUh-;!=$(;u&g0Z52Dh`{-*bqmOU z;jLdw;8vc<%d_x2Gto!L%Y#UqDf@;E=@1 zo`Fe#1VH~l5GP{F|H+um0WvEt@ZX|cQEuq>Q$V{}JyJTH059ST#yvTj7^uh_eXPFrL=XYc#tWurBfQ0pv^%r5DAw;D1< zxE6$rq2`cjbEtut_TvNp_Cq|dfzEa-1%o&undHy7%(b5PDlMSdU<&}b;q?G8;|mD+ z-|O`XI(p*Ghidx*QA0z&Hi`=jzJK$E+=eb=8a#?$54zbC)s?mUMAa`K1l?$-ybh)q zDFL0Frby}Y63{X>?1|V|4r-5NzJH)0(S}$E4UlKg{MZBX zYPmJs1s&xaO1D!yx8(Egy@}E*jSB>Ru`YN=<<~&zp={BV0rhxF*TSv7kt3*mJLeI2 zcIEY^uS!epvrcy${W&+c@KwAp2p^LvnXxN&!PJLqw7%K91?2?Odz z(PxX2g56BE=6(?Y4WN#SBbvAVJhZ84T?OrrN-z;%%md5>-A~jWATyQ&l4y=n5^-45 zDwypkv)tYxSd>0n3$REd?_wpF4SoTWz*`B(IZuH6`UQylBjaJ`A>W=`d3VW!sgb8$ z*^eq-?XC{@1A?uD1yE7O3U_VJkJ168T#q=&a~NpVhDw0yU{y)$nsk!|S+qG;>$kD| zD>v+HIBx)O-MBVZz+D=8&y+<4E@4A?*(%~3SoYwtrUQ*IaPN!GLgFNwSs zs8DNQfPw-%o>QUwt1F1qKYcY#>&3v6i-w^##v}y)O3R{KYc#9e>zIh}J z@kevn6A9wAuWwfZ-|FvNmo6w~8&iW{q%;JCpd|yz#zBV$GqVrA4}E*7RE_KpGx~B} zvm>$Idi6xVWXO=P&f!*S2ZpY_yarU;;?--bjAVJAzKxFn^pImS{TsU|c49oYqLtO= z@2+7dEtqb4lQl$RA{gwCZjZ8tqsK#gzY_@6Y2Rs`DOn2e_stqXSW5bah@GYio6QuQ z7);a~@h}O($+|M`5dqw_4fDL3vM0^$)Od;Awkj~j^2)y7@(XS#GWxU!bToSRq8(-D zK?s@5S?=EwyEA4gLu7a#bS74pb>9#~e50&bdz9=Hv0Kq&pMfN}2Y^7;$LzlbUMR*Y zBGRpVkyUigKmP(F=pDUgLG{0tp&t8F+=(BsyOe^C!DpMjX7?iQs&??tARsNpr62_8 zr?OImQBcu#(DhED;Z^-x2$&}^WO;0q^ODu}Z9tBzyPA6l7}q?=l?y&~UT3t|E>wF12`6^6{A+IbD8ZEN3P_9H>yqv zt&-+JSep36MWa9&Y(y6~ATr-WvjzT8E6?nEll3^azF z>(1NEUosKrS^bKudE@q`VmR?thQcBDo^hRM9Y9QpaNLs_Z`I6Y^&%jcMgueK+&wd$ zt~qo;hEBXTURV0PjK8>7P_V7lZg-BacILefD$d`|UQf3*i3HusknJ|VN6@hXV}5!hE=0(1$ujnI9iEJT7LCcnvP zA6#C1j0Ij$()n_pg3*JW1Ty-kwFLXvnyy8FM_(l4gmI(Q+|l$NW!2M{1UCTMYyj8K z$0CAx1EwtYDaI(@iXv@tqAcQVOFuH=*`H}C7n|n$QtZ$xxFO9yJ*f^RY`R~oo{Z>5 zeh2$7f1mO6h5z%J+vAvbGnf0Uc@LX6VxHQ1D+8%n(K)4#zS%1n9L+cU%c*pX5Z+O? zppz0r0Me~MV?@`@%89jX~$J^N*)bJ^cp~^KTV*w z=fqtO(QBdiX{ECD)(rA&ICT0Z)0BfYv#prsb-cVrE}9%TjTQ*DRe*%T?ZLbMG6pp1 zZW%|pM$ir5Fu2Gm!<+Ql!E>rZE&}SK{aaI?I8}CvD}?8p`fv0wKkW`MQr4EW!z!kx z1CNAP+Xy}oGOSGGEc=J**+M`A4o!PXE< zJ=qXEo&|YfEAgZml4o{|6v15wKJxc1Qk{AB)l<;;xv~PcPl=OXW%KByB=}3n%D~Pt zNa+nT^w<>8MFff+LChOD7iw4+*X_)IHRT;6%6r4?-3n{hgwc9NQ6t=7OlMkuJl zUF>M77!z0zvA2@Vl*~EJV(r}oc1pq=By`Kw+wtLe5#GQ>{%>V`?s8EfD5$r2sfyiz zb{|IX_{x)ga06KwG=Ilo=FE;rO)`bvS&n{mC;&geTF}^x%r9gJHDJHOO4SghsoW-w zCT<9S&%WTqc>H$!@ZG-5hmSYzKR$D8WUBZ!tzwtV$Fxp0++?=ZMa{5l5^{u z*4$Sx3r+LWu;&w-mB71ktr5`JE3wQ}{Lg9BMANl@@U41nKNk$w>edxY@L2p13Seoz z;G{SLhGk9)ND#PE#G?DXS$e&;MEgi&Uc}cJ0%lXJ#Q>BF8dW!00g@;+5DwG@22KmE z3UCGte0SDHrA?TeC38-1>@R$OJUn3ZI_z1u(w{uO7I`=hEk5vK#oP>8`Qe_p2AYN? zR+8SZ95;0-z!=GO9rg#Uhc*ymaWe>+WEdsmtDb^-fFYCXJk|Z8a{?%MZxP+11=$UZ zBZlE!$)fXehZ-5PL2k-8KPo@*o}RT=ld$CSx#tSR=qC$?fvx2Xq&bdQSr_+|LtFKb-|9l}j z#7Fieyuiv%4PVy0l^8FMS(8rVNP(mQrFK1bcpVIz$$e}||7Hio!aKf3qIT|R>xw~S zLKk0{YzR>U@%0#6BTe7>{D&rrsw(o+3|VTdkSz})N1%yxG~e(Jm8R}YB3g(cf_Crj zvp7~*d-`oDHQuJkBN>WV@;o6)Y^VlWK+1-4UdH|TU9~DS2&X{K7;-W8xX(qZth~x$NB$f)L+MYSrGNf2^9xh7jjJrQEF% z1~o@aI(L~TY)a0dL-yXj?(3HqWX@eEqv-*%;bN(lG%e%l?;=CqZ&8~W6afc*#Vv!{ zyyh_~B2+BsJhX(WG2Ic$M>^O+Vld~ou0f{v(ml1Xq9+O=5oQMOIbv$EYG9xEP}!}^ zR#$BV4Ei9NG7YPKIK{#~;?l1&`bpcYV$qjxiVcWVc_cAmR=c=B_}^=P?uQgUpFuU@<+BvgptmCk^&{gE%How=Ea<}`R&9BT+~gOj;GgZABq z2hB!LGBcvk+EXp@{Q(S|?lA3tblq}mI;n!lM?Lve$~mnY&^?HhQ~z!4G&(7Kljzgv zv$-urj0=pq0E~HmmV+(>Nu^Ph9CZiUQ;o1D-l>*oL~{z`W2sr&hv+9~t$_WbZBb2m z@e}yf;MWg-^oow?-@o+kqLlV5Z%n*OOx5Atz~uVgTvfY`@;QeIQG*KctP_OqZEiHn z3z?!FxK~3WE;P=ntxyqG*sh>ofENmr5{B3r%8cH0xGh)awMWlaUr(Rcmamn^hk8NH zi=oUd=U*iiZE4gJ8$2_S`Xwyo{&ewoo`JF>SLBl)6S}lg8J{}Rwqc#EMFYSc^M#Jf>Bb?a09=52=10-vA#ky8 z+xIS2og+FWI`0w1idpgkFu$k`%^e)X`)a3s1XKcj+wL;|Q+u6GLtXCy@FXz7%-<4> ztM6P&?KpW}{{vMs6zc;qBv7wV!{*5OX-VzZsL1o)E2cyE2jTO}m1}#(?>ae!1R~+^ zw+(Z6`4uRos;--v%X!+iaLKR4vBw9v@z3(Q!NrK;uY((E!JK!7`AMGDK=R%n=E&L~ z;#Z?o9=gYief7H{e=gwFdUWkCBQCz4`W-{ehpT&%XGvHuL$*C122WZkwyVGrPpFN& z&5n0AK^gVIQTJ-TO6!{GjwrR_)kh$CLZPJyZH2@A1MEXY*=s%~E7)!{tq9l)FO<}J z?s``7k#TCCAlb|?ip&lgH=&>nVQk0v1EHbJ^MlG+ABsaxZBelJTYKD@g(fI;OWRW! zUqrDAGDOCzCD4mb{!M&=o4EzQi796h#c-AtCAgz>b@$uI_{lE6^Cj3C+l;hCrA`=2 zW(mPwp-s+i!1l(GQ>q4!Lk5@=c#XBr>ggNCl|8~ggtN7IkJm}P=)Z9e;XL$j>z^R? z!yvdEVKZ>6*)EajKx01`&j6=J+->T!+`ICLZ>Ge&w%Y{BwHwOKn~NR2yQ7jCgzv=a zaDt0wYFi;?G()#CF2Y+++guD{Y$G5FWgt(jWCA zjuG#_wHKbkR!ggdjxUyu=onSs%9!1^hT1D1`8G>C)hge4<7A0VdVd;s&ESCEzx<;S z6n9v3Mqq{MhixfoBeac{n+fQzYH<RQ$ zRU{O3Zv}55=!)XE;=CfS33f&on?a{N>-3_;`0-ciBXqgIg}0OZA2Tch(#2-`m$AMD z5vKZ}*4N=0$Y!f8TR-kD?8eqqJko$D@%N=@4-9}<)0=9MsKhwrHDiZPq&C0($I|Gy zATG5*I|4$7aQWQ?fDAm*MoT*#o|^^XR+8r}@fHOUP+GASJ~!1B5s+uO;C9b_YUElLTYZ0l+hZL&Z{6bySd@;t;_sbArRXXiS^beL0qHagYV&-Y zid}|Z-?%u80<9iD$I4SJvZWOlDZC;_$<1};%V*mJNTV#v>*l~;8a8ZcDjiX^293m< zMbYw=QJ5guAYC^qtupU@j*)qSdNuLZ>(T9u`b1OP3ba;>)0_SvgS%$GapW(9nsbr* zUP8KcjHuSk-w)QENq^jeG(GPE>u!oKSuIHnegYwZk52tM3J&wWmNy4SVoH*e*AuBU zXT#;9LGdJFZ{{C=+|)sNv15J{MYBybDJ3K937VlUgFTwQh!z>n%6ya(Vq;_Q87*Sh zJZp31?(x#T^u#7o4x=>^U9S1>(#F`-hWj49tk?D76X+rC>i_~&S&`FZ_E2Rn3FH+5 zyyBwPbIn%~QVA4m@hY;n$mnTdbIGhBQLoxuZLoaW$Iwlv$D;EQ>WC@Ku{4$MzPl90 zX-kaTeK8t5G7Q~pG@`^fk&%tGMbYU%oK@rNBfZIe@0!64B&I0?yfdTJlN@xEt8~t% zGZ^d4lKQ;Ad~}%-lS_ZievNLlqt>N5`j8^)eU4Y$updCq+-4ZjFf(vJ z;U_zSaFCfFu_iG&hvx<;u=q5IcrmbTV0*gLrz6JjN96S6g9DsFBAF1*_hIw|bSFaD zE*Ihohe_Ly+l^n!mvnU#*J#&KJ7nDw?b31umn-XcVgBxMS{Ij_b(OpgkR?uQ?T-Mj zGOY7W!Myu?65(&m3!7r;=>XfF&K}6HYU^OjCXxY9vBT}n=Nmm~=peZR?-ra^HfAS? zdn*gbhcvV0>CeA63o`qB>hqT~%ljiU4w|P}Y8z_H?HJNBC8bbydc^io!P^5_g7Sp> zew^Ygd!Q|E`h76UCvliRNJI+4{5gK=OceIHoL9_oV)p-RgZWH0y7`x09!@+{&m*-o z=V7!HKfx%jW$X#N4dnW4li=r4oz)7nTAWPyr)Y5)ZGTxLw%!741J}zf+gHau+YXSw z+f-9_V{gk1GSLdeU@k2TmUHc;Yb$=?k-zch*M{perTf-yM0+dqba3zMU<+Z-%F>&( z*)Vqy^F|gZ=eLE7PFym*zOsb-t~J^(NN?0Lgx&I3>Y8>z1Ihse4j>AWGZICz}?*=O&)Qm}$?@Qs8@E0v{*|syih1;dCjDkCnfYy*8G+)>G9(fKKpS^@Oh=Yb1~HBT5*vinQowyvylHfu_@q_ z*WyPKM$bIZ%mQG4>HIk@r9V)7fb;3)PW*PejQN??r zS&wf^w7tvDV?lj6{YX)o%!H@>Y9#4V%)dG8>0ZJaBaTl2c-PVC6fw5uIPlouq>Rh| z6*~AUhVVvF+EbN(=94v^Y@W9sY4d7*^u*U7*WBd8ca}V2DPJPTy6fFvgRBOz%_C(F zz~#4h#^SoLWW0E!$voWqTcX$MuMA zcX|M7ASOble=a>;{+Vq{El+x;Wv0W{*2=gIe3}cH`*o?13TiI6^~))gR$}mm8p1qx z(sw@Co*Doq@&hC(tx$QodDrEOP@-cZfbIS1`xXly`>&-r8ioR%>TNG{9}=v7*ep@h7#81^ zu$axZx)TdM|Fb8$%AMQdGS`#mjT)2}p z^!8ftBf}zuvQ9Ra(3rrCQkLGT!KEwVeY%|8UjUNilum_!Hk!ROEL>-IHq7+7g>8SA z*JW@!tEz3!?VOXE&AwH9UCi*-Y#X5GVM^%Q4TPq07mCm7MY5o@dL6Q>56bR3Q<^PA zboFZ#+NgZZ)WS)=(_~jjonOJbV8HaT)s#TilPD1=(@2|sGi5&sjTKmL%O8G~dQg^H z2ou0^;lOI3zZc>MaQ3qSthflY*P7T(03|L2CG@ZM6_anQf?mA_v4#eaxn^}H{hGQ9 z;w8n9g7B5rYtP>KPKT7x%RPykdYcf<`6*&Q`?Ioq*Lng12}@a=PK$e{GoV~Q9e}C#Ue5bh)%383IxUrqhh#|rTF}VS)6qa(lBiJ~&ZN)}K&>?b z%hP}+Ui${I*!(4g^G&@3EbBTRU;rzosF|zI9{@zxN+dFJ40>frj~0P$Z3;TC@dWs* zR(2T%y_eb>B@WTI_c7f$PF-``8`mf~EdiN6^#o zgY%tRmNHh|Uf}az!44=@Ip|Vz4Ft5#^qyB7GcPz zH@o~~>^s3R1W|>mME9NmFGB2`WDCF0q5Uio)y@up`;Q&OZM+#AI#{Cg#!4=w=b9;u7UMzOeo(3l;x?9m z{@i%1$H$f*^6h*9;cMt;yWWo0Q@8aUBdOm0`clyrjgVm3YM!-e(5@RRn-RK0hRNUs z5-KstE?+*1yC*+=*@V!Sv%_JD{din__Z0Tr?up5(6c9H_v#qQ-C^+1*@`yE2k@L^1oS>u-svxtyiX0V zV4uA;PhhYsV@LXo-G(_*?sl;33Y?>^B)cqCZaP^xbkPQw3vWDb<$d2%Wjy-okxrDZ z@Fa-7IEy@jqQvE>-<8B2<$G{dCkRn3aN_p>?O3FBw%bm4YKlI zh7S*%i`Dc&GHh`2jk2_$xbZ@UHoCSmWkE#5`nkFAJ}GpX%_0eoIUNy>4TSnW!c2V_ zW{*ZsqZ^B`#sp#z3>{p;isVo#j}2!VN)14+P+W=30Li6?L1+wiFK_1FTQ6YTQ`opd z|GkA0hot#z^Ol)~J&%kDLuMf)HxtJ^J1_j~q5rxAAFe!Sf0OA8&!*9tWKf>13mKw81dYTe_#L!ZNMzUe=kI;3 zICl;8ppD!X)k51@stwJ5$ zh56`0ik09(g3fK|&KUOeDL~5|oP>?*QO;X6Sf=a(AUaxnJZRMBY>Qf{DKR0wgv2;T z?LgA#v4iNkX=ai^5bg$5Yb=(kb^sEZ7B=WScGhNO1C#VW#$TrG#%g2^)+#JScUbT; z3R^MN;y8t1pH|lIgf|=VVjCu9T`*3^o3I9iZa4CK*zjcE&qr_6Pv$YpeCRqzHONTI zxZ9cy$%y(+y290IZ()v!qG#;hRU z*yoWr`xZZ)y3=n6=OE0#Bs3Sdv%3bFW~FWH?Mt9a$ToE*tm3IA7b;(T3p^Euxn5)H+kO4qS0sXJ^X~dy z5K|p%k~-}#GApw}AM_HQaV`Xf332SxFcBLJCsnBq1|5Fv{XCvoAR*!Cu*f#^ z4(;eMz8V5q#r+`qU!o%-FX@Qz+Nu48&@>H=G45!dQe0dzF_cEve$XjvLbr81)-@?( z+O+RMyay}0W1A!BQ$B>F_2GWcQ?f*`e=VBvd!uoHy(y7A70pfMfg?Eeo{rl+%@#>S zEsfnU<$5iKzVRA4Nz@ZkMPHrsb9PzEAiC_bUFt7-CN<)G5kDdEWMp*ZH(DIR#_&*E z<1?d!FCQ9ED9E$;`uiQ1ztjI!uW_(BRZN?x2srsc<_ub;Vu( zMcZx^U5k454#lu#q#z>C>}i_76u=tHG{%IX;B1|Nfc-l%4*L(3sz`WdQZE;;k@y8O zbG?(iTm73$e7y*XLH`9M>hhYPnpCcRVe93uCArcVN`FV*y_jbdjkFGtsyW7z^6r~> zv2trHj{@r71Z%`;qc4a#S$`4C>30_uFkX00HGax0p~@sU$peeVOLlLK=F`W$k--A{ z5-JfwC57Dup;ej#RS>E#h!PC;WXpBB$A-PQPdcYSl$~Rk<=^HjujcC%UU1vw=gsmS zx5fsDOwUu;=YQ9Gjtz&hx&6cQawFH6gS@!jh?2OeZ~`4{y{c^ucfUDqs!3>-Km2+I zo$f}jJlWOJ7ZgoEvvY0OXdWcC+i3JWLw`Nb*<|0$+9*xYEk$7Up}kw)UMa*>Ups0V zEZhLv)9!}NjpB?1GNwSDaJ;=b`?2p&gU^RLVZzT?YuZzbh&C#&@D9}3H`#Na!-#mZ zU9K@~VtpZA65{*Z&E+C{AbDuW$kq#heaf2Qf0sk)N5%7u)9h!ft3|v3{;Ru$I+aY}wL|6oyy{;5 zwG`?_Gcw}aDo+%xdG)6+PvCDHX!fP6pYU47Wxw+YyhY+GD-W)Eb7C1XLPP$q%kzPp2&*bdcJc z?D#80XR}s)PPpbAH?8En8k_?_S1 z&iq*r-y~^2Ayb^3d>L#WgKjdYG%0`Fg$)NHAq<6Ly%x4zK%AmnaiPZh=}8ilpG^?M zeEeB5%obG$U(uz<)3>JBjmdH!J!Cg=su?7Sm3s*?JFsW$k!ktAogy2P@Hy$EVX0D2 zlFTihb+y)_)|GjuPU`Zx*u&|>FuKj%M9T9P)Zu`Jdos2?^h5_x8jAGlIy}gFL3V-{ zPbS7Stf{!RUQeMQ*^tZw!<}I~j&DRgu+H0Jrh?~n?VfZ7gI%HvbI>-Luazz3d33LP zpGX1qnM=jYnUu*nYU$PfzcU03Csi4*?1{@sko*$WW4yzz$Eh>G-dmgvL52ET@0|j2 z5*ripMkaTUDZ~nL$92REP|#(3%JpRSdA3iGVrb1HFO6C-ZVqGUth}&|ad5QGB(^Em zL32E4TGRhn8YFSwx|J#HHmnE2(WhaPbk$dN8WO%PB<}&u(xK9>_&lcKUhR?xhRxXS z^}b-I{K_z0KrfC+DiR559!=;Wkew%{oH9T&l3o&0$Sc0VZ>v_tKa2kd`zL z#O)fcvYB}eZdZLx1uL_}-V_=Xd0|~rQl*2I zJ!WiFw_+qyyTW9b#dN~EjW19z96xR=hvEg~l#J`-)R-}h=hD)SGpMuPjCiKeU>e}e znPWt6D7%ao_&$NlTD)I|vVaAG`D&RFxMxfj?ym;qH`KOGqE+Nk^bbjtRNU;Jx@4SU zOO2zi=U<33u0h+%Z|A5r;hE%5(G7i-MJtVkVqa*y$PZ9njwYiOt?LHSCu~D$_jTEc5&uMr#>tDJJ z2Nlun)Kd8)J!QTOay0y|ZB{#K=G;UfY%N|T$benc;H=3R3r8*z$i zYcwVUi8zz}}F!HIO>Q?BXGlI4~LZA0=$8ZeUi8a#=f&mYHHEgJ~ac5)TJ zkVPBEtG=15((w)7)pv|;7l`C@Uvcena`9Uz(~n(<^)@rF9^F@TD}Wur%GEmL zR}KT$`B>8s{KN|%iZ!$#f#c`8_Q8S-Yyxg;CIf{j24qaKCzd!y&&HD-XJm8nrj6BC z`5|hQVjXusPDo`}gxhFbnfX-gVlf}$BI_V~+maCH%$((yyabYr zcvKK^t7Vw-;2UhJ{c`PT8*kc5Lm-FiXbF14o4xkAuiKVHuf}<5YFbfaZveWvLLz@( zw`5D(in~)6)2)`p;w97~e>P|IotL?`4UHd$fXu=J6soFp-}#v!2_#XJjrIDU#@F?8JlFzowyvN*9<57jM% zMzJDn%P2goq|+DEm&#_*FBjA7T&QsvP8hx1L-k#oHgWJh*05w>Wq&4+Y>d>}4U}_dBA&^zx$eEtj$=vx6LjzW9;EweUBRWTMy1tGzxx&X7{8s;UI3F~JJ;x2vZZ&|F`r+g%ZHE1I3o?`E zxpDQ}rsw@waz)>%%lJvMG5WG!jk9Hp`asq0)^)-A($tj4B)zAp?Xu6UuD$btXvQ&o zR=;`q9%{$$YPKPGOeXhqLF#D!67lcR<2cC~rFp4aQ61f08D`>7)MnpzMRYuom)Ms% z?Qp|dk6ZzV{P1Qh$zz?mX*wcOiu{8)%h9c|P5~kY3Qp9mj3v;tw6UItxm|dr=(P45 zUdPWd*Y@tKyyrn_VeuSpLFC3Jqftf9%~(}-jbzD*Qzf#9cp$V=^Fn9XK7kd%eh1c! z#$>Q@htHk&o;44~kR8?e9BaUBBk-^}SwxoIv?;fgOK1|r)1yD>N*OiDFp}JO$Wi>_ zHxN~<46(K@`DsS&(s2rV%b{InLxx7Bd?BOMwEcElI(A*O+?n(d0t(M3@$CG*@!S`oFjlV8a-qM_k4u(5!}BNd-p#ZiJ5{iHzX|Rrarxycn|kDQ%_Z57KLssuVHF2Oc zIi2qAgx*O2mjyp}X9D5=&mV3X0GDq())>%M_qjFTWMd_Klgz|wGtrXw$YqST5&*6K z9rXC>|Hd17tHe@l>n#BZeY{GqVamef;)0`KQs~bMRpM7RVy2c&4ZclF`QEYc==zn& zs%n5UE7eQeGX*v4_$1G}0oObAOX{puo+pWgwqCkznC(2{tw0aq6K}mNE+yje3KGG= zRC&{X715mz{-84XpkC4|J~io`z2@B4*P$X~sJl?x(FSUTDr=BCD|fledjN()w%NC? zbH{6c-3I9Gg@C8%}J7vXH zHBLO)$ktC5A~(=VRxL8+0{@R(2qbgqyxv3ac+k6P z503`maUKY=#c;Um24#}!K1lgrl;r-IZ>A~+DlgU`t}euQ93gt|BII}k60U6nIKiUw z$$B2b#snN!XtVsEcRW(Ter*6UiG}A}Ab-QYn^nH}XkmO9v_UKoF2Bd& z{^nx$ENB~-Sdkq*{Cw}1Dt0s89zYJDH1-pvt#pT~m@v-^-W5XBW*B?)3N z!5@O)o%_y#__i4V@UaJ(AH+TGQgnhk2X{6Xhis70K>1SDfc|;gvC@m@V#w!;bCQ59 z+_>`ID~D`W`H-~cDZSH(H*VP@qv2vZ!(L}U_vN%~Q9j>Ikk?u0uXi+3D$Bx#6EbO| z8FC7FtM|Hs1?LH=>Q!cl=c#w=o?Dy@+S;2#0tmA@^X$L*_pAF%WBZUK25Y;)68o%5 z?4|M0;rADdOWqorh!!lp+o_-QGY-N21{B|h&TB8%reySamKT(Bz3K*?9v}ZrYVFj5 zE6}Io_MrxBh!aSklw?E;-)q{s14UZrYYBKCn^bFJ(6ZbfNnt*b? zax>dBDIF>usT_htS^F!kOQ5%-8W&n?f0k<1Pj#rw(P5Qt+!Thqz$8?nCsX1GOM75s zb4}DF*(G>{Y$8WN9?>fq)IowjtTE;H4BTm6P%5rk2Jx@nw_YG@(&|~bXFw_PFo`>_ znbCF#iM8Byh>@ar5hUD*zStNqR2Np|E43;ykH!7Q)5u}674Q7hzNnr$GQzMwS9_2l zcc+07ldkJ#ylTlpUH624n3z@^3-jhmu_5#&)!<&Dz~VGh55LRh;@m}(z)H8xkaolO z^Lx;nvG+UVFitCR9|%iP0xzW{_7koZ$vrsA&=eu^J5~3B*qIUCE~e50A(Vx1Hhb>x^g-R`QQDI_ zpa(SA_yF+P$Dz-1lP_u=fChdSFrJ=Yb~*%>=Or~}wbq$yelp_8?F8HfVagKg-&t&) zH>I8?yzpzbQPBFn1t%jK&P?WhjG&{G`8BkO9?~2s=o~r9RPjuXv(&H8v3fTA_?Ua;!#uzftTIQvG zPl9=^YZ_x-?OTD(U?Lic7})~tAo46Pl9qc-rZK@HPMA6$l=mh7x*PkHxXbml_74N; zo+*~`K!qnfsAfl8OD3Ls-cv#9GHb5XJcdxJovWxORRTWkWRZR51~XT3N`wpe;wsUw zt{u3QOBtl%=CC^Q70VkmWSEDkH`+-4!mP7KQGp=q)Fb-#l2M#BXx3&`w%$eJuGEJJ zff=u}zyj2Y3cwWhLe33YuR*<99H%_}Jo>oUMzfB~1%s@3i;~gSM>HEDuU^mxmdI`A zJ^?OuyUK^uB}(E}55LIwJE7urXYpm_F)wPT)eZG9 zP6*I%ismMF-3F7v_9!H?vLMhEZ}&?Xji)U}YL3*#2fFD6K2Sx9O^$cG6MQK;3qdg_ zS?am&mp6E3der7u3%XSt#=}n1&JsHSC&JV7Ok^xooqbrrbD%_iuzPRSfyl1K-_E7# ziGD<_8|YC>8$FH=BQY8&1c|27Z4Qe;hD-d_pCju{Ud}h(`SE$i@+HSEbipz4kGrZ(K~&qcc=p{*aaEe$XG+y;=M|aubrBtTy|V?zL*NXv!;(&7=>@c(Nc_j z58t9RKzU$!Qm9i-`b#QcKd)e%(D}%zIhBR)$60f10*l!`geA zA?^%@*xI*#i{VMC9tJ(ea)PasG~!wQwW^?Dfr;k}H!ghjqnW^A*ot_=6oAXpYt(^a z_T>2-Zp3AOxx?KzKXiy84k^^lLoi7{$-d*CJKMxii$5ml5pxaA(c3OC7=7Nqd8J5I ze_E{3Ipb&0gUMsRn`D?9c0#vBZYI-fT6$hP#wA>hc0D()t%42+4u$kKdAa&V@j2pE z+;DAXYL`Y4SpraXz_RtA8%0wdm0Zga>T(7~<6nS%DV}|O+PgXQB=G)%nkdXBKV$pn zn6AyFWen8lUEkFWRm{LMX$-PoCBO^!%dHbXJ07xyPt3b~nbrtY9p+!kogm$4(HZ%C z@imG#U`TZN zp1*!>A{P9cw{EOk=a+nqE0|H_cVBzjGJC^x47PsOVyO?)@lNqlf7vAD$>Bvds#QQ$ z7iuY?6cIjgAt9%E9kz|Fde$|dF+3*WjN`E1Q69T+$WKKn)GF_eR<3EpknJHNaf^=! z*{F4?F9^lHZ)u|ZE!zPh9h%BS1wL6Bze6*!A8!?ii%Asj*BI5SvD|syrll^M^EsD6 zGr|0{kVl32>s<)J6^wp(bx%PTxcw7bD2nP_6OBM7s;bu4PIaWos2Aj>)-U~8f2*MM zT&K{TGVd8NX#9sNw1|oaO^xYUlIPRMTbO+Ua86>y5BF9DfxM=rw}ccexbqX_G6qct z#K`5H9$vUh&G3sUWzW$%$e(oV3PxBY(V<;5i#yJ$eKo=hd!YP6kU~Gf1W{fSscJm7blKNQm{X z(Whj`Zc~K$d5+{#)JHvt((k$I7tWiuj`9^L60xvoA>1ZmKvR7ce=@ZorcDPl#t`YJ zL>kSV1hQ+Q<8=hGAdQyq@CF@FK`&1e{g&cc(2sBJC9g}lgXuL93(Ln#$~|M#uMmw6 zM}OKxcoyefj_`EZ=c?{spJM4||&4!Gl?wE7N1lvz!nzPN8 z>kYOk8JJ!hmK2X9U?EN1QNaqb0|`02J9G$So8d9Lf5LcxMnW&=_T>z&tYa?kL-0|)FOzouRw!U0R6 z>0SJh)mz_eO2`(hd}4`b^nLA~RL2I{aqp8Ic(kErXqsBn7r`1AUp#B3_~}?XbHxWI zR_5okno5ymksOd`dsZIRl!EG3c*>zA$u=QhoTD~goxAI57Fr!LeVo=!Zje#^`6kUn zrJy<;F3@73sAr^3x`fE!FW#Z;{A@oFk1M{>FmstoAG>^=gb}w>)ei-eIM%rT`7@cb zrx#fJ>#I#pH3eM-aC~CBX zzJJ947jt~)MH5!(Dyq}Aw#S0N#)L_UrcVByYR1oH9xd8^xd_~}XptOiy}qaC2xPGg zDW?`b6ZKK&D^6b730D@|M1+H6R^C3rYGw@A3Nb^GW~T_%L&l%qHnBuS(Z2L&^V=Yz z>Qv8k3gzd=ERrkkc{MS2TS2~F<|(C5Oyg_sY4Pz<31iZhgvQ!zdw?v!x1ckXgTp*;-1y6rQWVBLEb zu5+UJ7L5lnn^`-WTJhGeZ9SbpFz#>ku@6c6qi@$ z4sKG2vu(v;necP_#(xDU^QV2WzSA7qtrjPwp5d-yYyVVK*UaQ>xl~0+n?No3gYmtQ zv#aGGZ9_=ImSByp!;Ie)5j^* z@MDw`({(h-akAN`=5^y_1=U-DR~6S`f;t5fu7r}Q#_{6Rsq;p%auCWqL$Srt$crq+ zF){x>#IpHN+2EIs4>s%sEBLxA0a@c-4vorH5AczVXqP`izc84v<>Zg6T1< zzLl2^(3eFin`(@1x|*G)x-VXvIA=^ED{sQ^F5FL2(bDXK?efU``c{@-$~5R_F;uT+ zY4Bx(Wa_=u_g{=tqzd@ldb7>a7+iYB=mG-?AAS!i2{s5nQ1`X; z>J9=~kR(p-imlm(^~JcCnrF+QngI;Bi_MA8sb?A0*fLKf%(mSFY6xNXUT9uE+H@S z0oSgC7ks4c{*m*Xj+vpc8>{dX$Asvks>v7c+_?2vKxleQwE z4+v;tAo(WxZG=Q3CBx6%Dl#Ifu*u7pROY%O&XsV0^;G89_X>1 zQaV)-sZqC_Pj9@K;&piKwR_iOWR$XN|Blf36+GFtq=c%Qc(OCPGc6nRpWWtM zc+ZCG%FnN$*hqrJeFiu#@>+%BBx~nV{Kz-J`L+2yFyptf9JKC&Qd#OK!keWo#XozL z5^H`5c(a73v&L8%>1D2;4!?2Y4TVD=cX|ZM@97^u($C?3_fZ<@_KG5O=0Gphtn=qI@ z$v>AQLhJb--2pEk&5zf-WWnkY$t`nTDD405w*SA|{x*^RzdGA)eW{QBdip=s6=Rss zXTXp(Hqw7qx(xuu{i{J5fqW7voVqxY-&;{#qtB%Oo`e4pvQ7SGp-5pNDxVmhY*6|q z5mOOu`6EH2@`&P(G)^952(VkFv=GA`@8gz23Mu=4V=M%`!RKI=UV6&Nf^w8*D>f!$ zK@}Qje{BET=&jR3z5%UfWZFM$1=Le`Q2ZZlQbsPq`zDrj9Z#0#+m}qNJjA@PR_XbF zGZhj@L0;sXBJa`aC;PvyGNNk$?+v6Mic<&%m7=}wkt#gR~5$EV|c@LsX zK)8oHIP9gPZ~lfkR?9uK~G(G0yFvFwf&cm^c@D4xfUgK%bO3st&jwFB$MMU znDZ|pf6L4;M*b)ypX3C0vNZcZJu3~bMU%vFUj?V= znLFlIDSL9kLKuC7qbi3KjO$C9UoWL$UmvHuSNiVL(G5)6itIY)yZb{eKn>`Wk&Fx^ zRT>}yNvzFxzhBWaQ#EsYTjC@y*z%We6e`9F3`Lka?6+92`nHDXH&~~C=@y34rmg2n zzt5Kiw-wF4I;d)ry1cteW?F0)evp8TJlt-uZs~4SnKpB3#xI;oqdwA~Nk3pP)$?r4 zL?Ocpcl(h)S}EW^AAc%2c37bN(KG+p^@&hZ;c6qNsR7enE)%Do{+U?)OnQM4+kCfS zK{vC|HIPtFkB@xxXTMILIbq=&kzoOvgtgQ3hED1cxB8u6J(aOVCL)W=7EhBCHKdK| zpyvPZIA6Q6f4;ASbLf=!CR?nS2>+7R@ zdDYdH8=VySu4V$e`|75 zzygF?m?7J_e@qZ0$!L%iNXKP`^z{{`yN7~vQMIx0uJ`B5YRi4mx=seI^LguRdo(2G zZC-DcrBv*_l2<)bRJ6$QqUOf4?#zp_tILii{WSz%hyi(`> zV^Dyj`W|Z9Gm+eP;zn`#33Z=0xU76<|3ZGa;d%MQA=bTkfhQS#d*@>z@h5-Pi~l42 z2TKiT>E5r zkQuq6K9d})V%`;9JFX|cD85O!e|cR?rl#e0yI+oX8vy4QlIkw+GSD7tKR@G<$Vrme z7u=V@VYHk%Y_k)ZD>ZF8FaJr7ApDwSFt@m+(87%G@EoT}X@S;}q`0INoG$!}y1hv9 zC(nPpKNv@%M~X>zWIk-Y??MS%k<${m?Yxp*Yu7g2pM3LekeDFUx6M(eVn#zn+hwZ5 z^A*0V)Hw_@EiRKfO6_A)fhlLJ(Z<+$Q-|}6?g!)~m<@J!$N91I;}ayM^=1oR9*U!7 zwPG)vcq<&(Z=$Z^@?>|kPj%^NUHrrPdO!cmp6TbpKK2F{eCXJJO!a0Zbc(lKC{G33 z2ej`qm({K;(`pX!SwYjdz93%|113s>VCe*hN`=Fd(mqyl4B9En{XuPDg0SxA46{)_ zzOI1VG0M6w-#J-PG;3jf7Ikd$XnTG8+4WC&p#hFkY0G0XS_Hgh1~Z3QBSY7^&oW}R z_8B;T6@As@`{O+!XOP0XKvt=T9LQ*XMCk$~7k_CWmMJ|VFH2QUnnc!uedf(`vSxL3 z%I^xUov>PyyYhW;6^)q5wf$Ypc}1zhXMJaV{pi(46$WNPKn(t)l?n%H{i6Kxau)re zImM>??;*@45%*C3GULnw+pqj72~+1vlMUO^A!+8SD~-KPS;Oy^Yo}|^e{nC%Ubamh zC*Td1{J3n(fb(I3z;TQucgi_o`3HZOvR$-|XZ}xy?+=+Yz5g_ZrT^Hljw>2ceoDCpV_9jFJMU}SW%+PKPlIG z%~&gl&G&1|1KF2^eWR3CZ5dPCciIhHk{w5XG7<1buwI#-@nLT`{}t`GV`x`oyM&kc z&MSMlAI##Uq&v*+QVe)RB0HN)hC(+aE^D6)=lS3R1ICdw^Lht44tq6n z$Q((6y%Qc%9|Io?&0H6J`w@9H@@o9hzmkCe zZ+tKP3z$o)! zMj{3OPaLP<1Wc98Z#U9Se!?VcaU*s#-|yE)@-{MtfH~#VMB|Kw>_r$Am_G7{Z}YNakZaWIqaZ zh_7N1wv5D+wH)B(bV6}=kzF8ZSp$oy2;mqdxQyXJRBp-|vKWgH$woDWzw(4L%VyJy zKsb}aRUax-2@O5&?gKp!i@Ua^@5%gmshdNN~aptels&z-SA-obIB?wkC<3`F{l zKeRyTv%cuZ%|909zt=SyS+0=E{vnE9-Xw6C;W1EeN)(Du6VqC+wh0@aC&s*0dwZ=9 zX2}4k;kQ9vZqs9rbgeE*HVUF2UO>|xrZI{1t@yMcT%z^wf zT{aI4gxp>Ox3=aww!;Vcki*(`l{gagu3JdDhL2d$d&V#&?vpUy5`#t+@@o<~(to=y zGwsI|k^_30T>bd8`*-enpNgX|fTQR@yV#O=IDNg@_#srQyMa!BM#Fyeu*eRI!(sEX zIF`ui(CK+gG7-*&izt%gYhQV*OQPhWlq#s-@Os0+MoxkA=r{jmxX+QWBj_nBpr`9+ zF(Kl=uWwHGBAL?th=3#(QmChYlGlGt@+gDEN1I>G;X?O5AH$O^Eg(JW+G)W0nEjq` zyF4G6T{qseaPlKJbQva2j9D74u3ldl==D6aM|E7}-j_-uQlYQZ3MX=)zv_J|Ozu9IT=%I$Gr)my_k>bOw% zSx^tu&QJUe8V=)yVf+1qE?xP*6f0iSf8Ws0IDN$SO8)@EqZOLYK;XjtZ|06duw39L4+giCs zsr0AI1HWe?hI?*3#xhO{>7HJ;bTw~jZJ%$Dt9-9l&mkDJk&T3aKXj!Cp~xY&sR-hE z!olmnrZk86b{YcfRPD@i&Vl# zww_mU5?Dg{V2cK?;_lDhuAVKM1U6_fBBcZ5Hz`EBcurdDsB0kY22Ra)+WimO0#VTN z$=|nUNKUMiVkGz1G{nH4utAgq+JuJ(5h3l0jw#>?=EJt|MF_GUV_hd>7W~-~azIPD zedDK_-Cg&U27PW4%(YNjn@?F~>6suram@fB`hv{!W1bW72J7IsY2F_$YSdx;I z14L+h0SYhXAu_T^?XrNy9}P#Q)i*_08;y#&ZP_2c79NMI}ha(p~h5dv@yOIRCz0_y5$6f+I}D+j>*S?xNoPvf&rve ze~Jg92%E>7KPPZ zl-$=pG99l(!l0-uxE6{bU}N69j}@1h{@fkzIR9&Rs4w?$`sq65kHb)f-k6d)EQLUE zcJL5rIWG<)O3NVLu0QLS(kji#^v8}0$3_{r@tvG*&51#_XRBz3T%{-^F%>kzJw@OaGD4E^I;B5_naJ9|nr^jO;YfHvG#u{kK1t0}+Im#pH zsPidpfVf*$9Ulxb<&lNIu8sSnFac7VcSreGf$E5ejQpj<;qIBhobnX&ZsAO%91ZG!VwZ9A$RovUaqT0#srNL+oB4U|kQ` zj(Vw#x7beDRdUIEP%)3R!G$k;mx&)zQjVnmu2lKH?C7p*0<*=E%Pz7f0s6Dhth&hk z(<~2)#CBWiu={u1_hU!;by^y~D+bxM{F-QU+-yr5o%*IO#d>79 z>JJ&XPx9^Vbrahx1Q)fx%ldKu@?u)zE|~fkjLRragRxnLDBC!R9M+SZhd8^ zGKHh-IRJ(uWY$)&NCn>O2VO>AX0Wuxsbm5b$-&9l$|ReD^{hgzyyxY1C1ZMShnc;Z z!%t%G6zjV6b9P8tWQ%V`*yOG*+0#Bszc`e*1r_l}MiG-tL$hecB+DgIP) zhvdO$9tjh_%<6`aqVM?otlut**E+3yR2VqJ@0f4%Gpu|4Ut9p(`Man1h8U0BHXez& zPpuW+xr1!!vrXEfz0yerKHokn5M8G2{cSNKBbPD2C1KMlY&an2Ojsyv==P;Cn8r$C za(W`gdGfA#XO>lm*vh55#(j|lp)MX8F5j(6_RR)GV%-iTi)vnr^;H{E+ji{DN$z^K z7B6-d9GuEEdrg}k#k{PMyr(MV-eDT+U>j>WYf+X}x|cC-6L2xDfaYK`=|c02_K{&U zN7~(A9r~m8U(iD`KC+ix^H}+)@3t}{bT`}8I(S_e1cThLswQ3qH#)9l(J$}a|Fmyv z5^K~Lv+$lo6+4$LZgzJuub6vzu0fvwjj7r-E#U5R*1GFi=s5pL)|~r&)`>}Wx^gh0P1aejWx#!KyKw7&1Y)5-4yW>-~*n5^k44~yLHXHHQ&^lh9ZzSN)ne{OLF)X`Mog6%1?cnLqA3}#EG0BPfycp&SD(JHp2gjz4cyqn>d=or zj%Vf_;fQ#@M%?R(@>_(>$H9V%PUC19sUr>Lk=k{kI{eD(o9(%vna5eS88-wK0%lur zl6j?eu|GY=hl|z%K7nhX`crGNiLS!rcASS#X7M&r(all2zq`Tj!Hm3oa0EbuMILzY zm*MIEH zs$jXT)26MnAgn*}HtezgkwEujcDRnFf5p-%6_&<>0F9UjNHHI6=E%?^*Zr^DFNsvf zymaDVse2)7@9^RtmEl!oj>-;0!v&8>?cA&{%hR3n`7b$MRZi3c35KZ{|5?@})}Qf`HxdoAGSJ;LXovpAJZ!gLJ_wTcX0T=*M=gZ8cw}zgfcOwq!?raT_XmtFh?a-REeInPoLsl=QI^=%>eU z=#L#o)xSI@%l_I(+4?P7)}PR$fGPwX;N<>+=C2R`dELwF#~32&KQ#Tn`aZOpD5R{9 zX?q;yLgOv5Kl?zMJ{t#8ckkN_6t!8!$l%L?`b@p#d)Z?QnjGOuIug(Dlii2I$ezbc z%Klf!NhdMm4FYD}lWWqH(kn4M9-6dZ{y!uk{);Ou^*$PtzNl8}b{ z#XtJ{uP&9Y!NVcHxSfTPWgnKm$b0hyD*4ikgRS8S6vGwVKYN~jMBx~N{P0uhMI5wj zDgQgn3A8kd{20g8KVO8N6+Yy{V{Su5!dPnV#SMlYll7}Qe*DiiZjnf%I)&-O#ExTb zEuOiUdZwxrzvcn*UVeD+;ms3@{IV=h(fH(2Y@1B*Z;$X=g7+ao5vQBXAFZbs zf*a)Gu@K^g!$ak_;`$S%9Wmc{RHKz+%{G4U9ZX1G`jXL$%D>q3MlhR->x9PCn@q3J zzWw;tkbY(?pH|;oZE>6J);H@KivtB3f$!G!n)$9CQ49IgcKNG6FI$zT>TU*qdL*>r z`)9|^kV&H=J1|{Cl!PBS)bFee2@$8w26B4OW7bt?*ee=_gSPnlzTwC;#X&?^@_F@(^wowij@v zXfDsk*XNW_T5*@1G%a_qs$S6xl~sLDLtCp}0qi6Crhz~U;`#+_ow$K#M4@dr}vHJQpzK!FzE?=@Kac0nv>Js5D4-m$Z_KfP#W_Y)TPDT1lmR=hFLl z@E-r?+xy}Fj`4gtXABo>ueE+P=XG84n${K-wvTE133F|3x|`q#~>9X_CckLFJsgp;$PpV+Z$-ZXVa7VNH$c_Q=St z#!iU=boc|0pV_@XG;A4-6IrL;KYYulN3Ta)TUihnt~M}l*I)8%=xXkoEiHY8@w}je zb6E+?bVfyVheE}tyk5c294di@UhM>)^TLINLqRNoJYT^$r$M1F(@Q~(H}$OKuDP0W z6DCXZ)%Ms4#XT6x@xf@9H$g1;GYT9$+b_Ra*kDEI@hmwwZ-@L^z zaR8VZ#osN#i&$cPA!lmrGEVFV=apgiNd!@GfS!uurfL{p=~)sE<=?0 zEAvl&##sjqM2=a+S-8DpvRKx1q)_zL-ujU{ciQ|k^i#Rh|H3wS#$)UF&t8{AKMUdF z8YLHx_{lz9mR8=K`&x;b_FDHsufzC>RiPGiO|EM9ZdnrXjkK;${ld#A(FF3K}l&*qASz2|Jq@rOHf z70*k#Ez2`^tVIIid5a?A$SW0X=2INsR$q$u3?2~uSs<3-O*q20X{XbN59M}=8#8w^ zePyoMcc(wK8uz7HOQ-i37pI}BRYY%N_gI#HAw#<8oO)y8!@OPVCGs?E3a$PRNM%D| z=er}3s*ua;wzK%tZl*UkD(xl^qXnwT=FU&BOpKXLZ3lAwQ`j6XTblmKYm;)o$0KK{FqR7b=l+P7;9Aqgk+;VWe_~ z6l1=4jlh@JFn9R!4{{timL_}bNdI|;xD|sRQ5wZp2A*;G@~@s+I~$`+qRH4UXc#{~ zzLb1wJFG3MEB;y1_eQ0k#nU>5StcIE)>pNU;aI;lIN(8v;Otwa`4L1=g73un&4j*F5Px2^ijj`SiC(MFXH89bfJ^;&&`ng z((U&HHq>x)Z9cf;GuRxLih6HM&euvb>nwk6x1a9L^bqDcAR&%C_FXx06f1toTk;W> z?605Pi;AR2Yh_H*7}mc!D>3YcZga=79K}eTz0VFqbxl&TdG{-zh5S;^F^p45qdvtm zfAW{VP4=JA(Em72p{iGKaQDS~{JJ4RW379v{_zV$E7Dwyz0A^JLH7k}_Nx#sszdDO zEpXUaJw4o&E;|gOjmyCO1QX4I6LCA;1=qiMMj^}qwLOgX*sh#w=1G}D2!4IY&WttRi zI?N%;RTM+HkJAaeNc7BbRk3`_&0#8Cqbm^0SAy$Ady@B>e!PHF+U6#7uCs?HO0KZ($+9z579Ixr z#eQMr8B%aQI_=aDieBXJyMN+d+`0SblsJQiTw0AjQy>HO5(WBnVc53G^^n7(1}qf+|QS zTZPt^P@d~LPw10RgEoQsV6rm@emZLp`+-fL$O`ByG9JbTAZ`tKHQWYYJO1C*_&O0* ztoyylJ$vzJZ@O?Ih--TXgKSZUX@z=~M-iiIJJ7apeZ)BNc&*vh0Q2_-A6n{xn538T zM_O+utBP&+a*!HVIT%+Zx1Hj%dprswT-iv}v$rAdB`sb24 zE|1yuH~9L-h-eMnL+3p}`~H<9dB6{N{?`w5fk5T862FxD8(NS=#PI6 z`4Ecduf!_l6byOa?G_9=M;y|ynEO~i9BC?8)ekCf&w%l}?Ix7L&q2ZXb=w2h7pxo+ zp@aR>WNUwoyCZDEUL;YwHcL5HSnV40sx`~7-A1a$MMm-OgC%9KC|ex6I9QTi;W$3F zv$N^gGdmS^pd835{q2p_(Q0venFmj3-AB3*MvWkSPi?GGQDXOo|2hzco$1zTvb{*E zDEs>vNhlZxC69_+Tb_D{aB~LvI)!%q1^Qrq^lm6aK4N5cs^b0%tPe|CAs1#Z&69AB z82c;i+LE!z(eEdb-RgmiudoK`+k<|$zy-J0pL^E=r`zLah3q?QXL_@Ekp1+cLrF0& zj(H7}<+uA8bNJxhE4rNMh}w{og6#j*lOol>vpZ8!dFt}rf0rFB3I)iwb5_h`9;U+( zP}V(GOQ%FW78sI`BF!Cyp;E>kR1nT~vcS!YZGF89QAK50tlH zpPmD=A=e=Z5Lnm@i6SmpH695d((A8wwG~3`K0xW-OTt`eFT!-)05|o=M4|Pl7;G0P zM45%buQ*XDR|z%$HcK%d>-4f(Fd_TA$SZMrP7rCZ$n3kfa_wbMv|U zF>};BIQ18$A(m-0^hl2a71TByx^AEp8~*y?<>+{YON)Kg=;wZ4#6)TI#EOB1q`m+Q zXhMWy+m7|%lhomCkLAxQjaF|ndG&agao&w@&>GQUQV&}^H`LuiTCO8P#$SGLZ#<$R ztQ(-0O8#jhj>S~2RrewD6YHBW$o&k&IS-NkGJS=HRqSEnR}tCVyRlo#?N-D&xI#3J z;aJwBcBvXp4$D@A;Q`&JJN=)N@-D}plX7$si@%o0_4}xtZdn5PrBGlxjUQhWFVN~W zaEQNoaMS!)??iXC-#iF`Iz-EMZKiNw?!fZunxjoZ6MTPekDi#@DBXh2B`)V=$}r_7 zdHB#4^|q&BmvYt9@6o7QSGFK~_cS$becZRf8=ANvr{uup!u>V<2DVWTMRbdydt5sH|i-mR6UxK#W$wYt{Lj1VchUYE`Su|Aedld)mmB z-eS8&wKa=ZPN_1>eePBOO_L?_Yozm&$S4CN$&MY{qobL(Ve51or z3>uHbc5X7^5huR*OCF_#AUP|N2bac}LQshES-gFG11IMjA<#{Br0^$wYkGx$WOI71 zbz5gw<+WCF_gV`ltCj$6Ppq%YTc`rPT4vE+3dbEB|3V#(4N3{TG}nSTDzEo6m!GlQ zSOZr@0wNnz;Ol%^qy-BlSBmw@Dti`MBkny6z?`C<^?idq$~ZaOO{B$=`KAJj#s4d) zeAzdR)i4<})f$YeWuQMBP7OG7N+6y2%BbJpAn@tE-1xL1%q0w#Iz9HN>=}=YHyzIq zv$wfJ@ZuZMrPeF{Px;J`O)Gj9F2*HcTp$LkIgnBj8PMbpZQe zB=V0IpeA*W&V0vZw!giwoz*Yh)ubY{oIAApA|4yGwf06z*S6kmacne7)2Rm9Iw!pE zF0axcpmCX$g+zT7FpkvX+-(M@g?h4@Q=@Dit3nzo&xq*#5bw32GhTCHDPOv^b{{R1 zfGb7PG~j3xIq{Ly8Rh~j-Dk{qz_E5FsfuRz$SMn(PJ3ov~%o^z@{y8;<0xlH*yYi^TClvBpsc#`*izHymvee?}EV|%itPd zFj=u#bHQXx>uxj}<76Gr4O^u@91#*%!CDd(rBP0>3p)Z>%#BD`ux_m+n)Ro84!v z?a4u$RW5G-qYd4CX#kPpB8s@pfrgz5BQ1veD{B}3f(^v!E<&g;vVY$*NQpmngp!kr zr-b6a*JAxnf$yvC^t zQZbspPOjT%az71yt+*Wn1K71PsfwF=`Al)l7oJ-&@4k*)3UX7T1MykMA3*qX*@{Kr z1SCdAU!2%?Up1i&$Kc}k?&=9hyr}{%TIQaESb@r~M0odAtG+E>3nAtNt*L?TY51Xu>z~=Rv1l!na?)CGh3!C5P-fZhdp6yB-aS~3 zzxO*&p+Y^N74GLVs=7k;==HlIMCrLY-NPj2pQ%8?TB)ZcAPaX-|pizFB=x^l{RI{0~_12vIxL1@-YD|Ox7D~U1&KE~5pLBmVP z!%`1~J8HjxsIm9--JhEc;ZsukcZbiRQgA1%Od{V@JVbN|6~slf#DH&k=rd|Uz44UR zW9di#wZv7UQN~*W1dEREFWhEFIJjI3RWQLPILfO`y9>j{)3u=ETj`8n&KFd|^8dp``G5V;jFkJAo$YiSU z+@@oFIqmw$yBS_PyJVl0dT*hf{lZ`GQxtw`iJwyA4)|+*A*b56kP95_mtagkP&?nV zLEkS!Z~S;=?e>#HS1)_N*Whu(#itKcY)FSw;yEk&bc!tlRzwR=H3<;y26tKkWKd8k zu(tjHK?Lcm=x}F*)p6-*_Ezqxs4aY_ud7!_{Lp@EWPGmu*gL|2sZx3Kp%Vt_75#2Q z!n2NpS$5;Xn~9F`6{CE-&9)9Uc>{Wzw%^PQtEAb6^KDf(Z#C*|ew`0no2o^U{zoiB zJt^uD4~ULuc8GlpShQ=;nk6Vy*u=~)zc17^Y^TyKUwD-qwO{baU$buu8PKy=4fJlp zpfz6nOxE{{dfJSkUTaE;w{NeAAL>Xv^2BqMW+=`7(Awhwbmhx4# ewu4Ph zi$gKVkE)7fldjR&mFL>};XBVdPYr&auc|6^8yxjfj#{02xxa9&&9&gl$E?cPF=P90 zror}uSMM5SsfJK=9O+#;u>7;UB;B;E%9Pr|_=rdg!S0!B%Ev*H-QQ9kxH|=#No#je zc-|%Xh|(pQ6o_xA3ZCBUG%O~oa5trK5K+1GDz2!;N1q1!=yOM`{i`G=k63h`e!9iX zcuZ7X>Rj$7@nB?YGC}@?k!5~?!N7cW>6nk}<3y)VzW{-2=1U8+T!fsMBZ;l=W~+u2}d2U#QXl03&L9ke_!q^$pt=nzbC)v6>km z%-X-lt1U+tYT6Rhs;i)se=g7nFBGulGvK>bH@-ctvTq7~(O)>g(m&zb(@T0g`rIyz zicSsT5E#zlPN5MF0Y&7;%C4I7829DHa8YHZVsysXM&HG&%rpEBW^9F8Y>qgq{&!*o zShWs@4rLLAYEUF2=$pXTccyy&)bR4{g|*_|@&AJN-og*8kfA044T}_(RAAnN01#dd zPY2`p49A7ej=bsHTKyn7m+n6PWX#gU!BsO@!fSXl-K5f5lX18{V6~EJnHkUMyHO&t z+(}z$x-Q!9S>|Rw)l)6GSi5m&wbwwV-GK7;f6>WjkX3!3z7Pdl8MDz&!e{{O7NPxQ z-L{{rhMuJxzV>Jx_Z{BeUv$0Oa@csSwA!|}%CX@`GZWyuhxmlup8VV#k8gxM z6mNTFMtj*Y-thkKJt9LA@WI;K1!B&fTzy95ZaIxc#bS%L;G{-2}>L zu6;l?%oDbkXWZO(ekyLW7s@E-c_vi(x`yQ2Q004`Dc&+WtJ~*RB1WW;C~iKW*X$*2 zzNuU5c6ZEnt3z|>$C9^^`L_br5WCgn&Wle|D2DGQ+1Fk z6<&SyC@OlO*2CGbx?JdPfA?UG!~QaEPzHl1r`fRLKaHWB|Ngfs z28#$g0@Xtt*L(+Y#zUmRJ@aG!2w!c9I z9IP+Wy+YaSCt>!!=+wB*Nz`~8hrp#4?Qz;QBuLpWQo}|Yg3jZUIr7&s%z))^j_L?& z1U>xiqdkJP=ZP9Aap4l-f_+iAB(kM82^3XUh#Kqe9X|+i7cIMqgyK+G`~DeUY+g5 zwV8@Bgbx8{9gbf5db=-A`wgMo`YOHbqjK;&XS>>!d}}E%5ef>|pq`mnq{18MIt1E6 z2(%dC&qxtDgJSd`xa#Y#@i$KZmjjfM*IS$Z0_yLVqULj)mE!r@i=e9cliNI$C{9~K z!E;f;<4-$98$JN5^cR~p-0)rA^o~+}zIWF-stroKJZ|nI_s_%7y^l>&0@o*Yul0f}>RV&S-r*2)7^ILUitC4g@>n=j_b{z7#ER!JBjpFfvXwy>08>48$hcUW4F*4OvcHa(TQ9)Q@0`WN?;iTo~3V( z-~4#k{SovuY1vlZ^rXfe6-B@tDIE z0B1ktUj4cR_1)MRQQXx(OIf@giQ$wZmm0BXFHHG#R($nd0<@;qY|-D}>m{ZW=4T*4oVfHChM^+!Md*m0N|; zbee)6JZG&>!@3z43049dn96=S@FC3nAx4#iFW26|Oq*@Ghwp@9$ty6`VkvOs7fiiZ zqYz3q>p>ip^mYP(D~oh!DS#NQNOgt~E8`u{N{5YbFKuV zyqb-IXP@I76k>ebfu|P0WXyg*@F{W&C{%G82%OmhgiDYQC~AJ?gPLxkllM&Th3cMY z%|@#A0u$0739)|slvdi@6wjKy%k_Pv=+yS+*OpK4S0_p06^D|5@aeAm+@` z%Met(fWqQX)}8r#BZv(MH>hx@uG^xsl4tDN+B)CQgFwLbu06?ZM&&rI?3}?f6w(+i zr}mUv#TYD0Pp&3YzHv<@1!J5|X8Yp$^os#er1?&K9IwaHH6~iIjW6^yP}OWi9K<9n zy7g7E1}xnZ(BUp>j4Hrn8L5*i%!DKgySvT)EVt@Po1+9q$#tJ|Bc7!G(}80+^0EmX zC*#w~)KFLd(&$*g=^JwT-VFH*b?YWfA+^fXY9`T2fV9qUzQ23402Yc0K`!zdp_lWU zM%F_Cg$MEla&7W?bFh{w!G=RX2PuMncJ5McCOh_5tCo*>);3A#ZjmX3Q=rx#s{~Y- z09B9k9rV;pEYbDyTg>^>F{VCFL!#j#1XfZ^^P4v(Bj_~r$t{s~#1RGrmz}GR1c@<} z!@`V*|2cI3DAa-^=#JsOyrvH+hYH^AF|S3DVUT;g`NbVQb==BUNt_`z*;w0w#tZJ@ zGDCYeqD}UTD!qkxmm*<8u>6)Id<hzA6ZjS}magSmT@MPwDZ?38MFiXny5U}Lk{-M|WHzJwvk@g=0iCgK-XXO-f z9ii(`jrpizgKl#4?Qd*cbzB?fDEY~p#PC!CR-UI%zNZqainV_vvZlv`i}cTAKPAt} zac17WTBTgT)tnk<6Vj2Lt22@JR@8i2YotkzT3Gh96CP=o1E%%Gf+X22&S!j;NqQ=Z ztZn1&Mb*orScu6j2rdtUlH0z4DlgGRRXyubmGwqkR#>1v_mh=$`_FG<7aH;+vi8kX z$Jm{^U?$Z9F5g(?3%!h(4By#XGqNzc1}Ymg)VMeP$)^9vo%Z;#LUyA~aza=(LHr!t z`7F=VzW1}RQM&I}a%_8?b%j~@1N=!VJmPctbdY5jC(n@1026jPZOiHd~bmahznhY^!&%7B}u-CDtvCO?w zTWyMq0pxLQ=fiJnFedVwE0w0JCX22?@A3g4C*OAz=Gup6j~TW^hNahYQZE7DQ*EoD znWdFPNS#$1;l1`mG{APY4mRclxIKx;>G3>I zhJnKy5mz1Jowh}>MBwh)9IodM~#$HKoJk#_X z2so~V4&WNLL7TivuWa8d#7)35GHhdYxVOjN$aK|*9}Pbo&IAj*G~xvL1p_60YDw}V z27~(EGWsktG2zT@dSNS}a^XB`GT}}BSuxfdk)<$T z&HHew5>9TPYN_G+d1aBv(1?`iT8n>nJjjf5L?K9A9df$1g2Kw6ScYZpsvN%0YcF~C zg7R|bG!aI!>Hm*L#0_y6?97E4F3W1chJ+Uy}VKw{bI`xVO!yGxBW1 z4WJAPqtfM=GtEjeeL5~(xj~`k^)O_mU~d+Hi}CHBDFIb6i1k^N@&_e%1`PR zWjTHsi7Sl|HMavx0C#<*ww;Ws;Yi})G*IrKl;=XlAbP2=Y*5{&fr^qy*%Rqr##)JxlQ1}{ zThnGMF_@pT77FR-3%k^n|{9SYcpT+-G(#0uQ{meg=SS}nTY9z;bfUahkoWi}2LH{rq5wR=h)+cxKy=9`4M;ou#RqXYn%OF#J}k50 z{{SkZGN`?L`tca9ujTb$BEkEVuvRk~CCqO?ZA;rRx_}e+#6iozEytZ*t?@cS!bA{g zx0PcGf01$}ke~%ACoEHe7CtA`g{6joz&eQgluNE=lu~NYt5G=Ruc5SSPF$zkjqRiX zDoQ!Rc3(2*M-Uv(Wghpt3xl;yGdbI*sMS=A1v>g@)5@t^>u+~=aAe;#u8JE@uP zjZjh&C~axdAFcFwHO{)*|7MM_8~%R*KyU=pX)qfy9>e8`)8ftkB$>$Nh_VmWDQNxz z9)|Q@^c`mmngJ83OsdUSNQQ}2#=y?tGMk5>PP0wJ6oP{oCX<=qNJ%+1aqqCL+sSO=Q3gB(83 zwjYUZSj~242Gd@DzPFDveeht=?AzFj^Uxu5U1Sxo<$1)k50Q9*+=%LF(9i_+Q?%PI zqz#1#$4^oTM}rJmjf99LQ2Rj|S|J8Vi;6OsF*+MI8ySaEq0ML0On;#*6tk8fR)W72 zK?!+m{s?XWfnXmZO6Q1ubM8KRW45@V!2C@#(iIf6ij1XNl>b3e{IG!WGhCSYW`7aK zTwlU59WAYmBFU2q^}+hDMA#o<#e-;E|-;AgaM;&mU2xO{GySQBsVu4R8!neL( zk9mkKId9v~4#t0$L*Eb!ED=CbRNj_@R2;Iz$&QP@JGW{;yJim^Gj)nhMDk4=>UnMZ z-;&VGr$2}WdD^p$98M7JRS9Q9#!a=vjlYDM+@42ZlNWan)+|Hg%yG9KW&R;}iG>|@ zXmi4nvm^Wgv?*fCBRQ78=KL-T|YY zdxvR0>&fBd&maoSN|5DnonV~*2`Cz&(f`osJCM@YfHo~Sf_o~_J&D7lb}-5bys`Ab zY@?0#Cp?Xy8?R08`3hGsj9Y`Dw7le4joyGhn)Bd)3GWr7~u}^Ob8lIJxbQ0@M!$l#sN5fL0-*BGr~@+1pHq z*2YXKQ?NjZnL86t>6f)lKsf~fSgO~bgCQz%h5T{VD1c3}O82=<2nd$FTnQgJDr z0*!J76dJrBNd>3Cm(Y^!BGWaj6yi&U=g{Dv z&HR-NqovHGsX?~HpS%uZ`=L9bRB&m5k7#rMl zw~8|~%ib8|w<@T4FsgI|U7>JSit+?*DI35F2N4?i6OQr1?nQ6nV$WZ>lM}T)fc@Y@+O*-2 z&tkLGIZ1NaA)fi+s;d4%!x}P-vMu71BfXFR4hi$*%|_X7d5xJ+$s!#jcvlTHUsol z<)gJk+#RfZ+UnKv&msL(-OK@!QSR(`;K{7 z1}8Zru~CQb1k1WPZdaaBg>Z=p^Zdff8=A=g0Wwt;X9daES}D^jXKPeGH1!z5hmNIX z^7@tMlyjS56R{~;TotZS+B#0QOK{}8D*yb5ua#W+2f?naeGU|vE)C1v_*(wx?e&JA zuKVQeJ6A3$)6arc1BGPUb&Vgo#;#Em!qabi-INU@3e-nk9GrR$#L1=GTEY?{9)}y% zfKi^2NNr&ny1QY!BfpwOQcm9hv1W~s%WEJC&w9l2M*fc6Y7~gjMCH@6l;f+b*@Wd> z1p%-dagz8RGjc%H|Ie8%9*7|Kaw$0$h!#xz@nG&CQQA2?YlOs^6Tebyj%ap?R2B^a zmI!+k%f`KJ(o$z}AsLs(;Fm6%!D3|-;9jdM zTX=YqgTO(Zm3sxzbVityU!Gt)mhNd?9f#H5&{0QCJNis1n`#-{kXzNE(dJh^)a$90 zop2+_dNc)6{hDce@@wQ~3M{=NUb&;Nd(|Eu#ym>ttw z>#>}Zp6auEp4}D?dfuC>8QifKqvbN0@Z>g3?*n+nu0AHD5~F_bdpf|jXB{v#cI z!}%P#|L_@1q~@T&$$xp%J^Q3FzpmAa{CXq&8~`CyBs|%d#5{!he2|79#M3-bu>H`{R~U>; z73-{f&;EIBY@E2>#{+n^v>M~NQz7UeW4VuADe%TnAF}`8k#Z!Czy`o`@eYI8Y@!j^ z?;bim*|w9kCJ`~P8xPuh-<5|a zfc#&i={+w1yVP}^Ien>pS4Q8nsZb-{=n#9o;~TuZe-jc9yVs$ZtZENKbKmtfO__Il z51zifv4>rla!>&BqM~kq`=ri4i4Q|C1|{>-2$I7bNAnCxN&uhhTmP z{PwRv+~&U7TWC1YuXpvZZVR4-0`Vzu+xqru82XDF6*0+GG6&Bf+5&__M;JHvEeQRu zK=CaK8-~5sua(eg(tgqs>e>ZnF7im?SucSZZE6q;wY>VTwpCO0=(YOh%fbAQ$*UH{-*1T>0nikA?!&OIk zC0&WTLQ>-!1kaU$6in3uc}I713X?K)1>g4Jn5&*Txs<~KN~1bzvn=*&+(=WQy830S ze{=AOn!R;RDjPaO<8|J=#?z2ky4*4Q(CDu+NCx%6hzW0Lu2o$B3Ov9tQV|D4u)D7p z5O#&`ixADHAQf+d7#`=(N=%_hD9XfzH9R6XiH_$s4>AVpVU?Uq;HXa*;W8h)XO|uT;OiX2>Liwb#)%kGgRh`+%o;0ZMC1(?diFeTJJG z+oIc-Nm7y0hYI~q3UjA#zV~|+<5LA$g>Y*`VG=nmTo>?yC&7Ktm)!!f5YE?ZCc|uR zgV(bAKIP5nk%{nngaqfLSGqG>F<|1Xe}&vOFJ>nFan5P^OY#_HhB;uO;^TDkbvV4B zwSf_F?4vL=QWZ82Nw1S@*Z9z`y?o=Oi0(@%SJj4QWgUdlHh{R=Z43tVJ{p!Dllfeu(dcB1Ai+jMTTM zAbhu!Cybf}TNAIBS>SI%^o&qY@JBVsm@rbw&Ouwa>a8B`DCe({y(!eZ2K&@_4VawB-U{g@~%QC ze&-C98e(dD(3Lf<5RN7%ZS%<$FrR~!<31#L_ux@ZHbpZtKo4B?+49Pof)LB`4s(JQ zC2rWUp)S6ZF)&Tpbr5=m+pWzFsq6u8Mc3TE===czh?Kx|oUJ#FzG!O_+6!HAKCLus@6`=>pb#I{qo3DTIZmACIi_n;YexR(927 z$1aoyhWT(=kZZ&d=ge`LUqj#-&U_%4N(Ri{@Ae@1fgD_ZWT{(CAsT6jCB{PmwO~@B z#_AVdkQ2PLY76aOQok2mX;SC;%oWe$FK1ZU;6;r!DMJ`+zfvW{M?WU^9QCHSh4(3zjNW*r3F#mFrpLjk;P znyGXL!2tUGTEs|aoCKoTVq?oedxA4}LX*11gRCrLD8f^b`mrr4G6GCgrRN4Pnd{8Q z$@$yL^PVOrqcyf8SLt=eR(Q8nhRnk4!*vaW<@FWYzJz!77>)z|c`f{;4Iv?G4ugMM z_lK?W+oC|()&ipI%g%D)5=7Hq2c18&|9E16Tu)#5WoKAZ+dLu)J&EO@Y>C9`ff@?fGIJs8l4~5% z9HJBFx|SLbaS80z{I(vh6-WHp{-5`+Gcm!*lB@Q7X}Yvsu?!h&);E@PMQVvcgNhq{ z+V}dPPszk&NL5ZbKBTmiXH|(?&RbG*9~Hm3i#&T6rfL4jpJRM7=Jzq~^HI+F10qg- zlU%lmhM>sQNqySvpd3!F5!dL)VYIJpt;q z5(7#3L)UGUvC4~A|4s1G_mBS$YphoH+eFa7iSDo^08KVv)&+3jN%0YXgV1chkN=Vx znLtM}9P>A0y0|@XDewt*O>Wbh61IDLTvXosHXlBgNvuU|NQ`@XAD%-kXoyAji0q3I(5Bk z*{1jPC8D^U8Gpy2&R>{!`HAh>cYx`49Qw8M#+`F8m{iC3 zvPo6w3TQ6jm~TE9r1&%Hz=~Ws_G|u=vP~pZp&=UfPf>^!M55skbeo_H62}l~0~N1{{1%Ke6fF%hK0y+kt;Hm_y z{`1JfEvhFodHDnYJsm)kr&1sE)=*H_#3OSX;C43%qc)>|K$ZP49BqyObH5$-sJr9v z^aJrLi_R1Yq~B0Hk99c6|2#azR4ezr6mqOTMP0rlvHnK4Zf)@DQDKO!j2pxtH<$;h zU7NuQz~Ud*0Q{0|*#g0tWV3F#jBWv>4WvSqzGod*^_O$QfY%jB1|0Ff zRCnN+gSKDF=7B=H-UajOce6Vc;7k)~iz}HxM;bP7-{{Pz4n*h+z7;DV&M$>vrg2e6}kf;!SlmZ70%htBp>US5o-|;oIZvB!Wr#| zG>}Pz9u+!?vvf35W)+(6TsG{^i22+adwB*~W|8Y^8kdpe5Ysr-yMqkvD`}KB7^w8a zi6tk`%0-YMegOU`WtQ(M5$^(U=^1$m7}mBqSfkjhbG>@f5nn7rT znu8==uO`@vtp-baHoSHdIdF_>h@eATKeQm0iN)vLID5E}8WZ^}I}>?J3MrKKl+bH8 zW2*c_P}D4rxQ}~Rxy}uibkpVwspOW6yFt1PwWJPEOX`HiksscWnF2{tB^WX2fr*r{ z&I97)4V}-o_a8a${qj6m2N+ssM$N&1RXuZ_st(qmW;;mh63QZ(3i$*=^9%!2YzH<$ z4h;}q60($HC)#=g@5aNk8a0>SEoPTG=hWzNsM)i%yQ@n47`p`-g+C3}ZQGB!j}Zvzttm z8h(7H(e*WdnzKE_MKJxQLuT2LFg;U9MlN>7XBA@adUK(l5Vh(T?F5;Iy5%MRSk1R{ z(n{8ik9?T@mTWZ{G00j`TLG`E8qrMvu?Zj#vpf}Q;yIo9i5pMJwHksD4ck^Vz>OmD zsCg_OMD@u?YPWE@dI#*Mybx-YR+XzqZgpUF3wmf(*H?pCK#g~ANZo^r=5jfX`6fYO zdMp&mC5Q}aw4$0|lmF~0P*eKu^+EB3ETP#rBSg*o1K2j{XS3aJnmV^}PNwidjg7HQ zTa}3Tw*Ij%(?41OiRak zi?SO~2QH%0C(o;RA-%d`+F5?v5&ncd`0m0XpY`PxGK`o*_0ddA->jpQ+cUf%y&m^5 z`SxaiaV?~KY_r~c#;6~ovm!||F*8$0LX@mN6XTO?ykRE*oUE~_*4#>Lil0xGEiiLp zzB^k5^RN5GAZ0f>AvU_cBADqV1?|k#Qx@%7t9P^yb zV_!#+qzbvtT&o-3^mfZRbN1F(znZL7{amjh=y@|bC9_dx=9P5 zeaU;FKSK|Jr``uEGgg1Jf>MrRekyDSzbJ}>AnkXG74p2^^>PdG!h+KEN<>;u66=5H zMD^a}?neHhYZU9~XYG8$0%2bvAFq+PFDkX_m$&y0akA(Ap6Av`)`QCessE>a2|Jhu zNV%&&YOetgX-$p^Cs*(vxGZ;66Uq+?-*9(I;wl49N9Fq!o^e>YopF`-yLRQn?l|7} z8u8uyBDbbf_3rXjSk}x}I`%EQ1w6e}f@iGJJQcFx@RtOqN7_#&eLInq?=t z#ZgGiaCz!Q-|l{09J^-hVfb|8}3`CMfq!nG6_w5QVR1L&WW` z_|S=U0PX~G(`NPWz62h3q_q=lB6hxm*rCSnIbaQ+iLaar%@sqec^q!1|BN9kV#cpL zFA$iHize&d0X_G9ic==utSZ!pmL8To8t@ixRC3j2?>*6^>iX7YQJsd{xQZ&>SA28Mbmv)M8ofH?0ddgrf5q3MYM3U9a;)m0oa+l$P0}n z9N)Oj&z^b{*}4H4xcR37E>f4H`cUOCDk7gupBKVy=IYoZ!Z1D`iY8rpdoIamq|e0o zW>n_G^)C6LKvXn&sAy824-#_53&naX|N}hm6^ksMN`(>I{QZ2Qe81WQP@azISFx zRG%qb-NTB>IC$}}+Daz^L)ILmaqPL9RBt?V`R?I4Q0u$J(h*i}3pPXGitR*Z#vE&H)qdq z+{oIwbBOc${!8|rP^@Zk$}TWuX2L%2?n&#=8K~5P^~Wjd`8@aO^aogYpl*oD^s6T@ ze_%*W3sAYx^UaK|D;+97xiE~eOw_546Ns};%(oQ3IzExk*(-K$A7AhTF5kCT)n#X; zA_dFN^SX~teTb2OrR6zK{?1{v`q_(OK9K~r{O6m|LKH{A%=$^f^p=U(3Nf?9uzoCh zDLB0&TQp7LiJn}y_3a#qha1m|s6tA7e7ohDI6zxJJxRh~d}zii95=Q{r&Q|h5pS;| zTZ#Ri#vy=F+hj}0G_T%cJyPCmdbEL{RsS92yd2n)^4zo2^=j%UME6pZwDGxr80kmI zpMzjo7mF~IQYaRk>PL}**|suE((~ zuJOz2fK3w2YD61>V)9I$TmHDGx8b^Kh2r+}MQ4WL?elVCQDo%#O16$3PMsAWel3t# zN}mykBV&us%6H*NFy5cxylU3OsSMQ;csd8_wAS}DyesnEo;59cm#btyRAl{ats~>O_-597Ty6UK@lOf8q!VrZIHH%$z`;tVx65AY7Gx>kmd#|Xdwk=vz zaseVmk{k*Ok)TKvBvt_e5+x`|mMo}b1PKB~RG}y`NDh*ts02xp1qn)21d$AaZrp@$-COPJ)@2 z^_i1hIln_|U{w3-podQDel2~GmL5=I2{zKQ%MdJhCwv{;47fY>Mpc^csmf2a$t}YQ zh@R0S)40rGbUJW+%X;Yp9r#q(7^JS#gb_VA@}4*vFPOhg%C+WtyHEST@j7fjWYRia z*t#d3(w`UdCbiht@fIU(MZtKh4cJ$`Rfh8(D#vzZ2GkReAG|iD%MQ5+yz0Szf?W5fuNL&c64EX>6 z($$Zd50|$x%K|dk8bl4WuVbykuWG0sTGODrt)j0CF6e-QSzv&zb*33{R1at1(%@9C0W3*T=O7y*8X+C1dt$f&S*tmOFVG&MYw z*eGdf49v58Hh+7C+p-?-M!&}m!UHIc{LG;~`C!>Xhf%w`pGsNADMHKpbx13q8$tbAQzf-c)Z#cP> znejOJ&8@hD=o8R%H*s#GyZr51;~V>5Spk-p0VhoDSoJ=;q)SfGrbQ_0T&3sHs?5?z zQt20U^uqHE)sd|x(*DL$vXE-bz-guI2Df(;hox1Ki z@&T=KRO9q_QiRC|zE3o0AQ-BDDC7R{=9fVdp?fik&(q;JY+=ja1mA z2r$2**8Y2hY!d|^yfN@jk)?5)SiGYxHzG4$W|l(^(mSR^z8@{jTZMA>#tRr;cBsv3 zoxYirH`u)gI9XP}aI-H-fl)jR2A?DnIZ(upwaZ{ae|$V%ti* zZw^Ar3fTw0Dd5Gf*(>0s3MFHN_GKX|Gg7e~w)!QiqA%AOD0sfnVA z`|gio!I=6LMc;r72a{b@Rl7CQESJrR$PBc~NGiLh#(mv%ZMoRbIv&sR3BPWp4D~J5OzJurlCYD(JQ~ z76nJc^et675lRqPhHs^L(;Ywe0yqSm5AslT#2emcbT2$7=E6xcVeZK%UdFh+knP!? zD#I&lm>7QJ=lukB;1qYiJF$W2^O-vYHojkqLya_^4-h0YR1BO^CoraXz-^{S63*-E zwESHWtvWfUe;c-lP{vo7Mj2{(KN)(D?bh{()fwh)&?Cuhk#^KrnAg< zaWZ!K$otW!3}xeb4N8~7P%A7kKGT(@HSY}FIX&!I1nc~znWESBPs(`tBK!}$wli|< ziO1!9pJdctFq9K7O>&r>Ci@dJ#l(kIx`=kgtwy~u}&y%eK2h;?| z&n=8?o?qOhNNRd-iTIwICfsdX@+HeRSde%0v?#ucGQ`LiT*Qd_YFFqHC-B4|7whqg ziCd-%1K#8j^6DxPm|0UkLaWT*l8b$$ZHUXc7vl%=4L*fW>BtJVpG6e4ulY48w0pKM zy=@p6lF=q0ULSf>?pKyN(m+kpO$|(~Dm6#6+B8PuPx3s9j43i=_rH6tb$LwWC8R?5 zz|y7FpK;lH_9aZtTcBr>A?AD2o|Lrl5J#+WH^bhG&*=E(h#Pns6zWw&x^2Ue_k5UJX zk*LPS@d#Xnl5m7sV5(+1Y`^ZK!1EVYJy&jImjKvM=ankhIAnc&Zc+ zHL_liXFqWvC+ehbrpCj*t&^8dyopnB%+<)4ZhG^|NZ}^} z{^H5%(*vAF;7uJx-7Taczs-4KK_UOg=Qp+&_lvA6Z;(0EX9yPYH7Ih^Z~Qjfb1VfI zY7OZt3=J-FG~|->!-8TK{`APKcKLa=3j)NsoYa)4hA;!VzNIvMUq_S`Wf_Ise~O zl&u4NvBJNFa(GbBAu0vVXC8Ku>7R{{%)c5Rmu6nP{d?R{&0mcV84uu7{@(^r0Q#@S zhnp2|Vpnp~3SGNdxV%h0h}{i{I{#mfD!21Wm0 z4Ioy-Zov!SS|NMa(kAFy`WrBNXWO;yp)rn}#wF1!#rQuHPm-tcQ?tg~h-VFd(&)KN z^=B$f;5P#bi>c;IP%_J`01)jpVD!tqblGrr@~#hhE$G{LcROKgr0nS=gzN+GR$CT6 zqj%l=-4WT~^mlLT*QRA@AL38E*NnRdD&i(w3u#6&j(Og#E*`nuH!JO!?-nKP7%4XV z1VB~#-8(zT2j2UwkRAN{P|7#s)eeW8aA5VrRC7Dt;Zv0c0JtDe$ycirb+;o*E0?PV z@$px*QWj0dj-(Xkh!P>0INcsD0u~h{)M%&=9b@bLyq0(jHwsuHWqM@# z9=>xy&hDin<#ItmTa7-j%)7t(xI=Ezq1mohUxI!O0g&8mNu0<30Jh))DKI%2rf4Wc ztFX6w%PawEi=&85PdQ|4419VY09*T48cD)!z*-xmpQ$JJlN+2*LFKMaspqZoIdm4n z@%Z!KJ=6zYo-gHR^GLMjAs!RR(gg^8MK*!q^B7OM&-9WQpm=!>@K-5O<`j+qs{>m= zWfDqt1=4NE)BtL;2*`-Y{~Tzf7c^bjkpwupF(c|rdPxjKPVs=WCzVShWoxEM?;yX4 zk)EYYt?hfj=9s_bG3*FfT6YVVLf#Jss-}w;hq1bv6@Uv6CuRNGW2xD6au~dn54W?Rn4GMid~sTCP_;8&ZL{XOT2t!U zJZ4)WCbCPjDZMLQwS%~XC;P+TLCtKB@CO0Q4r%anrHBJ{Ji@)^0B7H!BjhWzM^Z-9 zA5DkH=)nSn;uHYg|1~5d7z5<(9^f@?RoZZH&lN}xQXt+|cLk8(+&TwIR%$?(qvRQb zS?D=dM}NraB!`YPjCm3wgUv${9jTWPAIuoY7IwVdLdukP=R^H|5uiRuV~Ko)luv8B zE~NB)lY8{bT+J_w3u17!ffwN+3_1`WKVLO7f0oeA-mbps$*X|TPM7uV0lW9%&j|Ld zM?m&eo~WJl-WKv4PXYwJpYz62fT82MHK3dVcm1)Cx;?f{7F0 zU8@dQAZOoZhk~gZ)rO#Xa~5wCOrp2F{BBF<6Vw&C5^`V&XcWEAL_XSqoonsP0_wIQ z|1JD6F6J!@z;3UoR2S#7;Yc2kv#Qz}R5Oyy9Qio!1EjYF9<+t<8yAc;`xq0-=%<$7 zUGf>oD9!EY z(PdE7Ir`%12lx=}a79Vq^=+6$?^a#N8KZP~KFK#AHz(OAy*XM z+#Y;|hBYtZg4)96d~klXALxpbjeWRy(y6zG(MyKX3? z=Tk27%US{k;qdF`5ZgSl+lUz$!jobTqR((2dhUd4^;s@wu7~(6H3bmxRE+fBneOY0 zvd5h)f~HYwzRRbMn0?2Jw(}uNWOw6$jZf~l3H1l;392%9t7DfJz`;3^(_vi2m$F5Q zvmuJV33Hg-ui#Fy$?4lZ`$C2nZ*6oW@6=1JzU6Yi-~ML$)4*dty9L{hNY)aQ*=AD7 zt(CD)?T%YNR~$>Khbt=-0K$Erj+r7ocvA6pGlf}LnfbB#JMOuR?X}6tQcC0Z;}{JK z9>YGh$b`|DM;V|IP_u#@dgS8Az;C73+?9@7b$X(>DVgbom9%3ZlZjW*XDRJgJU zIARPQQixGpvx8mQlqQE#mTDH-Tv118WFLlrtM>3csejjUfDvc8Zd2J}j>K zNOl2O&es-FG$6=1q&VCXJvTZ>L71|Sk?Lerkyu6SE8mu|k#~Wb8(%o$8%Rf+@JxO@ z1grAd7&NpcOT}qEDhl)4h^UKplRNP%fIS497htUZeuViw&nFz2l+*fehY6ovOP7F# zb!xmAklC^V)-hKgAr&{9g3GU2{mTf>tR&C zWI(00VmqbIsa}~FLY`x+B6lQo*hLaffF~);F`(?P;u1}9ph11+0wkQ^K8s+|1dLI0 zIixY+JRk$74oO*s$f2(GYvJS%#()B_gnjXI!6(H7t-HO)phI2qj>Ny|5jIBc5s)_S z=}GY=z*` z)Uf&yWPyfrK|WnzLUsVraE&K*{##o3HiaA_DTz0*r3fn&+{fgcUaBOCaCURg%Id5M<%B)a|x)r(= zn-mr+ud7$+65 zBhgqTa(zvSmZ+)ocb~M2JlwD5QU=gH$HWw#?_{w;!6}kd`mb{kC&@c0 z$0a#H!*(<(85-ppK&x|PRg^~Yus6X|IP)btzS0)0jQLkMnLP$x_w8BwObQtT(+GOw ziL1)F6N7GU3`Y;gD-{DquI4MvmnyAHd(tx!uXD;P5p-R+rrx5ptqn^MIR4eK@_|)2 zv7%p2oWO#BH`IsYf(o$-0sjhx7iJ4!0#aPzxsolIoJ2CgLf(GgFficFgx#2Om2h{} zZ-u3$d!NNot~c_YDr~^BGhyn~xGgG$-Q(Fqj-=e1_lZs6q_c1|u#HoF+Kakj-qAB8 zrji2o1{cl>clk;-6JqSf1bZd>sbt&&yF@1aXq8mU4ByRu$ze9W;tgYE9kxoNE#ek9 zi{0MEBT89n0R`{nK1%ksYh8&)*NF(2@sJ&*Hxb_tpXPk4jexDb7w2%V2ojLI;-R7& zrp(kpWh9`FpdGyJ^C(U6sv<1fN;kK4vx+bFg)cg7reeGQ(YlZqEVH;8LveWohrzC; zSI)in4weh47qGrsDmeTCdxGf0o|Jb5X*COGWshb$EWm%{WF=Ho`Q?boUHDMtQK5Oz z_o0P`lYVkW+^GNN3aQRTLCsbtzq9Qkd#Uau=`D?yzcneFzkz12Y#+8}wvv1eIQg3k z5H9Lx`?K{N;`~ISfm-;@@|fcJ{U7dqKx$Q}(@Yso1*quna986}UdH-lT!!PHa&n+m z?)76~qBICa&UsU6wLoQzs6Cd}( z@*nMhmcRoVL?ImxtjE69Fosr2OPl!@*NJFRwOp8^YyzZk=Z7+Qpt|`RY(|oKRJ%bF zeqeBof{r;?XMEEElrt-1kGd~jK|TpJ-|SFM*OPe5M}lv|B(-Tzht^a2bu|#p=aVlG zxMkGgH>T7p)i{ z=@x!`kpIw3nw)iRlx|~(G!uRqdCu_Qd;8BQa(pTiYjwRGr)N~Gp+3`Qh1|xLJal8$ z+X{orksHqG{zx+aAkB@SqXJmK*I&#dK~g!d>L!m@8%4!>BlEkRy}!$e51L=Vf*F*i%q0#2|PkbFjBsfIXb0U|Qi#zle`O`u?S5 zKb5c8vrEct0`z(qUm3Yke%YRV@;s&|YuRfh$eqy48_=x2 z*vGWG%5}7JNPSK2nv@uvK71-Lx%ujoHE1485w(H?>k#Kc`j_GWD~!3dCj9Fj z^Ge)xI?@wjIp)FJTLnyL81BV$I+bYc(_-=l-}uUHfy1m?u^C-oxwPtdD}8Vb@H)uC zNk+MrU3u$VcL29?P;U4o_C(%gT9+eWg35DOU4?VlEAmLl!&BOe^Zm(gg0}j3Z%@jT zW`EQ3X;?w??{0(oJJh#l+jtq@XZE$;Yb7lv7!BfT95cB4bI#zx<}}l8q{^<;I(=&a zGmQhC_j}i>wD~Cdv#bw5Dt%9H&lBkMDhA`Kl|lOTQ)r0B;mH$N<~kkCrPv;Yi}3zR zT`=Y_;60j>R36@Rle;^Yv|Eh|eU!@H9Z_I_2;=VL=nu5&R)gKtP;_cK7dOd3-z9({ z?;=An#ZeL>SuSw|a4ecvSSizIHS=I#)2bV=Rno*H{w|8yy;5r`G@&D*%*qAoDc908 zSry?&=aDqZ-(U>9MDwW}u^zgM?-^7@eOiK@b)MrFG&}RcVE9(0+!hwadTbTU9YPGa zy_sksoI)oH&dT)8jk{>ZtFBgOr_M;WJ&-ys-|SjtmGajME8kS1JDV6|xBr9S;hbW5 z&tvk@XhO3y;xI5 zeq0m4mrA3XdS0M1cUfM~)Y#lkuv$jP#W{88gm^KsoHu+(nn`tD>a;@LiBSA@* zf9b2Z@aFOAT)ewnEkk8`4!e5Q2Z7@vZiFKD&CUnwyYN12^8)>~t`}H>*6VDCO(Z|& z``xK^jxLelJx6pN}8mi$3TUF#sx?((F4CH_XNF9K{#ITjT}=39pC~3Q;Xh@?e0# z>ox9mnzdQ65h8u*3qR(%-KF)$6;4CM3=P_Fb3Hsrpl*F~plwv)fCNE3$nI%%cCv>l z^7P5ddmaVCo592=9WC@la;;%9H+rclR_Ee1IPa*Q=?k3D3oRI`DHsL=#dAIM1ceJn=(Zh7FRQ@kPV3p0Emp(ZJ4i zQp~NrDfX(YQ(jTt?}kTWRMQ_0`_E-$P) zM7bFt&$W{s=u%H2$ITY$mm1Ty9yY%BFzlXro}Vl>qzqq9Fh`2+tTKnmd6eb1Uy-NLzYXN zk{InKO%Bd}-^fM|5vT8C##+=VxuFO&EM+S7+5+Km#?!53zN)W#VEZ7+>^*3&3CHKo zH<-SSfVa2!C^TSlxK&}9{MZ+x_B`3d0=H!*6X{^6Wn;1BD1|K1 z1P5tI(@Xq#_cL|qc7$(mM(b%+63Uu{E?n=-L7e8+A`HP-XyQ&~jo zxIy=D)yfB|@Ak4{_GOvX$eO%b%U`Oev0~{+XDqj1VNaMpqiozNl7%@)9+f`8 zz9MBf5c7HL9zW)hDNM^Pm}{-rXd(QI9qQ%gbnLVHK4_S)lc1UH$KnA;b@&$y3?crr zT*@7WVN>0q3cVQ{6ffX$Eu*`!+a^-TIps?n$21fhXDuahS zzy>-xs?^Gb+6B@1LNopG$_RA6_+V6=U>Lmc`O#KvJ5<_RcA~OXIKZBJ3dix0;UPE( zF;C!de6&0a7SOKDajSs?oxkp`z||84&YvTL#cq76u-Pdk7~MrHckcgQ3w@AJ@z3Sx zfW_A#ir~8yB9ww6po>*^|MR8Lg&6rCTyTCfIO6kOZ6?(mbf133Zq@;?R+nF#7&6+* zTI%07@cUZmZs<6FU#^$g294s91dL`1*PSnrRu=$}OE+HX4Et6bDVUn@cYDSY-3g{2 zx#N$1KMLGTOHQ>8cep$ejWqUT|0Q}+}XuK>y=4C|yjG%>K zovaOh$SnQHY2u1C1^3P)z{6V}RxTj9Ev)6=TMv`~J&+=Hqz|CS6F$HMCMnT+%&?$B z{QaaQRD33*q(s~6!J_Vd8e7Uw8(?s*!5F;&BxxcLoh}I&@+?PaRd?C$u1@qqo~XkB zrR;H@dsjN%Roj{@-}W7%Q%ki2Qh4on$H-4Vvv0tBB6J_J&T(k~HpP72DyDS`>40-A z=nBZ&dY`p6@~(4WAkz2pL)^?Wx#2r~%;*L1d10U-E+&nPlSan-cNRG)?SH&c&cB;> zXIU%%6j<$ys>>a3?eL6&Sm)J|v|H=q%r|`i&fj==goW>cyj#AB@OBT*`lyXvp7 z_yhX==RU6seZ)n8O8UhOnEl-S3Ov(QKf3`P$Q&R6mXG#<)f!?V*~x!0U!p>4IkFf}jv4?Rz95-o@!2wKE@%We}GXB%d5 zUF*CLu$YHQ^mW^i z{-DhQu){@w6StFEE*<^8>PPUPM=A-(UXQjxm4UO9%N~$;=x!%1UE+H^8ZA46RS;>|So)6(U1>M-+i&v$FFMcY&4W2W zAT6&|R_X`O*#agDUdMyFx=8j!pmucWO(FcCU&%dBfRg_4Oyo{uz)yCMQHz4rYfs^v z70ct*MT9c|A0C zwc59^3F{euR5{1I!PpQL+7$9EyAt25DY0C`CwNV2fTl6n@fVN*tG4lMdBvw;ewnk_ zH#kCIx1QK{e{fJivsQ~J4LyST&UltS1bU0@OOP)51Ji7w0c&Q^O7uL?06e(A4r%^( zr(BP^gMm}K5p!D_q_}q-vraH%ZPMI~6j^ikIyy6gnTfY8nLiOo0nB{A?J%y$a(#Al zC(tALQr$lycSNxB3mQ7-p2EIGC9SYA@8<|VS|=D~b0PjwEH=^+CKkBOV%Aa{&Z?V) zvvxq7b~9bI>Vz-$z2H1@T+{NQ=%XP!gLCVCJp*={`;r9FY0g*jH< z4S01Uzd!i|@^dRrQWL1aY|A}3HojMod#A7v?f3po734p^Gz*FF-SK=2^@;8;P2X$^ zt=Is5)gd^$V;>_Ha&%mr|NeRbS8-d7RlnlB?{VSj`DUL;0 z1kj?jAho{&R-v4%fcP75Tr*#qCzMC>u;>P!<+SG>^8wobpH;>|qd)earE^f~gb+G` zK2~W294(8L1UAIL94Bc+a{h&-oTcB^_cxIJgQU}zmA7eLAzN+V$-`Tm&(4t+ zv^h&tQ@|tw(YVWL%J_k+bJ3phrZWhPah75hjLlG$DEvYPPDLKf9o0xtyvrkth|TRY zjKf1(jh@-_1ANz=Q6`TrTLeD$b9i75>6k7KPBz9v`UO&YxKqj4_`yBlHY`Q*$|{}Z z3b4C4Qa9<(WpZmegrwf?Knq}KCNiX_OGJuj4>H?iZop#=$$ui>esyXW8WaAM=Ng3} z=3_<7JMeCdS`PaaC^f2WSCuaJ(5t@wL0 zeoe8IBNCPPgTE%Uw^U$~x`ob!9Do%V?FGMK%@MwV4kSV^Tx-W2=JoweGx3-JXj2Z$ z@*yyl!*1oy4mKO7k;Y{EMq%D=Va{2%9V09rI%44d=t3G1afheBv!+|m2i|^ygOR@P z>{Otf=^iN}P=}g4VGGx448%n=7V7d!PD+e|0c)UPX1n&CRZf}JJ(dDzPv}HAv64Rr zO0YE60R?%r7Chcq^;@th1Puc_DISVD{JH-G5J@=jyKrdQA)g^J&qf|*?psZmokO%n zB2gRx!cud3(dq`gZn>ga#S{z4M(0EiIVSmkUFzxP$Fn z-H&~eaTT1CUWOQWfPcQfSv=u7;xOmhlbN$A9djHKh~-2?nNcN{f!?Xz~CJz{Wi>Ec%;+*xgWyadkl zs@6+;*7}OUP?YZ?%LKt@oVrRh?CjhUb;&Es8of`sfb+2hTG=9`vWTw>f;l|!1B7(q z?PS3pUhIB*nL|>g&2QwxvVstGc%FR1pS~p}fLmEDXR8S@$*x-<59ib81Wh=f4`K^2 z2MP!FkvUe|Z_7R=%9DdfO`zezvXu%C^%_19fEFm3%&^}TXNoT!{?z;u=&(X2rMPh8x% zNRd4Tqs=Wlw%zCbJPkcwV;R-TjU9`FqYWLrdC_6+{JoRcsyMhLfsBkK_+S_-MoYDr|QHWK>eJIy1NGfTcE^+&kq=Hlh?`14!DABWQ_Ge7*Tg8n9;!Ycq^it z{PY@rBdf<~<}3qExa5fQ%EdmP%2mNoI49}d-H55+sd(ZGgmEk>h>KhqbD<_5faLBQ zafy98jeMm@uX|~OngLqt|Qo7|mXeO`>@!oX$}jt@4^egEL|OF_>e)|=6r4ZPg*D%VvE ztw<}H4JxiGHwCT?JkRz(iY+Lp_~p)} z%!zR19g!glukWRXT&|r|phF{n*^BRiU_C|_g_K3sFIa83$`XW%%6w0Qvvo@LCZLcR z4_Qv9I|am*t9eLWJ0JX#1O4o5-!uofN-@_0?^`Z2aY_arKc=K?%RK=mZ@lKHqk=*w zOg~7YM_edIoGD}zsxS9RAYN@4&woLldU~X95x`#{CRO8%(yC=Y%=y;kjYn0pSGJ3- zgl)64u9G61Tux%$$$AiW=nbAodF2Am%z@BD89_v=?*jRzO);{iGxbI`wj|i8Eov6b zPdutR^~=1mC8P=qzMChIzG8(*%YGU8cE+{S0p_v7!GB%_I=J)Z5#Z8Gk22+sv{8yg zG$(BSZO$U%&NU;NCoO7lD;i4HrHg7e1bKJOIym?{zccjg4_%~xvyP91_UM^XXFJQPRQ zDd{^@DVx{rh_ShiBWr}CjcNT(8ZB=P9!F6&9&G-*0g@fefYHXBsyKYPnub));xV^5Aw z3Pe1-Tk$@ir6yt894Rf;(aB65)k^ZL60G45qhl1;%T$;U9})?WnQFIKz^K1(h9l9j z*rS+OslkXJD&W&`SM)T51YuY4^~1QS4I?|81ZQBv)2Z|?N9%yaS7dlJEW+u!@fZJd=HOj+IY7#6B%n~szEs$bir)O-W^ zJ3(&ST{q{nBjAuNt3XZoSyl6BITtR;ETB8YnO<8^(KsE`IOM)e>PN)aMB z9Ni7Cf^a-UaYyDY6O<%m!4NpX^YakfhXqKT&+~Os;M_faj(Tx62lQlSy%+4Tb$;0r zO!NX1tu#S}j`g7Vs_Q(h!q&+Wd6r5Yx24^NzDs1^DgiU+fGvzPMk+pS#c-t3Q!8Y^ z-Sz|n+=Mk#DAR*iwYV@0BF&ec-?-RVL9RMPh>Cq&Ac~|2M+m+_hJ%(zQf>>u*+E$n z@^5j?(gQ+#QI~<7rp_VW5g8Y$l4zUe<%m-iyLlc#D2e4xcL6yk=G-p@)oZHw`kI|m zYGyvufiuqzB}4EEFy}a2e{J$(DraUbW87Km_TdI_DXis!kV_{RRf4E(f6* z^9USk#{uukW5PApPY8HO0xPky3g`N-sumUPxVVyX3#{ygw8vbBMbic!fX)Hpa3-I| zTP>K}TfGuSg&$4LiD|mlSI=sX3n|mWnj6(y5_&y(KFNWR_!#HY0pFPmBm0(%S?SkO zmY=%Hfm4m`m-AsgfLGe?>e!oR^&a($@x4%S!=2y>;}3?dZ#ZrR&RkGS2TRyZyw`T> zDF8ki)-d_IMxDgX2#nVFyrDOBOOSKUzHpd(B-3~P=A>>pPvb$VX?qU^xz1=S7T>0H z4$p7??+=2tluY5xdM~FoC8k@wTL(l{&Kv|BZJBv2capk2aqwdEe((H3`bX=n4da9n zO8IA?94tPe&?ngR>YR)5cwRO=%0S5Gp#`Dx zFYv7~TWQQ*WR<-F;xHFTZMUQ`+MVeVQe38(8A5^;T54P`=Me$3(EnThV2sX>Zu)R7 zYgpySX9JA9DWQb;b@Sc_{NV`2^U4!@@5Yi>MABPv;Z_ZGZu;4rSR|6k{lzxJ?W!?p z<^c z*NH^o2=l1xzg+A+atp+>?geIF>K$E*_7UYem)kCY&ZkB9ZiANJ>7jU=ja_}rGcKk3 zxkK-Hp2GP|AOB8+A`+^-i`nrxF-xH(X~snO6)Zj?G-&;%5~JY1@!CJosxIOWw5oS6 zt2rJZyjZ;)#2?6347fjQ{~Inf=Oq07?Kxp3q564m4qyUV`;5-!1fa_hKunAOudn`HY!AL0#(+|L7}@6Ii^Vk7r!=J1sKr%T_nhA)>DKwfA&BcJ!D>f1@iVk~t_5iqB^a9hbhme)t zVaQCr5@a9P1KCN~03(D5wgh+o5*VU^(V?67r6|M(hGj~nA<~{Z>^@)#Rt<@W z^oqtv0`D$p^qgf^ATU^dZw&0tY=HOG>s>a0E6srkHCN1?07Q}uaOiH_MW5Ln2vdY? z$6Wz}mKa1SJ6aFS&vwTAtf}J!0T@`k59kU#5LkH#`I`3tG~C{MclADG%;q|vM9-)~ zIkIR8wy8t14&b9= zfC~UkA2S5iVlOy6t;EOexLkiczp9y2vi1HgV)&fBg&a{#`BVxX>jTOu+2 z5IA{cmbh&2`-p#gWC?(h&Cv=!0Jhn@r?~pcOjp*XhvCaSptb*GIhvc)+8%_c=IhBo zTi*)`8b?j(b;T;c1JrV(Jl4hA0a%~W{)5+JHqc~7VSoe200hg#QHj>Y2!NRamjP?> zC8Mxa*y3bZ;MT@$jt}?Yk8xVhXAb>1(f$n;YQ&3*9)fI*}8|%!ODO|<81t6eiy-i zY^Q!Q_@6EXQFk`)D-J+!=?3%`(ER`nA~17O6oQAnz6a9_AYK7;xMbkQ(?*Sk)BBu5 z$hY``H-8781V4a~eJ~(fA5MVWgcys400ZfT+6(apcS+Tbiox8uck9jPl+-+qwjH=z{ar<&~;JL#c!u0JicNndV(x(KulC_**U=X z+XVW7Bxtnc%BxSB;BTi1RrQLe$eg$XxQ`i@4xH|}HaiXS(J<;TXB0K?2=$5AGjwVu zy2RV=%1DnPwHg8i{r$u*Ak7mZMTi3L%4FcmW2h4rfpq$66{8`bijVRDeFiZ6sXSC5 zAQtaasY%`VI+8RKthYg&25=Nf;4XVhq66~{b_*EIx<{IN=3!yNh8_&%4M?tcY=B`< z^)43yEEjlH8q_GP6ax=xk8=wjfK8zd08&KNXhHL!z4$`jU_nzyu6GBly5{ zO6%}Ah?{l`hyDCX1r!)*-b=hj-#5Uf`O?Z7XaSllZ)V8XQVgx4mF^(-Ku?8{51W4n z(2?)=fPHIsdR<8yWF!{y`e&ctVBj;)7wF=|JURjIAAl=%^!kEWUQrlXVFt%bhFyT? zeG+bmu;_Jpb*py@7neInL7&4eox&6=uh$_ThFaA|yl)f@D+2p@>=~<+2Q@luT{rhf zq&+GN6(K)(1*$mxIX^hjq3MmC0>0ZZ8%P_B(4c{v=$inyz~aEE?3)8sU?k<} zy-QXqxwOI<33Q}a)H76V0A(vZ!xbC8%KW)N0f>iV)c%nnQLgptO=VVVP@C`S>?;J* zdlRu?14LyhsnA|C=j?MX{3H5aNB=oLqF>2d7MusC%@Ak);d0;~`l6yzD3@lw;`aqQ z&~0uyC$(Z8MIpIy1{Z93tKKn@kD2dTe&rb0Q@y~7(Y|axEXIL_Jq39ndmI^sDv(n@}rHFDroAitaSQyCEA0Q z&CN~|jJC$3w z4oLhS#-N>BD3|Au$3P73-!mz4&M7D4Uz|>rl|>LD z%D=jN7;{)ic$k%i&Y=Yh)M+6zRMUmFo99jw2q7rHo0$Zt=4hdI^*GvAjRG!9V2RW9 z(ENw1O8D>jAm6lE35AGj2qkYlM29UQIwkmg6Um44>epa8Jb)6G752jwd%yl-@(sbr zP!w+cgy(h~5!L&OCdk0#BPlNA9(QAnCk%CAgTe~ytH&TYCVZLiJ(jVR9Z9RN*GU}s z3#yu3GK9$o!*?%dnk^XP;P_gF&%qd0fv0k764Mffk{GyfNROiJ&_7ONIP>rGKXVj! z`?MNrP+uDOTy-x5T7j1ltr)Knayh*Rzl6=h*l-UbaWx6boA@@7@aP%co34;4Z#P{S zC$%Gt*z`q@ltup6jM`wX8(+1A`{rd4ijDOG41@T0Lp=NBb7DC$5=(7T&mJ{WA#n~6 zG^Qlc-Ri^0ZbjR-1P0N}spTR{CGd%

@Tl(E%}!$^{rz`IN61oVA{uBr6AohE0e6 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 literal 0 HcmV?d00001 diff --git a/docs/projects/centurion_erp/development/media/ticket-issue-ux-wireframe.drawio b/docs/projects/centurion_erp/development/media/ticket-issue-ux-wireframe.drawio new file mode 100644 index 00000000..2e95ee48 --- /dev/null +++ b/docs/projects/centurion_erp/development/media/ticket-issue-ux-wireframe.drawio @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/pull_request_template.md b/docs/pull_request_template.md new file mode 100644 index 00000000..d21df5eb --- /dev/null +++ b/docs/pull_request_template.md @@ -0,0 +1,39 @@ +### :books: Summary + + + + +### :link: Links / References + + + + +### :construction_worker: Tasks + + - [ ] Add your tasks here as required (delete task if n/a) + + + +- [ ] :orange_circle: Related issue(s) closed via [commit message](https://www.conventionalcommits.org/en/v1.0.0) footer + +- [ ] :yellow_circle: Contains `breaking-change` Any Breaking change(s)? [commit message](https://www.conventionalcommits.org/en/v1.0.0) + + _Breaking Change must also be notated in the commit that introduces it and in [Conventional Commit Format](https://www.conventionalcommits.org/en/v1.0.0/)._ + + - [ ] :memo: Release notes updated + +- [ ] :large_blue_circle: Documentation Documentation written + + _All features to be documented within the correct section(s). Administration, Development and/or User_ + +- [ ] Milestone assigned + +- [ ] :red_circle: [Unit Test(s) Written](https://nofusscomputing.com/projects/centurion_erp/development/testing/) + + _ensure test coverage delta is not less than zero_