diff --git a/api/siteapp/serializers/location.py b/api/siteapp/serializers/location.py new file mode 100644 index 000000000..fc5a07761 --- /dev/null +++ b/api/siteapp/serializers/location.py @@ -0,0 +1,19 @@ +from rest_framework import serializers +from api.base.serializers.types import ReadOnlySerializer, WriteOnlySerializer +from siteapp.models import Location + +class SimpleLocationSerializer(ReadOnlySerializer): + class Meta: + model = Location + fields = ['title', 'address_type', 'street', 'apt', 'city', 'state', 'zipcode', 'country'] + +class DetailedLocationSerializer(SimpleLocationSerializer): + class Meta: + model = Location + fields = SimpleLocationSerializer.Meta.fields + ['uuid', 'remarks'] + +class WriteLocationSerializer(WriteOnlySerializer): + uuid = serializers.UUIDField(format='hex_verbose') + class Meta: + model = Location + fields = ['uuid', 'title', 'address_type', 'street', 'apt', 'city', 'state', 'zipcode', 'country', 'remarks'] diff --git a/api/siteapp/urls.py b/api/siteapp/urls.py index 33943b468..fb430012c 100644 --- a/api/siteapp/urls.py +++ b/api/siteapp/urls.py @@ -14,6 +14,7 @@ from api.siteapp.views.appointment import AppointmentViewSet from api.siteapp.views.request import RequestViewSet from api.siteapp.views.proposal import ProposalViewSet +from api.siteapp.views.location import LocationViewSet router = routers.DefaultRouter() router.register(r'organizations', OrganizationViewSet) @@ -28,6 +29,7 @@ router.register(r'appointments', AppointmentViewSet) router.register(r'requests', RequestViewSet) router.register(r'proposals', ProposalViewSet) +router.register(r'locations', LocationViewSet) project_router = NestedSimpleRouter(router, r'projects', lookup='projects') diff --git a/api/siteapp/views/location.py b/api/siteapp/views/location.py new file mode 100644 index 000000000..6e288f5d5 --- /dev/null +++ b/api/siteapp/views/location.py @@ -0,0 +1,23 @@ +from django.db.models import Q +from rest_framework import filters + +from api.base.views.base import SerializerClasses +from api.base.views.viewsets import ReadWriteViewSet +from api.siteapp.serializers.location import SimpleLocationSerializer, DetailedLocationSerializer, WriteLocationSerializer +from siteapp.models import Location + +class LocationViewSet(ReadWriteViewSet): + queryset = Location.objects.all() + + search_fields = ['street'] + filter_backends = (filters.SearchFilter,) + + serializer_classes = SerializerClasses(retrieve=DetailedLocationSerializer, + list=SimpleLocationSerializer, + create=WriteLocationSerializer, + update=WriteLocationSerializer, + destroy=WriteLocationSerializer, + ) + + def search(self, request, keyword): + return Q(name__icontains=keyword) \ No newline at end of file diff --git a/controls/migrations/0074_auto_20220531_1518.py b/controls/migrations/0074_auto_20220531_1518.py new file mode 100644 index 000000000..eb118e308 --- /dev/null +++ b/controls/migrations/0074_auto_20220531_1518.py @@ -0,0 +1,39 @@ +# Generated by Django 3.2.13 on 2022-05-31 15:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('siteapp', '0066_auto_20220531_1518'), + ('controls', '0073_auto_20220516_1548'), + ] + + operations = [ + migrations.AlterField( + model_name='element', + name='appointments', + field=models.ManyToManyField(blank=True, related_name='element', to='siteapp.Appointment'), + ), + migrations.AlterField( + model_name='element', + name='requests', + field=models.ManyToManyField(blank=True, related_name='element', to='siteapp.Request'), + ), + migrations.AlterField( + model_name='element', + name='tags', + field=models.ManyToManyField(blank=True, related_name='element', to='siteapp.Tag'), + ), + migrations.AlterField( + model_name='system', + name='proposals', + field=models.ManyToManyField(blank=True, related_name='system', to='siteapp.Proposal'), + ), + migrations.AlterField( + model_name='system', + name='tags', + field=models.ManyToManyField(blank=True, related_name='system', to='siteapp.Tag'), + ), + ] diff --git a/controls/views.py b/controls/views.py index de6b25ad8..11e89c27a 100644 --- a/controls/views.py +++ b/controls/views.py @@ -53,6 +53,7 @@ from .models import * from .utilities import * from siteapp.utils.views_helper import project_context +from siteapp.models import Role, Party, Appointment, Request, Proposal, Location from integrations.models import Integration logging.basicConfig() @@ -967,12 +968,74 @@ def as_json(self): control_implementations = [] props = [] orgs = list(Organization.objects.all()) # TODO: orgs need uuids, not sure which orgs to use for a component - parties = [{"uuid": str(uuid.uuid4()), "type": "organization", "name": org.name} for org in orgs] - responsible_roles = [{ - "role-id": "supplier",# TODO: Not sure what this refers to - "party-uuids": [ str(party.get("uuid")) for party in parties ] + + list_of_parties = [] + list_of_roles = [] + list_of_resp_parties = [] + list_of_resp_roles = [] + list_of_locations = [] + + for location in Location.objects.all(): + # import ipdb; ipdb.set_trace() + loc = { + "uuid": str(location.uuid), + "title": location.title, + "address": { + "type": location.address_type, + "addr-lines": [location.apt, location.street], + "city": location.city, + "state": location.state, + "postal-code": location.zipcode, + }, + "remarks": location.remarks, + } + list_of_locations.append(loc) + for appointment in self.element.appointments.all(): + party = { + "uuid": str(appointment.party.uuid), + "type": appointment.party.party_type, + "name": appointment.party.name, + "short-name": appointment.party.short_name, + "email-addresses": appointment.party.email, + "telephone-numbers": appointment.party.phone_number, + } + role = { + "id": appointment.role.role_id, + "title": appointment.role.title, + "short-name": appointment.role.short_name, + "description": appointment.role.description, + } + respParty = { + 'role-id': role["id"], + 'party-uuids': [party["uuid"]] + } + + if len(list_of_resp_parties) == 0: + list_of_resp_parties.append(respParty) + + if role["id"] in [x['role-id'] for x in list_of_resp_parties]: + for x in list_of_resp_parties: + if x['role-id'] == role["id"] and party['uuid'] not in x['party-uuids']: + x['party-uuids'].append(party["uuid"]) + else: + list_of_resp_parties.append(respParty) + + if party not in list_of_parties: + list_of_parties.append(party) - }] + if role not in list_of_roles: + list_of_roles.append(role) + + parties = [ + { + "uuid": str(uuid.uuid4()), + "type": "organization", + "name": org.name + } for org in orgs] + parties.extend(list_of_parties) + + + of = { "component-definition": { "uuid": str(uuid4()), @@ -981,7 +1044,11 @@ def as_json(self): "last-modified": self.element.updated.replace(microsecond=0).isoformat(), "version": self.element.updated.replace(microsecond=0).isoformat(), "oscal-version": self.element.oscal_version, - "parties": parties + + "roles": list_of_roles, + "locations": list_of_locations, + "parties": parties, + "responsible-parties": list_of_resp_parties, }, "components": [ { @@ -989,7 +1056,7 @@ def as_json(self): "type": self.element.component_type.lower() if self.element.component_type is not None else "software", "title": self.element.full_name or self.element.name, "description": self.element.description, - "responsible-roles": responsible_roles, # TODO: gathering party-uuids, just filling for now + "responsible-roles": list_of_resp_roles, #TODO: Need to add responsible roles "props": props, "control-implementations": control_implementations } diff --git a/frontend/src/components/cmpt_parties/cmpt_parties.js b/frontend/src/components/cmpt_parties/cmpt_parties.js index 0e40c6d4b..6d12aff13 100644 --- a/frontend/src/components/cmpt_parties/cmpt_parties.js +++ b/frontend/src/components/cmpt_parties/cmpt_parties.js @@ -112,6 +112,7 @@ export const ComponentParties = ({ elementId, poc_users, isOwner }) => { }); const [tempRoleToAdd, setTempRoleToAdd] = useState([]); const [partyNamesList, setPartyNamesList] = useState([]); + const [allPartiesNames, setAllPartiesNames] = useState([]); const editToolTip = ( Edit role) const deleteToolTip = ( Delete role) @@ -124,16 +125,24 @@ export const ComponentParties = ({ elementId, poc_users, isOwner }) => { } useEffect(() => { - axios(`/api/v2/elements/${elementId}/`).then(response => { - setData(response.data.parties); - let names = []; - response.data.parties.map(party => { - names.push(party.name); - }); - setPartyNamesList(names); + axios(`/api/v2/elements/${elementId}/`).then(response => { + setData(response.data.parties); + let names = []; + response.data.parties.map(party => { + names.push(party.name.toLowerCase()); }); - + setPartyNamesList(names); + }); + axios(`/api/v2/parties/`).then((response) => { + let names = []; + + response.data.data.map(party => { + names.push(party.name.toLowerCase()); + }); + setAllPartiesNames(names); + }); + }, []); useEffect(() => { @@ -617,7 +626,34 @@ export const ComponentParties = ({ elementId, poc_users, isOwner }) => { } } - if (partyNamesList.includes(createNewParty.name)){ + if (partyNamesList.includes(createNewParty.name.toLowerCase()) || allPartiesNames.includes(createNewParty.name.toLowerCase())){ + if(validated.name === 'error'){ + return 'error'; + } else { + setValidated((prev) => ({...prev, name: 'error'})); + return 'error'; + } + } else { + if(validated.name !== 'success'){ + setValidated((prev) => ({...prev, name: 'success'})); + return 'success'; + } else { + return 'success'; + } + } + } + + const getExistingPartyNameValidation = () => { + if(createNewParty.name === ''){ + if(validated.name === 'warning'){ + return 'warning'; + } else { + setValidated((prev) => ({...prev, name: 'warning'})); + return 'warning'; + } + } + + if (partyNamesList.includes(createNewParty.name.toLowerCase())){ if(validated.name === 'error'){ return 'error'; } else { @@ -651,6 +687,7 @@ export const ComponentParties = ({ elementId, poc_users, isOwner }) => { } } } + const getPartyEmailValidation = () => { const emailRegex = RegExp('[a-z0-9]+@[a-z]+\.[a-z]{2,3}'); if (createNewParty.email.length === 0) { @@ -714,6 +751,7 @@ export const ComponentParties = ({ elementId, poc_users, isOwner }) => { } } } + const getPartyMobilePhoneValidation = () => { if (createNewParty.mobile_phone.length === 0) { if(validated.mobile_phone === null){ @@ -753,7 +791,7 @@ export const ComponentParties = ({ elementId, poc_users, isOwner }) => { if(data.length > 0 && currentParty.name !== undefined) { const initialCurrentParty = data[currentParty.id-1]; - const updatedPartyList = partyNamesList.filter((party) => party !== initialCurrentParty.name) + const updatedAllPartiesNames = allPartiesNames.filter((party) => party !== initialCurrentParty.name) if(currentParty.name === ''){ if(editValidated.name === 'warning'){ @@ -763,15 +801,15 @@ export const ComponentParties = ({ elementId, poc_users, isOwner }) => { return 'warning'; } } - - if(initialCurrentParty.name === currentParty.name){ + + if(initialCurrentParty.name.toLowerCase() === currentParty.name.toLowerCase()){ if(editValidated.name === 'success'){ return 'success'; } else { setEditValidated((prev) => ({...prev, name: 'success'})); return 'success'; } - } else if (updatedPartyList.includes(currentParty.name)){ + } else if (updatedAllPartiesNames.includes(currentParty.name.toLowerCase())){ if(editValidated.name === 'error'){ return 'error'; } else { @@ -856,7 +894,7 @@ export const ComponentParties = ({ elementId, poc_users, isOwner }) => { } } } - + return (
@@ -1136,7 +1174,7 @@ export const ComponentParties = ({ elementId, poc_users, isOwner }) => { - + {'Name'} diff --git a/siteapp/migrations/0066_auto_20220531_1518.py b/siteapp/migrations/0066_auto_20220531_1518.py new file mode 100644 index 000000000..4a2df079c --- /dev/null +++ b/siteapp/migrations/0066_auto_20220531_1518.py @@ -0,0 +1,40 @@ +# Generated by Django 3.2.13 on 2022-05-31 15:18 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('siteapp', '0065_alter_proposal_req'), + ] + + operations = [ + migrations.CreateModel( + name='Location', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True, db_index=True)), + ('updated', models.DateTimeField(auto_now=True, db_index=True, null=True)), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, help_text='A UUID (a unique identifier) for this Location.')), + ('title', models.CharField(help_text='Title of Location.', max_length=250)), + ('address_type', models.CharField(blank=True, help_text='Type of address', max_length=250, null=True)), + ('street', models.CharField(blank=True, help_text='Street address', max_length=250, null=True)), + ('apt', models.CharField(blank=True, help_text='Apt/Suite', max_length=250, null=True)), + ('city', models.CharField(blank=True, help_text='City', max_length=250, null=True)), + ('state', models.CharField(blank=True, help_text='State', max_length=250, null=True)), + ('zipcode', models.CharField(blank=True, help_text='Zip', max_length=250, null=True)), + ('country', models.CharField(blank=True, help_text='Country', max_length=250, null=True)), + ('remarks', models.TextField(blank=True, help_text='Remarks', null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.AlterField( + model_name='project', + name='tags', + field=models.ManyToManyField(blank=True, related_name='project', to='siteapp.Tag'), + ), + ] diff --git a/siteapp/models.py b/siteapp/models.py index 33b0741cc..57bd1df4f 100644 --- a/siteapp/models.py +++ b/siteapp/models.py @@ -1411,7 +1411,18 @@ def __str__(self): def serialize(self): return {"label": self.label, "system_created": self.system_created, "id": self.id} - +class Location(BaseModel): + uuid = models.UUIDField(default=uuid.uuid4, editable=False, help_text="A UUID (a unique identifier) for this Location.") + title = models.CharField(max_length=250, unique=False, blank=False, null=False, help_text="Title of Location.") + address_type = models.CharField(max_length=250, unique=False, blank=True, null=True, help_text="Type of address") + street = models.CharField(max_length=250, unique=False, blank=True, null=True, help_text="Street address") + apt = models.CharField(max_length=250, unique=False, blank=True, null=True, help_text="Apt/Suite") + city = models.CharField(max_length=250, unique=False, blank=True, null=True, help_text="City") + state = models.CharField(max_length=250, unique=False, blank=True, null=True, help_text="State") + zipcode = models.CharField(max_length=250, unique=False, blank=True, null=True, help_text="Zip") + country = models.CharField(max_length=250, unique=False, blank=True, null=True, help_text="Country") + remarks = models.TextField(unique=False, blank=True, null=True, help_text="Remarks") + class Party(BaseModel): uuid = models.UUIDField(default=uuid.uuid4, editable=False, help_text="A UUID (a unique identifier) for this Party.") party_type = models.CharField(max_length=100, unique=False, help_text="type for Party.")