Skip to content

Commit

Permalink
Merge pull request #15 from City-of-Turku/release-candidate
Browse files Browse the repository at this point in the history
Release candidate
  • Loading branch information
SanttuA authored Aug 14, 2020
2 parents c4f6fcd + 64427ea commit 11642a0
Show file tree
Hide file tree
Showing 26 changed files with 688 additions and 141 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,6 @@ var/
migrator/*.json
migrator/*.xml
config_dev.toml
protected_media/
media/
.vscode
5 changes: 5 additions & 0 deletions config_dev.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ SOCIAL_AUTH_TUNNISTAMO_KEY=https://auth.example.com/kerrokantasi
SOCIAL_AUTH_TUNNISTAMO_SECRET=your-tunnistamo-secret
SOCIAL_AUTH_TUNNISTAMO_OIDC_ENDPOINT=https://tunnistamo.example.com/openid

# Comma separated list of authentication providers which provide strong
# authentication e.g. Suomi.fi. User's auth provider name comes from oidc
# api token's amr (authentication methods references) field.
STRONG_AUTH_PROVIDERS=

# URL prefix for this Kerrokantasi installation. This can be used when
# Kerrokantasi is made available at a sub-path of domain
# (eg. api.yourorg.org/kerrokantasi). sub-paths are mostly handled
Expand Down
2 changes: 1 addition & 1 deletion democracy/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ class ContactPersonAdmin(TranslatableAdmin, admin.ModelAdmin):
class CommentAdmin(admin.ModelAdmin):
list_display = ('id', 'section', 'author_name', 'content')
search_fields = ('section__id', 'author_name', 'title', 'content')
fields = ('title', 'content', 'reply_to', 'author_name', 'organization', 'geojson',
fields = ('title', 'content', 'reply_to', 'author_name', 'organization', 'geojson', 'map_comment_text',
'plugin_identifier', 'plugin_data', 'pinned', 'label', 'language_code', 'voters', 'section')
readonly_fields = ('reply_to', 'author_name', 'organization', 'geojson',
'plugin_identifier', 'plugin_data', 'label', 'language_code', 'voters', 'section')
Expand Down
12 changes: 12 additions & 0 deletions democracy/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,27 @@ class Commenting(Enum):
NONE = 0
REGISTERED = 1
OPEN = 2
STRONG = 3

class Labels:
NONE = _("No commenting")
REGISTERED = _("Registered users only")
OPEN = _("Open commenting")
STRONG = _("Strong authentication only")


class InitialSectionType:
MAIN = "main"
PART = "part"
SCENARIO = "scenario"
CLOSURE_INFO = "closure-info"

class CommentingMapTools(Enum):
NONE = 0
MARKER = 1
ALL = 2

class Labels:
NONE =_("none")
MARKER = _("marker")
ALL = _("all")
19 changes: 19 additions & 0 deletions democracy/migrations/0051_auto_20200225_1349.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 2.2.10 on 2020-02-25 13:49

import django.core.files.storage
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('democracy', '0050_add_on_deletes'),
]

operations = [
migrations.AlterField(
model_name='sectionfile',
name='file',
field=models.FileField(max_length=2048, storage=django.core.files.storage.FileSystemStorage(location='/home/santtua/kerrokantasi/protected_media'), upload_to='files/%Y/%m', verbose_name='file'),
),
]
36 changes: 36 additions & 0 deletions democracy/migrations/0052_auto_20200401_1115.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Generated by Django 2.2.10 on 2020-04-01 11:15

import democracy.enums
import django.core.files.storage
from django.db import migrations, models
import enumfields.fields


class Migration(migrations.Migration):

dependencies = [
('democracy', '0051_auto_20200225_1349'),
]

operations = [
migrations.AddField(
model_name='section',
name='commenting_map_tools',
field=enumfields.fields.EnumIntegerField(default=0, enum=democracy.enums.CommentingMapTools, verbose_name='commenting_map_tools'),
),
migrations.AddField(
model_name='sectioncomment',
name='commenting_map_tools',
field=enumfields.fields.EnumIntegerField(default=0, enum=democracy.enums.CommentingMapTools, verbose_name='commenting_map_tools'),
),
migrations.AddField(
model_name='sectioncomment',
name='map_comment_text',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='map_comment_text'),
),
migrations.AlterField(
model_name='sectionfile',
name='file',
field=models.FileField(max_length=2048, storage=django.core.files.storage.FileSystemStorage(location='/home/hienous/kerrokantasi/protected_media'), upload_to='files/%Y/%m', verbose_name='file'),
),
]
71 changes: 71 additions & 0 deletions democracy/migrations/0053_auto_20200813_1112.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Generated by Django 2.2.12 on 2020-08-13 11:12

import django.core.files.storage
from django.db import migrations, models
import django.db.models.deletion
import parler.fields


class Migration(migrations.Migration):

dependencies = [
('democracy', '0052_auto_20200401_1115'),
]

operations = [
migrations.AlterField(
model_name='contactpersontranslation',
name='master',
field=parler.fields.TranslationsForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='translations', to='democracy.ContactPerson'),
),
migrations.AlterField(
model_name='hearingtranslation',
name='master',
field=parler.fields.TranslationsForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='translations', to='democracy.Hearing'),
),
migrations.AlterField(
model_name='labeltranslation',
name='master',
field=parler.fields.TranslationsForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='translations', to='democracy.Label'),
),
migrations.AlterField(
model_name='projectphasetranslation',
name='master',
field=parler.fields.TranslationsForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='translations', to='democracy.ProjectPhase'),
),
migrations.AlterField(
model_name='projecttranslation',
name='master',
field=parler.fields.TranslationsForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='translations', to='democracy.Project'),
),
migrations.AlterField(
model_name='sectionfile',
name='file',
field=models.FileField(max_length=2048, storage=django.core.files.storage.FileSystemStorage(location='/srv/kerrokantasi/user_uploads'), upload_to='files/%Y/%m', verbose_name='file'),
),
migrations.AlterField(
model_name='sectionfiletranslation',
name='master',
field=parler.fields.TranslationsForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='translations', to='democracy.SectionFile'),
),
migrations.AlterField(
model_name='sectionimagetranslation',
name='master',
field=parler.fields.TranslationsForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='translations', to='democracy.SectionImage'),
),
migrations.AlterField(
model_name='sectionpolloptiontranslation',
name='master',
field=parler.fields.TranslationsForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='translations', to='democracy.SectionPollOption'),
),
migrations.AlterField(
model_name='sectionpolltranslation',
name='master',
field=parler.fields.TranslationsForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='translations', to='democracy.SectionPoll'),
),
migrations.AlterField(
model_name='sectiontranslation',
name='master',
field=parler.fields.TranslationsForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='translations', to='democracy.Section'),
),
]
13 changes: 12 additions & 1 deletion democracy/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from django.utils.crypto import get_random_string
from django.utils.translation import ugettext_lazy as _
from enumfields.fields import EnumIntegerField
from democracy.enums import Commenting
from democracy.enums import Commenting, CommentingMapTools

ORDERING_HELP = _("The ordering position for this object. Objects with smaller numbers appear first.")

Expand Down Expand Up @@ -126,6 +126,7 @@ class Commentable(models.Model):
db_index=True
)
commenting = EnumIntegerField(Commenting, verbose_name=_('commenting'), default=Commenting.NONE)
commenting_map_tools = EnumIntegerField(CommentingMapTools, verbose_name=_('commenting_map_tools'), default=CommentingMapTools.NONE)
voting = EnumIntegerField(Commenting, verbose_name=_('voting'), default=Commenting.REGISTERED)

def recache_n_comments(self):
Expand All @@ -150,6 +151,11 @@ def check_commenting(self, request):
elif self.commenting == Commenting.REGISTERED:
if not is_authenticated:
raise ValidationError(_("%s does not allow anonymous commenting") % self, code="commenting_registered")
elif self.commenting == Commenting.STRONG:
if not is_authenticated:
raise ValidationError(_("%s requires strong authentication for commenting") % self, code="commenting_registered_strong")
elif not request.user.has_strong_auth and not request.user.get_default_organization():
raise ValidationError(_("%s requires strong authentication for commenting") % self, code="commenting_registered_strong")
elif self.commenting == Commenting.OPEN:
return
else: # pragma: no cover
Expand All @@ -168,6 +174,11 @@ def check_voting(self, request):
elif self.voting == Commenting.REGISTERED:
if not is_authenticated:
raise ValidationError(_("%s does not allow anonymous voting") % self, code="voting_registered")
elif self.voting == Commenting.STRONG:
if not is_authenticated:
raise ValidationError(_("%s requires strong authentication for voting") % self, code="voting_registered_strong")
elif not request.user.has_strong_auth and not request.user.get_default_organization():
raise ValidationError(_("%s requires strong authentication for voting") % self, code="voting_registered_strong")
elif self.voting == Commenting.OPEN:
return
else: # pragma: no cover
Expand Down
1 change: 1 addition & 0 deletions democracy/models/comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class BaseComment(BaseModel):
parent_field = None # Required for factories and API
parent_model = None # Required for factories and API
geojson = GeoJSONField(blank=True, null=True, verbose_name=_('location'))
map_comment_text = models.CharField(verbose_name=_('map_comment_text'), max_length=255, blank=True, null=True)
geometry = models.GeometryField(blank=True, null=True, verbose_name=_('location geometry'))
authorization_code = models.CharField(verbose_name=_('authorization code'), max_length=32, blank=True)
author_name = models.CharField(verbose_name=_('author name'), max_length=255, blank=True, null=True)
Expand Down
2 changes: 1 addition & 1 deletion democracy/models/hearing.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def preview_code(self):
def preview_url(self):
if not (self.preview_code and hasattr(settings, 'DEMOCRACY_UI_BASE_URL')):
return None
url = urljoin(settings.DEMOCRACY_UI_BASE_URL, '/hearing/%s/?preview=%s' % (self.pk, self.preview_code))
url = urljoin(settings.DEMOCRACY_UI_BASE_URL, '/%s/?preview=%s' % (self.pk, self.preview_code))
return url

def save(self, *args, **kwargs):
Expand Down
57 changes: 55 additions & 2 deletions democracy/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from django.utils.timezone import now
from rest_framework.test import APIClient

from democracy.enums import Commenting, InitialSectionType
from democracy.enums import Commenting, InitialSectionType, CommentingMapTools
from democracy.factories.hearing import HearingFactory, LabelFactory
from democracy.models import ContactPerson, Hearing, Label, Project, ProjectPhase, Section, SectionFile, SectionType, Organization
from democracy.tests.utils import FILES, assert_ascending_sequence, create_default_images, create_default_files, get_file_path
Expand Down Expand Up @@ -70,6 +70,7 @@ def default_hearing(john_doe, contact_person, default_organization, default_proj
Fixture for a "default" hearing with three sections (one main, two other sections).
All objects will have the 3 default images attached.
All objects will allow open commenting.
All objects will have commenting_map_tools all
"""
hearing = Hearing.objects.create(
title='Default test hearing One',
Expand All @@ -85,7 +86,8 @@ def default_hearing(john_doe, contact_person, default_organization, default_proj
abstract='Section %d abstract' % x,
hearing=hearing,
type=SectionType.objects.get(identifier=section_type),
commenting=Commenting.OPEN
commenting=Commenting.OPEN,
commenting_map_tools=CommentingMapTools.ALL
)
create_default_images(section)
create_default_files(section)
Expand All @@ -100,6 +102,35 @@ def default_hearing(john_doe, contact_person, default_organization, default_proj
return hearing


@pytest.fixture()
def strong_auth_hearing(john_doe, contact_person, default_organization, default_project):
"""
Fixture for a "strong auth requiring" hearing with one main section.
Commenting requires strong auth.
"""
hearing = Hearing.objects.create(
title='Strong auth test hearing',
open_at=now() - datetime.timedelta(days=1),
close_at=now() + datetime.timedelta(days=1),
slug='strong-auth-hearing-slug',
organization=default_organization,
project_phase=default_project.phases.all()[0],
)

section_type = InitialSectionType.MAIN
Section.objects.create(
abstract='Section abstract for strong auth',
hearing=hearing,
type=SectionType.objects.get(identifier=section_type),
commenting=Commenting.STRONG
)

assert_ascending_sequence([s.ordering for s in hearing.sections.all()])
hearing.contact_persons.add(contact_person)

return hearing


@pytest.fixture()
def default_project():
project_data = {
Expand Down Expand Up @@ -179,6 +210,28 @@ def jane_doe_api_client(jane_doe):
api_client.user = jane_doe
return api_client

@pytest.fixture()
def stark_doe():
"""
Stark Doe is another average registered user.
"""
user = get_user_model().objects.filter(username="stark_doe").first()
if not user: # pragma: no branch
user = get_user_model().objects.create_user("stark_doe", "[email protected]", password="password")
return user


@pytest.fixture()
def stark_doe_api_client(stark_doe):
"""
Stark Doe is another average registered user; this is his API client.
Stark uses strong authentication.
"""
api_client = APIClient()
api_client.force_authenticate(user=stark_doe)
api_client.user = stark_doe
api_client.user.has_strong_auth = True
return api_client

@pytest.fixture()
def john_smith(default_organization):
Expand Down
40 changes: 40 additions & 0 deletions democracy/tests/test_comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def get_comment_data(**extra):
return dict({
'content': default_comment_content,
'geojson': default_geojson_feature,
'map_comment_text': '',
'section': None
}, **extra)

Expand Down Expand Up @@ -102,6 +103,45 @@ def test_56_add_comment_to_section_without_authentication_with_reply_to(api_clie
assert response.status_code == 201
assert response.data['reply_to'] == 'Previous commenter'

@pytest.mark.django_db
def test_56_add_comment_to_section_requiring_strong_auth_without_authentication(api_client,
strong_auth_hearing, get_comments_url_and_data):

section = strong_auth_hearing.sections.first()
url, data = get_comments_url_and_data(strong_auth_hearing, section)
response = api_client.post(url, data=data)
# expect request to not go through
assert response.status_code == 403

@pytest.mark.django_db
def test_56_add_comment_to_section_requiring_strong_auth_with_weak_auth(john_doe_api_client,
strong_auth_hearing, get_comments_url_and_data):

section = strong_auth_hearing.sections.first()
url, data = get_comments_url_and_data(strong_auth_hearing, section)
response = john_doe_api_client.post(url, data=data)
# expect request to not go through
assert response.status_code == 403

@pytest.mark.django_db
def test_56_add_comment_to_section_requiring_strong_auth_with_strong_auth(stark_doe_api_client,
strong_auth_hearing, get_comments_url_and_data):

section = strong_auth_hearing.sections.first()
url, data = get_comments_url_and_data(strong_auth_hearing, section)
response = stark_doe_api_client.post(url, data=data)
# expect request to go through
assert response.status_code == 201

@pytest.mark.django_db
def test_56_add_comment_to_section_requiring_strong_auth_with_organization(john_smith_api_client,
strong_auth_hearing, get_comments_url_and_data):

section = strong_auth_hearing.sections.first()
url, data = get_comments_url_and_data(strong_auth_hearing, section)
response = john_smith_api_client.post(url, data=data)
# expect request to go through
assert response.status_code == 201

@pytest.mark.django_db
def test_56_add_comment_to_section_without_data(api_client, default_hearing, get_comments_url_and_data):
Expand Down
2 changes: 1 addition & 1 deletion democracy/tests/test_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def test_get_section_with_files(api_client, default_hearing):
])
@pytest.mark.django_db
def test_unpublished_section_files_excluded(client, expected, request, default_hearing):
api_client = request.getfuncargvalue(client)
api_client = request.getfixturevalue(client)

file_obj = default_hearing.sections.all()[2].files.get(translations__title=FILES['TXT'])
file_obj.published = False
Expand Down
Loading

0 comments on commit 11642a0

Please sign in to comment.