diff --git a/cadasta/config/permissions/data-collector.json b/cadasta/config/permissions/data-collector.json index d6da8b98c..f7ca20d20 100644 --- a/cadasta/config/permissions/data-collector.json +++ b/cadasta/config/permissions/data-collector.json @@ -11,7 +11,11 @@ "action": ["resource.*"], "object": ["resource/$organization/$project/*"] }, - + { + "effect": "deny", + "action": ["resource.unarchive"], + "object": ["resource/$organization/$project/*"] + }, { "effect": "allow", "action": ["spatial.*", "spatial.resources.*"], diff --git a/cadasta/config/permissions/org-admin.json b/cadasta/config/permissions/org-admin.json index 4f695b00d..abb550ae2 100644 --- a/cadasta/config/permissions/org-admin.json +++ b/cadasta/config/permissions/org-admin.json @@ -47,6 +47,11 @@ "effect": "allow", "action": ["resource.*"], "object": ["resource/$organization/*/*"] + }, + { + "effect": "deny", + "action": ["resource.unarchive"], + "object": ["resource/$organization/*/*"] } ] } diff --git a/cadasta/config/permissions/project-manager.json b/cadasta/config/permissions/project-manager.json index 50da48204..f47a020b1 100644 --- a/cadasta/config/permissions/project-manager.json +++ b/cadasta/config/permissions/project-manager.json @@ -47,6 +47,11 @@ "effect": "allow", "action": ["resource.*"], "object": ["resource/$organization/$project/*"] + }, + { + "effect": "deny", + "action": ["resource.unarchive"], + "object": ["resource/$organization/$project/*"] } ] } diff --git a/cadasta/core/static/css/main.css b/cadasta/core/static/css/main.css index ca37c433c..eb3db6c69 100644 --- a/cadasta/core/static/css/main.css +++ b/cadasta/core/static/css/main.css @@ -3994,12 +3994,9 @@ div.org-logo { /* =Resources -------------------------------------------------------------- */ -div.resource-60 { - float: left; - padding: 0 10px; } - div.resource-text { - margin-left: 90px; + display: table-cell; + margin-left: 80px; word-wrap: break-word; } .panel-body h4 { @@ -4011,8 +4008,9 @@ ul.resource-actions > li { padding: 4px 12px; margin: 12px 0; } ul.resource-actions > li:first-child { - padding-left: 0; - border-right: 1px solid #e7e8ea; } + padding-left: 0; } + ul.resource-actions > li + li { + border-left: 1px solid #e7e8ea; } /* =For styles only found in single project or organization pages -------------------------------------------------------------- */ @@ -5740,12 +5738,13 @@ textarea.form-control { text-transform: uppercase; font-size: 12px; opacity: 0.7; - padding: 4px 24px; background: #f2f4f7; } .table > tbody > tr.linked > td:hover { cursor: pointer; } .table .btn-sm { - min-width: 90px; } + min-width: 80px !important; } + .table .table-condensed .btn-sm { + min-width: 60px !important; } .table div.org-logo { padding: 4px 0; } @@ -5928,7 +5927,7 @@ div.add-btn-btm { .btn-full .btn { min-width: 100px; } } -/* =Alerts +/* =Alerts and labels -------------------------------------------------------------- */ .alert { text-align: center; @@ -5972,6 +5971,14 @@ div.add-btn-btm { .alert-error .alert-link { color: #843534; } +.label { + font-size: 11px; + font-weight: 500; + vertical-align: middle; } + +h1.label { + font-size: 14px; } + @media (max-width: 991px) { .alert { max-width: none; } } diff --git a/cadasta/core/static/css/main.scss b/cadasta/core/static/css/main.scss index 8e1e20e79..756d9c9ee 100644 --- a/cadasta/core/static/css/main.scss +++ b/cadasta/core/static/css/main.scss @@ -1051,14 +1051,17 @@ textarea.form-control { text-transform: uppercase; font-size: 12px; opacity: 0.7; - padding: 4px 24px; + //padding: 4px 24px; background: $body-bg; } > tbody > tr.linked > td:hover { cursor: pointer; } .btn-sm { - min-width: 90px; + min-width: 80px !important; + } + .table-condensed .btn-sm { + min-width: 60px !important; } div.org-logo { padding: 4px 0; @@ -1300,7 +1303,7 @@ div.add-btn-btm { // add party link at bottom of table } } -/* =Alerts +/* =Alerts and labels -------------------------------------------------------------- */ .alert { @@ -1346,6 +1349,17 @@ div.add-btn-btm { // add party link at bottom of table @include alert-variant($alert-danger-bg, $alert-danger-border, $alert-danger-text); } +.label { + font-size: 11px; + font-weight: 500; + vertical-align: middle; +} + +h1.label { + font-size: 14px; +} + + @media (max-width: $screen-sm-max) { .alert { max-width: none; diff --git a/cadasta/core/static/css/resources.scss b/cadasta/core/static/css/resources.scss index 5c0a9a084..8e03337bb 100644 --- a/cadasta/core/static/css/resources.scss +++ b/cadasta/core/static/css/resources.scss @@ -1,14 +1,12 @@ /* =Resources -------------------------------------------------------------- */ - -div.resource-60 { - float: left; - padding: 0 10px; -} + div.resource-60 { + } div.resource-text { - margin-left: 90px; + display: table-cell; + margin-left: 80px; word-wrap: break-word; } @@ -23,6 +21,8 @@ ul.resource-actions > li { margin: 12px 0; &:first-child { padding-left: 0; - border-right: 1px solid $table-border-color; + } + &+li { + border-left: 1px solid $table-border-color; } } diff --git a/cadasta/organization/models.py b/cadasta/organization/models.py index 52aec255a..4439a8185 100644 --- a/cadasta/organization/models.py +++ b/cadasta/organization/models.py @@ -1,3 +1,4 @@ +from django.core.urlresolvers import reverse from django.conf import settings from django.db import models from django_countries.fields import CountryField @@ -225,6 +226,20 @@ def __str__(self): def __repr__(self): return str(self) + @property + def ui_class_name(self): + return _("Project") + + @property + def ui_detail_url(self): + return reverse( + 'organization:project-dashboard', + kwargs={ + 'organization': self.organization.slug, + 'project': self.slug, + }, + ) + def save(self, *args, **kwargs): if ((self.country is None or self.country == '') and self.extent is not None): diff --git a/cadasta/organization/tests/test_models.py b/cadasta/organization/tests/test_models.py index 419e5af68..841c27973 100644 --- a/cadasta/organization/tests/test_models.py +++ b/cadasta/organization/tests/test_models.py @@ -135,6 +135,17 @@ def test_can_create_private(self): project = ProjectFactory.create(access='private') assert not project.public() + def test_ui_class_name(self): + project = ProjectFactory.create() + assert project.ui_class_name == "Project" + + def test_ui_detail_url(self): + project = ProjectFactory.create() + assert project.ui_detail_url == ( + '/organizations/{org}/projects/{prj}/'.format( + org=project.organization.slug, + prj=project.slug)) + class ProjectRoleTest(UserTestCase): def setUp(self): diff --git a/cadasta/party/models.py b/cadasta/party/models.py index 154071723..09f208ef9 100644 --- a/cadasta/party/models.py +++ b/cadasta/party/models.py @@ -1,6 +1,7 @@ """Party models.""" from core.models import RandomIDModel +from django.core.urlresolvers import reverse from django.conf import settings from django.contrib.gis.db import models from django.contrib.postgres.fields import JSONField @@ -115,6 +116,21 @@ def __str__(self): def __repr__(self): return str(self) + @property + def ui_class_name(self): + return _("Party") + + @property + def ui_detail_url(self): + return reverse( + 'parties:detail', + kwargs={ + 'organization': self.project.organization.slug, + 'project': self.project.slug, + 'party': self.id, + }, + ) + @fix_model_for_attributes @permissioned_model @@ -268,13 +284,34 @@ class TutelaryMeta: ) def __str__(self): - return " {type} <{su}>>".format( - party=self.party.name, su=self.spatial_unit.get_type_display(), - type=self.tenure_type.label) + return "".format(self.name) def __repr__(self): return str(self) + @property + def name(self): + return "<{party}> {type} <{su}>".format( + party=self.party.name, + su=self.spatial_unit.name, + type=self.tenure_type.label, + ) + + @property + def ui_class_name(self): + return _("Relationship") + + @property + def ui_detail_url(self): + return reverse( + 'parties:relationship_detail', + kwargs={ + 'organization': self.project.organization.slug, + 'project': self.project.slug, + 'relationship': self.id, + }, + ) + class TenureRelationshipType(models.Model): """Defines allowable tenure types.""" diff --git a/cadasta/party/tests/test_models.py b/cadasta/party/tests/test_models.py index caf8d66cb..310111739 100644 --- a/cadasta/party/tests/test_models.py +++ b/cadasta/party/tests/test_models.py @@ -51,6 +51,18 @@ def test_adding_attributes(self): }) assert party.attributes['description'] == 'Mad Hatters Tea Party' + def test_ui_class_name(self): + party = PartyFactory.create() + assert party.ui_class_name == "Party" + + def test_ui_detail_url(self): + party = PartyFactory.create() + assert party.ui_detail_url == ( + '/organizations/{org}/projects/{prj}/records/parties/{id}/'.format( + org=party.project.organization.slug, + prj=party.project.slug, + id=party.id)) + class PartyRelationshipTest(UserTestCase): @@ -177,6 +189,25 @@ def test_left_and_right_project_ids(self): party__project=project ) + def test_name(self): + tenurerel = TenureRelationshipFactory.create() + assert tenurerel.name == "<{party}> {type} <{su}>".format( + party=tenurerel.party.name, + type=tenurerel.tenure_type.label, + su=tenurerel.spatial_unit.get_type_display()) + + def test_ui_class_name(self): + tenurerel = TenureRelationshipFactory.create() + assert tenurerel.ui_class_name == "Relationship" + + def test_ui_detail_url(self): + tenurerel = TenureRelationshipFactory.create() + assert tenurerel.ui_detail_url == ( + '/organizations/{org}/projects/{prj}/relationships/{id}/'.format( + org=tenurerel.project.organization.slug, + prj=tenurerel.project.slug, + id=tenurerel.id)) + class TenureRelationshipTypeTest(UserTestCase): """Test TenureRelationshipType.""" diff --git a/cadasta/party/tests/test_views_default.py b/cadasta/party/tests/test_views_default.py index 74bc042a5..ed5f1673c 100644 --- a/cadasta/party/tests/test_views_default.py +++ b/cadasta/party/tests/test_views_default.py @@ -431,9 +431,9 @@ class PartyResourcesAddTest(TestCase): def set_up_models(self): self.project = ProjectFactory.create() self.party = PartyFactory.create(project=self.project) - self.assigned = ResourceFactory.create(project=self.project, + self.attached = ResourceFactory.create(project=self.project, content_object=self.party) - self.unassigned = ResourceFactory.create(project=self.project) + self.unattached = ResourceFactory.create(project=self.project) def assign_policies(self): assign_policies(self.authorized_user) @@ -457,8 +457,8 @@ def get_success_url_kwargs(self): def get_post_data(self): return { - self.assigned.id: False, - self.unassigned.id: True, + self.attached.id: False, + self.unattached.id: True, } def test_get_with_authorized_user(self): @@ -492,8 +492,10 @@ def test_post_with_authorized_user(self): assert response.status_code == 302 assert response['location'] == self.expected_success_url - assert self.party.resources.count() == 1 - assert self.party.resources.first() == self.unassigned + party_resources = self.party.resources.all() + assert len(party_resources) == 2 + assert self.attached in party_resources + assert self.unattached in party_resources def test_post_with_unauthorized_user(self): response = self.request(method='POST', user=self.unauthorized_user) @@ -502,7 +504,7 @@ def test_post_with_unauthorized_user(self): in [str(m) for m in get_messages(self.request)]) assert self.party.resources.count() == 1 - assert self.party.resources.first() == self.assigned + assert self.party.resources.first() == self.attached def test_post_with_unauthenticated_user(self): response = self.request(method='POST') @@ -510,7 +512,7 @@ def test_post_with_unauthenticated_user(self): assert '/account/login/' in response['location'] assert self.party.resources.count() == 1 - assert self.party.resources.first() == self.assigned + assert self.party.resources.first() == self.attached @pytest.mark.usefixtures('make_dirs') @@ -864,9 +866,9 @@ def set_up_models(self): self.project = ProjectFactory.create() self.relationship = TenureRelationshipFactory.create( project=self.project) - self.assigned = ResourceFactory.create( + self.attached = ResourceFactory.create( project=self.project, content_object=self.relationship) - self.unassigned = ResourceFactory.create(project=self.project) + self.unattached = ResourceFactory.create(project=self.project) def assign_policies(self): assign_policies(self.authorized_user) @@ -892,8 +894,8 @@ def get_success_url_kwargs(self): def get_post_data(self): return { - self.assigned.id: False, - self.unassigned.id: True, + self.attached.id: False, + self.unattached.id: True, } def test_get_with_authorized_user(self): @@ -928,8 +930,10 @@ def test_post_with_authorized_user(self): assert response.status_code == 302 assert response['location'] == self.expected_success_url - assert self.relationship.resources.count() == 1 - assert self.relationship.resources.first() == self.unassigned + relationship_resources = self.relationship.resources.all() + assert len(relationship_resources) == 2 + assert self.attached in relationship_resources + assert self.unattached in relationship_resources def test_post_with_unauthorized_user(self): response = self.request(method='POST', user=self.unauthorized_user) @@ -939,7 +943,7 @@ def test_post_with_unauthorized_user(self): in [str(m) for m in get_messages(self.request)]) assert self.relationship.resources.count() == 1 - assert self.relationship.resources.first() == self.assigned + assert self.relationship.resources.first() == self.attached def test_post_with_unauthenticated_user(self): response = self.request(method='POST') @@ -947,7 +951,7 @@ def test_post_with_unauthenticated_user(self): assert '/account/login/' in response['location'] assert self.relationship.resources.count() == 1 - assert self.relationship.resources.first() == self.assigned + assert self.relationship.resources.first() == self.attached @pytest.mark.usefixtures('make_dirs') diff --git a/cadasta/party/views/default.py b/cadasta/party/views/default.py index fb389048b..d26635130 100644 --- a/cadasta/party/views/default.py +++ b/cadasta/party/views/default.py @@ -6,7 +6,7 @@ from organization.views import mixins as organization_mixins from resources.forms import AddResourceFromLibraryForm -from resources.views.mixins import ProjectHasResourcesMixin +from resources.views import mixins as resource_mixins from . import mixins from .. import forms from .. import messages as error_messages @@ -42,7 +42,8 @@ class PartiesDetail(LoginPermissionRequiredMixin, JsonAttrsMixin, mixins.PartyObjectMixin, organization_mixins.ProjectAdminCheckMixin, - ProjectHasResourcesMixin, + resource_mixins.HasUnattachedResourcesMixin, + resource_mixins.DetachableResourcesListMixin, generic.DetailView): template_name = 'party/party_detail.html' permission_required = 'party.view' @@ -95,7 +96,7 @@ def post(self, request, *args, **kwargs): class PartyResourcesNew(LoginPermissionRequiredMixin, mixins.PartyResourceMixin, organization_mixins.ProjectAdminCheckMixin, - ProjectHasResourcesMixin, + resource_mixins.HasUnattachedResourcesMixin, generic.CreateView): template_name = 'party/resources_new.html' permission_required = 'party.resources.add' @@ -106,7 +107,8 @@ class PartyRelationshipDetail(LoginPermissionRequiredMixin, JsonAttrsMixin, mixins.PartyRelationshipObjectMixin, organization_mixins.ProjectAdminCheckMixin, - ProjectHasResourcesMixin, + resource_mixins.HasUnattachedResourcesMixin, + resource_mixins.DetachableResourcesListMixin, generic.DetailView): template_name = 'party/relationship_detail.html' permission_required = 'tenure_rel.view' @@ -144,7 +146,7 @@ def get_success_url(self): class PartyRelationshipResourceNew(LoginPermissionRequiredMixin, mixins.PartyRelationshipResourceMixin, organization_mixins.ProjectAdminCheckMixin, - ProjectHasResourcesMixin, + resource_mixins.HasUnattachedResourcesMixin, generic.CreateView): template_name = 'party/relationship_resources_new.html' permission_required = 'tenure_rel.resources.add' diff --git a/cadasta/party/views/mixins.py b/cadasta/party/views/mixins.py index 8f6800774..3c3400cc6 100644 --- a/cadasta/party/views/mixins.py +++ b/cadasta/party/views/mixins.py @@ -115,10 +115,8 @@ def get_object(self): class PartyRelationshipResourceMixin(ResourceViewMixin, PartyRelationshipObjectMixin): - # UNUSED until party relationship list view is used - # - # def get_content_object(self): - # return self.get_object() + def get_content_object(self): + return self.get_object() def get_form_kwargs(self, *args, **kwargs): kwargs = { diff --git a/cadasta/resources/forms.py b/cadasta/resources/forms.py index 560d24c74..210696c6c 100644 --- a/cadasta/resources/forms.py +++ b/cadasta/resources/forms.py @@ -1,5 +1,4 @@ from django import forms -from django.contrib.contenttypes.models import ContentType from .models import Resource, ContentObject from .fields import ResourceField @@ -36,17 +35,18 @@ def __init__(self, content_object, project_id, *args, **kwargs): super().__init__(*args, **kwargs) self.content_object = content_object - self.project_resources = Resource.objects.filter(project_id=project_id) + self.project_resources = Resource.objects.filter(project_id=project_id, + archived=False) for resource in self.project_resources: - self.fields[resource.id] = ResourceField( - label=resource.name, - initial=(resource in content_object.resources), - resource=resource - ) + if resource not in content_object.resources: + self.fields[resource.id] = ResourceField( + label=resource.name, + initial=False, + resource=resource + ) def save(self): - model_type = ContentType.objects.get_for_model(self.content_object) object_resources = self.content_object.resources.values_list('id', flat=True) for key, value in self.cleaned_data.items(): @@ -55,11 +55,5 @@ def save(self): resource_id=key, content_object=self.content_object ) - elif not value and key in object_resources: - ContentObject.objects.filter( - resource_id=key, - content_type__pk=model_type.id, - object_id=self.content_object.id - ).delete() self.content_object.reload_resources() diff --git a/cadasta/resources/messages.py b/cadasta/resources/messages.py index dff881e08..415238b44 100644 --- a/cadasta/resources/messages.py +++ b/cadasta/resources/messages.py @@ -4,5 +4,5 @@ RESOURCE_ADD = _("You don't have permission to add resources.") RESOURCE_VIEW = _("You don't have permission to view this resource.") RESOURCE_EDIT = _("You don't have permission to edit this resource.") -RESOURCE_ARCHIVE = _("You don't have permission to archive this resource.") -RESOURCE_UNARCHIVE = _("You don't have permission to unarchive this resource.") +RESOURCE_ARCHIVE = _("You don't have permission to delete this resource.") +RESOURCE_UNARCHIVE = _("You don't have permission to restore this resource.") diff --git a/cadasta/resources/models.py b/cadasta/resources/models.py index 593418d12..01f8475b2 100644 --- a/cadasta/resources/models.py +++ b/cadasta/resources/models.py @@ -67,7 +67,7 @@ class TutelaryMeta: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._orginial_url = self.file.url + self._original_url = self.file.url @property def file_name(self): @@ -102,17 +102,21 @@ def num_entities(self): @receiver(models.signals.pre_save, sender=Resource) def archive_file(sender, instance, **kwargs): - if instance._orginial_url and instance._orginial_url != instance.file.url: + if instance._original_url and instance._original_url != instance.file.url: now = str(datetime.now()) if not instance.file_versions: instance.file_versions = {} - instance.file_versions[now] = instance._orginial_url - instance._orginial_url = instance.file.url + instance.file_versions[now] = instance._original_url + instance._original_url = instance.file.url + + # Detach the resource when it is archived + if instance.archived: + ContentObject.objects.filter(resource=instance).delete() @receiver(models.signals.post_save, sender=Resource) def create_thumbnails(sender, instance, created, **kwargs): - if created or instance._orginial_url != instance.file.url: + if created or instance._original_url != instance.file.url: if 'image' in instance.mime_type: io.ensure_dirs() file_name = instance.file.url.split('/')[-1] diff --git a/cadasta/resources/tests/test_forms.py b/cadasta/resources/tests/test_forms.py index afa120d8d..c04fe1b29 100644 --- a/cadasta/resources/tests/test_forms.py +++ b/cadasta/resources/tests/test_forms.py @@ -62,13 +62,12 @@ def test_update_resource(self): class AddResourceFromLibraryFormTest(UserTestCase): def test_init(self): prj = ProjectFactory.create() - prj_res = ResourceFactory.create(project=prj, content_object=prj) + ResourceFactory.create(project=prj, content_object=prj) res = ResourceFactory.create(project=prj) form = AddResourceFromLibraryForm(project_id=prj.id, content_object=prj) - assert len(form.fields) == 2 - assert form.fields[prj_res.id].initial is True + assert len(form.fields) == 1 assert form.fields[res.id].initial is False def test_save(self): @@ -87,6 +86,6 @@ def test_save(self): assert form.is_valid() is True form.save() - assert prj.resources.count() == 1 + assert prj.resources.count() == 2 assert res in prj.resources - assert prj_res not in prj.resources + assert prj_res in prj.resources diff --git a/cadasta/resources/tests/test_views_api.py b/cadasta/resources/tests/test_views_api.py index 6685088ae..53757d3a3 100644 --- a/cadasta/resources/tests/test_views_api.py +++ b/cadasta/resources/tests/test_views_api.py @@ -1,3 +1,4 @@ +import copy import pytest import os import json @@ -27,8 +28,8 @@ 'effect': 'allow', 'object': ['resource/*/*/*'], 'action': ['resource.*'] - } - ] + }, + ], } @@ -44,24 +45,32 @@ def setUp(self): self.project = ProjectFactory.create() self.resources = ResourceFactory.create_batch( - 2, content_object=self.project) + 2, content_object=self.project, project=self.project) self.denied = ResourceFactory.create(content_object=self.project, project=self.project) ResourceFactory.create() self.view = api.ProjectResources.as_view() self.url = '/v1/organizations/{org}/projects/{prj}/resources/' - clauses['clause'].append({ - 'effect': 'deny', - 'object': ['resource/*/*/' + self.denied.id], - 'action': ['resource.*'] - }) - - policy = Policy.objects.create( + additional_clauses = copy.deepcopy(clauses) + additional_clauses['clause'] += [ + { + 'effect': 'deny', + 'object': ['resource/*/*/' + self.denied.id], + 'action': ['resource.*'], + }, + { + 'effect': 'deny', + 'object': ['resource/*/*/*'], + 'action': ['resource.unarchive'], + }, + ] + + self.policy = Policy.objects.create( name='allow', - body=json.dumps(clauses)) + body=json.dumps(additional_clauses)) self.user = UserFactory.create() - self.user.assign_policies(policy) + self.user.assign_policies(self.policy) def _get(self, org, prj, query=None, user=None, status=None, count=None): if user is None: @@ -149,7 +158,7 @@ def test_add_resource_with_unauthorized_user(self): count=3, user=UserFactory.create()) - def test_add_exisiting_resource(self): + def test_add_existing_resource(self): new_resource = ResourceFactory.create() data = {'id': new_resource.id} self._post(self.project.organization.slug, @@ -174,40 +183,72 @@ def test_add_invalid_resource(self): def test_search_for_file(self): not_found = self.storage.save('resources/bild.jpg', self.file) - project = ProjectFactory.create() + prj = ProjectFactory.create() ResourceFactory.create_from_kwargs([ - {'content_object': project, 'file': self.file_name}, - {'content_object': project, 'file': self.file_name}, - {'content_object': project, 'file': not_found} + {'content_object': prj, 'project': prj, 'file': self.file_name}, + {'content_object': prj, 'project': prj, 'file': self.file_name}, + {'content_object': prj, 'project': prj, 'file': not_found} ]) - self._get(project.organization.slug, - project.slug, + self._get(prj.organization.slug, + prj.slug, query='search=image', status=200, count=2) - def test_filter_active(self): - project = ProjectFactory.create() + def test_filter_unarchived(self): + prj = ProjectFactory.create() + ResourceFactory.create_from_kwargs([ + {'content_object': prj, 'project': prj, 'archived': True}, + {'content_object': prj, 'project': prj, 'archived': True}, + {'content_object': prj, 'project': prj, 'archived': False}, + ]) + self._get(prj.organization.slug, + prj.slug, + query='archived=False', + status=200, + count=1) + + def test_filter_archived_with_nonunarchiver(self): + prj = ProjectFactory.create() ResourceFactory.create_from_kwargs([ - {'content_object': project, 'archived': True}, - {'content_object': project, 'archived': True}, - {'content_object': project, 'archived': False}, + {'content_object': prj, 'project': prj, 'archived': True}, + {'content_object': prj, 'project': prj, 'archived': True}, + {'content_object': prj, 'project': prj, 'archived': False}, ]) - self._get(project.organization.slug, - project.slug, + self._get(prj.organization.slug, + prj.slug, query='archived=True', status=200, - count=2) + count=0) + + def test_filter_archived_with_unarchiver(self): + prj = ProjectFactory.create() + ResourceFactory.create_from_kwargs([ + {'content_object': prj, 'project': prj, 'archived': True}, + {'content_object': prj, 'project': prj, 'archived': True}, + {'content_object': prj, 'project': prj, 'archived': False}, + ]) + unarchiver = UserFactory.create() + policy = Policy.objects.create( + name='allow', + body=json.dumps(clauses)) + unarchiver.assign_policies(policy) + self._get(prj.organization.slug, + prj.slug, + query='archived=True', + status=200, + count=2, + user=unarchiver) def test_ordering(self): - project = ProjectFactory.create() + prj = ProjectFactory.create() ResourceFactory.create_from_kwargs([ - {'content_object': project, 'name': 'A'}, - {'content_object': project, 'name': 'B'}, - {'content_object': project, 'name': 'C'}, + {'content_object': prj, 'project': prj, 'name': 'A'}, + {'content_object': prj, 'project': prj, 'name': 'B'}, + {'content_object': prj, 'project': prj, 'name': 'C'}, ]) - content = self._get(project.organization.slug, - project.slug, + content = self._get(prj.organization.slug, + prj.slug, query='ordering=name', status=200, count=3) @@ -215,14 +256,14 @@ def test_ordering(self): assert(names == sorted(names)) def test_reverse_ordering(self): - project = ProjectFactory.create() + prj = ProjectFactory.create() ResourceFactory.create_from_kwargs([ - {'content_object': project, 'name': 'A'}, - {'content_object': project, 'name': 'B'}, - {'content_object': project, 'name': 'C'}, + {'content_object': prj, 'project': prj, 'name': 'A'}, + {'content_object': prj, 'project': prj, 'name': 'B'}, + {'content_object': prj, 'project': prj, 'name': 'C'}, ]) - content = self._get(project.organization.slug, - project.slug, + content = self._get(prj.organization.slug, + prj.slug, query='ordering=-name', status=200, count=3) @@ -236,14 +277,25 @@ def setUp(self): super().setUp() self.project = ProjectFactory.create() - self.resource = ResourceFactory.create(content_object=self.project) + self.resource = ResourceFactory.create(content_object=self.project, + project=self.project) self.view = api.ProjectResourcesDetail.as_view() self.url = '/v1/organizations/{org}/projects/{prj}/resources/{res}' - policy = Policy.objects.create( + + additional_clauses = copy.deepcopy(clauses) + additional_clauses['clause'] += [ + { + 'effect': 'deny', + 'object': ['resource/*/*/*'], + 'action': ['resource.unarchive'], + }, + ] + + self.policy = Policy.objects.create( name='allow', - body=json.dumps(clauses)) + body=json.dumps(additional_clauses)) self.user = UserFactory.create() - self.user.assign_policies(policy) + self.user.assign_policies(self.policy) def _get(self, org, prj, res, user=None, status=None, count=None): if user is None: @@ -315,14 +367,14 @@ def test_get_resource_from_org_that_does_not_exist(self): self.project.slug, self.resource.id, status=404) - assert content['detail'] == "Project not found." + assert content['detail'] == "Not found." def test_get_resource_from_project_that_does_not_exist(self): content = self._get(self.project.organization.slug, 'some-prj', self.resource.id, status=404) - assert content['detail'] == "Project not found." + assert content['detail'] == "Not found." def test_update_resource(self): data = {'name': 'Updated'} @@ -383,11 +435,17 @@ def test_unarchive_resource(self): self.resource.archived = True self.resource.save() data = {'archived': False} + unarchiver = UserFactory.create() + policy = Policy.objects.create( + name='allow', + body=json.dumps(clauses)) + unarchiver.assign_policies(policy) content = self._patch(self.project.organization.slug, self.project.slug, self.resource.id, data, - status=200) + status=200, + user=unarchiver) assert content['id'] == self.resource.id self.resource.refresh_from_db() assert self.resource.archived is False @@ -396,12 +454,8 @@ def test_unarchive_resource_with_unauthorized_user(self): self.resource.archived = True self.resource.save() data = {'archived': False} - content = self._patch(self.project.organization.slug, - self.project.slug, - self.resource.id, - data, - status=403, - user=UserFactory.create()) - assert 'id' not in content - self.resource.refresh_from_db() - assert self.resource.archived is True + self._patch(self.project.organization.slug, + self.project.slug, + self.resource.id, + data, + status=404) diff --git a/cadasta/resources/tests/test_views_default.py b/cadasta/resources/tests/test_views_default.py index 783e9089b..d883c44ca 100644 --- a/cadasta/resources/tests/test_views_default.py +++ b/cadasta/resources/tests/test_views_default.py @@ -1,3 +1,4 @@ +import copy import json import os import pytest @@ -15,7 +16,11 @@ from core.tests.base_test_case import UserTestCase from core.tests.util import make_dirs # noqa from organization.tests.factories import ProjectFactory +from spatial.tests.factories import SpatialUnitFactory +from party.tests.factories import PartyFactory, TenureRelationshipFactory from accounts.tests.factories import UserFactory +from resources.models import Resource +from ..models import ContentObject from ..views import default from ..forms import ResourceForm, AddResourceFromLibraryForm from .factories import ResourceFactory @@ -27,14 +32,14 @@ { 'effect': 'allow', 'object': ['project/*/*'], - 'action': ['resource.*'] + 'action': ['resource.*'], }, { 'effect': 'allow', 'object': ['resource/*/*/*'], - 'action': ['resource.*'] - } - ] + 'action': ['resource.*'], + }, + ], } @@ -54,20 +59,29 @@ def setUp(self): setattr(self.request, 'method', 'GET') self.user = UserFactory.create() - clauses['clause'].append({ - 'effect': 'deny', - 'object': ['resource/*/*/' + self.denied.id], - 'action': ['resource.*'] - }) + additional_clauses = copy.deepcopy(clauses) + additional_clauses['clause'] += [ + { + 'effect': 'deny', + 'object': ['resource/*/*/' + self.denied.id], + 'action': ['resource.*'], + }, + { + 'effect': 'deny', + 'object': ['resource/*/*/*'], + 'action': ['resource.unarchive'], + }, + ] self.policy = Policy.objects.create( name='allow', - body=json.dumps(clauses)) + body=json.dumps(additional_clauses)) assign_user_policies(self.user, self.policy) def _get(self, user=None, status=None, resources=None): if user is None: user = self.user + Policy.objects.get(name='default') if resources is None: resources = self.resources @@ -85,13 +99,28 @@ def _get(self, user=None, status=None, resources=None): if response.status_code == 200: content = response.render().content.decode('utf-8') + + resource_list = [] + if len(resources) > 0: + object_id = resources[0].project.id + attachments = ContentObject.objects.filter(object_id=object_id) + attachment_id_dict = {x.resource.id: x.id for x in attachments} + for resource in resources: + resource_list.append(resource) + attachment_id = attachment_id_dict.get(resource.id, None) + setattr(resource, 'attachment_id', attachment_id) + + resource_set = self.project.resource_set.filter(archived=False) expected = render_to_string( 'resources/project_list.html', { 'object_list': resources, 'object': self.project, - 'project_has_resources': ( - self.project.resource_set.exists()), + 'has_unattached_resources': ( + resource_set.exists() and + resource_set.count() != self.project.resources.count() + ), + 'resource_list': resource_list, }, request=self.request ) @@ -99,7 +128,33 @@ def _get(self, user=None, status=None, resources=None): return response def test_get_list(self): - self._get(status=200) + resources = Resource.objects.filter(project=self.project, + archived=False).exclude( + pk=self.denied.pk) + self._get(status=200, resources=resources) + + def test_get_list_with_unattached_resource_using_nonunarchiver(self): + ResourceFactory.create(project=self.project) + resources = Resource.objects.filter(project=self.project).exclude( + pk=self.denied.pk) + self._get(status=200, resources=resources) + + def test_get_list_with_archived_resource(self): + ResourceFactory.create(project=self.project, archived=True) + resources = Resource.objects.filter(project=self.project, + archived=False).exclude( + pk=self.denied.pk) + self._get(status=200, resources=resources) + + def test_get_list_with_archived_resource_using_unarchiver(self): + unarchiver = UserFactory.create() + policy = Policy.objects.create( + name='allow', + body=json.dumps(clauses)) + assign_user_policies(unarchiver, policy) + ResourceFactory.create(project=self.project, archived=True) + resources = Resource.objects.filter(project=self.project) + self._get(status=200, user=unarchiver, resources=resources) def test_get_with_unauthorized_user(self): self._get(status=200, user=UserFactory.create(), resources=[]) @@ -120,9 +175,9 @@ class ProjectResourcesAddTest(UserTestCase): def setUp(self): super().setUp() self.project = ProjectFactory.create() - self.assigned = ResourceFactory.create(project=self.project, + self.attached = ResourceFactory.create(project=self.project, content_object=self.project) - self.unassigned = ResourceFactory.create(project=self.project) + self.unattached = ResourceFactory.create(project=self.project) self.view = default.ProjectResourcesAdd.as_view() self.request = HttpRequest() @@ -165,8 +220,8 @@ def _get(self, user=None, status=None): def _post(self, user=None, status=None, expected_redirect=None): data = { - self.assigned.id: False, - self.unassigned.id: True, + self.attached.id: False, + self.unattached.id: True, } if user is None: @@ -215,21 +270,31 @@ def test_update(self): } ) self._post(status=302, expected_redirect=redirect_url) - assert self.project.resources.count() == 1 - assert self.project.resources.first() == self.unassigned + project_resources = self.project.resources.all() + assert len(project_resources) == 2 + assert self.attached in project_resources + assert self.unattached in project_resources + + def test_update_with_custom_redirect(self): + setattr(self.request, 'GET', {'next': '/organizations/'}) + self._post(status=302, expected_redirect='/organizations/#resources') + project_resources = self.project.resources.all() + assert len(project_resources) == 2 + assert self.attached in project_resources + assert self.unattached in project_resources def test_post_with_unauthorized_user(self): self._post(status=302, user=UserFactory.create()) assert ("You don't have permission to add resources." in [str(m) for m in get_messages(self.request)]) assert self.project.resources.count() == 1 - assert self.project.resources.first() == self.assigned + assert self.project.resources.first() == self.attached def test_post_with_unauthenticated_user(self): self._post(status=302, user=AnonymousUser(), expected_redirect='/account/login/') assert self.project.resources.count() == 1 - assert self.project.resources.first() == self.assigned + assert self.project.resources.first() == self.attached @pytest.mark.usefixtures('clear_temp') @@ -339,6 +404,12 @@ def test_create(self): assert self.project.resources.count() == 1 assert self.project.resources.first().name == self.data['name'] + def test_create_with_custom_redirect(self): + setattr(self.request, 'GET', {'next': '/organizations/'}) + self._post(status=302, expected_redirect='/organizations/#resources') + assert self.project.resources.count() == 1 + assert self.project.resources.first().name == self.data['name'] + def test_post_with_unauthorized_user(self): self._post(status=302, user=UserFactory.create()) assert ("You don't have permission to add resources." @@ -357,8 +428,27 @@ class ProjectResourcesDetailTest(UserTestCase): def setUp(self): super().setUp() self.project = ProjectFactory.create() - self.resource = ResourceFactory.create(content_object=self.project, - project=self.project) + self.org_slug = self.project.organization.slug + self.resource = ResourceFactory.create(project=self.project) + self.location = SpatialUnitFactory.create(project=self.project) + self.party = PartyFactory.create(project=self.project) + self.tenurerel = TenureRelationshipFactory.create(project=self.project) + self.project_attachment = ContentObject.objects.create( + resource_id=self.resource.id, + content_object=self.project, + ) + self.location_attachment = ContentObject.objects.create( + resource_id=self.resource.id, + content_object=self.location, + ) + self.party_attachment = ContentObject.objects.create( + resource_id=self.resource.id, + content_object=self.party, + ) + self.tenurerel_attachment = ContentObject.objects.create( + resource_id=self.resource.id, + content_object=self.tenurerel, + ) self.view = default.ProjectResourcesDetail.as_view() self.request = HttpRequest() @@ -380,7 +470,7 @@ def _get(self, user=None, status=None): setattr(self.request, '_messages', self.messages) response = self.view(self.request, - organization=self.project.organization.slug, + organization=self.org_slug, project=self.project.slug, resource=self.resource.id) @@ -391,9 +481,30 @@ def _get(self, user=None, status=None): content = response.render().content.decode('utf-8') expected = render_to_string( 'resources/project_detail.html', - {'object_list': self.project.resources, - 'object': self.project, - 'resource': self.resource}, + { + 'object': self.project, + 'resource': self.resource, + 'can_edit': True, + 'can_archive': True, + 'attachment_list': [ + { + 'object': self.project, + 'id': self.project_attachment.id, + }, + { + 'object': self.location, + 'id': self.location_attachment.id, + }, + { + 'object': self.party, + 'id': self.party_attachment.id, + }, + { + 'object': self.tenurerel, + 'id': self.tenurerel_attachment.id, + }, + ], + }, request=self.request ) assert expected == content @@ -471,10 +582,11 @@ def _get(self, user=None, status=None): cancel_url += '#resources' else: cancel_url = reverse( - 'resources:project_list', + 'resources:project_detail', kwargs={ 'organization': self.project.organization.slug, - 'project': self.project.slug + 'project': self.project.slug, + 'resource': self.resource.id, } ) expected = render_to_string( @@ -527,17 +639,6 @@ def _post(self, user=None, status=None, expected_redirect=None, get=None): def test_get_form(self): self._get(status=200) - def test_get_form_with_next_query_parameter(self): - self.request.GET['next'] = '/organizations/' - self._get(status=200) - - def test_get_form_with_location_next_query_parameter(self): - url = ('https://example.com/organizations/sample-org/' - 'projects/sample-proj/records/' - 'locations/jvzsiszjzrbpecm69549u2z5/') - self.request.GET['next'] = url - self._get(status=200) - def test_get_non_existent_project(self): setattr(self.request, 'user', self.user) with pytest.raises(Http404): @@ -565,10 +666,11 @@ def test_get_with_unauthenticated_user(self): def test_update(self): redirect_url = reverse( - 'resources:project_list', + 'resources:project_detail', kwargs={ 'organization': self.project.organization.slug, - 'project': self.project.slug + 'project': self.project.slug, + 'resource': self.resource.id, } ) self._post(status=302, expected_redirect=redirect_url) @@ -613,10 +715,11 @@ def setUp(self): setattr(self.request, '_messages', self.messages) self.redirect_url = reverse( - 'resources:project_list', + 'resources:project_detail', kwargs={ 'organization': self.project.organization.slug, - 'project': self.project.slug + 'project': self.project.slug, + 'resource': self.resource.id, } ) self.policy = Policy.objects.create( @@ -647,6 +750,38 @@ def test_archive(self): self.resource.refresh_from_db() assert self.resource.archived is True + def test_archive_with_custom_redirect(self): + setattr(self.request, 'GET', {'next': '/dashboard/'}) + self._get(status=302, redirect_url='/dashboard/#resources') + + self.resource.refresh_from_db() + assert self.resource.archived is True + + def test_archive_with_no_unarchive_permission(self): + additional_clauses = copy.deepcopy(clauses) + additional_clauses['clause'] += [ + { + 'effect': 'deny', + 'object': ['resource/*/*/*'], + 'action': ['resource.unarchive'], + }, + ] + policy = Policy.objects.create( + name='allow', + body=json.dumps(additional_clauses)) + assign_user_policies(self.user, policy) + redirect_url = reverse( + 'resources:project_list', + kwargs={ + 'organization': self.project.organization.slug, + 'project': self.project.slug, + } + ) + self._get(status=302, redirect_url=redirect_url) + + self.resource.refresh_from_db() + assert self.resource.archived is True + def test_archive_with_project_does_not_exist(self): setattr(self.request, 'user', self.user) with pytest.raises(Http404): @@ -668,7 +803,7 @@ def test_archive_resource_does_not_exist(self): def test_archive_with_unauthorized_user(self): self._get(status=302, user=UserFactory.create()) - assert ("You don't have permission to archive this resource." + assert ("You don't have permission to delete this resource." in [str(m) for m in get_messages(self.request)]) self.resource.refresh_from_db() @@ -700,10 +835,11 @@ def setUp(self): setattr(self.request, '_messages', self.messages) self.redirect_url = reverse( - 'resources:project_list', + 'resources:project_detail', kwargs={ 'organization': self.project.organization.slug, - 'project': self.project.slug + 'project': self.project.slug, + 'resource': self.resource.id, } ) self.policy = Policy.objects.create( @@ -750,7 +886,7 @@ def test_unarchive_with_project_does_not_exist(self): self.resource.refresh_from_db() assert self.resource.archived is True - def test_archive_project_does_not_exist(self): + def test_unarchive_resource_does_not_exist(self): setattr(self.request, 'user', self.user) with pytest.raises(Http404): self.view(self.request, @@ -759,9 +895,12 @@ def test_archive_project_does_not_exist(self): resource='abc123') def test_unarchive_with_unauthorized_user(self): - self._get(status=302, user=UserFactory.create()) - assert ("You don't have permission to unarchive this resource." - in [str(m) for m in get_messages(self.request)]) + setattr(self.request, 'user', UserFactory.create()) + with pytest.raises(Http404): + self.view(self.request, + organization=self.project.organization.slug, + project=self.project.slug, + resource=self.resource.id) self.resource.refresh_from_db() assert self.resource.archived is True @@ -772,3 +911,172 @@ def test_unarchive_with_unauthenticated_user(self): self.resource.refresh_from_db() assert self.resource.archived is True + + +@pytest.mark.usefixtures('make_dirs') +class ResourceDetachTest(UserTestCase): + def setUp(self): + super().setUp() + + self.project = ProjectFactory.create() + self.location = SpatialUnitFactory.create(project=self.project) + self.resource = ResourceFactory.create(project=self.project) + self.project_attachment = ContentObject.objects.create( + resource_id=self.resource.id, + content_object=self.project, + ) + self.location_attachment = ContentObject.objects.create( + resource_id=self.resource.id, + content_object=self.location, + ) + + self.user = UserFactory.create() + self.policy = Policy.objects.create( + name='allow', + body=json.dumps(clauses)) + assign_user_policies(self.user, self.policy) + + self.view = default.ResourceDetach.as_view() + self.request = HttpRequest() + setattr(self.request, 'method', 'POST') + setattr(self.request, 'session', 'session') + self.messages = FallbackStorage(self.request) + setattr(self.request, '_messages', self.messages) + self.redirect_url = reverse( + 'resources:project_list', + kwargs={ + 'organization': self.project.organization.slug, + 'project': self.project.slug + } + ) + + def _post(self, attachment_id, user=None): + storage = FakeS3Storage() + file = open(path + '/resources/tests/files/image.jpg', 'rb').read() + file_name = storage.save('resources/image.jpg', file) + self.data = { + 'name': 'Some name', + 'description': '', + 'file': file_name, + 'original_file': 'image.png', + 'mime_type': 'image/jpeg' + } + + if user is None: + user = self.user + setattr(self.request, 'user', user) + + response = self.view( + self.request, + organization=self.project.organization.slug, + project=self.project.slug, + resource=self.resource.id, + attachment=attachment_id, + ) + return response + + def refresh_objects_from_db(self): + self.resource.refresh_from_db() + self.project.refresh_from_db() + self.project.reload_resources() + self.location.refresh_from_db() + self.location.reload_resources() + + def test_detach_from_project(self): + response = self._post(self.project_attachment.id) + assert response.status_code == 302 + assert self.redirect_url in response['location'] + self.refresh_objects_from_db() + assert self.resource.num_entities == 1 + assert self.project.resources.count() == 0 + location_resources = self.location.resources + assert location_resources.count() == 1 + assert location_resources.first() == self.resource + + def test_detach_from_location(self): + response = self._post(self.location_attachment.id) + assert response.status_code == 302 + assert self.redirect_url in response['location'] + self.refresh_objects_from_db() + assert self.resource.num_entities == 1 + assert self.location.resources.count() == 0 + project_resources = self.project.resources + assert project_resources.count() == 1 + assert project_resources.first() == self.resource + + def test_detach_with_custom_redirect(self): + setattr(self.request, 'GET', {'next': '/dashboard/'}) + response = self._post(self.project_attachment.id) + assert response.status_code == 302 + assert '/dashboard/#resources' in response['location'] + self.refresh_objects_from_db() + assert self.resource.num_entities == 1 + assert self.project.resources.count() == 0 + location_resources = self.location.resources + assert location_resources.count() == 1 + assert location_resources.first() == self.resource + + def test_detach_with_nonexistent_project(self): + setattr(self.request, 'user', self.user) + with pytest.raises(Http404): + self.view( + self.request, + organization='some-org', + project='some-project', + resource=self.resource.id, + attachment=self.project_attachment.id, + ) + self.refresh_objects_from_db() + assert self.resource.num_entities == 2 + assert self.project.resources.count() == 1 + assert self.location.resources.count() == 1 + + def test_detach_with_nonexistent_resource(self): + setattr(self.request, 'user', self.user) + with pytest.raises(Http404): + self.view( + self.request, + organization=self.project.organization.slug, + project=self.project.slug, + resource='abc123', + attachment=self.project_attachment.id, + ) + self.refresh_objects_from_db() + assert self.resource.num_entities == 2 + assert self.project.resources.count() == 1 + assert self.location.resources.count() == 1 + + def test_detach_with_nonexistent_attachment(self): + setattr(self.request, 'user', self.user) + with pytest.raises(Http404): + self.view( + self.request, + organization=self.project.organization.slug, + project=self.project.slug, + resource=self.resource.id, + attachment='abc123', + ) + self.refresh_objects_from_db() + assert self.resource.num_entities == 2 + assert self.project.resources.count() == 1 + assert self.location.resources.count() == 1 + + def test_detach_with_unauthorized_user(self): + response = self._post(self.project_attachment.id, + user=UserFactory.create()) + assert ("You don't have permission to edit this resource." + in [str(m) for m in get_messages(self.request)]) + assert response.status_code == 302 + self.refresh_objects_from_db() + assert self.resource.num_entities == 2 + assert self.project.resources.count() == 1 + assert self.location.resources.count() == 1 + + def test_detach_with_unauthenticated_user(self): + response = self._post(self.project_attachment.id, user=AnonymousUser()) + assert response.status_code == 302 + assert '/account/login/' in response['location'] + self.refresh_objects_from_db() + assert self.resource.num_entities == 2 + assert self.project.resources.count() == 1 + assert self.location.resources.count() == 1 diff --git a/cadasta/resources/tests/test_widgets.py b/cadasta/resources/tests/test_widgets.py index c1d87468a..c0275f2dd 100644 --- a/cadasta/resources/tests/test_widgets.py +++ b/cadasta/resources/tests/test_widgets.py @@ -1,42 +1,75 @@ from datetime import datetime from django.test import TestCase from django.template.defaultfilters import date +from django.contrib.contenttypes.models import ContentType from core.tests.util import make_dirs # noqa from accounts.tests.factories import UserFactory +from organization.tests.factories import ProjectFactory from .factories import ResourceFactory +from ..models import ContentObject from ..widgets import ResourceWidget class ResourceWidgetTest(TestCase): + def setUp(self): + super().setUp() + + # Create a floating resource + self.user = UserFactory.build( + full_name='John Lennon', + username='john' + ) + self.last_updated = datetime.now() + self.resource = ResourceFactory.build( + name='Resource Name', + file='https://example.com/file.txt', + original_file='original_file.jpg', + mime_type='image/png', + contributor=self.user, + last_updated=self.last_updated, + ) + + # Attach it to a project + project = ProjectFactory.create() + ContentObject.objects.create( + resource=self.resource, + content_type=ContentType.objects.get_for_model(project), + object_id=project.id, + ) + def test_render(self): expected_html = ( ' ' - ' ' - ' ' - '
file.txt' + ' ' + '
original_file.jpg' ' ' ' txt' - ' 0' ' ' ' John Lennon
' ' john' ' {updated}' + ' Attached to 1 other entity' ) - user = UserFactory.build( - full_name='John Lennon', - username='john' - ) - last_updated = datetime.now() - resource = ResourceFactory.build( - name='Resource', - file='https://example.com/file.txt', - mime_type='image/png', - contributor=user, - last_updated=last_updated - ) - widget = ResourceWidget(resource=resource) + widget = ResourceWidget(resource=self.resource) rendered = widget.render('file', True) assert expected_html.format( - updated=date(last_updated, 'N j, Y, P')) in rendered + updated=date(self.last_updated, 'N j, Y, P'), + ) in rendered + + def test_attachment_text_for_0_entities(self): + widget = ResourceWidget(resource=self.resource) + assert widget.get_attachment_text(0) == "Unattached" + + def test_attachment_text_for_1_entity(self): + widget = ResourceWidget(resource=self.resource) + assert widget.get_attachment_text(1) == ( + "Attached to 1 other entity") + + def test_attachment_text_for_more_entities(self): + widget = ResourceWidget(resource=self.resource) + for i in range(2, 11): + assert widget.get_attachment_text(i) == ( + "Attached to {} other entities".format(i)) diff --git a/cadasta/resources/urls/default.py b/cadasta/resources/urls/default.py index 0f65fc06a..c1211f374 100644 --- a/cadasta/resources/urls/default.py +++ b/cadasta/resources/urls/default.py @@ -32,6 +32,10 @@ r'^resources/(?P[-\w]+)/unarchive/$', default.ResourceUnarchive.as_view(), name='unarchive'), + url( + r'^resources/(?P[-\w]+)/detach/(?P[-\w]+)/$', + default.ResourceDetach.as_view(), + name='detach'), ] urlpatterns = [ diff --git a/cadasta/resources/views/api.py b/cadasta/resources/views/api.py index 0059fd34c..778e0e9ef 100644 --- a/cadasta/resources/views/api.py +++ b/cadasta/resources/views/api.py @@ -1,10 +1,10 @@ from rest_framework import generics, filters from tutelary.mixins import APIPermissionRequiredMixin -from .mixins import ProjectResourceMixin +from . import mixins class ProjectResources(APIPermissionRequiredMixin, - ProjectResourceMixin, + mixins.ProjectResourceMixin, generics.ListCreateAPIView): filter_backends = (filters.DjangoFilterBackend, filters.SearchFilter, @@ -16,11 +16,19 @@ class ProjectResources(APIPermissionRequiredMixin, 'GET': 'resource.list', 'POST': 'resource.add' } - permission_filter_queryset = ('resource.view',) + + def filter_archived_resources(self, view, obj): + if obj.archived: + return ('resource.view', 'resource.unarchive') + else: + return ('resource.view',) + + permission_filter_queryset = filter_archived_resources + use_resource_library_queryset = True class ProjectResourcesDetail(APIPermissionRequiredMixin, - ProjectResourceMixin, + mixins.ResourceObjectMixin, generics.RetrieveUpdateAPIView): def patch_actions(self, request): if hasattr(request, 'data'): @@ -37,3 +45,4 @@ def patch_actions(self, request): 'GET': 'resource.view', 'PATCH': patch_actions } + use_resource_library_queryset = True diff --git a/cadasta/resources/views/default.py b/cadasta/resources/views/default.py index a60a9f1fb..cbc888767 100644 --- a/cadasta/resources/views/default.py +++ b/cadasta/resources/views/default.py @@ -1,9 +1,12 @@ +from django.core.urlresolvers import reverse +from django.http import Http404 from core.views import generic import django.views.generic as base_generic from core.views.mixins import ArchiveMixin from core.mixins import LoginPermissionRequiredMixin +from ..models import Resource, ContentObject from . import mixins from organization.views import mixins as organization_mixins from ..forms import AddResourceFromLibraryForm @@ -12,13 +15,25 @@ class ProjectResources(LoginPermissionRequiredMixin, mixins.ProjectResourceMixin, - mixins.ProjectHasResourcesMixin, + mixins.HasUnattachedResourcesMixin, + mixins.DetachableResourcesListMixin, organization_mixins.ProjectAdminCheckMixin, generic.ListView): template_name = 'resources/project_list.html' permission_required = 'resource.list' permission_denied_message = error_messages.RESOURCE_LIST - permission_filter_queryset = ('resource.view',) + + def filter_archived_resources(self, view, obj): + if obj.archived: + return ('resource.view', 'resource.unarchive') + else: + return ('resource.view',) + + permission_filter_queryset = filter_archived_resources + use_resource_library_queryset = True + + def get_resource_list(self): + return self.get_queryset() class ProjectResourcesAdd(LoginPermissionRequiredMixin, @@ -44,7 +59,7 @@ def post(self, request, *args, **kwargs): class ProjectResourcesNew(LoginPermissionRequiredMixin, mixins.ProjectResourceMixin, - mixins.ProjectHasResourcesMixin, + mixins.HasUnattachedResourcesMixin, organization_mixins.ProjectAdminCheckMixin, generic.CreateView): template_name = 'resources/project_add_new.html' @@ -65,7 +80,18 @@ class ProjectResourcesDetail(LoginPermissionRequiredMixin, def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) - context['object_list'] = self.get_queryset() + + # Construct list of objects resource is attached to + attachments = ContentObject.objects.filter(resource=self.get_object()) + attachment_list = [ + { + 'object': attachment.content_type.get_object_for_this_type( + pk=attachment.object_id), + 'id': attachment.id, + } for attachment in attachments + ] + + context['attachment_list'] = attachment_list return context @@ -91,6 +117,31 @@ class ResourceArchive(LoginPermissionRequiredMixin, permission_required = 'resource.archive' permission_denied_message = error_messages.RESOURCE_ARCHIVE + def get_success_url(self): + next_url = self.request.GET.get('next', None) + if next_url: + return next_url + '#resources' + + project = self.get_project() + resource = self.get_object() + if self.request.user.has_perm('resource.unarchive', resource): + return reverse( + 'resources:project_detail', + kwargs={ + 'organization': project.organization.slug, + 'project': project.slug, + 'resource': resource.id, + } + ) + else: + return reverse( + 'resources:project_list', + kwargs={ + 'organization': project.organization.slug, + 'project': project.slug, + } + ) + class ResourceUnarchive(LoginPermissionRequiredMixin, ArchiveMixin, @@ -99,3 +150,43 @@ class ResourceUnarchive(LoginPermissionRequiredMixin, do_archive = False permission_required = 'resource.unarchive' permission_denied_message = error_messages.RESOURCE_UNARCHIVE + + +class ResourceDetach(LoginPermissionRequiredMixin, + organization_mixins.ProjectMixin, + generic.DeleteView): + http_method_names = ('post',) + model = ContentObject + pk_url_kwarg = 'attachment' + permission_required = 'resource.edit' + permission_denied_message = error_messages.RESOURCE_EDIT + + def get_object(self): + try: + return ContentObject.objects.get( + id=self.kwargs['attachment'], + resource__id=self.kwargs['resource'], + resource__project__slug=self.kwargs['project'], + ) + except ContentObject.DoesNotExist as e: + raise Http404(e) + + def get_perms_objects(self): + try: + return [Resource.objects.get(pk=self.kwargs['resource'])] + except Resource.DoesNotExist as e: + raise Http404(e) + + def get_success_url(self): + next_url = self.request.GET.get('next', None) + if next_url: + return next_url + '#resources' + + project = self.get_project() + return reverse( + 'resources:project_list', + kwargs={ + 'organization': project.organization.slug, + 'project': project.slug, + } + ) diff --git a/cadasta/resources/views/mixins.py b/cadasta/resources/views/mixins.py index d0bf596cb..b15911025 100644 --- a/cadasta/resources/views/mixins.py +++ b/cadasta/resources/views/mixins.py @@ -1,9 +1,10 @@ from django.core.urlresolvers import reverse +from django.contrib.contenttypes.models import ContentType from django.http import Http404 from organization.views.mixins import ProjectMixin -from ..models import Resource +from ..models import Resource, ContentObject from ..serializers import ResourceSerializer from ..forms import ResourceForm @@ -13,7 +14,10 @@ class ResourceViewMixin: serializer_class = ResourceSerializer def get_queryset(self): - return self.get_content_object().resources.all() + if hasattr(self, 'use_resource_library_queryset'): + return self.get_project().resource_set.all() + else: + return self.get_content_object().resources.all() def get_model_context(self): return { @@ -50,44 +54,121 @@ def get_context_data(self, *args, **kwargs): return context def get_success_url(self): - project = self.get_project() - next_url = self.request.GET.get('next', None) if next_url: return next_url + '#resources' + project = self.get_project() return reverse( 'resources:project_list', kwargs={ 'organization': project.organization.slug, - 'project': project.slug + 'project': project.slug, } ) class ResourceObjectMixin(ProjectResourceMixin): def get_object(self): + if hasattr(self, 'resource'): + return self.resource try: - return Resource.objects.get( + resource = Resource.objects.get( + project__organization__slug=self.kwargs['organization'], project__slug=self.kwargs['project'], id=self.kwargs['resource'] ) except Resource.DoesNotExist as e: raise Http404(e) + if ( + resource.archived and + not self.request.user.has_perm('resource.unarchive', resource) + ): + raise Http404(Resource.DoesNotExist) + else: + self.resource = resource + return resource def get_perms_objects(self): return [self.get_object()] def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) - context['resource'] = self.get_object() + resource = self.get_object() + user = self.request.user + context['resource'] = resource + context['can_edit'] = user.has_perm('resource.edit', resource) + context['can_archive'] = user.has_perm('resource.archive', resource) return context + def get_success_url(self): + next_url = self.request.GET.get('next', None) + if next_url: + return next_url + '#resources' -class ProjectHasResourcesMixin(ProjectMixin): + project = self.get_project() + return reverse( + 'resources:project_detail', + kwargs={ + 'organization': project.organization.slug, + 'project': project.slug, + 'resource': self.get_object().id, + } + ) + + +class HasUnattachedResourcesMixin(ProjectMixin): + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + + # Determine the object that can have resources + if hasattr(self, 'get_content_object'): + # This is for views for uploading a new resource + # or the ProjectResources view + object = self.get_content_object() + elif hasattr(self, 'object'): + # This is for views that list entity resources + object = self.object + + project_resource_set_count = self.get_project().resource_set.filter( + archived=False).count() + if ( + project_resource_set_count > 0 and + project_resource_set_count != object.resources.count() + ): + context['has_unattached_resources'] = True + + return context + + +class DetachableResourcesListMixin(ProjectMixin): def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) - context['project_has_resources'] = ( - self.get_project().resource_set.exists() + + # Get current object whose resources is being listed + if hasattr(self, 'get_object'): + content_object = self.get_object() + else: + content_object = self.get_project() + model_type = ContentType.objects.get_for_model(content_object) + + # Get the list of resources to be displayed + if hasattr(self, 'get_resource_list'): + resource_list = self.get_resource_list() + else: + resource_list = content_object.resources.all() + + # Get attachment IDs as a dictionary keyed on resource IDs + attachments = ContentObject.objects.filter( + content_type__pk=model_type.id, + object_id=content_object.id, ) + attachment_id_dict = {x.resource.id: x.id for x in attachments} + + # Update resource list with attachment IDs referring to the object + for resource in resource_list: + attachment_id = attachment_id_dict.get(resource.id, None) + setattr(resource, 'attachment_id', attachment_id) + + context['resource_list'] = resource_list return context diff --git a/cadasta/resources/widgets.py b/cadasta/resources/widgets.py index 09363d6dc..e9536764b 100644 --- a/cadasta/resources/widgets.py +++ b/cadasta/resources/widgets.py @@ -1,5 +1,5 @@ from django.utils import formats -from django.utils.translation import get_language +from django.utils.translation import get_language, ugettext as _ from django.forms import CheckboxInput from django.template.defaultfilters import date @@ -9,17 +9,17 @@ class ResourceWidget(CheckboxInput): '' ' {checkbox}' ' ' - ' ' ' ' - '
{resource.file_name}' + '
{resource.original_file}' ' ' ' {resource.file_type}' - ' {resource.num_entities}' ' ' ' {resource.contributor.full_name}
' ' {resource.contributor.username}' ' {date_updated}' + ' {attachment_text}' '' ) @@ -27,13 +27,26 @@ def __init__(self, resource, *args, **kwargs): super().__init__(*args, **kwargs) self.resource = resource + def get_attachment_text(self, num_entities): + # TODO: There should be a way to make the pluralization logic + # localizable (maybe plural is > 2 in some locales) + if num_entities == 0: + return _("Unattached") + elif num_entities == 1: + return _("Attached to 1 other entity") + else: + plural_text = _("Attached to {number} other entities") + return plural_text.format(number=num_entities) + def render(self, name, value, attrs=None): date_format = formats.get_format("DATETIME_FORMAT", lang=get_language()) checkbox = super().render(name, value, attrs=attrs) + num_entities = self.resource.num_entities return self.html.format( name=name, resource=self.resource, checkbox=checkbox, - date_updated=date(self.resource.last_updated, date_format) + date_updated=date(self.resource.last_updated, date_format), + attachment_text=self.get_attachment_text(num_entities), ) diff --git a/cadasta/spatial/models.py b/cadasta/spatial/models.py index bcd96cdc4..8f45ef871 100644 --- a/cadasta/spatial/models.py +++ b/cadasta/spatial/models.py @@ -1,4 +1,5 @@ from core.models import RandomIDModel +from django.core.urlresolvers import reverse from django.contrib.gis.db.models import GeometryField from django.db import models from django.utils.translation import ugettext as _ @@ -84,11 +85,30 @@ class TutelaryMeta: ) def __str__(self): - return "".format(self.get_type_display()) + return "".format(self.name) def __repr__(self): return str(self) + @property + def name(self): + return self.get_type_display() + + @property + def ui_class_name(self): + return _("Location") + + @property + def ui_detail_url(self): + return reverse( + 'locations:detail', + kwargs={ + 'organization': self.project.organization.slug, + 'project': self.project.slug, + 'location': self.id, + }, + ) + def reassign_spatial_geometry(instance): coords = list(instance.geometry.coords) diff --git a/cadasta/spatial/tests/test_models.py b/cadasta/spatial/tests/test_models.py index 1a31b04e7..865ad0861 100644 --- a/cadasta/spatial/tests/test_models.py +++ b/cadasta/spatial/tests/test_models.py @@ -78,6 +78,23 @@ def test_adding_attributes(self): }) assert space.attributes['description'] == 'The happiest place on earth' + def test_name(self): + su = SpatialUnitFactory.create(type="RW") + assert su.name == "Right-of-way" + + def test_ui_class_name(self): + su = SpatialUnitFactory.create() + assert su.ui_class_name == "Location" + + def test_ui_detail_url(self): + su = SpatialUnitFactory.create() + assert su.ui_detail_url == ( + '/organizations/{org}/projects/{prj}/' + 'records/locations/{id}/'.format( + org=su.project.organization.slug, + prj=su.project.slug, + id=su.id)) + class SpatialRelationshipTest(UserTestCase): diff --git a/cadasta/spatial/tests/test_views_default.py b/cadasta/spatial/tests/test_views_default.py index b7bf4e755..60ff7bcce 100644 --- a/cadasta/spatial/tests/test_views_default.py +++ b/cadasta/spatial/tests/test_views_default.py @@ -473,9 +473,9 @@ class LocationResourceAddTest(TestCase): def set_up_models(self): self.project = ProjectFactory.create() self.location = SpatialUnitFactory.create(project=self.project) - self.assigned = ResourceFactory.create(project=self.project, + self.attached = ResourceFactory.create(project=self.project, content_object=self.location) - self.unassigned = ResourceFactory.create(project=self.project) + self.unattached = ResourceFactory.create(project=self.project) def assign_policies(self): assign_policies(self.authorized_user) @@ -500,8 +500,8 @@ def get_success_url_kwargs(self): def get_post_data(self): return { - self.assigned.id: False, - self.unassigned.id: True, + self.attached.id: False, + self.unattached.id: True, } def test_get_with_authorized_user(self): @@ -536,8 +536,10 @@ def test_post_with_authorized_user(self): assert response.status_code == 302 assert response['location'] == self.expected_success_url + '#resources' - assert self.location.resources.count() == 1 - assert self.location.resources.first() == self.unassigned + location_resources = self.location.resources.all() + assert len(location_resources) == 2 + assert self.attached in location_resources + assert self.unattached in location_resources def test_post_with_unauthorized_user(self): response = self.request(method='POST', user=self.unauthorized_user) @@ -546,14 +548,14 @@ def test_post_with_unauthorized_user(self): "add resources to this location." in [str(m) for m in get_messages(self.request)]) assert self.location.resources.count() == 1 - assert self.location.resources.first() == self.assigned + assert self.location.resources.first() == self.attached def test_post_with_unauthenticated_user(self): response = self.request(method='POST') assert response.status_code == 302 assert '/account/login/' in response['location'] assert self.location.resources.count() == 1 - assert self.location.resources.first() == self.assigned + assert self.location.resources.first() == self.attached @pytest.mark.usefixtures('make_dirs') diff --git a/cadasta/spatial/views/default.py b/cadasta/spatial/views/default.py index 00f07a3e8..8ccc98f1f 100644 --- a/cadasta/spatial/views/default.py +++ b/cadasta/spatial/views/default.py @@ -7,7 +7,7 @@ from core.mixins import LoginPermissionRequiredMixin from resources.forms import AddResourceFromLibraryForm -from resources.views.mixins import ProjectHasResourcesMixin +from resources.views import mixins as resource_mixins from party.messages import TENURE_REL_CREATE from . import mixins from organization.views import mixins as organization_mixins @@ -62,7 +62,8 @@ class LocationDetail(LoginPermissionRequiredMixin, JsonAttrsMixin, mixins.SpatialUnitObjectMixin, organization_mixins.ProjectAdminCheckMixin, - ProjectHasResourcesMixin, + resource_mixins.HasUnattachedResourcesMixin, + resource_mixins.DetachableResourcesListMixin, generic.DetailView): template_name = 'spatial/location_detail.html' permission_required = 'spatial.view' @@ -120,7 +121,7 @@ def post(self, request, *args, **kwargs): class LocationResourceNew(LoginPermissionRequiredMixin, mixins.SpatialUnitResourceMixin, organization_mixins.ProjectAdminCheckMixin, - ProjectHasResourcesMixin, + resource_mixins.HasUnattachedResourcesMixin, generic.CreateView): template_name = 'spatial/resources_new.html' permission_required = 'spatial.resources.add' diff --git a/cadasta/templates/party/party_detail.html b/cadasta/templates/party/party_detail.html index 57504e44d..13eecf226 100644 --- a/cadasta/templates/party/party_detail.html +++ b/cadasta/templates/party/party_detail.html @@ -34,7 +34,7 @@

{% trans "Details" %}

- + @@ -56,20 +56,24 @@

{% trans "Details" %}

{% trans "Resources" %}

{% if party.resources %}
+ {% if has_unattached_resources %} - {% trans "Add" %} + {% else %} + + {% endif %} + {% trans "Attach" %}
- {% include 'resources/table.html' with object_list=party.resources %} + {% include 'resources/table.html' %} {% else %}
-

{% trans "This party does not have any connected resources. To add a resource, select the button below." %}

+

{% trans "This party does not have any attached resources. To attach a resource, select the button below." %}

- {% if project_has_resources %} + {% if has_unattached_resources %} {% else %} {% endif %} - {% trans "Add" %} + {% trans "Attach" %}
{% endif %} diff --git a/cadasta/templates/party/relationship_detail.html b/cadasta/templates/party/relationship_detail.html index 6e72251b1..92902863b 100644 --- a/cadasta/templates/party/relationship_detail.html +++ b/cadasta/templates/party/relationship_detail.html @@ -46,20 +46,24 @@

{% trans "Details" %}

{% trans "Resources" %}

{% if relationship.resources %}
+ {% if has_unattached_resources %} - {% trans "Add" %} + {% else %} + + {% endif %} + {% trans "Attach" %}
- {% include 'resources/table_sm.html' with object_list=relationship.resources %} + {% include 'resources/table_sm.html' %} {% else %}
-

{% trans "This relationship does not have any connected resources. To add a resource, select the button below." %}

+

{% trans "This relationship does not have any attached resources. To attach a resource, select the button below." %}

- {% if project_has_resources %} + {% if has_unattached_resources %} {% else %} {% endif %} - {% trans "Add" %} + {% trans "Attach" %}
{% endif %} diff --git a/cadasta/templates/resources/modal_add_lib.html b/cadasta/templates/resources/modal_add_lib.html index f8ddcd9fa..644974749 100644 --- a/cadasta/templates/resources/modal_add_lib.html +++ b/cadasta/templates/resources/modal_add_lib.html @@ -9,9 +9,9 @@ - + diff --git a/cadasta/templates/resources/modal_archive.html b/cadasta/templates/resources/modal_archive.html new file mode 100644 index 000000000..946976d57 --- /dev/null +++ b/cadasta/templates/resources/modal_archive.html @@ -0,0 +1,31 @@ +{% load i18n %} + + diff --git a/cadasta/templates/resources/modal_unarchive.html b/cadasta/templates/resources/modal_unarchive.html new file mode 100644 index 000000000..91b424877 --- /dev/null +++ b/cadasta/templates/resources/modal_unarchive.html @@ -0,0 +1,27 @@ +{% load i18n %} + + diff --git a/cadasta/templates/resources/modal_upload.html b/cadasta/templates/resources/modal_upload.html index a37b885d4..aa83eab9e 100644 --- a/cadasta/templates/resources/modal_upload.html +++ b/cadasta/templates/resources/modal_upload.html @@ -10,9 +10,9 @@ - {% if project_has_resources %} + {% if has_unattached_resources %} {% endif %} diff --git a/cadasta/templates/resources/project_detail.html b/cadasta/templates/resources/project_detail.html index 88d60edf7..2c1f7990e 100644 --- a/cadasta/templates/resources/project_detail.html +++ b/cadasta/templates/resources/project_detail.html @@ -1,7 +1,7 @@ {% extends "organization/project_wrapper.html" %} {% load i18n %} -{% block page_title %}Resource detail | {% endblock %} +{% block page_title %}Resource detail: {{ resource.name }} | {% endblock %} {% block left-nav %}resources{% endblock %} {% block content %} @@ -16,84 +16,84 @@

{% trans "Resource detail" %}

-

{{ resource.name }}

-

{{ resource.description }}
{{ resource.file_name }}

+

+ {{ resource.name }} + {% if resource.archived %} + {% trans "Deleted" %} + {% endif %} +

+

{{ resource.description }}
{{ resource.original_file }}

{% blocktrans with date=resource.last_updated user=resource.contributor.full_name %}Added on {{ date }} by {{ user }}{% endblocktrans %}

-
- +
+ + + {% if not resource.archived %} + +
+
+

{% trans "Attached to" %}

+ {% if attachment_list %} +
{{ party.name }}
+ + + + + + + + + {% for attachment in attachment_list %} + + + + + + {% endfor %} + +
{% trans "Entity Class" %}{% trans "Entity Name or Description" %}{% trans "Detach" %}
{{ attachment.object.ui_class_name }}{{ attachment.object.name }} +
+ {% csrf_token %} + +
+
+ {% else %} + {% trans "This resource is currently not attached to anything." %} + {% endif %} + + + {% endif %} {% endblock %} -{% block modals %} - - - - - +{% block form_modal %} +{% url 'resources:archive' object.organization.slug object.slug resource.id as archive_url %} +{% url 'resources:unarchive' object.organization.slug object.slug resource.id as unarchive_url %} +{% include 'resources/modal_archive.html' %} +{% include 'resources/modal_unarchive.html' %} {% endblock %} diff --git a/cadasta/templates/resources/project_list.html b/cadasta/templates/resources/project_list.html index 6a8cf0a12..04c987d21 100644 --- a/cadasta/templates/resources/project_list.html +++ b/cadasta/templates/resources/project_list.html @@ -16,12 +16,12 @@

{% trans "Resources" %}

- {% if project_has_resources %} + {% if has_unattached_resources %} {% else %} {% endif %} - {% trans "Add" %} + {% trans "Attach" %}
{% include 'resources/table.html' %}
@@ -31,3 +31,7 @@

{% trans "Resources" %}

{% endblock %} + +{% block form_modal %} +{% include 'resources/modal_unarchive.html' %} +{% endblock %} diff --git a/cadasta/templates/resources/table.html b/cadasta/templates/resources/table.html index bad83db1b..4f9ec9145 100644 --- a/cadasta/templates/resources/table.html +++ b/cadasta/templates/resources/table.html @@ -5,29 +5,78 @@ {% trans "Resource" %} - {% trans "Type" %} - {% trans "Connections" %} + {% trans "Type" %} {% trans "Contributor" %} {% trans "Last updated" %} + {% trans "Attached to" %} +   - {% for resource in object_list %} - + {% for resource in resource_list %} +
-

{{ resource.name }}

- {{ resource.file_name }} +

{{ resource.name }} + {% if resource.archived %} + {% trans "Deleted" %} + {% endif %} +

+

{{ resource.original_file }}

{{ resource.file_type }} - {{ resource.num_entities }} {{ resource.contributor.full_name }}
{{ resource.contributor.username }} {{ resource.last_updated }} + + {% if resource.attachment_id %} + {% with amount=resource.num_entities|add:"-1" %} + {% blocktrans %}{{ amount }} other{% endblocktrans %} + {% endwith %} + {% else %} + {% if resource.num_entities %} + {{ resource.num_entities }} + {% else %} + {% trans "Unattached" %} + {% endif %} + {% endif %} + + + {% if resource.attachment_id %} +
+ {% csrf_token %} + +
+ {% else %} + {% if resource.archived %} + + {% endif %} + {% endif %} + {% endfor %} + + diff --git a/cadasta/templates/resources/table_add_lib.html b/cadasta/templates/resources/table_add_lib.html index 34530bb52..580af35fc 100644 --- a/cadasta/templates/resources/table_add_lib.html +++ b/cadasta/templates/resources/table_add_lib.html @@ -1,5 +1,4 @@ {% load i18n %} -{% load widget_tweaks %} {% csrf_token %} @@ -8,14 +7,14 @@ - + {% for field in form %} - {% render_field field %} + {{ field }} {% endfor %}
{% trans "Resource" %} {% trans "Connections" %} {% trans "Attachments" %}
diff --git a/cadasta/templates/resources/table_sm.html b/cadasta/templates/resources/table_sm.html index adcfee199..4433746b2 100644 --- a/cadasta/templates/resources/table_sm.html +++ b/cadasta/templates/resources/table_sm.html @@ -1,25 +1,33 @@ {% load i18n %} - +
- + - {% for resource in object_list %} + {% for resource in resource_list %} - + {% endfor %} diff --git a/cadasta/templates/spatial/location_detail.html b/cadasta/templates/spatial/location_detail.html index caf742f0a..27be0374d 100644 --- a/cadasta/templates/spatial/location_detail.html +++ b/cadasta/templates/spatial/location_detail.html @@ -9,7 +9,7 @@
-

{% trans "Location" %} {{ location.get_type_display }}

+

{% trans "Location" %} {{ location.name }}

{% trans "Resource" %}{% trans "Connections" %} 
- {{ resource.name }}
{{ resource.file_name }} +

{{ resource.name }}

+

{{ resource.original_file }}

{{ resource.num_entities }} +
+ {% csrf_token %} + +
+